Porównaj commity

...

94 Commity
v1.5 ... main

Autor SHA1 Wiadomość Data
Kyle Maas c5047d8df8
Fix null bug in More Options button (#913) 2023-11-14 09:24:05 +02:00
Markos Gogoulos dcbfaca91c
Developer Experience (#911)
local dev environment
2023-11-13 11:13:08 +02:00
Kyle Maas 918df010f5
Fix bug that crashes page if an encoding has a null URL (#912) 2023-11-13 11:09:16 +02:00
Markos Gogoulos e9739bab45
Feat celery run (#860)
* avoid calling post save signals
* remove stale celery ids that prevent new services from starting
2023-11-10 16:06:17 +02:00
Kyle Maas e7ce9ef5c0
Add admin action to generate missing encodings for a particular Media (#883)
* Add admin action to generate missing encodings for a particular Media
* Only regenerate the encodings that are missing
2023-11-10 15:41:20 +02:00
Kyle Maas 4829adf110
Add useful fields to the Encodings admin screen (#885) 2023-11-10 15:37:40 +02:00
lavirez fdff0811a1
cli.py missing f string (#877) 2023-11-10 15:09:22 +02:00
Kyle Maas 92c0ff579a
Add sitemap (#572)
add sitemap.xml
2023-11-10 15:03:36 +02:00
Markos Gogoulos 847cff2b5c license section 2023-11-10 14:30:14 +02:00
Markos Gogoulos e8d3ff25be
Disable encoding and show only original file (#829)
Disable encoding and show only original file #829
2023-11-10 14:25:10 +02:00
Markos Gogoulos 15d217453b
Update admins_docs.md (#842) 2023-07-17 16:47:06 +03:00
Markos Gogoulos 029665145e
Python requirements and Docker version upgrades (#826)
v3.0.0: Python, Django, Celery and other version upgrades
2023-07-03 13:40:39 +03:00
Adi 487e098b96
Upgrade postgres docker compose (#749)
* Upgrade PG to latest stable version alpine
2023-07-03 13:39:15 +03:00
Markos Gogoulos fe7427a1f2
check resolution for HLS (#832) 2023-07-03 12:19:23 +03:00
Markos Gogoulos 4bf41fe80e
fix issue with AVI not being recognised as videos (#833) 2023-07-03 12:18:24 +03:00
Markos Gogoulos 1fd04ca947 pass secrets to workflow 2023-06-28 15:32:32 +03:00
Markos Gogoulos a1962d4b32 add secret 2023-06-27 18:22:37 +03:00
Markos Gogoulos 6e9c9ed81f add secret 2023-06-27 18:20:30 +03:00
Markos Gogoulos 51186e3253 add secret 2023-06-27 18:16:01 +03:00
Markos Gogoulos 150967b342 add secret 2023-06-27 18:14:56 +03:00
Markos Gogoulos bb6244d862 trigger build 2023-06-27 18:07:14 +03:00
Markos Gogoulos a002422b77 update version for workflow 2023-06-27 17:50:16 +03:00
Markos Gogoulos 24167b9624 CI fix branch 2023-06-27 17:30:51 +03:00
Markos Gogoulos b9db1a5e2e
Update README.md (#823)
* Update README.md
2023-06-27 17:26:54 +03:00
Markos Gogoulos 296aeac567
Update admins_docs.md 2023-06-27 13:41:58 +03:00
Markos Gogoulos 10c386f886
Update README.md (#822) 2023-06-27 13:02:21 +03:00
Adi 367faaddd1
Add workflow for docker build and push (#750)
* Add workflow for docker build and push
2023-06-26 09:49:37 +03:00
nmlsdev 3d59b87f09
add rhel8 installation script (#792)
* add rhel8 installation script
2023-06-14 15:18:12 +03:00
Markos Gogoulos 5dee41de39 version 2.0.0 2023-06-13 22:45:03 +03:00
Markos Gogoulos 08bba5fc05 remove redundant file 2023-06-13 21:21:27 +03:00
Markos Gogoulos 102414b514
fix issues with comments (#802)
* fix issues with comments
2023-06-13 19:01:52 +03:00
Markos Gogoulos c866fdd6ba
allow tags to contain chars, not only English alphabet (#801)
* allow tags to contain chars, not only English alphabet
2023-06-13 15:41:13 +03:00
Markos Gogoulos 5b601698a4 increase uwsgi buffer-size para, 2023-06-13 12:44:31 +03:00
Markos Gogoulos f040f73f51 black formatting 2023-06-12 17:22:39 +03:00
Markos Gogoulos b7a70d92fa fix typo 2023-06-12 17:13:44 +03:00
mostafa hosseini 2f43cef8da
add api_url field to search api (#692)
Co-authored-by: Mostafa Hosseini <mostafa.h@rahgosahgroup.com>
2023-06-12 16:42:30 +03:00
Markos Gogoulos ad633e6fdf
simple cookie consent code (#799)
* simple cookie consent code
2023-06-12 16:40:53 +03:00
Stella cd8d0ea49a
Allow password reset & email verify pages on global login required (#790) 2023-05-31 16:36:31 +03:00
Markos Gogoulos a3997bfb1c
update versions for pre-commit (#741) 2023-03-14 14:09:52 +02:00
Markos Gogoulos 4b0718c43f version 2.0 2023-03-14 13:30:26 +02:00
Markos Gogoulos 91d8179fa0 improve formating on Readme 2023-03-14 13:15:17 +02:00
Markos Gogoulos 6532b19849
Update README.md 2023-03-14 13:13:55 +02:00
Markos Gogoulos 6ea8fd12a3
fix issue with uninitialized video player 2023-03-14 13:13:15 +02:00
Markos Gogoulos d971bb955f
pin versions (#718) 2023-02-17 12:46:45 +02:00
Markos Gogoulos b52b008f89
enable cors for media dir (#701) 2023-02-17 11:51:48 +02:00
MICRUFUN 30cf5d7176
How to modify encode profiles (#682) 2023-01-03 12:33:38 +02:00
masavini 6fd9a7d37f
remove zombie thumbnails (#657)
remove zombie thumbnails
2022-12-05 12:31:31 +02:00
Markos Gogoulos 9c6d13559b update doc 2022-11-29 17:24:13 +02:00
Markos Gogoulos 8ec97a8219 pre-commit yaml 2022-11-29 15:27:15 +02:00
Markos Gogoulos de8f9ca718 fix pre-commit version 2022-11-29 14:55:00 +02:00
Markos Gogoulos a4bedca4db fix pre-commit version 2022-11-29 14:53:05 +02:00
Kyle Maas da565b3bfc
Fix double slashes in URIs (#558) 2022-11-29 11:22:30 +02:00
Kyle Maas 239ff6cb60
Add meta description tag for search engines (#551) 2022-11-29 11:02:16 +02:00
masavini da840b156d
add api path to LOGIN_REQUIRED_IGNORE_PATHS (#483) 2022-11-29 10:59:36 +02:00
Markos Gogoulos b08d493823 static files 2022-09-20 12:27:33 +00:00
masavini 25eaa35758
Fix admin user creation (#472)
add ADMIN_USER, ADMIN_PASSWORD and ADMIN_EMAIL
2022-09-20 15:18:25 +03:00
MrPercheul cba2ed75ed
Show comments in the Timebar (#442)
Show comments in the Timebar
2022-09-20 15:16:16 +03:00
Markos Gogoulos de5bd07a90
Feat videojs update (#519)
update video.js to 7.20.2
2022-09-07 12:33:11 +03:00
Markos Gogoulos 34c1a4e33e
static files (#506) 2022-08-22 17:40:54 +03:00
Markos Gogoulos 80be0d06e5
correctly identify audio (#505)
* correctly identify audio
2022-08-22 17:35:11 +03:00
masavini a6322fae6d
missing f-strings (#490) 2022-08-12 15:45:35 +03:00
Markos Gogoulos 3d4dd27220
Update user_docs.md 2022-06-01 14:23:42 +03:00
MrPercheul f6a78dd0b4
Mention users in comments (#429)
Mention users in comments (#429)
2022-06-01 14:23:21 +03:00
DecaTec cbc9633fe2
Fix pre-commit (#440) 2022-06-01 13:51:46 +03:00
DecaTec 3e7b106482
Vp9 fixes (#438)
* Fix fo ffprobe appending pipe
* Fix media remains pending when only encoding to webm
2022-06-01 13:42:35 +03:00
DecaTec 0f182c2b15
Fixed duplicate 'pk' on fixtures
Fixed duplicate 'pk' on fixtures
2022-06-01 13:21:22 +03:00
Markos Gogoulos 1ddfae7c95
update black (#452) 2022-05-10 13:46:22 +03:00
Markos Gogoulos f1969e4637 pin version of debug toolbar 2022-05-09 21:05:25 +03:00
Markos Gogoulos 9e7a7a7482
Update developers_docs.md 2022-04-27 21:53:11 +03:00
Markos Gogoulos 6e478e6e82 fix typo on documentation 2022-03-23 20:48:26 +02:00
Behnam B.Marandi e06deed3b8
Add configuration for only member access to media (#424)
I added an entry under 'Configuration 'per discussion in the Q&A section:
https://github.com/mediacms-io/mediacms/discussions/422
2022-03-23 20:47:06 +02:00
Markos Gogoulos f8376c5c58
static files (#432) 2022-03-23 19:04:59 +02:00
MrPercheul e7ae2833d9
Timestamp (#430)
* Test

Messing around to figure out docker and whatnot

* Revert "Test"

This reverts commit 43b76932c5.

* Update docker dev yaml

Update the docker to the environement with the latest version of selenium (and their new arguments), and to be on firefox

* Timestamp on play

Adds the a one time event that jumps to a timestamp if there is a t parameter in the URL

* Timestamp when sharing DRAFT

First draft to show the timestamp when sharing. Missing checkbox and appending the get param to shown link

* Checkbox and update share url

Checkbox show a clean timestamp and the media URL updates with the correct timestamp upon selection

* Cleaning before PR

removing un-necessary modified  files

* Clean up before PR - remove statics

* Timestamp in comments

Parse the comments to wrap timestamps with an appropriate anchor

* Forgotten comments and console.logs

* Last touch for PR

- Cleaning media.js for PR
- Using  MediaPageStore instead of window.location when wrapping the timestamp in comments

* Screenshot

Adding the screenshot for the user_docs

* PR amends

Amending VideoPlayer componnent to take check if the Get param 't' is a number, and to keep it within the duration of the video.
Required to change the listener from 'play' to 'loadedmetadata' to have access to the video duration (otherwise it was too early)

Also changed the User_doc file to inform users of the timestamp function
2022-03-23 17:38:02 +02:00
Markos Gogoulos fb0f3ee739
dislike button fix (#411) 2022-02-03 18:44:56 +02:00
Markos Gogoulos c0701de047 reverting image logo 2022-01-31 21:44:43 +02:00
Shubhank Saxena 0d4918a715
CLI API Tool (#273)
CLI tool
2022-01-31 21:12:29 +02:00
Megidd 8093c4ccb5
Fix logo height (#348)
* Set logo height to 100%
2022-01-31 21:07:02 +02:00
Megidd 2dbd97cb22
Fix dislike count (#388) 2022-01-31 21:05:37 +02:00
Markos Gogoulos 6b6662420f
adds instructions to get coverage (#299)
Co-authored-by: Markos Gogoulos <markos@orfium.com>
2022-01-31 20:53:57 +02:00
sthierolf f1a1e342db
Update install.sh (#303)
Tested with newly release Debian 11 Bullseye, added to OS Version check.
2021-10-14 10:24:07 +03:00
Markos Gogoulos 738247c32b
pin PG version on docker compose files (#300) 2021-10-12 21:07:45 +03:00
Markos Gogoulos f974d78270
improve validation on text indexing, for search (#294) 2021-10-01 17:49:41 +03:00
multiflexi 28031f07e5
Filters (#289)
* Framerate fix

Keep original framerate up to 60fps, halve any framerate above 60fps. Because of "video_frame_rate": Fraction(video_info["r_frame_rate"]), it does not work, when float used, the video is encoded but framerate suffers from rounding error.

* Framerate fix

Keep original framerate up to 60fps, halve any framerate above 60fps. Because of "video_frame_rate": Fraction(video_info["r_frame_rate"]), it does not work, when float used, the video is encoded but framerate suffers from rounding error.

* Introduction of minimum bitrate modifier

A minimum bitrate modifier introduced as per https://developers.google.com/media/vp9/settings/vod

* Introduction of minimum bitrate modifier

A minimum bitrate modifier introduced as per https://developers.google.com/media/vp9/settings/vod

* Deinterlacing and better filter logic
2021-10-01 17:48:52 +03:00
Markos Gogoulos 4480fa7de1
documentation, admin section, plus fix of a typo on models (#293)
* documentation, admin section, plus fix of a typo on models
2021-09-27 15:07:17 +03:00
Markos Gogoulos 32e07035f3
add tests for new uploads (#291)
* add tests for new uploads
2021-09-23 18:34:27 +03:00
Markos Gogoulos 2ce8dba163
Feat fix precommit (#280)
* add version file, fix pre-commit
2021-08-20 14:16:19 +03:00
multiflexi 48838ac406
Framerate fix (#246)
* Framerate fix

Keep original framerate up to 60fps, halve any framerate above 60fps. Because of "video_frame_rate": Fraction(video_info["r_frame_rate"]), it does not work, when float used, the video is encoded but framerate suffers from rounding error.

* Framerate fix

Keep original framerate up to 60fps, halve any framerate above 60fps. Because of "video_frame_rate": Fraction(video_info["r_frame_rate"]), it does not work, when float used, the video is encoded but framerate suffers from rounding error.

* Introduction of minimum bitrate modifier

A minimum bitrate modifier introduced as per https://developers.google.com/media/vp9/settings/vod

* Introduction of minimum bitrate modifier

A minimum bitrate modifier introduced as per https://developers.google.com/media/vp9/settings/vod
2021-08-20 14:04:50 +03:00
Markos Gogoulos 062e4be6c2
small typo (#277) 2021-08-20 13:57:55 +03:00
Markos Gogoulos fb00f94bfa
add more tests (#268)
* add more tests as STUBS
2021-08-17 00:01:52 +03:00
Markos Gogoulos 755df50c41
pin Django 3.1.12 (#260)
* pin Django 3.1.12, this requires to remove absolute paths for upload dirs
2021-08-16 23:46:25 +03:00
Markos Gogoulos ba94989e6a
Feat urls (#257)
add new URLS, add swaggger doc, add tests
2021-08-05 13:25:25 +03:00
Markos Gogoulos 86cc0442d8
move envs from Dockerfile to docker-compose files (#264) 2021-08-02 22:03:32 +03:00
Markos Gogoulos 2dde4257f7
remove smoke test that breaks (#263) 2021-08-02 19:14:23 +03:00
swiftugandan 3afff52ebf
restore /tmp that was deleted while adding static ffmpeg (#262) 2021-08-02 18:40:12 +03:00
176 zmienionych plików z 146825 dodań i 53158 usunięć

4
.coveragerc 100644
Wyświetl plik

@ -0,0 +1,4 @@
[run]
omit =
*bento4*
*/migrations/*

20
.github/workflows/ci.yml vendored 100644
Wyświetl plik

@ -0,0 +1,20 @@
---
name: "CI"
on:
pull_request:
push:
branches:
- main
paths-ignore:
- '**/README.md'
jobs:
pre-commit:
uses: ./.github/workflows/pre-commit.yml
test:
uses: ./.github/workflows/python.yml
needs: [pre-commit]
release:
uses: ./.github/workflows/docker-build-push.yml
secrets: inherit # pass all secrets
needs: [test]
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'

Wyświetl plik

@ -0,0 +1,52 @@
name: Docker build and push
on:
workflow_call:
push:
tags:
- v*.*.*
jobs:
release:
name: Build & release to DockerHub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
# List of Docker images to use as base name for tags
images: |
mediacms/mediacms
# Generate Docker tags based on the following events/attributes
# Set latest tag for default branch
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
labels: |
org.opencontainers.image.title=MediaCMS
org.opencontainers.image.description=MediaCMS is a modern, fully featured open source video and media CMS, written in Python/Django and React, featuring a REST API.
org.opencontainers.image.vendor=MediaCMS
org.opencontainers.image.url=https://mediacms.io/
org.opencontainers.image.source=https://github.com/mediacms-io/mediacms
org.opencontainers.image.licenses=AGPL-3.0
- name: Login to Docker Hub
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

Wyświetl plik

@ -1,15 +0,0 @@
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 }}

Wyświetl plik

@ -0,0 +1,15 @@
name: pre-commit
on:
workflow_call:
jobs:
pre-commit:
name: Pre-Commit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

Wyświetl plik

@ -1,14 +1,11 @@
name: Python Tests
on:
pull_request:
push:
branches:
- main
workflow_call:
jobs:
build:
name: Build & test via docker-compose
runs-on: ubuntu-latest
steps:
@ -29,7 +26,10 @@ jobs:
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec -T web pytest
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
# Run with coverage, saves report on htmlcov dir
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc
- name: Tear down the Stack
run: docker-compose -f docker-compose-dev.yaml down
run: docker-compose -f docker-compose-dev.yaml down

4
.gitignore vendored
Wyświetl plik

@ -1,3 +1,5 @@
cli-tool/.env
frontend/package-lock.json
media_files/encoded/
media_files/original/
media_files/hls/
@ -14,4 +16,4 @@ static/mptt/
static/rest_framework/
static/drf-yasg
cms/local_settings.py
deploy/docker/local_settings.py
deploy/docker/local_settings.py

Wyświetl plik

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

Wyświetl plik

@ -1,15 +1,16 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.5.4
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 23.1.0
hooks:
- id: black
language_version: python3
language_version: python3
additional_dependencies: [ 'click==8.0.4' ]

Wyświetl plik

@ -1,8 +1 @@
Wordgames.gr - https://www.wordgames.gr
Yiannis Stergiou - ys.stergiou@gmail.com
Markos Gogoulos - mgogoulos@gmail.com
Contributors
Swift Ugandan - swiftugandan@gmail.com
Please see https://github.com/mediacms-io/mediacms/graphs/contributors for complete list of contributors to this repository!

Wyświetl plik

@ -1,4 +1,4 @@
FROM python:3.8-buster AS compile-image
FROM python:3.11.4-bookworm AS compile-image
SHELL ["/bin/bash", "-c"]
@ -11,6 +11,7 @@ RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python
# Install dependencies:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms
@ -24,13 +25,10 @@ RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-u
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.8-slim-buster as runtime-image
FROM python:3.11.4-bookworm as runtime-image
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV ADMIN_USER='admin'
ENV ADMIN_EMAIL='admin@localhost'
#ENV ADMIN_PASSWORD='uncomment_and_set_password_here'
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
ENV CELERY_APP='cms'
@ -56,10 +54,10 @@ RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-reco
apt-get clean
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
mkdir -p tmp && \
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C tmp && \
cp -v tmp/ffmpeg tmp/ffprobe tmp/qt-faststart /usr/local/bin && \
rm -rf tmp ffmpeg-release-amd64-static.tar.xz
mkdir -p ffmpeg-tmp && \
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
WORKDIR /home/mediacms.io/mediacms

Wyświetl plik

@ -1,4 +1,4 @@
FROM mediacms/mediacms:latest
FROM python:3.11.4-bookworm AS compile-image
SHELL ["/bin/bash", "-c"]
@ -7,10 +7,67 @@ ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
COPY requirements.txt .
# Install dependencies:
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms
RUN wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d ../bento4 && \
mv ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* ../bento4/ && \
rm -rf ../bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
rm -rf ../bento4/docs && \
rm Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
############ RUNTIME IMAGE ############
FROM python:3.11.4-bookworm as runtime-image
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# See: https://github.com/celery/celery/issues/6285#issuecomment-715316219
ENV CELERY_APP='cms'
# Use these to toggle which processes supervisord should run
ENV ENABLE_UWSGI='yes'
ENV ENABLE_NGINX='yes'
ENV ENABLE_CELERY_BEAT='yes'
ENV ENABLE_CELERY_SHORT='yes'
ENV ENABLE_CELERY_LONG='yes'
ENV ENABLE_MIGRATIONS='yes'
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --chown=www-data:www-data --from=compile-image /home/mediacms.io /home/mediacms.io
RUN apt-get update -y && apt-get -y upgrade && apt-get install --no-install-recommends \
supervisor nginx imagemagick procps wget xz-utils -y && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge --auto-remove && \
apt-get clean
RUN wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
mkdir -p ffmpeg-tmp && \
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C ffmpeg-tmp && \
cp -v ffmpeg-tmp/ffmpeg ffmpeg-tmp/ffprobe ffmpeg-tmp/qt-faststart /usr/local/bin && \
rm -rf ffmpeg-tmp ffmpeg-release-amd64-static.tar.xz
WORKDIR /home/mediacms.io/mediacms
EXPOSE 9000 80
RUN chmod +x ./deploy/docker/entrypoint.sh
ENTRYPOINT ["./deploy/docker/entrypoint.sh"]
CMD ["./deploy/docker/start.sh"]

23
HISTORY.md 100644
Wyświetl plik

@ -0,0 +1,23 @@
# History
## 3.0.0
### Features
- Updates Python/Django requirements and Dockerfile to use latest 3.11 Python - https://github.com/mediacms-io/mediacms/pull/826/files. This update requires some manual steps, for existing (not new) installations. Check the update section under the [Admin docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#2-server-installation), either for single server or for Docker Compose installations
- Upgrade postgres on Docker Compose - https://github.com/mediacms-io/mediacms/pull/749
### Fixes
- video player options for HLS - https://github.com/mediacms-io/mediacms/pull/832
- AVI videos not correctly recognised as videos - https://github.com/mediacms-io/mediacms/pull/833
## 2.1.0
### Fixes
- Increase uwsgi buffer-size parameter. This prevents an error by uwsgi with large headers - [#5b60](https://github.com/mediacms-io/mediacms/commit/5b601698a41ad97f08c1830e14b1c18f73ab8315)
- Fix issues with comments. These were not reported on the tracker but it is certain that they would not show comments on media files (non videos but also videos). Unfortunately this reverts work done with Timestamps on comments + Mentions on comments, more on PR [#802](https://github.com/mediacms-io/mediacms/pull/802)
### Features
- Allow tags to contains other characters too, not only English alphabet ones [#801](https://github.com/mediacms-io/mediacms/pull/801)
- Add simple cookie consent code [#799](https://github.com/mediacms-io/mediacms/pull/799)
- Allow password reset & email verify pages on global login required [#790](https://github.com/mediacms-io/mediacms/pull/790)
- Add api_url field to search api [#692](https://github.com/mediacms-io/mediacms/pull/692)

Wyświetl plik

@ -1,15 +1,12 @@
# MediaCMS
[![Code Quality: Cpp](https://img.shields.io/lgtm/grade/python/g/mediacms-io/mediacms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/mediacms-io/mediacms/context:python)
[![Code Quality: Cpp](https://img.shields.io/lgtm/grade/javascript/g/mediacms-io/mediacms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/mediacms-io/mediacms/context:javascript)
<br/>
[![GitHub license](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://raw.githubusercontent.com/mediacms-io/mediacms/main/LICENSE.txt)
[![Releases](https://img.shields.io/github/v/release/mediacms-io/mediacms?color=green)](https://github.com/mediacms-io/mediacms/releases/)
[![DockerHub](https://img.shields.io/docker/pulls/mediacms/mediacms)](https://hub.docker.com/repository/docker/mediacms/mediacms/)
[![DockerHub](https://img.shields.io/docker/pulls/mediacms/mediacms)](https://hub.docker.com/r/mediacms/mediacms)
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
It is built mostly using the modern stack Django + React and includes a REST API.
@ -59,15 +56,15 @@ A demo is available at https://demo.mediacms.io
## Philosophy
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
## License
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
Copyright Markos Gogoulos and Yiannis Stergiou
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
Copyright Markos Gogoulos.
## Support and paid services
@ -76,9 +73,9 @@ We provide custom installations, development of extra functionality, migration f
## Hardware dependencies
## Hardware considerations
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
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).
@ -92,21 +89,29 @@ Visit [Releases Page](https://github.com/mediacms-io/mediacms/releases) for deta
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services. Find the related pages:
* [Single Server](docs/Single_Server.md) page
* [Docker Compose](docs/Docker_Compose.md) page
* [Single Server](docs/admins_docs.md#2-server-installation) page
* [Docker Compose](docs/admins_docs.md#3-docker-installation) page
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
## Configuration
Visit [Configuration](docs/Configuration.md) page.
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
## Information for developers
Check out the new section on the [Developer Experience](docs/dev_exp.md) page
## Documentation
* [Users documentation](docs/user_docs.md) page
* [Administrators documentation](docs/admins_docs.md) page
* [Developers documentation](docs/developers_docs.md) page
## Technology
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, uWSGI, React, Fine Uploader, video.js, FFMPEG, Bento4
@ -114,7 +119,7 @@ This software uses the following list of awesome technologies: Python, Django, D
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.aagl.org/
## How to contribute
@ -124,10 +129,12 @@ If you like the project, here's a few things you can do
- 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
- Open issues, participate on [discussions](https://github.com/mediacms-io/mediacms/discussions), report bugs, suggest ideas
- [Show and tell](https://github.com/mediacms-io/mediacms/discussions/categories/show-and-tell) how you are using the project
- Star the project
- Add functionality, work on a PR, fix an issue!
- Add functionality, work on a PR, fix an issue!
## Contact
info@mediacms.io
info@mediacms.io

Wyświetl plik

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []

Wyświetl plik

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

Wyświetl plik

@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

10
cli-tool/README.md 100644
Wyświetl plik

@ -0,0 +1,10 @@
## MediaCMS CLI Tool
This is the CLI tool to interact with the API of your installation/instance of MediaCMS.
### How to configure and use the tools
- Make sure that you have all the required installations (`cli-tool/requirements.txt`)installed. To install it -
- Create a new virtualenv using any python virtualenv manager.
- Then activate the virtualenv and enter `pip install -r requirements.txt`.
- Create an .env file in this folder (`mediacms/cli-tool/`)
- Run the cli tool using the command `python cli.py login`. This will authenticate you and store necessary creds for further authentications.
- To check the credentials and necessary setup, run `python cli.py whoami`. This will show your details.

167
cli-tool/cli.py 100644
Wyświetl plik

@ -0,0 +1,167 @@
import json
import os
import click
import requests
from decouple import config
from rich import print
from rich.console import Console
from rich.table import Table
console = Console()
print("Welcome to the CLI Tool of [bold blue]MediaCMS![/bold blue]", ":thumbs_up:")
BASE_URL = 'https://demo.mediacms.io/api/v1'
AUTH_KEY = ''
USERNAME = ''
EMAIL = ''
def set_envs():
with open('.env', 'r') as file:
if not file.read(1):
print("Use the Login command to set your credential environment variables")
else:
global AUTH_KEY, USERNAME, EMAIL
AUTH_KEY = config('AUTH_KEY')
USERNAME = config('USERNAME')
EMAIL = config('EMAIL')
set_envs()
@click.group()
def apis():
"""A CLI wrapper for the MediaCMS API endpoints."""
@apis.command()
def login():
"""Login to your account."""
email = input('Enter your email address: ')
password = input('Enter your password: ')
data = {
"email": f"{email}",
"password": f"{password}",
}
response = requests.post(url=f'{BASE_URL}/login', data=data)
if response.status_code == 200:
username = json.loads(response.text)["username"]
with open(".env", "w") as file:
file.writelines(f'AUTH_KEY={json.loads(response.text)["token"]}\n')
file.writelines(f'EMAIL={json.loads(response.text)["email"]}\n')
file.writelines(f'USERNAME={json.loads(response.text)["username"]}\n')
print(f"Welcome to MediaCMS [bold blue]{username}[/bold blue]. Your auth creds have been suceesfully stored in the .env file", ":v:")
else:
print(f'Error: {"non_field_errors": ["User not found."]}')
@apis.command()
def upload_media():
"""Upload media to the server"""
headers = {'authorization': f'Token {AUTH_KEY}'}
path = input('Enter the location of the file or directory where multiple files are present: ')
if os.path.isdir(path):
for filename in os.listdir(path):
files = {}
abs = os.path.abspath(f"{path}/{filename}")
files['media_file'] = open(f'{abs}', 'rb')
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
if response.status_code == 201:
print(f"[bold blue]{filename}[/bold blue] successfully uploaded!")
else:
print(f'Error: {response.text}')
else:
files = {}
files['media_file'] = open(f'{os.path.abspath(path)}', 'rb')
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
if response.status_code == 201:
print(f"[bold blue]{filename}[/bold blue] successfully uploaded!")
else:
print(f'Error: {response.text}')
@apis.command()
def my_media():
"""List all my media"""
headers = {'authorization': f'Token {AUTH_KEY}'}
response = requests.get(url=f'{BASE_URL}/media?author={USERNAME}', headers=headers)
if response.status_code == 200:
data_json = json.loads(response.text)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Name of the media")
table.add_column("Media Type")
table.add_column("State")
for data in data_json['results']:
table.add_row(data['title'], data['media_type'], data['state'])
console.print(table)
else:
print(f'Could not get the media: {response.text}')
@apis.command()
def whoami():
"""Shows the details of the authorized user"""
headers = {'authorization': f'Token {AUTH_KEY}'}
response = requests.get(url=f'{BASE_URL}/whoami', headers=headers)
for data, value in json.loads(response.text).items():
print(data, ' : ', value)
@apis.command()
def categories():
"""List all categories."""
response = requests.get(url=f'{BASE_URL}/categories')
if response.status_code == 200:
data_json = json.loads(response.text)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Category")
table.add_column("Description")
for data in data_json:
table.add_row(data['title'], data['description'])
console.print(table)
else:
print(f'Could not get the categories: {response.text}')
@apis.command()
def encodings():
"""List all encoding profiles"""
response = requests.get(url=f'{BASE_URL}/encode_profiles/')
if response.status_code == 200:
data_json = json.loads(response.text)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Name")
table.add_column("Extension")
table.add_column("Resolution")
table.add_column("Codec")
table.add_column("Description")
for data in data_json:
table.add_row(data['name'], data['extension'], str(data['resolution']), data['codec'], data['description'])
console.print(table)
else:
print(f'Could not get the encodings: {response.text}')
if __name__ == '__main__':
apis()

Wyświetl plik

@ -0,0 +1,4 @@
click
python-decouple
requests
rich

Wyświetl plik

@ -3,6 +3,7 @@ from __future__ import absolute_import
import os
from celery import Celery
from django.conf import settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
app = Celery("cms")
@ -14,5 +15,8 @@ app.conf.beat_schedule = app.conf.CELERY_BEAT_SCHEDULE
app.conf.broker_transport_options = {"visibility_timeout": 60 * 60 * 24} # 1 day
# http://docs.celeryproject.org/en/latest/getting-started/brokers/redis.html#redis-caveats
# setting this to settings.py file only is not respected. Setting here too
app.conf.task_always_eager = settings.CELERY_TASK_ALWAYS_EAGER
app.conf.worker_prefetch_multiplier = 1

Wyświetl plik

@ -18,7 +18,6 @@ class FastPaginationWithoutCount(PageNumberPagination):
django_paginator_class = FasterDjangoPaginator
def get_paginated_response(self, data):
return Response(
OrderedDict(
[

Wyświetl plik

@ -0,0 +1,48 @@
# Development settings, used in docker-compose-dev.yaml
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'imagekit',
'files.apps.FilesConfig',
'users.apps.UsersConfig',
'actions.apps.ActionsConfig',
'debug_toolbar',
'mptt',
'crispy_forms',
'uploader.apps.UploaderConfig',
'djcelery_email',
'ckeditor',
'drf_yasg',
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
STATIC_ROOT = None

Wyświetl plik

@ -7,6 +7,7 @@ DEBUG = False
# PORTAL NAME, this is the portal title and
# is also shown on several places as emails
PORTAL_NAME = "MediaCMS"
PORTAL_DESCRIPTION = ""
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/London"
@ -86,10 +87,15 @@ MAX_MEDIA_PER_PLAYLIST = 70
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
MAX_CHARS_FOR_COMMENT = 10000 # so that it doesn't end up huge
TIMESTAMP_IN_TIMEBAR = False # shows timestamped comments in the timebar for videos
ALLOW_MENTION_IN_COMMENTS = False # allowing to mention other users with @ in the comments
# valid options: content, author
RELATED_MEDIA_STRATEGY = "content"
# Whether or not to generate a sitemap.xml listing the pages on the site (default: False)
GENERATE_SITEMAP = False
USE_I18N = True
USE_L10N = True
USE_TZ = True
@ -146,10 +152,13 @@ STATIC_ROOT = BASE_DIR + "/static/"
# where uploaded + encoded media are stored
MEDIA_ROOT = BASE_DIR + "/media_files/"
MEDIA_UPLOAD_DIR = os.path.join(MEDIA_ROOT, "original/")
MEDIA_ENCODING_DIR = os.path.join(MEDIA_ROOT, "encoded/")
THUMBNAIL_UPLOAD_DIR = os.path.join(MEDIA_UPLOAD_DIR, "thumbnails/")
SUBTITLES_UPLOAD_DIR = os.path.join(MEDIA_UPLOAD_DIR, "subtitles/")
# these used to be os.path.join(MEDIA_ROOT, "folder/") but update to
# Django 3.1.9 requires not absolute paths to be utilized...
MEDIA_UPLOAD_DIR = "original/"
MEDIA_ENCODING_DIR = "encoded/"
THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
FFMPEG_COMMAND = "ffmpeg" # this is the path
@ -441,6 +450,13 @@ LOCAL_INSTALL = False
# it is placed here so it can be overrided on local_settings.py
GLOBAL_LOGIN_REQUIRED = False
# TODO: separate settings on production/development more properly, for now
# this should be ok
CELERY_TASK_ALWAYS_EAGER = False
if os.environ.get("TESTING"):
CELERY_TASK_ALWAYS_EAGER = True
try:
# keep a local_settings.py file for local overrides
from .local_settings import * # noqa
@ -454,7 +470,7 @@ except ImportError:
if "http" not in FRONTEND_HOST:
# FRONTEND_HOST needs a http:// preffix
FRONTEND_HOST = f"http://{FRONTEND_HOST}"
FRONTEND_HOST = f"http://{FRONTEND_HOST}" # noqa
if LOCAL_INSTALL:
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https")
@ -468,4 +484,22 @@ if GLOBAL_LOGIN_REQUIRED:
r'/accounts/login/$',
r'/accounts/logout/$',
r'/accounts/signup/$',
r'/accounts/password/.*/$',
r'/accounts/confirm-email/.*/$',
r'/api/v[0-9]+/',
]
# if True, only show original, don't perform any action on videos
DO_NOT_TRANSCODE_VIDEO = False
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# the following is related to local development using docker
# and docker-compose-dev.yaml
try:
DEVELOPMENT_MODE = os.environ.get("DEVELOPMENT_MODE")
if DEVELOPMENT_MODE:
# keep a dev_settings.py file for local overrides
from .dev_settings import * # noqa
except ImportError:
pass

Wyświetl plik

@ -1,5 +1,5 @@
import debug_toolbar
from django.conf.urls import include, url
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, re_path
from django.views.generic.base import TemplateView
@ -15,15 +15,15 @@ schema_view = get_schema_view(
urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)),
re_path(r"^__debug__/", include(debug_toolbar.urls)),
path(
"robots.txt",
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
),
url(r"^", include("files.urls")),
url(r"^", include("users.urls")),
url(r"^accounts/", include("allauth.urls")),
url(r"^api-auth/", include("rest_framework.urls")),
re_path(r"^", include("files.urls")),
re_path(r"^", include("users.urls")),
re_path(r"^accounts/", include("allauth.urls")),
re_path(r"^api-auth/", include("rest_framework.urls")),
path("admin/", admin.site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),

Wyświetl plik

@ -16,6 +16,10 @@ server {
location /media {
alias /home/mediacms.io/mediacms/media_files ;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
location / {

Wyświetl plik

@ -7,7 +7,7 @@ if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
echo "Running migrations service"
python manage.py migrate
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
if [ "$EXISTING_INSTALLATION" = "True" ]; then
if [ "$EXISTING_INSTALLATION" = "True" ]; then
echo "Loaddata has already run"
else
echo "Running loaddata and creating admin user"
@ -17,7 +17,7 @@ if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
# post_save, needs redis to succeed (ie. migrate depends on redis)
DJANGO_SUPERUSER_PASSWORD=$ADMIN_PASSWORD python manage.py createsuperuser \
--no-input \
--username=admin \
--username=$ADMIN_USER \
--email=$ADMIN_EMAIL \
--database=default || true
echo "Created admin user with password: $ADMIN_PASSWORD"
@ -67,4 +67,5 @@ 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
rm /var/run/mediacms/* -f # remove any stale id, so that on forced restarts of celery workers there are no stale processes that prevent new ones
fi

Wyświetl plik

@ -21,3 +21,4 @@ vacuum = true
hook-master-start = unix_signal:15 gracefully_kill_them_all
need-app = true
die-on-term = true
buffer-size=32768

Wyświetl plik

@ -8,15 +8,13 @@ User=www-data
Group=www-data
Restart=always
RestartSec=10
Environment=APP_DIR="/home/mediacms.io/mediacms"
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERY_APP="cms"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/beat%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/beat%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
Environment=APP_DIR="/home/mediacms.io/mediacms"
ExecStart=/bin/sh -c '${CELERY_BIN} beat -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR}'
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms beat --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
ExecStop=/bin/kill -s TERM $MAINPID
[Install]

Wyświetl plik

@ -8,23 +8,21 @@ User=www-data
Group=www-data
Restart=always
RestartSec=10
Environment=APP_DIR="/home/mediacms.io/mediacms"
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="long1"
Environment=CELERY_QUEUE="long_tasks"
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERY_APP="cms"
Environment=CELERYD_MULTI="multi"
Environment=CELERYD_OPTS="-Ofair --prefetch-multiplier=1"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
Environment=APP_DIR="/home/mediacms.io/mediacms"
ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target

Wyświetl plik

@ -8,14 +8,13 @@ User=www-data
Group=www-data
Restart=always
RestartSec=10
Environment=APP_DIR="/home/mediacms.io/mediacms"
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="short1 short2"
Environment=CELERY_QUEUE="short_tasks"
# Absolute or relative path to the 'celery' command:
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
# App instance to use
# comment out this line if you don't use an app
Environment=CELERY_APP="cms"
# or fully qualified:
#CELERY_APP="proj.tasks:app"
# How to call manage.py
@ -28,13 +27,12 @@ Environment=CELERYD_OPTS="--soft-time-limit=300 -c10"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
Environment=APP_DIR="/home/mediacms.io/mediacms"
ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} --workdir=${APP_DIR} -Q ${CELERY_QUEUE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target

Wyświetl plik

@ -0,0 +1,34 @@
module selinux-mediacms 1.0;
require {
type init_t;
type var_t;
type redis_port_t;
type postgresql_port_t;
type httpd_t;
type httpd_sys_content_t;
type httpd_sys_rw_content_t;
class file { append create execute execute_no_trans getattr ioctl lock open read rename setattr unlink write };
class dir { add_name remove_name rmdir };
class tcp_socket name_connect;
class lnk_file read;
}
#============= httpd_t ==============
allow httpd_t var_t:file { getattr open read };
#============= init_t ==============
allow init_t postgresql_port_t:tcp_socket name_connect;
allow init_t redis_port_t:tcp_socket name_connect;
allow init_t httpd_sys_content_t:dir rmdir;
allow init_t httpd_sys_content_t:file { append create execute execute_no_trans ioctl lock open read rename setattr unlink write };
allow init_t httpd_sys_content_t:lnk_file read;
allow init_t httpd_sys_rw_content_t:dir { add_name remove_name rmdir };
allow init_t httpd_sys_rw_content_t:file { create ioctl lock open read setattr unlink write };

Wyświetl plik

@ -24,4 +24,4 @@ vacuum = true
logto = /home/mediacms.io/mediacms/logs/errorlog.txt
disable-logging = true
buffer-size=32768

Wyświetl plik

@ -1,6 +1,22 @@
version: "3"
services:
migrations:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
volumes:
- ./:/home/mediacms.io/mediacms/
command: "python manage.py migrate"
environment:
DEVELOPMENT_MODE: "True"
restart: on-failure
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
frontend:
image: node:14
volumes:
@ -18,32 +34,20 @@ services:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
command: "python manage.py runserver 0.0.0.0:80"
environment:
DEVELOPMENT_MODE: "True"
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ports:
- "80:80"
volumes:
- ./:/home/mediacms.io/mediacms/
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
selenium_hub:
container_name: selenium_hub
image: selenium/hub
ports:
- "4444:4444"
selenium_chrome:
container_name: selenium_chrome
image: selenium/node-chrome-debug
environment:
- HUB_PORT_4444_TCP_ADDR=selenium_hub
- HUB_PORT_4444_TCP_PORT=4444
ports:
- "5900:5900"
depends_on:
- selenium_hub
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
@ -51,8 +55,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5
@ -64,3 +69,16 @@ services:
interval: 30s
timeout: 10s
retries: 3
celery_worker:
image: mediacms/mediacms-dev: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:
- web

Wyświetl plik

@ -18,6 +18,9 @@ services:
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
command: "./deploy/docker/prestart.sh"
restart: on-failure
depends_on:
@ -65,7 +68,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always
@ -73,8 +76,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

Wyświetl plik

@ -38,6 +38,9 @@ services:
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
VIRTUAL_HOST: localhost
depends_on:
- migrations
@ -67,7 +70,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always
@ -75,8 +78,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

Wyświetl plik

@ -38,6 +38,9 @@ services:
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
command: "./deploy/docker/prestart.sh"
restart: on-failure
depends_on:
@ -87,7 +90,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
@ -95,8 +98,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 30s
timeout: 10s
retries: 5

Wyświetl plik

@ -11,6 +11,9 @@ services:
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
command: "./deploy/docker/prestart.sh"
restart: on-failure
depends_on:
@ -63,7 +66,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always
@ -71,8 +74,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 30s
timeout: 10s
retries: 5

Wyświetl plik

@ -11,6 +11,9 @@ services:
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
#ADMIN_PASSWORD: 'uncomment_and_set_password_here'
command: "./deploy/docker/prestart.sh"
restart: on-failure
depends_on:
@ -59,7 +62,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:15.2-alpine
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
@ -67,8 +70,9 @@ services:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
TZ: Europe/London
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
test: ["CMD-SHELL", "pg_isready", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5

Wyświetl plik

@ -1,133 +0,0 @@
## How To Add A Static Page To The Sidebar
### 1. Create your html page in templates/cms/
e.g. duplicate and rename about.html
```
sudo cp templates/cms/about.html templates/cms/volunteer.html
```
### 2. Create your css file in static/css/
```
touch static/css/volunteer.css
```
### 3. In your html file, update block headermeta to reflect your new page
```
{% block headermeta %}
<meta property="og:title" content="Volunteer - {{PORTAL_NAME}}">
<meta property="og:type" content="website">
<meta property="og:description" content="">
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"name": "{{PORTAL_NAME}}",
"item": {
"@type": "WebPage",
"@id": "{{FRONTEND_HOST}}"
}
},
{
"@type": "ListItem",
"position": 2,
"name": "Volunteer",
"item": {
"@type": "VolunteerPage",
"@id": "{{FRONTEND_HOST}}/volunteer"
}
}]
}
</script>
<link href="{% static "css/volunteer.css" %}" rel="stylesheet"/>
{% endblock headermeta %}
```
### 4. In your html file, update block innercontent to reflect your actual content
Write whatever you like.
### 5. In your css file, write matching styles for you html file.
Write whatever you like.
### 6. Add your view to files/views.py
```
def volunteer(request):
"""Volunteer view"""
context = {}
return render(request, "cms/volunteer.html", context)
```
### 7. Add your url pattern to files/urls.py
```
urlpatterns = [
url(r"^$", views.index),
url(r"^about", views.about, name="about"),
url(r"^volunteer", views.volunteer, name="volunteer"),
```
### 8. Add your page to the left sidebar
To add a link to your page as a menu item in the left sidebar,
add the following code after the last line in _commons.js
```
/* Checks that a given selector has loaded. */
const checkElement = async selector => {
while ( document.querySelector(selector) === null) {
await new Promise( resolve => requestAnimationFrame(resolve) )
}
return document.querySelector(selector);
};
/* Checks that sidebar nav menu has loaded, then adds menu item. */
checkElement('.nav-menu')
.then((element) => {
(function(){
var a = document.createElement('a');
a.href = "/volunteer";
a.title = "Volunteer";
var s = document.createElement('span');
s.className = "menu-item-icon";
var icon = document.createElement('i');
icon.className = "material-icons";
icon.setAttribute("data-icon", "people");
s.appendChild(icon);
a.appendChild(s);
var linkText = document.createTextNode("Volunteer");
var t = document.createElement('span');
t.appendChild(linkText);
a.appendChild(t);
var listItem = document.createElement('li');
listItem.className = "link-item";
listItem.appendChild(a);
//if signed out use 3rd nav-menu
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
var loc = elem.innerText;
if (loc.includes("About")){
elem.insertBefore(listItem, elem.children[2]);
} else { //if signed in use 4th nav-menu
elem = document.querySelector(".nav-menu:nth-child(4) nav ul");
elem.insertBefore(listItem, elem.children[2]);
}
})();
});
```
### 9. Restart the mediacms web server
On docker:
```
sudo docker stop mediacms_web_1 && sudo docker start mediacms_web_1
```
Otherwise
```
sudo systemctl restart mediacms
```

Wyświetl plik

@ -1,270 +0,0 @@
## Configuration
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 `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.
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
```bash
#systemctl restart mediacms
```
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
```bash
#docker-compose restart web celery_worker celery_beat
```
### change portal logo
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`static/images/logo_light.svg`)
### set global portal title
set `PORTAL_NAME`, eg
```
PORTAL_NAME = 'my awesome portal'
```
### who can add media
By default `CAN_ADD_MEDIA = "all"` means that all registered users can add media. Other valid options are:
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
- **advancedUser**, only users that are marked as advanced users can add media. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
### what is the portal workflow
The `PORTAL_WORKFLOW` variable specifies what happens to newly uploaded media, whether they appear on listings (as the index page, or search)
- **public** is the default option and means that a media can appear on listings. If media type is video, it will appear once at least a task that produces an encoded version of the file has finished succesfully. For other type of files, as image/audio they appear instantly
- **private** means that newly uploaded content is private - only users can see it or MediaCMS editors, managers and admins. Those can also set the status to public or unlisted
- **unlisted** means that items are unlisted. However if a user visits the url of an unlisted media, it will be shown (as opposed to private)
### show/hide the Sign in button
to show button:
```
LOGIN_ALLOWED = True
```
to hide button:
```
LOGIN_ALLOWED = False
```
### show/hide the Register button
to show button:
```
REGISTER_ALLOWED = True
```
to hide button:
```
REGISTER_ALLOWED = False
```
### show/hide the upload media button
To show:
```
UPLOAD_MEDIA_ALLOWED = True
```
To hide:
```
UPLOAD_MEDIA_ALLOWED = False
```
### show/hide the actions buttons (like/dislike/report)
Make changes (True/False) to any of the following:
```
- CAN_LIKE_MEDIA = True # whether the like media appears
- CAN_DISLIKE_MEDIA = True # whether the dislike media appears
- CAN_REPORT_MEDIA = True # whether the report 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
set a low number for variable `REPORTED_TIMES_THRESHOLD`
eg
```
REPORTED_TIMES_THRESHOLD = 2
```
once the limit is reached, media goes to private state and an email is sent to admins
### set a custom message on the media upload page
this message will appear below the media drag and drop form
```
PRE_UPLOAD_MEDIA_MESSAGE = 'custom message'
```
### set email settings
Set correct settings per provider
```
DEFAULT_FROM_EMAIL = 'info@mediacms.io'
EMAIL_HOST_PASSWORD = 'xyz'
EMAIL_HOST_USER = 'info@mediacms.io'
EMAIL_USE_TLS = True
SERVER_EMAIL = DEFAULT_FROM_EMAIL
EMAIL_HOST = 'mediacms.io'
EMAIL_PORT = 587
ADMIN_EMAIL_LIST = ['info@mediacms.io']
```
### disallow user registrations from specific domains
set domains that are not valid for registration via this variable:
```
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
'xxx.com', 'emaildomainwhatever.com']
```
### require a review by MediaCMS editors/managers/admins
set value
```
MEDIA_IS_REVIEWED = False
```
any uploaded media now needs to be reviewed before it can appear to the listings.
MediaCMS editors/managers/admins can visit the media page and edit it, where they can see the option to mark media as reviewed. By default this is set to True, so all media don't require to be reviewed
### specify maximum number of media for a playlist
set a different threshold on variable `MAX_MEDIA_PER_PLAYLIST`
eg
```
MAX_MEDIA_PER_PLAYLIST = 14
```
### specify maximum size of a media that can be uploaded
change `UPLOAD_MAX_SIZE`.
default is 4GB
```
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
```
### specify maximum size of comments
change `MAX_CHARS_FOR_COMMENT`
default:
```
MAX_CHARS_FOR_COMMENT = 10000
```
### how many files to upload in parallel
set a different threshold for `UPLOAD_MAX_FILES_NUMBER`
default:
```
UPLOAD_MAX_FILES_NUMBER = 100
```
### force users confirm their email upon registrations
default option for email confirmation is optional. Set this to mandatory in order to force users confirm their email before they can login
```
ACCOUNT_EMAIL_VERIFICATION = 'optional'
```
### rate limit account login attempts
after this number is reached
```
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
```
sets a timeout (in seconds)
```
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
```
### disallow user registration
set the following variable to False
```
USERS_CAN_SELF_REGISTER = True
```
### configure notifications
Global notifications that are implemented are controlled by the following options:
```
USERS_NOTIFICATIONS = {
'MEDIA_ADDED': True,
}
```
If you want to disable notification for new media, set to False
Admins also receive notifications on different events, set any of the following to False to disable
```
ADMINS_NOTIFICATIONS = {
'NEW_USER': True,
'MEDIA_ADDED': True,
'MEDIA_REPORTED': True,
}
```
- NEW_USER: a new user is added
- MEDIA_ADDED: a media is added
- MEDIA_REPORTED: the report for a media was hit
### Google Analytics
Checkout the instructions by alberto98fx on [Google Analytics](/docs/robots_and_analytics.md) page.

Wyświetl plik

@ -1,59 +0,0 @@
# Docker Compose
## Installation
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
For Ubuntu 18/20 systems this is:
```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```
Then run as root
```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).
If you want to explore more options (including setup of https with letsencrypt certificate) checkout the docs on the [Docker deployment](/docs/Docker_deployment.md) page for different docker-compose setups to use.
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
A user admin has been created with random password, you should be able to see it at the end of migrations container, eg
```
migrations_1 | Created admin user with password: gwg1clfkwf
```
or if you have set the ADMIN_PASSWORD variable on Dockerfile, that variable will be set as the admin user's password
## Update
Get latest MediaCMS image and stop/start containers
```bash
cd /path/to/mediacms/installation
docker pull mediacms/mediacms
docker-compose down
docker-compose up
```
## Configuration
Checkout the configuration docs on [Configuration](/docs/Configuration.md) page.
## Maintenance
Database is stored on ../postgres_data/ and media_files on media_files/

Wyświetl plik

@ -1,53 +0,0 @@
# 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.
# Server with ssl certificate through letsencrypt service, accessed as https://my_domain.com
Before trying this out make sure the ip points to my_domain.com.
With this method [this deployment](../docker-compose-letsencrypt.yaml) is used.
Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my_domain.com, and your email on `LETSENCRYPT_EMAIL`
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
Now run docker-compose -f docker-compose-letsencrypt.yaml up, when installation finishes you will be able to access https://my_domain.com using a valid Letsencrypt certificate!
# 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)

Wyświetl plik

@ -1,38 +0,0 @@
# 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.
Installation on a Ubuntu 18 or 20 system with git utility installed should be completed in a few minutes with the following steps.
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
Automated script - tested on Ubuntu 18, Ubuntu 20, and Debian Buster
```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/
git clone https://github.com/mediacms-io/mediacms
cd /home/mediacms.io/mediacms/ && bash ./install.sh
```
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
## Update
If you've used the above way to install MediaCMS, update with the following:
```bash
cd /home/mediacms.io/mediacms # enter mediacms directory
source /home/mediacms.io/bin/activate # use virtualenv
git pull # update code
python manage.py migrate # run Django migrations
sudo systemctl restart mediacms celery_long celery_short # restart services
```
## Configuration
Checkout the configuration docs on [Configuration](/docs/Configuration.md) page.
## Maintenance
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions

Wyświetl plik

@ -1,20 +0,0 @@
## User scenarios to test
## test video media + image
try uploading a video + image, make sure they get encoded well and check they appear on index/search/category/author page
try editing/setting metadata, confirm action is performed, also that are searchable
try adding custom poster, confirm it loads well on video page/listings
try specifying different thumbnail time, confirm an automatic screenshot is taken
## portal workflow
change workflow to unlisted, check they don't appear on index/search/category/author page
## users management
create an admin, a MediaCMS editor and MediaCMS manager. All should see edit/delete on a media and also comments, and action should work.
For users edit and delete, only MediaCMS manager and admin should see edit/delete and these actions should work.
## test subtitle
add language and test subtitling

Wyświetl plik

@ -1,29 +1,781 @@
# Administrators documentation
## Table of contents
- [Manage pages](#manage-pages)
- [Django admin dashboard](#django-admin-dashboard)
- [On portal workflow](#on-portal-workflow)
- [On user roles](#on-user-roles)
- [Adding languages for Captions and subtitles](#adding-languages-for-captions-and-subtitles)
- [Add/delete categories and tags](#adddelete-categories-and-tags)
- [Video transcoding](#video-transcoding)
- [1. Welcome](#1-welcome)
- [2. Server Installaton](#2-server-installation)
- [3. Docker Installation](#3-docker-installation)
- [4. Docker Deployment options](#4-docker-deployment-options)
- [5. Configuration](#5-configuration)
- [6. Manage pages](#6-manage-pages)
- [7. Django admin dashboard](#7-django-admin-dashboard)
- [8. On portal workflow](#8-on-portal-workflow)
- [9. On user roles](#9-on-user-roles)
- [10. Adding languages for Captions and subtitles](#10-adding-languages-for-captions-and-subtitles)
- [11. Add/delete categories and tags](#11-adddelete-categories-and-tags)
- [12. Video transcoding](#12-video-transcoding)
- [13. How To Add A Static Page To The Sidebar](#13-how-to-add-a-static-page-to-the-sidebar)
- [14. Add Google Analytics](#14-add-google-analytics)
- [15. Debugging email issues](#15-debugging-email-issues)
- [16. Frequently Asked Questions](#16-frequently-asked-questions)
- [17. Cookie consent code](#17-cookie-consent-code)
- [18. Disable encoding and show only original file](#18-disable-encoding-and-show-only-original-file)
## 1. Welcome
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
## 2. 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 (tested on versions 20, 22).
Installation on an Ubuntu system with git utility installed should be completed in a few minutes with the following steps.
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
Automated script - tested on Ubuntu 20, Ubuntu 22 and Debian Buster
```bash
mkdir /home/mediacms.io && cd /home/mediacms.io/
git clone https://github.com/mediacms-io/mediacms
cd /home/mediacms.io/mediacms/ && bash ./install.sh
```
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
## Manage pages
### Update
## Django admin dashboard
If you've used the above way to install MediaCMS, update with the following:
## On portal workflow
```bash
cd /home/mediacms.io/mediacms # enter mediacms directory
source /home/mediacms.io/bin/activate # use virtualenv
git pull # update code
pip install -r requirements.txt -U # run pip install to update
python manage.py migrate # run Django migrations
sudo systemctl restart mediacms celery_long celery_short # restart services
```
### Update from version 2 to version 3
Version 3 is using Django 4 and Celery 5, and needs a recent Python 3.x version. If you are updating from an older version, make sure Python is updated first. Version 2 could run on Python 3.6, but version 3 needs Python3.8 and higher.
The syntax for starting Celery has also changed, so you have to copy the celery related systemctl files and restart
```
# cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
# cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
# cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
# systemctl daemon-reload
# systemctl start celery_long celery_short celery_beat
```
### Configuration
Checkout the configuration section here.
### Maintenance
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
## 3. Docker Installation
## Installation
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
For Ubuntu 20/22 systems this is:
```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```
Then run as root
```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).
If you want to explore more options (including setup of https with letsencrypt certificate) checkout [Docker deployment](/docs/admins_docs.md#4-docker-deployment-options) section for different docker-compose setups to use.
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
A user admin has been created with random password, you should be able to see it at the end of migrations container, eg
```
migrations_1 | Created admin user with password: gwg1clfkwf
```
or if you have set the ADMIN_PASSWORD variable on docker-compose file you have used (example `docker-compose.yaml`), that variable will be set as the admin user's password
### Update
Get latest MediaCMS image and stop/start containers
```bash
cd /path/to/mediacms/installation
docker pull mediacms/mediacms
docker-compose down
docker-compose up
```
### Update from version 2 to version 3
Version 3 is using Python 3.11 and PostgreSQL 15. If you are updating from an older version, that was using PostgreSQL 13, the automatic update will not work, as you will receive the following message when the PostgreSQL container starts:
```
db_1 | 2023-06-27 11:07:42.959 UTC [1] FATAL: database files are incompatible with server
db_1 | 2023-06-27 11:07:42.959 UTC [1] DETAIL: The data directory was initialized by PostgreSQL version 13, which is not compatible with this version 15.2.
```
At this point there are two options: either edit the Docker Compose file and make use of the existing postgres:13 image, or otherwise you have to perform the migration from postgresql 13 to version 15. More notes on https://github.com/mediacms-io/mediacms/pull/749
## Configuration
Checkout the configuration docs here.
### Maintenance
Database is stored on ../postgres_data/ and media_files on media_files/
## 4. Docker Deployment options
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.
### Server with ssl certificate through letsencrypt service, accessed as https://my_domain.com
Before trying this out make sure the ip points to my_domain.com.
With this method [this deployment](../docker-compose-letsencrypt.yaml) is used.
Edit this file and set `VIRTUAL_HOST` as my_domain.com, `LETSENCRYPT_HOST` as my_domain.com, and your email on `LETSENCRYPT_EMAIL`
Edit `deploy/docker/local_settings.py` and set https://my_domain.com as `FRONTEND_HOST`
Now run docker-compose -f docker-compose-letsencrypt.yaml up, when installation finishes you will be able to access https://my_domain.com using a valid Letsencrypt certificate!
### 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)
## 5. Configuration
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 `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.
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
```bash
#systemctl restart mediacms
```
Docker Compose installation: edit `deploy/docker/local_settings.py`, make a change and restart MediaCMS containers
```bash
#docker-compose restart web celery_worker celery_beat
```
### 5.1 Change portal logo
Set a new svg file for the white theme (`static/images/logo_dark.svg`) or the dark theme (`static/images/logo_light.svg`)
### 5.2 Set global portal title
set `PORTAL_NAME`, eg
```
PORTAL_NAME = 'my awesome portal'
```
### 5.3 Control who can add media
By default `CAN_ADD_MEDIA = "all"` means that all registered users can add media. Other valid options are:
- **email_verified**, a user not only has to register an account but also verify the email (by clicking the link sent upon registration). Apparently email configuration need to work, otherise users won't receive emails.
- **advancedUser**, only users that are marked as advanced users can add media. Admins or MediaCMS managers can make users advanced users by editing their profile and selecting advancedUser.
### 5.4 What is the portal workflow
The `PORTAL_WORKFLOW` variable specifies what happens to newly uploaded media, whether they appear on listings (as the index page, or search)
- **public** is the default option and means that a media can appear on listings. If media type is video, it will appear once at least a task that produces an encoded version of the file has finished succesfully. For other type of files, as image/audio they appear instantly
- **private** means that newly uploaded content is private - only users can see it or MediaCMS editors, managers and admins. Those can also set the status to public or unlisted
- **unlisted** means that items are unlisted. However if a user visits the url of an unlisted media, it will be shown (as opposed to private)
### 5.5 Show or hide the Sign in button
to show button:
```
LOGIN_ALLOWED = True
```
to hide button:
```
LOGIN_ALLOWED = False
```
### 5.6 Show or hide the Register button
to show button:
```
REGISTER_ALLOWED = True
```
to hide button:
```
REGISTER_ALLOWED = False
```
### 5.7 Show or hide the upload media button
To show:
```
UPLOAD_MEDIA_ALLOWED = True
```
To hide:
```
UPLOAD_MEDIA_ALLOWED = False
```
### 5.8 Show or hide the actions buttons (like/dislike/report)
Make changes (True/False) to any of the following:
```
- CAN_LIKE_MEDIA = True # whether the like media appears
- CAN_DISLIKE_MEDIA = True # whether the dislike media appears
- CAN_REPORT_MEDIA = True # whether the report media appears
- CAN_SHARE_MEDIA = True # whether the share media appears
```
### 5.9 Show or hide the download option on a media
Edit `templates/config/installation/features.html` and set
```
download: false
```
### 5.10 Automatically hide media upon being reported
set a low number for variable `REPORTED_TIMES_THRESHOLD`
eg
```
REPORTED_TIMES_THRESHOLD = 2
```
once the limit is reached, media goes to private state and an email is sent to admins
### 5.11 Set a custom message on the media upload page
this message will appear below the media drag and drop form
```
PRE_UPLOAD_MEDIA_MESSAGE = 'custom message'
```
### 5.12 Set email settings
Set correct settings per provider
```
DEFAULT_FROM_EMAIL = 'info@mediacms.io'
EMAIL_HOST_PASSWORD = 'xyz'
EMAIL_HOST_USER = 'info@mediacms.io'
EMAIL_USE_TLS = True
SERVER_EMAIL = DEFAULT_FROM_EMAIL
EMAIL_HOST = 'mediacms.io'
EMAIL_PORT = 587
ADMIN_EMAIL_LIST = ['info@mediacms.io']
```
### 5.13 Disallow user registrations from specific domains
set domains that are not valid for registration via this variable:
```
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = [
'xxx.com', 'emaildomainwhatever.com']
```
### 5.14 Require a review by MediaCMS editors/managers/admins
set value
```
MEDIA_IS_REVIEWED = False
```
any uploaded media now needs to be reviewed before it can appear to the listings.
MediaCMS editors/managers/admins can visit the media page and edit it, where they can see the option to mark media as reviewed. By default this is set to True, so all media don't require to be reviewed
### 5.15 Specify maximum number of media for a playlist
set a different threshold on variable `MAX_MEDIA_PER_PLAYLIST`
eg
```
MAX_MEDIA_PER_PLAYLIST = 14
```
### 5.16 Specify maximum size of a media that can be uploaded
change `UPLOAD_MAX_SIZE`.
default is 4GB
```
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
```
### 5.17 Specify maximum size of comments
change `MAX_CHARS_FOR_COMMENT`
default:
```
MAX_CHARS_FOR_COMMENT = 10000
```
### 5.18 How many files to upload in parallel
set a different threshold for `UPLOAD_MAX_FILES_NUMBER`
default:
```
UPLOAD_MAX_FILES_NUMBER = 100
```
### 5.18 force users confirm their email upon registrations
default option for email confirmation is optional. Set this to mandatory in order to force users confirm their email before they can login
```
ACCOUNT_EMAIL_VERIFICATION = 'optional'
```
### 5.20 Rate limit account login attempts
after this number is reached
```
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 20
```
sets a timeout (in seconds)
```
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 5
```
### 5.21 Disallow user registration
set the following variable to False
```
USERS_CAN_SELF_REGISTER = True
```
### 5.22 Configure notifications
Global notifications that are implemented are controlled by the following options:
```
USERS_NOTIFICATIONS = {
'MEDIA_ADDED': True,
}
```
If you want to disable notification for new media, set to False
Admins also receive notifications on different events, set any of the following to False to disable
```
ADMINS_NOTIFICATIONS = {
'NEW_USER': True,
'MEDIA_ADDED': True,
'MEDIA_REPORTED': True,
}
```
- NEW_USER: a new user is added
- MEDIA_ADDED: a media is added
- MEDIA_REPORTED: the report for a media was hit
### 5.23 Configure only member access to media
- Make the portal workflow public, but at the same time set `GLOBAL_LOGIN_REQUIRED = True` so that only logged in users can see content.
- You can either set `REGISTER_ALLOWED = False` if you want to add members yourself or checkout options on "django-allauth settings" that affects registration in `cms/settings.py`. Eg set the portal invite only, or set email confirmation as mandatory, so that you control who registers.
### 5.24 Enable the sitemap
Whether or not to enable generation of a sitemap file at http://your_installation/sitemap.xml (default: False)
```
GENERATE_SITEMAP = False
```
## 6. Manage pages
to be written
## 7. Django admin dashboard
## 8. On portal workflow
Who can publish content, how content appears on public listings.Difference between statuses (private, unlisted, public)
## On user roles
## 9. On user roles
Differences over MediaCMS manager, MediaCMS editor, logged in user
## Adding languages for Captions and subtitles
## 10. Adding languages for Captions and subtitles
to be written
## Add/delete categories and tags
## 11. Add/delete categories and tags
Through the admin section - http://your_installation/admin/
## Video transcoding
Add / remove resolutions and profiles through http://your_installation/admin/encodeprofile
## 12. Video transcoding
Add / remove resolutions and profiles by modifying the database table of `Encode profiles` through https://your_installation/admin/files/encodeprofile/
For example, the `Active` state of any profile can be toggled to enable or disable it.
## 13. How To Add A Static Page To The Sidebar
### 1. Create your html page in templates/cms/
e.g. duplicate and rename about.html
```
sudo cp templates/cms/about.html templates/cms/volunteer.html
```
### 2. Create your css file in static/css/
```
touch static/css/volunteer.css
```
### 3. In your html file, update block headermeta to reflect your new page
```
{% block headermeta %}
<meta property="og:title" content="Volunteer - {{PORTAL_NAME}}">
<meta property="og:type" content="website">
<meta property="og:description" content="">
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"name": "{{PORTAL_NAME}}",
"item": {
"@type": "WebPage",
"@id": "{{FRONTEND_HOST}}"
}
},
{
"@type": "ListItem",
"position": 2,
"name": "Volunteer",
"item": {
"@type": "VolunteerPage",
"@id": "{{FRONTEND_HOST}}/volunteer"
}
}]
}
</script>
<link href="{% static "css/volunteer.css" %}" rel="stylesheet"/>
{% endblock headermeta %}
```
### 4. In your html file, update block innercontent to reflect your actual content
Write whatever you like.
### 5. In your css file, write matching styles for you html file.
Write whatever you like.
### 6. Add your view to files/views.py
```
def volunteer(request):
"""Volunteer view"""
context = {}
return render(request, "cms/volunteer.html", context)
```
### 7. Add your url pattern to files/urls.py
```
urlpatterns = [
url(r"^$", views.index),
url(r"^about", views.about, name="about"),
url(r"^volunteer", views.volunteer, name="volunteer"),
```
### 8. Add your page to the left sidebar
To add a link to your page as a menu item in the left sidebar,
add the following code after the last line in _commons.js
```
/* Checks that a given selector has loaded. */
const checkElement = async selector => {
while ( document.querySelector(selector) === null) {
await new Promise( resolve => requestAnimationFrame(resolve) )
}
return document.querySelector(selector);
};
/* Checks that sidebar nav menu has loaded, then adds menu item. */
checkElement('.nav-menu')
.then((element) => {
(function(){
var a = document.createElement('a');
a.href = "/volunteer";
a.title = "Volunteer";
var s = document.createElement('span');
s.className = "menu-item-icon";
var icon = document.createElement('i');
icon.className = "material-icons";
icon.setAttribute("data-icon", "people");
s.appendChild(icon);
a.appendChild(s);
var linkText = document.createTextNode("Volunteer");
var t = document.createElement('span');
t.appendChild(linkText);
a.appendChild(t);
var listItem = document.createElement('li');
listItem.className = "link-item";
listItem.appendChild(a);
//if signed out use 3rd nav-menu
var elem = document.querySelector(".nav-menu:nth-child(3) nav ul");
var loc = elem.innerText;
if (loc.includes("About")){
elem.insertBefore(listItem, elem.children[2]);
} else { //if signed in use 4th nav-menu
elem = document.querySelector(".nav-menu:nth-child(4) nav ul");
elem.insertBefore(listItem, elem.children[2]);
}
})();
});
```
### 9. Restart the mediacms web server
On docker:
```
sudo docker stop mediacms_web_1 && sudo docker start mediacms_web_1
```
Otherwise
```
sudo systemctl restart mediacms
```
## 14. Add Google Analytics
Instructions contributed by @alberto98fx
1. Create a file:
``` touch $DIR/mediacms/templates/tracking.html ```
2. Add the Gtag/Analytics script
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
```
<head>
{% block head %}
<title>{% block headtitle %}{{PORTAL_NAME}}{% endblock headtitle %}</title>
{% include "common/head-meta.html" %}
{% block headermeta %}
<meta property="og:title" content="{{PORTAL_NAME}}">
<meta property="og:type" content="website">
{%endblock headermeta %}
{% block externallinks %}{% endblock externallinks %}
{% include "common/head-links.html" %}
{% block topimports %}{%endblock topimports %}
{% include "config/index.html" %}
{% endblock head %}
</head>
```
4. Add ``` {% include "tracking.html" %} ``` at the end inside the section ```<head>```
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
```
web:
image: mediacms/mediacms:latest
restart: unless-stopped
ports:
- "80:80"
deploy:
replicas: 1
volumes:
- ./templates/root.html:/home/mediacms.io/mediacms/templates/root.html
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
```
## 15. Debugging email issues
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
Enter the Django shell, example if you're using the Single Server installation:
```bash
source /home/mediacms.io/bin/activate
python manage.py shell
```
and inside the shell
```bash
from django.core.mail import EmailMessage
from django.conf import settings
settings.EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
email = EmailMessage(
'title',
'msg',
settings.DEFAULT_FROM_EMAIL,
['recipient@email.com'],
)
email.send(fail_silently=False)
```
You have the chance to either receive the email (in this case it will be sent to recipient@email.com) otherwise you will see the error.
For example, while specifying wrong password for my Gmail account I get
```
SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 https://support.google.com/mail/?p=BadCredentials d4sm12687785wrc.34 - gsmtp')
```
## 16. Frequently Asked Questions
Video is playing but preview thumbnails are not showing for large video files
Chances are that the sprites file was not created correctly.
The output of files.tasks.produce_sprite_from_video() function in this case is something like this
```
convert-im6.q16: width or height exceeds limit `/tmp/img001.jpg' @ error/cache.c/OpenPixelCache/3912.
```
Solution: edit file `/etc/ImageMagick-6/policy.xml` and set bigger values for the lines that contain width and height. For example
```
<policy domain="resource" name="height" value="16000KP"/>
<policy domain="resource" name="width" value="16000KP"/>
```
Newly added video files now will be able to produce the sprites file needed for thumbnail previews. To re-run that task on existing videos, enter the Django shell
```
root@8433f923ccf5:/home/mediacms.io/mediacms# source /home/mediacms.io/bin/activate
root@8433f923ccf5:/home/mediacms.io/mediacms# python manage.py shell
Python 3.8.14 (default, Sep 13 2022, 02:23:58)
```
and run
```
In [1]: from files.models import Media
In [2]: from files.tasks import produce_sprite_from_video
In [3]: for media in Media.objects.filter(media_type='video', sprites=''):
...: produce_sprite_from_video(media.friendly_token)
```
this will re-create the sprites for videos that the task failed.
## 17. Cookie consent code
On file `templates/components/header.html` you can find a simple cookie consent code. It is commented, so you have to remove the `{% comment %}` and `{% endcomment %}` lines in order to enable it. Or you can replace that part with your own code that handles cookie consent banners.
![Simple Cookie Consent](images/cookie_consent.png)
## 18. Disable encoding and show only original file
When videos are uploaded, they are getting encoded to multiple resolutions, a procedure called transcoding. Sometimes this is not needed and you only need to show the original file, eg when MediaCMS is running on a low capabilities server. To achieve this, edit settings.py and set
```
DO_NOT_TRANSCODE_VIDEO = True
```
This will disable the transcoding process and only the original file will be shown. Note that this will also disable the sprites file creation, so you will not have the preview thumbnails on the video player.

60
docs/dev_exp.md 100644
Wyświetl plik

@ -0,0 +1,60 @@
# Developer Experience
There is ongoing effort to provide a better developer experience and document it.
## How to develop locally with Docker
First install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
Then run `docker-compose -f docker-compose-dev.yaml up`
```
user@user:~/mediacms$ docker-compose -f docker-compose-dev.yaml up
```
In a few minutes the app will be available at http://localhost . Login via admin/admin
### What does docker-compose-dev.yaml do?
It build the two images used for backend and frontend.
* Backend: `mediacms/mediacms-dev:latest`
* Frontend: `frontend`
and will start all services required for MediaCMS, as Celery/Redis for asynchronous tasks, PostgreSQL database, Django and React
For Django, the changes from the image produced by docker-compose.yaml are these:
* Django runs in debug mode, with `python manage.py runserver`
* uwsgi and nginx are not run
* Django runs in Debug mode, with Debug Toolbar
* Static files (js/css) are loaded from static/ folder
* corsheaders is installed and configured to allow all origins
For React, it will run `npm start` in the frontend folder, which will start the development server.
Check it on http://localhost:8088/
### How to develop in Django
Django starts at http://localhost and is reloading automatically. Making any change to the python code should refresh Django.
### How to develop in React
React is started on http://localhost:8088/ , code is located in frontend/ , so making changes there should have instant effect on the page. Keep in mind that React is loading data from Django, and that it has to be built so that Django can serve it.
### Making changes to the frontend
The way React is added is more complicated than the usual SPA project and this is because React is used as a library loaded by Django Templates, so it is not a standalone project and is not handling routes etc.
The two directories to consider are:
* frontend/src , for the React files
* templates/, for the Django templates.
Django is using a highly intuitive hierarchical templating system (https://docs.djangoproject.com/en/4.2/ref/templates/), where the base template is templates/root.html and all other templates are extending it.
React is called through the Django templates, eg templates/cms/media.html is loading js/media.js
In order to make changes to React code, edit code on frontend/src and check it's effect on http://localhost:8088/ . Once ready, build it and copy it to the Django static folder, so that it is served by Django.
### Development workflow with the frontend
1. Edit frontend/src/ files
2. Check changes on http://localhost:8088/
3. Build frontend with `docker-compose -f docker-compose-dev.yaml exec frontend npm run dist`
4. Copy static files to Django static folder with`cp -r frontend/dist/static/* static/`
5. Restart Django - `docker-compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
6. Commit the changes

Wyświetl plik

@ -1,26 +1,51 @@
# Developers documentation
## Table of contents
- [System architecture](#system-architecture)
- [API documentation](#api-documentation)
- [How to contribute](#how-to-contribute)
- [Working with Docker tips](#working-with-docker-tips)
- [How video is transcoded](#how-video-is-transcoded)
- [1. Welcome](#1-welcome)
- [2. System architecture](#2-system-architecture)
- [3. API documentation](#3-api-documentation)
- [4. How to contribute](#4-how-to-contribute)
- [5. Working with Docker tips](#5-working-with-docker-tips)
- [6. Working with the automated tests](#6-working-with-the-automated-tests)
- [7. How video is transcoded](#7-how-video-is-transcoded)
## How to contribute
## 1. Welcome
This page is created for MediaCMS developers and contains related information.
## 2. System architecture
to be written
## 3. API documentation
API is documented using Swagger - checkout ot http://your_installation/swagger - example https://demo.mediacms.io/swagger/
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
An example of working with Python requests library:
```
import requests
auth = ('user' ,'password')
upload_url = "https://domain/api/v1/media"
title = 'x title'
description = 'x description'
media_file = '/tmp/file.mp4'
requests.post(
url=upload_url,
files={'media_file': open(media_file,'rb')},
data={'title': title, 'description': description},
auth=auth
)
```
## 4. How to contribute
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
## System architecture
## API documentation
API is documented using Swagger - checkout ot http://your_installation/swagger - example https://demo.mediacms.io/swagger/
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
## Working with Docker tips
## 5. Working with Docker tips
To perform the Docker installation, follow instructions to install Docker + Docker compose (docs/Docker_Compose.md) and then build/start docker-compose-dev.yaml . This will run the frontend application on port 8088 on top of all other containers (including the Django web application on port 80)
@ -29,11 +54,18 @@ docker-compose -f docker-compose-dev.yaml build
docker-compose -f docker-compose-dev.yaml up
```
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
```
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
```
### Frontend application changes
Eg change `frontend/src/static/js/pages/HomePage.tsx` , dev application refreshes in a number of seconds (hot reloading) and I see the changes, once I'm happy I can run
```
docker-compose -f docker-compose-dev.yaml -T frontend npm run dist
docker-compose -f docker-compose-dev.yaml exec -T frontend npm run dist
```
And then in order for the changes to be visible on the application while served through nginx,
@ -50,7 +82,8 @@ Media page: need to upload content through the main application (nginx/port 80),
There are some issues with CORS too to resolve, in order for some pages to function, eg the manage comments page
```http://localhost:8088/manage-media.html px manage_media
```
http://localhost:8088/manage-media.html manage_media
```
### Backend application changes
@ -81,3 +114,42 @@ there is also an experimental small service (not commited to the repo currently)
When the Encode object is marked as success and chunk=False, and thus is available for download/stream, there is a task that gets started and saves an HLS version of the file (1 mp4-->x number of small .ts chunks). This would be FILES_C
This mechanism allows for workers that have access on the same filesystem (either localhost, or through a shared network filesystem, eg NFS/EFS) to work on the same time and produce results.
## 6. Working with the automated tests
This instructions assume that you're using the docker installation
1. start docker-compose
```
docker-compose up
```
2. Install the requirements on `requirements-dev.txt ` on web container (we'll use the web container for this)
```
docker-compose exec -T web pip install -r requirements-dev.txt
```
3. Now you can run the existing tests
```
docker-compose exec --env TESTING=True -T web pytest
```
The `TESTING=True` is passed for Django to be aware this is a testing environment (so that it runs Celery tasks as functions for example and not as background tasks, since Celery is not started in the case of pytest)
4. You may try a single test, by specifying the path, for example
```
docker-compose exec --env TESTING=True -T web pytest tests/test_fixtures.py
```
5. You can also see the coverage
```
docker-compose exec --env TESTING=True -T web pytest --cov=. --cov-report=html
```
and of course...you are very welcome to help us increase it ;)

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 350 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 51 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 13 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 8.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 7.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 7.2 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 15 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 750 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 91 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 580 KiB

Wyświetl plik

@ -1,55 +0,0 @@
# Google Analytics
1. Create a file:
``` touch $DIR/mediacms/templates/tracking.html ```
2. Add the Gtag/Analytics script
3. Inside ``` $DIR/mediacms/templates/root.html``` you'll see a file like this one:
```
<head>
{% block head %}
<title>{% block headtitle %}{{PORTAL_NAME}}{% endblock headtitle %}</title>
{% include "common/head-meta.html" %}
{% block headermeta %}
<meta property="og:title" content="{{PORTAL_NAME}}">
<meta property="og:type" content="website">
{%endblock headermeta %}
{% block externallinks %}{% endblock externallinks %}
{% include "common/head-links.html" %}
{% block topimports %}{%endblock topimports %}
{% include "config/index.html" %}
{% endblock head %}
</head>
```
4. Add ``` {% include "tracking.html" %} ``` at the end inside the section ```<head>```
5. If you are using Docker and didn't mount the entire dir you need to bind a new volume:
```
web:
image: mediacms/mediacms:latest
restart: unless-stopped
ports:
- "80:80"
deploy:
replicas: 1
volumes:
- ./templates/root.html:/home/mediacms.io/mediacms/templates/root.html
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
```

Wyświetl plik

@ -5,6 +5,9 @@
- [Downloading media](#downloading-media)
- [Adding captions/subtitles](#adding-captionssubtitles)
- [Search media](#search-media)
- [Using Timestamps for sharing](#using-timestamps-for-sharing)
- [Mentionning users in comments](#Mentionning-users-in-comments)
- [Show comments in the Timebar](#Show-comments-in-the-Timebar)
- [Share media](#share-media)
- [Embed media](#embed-media)
- [Customize my profile options](#customize-my-profile-options)
@ -195,6 +198,54 @@ You can now watch the captions/subtitles play back in the video player - and tog
<img src="./images/CC-display.png"/>
</p>
## Using Timestamps for sharing
### Using Timestamp in the URL
An additional GET parameter 't' can be added in video URL's to start the video at the given time. The starting time has to be given in seconds.
<p align="left">
<img src="./images/Demo1.png"/>
</p>
Additionally the share button has an option to generate the URL with the timestamp at current second the video is.
<p align="left">
<img src="./images/Demo2.png"/>
</p>
### Using Timestamp in the comments
Comments can also include timestamps. They are automatically detected upon posting the comment, and will be in the form of an hyperlink link in the comment. The timestamps in the comments have to follow the format HH:MM:SS or MM:SS
<p align="left">
<img src="./images/Demo3.png"/>
</p>
## Mentionning users in comments
Comments can also mention other users by tagging with '@'. This will open suggestion box showing usernames, and the selection will refine as the user continues typing.
Comments send with mentions will contain a link to the user page, and can be setup to send a mail to the mentionned user.
<p align="left">
<img src="./images/Mention1.png"/>
<img src="./images/Mention2.png"/>
<img src="./images/Mention3.png"/>
<img src="./images/Mention4.png"/>
</p>
## Show comments in the Timebar
When enabled, comments including a timestamp will also be displayed in the current video Timebar as a little colorful dot. The comment can be previewed by hovering the dot (left image) and it will be displayed on top of the video when reaching the correct time (right image).
Only comments with correct timestamps formats (HH:MM:SS or MM:SS) will be picked up and appear in the Timebar.
<p align="left">
<img src="./images/TimebarComments_Hover.png" height="180" alt="Comment preview on hover"/>
<img src="./images/TimebarComments_Hit.png" height="180" alt="Comment shown when the timestamp is reached "/>
</p>
## Search media
How search can be used

Wyświetl plik

@ -40,6 +40,12 @@ class MediaAdmin(admin.ModelAdmin):
def get_comments_count(self, obj):
return obj.comments.count()
@admin.action(description="Generate missing encoding(s)", permissions=["change"])
def generate_missing_encodings(modeladmin, request, queryset):
for m in queryset:
m.encode(force=False)
actions = [generate_missing_encodings]
get_comments_count.short_description = "Comments count"
@ -74,7 +80,18 @@ class SubtitleAdmin(admin.ModelAdmin):
class EncodingAdmin(admin.ModelAdmin):
pass
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
list_filter = ["chunk", "profile", "status"]
def get_title(self, obj):
return str(obj)
get_title.short_description = "Encoding"
def has_file(self, obj):
return obj.media_encoding_url is not None
has_file.short_description = "Has file"
admin.site.register(EncodeProfile, EncodeProfileAdmin)

Wyświetl plik

@ -6,13 +6,16 @@ from .methods import is_mediacms_editor, is_mediacms_manager
def stuff(request):
"""Pass settings to the frontend"""
ret = {}
ret["FRONTEND_HOST"] = request.build_absolute_uri('/')
ret["FRONTEND_HOST"] = request.build_absolute_uri('/').rstrip('/')
ret["DEFAULT_THEME"] = settings.DEFAULT_THEME
ret["PORTAL_NAME"] = settings.PORTAL_NAME
ret["PORTAL_DESCRIPTION"] = settings.PORTAL_DESCRIPTION
ret["LOAD_FROM_CDN"] = settings.LOAD_FROM_CDN
ret["CAN_LOGIN"] = settings.LOGIN_ALLOWED
ret["CAN_REGISTER"] = settings.REGISTER_ALLOWED
ret["CAN_UPLOAD_MEDIA"] = settings.UPLOAD_MEDIA_ALLOWED
ret["TIMESTAMP_IN_TIMEBAR"] = settings.TIMESTAMP_IN_TIMEBAR
ret["CAN_MENTION_IN_COMMENTS"] = settings.ALLOW_MENTION_IN_COMMENTS
ret["CAN_LIKE_MEDIA"] = settings.CAN_LIKE_MEDIA
ret["CAN_DISLIKE_MEDIA"] = settings.CAN_DISLIKE_MEDIA
ret["CAN_REPORT_MEDIA"] = settings.CAN_REPORT_MEDIA

Wyświetl plik

@ -102,7 +102,7 @@ class SearchRSSFeed(Feed):
description = "Latest Media RSS feed"
def link(self, obj):
return f"/rss/search"
return "/rss/search"
def get_object(self, request):
category = request.GET.get("c", "")

Wyświetl plik

@ -3,7 +3,6 @@
import hashlib
import json
import math
import os
import random
import shutil
@ -23,6 +22,8 @@ CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
# you should use CRF encoding.
MAX_RATE_MULTIPLIER = 1.5
MIN_RATE_MULTIPLIER = 0.5
BUF_SIZE_MULTIPLIER = 1.5
# in seconds, anything between 2 and 6 makes sense
@ -238,10 +239,12 @@ def media_file_info(input_file):
- `filename`: Filename
- `file_size`: Size of the file in bytes
- `video_duration`: Duration of the video in `s.msec`
- `video_frame_rate`: Framerate in Hz
- `video_frame_rate_d`: Framerate franction denominator
- `video_frame_rate_n`: Framerate fraction nominator
- `video_bitrate`: Bitrate of the video stream in kBit/s
- `video_width`: Width in pixels
- `video_height`: Height in pixels
- `interlaced` : True if the video is interlaced
- `video_codec`: Video codec
- `audio_duration`: Duration of the audio in `s.msec`
- `audio_sample_rate`: Audio sample rate in Hz
@ -327,7 +330,7 @@ def media_file_info(input_file):
except ValueError:
hms, msec = duration_str.split(",")
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
video_duration = total_dur + float("0." + msec)
else:
# fallback to format, eg for webm
@ -367,17 +370,34 @@ def media_file_info(input_file):
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
if "r_frame_rate" in video_info.keys():
video_frame_rate = video_info["r_frame_rate"].partition("/")
video_frame_rate_n = video_frame_rate[0]
video_frame_rate_d = video_frame_rate[2]
interlaced = False
if video_info.get("field_order") in ("tt", "tb", "bt", "bb"):
interlaced = True
ret = {
"filename": input_file,
"file_size": file_size,
"video_duration": video_duration,
"video_frame_rate": float(Fraction(video_info["r_frame_rate"])),
"video_frame_rate_n": video_frame_rate_n,
"video_frame_rate_d": video_frame_rate_d,
"video_bitrate": video_bitrate,
"video_width": video_info["width"],
"video_height": video_info["height"],
"video_codec": video_info["codec_name"],
"has_video": has_video,
"has_audio": has_audio,
"color_range": video_info.get("color_range"),
"color_space": video_info.get("color_space"),
"color_transfer": video_info.get("color_space"),
"color_primaries": video_info.get("color_primaries"),
"interlaced": interlaced,
"display_aspect_ratio": video_info.get("display_aspect_ratio"),
"sample_aspect_ratio": video_info.get("sample_aspect_ratio"),
}
if has_audio:
@ -389,7 +409,7 @@ def media_file_info(input_file):
hms, msec = duration_str.split(".")
except ValueError:
hms, msec = duration_str.split(",")
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
audio_duration = total_dur + float("0." + msec)
else:
# fallback to format, eg for webm
@ -423,7 +443,8 @@ def media_file_info(input_file):
input_file,
]
stdout = run_command(cmd).get("out")
stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
# ffprobe appends a pipe at the end of the output, thus we have to remove it
stream_size = sum([int(line.replace("|", "")) for line in stdout.split("\n") if line != ""])
audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)
ret.update(
@ -475,6 +496,7 @@ def get_base_ffmpeg_command(
encoder,
audio_encoder,
target_fps,
interlaced,
target_height,
target_rate,
target_rate_audio,
@ -492,7 +514,8 @@ def get_base_ffmpeg_command(
codec {str} -- video codec
encoder {str} -- video encoder
audio_encoder {str} -- audio encoder
target_fps {int} -- target FPS
target_fps {fractions.Fraction} -- target FPS
interlaced {bool} -- true if interlaced
target_height {int} -- height
target_rate {int} -- target bitrate in kbps
target_rate_audio {int} -- audio target bitrate
@ -501,10 +524,33 @@ def get_base_ffmpeg_command(
enc_type {str} -- encoding type (twopass or crf)
"""
target_fps = int(target_fps)
# avoid Frame rate very high for a muxer not efficiently supporting it.
if target_fps > 90:
target_fps = 90
# avoid very high frame rates
while target_fps > 60:
target_fps = target_fps / 2
if target_fps < 1:
target_fps = 1
filters = []
if interlaced:
filters.append("yadif")
target_width = round(target_height * 16 / 9)
scale_filter_opts = [
f"if(lt(iw\\,ih)\\,{target_height}\\,{target_width})", # noqa
f"if(lt(iw\\,ih)\\,{target_width}\\,{target_height})", # noqa
"force_original_aspect_ratio=decrease",
"force_divisible_by=2",
"flags=lanczos",
]
scale_filter_str = "scale=" + ":".join(scale_filter_opts)
filters.append(scale_filter_str)
fps_str = f"fps=fps={target_fps}"
filters.append(fps_str)
filters_str = ",".join(filters)
base_cmd = [
settings.FFMPEG_COMMAND,
@ -514,9 +560,7 @@ def get_base_ffmpeg_command(
"-c:v",
encoder,
"-filter:v",
"scale=-2:" + str(target_height) + ",fps=fps=" + str(target_fps),
# always convert to 4:2:0 -- FIXME: this could be also 4:2:2
# but compatibility will suffer
filters_str,
"-pix_fmt",
"yuv420p",
]
@ -622,6 +666,8 @@ def get_base_ffmpeg_command(
str(keyframe_distance),
"-maxrate",
str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
"-minrate",
str(int(int(target_rate) * MIN_RATE_MULTIPLIER)) + "k",
"-bufsize",
str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
"-speed",
@ -669,8 +715,8 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
else:
return False
src_framerate = media_info.get("video_frame_rate", 30)
if src_framerate <= 30:
target_fps = Fraction(int(media_info.get("video_frame_rate_n", 30)), int(media_info.get("video_frame_rate_d", 1)))
if target_fps <= 30:
target_rate = VIDEO_BITRATES[codec][25].get(resolution)
else:
target_rate = VIDEO_BITRATES[codec][60].get(resolution)
@ -687,9 +733,6 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
# target_fps = 25
# else:
# adjust the target frame rate if the input is fractional
target_fps = src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
enc_type = "crf"
else:
@ -700,6 +743,8 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
elif enc_type == "crf":
passes = [2]
interlaced = media_info.get("interlaced")
cmds = []
for pass_number in passes:
cmds.append(
@ -711,6 +756,7 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
encoder=encoder,
audio_encoder=AUDIO_ENCODERS[codec],
target_fps=target_fps,
interlaced=interlaced,
target_height=resolution,
target_rate=target_rate,
target_rate_audio=AUDIO_BITRATES[codec],
@ -739,3 +785,11 @@ def clean_query(query):
query = query.replace(char, "")
return query.lower()
def get_alphanumeric_only(string):
"""Returns a query that contains only alphanumeric characters
This include characters other than the English alphabet too
"""
string = "".join([char for char in string if char.isalnum()])
return string.lower()

Wyświetl plik

@ -4,6 +4,7 @@
import itertools
import logging
import random
import re
from datetime import datetime
from django.conf import settings
@ -304,7 +305,6 @@ def show_related_media_author(media, request, limit):
def show_related_media_calculated(media, request, limit):
"""Return a list of related media based on ML recommendations
A big todo!
"""
@ -324,8 +324,6 @@ def update_user_ratings(user, media, user_ratings):
def notify_user_on_comment(friendly_token):
"""Notify users through email, for a set of actions"""
media = None
media = models.Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return False
@ -347,6 +345,55 @@ View it on %s
return True
def notify_user_on_mention(friendly_token, user_mentioned, cleaned_comment):
from users.models import User
media = models.Media.objects.filter(friendly_token=friendly_token).first()
if not media:
return False
user = User.objects.filter(username=user_mentioned).first()
media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
if user.notification_on_comments:
title = "[{}] - You were mentioned in a comment".format(settings.PORTAL_NAME)
msg = """
You were mentioned in a comment on %s .
View it on %s
Comment : %s
""" % (
media.title,
media_url,
cleaned_comment,
)
email = EmailMessage(title, msg, settings.DEFAULT_FROM_EMAIL, [user.email])
email.send(fail_silently=True)
return True
def check_comment_for_mention(friendly_token, comment_text):
"""Check the comment for any mentions, and notify each mentioned users"""
cleaned_comment = ''
matches = re.findall('@\\(_(.+?)_\\)', comment_text)
if matches:
cleaned_comment = clean_comment(comment_text)
for match in list(dict.fromkeys(matches)):
notify_user_on_mention(friendly_token, match, cleaned_comment)
def clean_comment(raw_comment):
"""Clean the comment fromn ID and username Mentions for preview purposes"""
cleaned_comment = re.sub('@\\(_(.+?)_\\)', '', raw_comment)
cleaned_comment = cleaned_comment.replace("[_", '')
cleaned_comment = cleaned_comment.replace("_]", '')
return cleaned_comment
def list_tasks():
"""Lists celery tasks
To be used in an admin dashboard

Wyświetl plik

@ -10,7 +10,6 @@ import files.models
class Migration(migrations.Migration):
initial = True
dependencies = []

Wyświetl plik

@ -8,7 +8,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

Wyświetl plik

@ -0,0 +1,17 @@
# Generated by Django 3.1.12 on 2021-09-27 11:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0002_auto_20201201_0712'),
]
operations = [
migrations.AlterField(
model_name='media',
name='reported_times',
field=models.IntegerField(default=0, help_text='how many time a media is reported'),
),
]

Wyświetl plik

@ -1,3 +1,4 @@
import glob
import json
import logging
import os
@ -15,7 +16,6 @@ from django.core.files import File
from django.db import connection, models
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
from django.dispatch import receiver
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils import timezone
from django.utils.html import strip_tags
@ -209,7 +209,7 @@ class Media(models.Model):
help_text="Rating category, if media Rating is allowed",
)
reported_times = models.IntegerField(default=0, help_text="how many time a Medis is reported")
reported_times = models.IntegerField(default=0, help_text="how many time a media is reported")
search = SearchVectorField(
null=True,
@ -313,7 +313,6 @@ class Media(models.Model):
self.__original_uploaded_poster = self.uploaded_poster
def save(self, *args, **kwargs):
if not self.title:
self.title = self.media_file.path.split("/")[-1]
@ -371,7 +370,6 @@ class Media(models.Model):
# will run only when a poster is uploaded for the first time
if self.uploaded_poster and self.uploaded_poster != self.__original_uploaded_poster:
with open(self.uploaded_poster.path, "rb") as f:
# set this otherwise gets to infinite loop
self.__original_uploaded_poster = self.uploaded_poster
@ -395,11 +393,11 @@ class Media(models.Model):
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
items = [
helpers.clean_query(self.title),
self.title,
self.user.username,
self.user.email,
self.user.name,
helpers.clean_query(self.description),
self.description,
a_tags,
b_tags,
]
@ -407,6 +405,8 @@ class Media(models.Model):
text = " ".join(items)
text = " ".join([token for token in text.lower().split(" ") if token not in STOP_WORDS])
text = helpers.clean_query(text)
sql_code = """
UPDATE {db_table} SET search = to_tsvector(
'{config}', '{text}'
@ -427,12 +427,16 @@ class Media(models.Model):
Performs all related tasks, as check for media type,
video duration, encode
"""
self.set_media_type()
if self.media_type == "video":
self.set_thumbnail(force=True)
self.produce_sprite_from_video()
self.encode()
if settings.DO_NOT_TRANSCODE_VIDEO:
self.encoding_status = "success"
self.save()
self.produce_sprite_from_video()
else:
self.produce_sprite_from_video()
self.encode()
elif self.media_type == "image":
self.set_thumbnail(force=True)
return True
@ -442,7 +446,6 @@ class Media(models.Model):
Set encoding_status as success for non video
content since all listings filter for encoding_status success
"""
kind = helpers.get_file_type(self.media_file.path)
if kind is not None:
if kind == "image":
@ -450,7 +453,7 @@ class Media(models.Model):
elif kind == "pdf":
self.media_type = "pdf"
if self.media_type in ["image", "pdf"]:
if self.media_type in ["audio", "image", "pdf"]:
self.encoding_status = "success"
else:
ret = helpers.media_file_info(self.media_file.path)
@ -468,13 +471,22 @@ class Media(models.Model):
self.media_type = ""
self.encoding_status = "fail"
audio_file_with_thumb = False
# handle case where a file identified as video is actually an
# audio file with thumbnail
if ret.get("is_video"):
# case where Media is video. try to set useful
# metadata as duration/height
self.media_type = "video"
self.duration = int(round(float(ret.get("video_duration", 0))))
self.video_height = int(ret.get("video_height"))
elif ret.get("is_audio"):
if ret.get("video_info", {}).get("codec_name", {}) in ["mjpeg"]:
# best guess that this is an audio file with a thumbnail
# in other cases, it is not (eg it can be an AVI file)
if ret.get("video_info", {}).get("avg_frame_rate", "") == '0/0':
audio_file_with_thumb = True
if ret.get("is_audio") or audio_file_with_thumb:
self.media_type = "audio"
self.duration = int(float(ret.get("audio_info", {}).get("duration", 0)))
self.encoding_status = "success"
@ -572,9 +584,7 @@ class Media(models.Model):
# attempt to break media file in chunks
if self.duration > settings.CHUNKIZE_VIDEO_DURATION and chunkize:
for profile in profiles:
if profile.extension == "gif":
profiles.remove(profile)
encoding = Encoding(media=self, profile=profile)
@ -635,15 +645,16 @@ class Media(models.Model):
def set_encoding_status(self):
"""Set encoding_status for videos
Set success if at least one mp4 exists
Set success if at least one mp4 or webm exists
"""
mp4_statuses = set(encoding.status for encoding in self.encodings.filter(profile__extension="mp4", chunk=False))
webm_statuses = set(encoding.status for encoding in self.encodings.filter(profile__extension="webm", chunk=False))
if not mp4_statuses:
if not mp4_statuses and not webm_statuses:
encoding_status = "pending"
elif "success" in mp4_statuses:
elif "success" in mp4_statuses or "success" in webm_statuses:
encoding_status = "success"
elif "running" in mp4_statuses:
elif "running" in mp4_statuses or "running" in webm_statuses:
encoding_status = "running"
else:
encoding_status = "fail"
@ -661,6 +672,13 @@ class Media(models.Model):
return ret
for key in ENCODE_RESOLUTIONS_KEYS:
ret[key] = {}
# if this is enabled, return original file on a way
# that video.js can consume
if settings.DO_NOT_TRANSCODE_VIDEO:
ret['0-original'] = {"h264": {"url": helpers.url_from_path(self.media_file.path), "status": "success", "progress": 100}}
return ret
for encoding in self.encodings.select_related("profile").filter(chunk=False):
if encoding.profile.extension == "gif":
continue
@ -812,6 +830,7 @@ class Media(models.Model):
"""
res = {}
valid_resolutions = [240, 360, 480, 720, 1080, 1440, 2160]
if self.hls_file:
if os.path.exists(self.hls_file):
hls_file = self.hls_file
@ -823,11 +842,20 @@ class Media(models.Model):
uri = os.path.join(p, iframe_playlist.uri)
if os.path.exists(uri):
resolution = iframe_playlist.iframe_stream_info.resolution[1]
# most probably video is vertical, getting the first value to
# be the resolution
if resolution not in valid_resolutions:
resolution = iframe_playlist.iframe_stream_info.resolution[0]
res["{}_iframe".format(resolution)] = helpers.url_from_path(uri)
for playlist in m3u8_obj.playlists:
uri = os.path.join(p, playlist.uri)
if os.path.exists(uri):
resolution = playlist.stream_info.resolution[1]
# same as above
if resolution not in valid_resolutions:
resolution = playlist.stream_info.resolution[0]
res["{}_playlist".format(resolution)] = helpers.url_from_path(uri)
return res
@ -995,10 +1023,8 @@ class Tag(models.Model):
return True
def save(self, *args, **kwargs):
self.title = slugify(self.title[:99])
strip_text_items = ["title"]
for item in strip_text_items:
setattr(self, item, strip_tags(getattr(self, item, None)))
self.title = helpers.get_alphanumeric_only(self.title)
self.title = self.title[:99]
super(Tag, self).save(*args, **kwargs)
@property
@ -1397,6 +1423,13 @@ def media_file_delete(sender, instance, **kwargs):
helpers.rm_dir(p)
instance.user.update_user_media()
# remove extra zombie thumbnails
if instance.thumbnail:
thumbnails_path = os.path.dirname(instance.thumbnail.path)
thumbnails = glob.glob(f'{thumbnails_path}/{instance.uid.hex}.*')
for thumbnail in thumbnails:
helpers.rm_file(thumbnail)
@receiver(m2m_changed, sender=Media.category.through)
def media_m2m(sender, instance, **kwargs):

Wyświetl plik

@ -150,10 +150,14 @@ class SingleMediaSerializer(serializers.ModelSerializer):
class MediaSearchSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
api_url = serializers.SerializerMethodField()
def get_url(self, obj):
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
def get_api_url(self, obj):
return self.context["request"].build_absolute_uri(obj.get_absolute_url(api=True))
class Meta:
model = Media
fields = (
@ -167,6 +171,7 @@ class MediaSearchSerializer(serializers.ModelSerializer):
"friendly_token",
"duration",
"url",
"api_url",
"media_type",
"preview_url",
"categories_info",

Wyświetl plik

@ -7,10 +7,11 @@ import tempfile
from datetime import datetime, timedelta
from celery import Task
from celery.decorators import task
from celery import shared_task as task
from celery.exceptions import SoftTimeLimitExceeded
from celery.signals import task_revoked
from celery.task.control import revoke
# 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
@ -268,7 +269,6 @@ def encode_media(
# return False
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
tf = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
tfpass = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
ffmpeg_commands = produce_ffmpeg_commands(
@ -461,10 +461,11 @@ def check_running_states():
if (now - encoding.update_date).seconds > settings.RUNNING_STATE_STALE:
media = encoding.media
profile = encoding.profile
task_id = encoding.task_id
# task_id = encoding.task_id
# terminate task
if task_id:
revoke(task_id, terminate=True)
# TODO: not imported
# if task_id:
# revoke(task_id, terminate=True)
encoding.delete()
media.encode(profiles=[profile])
# TODO: allign with new code + chunksize...
@ -643,7 +644,11 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
if action == "watch":
media.views += 1
media.save(update_fields=["views"])
Media.objects.filter(friendly_token=friendly_token).update(views=media.views)
# update field without calling save, to avoid post_save signals being triggered
# same in other actions
elif action == "report":
media.reported_times += 1
@ -658,10 +663,10 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
)
elif action == "like":
media.likes += 1
media.save(update_fields=["likes"])
Media.objects.filter(friendly_token=friendly_token).update(likes=media.likes)
elif action == "dislike":
media.dislikes += 1
media.save(update_fields=["dislikes"])
Media.objects.filter(friendly_token=friendly_token).update(dislikes=media.dislikes)
return True

Wyświetl plik

Wyświetl plik

@ -0,0 +1 @@
from .user_utils import create_account # noqa

Wyświetl plik

@ -0,0 +1,24 @@
from faker import Factory
from users.models import User
faker = Factory.create()
def create_account(username=None, email=None, password=None, name=None, **kwargs):
"Allow to create accounts by passing None or specific arguements"
email = email or faker.email()
username = username or email.split('@')[0]
password = password or faker.password()
name = name or faker.name()
description = kwargs.get('description') or faker.text()
is_superuser = kwargs.get('is_superuser') or False
is_manager = kwargs.get('is_manager') or False
is_editor = kwargs.get('is_editor') or False
user = User.objects.create(username=username, email=email, name=name, description=description, is_superuser=is_superuser, is_staff=is_superuser, is_editor=is_editor, is_manager=is_manager)
user.set_password(password)
user.save()
return user

Wyświetl plik

@ -1,91 +1,94 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls import include
from django.conf.urls.static import static
from django.urls import path
from django.urls import path, re_path
from . import management_views, views
from .feeds import IndexRSSFeed, SearchRSSFeed
urlpatterns = [
url(r"^$", views.index),
url(r"^about", views.about, name="about"),
url(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
url(r"^categories$", views.categories, name="categories"),
url(r"^contact$", views.contact, name="contact"),
url(r"^edit", views.edit_media, name="edit_media"),
url(r"^embed", views.embed_media, name="get_embed"),
url(r"^featured$", views.featured_media),
url(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
url(r"^history$", views.history, name="history"),
url(r"^liked$", views.liked_media, name="liked_media"),
url(r"^latest$", views.latest_media),
url(r"^members", views.members, name="members"),
url(
re_path(r"^$", views.index),
re_path(r"^about", views.about, name="about"),
re_path(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
re_path(r"^categories$", views.categories, name="categories"),
re_path(r"^contact$", views.contact, name="contact"),
re_path(r"^edit", views.edit_media, name="edit_media"),
re_path(r"^embed", views.embed_media, name="get_embed"),
re_path(r"^featured$", views.featured_media),
re_path(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
re_path(r"^history$", views.history, name="history"),
re_path(r"^liked$", views.liked_media, name="liked_media"),
re_path(r"^latest$", views.latest_media),
re_path(r"^members", views.members, name="members"),
re_path(
r"^playlist/(?P<friendly_token>[\w]*)$",
views.view_playlist,
name="get_playlist",
),
url(
re_path(
r"^playlists/(?P<friendly_token>[\w]*)$",
views.view_playlist,
name="get_playlist",
),
url(r"^popular$", views.recommended_media),
url(r"^recommended$", views.recommended_media),
re_path(r"^popular$", views.recommended_media),
re_path(r"^recommended$", views.recommended_media),
path("rss/", IndexRSSFeed()),
url("^rss/search", SearchRSSFeed()),
url(r"^search", views.search, name="search"),
url(r"^scpublisher", views.upload_media, name="upload_media"),
url(r"^tags", views.tags, name="tags"),
url(r"^tos$", views.tos, name="terms_of_service"),
url(r"^view", views.view_media, name="get_media"),
url(r"^upload", views.upload_media, name="upload_media"),
re_path("^rss/search", SearchRSSFeed()),
re_path(r"^search", views.search, name="search"),
re_path(r"^scpublisher", views.upload_media, name="upload_media"),
re_path(r"^tags", views.tags, name="tags"),
re_path(r"^tos$", views.tos, name="terms_of_service"),
re_path(r"^view", views.view_media, name="get_media"),
re_path(r"^upload", views.upload_media, name="upload_media"),
# API VIEWS
url(r"^api/v1/media$", views.MediaList.as_view()),
url(r"^api/v1/media/$", views.MediaList.as_view()),
url(
re_path(r"^api/v1/media$", views.MediaList.as_view()),
re_path(r"^api/v1/media/$", views.MediaList.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)$",
views.MediaDetail.as_view(),
name="api_get_media",
),
url(
re_path(
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
views.EncodingDetail.as_view(),
name="api_get_encoding",
),
url(r"^api/v1/search$", views.MediaSearch.as_view()),
url(
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/actions$",
views.MediaActions.as_view(),
),
url(r"^api/v1/categories$", views.CategoryList.as_view()),
url(r"^api/v1/tags$", views.TagList.as_view()),
url(r"^api/v1/comments$", views.CommentList.as_view()),
url(
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
re_path(r"^api/v1/tags$", views.TagList.as_view()),
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments$",
views.CommentDetail.as_view(),
),
url(
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments/(?P<uid>[\w-]*)$",
views.CommentDetail.as_view(),
),
url(r"^api/v1/playlists$", views.PlaylistList.as_view()),
url(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
url(
re_path(r"^api/v1/playlists$", views.PlaylistList.as_view()),
re_path(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
re_path(
r"^api/v1/playlists/(?P<friendly_token>[\w]*)$",
views.PlaylistDetail.as_view(),
name="api_get_playlist",
),
url(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
re_path(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
# ADMIN VIEWS
url(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
url(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
url(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
url(r"^api/v1/manage_users$", management_views.UserList.as_view()),
url(r"^api/v1/tasks$", views.TasksList.as_view()),
url(r"^api/v1/tasks/$", views.TasksList.as_view()),
url(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
url(r"^manage/comments$", views.manage_comments, name="manage_comments"),
url(r"^manage/media$", views.manage_media, name="manage_media"),
url(r"^manage/users$", views.manage_users, name="manage_users"),
re_path(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
re_path(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
re_path(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
re_path(r"^api/v1/manage_users$", management_views.UserList.as_view()),
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if hasattr(settings, "GENERATE_SITEMAP") and settings.GENERATE_SITEMAP:
urlpatterns.append(path("sitemap.xml", views.sitemap, name="sitemap"))

Wyświetl plik

@ -1,6 +1,5 @@
from datetime import datetime, timedelta
from celery.task.control import revoke
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -9,7 +8,6 @@ from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import slugify
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
@ -30,8 +28,9 @@ from cms.permissions import IsAuthorizedToAdd, IsUserOrEditor, user_allowed_to_u
from users.models import User
from .forms import ContactForm, MediaForm, SubtitleForm
from .helpers import clean_query, produce_ffmpeg_commands
from .helpers import clean_query, get_alphanumeric_only, produce_ffmpeg_commands
from .methods import (
check_comment_for_mention,
get_user_or_session,
is_mediacms_editor,
is_mediacms_manager,
@ -181,7 +180,8 @@ def edit_media(request):
media.tags.remove(tag)
if form.cleaned_data.get("new_tags"):
for tag in form.cleaned_data.get("new_tags").split(","):
tag = slugify(tag)
tag = get_alphanumeric_only(tag)
tag = tag[:99]
if tag:
try:
tag = Tag.objects.get(title=tag)
@ -292,6 +292,16 @@ def search(request):
return render(request, "cms/search.html", context)
def sitemap(request):
"""Sitemap"""
context = {}
context["media"] = list(Media.objects.filter(Q(listable=True)).order_by("-add_date"))
context["playlists"] = list(Playlist.objects.filter().order_by("-add_date"))
context["users"] = list(User.objects.filter())
return render(request, "sitemap.xml", context, content_type="application/xml")
def tags(request):
"""List tags view"""
@ -1277,6 +1287,9 @@ class CommentDetail(APIView):
serializer.save(user=request.user, media=media)
if request.user != media.user:
notify_user_on_comment(friendly_token=media.friendly_token)
# here forward the comment to check if a user was mentioned
if settings.ALLOW_MENTION_IN_COMMENTS:
check_comment_for_mention(friendly_token=media.friendly_token, comment_text=serializer.data['text'])
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -1392,5 +1405,6 @@ class TaskDetail(APIView):
permission_classes = (permissions.IsAdminUser,)
def delete(self, request, uid, format=None):
revoke(uid, terminate=True)
# This is not imported!
# revoke(uid, terminate=True)
return Response(status=status.HTTP_204_NO_CONTENT)

Wyświetl plik

@ -1 +1 @@
[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}]
[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 22, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}]

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 62 KiB

Wyświetl plik

@ -20,11 +20,11 @@ const formatPage = (page) => {
? templates.renderPageContent({ page: { id: pageContentId, component: page.component } })
: undefined;
const headLinks = [
{ rel: 'preload', href: './static/lib/video-js/7.7.5/video.min.js', as: 'script' },
{ rel: 'preload', href: './static/lib/video-js/7.20.2/video.min.js', as: 'script' },
...(page.headLinks ? page.headLinks : []),
];
const bodyScripts = [
{ src: './static/lib/video-js/7.7.5/video.min.js' },
{ src: './static/lib/video-js/7.20.2/video.min.js' },
...(page.bodyScripts ? page.bodyScripts : []),
];

28338
frontend/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -46,6 +46,7 @@
"normalize.css": "^8.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-mentions": "^4.3.1",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.1"

Wyświetl plik

@ -94,6 +94,7 @@ body.dark_theme {
--comment-date-hover-text-color: #fff;
--comment-text-color: rgba(255, 255, 255, 0.88);
--comment-text-mentions-background-color-highlight:#006622;
--comment-actions-material-icon-text-color: rgba(255, 255, 255, 0.74);

Wyświetl plik

@ -94,6 +94,7 @@ body {
--comment-date-hover-text-color: #0a0a0a;
--comment-text-color: #111;
--comment-text-mentions-background-color-highlight:#00cc44;
--comment-actions-material-icon-text-color: rgba(17, 17, 17, 0.8);

Wyświetl plik

@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { MentionsInput, Mention } from 'react-mentions';
import PropTypes from 'prop-types';
import { format } from 'timeago.js';
import { usePopup } from '../../utils/hooks/';
@ -7,6 +8,10 @@ import { PageActions, MediaPageActions } from '../../utils/actions/';
import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/';
import { PopupMain, UserThumbnail } from '../_shared';
import './videojs-markers.js';
import './videojs.markers.css';
import {enableMarkers, addMarker} from './videojs-markers_config.js'
import './Comments.scss';
const commentsText = {
@ -25,6 +30,7 @@ function CommentForm(props) {
const [madeChanges, setMadeChanges] = useState(false);
const [textareaFocused, setTextareaFocused] = useState(false);
const [textareaLineHeight, setTextareaLineHeight] = useState(-1);
const [userList, setUsersList] = useState('');
const [loginUrl] = useState(
!MemberContext._currentValue.is.anonymous
@ -42,6 +48,17 @@ function CommentForm(props) {
setTextareaFocused(false);
}
function onUsersLoad()
{
const userList =[...MediaPageStore.get('users')];
const cleanList = []
userList.forEach(user => {
cleanList.push({id : user.username, display : user.name});
});
setUsersList(cleanList);
}
function onCommentSubmit() {
textareaRef.current.style.height = '';
@ -61,6 +78,21 @@ function CommentForm(props) {
setMadeChanges(false);
}
function onChangeWithMention(event, newValue, newPlainTextValue, mentions) {
textareaRef.current.style.height = '';
setValue(newValue);
setMadeChanges(true);
const contentHeight = textareaRef.current.scrollHeight;
const contentLineHeight =
0 < textareaLineHeight ? textareaLineHeight : parseFloat(window.getComputedStyle(textareaRef.current).lineHeight);
setTextareaLineHeight(contentLineHeight);
textareaRef.current.style.height =
Math.max(20, textareaLineHeight * Math.ceil(contentHeight / contentLineHeight)) + 'px';
}
function onChange(event) {
textareaRef.current.style.height = '';
@ -81,7 +113,7 @@ function CommentForm(props) {
return;
}
const val = textareaRef.current.value.trim();
const val = value.trim();
if ('' !== val) {
MediaPageActions.submitComment(val);
@ -91,10 +123,18 @@ function CommentForm(props) {
useEffect(() => {
MediaPageStore.on('comment_submit', onCommentSubmit);
MediaPageStore.on('comment_submit_fail', onCommentSubmitFail);
if (MediaCMS.features.media.actions.comment_mention === true)
{
MediaPageStore.on('users_load', onUsersLoad);
}
return () => {
MediaPageStore.removeListener('comment_submit', onCommentSubmit);
MediaPageStore.removeListener('comment_submit_fail', onCommentSubmitFail);
if (MediaCMS.features.media.actions.comment_mention === true)
{
MediaPageStore.removeListener('users_load', onUsersLoad);
}
};
});
@ -104,16 +144,33 @@ function CommentForm(props) {
<UserThumbnail />
<div className="form">
<div className={'form-textarea-wrap' + (textareaFocused ? ' focused' : '')}>
<textarea
ref={textareaRef}
className="form-textarea"
rows="1"
placeholder={'Add a ' + commentsText.single + '...'}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
></textarea>
{ MediaCMS.features.media.actions.comment_mention ?
<MentionsInput
inputRef={textareaRef}
className="form-textarea"
rows="1"
placeholder={'Add a ' + commentsText.single + '...'}
value={value}
onChange={onChangeWithMention}
onFocus={onFocus}
onBlur={onBlur}>
<Mention
data={userList}
markup="@(___id___)[___display___]"
/>
</MentionsInput>
:
<textarea
ref={textareaRef}
className="form-textarea"
rows="1"
placeholder={'Add a ' + commentsText.single + '...'}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
></textarea>
}
</div>
<div className="form-buttons">
<button className={'' === value.trim() ? 'disabled' : ''} onClick={submitComment}>
@ -236,6 +293,10 @@ function Comment(props) {
};
}, []);
function parseComment(text) {
return { __html: text.replace(/\n/g, `<br />`) };
}
return (
<div className="comment">
<div className="comment-inner">
@ -255,7 +316,7 @@ function Comment(props) {
<div
ref={commentTextInnerRef}
className="comment-text-inner"
dangerouslySetInnerHTML={{ __html: props.text }}
dangerouslySetInnerHTML={parseComment(props.text)}
></div>
</div>
{enabledViewMoreContent ? (
@ -368,8 +429,69 @@ export default function CommentsList(props) {
const [displayComments, setDisplayComments] = useState(false);
function onCommentsLoad() {
displayCommentsRelatedAlert();
setComments([...MediaPageStore.get('media-comments')]);
const retrievedComments = [...MediaPageStore.get('media-comments')];
setComments([...retrievedComments]);
// TODO: this code is breaking, beed ti debug, until then removing the extra
// functionality related with video/timestamp/user mentions
// const video = videojs('vjs_video_3');
// if (MediaCMS.features.media.actions.timestampTimebar)
//{
// enableMarkers(video);
//}
//if (MediaCMS.features.media.actions.comment_mention === true)
//{
// retrievedComments.forEach(comment => {
// comment.text = setMentions(comment.text);
// });
//}
// TODO: this code is breaking
// video.one('loadedmetadata', () => {
// retrievedComments.forEach(comment => {
// comment.text = setTimestampAnchorsAndMarkers(comment.text, video);
// });
// displayCommentsRelatedAlert();
// setComments([...retrievedComments]);
//});
//setComments([...retrievedComments]);
}
function setMentions(text)
{
let sanitizedComment = text.split('@(_').join("<a href=\"/user/");
sanitizedComment = sanitizedComment.split('_)[_').join("\">");
return sanitizedComment.split('_]').join("</a>");
}
function setTimestampAnchorsAndMarkers(text, videoPlayer)
{
function wrapTimestampWithAnchor(match, string)
{
let split = match.split(':'), s = 0, m = 1;
let searchParameters = new URLSearchParams(window.location.search);
while (split.length > 0)
{
s += m * parseInt(split.pop(), 10);
m *= 60;
}
if (MediaCMS.features.media.actions.timestampTimebar)
{
addMarker(videoPlayer, s, text);
}
searchParameters.set('t', s)
const wrapped = "<a href=\"" + MediaPageStore.get('media-url').split('?')[0] + "?" + searchParameters + "\">" + match + "</a>";
return wrapped;
}
const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g');
return text.replace(timeRegex , wrapTimestampWithAnchor);
}
function onCommentSubmit(commentId) {

Wyświetl plik

@ -2,7 +2,7 @@
.comments-form-inner {
.form {
.form-textarea-wrap {
.form-textarea-wrap{
border-color: var(--comments-textarea-wrapper-border-color);
&:after {
@ -10,13 +10,20 @@
}
}
.form-textarea {
.form-textarea, .form-textarea__input, .form-textarea__suggestions__list {
color: var(--comments-textarea-text-color);
&:placeholder {
color: var(--comments-textarea-text-placeholder-color);
}
}
.form-textarea__suggestions__list {
background-color: var(--body-bg-color);
}
strong {
background-color: var(--comment-text-mentions-background-color-highlight)
}
}
}
@ -160,7 +167,7 @@
}
}
.form-textarea {
.form-textarea, .form-textarea__input {
position: relative;
resize: none;
display: block;
@ -204,7 +211,7 @@
text-decoration: none;
.form-textarea {
.form-textarea, .form-textarea__input {
opacity: 0.5;
}
}

Wyświetl plik

@ -0,0 +1,525 @@
// based on https://github.com/spchuang/videojs-markers
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['mediacms-player'], factory);
} else if (typeof exports !== "undefined") {
factory(require('mediacms-player'));
} else {
var mod = {
exports: {}
};
global = window;
factory(global.videojs);
global.videojsMarkers = mod.exports;
}
})(this, function (_video) {
/*! videojs-markers - v1.0.1 - 2018-02-03
* Copyright (c) 2018 ; Licensed */
'use strict';
var _video2 = _interopRequireDefault(_video);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
// default setting
var defaultSetting = {
markerStyle: {
'width': '7px',
'border-radius': '30%',
'background-color': 'red'
},
markerTip: {
display: true,
text: function text(marker) {
return "Break: " + marker.text;
},
time: function time(marker) {
return marker.time;
}
},
breakOverlay: {
display: true,
displayTime: 3,
text: function text(marker) {
return "Break overlay: " + marker.overlayText;
},
style: {
'width': '100%',
'height': '20%',
'background-color': 'rgba(0,0,0,0.7)',
'color': 'white',
'font-size': '17px'
}
},
onMarkerClick: function onMarkerClick(marker) {},
onMarkerReached: function onMarkerReached(marker, index) {},
markers: []
};
// create a non-colliding random number
function generateUUID() {
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : r & 0x3 | 0x8).toString(16);
});
return uuid;
};
/**
* Returns the size of an element and its position
* a default Object with 0 on each of its properties
* its return in case there's an error
* @param {Element} element el to get the size and position
* @return {DOMRect|Object} size and position of an element
*/
function getElementBounding(element) {
var elementBounding;
var defaultBoundingRect = {
top: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
right: 0
};
try {
elementBounding = element.getBoundingClientRect();
} catch (e) {
elementBounding = defaultBoundingRect;
}
return elementBounding;
}
var NULL_INDEX = -1;
function registerVideoJsMarkersPlugin(options) {
// copied from video.js/src/js/utils/merge-options.js since
// videojs 4 doens't support it by defualt.
if (!_video2.default.mergeOptions) {
var isPlain = function isPlain(value) {
return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && toString.call(value) === '[object Object]' && value.constructor === Object;
};
var mergeOptions = function mergeOptions(source1, source2) {
var result = {};
var sources = [source1, source2];
sources.forEach(function (source) {
if (!source) {
return;
}
Object.keys(source).forEach(function (key) {
var value = source[key];
if (!isPlain(value)) {
result[key] = value;
return;
}
if (!isPlain(result[key])) {
result[key] = {};
}
result[key] = mergeOptions(result[key], value);
});
});
return result;
};
_video2.default.mergeOptions = mergeOptions;
}
if (!_video2.default.createEl) {
_video2.default.createEl = function (tagName, props, attrs) {
var el = _video2.default.Player.prototype.createEl(tagName, props);
if (!!attrs) {
Object.keys(attrs).forEach(function (key) {
el.setAttribute(key, attrs[key]);
});
}
return el;
};
}
/**
* register the markers plugin (dependent on jquery)
*/
var setting = _video2.default.mergeOptions(defaultSetting, options),
markersMap = {},
markersList = [],
// list of markers sorted by time
currentMarkerIndex = NULL_INDEX,
player = this,
markerTip = null,
breakOverlay = null,
overlayIndex = NULL_INDEX;
function sortMarkersList() {
// sort the list by time in asc order
markersList.sort(function (a, b) {
return setting.markerTip.time(a) - setting.markerTip.time(b);
});
}
function addMarkers(newMarkers) {
newMarkers.forEach(function (marker) {
marker.key = generateUUID();
player.el().querySelector('.vjs-progress-holder').appendChild(createMarkerDiv(marker));
// store marker in an internal hash map
markersMap[marker.key] = marker;
markersList.push(marker);
});
sortMarkersList();
}
function getPosition(marker) {
return setting.markerTip.time(marker) / player.duration() * 100;
}
function setMarkderDivStyle(marker, markerDiv) {
markerDiv.className = 'vjs-marker ' + (marker.class || "");
Object.keys(setting.markerStyle).forEach(function (key) {
markerDiv.style[key] = setting.markerStyle[key];
});
// hide out-of-bound markers
var ratio = marker.time / player.duration();
if (ratio < 0 || ratio > 1) {
markerDiv.style.display = 'none';
}
// set position
markerDiv.style.left = getPosition(marker) + '%';
if (marker.duration) {
markerDiv.style.width = marker.duration / player.duration() * 100 + '%';
markerDiv.style.marginLeft = '0px';
} else {
var markerDivBounding = getElementBounding(markerDiv);
markerDiv.style.marginLeft = markerDivBounding.width / 2 + 'px';
}
}
function createMarkerDiv(marker) {
var markerDiv = _video2.default.createEl('div', {}, {
'data-marker-key': marker.key,
'data-marker-time': setting.markerTip.time(marker)
});
setMarkderDivStyle(marker, markerDiv);
// bind click event to seek to marker time
markerDiv.addEventListener('click', function (e) {
var preventDefault = false;
if (typeof setting.onMarkerClick === "function") {
// if return false, prevent default behavior
preventDefault = setting.onMarkerClick(marker) === false;
}
if (!preventDefault) {
var key = this.getAttribute('data-marker-key');
player.currentTime(setting.markerTip.time(markersMap[key]));
}
});
if (setting.markerTip.display) {
registerMarkerTipHandler(markerDiv);
}
return markerDiv;
}
function updateMarkers(force) {
// update UI for markers whose time changed
markersList.forEach(function (marker) {
var markerDiv = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
var markerTime = setting.markerTip.time(marker);
if (force || markerDiv.getAttribute('data-marker-time') !== markerTime) {
setMarkderDivStyle(marker, markerDiv);
markerDiv.setAttribute('data-marker-time', markerTime);
}
});
sortMarkersList();
}
function removeMarkers(indexArray) {
// reset overlay
if (!!breakOverlay) {
overlayIndex = NULL_INDEX;
breakOverlay.style.visibility = "hidden";
}
currentMarkerIndex = NULL_INDEX;
var deleteIndexList = [];
indexArray.forEach(function (index) {
var marker = markersList[index];
if (marker) {
// delete from memory
delete markersMap[marker.key];
deleteIndexList.push(index);
// delete from dom
var el = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
el && el.parentNode.removeChild(el);
}
});
try {
// clean up markers array
deleteIndexList.reverse();
deleteIndexList.forEach(function (deleteIndex) {
markersList.splice(deleteIndex, 1);
});
} catch (error) {
//Splice is the most likely culprit
console.log(error);
}
// sort again
sortMarkersList();
}
// attach hover event handler
function registerMarkerTipHandler(markerDiv) {
markerDiv.addEventListener('mouseover', function () {
var marker = markersMap[markerDiv.getAttribute('data-marker-key')];
if (!!markerTip) {
markerTip.querySelector('.vjs-tip-inner').innerText = setting.markerTip.text(marker);
// margin-left needs to minus the padding length to align correctly with the marker
markerTip.style.left = getPosition(marker) + '%';
var markerTipBounding = getElementBounding(markerTip);
var markerDivBounding = getElementBounding(markerDiv);
markerTip.style.marginLeft = -parseFloat(markerTipBounding.width / 2) + parseFloat(markerDivBounding.width / 4) + 'px';
markerTip.style.visibility = 'visible';
}
});
markerDiv.addEventListener('mouseout', function () {
if (!!markerTip) {
markerTip.style.visibility = "hidden";
}
});
}
function initializeMarkerTip() {
markerTip = _video2.default.createEl('div', {
className: 'vjs-tip',
innerHTML: "<div class='vjs-tip-arrow'></div><div class='vjs-tip-inner'></div>"
});
player.el().querySelector('.vjs-progress-holder').appendChild(markerTip);
}
// show or hide break overlays
function updateBreakOverlay() {
if (!setting.breakOverlay.display || currentMarkerIndex < 0) {
return;
}
var currentTime = player.currentTime();
var marker = markersList[currentMarkerIndex];
var markerTime = setting.markerTip.time(marker);
if (currentTime >= markerTime && currentTime <= markerTime + setting.breakOverlay.displayTime) {
if (overlayIndex !== currentMarkerIndex) {
overlayIndex = currentMarkerIndex;
if (breakOverlay) {
breakOverlay.querySelector('.vjs-break-overlay-text').innerHTML = setting.breakOverlay.text(marker);
}
}
if (breakOverlay) {
breakOverlay.style.visibility = "visible";
}
} else {
overlayIndex = NULL_INDEX;
if (breakOverlay) {
breakOverlay.style.visibility = "hidden";
}
}
}
// problem when the next marker is within the overlay display time from the previous marker
function initializeOverlay() {
breakOverlay = _video2.default.createEl('div', {
className: 'vjs-break-overlay',
innerHTML: "<div class='vjs-break-overlay-text'></div>"
});
Object.keys(setting.breakOverlay.style).forEach(function (key) {
if (breakOverlay) {
breakOverlay.style[key] = setting.breakOverlay.style[key];
}
});
player.el().appendChild(breakOverlay);
overlayIndex = NULL_INDEX;
}
function onTimeUpdate() {
onUpdateMarker();
updateBreakOverlay();
options.onTimeUpdateAfterMarkerUpdate && options.onTimeUpdateAfterMarkerUpdate();
}
function onUpdateMarker() {
/*
check marker reached in between markers
the logic here is that it triggers a new marker reached event only if the player
enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered)
*/
if (!markersList.length) {
return;
}
var getNextMarkerTime = function getNextMarkerTime(index) {
if (index < markersList.length - 1) {
return setting.markerTip.time(markersList[index + 1]);
}
// next marker time of last marker would be end of video time
return player.duration();
};
var currentTime = player.currentTime();
var newMarkerIndex = NULL_INDEX;
if (currentMarkerIndex !== NULL_INDEX) {
// check if staying at same marker
var nextMarkerTime = getNextMarkerTime(currentMarkerIndex);
if (currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) && currentTime < nextMarkerTime) {
return;
}
// check for ending (at the end current time equals player duration)
if (currentMarkerIndex === markersList.length - 1 && currentTime === player.duration()) {
return;
}
}
// check first marker, no marker is selected
if (currentTime < setting.markerTip.time(markersList[0])) {
newMarkerIndex = NULL_INDEX;
} else {
// look for new index
for (var i = 0; i < markersList.length; i++) {
nextMarkerTime = getNextMarkerTime(i);
if (currentTime >= setting.markerTip.time(markersList[i]) && currentTime < nextMarkerTime) {
newMarkerIndex = i;
break;
}
}
}
// set new marker index
if (newMarkerIndex !== currentMarkerIndex) {
// trigger event if index is not null
if (newMarkerIndex !== NULL_INDEX && options.onMarkerReached) {
options.onMarkerReached(markersList[newMarkerIndex], newMarkerIndex);
}
currentMarkerIndex = newMarkerIndex;
}
}
// setup the whole thing
function initialize() {
if (setting.markerTip.display) {
initializeMarkerTip();
}
// remove existing markers if already initialized
player.markers.removeAll();
addMarkers(setting.markers);
if (setting.breakOverlay.display) {
initializeOverlay();
}
onTimeUpdate();
player.on("timeupdate", onTimeUpdate);
player.off("loadedmetadata");
}
// setup the plugin after we loaded video's meta data
player.on("loadedmetadata", function () {
initialize();
});
// exposed plugin API
player.markers = {
getMarkers: function getMarkers() {
return markersList;
},
next: function next() {
// go to the next marker from current timestamp
var currentTime = player.currentTime();
for (var i = 0; i < markersList.length; i++) {
var markerTime = setting.markerTip.time(markersList[i]);
if (markerTime > currentTime) {
player.currentTime(markerTime);
break;
}
}
},
prev: function prev() {
// go to previous marker
var currentTime = player.currentTime();
for (var i = markersList.length - 1; i >= 0; i--) {
var markerTime = setting.markerTip.time(markersList[i]);
// add a threshold
if (markerTime + 0.5 < currentTime) {
player.currentTime(markerTime);
return;
}
}
},
add: function add(newMarkers) {
// add new markers given an array of index
addMarkers(newMarkers);
},
remove: function remove(indexArray) {
// remove markers given an array of index
removeMarkers(indexArray);
},
removeAll: function removeAll() {
var indexArray = [];
for (var i = 0; i < markersList.length; i++) {
indexArray.push(i);
}
removeMarkers(indexArray);
},
// force - force all markers to be updated, regardless of if they have changed or not.
updateTime: function updateTime(force) {
// notify the plugin to update the UI for changes in marker times
updateMarkers(force);
},
reset: function reset(newMarkers) {
// remove all the existing markers and add new ones
player.markers.removeAll();
addMarkers(newMarkers);
},
destroy: function destroy() {
// unregister the plugins and clean up even handlers
player.markers.removeAll();
breakOverlay && breakOverlay.remove();
markerTip && markerTip.remove();
player.off("timeupdate", updateBreakOverlay);
delete player.markers;
}
};
}
_video2.default.plugin('markers', registerVideoJsMarkersPlugin);
});
//# sourceMappingURL=videojs-markers.js.map

Wyświetl plik

@ -0,0 +1,44 @@
//markers on the timebar
const markerStyle = {
width: "15px",
"background-color": "#DD7373"
};
//the comment overlay
const overlayStyle = {
width: "100%",
height: "auto",
};
function enableMarkers(video) {
if (!(typeof video.markers === 'object')) {
video.markers({
markerStyle: markerStyle,
markerTip: {
display: true,
text: function (marker) {
return marker.text;
}
},
breakOverlay: {
display: true,
displayTime: 20,
text: function (marker) {
return marker.text;
},
style : overlayStyle
},
markers: []
});
}
}
function addMarker(videoPlayer, time, text)
{
videoPlayer.markers.add([{
time: time,
text: text
}]);
}
export {enableMarkers, addMarker};

Wyświetl plik

@ -0,0 +1,59 @@
.vjs-marker {
position: absolute;
left: 0;
bottom: 0em;
opacity: 1;
height: 100%;
transition: opacity .2s ease;
-webkit-transition: opacity .2s ease;
-moz-transition: opacity .2s ease;
z-index: 100;
}
.vjs-marker:hover {
cursor: pointer;
-webkit-transform: scale(1.3, 1.3);
-moz-transform: scale(1.3, 1.3);
-o-transform: scale(1.3, 1.3);
-ms-transform: scale(1.3, 1.3);
transform: scale(1.3, 1.3);
}
.vjs-tip {
visibility: hidden;
display: block;
opacity: 0.8;
padding: 5px;
font-size: 10px;
position: absolute;
bottom: 14px;
z-index: 100000;
}
.vjs-tip .vjs-tip-arrow {
background: url(data:image/gif;base64,R0lGODlhCQAJAIABAAAAAAAAACH5BAEAAAEALAAAAAAJAAkAAAIRjAOnwIrcDJxvwkplPtchVQAAOw==) no-repeat top left;
bottom: 0;
left: 50%;
margin-left: -4px;
background-position: bottom left;
position: absolute;
width: 9px;
height: 5px;
}
.vjs-tip .vjs-tip-inner {
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
padding: 5px 8px 4px 8px;
background-color: black;
color: white;
max-width: 200px;
text-align: center;
}
.vjs-break-overlay {
visibility: hidden;
position: absolute;
z-index: 100000;
top: 0;
}
.vjs-break-overlay .vjs-break-overlay-text {
padding: 9px;
text-align: center;
}

Wyświetl plik

@ -6,8 +6,8 @@ import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon } from '../_shared/';
export function MediaDislikeIcon() {
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-liked-media'));
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-disliked-media'));
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-dislikes'), false));
function updateStateValues() {
setDislikedMedia(MediaPageStore.get('user-disliked-media'));

Wyświetl plik

@ -22,7 +22,7 @@ function downloadOptions(mediaData, allowDownload) {
if (Object.keys(encodingsInfo[k]).length) {
for (g in encodingsInfo[k]) {
if (encodingsInfo[k].hasOwnProperty(g)) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress && null !== encodingsInfo[k][g].url) {
options[encodingsInfo[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodingsInfo[k][g].size + ')',
link: formatInnerLink(encodingsInfo[k][g].url, site.url),

Wyświetl plik

@ -182,9 +182,27 @@ function updateDimensions() {
};
}
function getTimestamp() {
const videoPlayer = document.getElementsByTagName("video");
return videoPlayer[0]?.currentTime;
}
function ToHHMMSS (timeInt) {
let sec_num = parseInt(timeInt, 10);
let hours = Math.floor(sec_num / 3600);
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
let seconds = sec_num - (hours * 3600) - (minutes * 60);
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
if (seconds < 10) {seconds = "0"+seconds;}
return hours >= 1 ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds;
}
export function MediaShareOptions(props) {
const containerRef = useRef(null);
const shareOptionsInnerRef = useRef(null);
const mediaUrl = MediaPageStore.get('media-url');
const [inlineSlider, setInlineSlider] = useState(null);
const [sliderButtonsVisible, setSliderButtonsVisible] = useState({ prev: false, next: false });
@ -192,6 +210,12 @@ export function MediaShareOptions(props) {
const [dimensions, setDimensions] = useState(updateDimensions());
const [shareOptions] = useState(ShareOptions());
const [timestamp, setTimestamp] = useState(0);
const [formattedTimestamp, setFormattedTimestamp] = useState(0);
const [startAtSelected, setStartAtSelected] = useState(false);
const [shareMediaLink, setShareMediaLink] = useState(mediaUrl);
function onWindowResize() {
setDimensions(updateDimensions());
}
@ -219,6 +243,17 @@ export function MediaShareOptions(props) {
});
}
function updateStartAtCheckbox() {
setStartAtSelected(!startAtSelected);
updateShareMediaLink();
}
function updateShareMediaLink()
{
const newLink = startAtSelected ? mediaUrl : mediaUrl + "&t=" + Math.trunc(timestamp);
setShareMediaLink(newLink);
}
function nextSlide() {
inlineSlider.nextSlide();
updateSlider();
@ -243,6 +278,10 @@ export function MediaShareOptions(props) {
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
MediaPageStore.on('copied_media_link', onCompleteCopyMediaLink);
const localTimestamp = getTimestamp();
setTimestamp(localTimestamp);
setFormattedTimestamp(ToHHMMSS(localTimestamp));
return () => {
PageStore.removeListener('window_resize', onWindowResize);
@ -273,10 +312,22 @@ export function MediaShareOptions(props) {
</div>
<div className="copy-field">
<div>
<input type="text" readOnly value={MediaPageStore.get('media-url')} />
<input type="text" readOnly value={shareMediaLink} />
<button onClick={onClickCopyMediaLink}>COPY</button>
</div>
</div>
<div className="start-at">
<label>
<input
type="checkbox"
name="start-at-checkbox"
id="id-start-at-checkbox"
checked={startAtSelected}
onChange={updateStartAtCheckbox}
/>
Start at {formattedTimestamp}
</label>
</div>
</div>
);
}
}

Wyświetl plik

@ -19,7 +19,7 @@ function downloadOptionsList() {
if (Object.keys(encodings_info[k]).length) {
for (g in encodings_info[k]) {
if (encodings_info[k].hasOwnProperty(g)) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress && null !== encodings_info[k][g].url) {
optionsList[encodings_info[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url),

Wyświetl plik

@ -192,6 +192,13 @@ export function VideoPlayer(props) {
document.addEventListener('visibilitychange', initPlayer);
}
player && player.player.one('loadedmetadata', () => {
const urlParams = new URLSearchParams(window.location.search);
const paramT = Number(urlParams.get('t'));
const timestamp = !isNaN(paramT) ? paramT : 0;
player.player.currentTime(timestamp);
});
return () => {
unsetPlayer();

Wyświetl plik

@ -1,5 +1,5 @@
@use "sass:math";
@import '../../../lib/video-js/7.7.5/video-js.min.css';
@import '../../../lib/video-js/7.20.2/video-js.min.css';
@import '../../../css/includes/_variables.scss';
@keyframes up-next-circle-countdown {

Wyświetl plik

@ -18,6 +18,7 @@ export function init(user, features) {
deleteProfile: false,
readComment: true,
addComment: false,
mentionComment: false,
deleteComment: false,
editMedia: false,
deleteMedia: false,
@ -60,6 +61,7 @@ export function init(user, features) {
MEMBER.can.deleteProfile = true === user.can.deleteProfile;
MEMBER.can.addComment = true === user.can.addComment;
MEMBER.can.mentionComment = true === user.can.mentionComment;
MEMBER.can.deleteComment = true === user.can.deleteComment;
MEMBER.can.editMedia = true === user.can.editMedia;
MEMBER.can.deleteMedia = true === user.can.deleteMedia;
@ -100,6 +102,7 @@ export function init(user, features) {
const mediaActions = features.media.actions;
MEMBER.can.addComment = MEMBER.can.addComment && true === mediaActions.comment;
MEMBER.can.mentionComment = MEMBER.can.mentionComment && true === mediaActions.comment_mention;
MEMBER.can.likeMedia = false === mediaActions.like ? false : true;
MEMBER.can.dislikeMedia = false === mediaActions.dislike ? false : true;

Wyświetl plik

@ -51,6 +51,7 @@ class MediaPageStore extends EventEmitter {
this.pagePlaylistId = null;
this.pagePlaylistData = null;
this.userList = null;
MediaPageStoreData[
Object.defineProperty(this, 'id', { value: 'MediaPageStoreData_' + Object.keys(MediaPageStoreData).length }).id
@ -158,6 +159,12 @@ class MediaPageStore extends EventEmitter {
getRequest(this.commentsAPIUrl, !1, this.commentsResponse);
}
loadUsers() {
this.usersAPIUrl = this.mediacms_config.api.users;
this.usersResponse = this.usersResponse.bind(this);
getRequest(this.usersAPIUrl, !1, this.usersResponse);
}
loadPlaylists() {
if (!this.mediacms_config.member.can.saveMedia) {
return;
@ -187,6 +194,9 @@ class MediaPageStore extends EventEmitter {
}
this.loadPlaylists();
if (MediaCMS.features.media.actions.comment_mention === true) {
this.loadUsers();
}
if (this.mediacms_config.member.can.readComment) {
this.loadComments();
@ -215,6 +225,13 @@ class MediaPageStore extends EventEmitter {
}
}
usersResponse(response) {
if (response && response.data) {
MediaPageStoreData.userList = response.data.count ? response.data.results : [];
this.emit('users_load');
}
}
playlistsResponse(response) {
if (response && response.data) {
let tmp_playlists = response.data.count ? response.data.results : [];
@ -403,6 +420,9 @@ class MediaPageStore extends EventEmitter {
i,
r = null;
switch (type) {
case 'users':
r = MediaPageStoreData.userList || [];
break;
case 'playlists':
r = MediaPageStoreData[this.id].playlists || [];
break;

Wyświetl plik

@ -379,21 +379,38 @@
.video-js.vjs-fluid,
.video-js.vjs-16-9,
.video-js.vjs-4-3 {
.video-js.vjs-4-3,
.video-js.vjs-9-16,
.video-js.vjs-1-1 {
width: 100%;
max-width: 100%;
}
.video-js.vjs-fluid:not(.vjs-audio-only-mode),
.video-js.vjs-16-9:not(.vjs-audio-only-mode),
.video-js.vjs-4-3:not(.vjs-audio-only-mode),
.video-js.vjs-9-16:not(.vjs-audio-only-mode),
.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
height: 0;
}
.video-js.vjs-16-9 {
.video-js.vjs-16-9:not(.vjs-audio-only-mode) {
padding-top: 56.25%;
}
.video-js.vjs-4-3 {
.video-js.vjs-4-3:not(.vjs-audio-only-mode) {
padding-top: 75%;
}
.video-js.vjs-fill {
.video-js.vjs-9-16:not(.vjs-audio-only-mode) {
padding-top: 177.7777777778%;
}
.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
padding-top: 100%;
}
.video-js.vjs-fill:not(.vjs-audio-only-mode) {
width: 100%;
height: 100%;
}
@ -406,6 +423,10 @@
height: 100%;
}
.video-js.vjs-audio-only-mode .vjs-tech {
display: none;
}
body.vjs-full-window {
padding: 0;
margin: 0;
@ -422,7 +443,7 @@ body.vjs-full-window {
right: 0;
}
.video-js.vjs-fullscreen {
.video-js.vjs-fullscreen:not(.vjs-ios-native-fs) {
width: 100% !important;
height: 100% !important;
padding-top: 0 !important;
@ -451,8 +472,8 @@ body.vjs-full-window {
.vjs-lock-showing {
display: block !important;
opacity: 1;
visibility: visible;
opacity: 1 !important;
visibility: visible !important;
}
.vjs-no-js {
@ -614,6 +635,11 @@ body.vjs-full-window {
color: #2B333F;
}
.video-js .vjs-menu *:not(.vjs-selected):focus:not(:focus-visible),
.js-focus-visible .vjs-menu *:not(.vjs-selected):focus:not(.focus-visible) {
background: none;
}
.vjs-menu li.vjs-menu-title {
text-align: center;
text-transform: uppercase;
@ -736,7 +762,8 @@ body.vjs-full-window {
background-color: rgba(43, 51, 63, 0.7);
}
.vjs-has-started .vjs-control-bar {
.vjs-has-started .vjs-control-bar,
.vjs-audio-only-mode .vjs-control-bar {
display: flex;
visibility: visible;
opacity: 1;
@ -746,6 +773,7 @@ body.vjs-full-window {
.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
visibility: visible;
opacity: 0;
pointer-events: none;
transition: visibility 1s, opacity 1s;
}
@ -755,9 +783,11 @@ body.vjs-full-window {
display: none !important;
}
.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar,
.vjs-audio-only-mode.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.vjs-has-started.vjs-no-flex .vjs-control-bar {
@ -774,18 +804,28 @@ body.vjs-full-window {
flex: none;
}
.video-js .vjs-control.vjs-visible-text {
width: auto;
padding-left: 1em;
padding-right: 1em;
}
.vjs-button > .vjs-icon-placeholder:before {
font-size: 1.8em;
line-height: 1.67;
}
.vjs-button > .vjs-icon-placeholder {
display: block;
}
.video-js .vjs-control:focus:before,
.video-js .vjs-control:hover:before,
.video-js .vjs-control:focus {
text-shadow: 0em 0em 1em white;
}
.video-js .vjs-control-text {
.video-js *:not(.vjs-visible-text) > .vjs-control-text {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
@ -1074,6 +1114,7 @@ body.vjs-full-window {
.video-js .vjs-volume-level:before {
position: absolute;
font-size: 0.9em;
z-index: 1;
}
.vjs-slider-vertical .vjs-volume-level {
@ -1082,6 +1123,7 @@ body.vjs-full-window {
.vjs-slider-vertical .vjs-volume-level:before {
top: -0.5em;
left: -0.3em;
z-index: 1;
}
.vjs-slider-horizontal .vjs-volume-level {
@ -1116,6 +1158,77 @@ body.vjs-full-window {
left: -2em;
}
.video-js .vjs-volume-tooltip {
background-color: #fff;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3em;
color: #000;
float: right;
font-family: Arial, Helvetica, sans-serif;
font-size: 1em;
padding: 6px 8px 8px 8px;
pointer-events: none;
position: absolute;
top: -3.4em;
visibility: hidden;
z-index: 1;
}
.video-js .vjs-volume-control:hover .vjs-volume-tooltip,
.video-js .vjs-volume-control:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
display: block;
font-size: 1em;
visibility: visible;
}
.video-js .vjs-volume-vertical:hover .vjs-volume-tooltip,
.video-js .vjs-volume-vertical:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
left: 1em;
top: -12px;
}
.video-js .vjs-volume-control.disabled:hover .vjs-volume-tooltip {
font-size: 1em;
}
.video-js .vjs-volume-control .vjs-mouse-display {
display: none;
position: absolute;
width: 100%;
height: 1px;
background-color: #000;
z-index: 1;
}
.video-js .vjs-volume-horizontal .vjs-mouse-display {
width: 1px;
height: 100%;
}
.vjs-no-flex .vjs-volume-control .vjs-mouse-display {
z-index: 0;
}
.video-js .vjs-volume-control:hover .vjs-mouse-display {
display: block;
}
.video-js.vjs-user-inactive .vjs-volume-control .vjs-mouse-display {
visibility: hidden;
opacity: 0;
transition: visibility 1s, opacity 1s;
}
.video-js.vjs-user-inactive.vjs-no-flex .vjs-volume-control .vjs-mouse-display {
display: none;
}
.vjs-mouse-display .vjs-volume-tooltip {
color: #fff;
background-color: #000;
background-color: rgba(0, 0, 0, 0.8);
}
.vjs-poster {
display: inline-block;
vertical-align: middle;
@ -1134,18 +1247,16 @@ body.vjs-full-window {
height: 100%;
}
.vjs-has-started .vjs-poster {
display: none;
}
.vjs-audio.vjs-has-started .vjs-poster {
display: block;
}
.vjs-has-started .vjs-poster,
.vjs-using-native-controls .vjs-poster {
display: none;
}
.vjs-audio.vjs-has-started .vjs-poster,
.vjs-has-started.vjs-audio-poster-mode .vjs-poster {
display: block;
}
.video-js .vjs-live-control {
display: flex;
align-items: flex-start;
@ -1166,6 +1277,7 @@ body.vjs-full-window {
}
.video-js .vjs-seek-to-live-control {
align-items: center;
cursor: pointer;
flex: none;
display: inline-flex;
@ -1252,6 +1364,7 @@ body.vjs-full-window {
pointer-events: none;
}
.video-js.vjs-controls-disabled .vjs-text-track-display,
.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display {
bottom: 1em;
}
@ -1278,6 +1391,7 @@ video::-webkit-media-text-track-display {
transform: translateY(-3em);
}
.video-js.vjs-controls-disabled video::-webkit-media-text-track-display,
.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display {
transform: translateY(-1.5em);
}
@ -1286,10 +1400,18 @@ video::-webkit-media-text-track-display {
cursor: pointer;
flex: none;
}
.video-js.vjs-audio-only-mode .vjs-picture-in-picture-control {
display: none;
}
.video-js .vjs-fullscreen-control {
cursor: pointer;
flex: none;
}
.video-js.vjs-audio-only-mode .vjs-fullscreen-control {
display: none;
}
.vjs-playback-rate > .vjs-menu-button,
.vjs-playback-rate .vjs-playback-rate-value {
position: absolute;
@ -1445,10 +1567,18 @@ video::-webkit-media-text-track-display {
border-top-color: #73859f;
}
}
.video-js.vjs-audio-only-mode .vjs-captions-button {
display: none;
}
.vjs-chapters-button .vjs-menu ul {
width: 24em;
}
.video-js.vjs-audio-only-mode .vjs-descriptions-button {
display: none;
}
.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder {
vertical-align: middle;
display: inline-block;
@ -1462,6 +1592,10 @@ video::-webkit-media-text-track-display {
line-height: inherit;
}
.video-js.vjs-audio-only-mode .vjs-subs-caps-button {
display: none;
}
.video-js .vjs-audio-button + .vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder {
vertical-align: middle;
display: inline-block;
@ -1475,62 +1609,38 @@ video::-webkit-media-text-track-display {
line-height: inherit;
}
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control, .video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control, .video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control {
.video-js.vjs-layout-small .vjs-current-time,
.video-js.vjs-layout-small .vjs-time-divider,
.video-js.vjs-layout-small .vjs-duration,
.video-js.vjs-layout-small .vjs-remaining-time,
.video-js.vjs-layout-small .vjs-playback-rate,
.video-js.vjs-layout-small .vjs-volume-control, .video-js.vjs-layout-x-small .vjs-current-time,
.video-js.vjs-layout-x-small .vjs-time-divider,
.video-js.vjs-layout-x-small .vjs-duration,
.video-js.vjs-layout-x-small .vjs-remaining-time,
.video-js.vjs-layout-x-small .vjs-playback-rate,
.video-js.vjs-layout-x-small .vjs-volume-control, .video-js.vjs-layout-tiny .vjs-current-time,
.video-js.vjs-layout-tiny .vjs-time-divider,
.video-js.vjs-layout-tiny .vjs-duration,
.video-js.vjs-layout-tiny .vjs-remaining-time,
.video-js.vjs-layout-tiny .vjs-playback-rate,
.video-js.vjs-layout-tiny .vjs-volume-control {
display: none;
}
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,
.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,
.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,
.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active {
.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover {
width: auto;
width: initial;
}
.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button, .video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button, .video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button {
.video-js.vjs-layout-x-small .vjs-progress-control, .video-js.vjs-layout-tiny .vjs-progress-control {
display: none;
}
.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer, .video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer {
.video-js.vjs-layout-x-small .vjs-custom-control-spacer {
flex: auto;
display: block;
}
.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer, .video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer {
.video-js.vjs-layout-x-small.vjs-no-flex .vjs-custom-control-spacer {
width: auto;
}
.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control, .video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control {
display: none;
}
.vjs-modal-dialog.vjs-text-track-settings {
background-color: #2B333F;
@ -1653,11 +1763,8 @@ video::-webkit-media-text-track-display {
.js-focus-visible .video-js *:focus:not(.focus-visible) {
outline: none;
background: none;
}
.video-js *:focus:not(:focus-visible),
.video-js .vjs-menu *:focus:not(:focus-visible) {
.video-js *:focus:not(:focus-visible) {
outline: none;
background: none;
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More