Compare commits

...

23 Commits
v1.3 ... v1.4

Author SHA1 Message Date
Yiannis Stergiou
aa6520daac Frontent dev env (#247)
* Added frontend development files/environment

* More items-categories related removals

* Improvements in pages templates (inc. static pages)

* Improvements in video player

* Added empty home page message + cta

* Updates in media, playlist and management pages

* Improvements in material icons font loading

* Replaced media & playlists links in frontend dev-env

* frontend package version update

* chnaged frontend dev url port

* static files update

* Changed default position of theme switcher

* enabled frontend docker container
2021-07-11 18:01:34 +03:00
Markos Gogoulos
060bb45725 Feat robots.txt (#237)
* add robots.txt

* removes robots.txt manual steps
2021-07-02 17:19:02 +03:00
Alb
1f0cc4ff87 Create robots_and_analytics.md (#234)
Added instructions to add Google Analytics and robots.txt
2021-07-02 17:07:20 +03:00
Shubhank Saxena
c28a39fa47 Segregation of Dev and Prod envs (#218)
Segregation of Dev and Prod envs, addition of tests

Co-authored-by: Markos Gogoulos <mgogoulos@gmail.com>
Co-authored-by: Ubuntu <shubhank@my-hostings.nxfutj5b2tlubjykddwgszqteb.bx.internal.cloudapp.net>
2021-07-01 18:05:43 +03:00
DieProgrammIDE
d17b3b4153 Fix or Playback Issues for iPad + GET-Parameters (autoplay,muted,time) for Embed (#179)
* Remove Loading Video Visibility Bug

* Quality Y-Overflow for Mobile

* Update _commons.js

* Oneliner

* Reset _common.js

* Use OnLoad

* Use Observer

* No Max-Height
2021-06-23 11:25:45 +03:00
DecaTec
950adcdd9d Webserver/setup optimizations (#220)
* Webserver security

* Create vHost dirs during install; link vHost to sites-enabled

* Remove default vHosts during install

* Only generate new DH params when also using real certificates

* Removed duplicate ssl_ecdh_curve
2021-06-18 16:56:45 +03:00
swiftugandan
235efbe151 keep pids in container scope (#225)
* keep pids in container scope

* increase client_max_body_size through nginx proxy deployments to 5.8G
2021-06-18 15:39:52 +03:00
Markos Gogoulos
8145ba0914 add missing validation for logging 2021-06-18 15:37:09 +03:00
Markos Gogoulos
f74d3c4b57 instructions for Docker setup update 2021-06-18 14:49:36 +03:00
Markos Gogoulos
9b9a718a18 Docker compose docs (#227) 2021-06-18 14:44:58 +03:00
Markos Gogoulos
26804dce40 stub testing (#226)
adds github action for tests
2021-06-17 23:11:14 +03:00
Markos Gogoulos
de30fe68f1 Readme 2021-06-16 20:54:00 +03:00
Markos Gogoulos
4f72c00598 fixes wrong path on Readme 2021-06-16 20:48:42 +03:00
Markos Gogoulos
d3a3934ce7 cleanup docs and add some more regarding Docker installation (#223)
* cleanup docs and add some more regarding Docker installation
2021-06-16 20:46:40 +03:00
Markos Gogoulos
ddbaa51285 pin Django 2021-06-16 20:31:59 +03:00
dependabot[bot]
7fae5992e7 Bump django from 3.1.8 to 3.1.12 (#222)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.12)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-16 19:29:20 +03:00
dependabot[bot]
46384a3c49 Bump pillow from 8.1.1 to 8.2.0 (#221)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-16 19:29:06 +03:00
swiftugandan
cdcf148b72 adding docker letsencrypt example (#178) 2021-06-16 19:27:33 +03:00
Markos Gogoulos
09e565e99b add shields (#212)
add shields
2021-06-04 12:26:37 +03:00
Markos Gogoulos
7bda0acd8b Feat flake8 strength (#209)
* remove warnings from flake
2021-06-03 18:26:53 +03:00
Markos Gogoulos
748d4bae4b Feat login required (#204)
* global login required option

* add option to require global login
2021-06-02 17:18:06 +03:00
Markos Gogoulos
171b9f84d7 Feat swagger (#203)
Swagger docs
2021-05-31 22:24:05 +03:00
Dan1ell
853b28130d Short tutorial on how to add a static page to the sidebar (#188)
* Create Add A Static Page To The Sidebar

Short tutorial on adding a static page to the sidebar.
2021-05-31 22:23:37 +03:00
594 changed files with 202710 additions and 66066 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
npm-debug.log

35
.github/workflows/python.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Python Tests
on:
pull_request:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Build the Stack
run: docker-compose -f docker-compose-dev.yaml build
- name: Start containers
run: docker-compose -f docker-compose-dev.yaml up -d
- name: List containers
run: docker ps
- name: Sleep for 60 seconds
run: sleep 60s
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec -T web pytest
- name: Tear down the Stack
run: docker-compose -f docker-compose-dev.yaml down

1
.gitignore vendored
View File

@@ -12,5 +12,6 @@ static/ckeditor/
static/debug_toolbar/
static/mptt/
static/rest_framework/
static/drf-yasg
cms/local_settings.py
deploy/docker/local_settings.py

View File

@@ -7,7 +7,7 @@ ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs,pids} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .

16
Dockerfile-dev Normal file
View File

@@ -0,0 +1,16 @@
FROM mediacms/mediacms:latest
SHELL ["/bin/bash", "-c"]
# Set up virtualenv
ENV VIRTUAL_ENV=/home/mediacms.io
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PIP_NO_CACHE_DIR=1
RUN cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install -r requirements-dev.txt
WORKDIR /home/mediacms.io/mediacms

View File

@@ -1,4 +1,13 @@
![MediaCMS](static/images/logo_dark.png)
# 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)
[![Downloads](https://img.shields.io/badge/download-all%20releases-brightgreen.svg)](https://github.com/mediacms-io/mediacms/releases/)
[![DockerHub](https://img.shields.io/docker/pulls/mediacms/mediacms)](https://hub.docker.com/repository/docker/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.
@@ -34,6 +43,7 @@ A demo is available at https://demo.mediacms.io
- **Subtitles/CC**: support for multilingual subtitle files
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
- **Chunked file uploads**: for pausable/resumable upload of content
- **REST API**: Documented through Swagger
## Example cases
@@ -73,95 +83,26 @@ For a small to medium installation, with a few hours of video uploaded daily, an
In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365).
## Installation
## Installation / Maintanance
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.
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:
### Docker Compose installation
Install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
* [Single Server](docs/Single_Server.md) page
* [Docker Compose](docs/Docker_Compose.md) page
Run as root
```bash
git clone https://github.com/mediacms-io/mediacms
cd mediacms
```
The default option is to serve MediaCMS on all ips available of the server (including localhost).
Now run
```bash
docker-compose up
```
This will download all MediaCMS related Docker images and start all containers. Once it finishes, MediaCMS will be installed and available on http://localhost or http://ip
For more instructions, checkout the docs on the [Docker deployment](docs/Docker_deployment.md) page. Docker Compose support has been contributed by @swiftugandan.
### Single server installation
The core dependencies are Python3, Django3, Celery, PostgreSQL, Redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But we strongly suggest installing on Linux Ubuntu 18 or 20 versions.
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
```
## Configure
## 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 cms/local_settings.py. All configuration options will be documented gradually on the [Configuration](docs/Configuration.md) page.
## Authors
MediaCMS is developed by Yiannis Stergiou and Markos Gogoulos. We are Wordgames - https://wordgames.gr.
## 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
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
## Who is using it
- **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
@@ -178,10 +119,10 @@ If you like the project, here's a few things you can do
## Developers info
- API documentation available under /swagger URL (example https://demo.mediacms.io/swagger/)
- API documentation through Swagger is available under /swagger URL of your installation -example https://demo.mediacms.io/swagger/
- We're working on proper documentation for users, managers and developers, until then checkout what's available on the docs/ folder of this repository
- Before you send a PR, make sure your code is properly formatted. For that, use `pre-commit install` to install a pre-commit hook and run `pre-commit run --all` and fix everything before you commit. This pre-commit will check for your code lint everytime you commit a code.
- Checkout the [Code of conduct page](CODE_OF_CONDUCT.md) if you want to contribute to this repository
## Contact
info@mediacms.io
info@mediacms.io

View File

@@ -349,6 +349,16 @@ FILE_UPLOAD_HANDLERS = [
LOGS_DIR = os.path.join(BASE_DIR, "logs")
error_filename = os.path.join(LOGS_DIR, "debug.log")
if not os.path.exists(LOGS_DIR):
try:
os.mkdir(LOGS_DIR)
except PermissionError:
pass
if not os.path.isfile(error_filename):
open(error_filename, 'a').close()
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@@ -356,7 +366,7 @@ LOGGING = {
"file": {
"level": "ERROR",
"class": "logging.FileHandler",
"filename": os.path.join(LOGS_DIR, "debug.log"),
"filename": error_filename,
},
},
"loggers": {
@@ -427,9 +437,13 @@ CELERY_BEAT_SCHEDULE = {
LOCAL_INSTALL = False
# this is an option to make the whole portal available to logged in users only
# it is placed here so it can be overrided on local_settings.py
GLOBAL_LOGIN_REQUIRED = False
try:
# keep a local_settings.py file for local overrides
from .local_settings import *
from .local_settings import * # noqa
# ALLOWED_HOSTS needs a url/ip
ALLOWED_HOSTS.append(FRONTEND_HOST.replace("http://", "").replace("https://", ""))
@@ -446,3 +460,12 @@ if LOCAL_INSTALL:
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https")
else:
SSL_FRONTEND_HOST = FRONTEND_HOST
if GLOBAL_LOGIN_REQUIRED:
# this should go after the AuthenticationMiddleware middleware
MIDDLEWARE.insert(5, "login_required.middleware.LoginRequiredMiddleware")
LOGIN_REQUIRED_IGNORE_PATHS = [
r'/accounts/login/$',
r'/accounts/logout/$',
r'/accounts/signup/$',
]

View File

@@ -2,6 +2,7 @@ import debug_toolbar
from django.conf.urls import include, url
from django.contrib import admin
from django.urls import path, re_path
from django.views.generic.base import TemplateView
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework.permissions import AllowAny
@@ -15,6 +16,10 @@ schema_view = get_schema_view(
urlpatterns = [
url(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")),

5
conftest.py Normal file
View File

@@ -0,0 +1,5 @@
from pytest_factoryboy import register
from tests.users.factories import UserFactory
register(UserFactory)

View File

@@ -7,11 +7,11 @@ ln -sf /dev/stdout /var/log/nginx/mediacms.io.access.log && ln -sf /dev/stderr /
cp /home/mediacms.io/mediacms/deploy/docker/local_settings.py /home/mediacms.io/mediacms/cms/local_settings.py
mkdir -p /home/mediacms.io/mediacms/{logs,pids,media_files/hls}
mkdir -p /home/mediacms.io/mediacms/{logs,media_files/hls}
touch /home/mediacms.io/mediacms/logs/debug.log
# Remove any dangling pids
rm -rf /home/mediacms.io/mediacms/pids/*
mkdir -p /var/run/mediacms
chown www-data:www-data /var/run/mediacms
TARGET_GID=$(stat -c "%g" /home/mediacms.io/mediacms/)

View File

@@ -1 +1 @@
client_max_body_size 1g;
client_max_body_size 5800M;

View File

@@ -9,4 +9,4 @@ user=www-data
directory=/home/mediacms.io/mediacms
priority=300
startinorder=true
command=/home/mediacms.io/bin/celery beat --pidfile=/home/mediacms.io/mediacms/pids/beat%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_beat.log
command=/home/mediacms.io/bin/celery beat --pidfile=/var/run/mediacms/beat%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_beat.log

View File

@@ -10,4 +10,4 @@ directory=/home/mediacms.io/mediacms
priority=500
startinorder=true
startsecs=0
command=/home/mediacms.io/bin/celery multi start long1 --pidfile=/home/mediacms.io/mediacms/pids/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_long.log -Ofair --prefetch-multiplier=1 -Q long_tasks
command=/home/mediacms.io/bin/celery multi start long1 --pidfile=/var/run/mediacms/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_long.log -Ofair --prefetch-multiplier=1 -Q long_tasks

View File

@@ -9,4 +9,4 @@ user=www-data
directory=/home/mediacms.io/mediacms
priority=400
startinorder=true
command=/home/mediacms.io/bin/celery multi start short1 short2 --pidfile=/home/mediacms.io/mediacms/pids/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_short.log --soft-time-limit=300 -c10 -Q short_tasks
command=/home/mediacms.io/bin/celery multi start short1 short2 --pidfile=/var/run/mediacms/%%n.pid --loglevel=INFO --logfile=/home/mediacms.io/mediacms/logs/celery_short.log --soft-time-limit=300 -c10 -Q short_tasks

View File

@@ -0,0 +1,13 @@
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEAo3MMiEY/fNbu+usIM0cDi6x8G3JBApv0Lswta4kiyedWT1WN51iQ
9zhOFpmcu6517f/fR9MUdyhVKHxxSqWQTcmTEFtz4P3VLTS/W1N5VbKE2VEMLpIi
wr350aGvV1Er0ujcp5n4O4h0I1tn4/fNyDe7+pHCdwM+hxe8hJ3T0/tKtad4fnIs
WHDjl4f7m7KuFfheiK7Efb8MsT64HDDAYXn+INjtDZrbE5XPw20BqyWkrf07FcPx
8o9GW50Ox7/FYq7jVMI/skEu0BRc8u6uUD9+UOuWUQpdeHeFcvLOgW53Z03XwWuX
RXosUKzBPuGtUDAaKD/HsGW6xmGr2W9yRmu27jKpfYLUb/eWbbnRJwCw04LdzPqv
jmtq02Gioo3lf5H5wYV9IYF6M8+q/slpbttsAcKERimD1273FBRt5VhSugkXWKjr
XDhoXu6vZgj8Opei38qPa8pI1RUFoXHFlCe6WpZQmU8efL8gAMrJr9jUIY8eea1n
u20t5B9ueb9JMjrNafcq6QkKhZLi6fRDDTUyeDvc0dN9R/3Yts97SXfdi1/lX7HS
Ht4zXd5hEkvjo8GcnjsfZpAC39QfHWkDaeUGEqsl3jXjVMfkvoVY51OuokPWZzrJ
M5+wyXNpfGbH67dPk7iHgN7VJvgX0SYscDPTtms50Vk7RwEzLeGuSHMCAQI=
-----END DH PARAMETERS-----

View File

@@ -46,6 +46,12 @@ server {
ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem;
ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem;
ssl_dhparam /etc/nginx/dhparams/dhparams.pem;
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve secp521r1:secp384r1;
ssl_prefer_server_ciphers on;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;

View File

@@ -19,10 +19,7 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

66
docker-compose-dev.yaml Normal file
View File

@@ -0,0 +1,66 @@
version: "3"
services:
frontend:
image: node:14
volumes:
- ${PWD}/frontend:/home/mediacms.io/mediacms/frontend/
working_dir: /home/mediacms.io/mediacms/frontend/
command: bash -c "npm install && npm run start"
env_file:
- ${PWD}/frontend/.env
ports:
- "8088:8088"
depends_on:
- web
web:
build:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
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
db:
image: postgres
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,115 @@
version: "3"
# Uses https://github.com/nginx-proxy/acme-companion
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- dhparam:/etc/nginx/dhparam
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./deploy/docker/reverse_proxy/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf:ro
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
volumes_from:
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
migrations:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
web:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_CELERY_BEAT: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
VIRTUAL_HOST: 'mediacms.52.209.5.113.nip.io'
LETSENCRYPT_HOST: 'mediacms.52.209.5.113.nip.io'
LETSENCRYPT_EMAIL: 'email@example.com'
depends_on:
- migrations
celery_beat:
image: mediacms/mediacms:latest
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- redis
celery_worker:
image: mediacms/mediacms:latest
deploy:
replicas: 1
volumes:
- ./:/home/mediacms.io/mediacms/
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
depends_on:
- migrations
db:
image: postgres
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always
environment:
POSTGRES_USER: mediacms
POSTGRES_PASSWORD: mediacms
POSTGRES_DB: mediacms
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mediacms"]
interval: 30s
timeout: 10s
retries: 5
redis:
image: "redis:alpine"
restart: always
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 3
volumes:
conf:
vhost:
html:
dhparam:
certs:
acme:

View File

@@ -0,0 +1,133 @@
## 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
```

View File

@@ -8,12 +8,19 @@ In case of a the single server installation, add to `cms/local_settings.py` .
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
Any change needs restart of MediaCMS in order to take effect. So edit `cms/local_settings.py`, make a change and restart MediaCMS
Any change needs restart of MediaCMS in order to take effect.
```
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
@@ -254,3 +261,10 @@ ADMINS_NOTIFICATIONS = {
- 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.

52
docs/Docker_Compose.md Normal file
View File

@@ -0,0 +1,52 @@
# 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
## 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/

View File

@@ -17,23 +17,36 @@ See example deployments in the sections below. These example deployments have be
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
# 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.
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.
## Advanced Deployment, accessed as http://localhost:8000
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
# 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
# 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)
# 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.

38
docs/Single_Server.md Normal file
View File

@@ -0,0 +1,38 @@
# 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

View File

@@ -0,0 +1,55 @@
# 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
```

View File

@@ -1,12 +1,11 @@
from django.conf import settings
from django.contrib.postgres.search import SearchQuery
from django.contrib.syndication.views import Feed
from django.db.models import Q
from django.urls import reverse
from django.utils.feedgenerator import Rss201rev2Feed
from . import helpers
from .models import Category, Media
from .models import Media
from .stop_words import STOP_WORDS

View File

@@ -381,7 +381,6 @@ def media_file_info(input_file):
}
if has_audio:
audio_duration = 1
if "duration" in audio_info.keys():
audio_duration = float(audio_info["duration"])
elif "tags" in audio_info.keys() and "DURATION" in audio_info["tags"]:

View File

@@ -24,7 +24,6 @@ from imagekit.processors import ResizeToFit
from mptt.models import MPTTModel, TreeForeignKey
from . import helpers
from .methods import notify_users
from .stop_words import STOP_WORDS
logger = logging.getLogger(__name__)
@@ -1343,6 +1342,8 @@ def media_save(sender, instance, created, **kwargs):
# SOS: do not put anything here, as if more logic is added,
# we have to disconnect signal to avoid infinite recursion
if created:
from .methods import notify_users
instance.media_init()
notify_users(friendly_token=instance.friendly_token, action="media_added")
@@ -1507,7 +1508,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
# to avoid that this is run twice
if (
len(orig_chunks)
== Encoding.objects.filter(
== Encoding.objects.filter( # noqa
media=instance.media,
profile=instance.profile,
chunks_info=instance.chunks_info,
@@ -1537,7 +1538,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
chunks_paths = [f.media_file.path for f in chunks]
all_logs = "\n".join([st.logs for st in chunks])
encoding.logs = "{0}\n{1}\n{2}".format(chunks_paths, all_logs)
encoding.logs = "{0}\n{1}".format(chunks_paths, all_logs)
workers = list(set([st.worker for st in chunks]))
encoding.worker = json.dumps({"workers": workers})
start_date = min([st.add_date for st in chunks])
@@ -1548,7 +1549,7 @@ def encoding_file_save(sender, instance, created, **kwargs):
who = Encoding.objects.filter(media=encoding.media, profile=encoding.profile).exclude(id=encoding.id)
who.delete()
pass # TODO: merge with above if, do not repeat code
# TODO: merge with above if, do not repeat code
else:
if instance.status in ["fail", "success"]:
instance.media.post_encode_actions(encoding=instance, action="add")
@@ -1556,7 +1557,6 @@ def encoding_file_save(sender, instance, created, **kwargs):
encodings = set([encoding.status for encoding in Encoding.objects.filter(media=instance.media)])
if ("running" in encodings) or ("pending" in encodings):
return
workers = list(set([encoding.worker for encoding in Encoding.objects.filter(media=instance.media)]))
@receiver(post_delete, sender=Encoding)

View File

@@ -38,7 +38,6 @@ class MediaSerializer(serializers.ModelSerializer):
"friendly_token",
"user",
"add_date",
"views",
"media_type",
"state",
"duration",
@@ -49,6 +48,7 @@ class MediaSerializer(serializers.ModelSerializer):
"reported_times",
"size",
"is_reviewed",
"featured",
)
fields = (
"friendly_token",
@@ -64,8 +64,6 @@ class MediaSerializer(serializers.ModelSerializer):
"duration",
"thumbnail_url",
"is_reviewed",
"url",
"api_url",
"preview_url",
"author_name",
"author_profile",

View File

@@ -592,7 +592,7 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
if not (user or session_key):
return False
if action in ["like", "dislike", "report"]:
if action in ["like", "dislike", "watch", "report"]:
if not pre_save_action(
media=media,
user=user,

View File

@@ -366,13 +366,18 @@ class MediaList(APIView):
"""Media listings views"""
permission_classes = (IsAuthorizedToAdd,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
openapi.Parameter(name='author', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='username'),
openapi.Parameter(name='show', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='show', enum=['recommended', 'featured', 'latest']),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='List Media',
operation_description='Lists all media',
responses={200: MediaSerializer(many=True)},
)
def get(self, request, format=None):
# Show media
@@ -414,10 +419,15 @@ class MediaList(APIView):
return paginator.get_paginated_response(serializer.data)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=True, description="media_file"),
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='Add new Media',
operation_description='Adds a new media, for authenticated users',
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
)
def post(self, request, format=None):
# Add new media
@@ -435,7 +445,7 @@ class MediaDetail(APIView):
"""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrEditor)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
def get_object(self, friendly_token, password=None):
try:
@@ -461,10 +471,13 @@ class MediaDetail(APIView):
)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='Get information for Media',
operation_description='Get information for a media',
responses={200: SingleMediaSerializer(), 400: 'bad request'},
)
def get(self, request, friendly_token, format=None):
# Get media details
@@ -492,10 +505,23 @@ class MediaDetail(APIView):
return Response(ret)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
openapi.Parameter(name='type', type=openapi.TYPE_STRING, in_=openapi.IN_FORM, description='action to perform', enum=['encode', 'review']),
openapi.Parameter(
name='encoding_profiles',
type=openapi.TYPE_ARRAY,
items=openapi.Items(type=openapi.TYPE_STRING),
in_=openapi.IN_FORM,
description='if action to perform is encode, need to specify list of ids of encoding profiles',
),
openapi.Parameter(name='result', type=openapi.TYPE_BOOLEAN, in_=openapi.IN_FORM, description='if action is review, this is the result (True for reviewed, False for not reviewed)'),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='Run action on Media',
operation_description='Actions for a media, for MediaCMS editors and managers',
responses={201: 'action created', 400: 'bad request'},
operation_id='media_manager_actions',
)
def post(self, request, friendly_token, format=None):
"""superuser actions
@@ -514,7 +540,6 @@ class MediaDetail(APIView):
action = request.data.get("type")
profiles_list = request.data.get("encoding_profiles")
result = request.data.get("result", True)
if action == "encode":
# Create encoding tasks for specific profiles
valid_profiles = []
@@ -548,10 +573,15 @@ class MediaDetail(APIView):
)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name="description", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="description"),
openapi.Parameter(name="title", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="title"),
openapi.Parameter(name="media_file", in_=openapi.IN_FORM, type=openapi.TYPE_FILE, required=False, description="media_file"),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='Update Media',
operation_description='Update a Media, for Media uploader',
responses={201: openapi.Response('response description', MediaSerializer), 401: 'bad request'},
)
def put(self, request, friendly_token, format=None):
# Update a media object
@@ -561,16 +591,24 @@ class MediaDetail(APIView):
serializer = MediaSerializer(media, data=request.data, context={"request": request})
if serializer.is_valid():
media_file = request.data["media_file"]
serializer.save(user=request.user, media_file=media_file)
if request.data.get('media_file'):
media_file = request.data["media_file"]
serializer.save(user=request.user, media_file=media_file)
else:
serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
manual_parameters=[],
manual_parameters=[
openapi.Parameter(name='friendly_token', type=openapi.TYPE_STRING, in_=openapi.IN_PATH, description='unique identifier', required=True),
],
tags=['Media'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='Delete Media',
operation_description='Delete a Media, for MediaCMS editors and managers',
responses={
204: 'no content',
},
)
def delete(self, request, friendly_token, format=None):
# Delete a media object
@@ -757,7 +795,7 @@ class MediaSearch(APIView):
media = media.filter(user__username=author)
if upload_date:
gte = lte = None
gte = None
if upload_date == 'today':
gte = datetime.now().date()
if upload_date == 'this_week':
@@ -769,8 +807,6 @@ class MediaSearch(APIView):
if upload_date == 'this_year':
year = datetime.now().date().year
gte = datetime(year, 1, 1)
if lte:
media = media.filter(add_date__lte=lte)
if gte:
media = media.filter(add_date__gte=gte)
@@ -803,6 +839,9 @@ class PlaylistList(APIView):
tags=['Playlists'],
operation_summary='to_be_written',
operation_description='to_be_written',
responses={
200: openapi.Response('response description', PlaylistSerializer(many=True)),
},
)
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
@@ -1005,8 +1044,8 @@ class EncodingDetail(APIView):
chunk=chunk,
chunk_file_path=chunk_file_path,
).count()
> 1
and force is False
> 1 # noqa
and force is False # noqa
):
Encoding.objects.filter(id=encoding_id).delete()
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
@@ -1124,6 +1163,9 @@ class CommentList(APIView):
tags=['Comments'],
operation_summary='Lists Comments',
operation_description='Paginated listing of all comments',
responses={
200: openapi.Response('response description', CommentSerializer(many=True)),
},
)
def get(self, request, format=None):
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
@@ -1280,6 +1322,9 @@ class CategoryList(APIView):
tags=['Categories'],
operation_summary='Lists Categories',
operation_description='Lists all categories',
responses={
200: openapi.Response('response description', CategorySerializer),
},
)
def get(self, request, format=None):
categories = Category.objects.filter().order_by("title")
@@ -1298,6 +1343,9 @@ class TagList(APIView):
tags=['Tags'],
operation_summary='Lists Tags',
operation_description='Paginated listing of all tags',
responses={
200: openapi.Response('response description', TagSerializer),
},
)
def get(self, request, format=None):
tags = Tag.objects.filter().order_by("-media_count")
@@ -1314,8 +1362,9 @@ class EncodeProfileList(APIView):
@swagger_auto_schema(
manual_parameters=[],
tags=['Encoding Profiles'],
operation_summary='to_be_written',
operation_description='to_be_written',
operation_summary='List Encoding Profiles',
operation_description='Lists all encoding profiles for videos',
responses={200: EncodeProfileSerializer(many=True)},
)
def get(self, request, format=None):
profiles = EncodeProfile.objects.all()

12
frontend/.babelrc Executable file
View File

@@ -0,0 +1,12 @@
{
"presets": [
"@babel/react", ["@babel/env", {
"modules": false,
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
"browsers": ["defaults"]
}
}]
]
}

12
frontend/.env Normal file
View File

@@ -0,0 +1,12 @@
MEDIACMS_ID=mediacms-frontend
MEDIACMS_TITLE=MediaCMS Frontend
MEDIACMS_URL=http://localhost
MEDIACMS_API=http://localhost/api/v1
MEDIACMS_USER_IS_ADMIN=true
MEDIACMS_USER_IS_ANONYMOUS=false
MEDIACMS_USER_USERNAME=admin
MEDIACMS_USER_NAME=Admin
MEDIACMS_USER_THUMB=http://localhost/media/userlogos/user.jpg

131
frontend/.gitignore vendored Executable file
View File

@@ -0,0 +1,131 @@
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
!modules/**/*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.development
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# System files
.fuse_hidden*
# Exports
/build
/dist
# Packages dev files
# /packages/**/package-lock.json
# Other
*playground*

1
frontend/.prettierignore Normal file
View File

@@ -0,0 +1 @@
config/templates/*.ejs

5
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 120
}

3
frontend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

29
frontend/README.md Normal file
View File

@@ -0,0 +1,29 @@
# MediaCMS Web Client (demo)
### **Requirements**
- nodejs: version >= 14.17.0
---
### **Installation**
npm install
---
### **Development**
npm run start
Open in browser: [http://localhost:8088](http://localhost:8088)
---
### **Build**
npm run dist
Generates the folder "**_frontend/dist_**".
Copy folders and files from "**_frontend/dist/static_**" into "**_static_**".

View File

@@ -0,0 +1,44 @@
module.exports = {
head: {
meta: [
{ charset: 'utf-8' },
{ content: 'ie=edge', 'http-equiv': 'x-ua-compatible' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'theme-color', content: '#fafafa' },
{ name: 'msapplication-TileColor', content: '#fafafa' },
{ name: 'msapplication-config', content: 'favicons/browserconfig.xml' },
],
links: [
/**
* Manifest file link.
*/
{ rel: 'manifest', href: 'static/favicons/site.webmanifest' },
/**
* Favicon links.
*/
{ rel: 'apple-touch-icon', sizes: '180x180', href: 'static/favicons/apple-touch-icon.png' },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: 'static/favicons/favicon-32x32.png' },
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: 'static/favicons/favicon-16x16.png' },
{ rel: 'mask-icon', href: 'static/favicons/safari-pinned-tab.svg', color: '#fafafa' },
{ rel: 'shortcut icon', href: 'static/favicons/favicon.ico' },
/**
* Stylesheet links
*/
{ rel: 'preload', href: 'static/css/_extra.css', as: 'style' },
{ rel: 'stylesheet', href: 'static/css/_extra.css' },
// 'https://fonts.googleapis.com/icon?family=Material+Icons',
{ rel: 'preload', href: 'static/lib/material-icons/material-icons.css', as: 'style' },
{ rel: 'stylesheet', href: 'static/lib/material-icons/material-icons.css' },
// 'https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,500,500i,700,700i&display=swap
{ rel: 'preload', href: 'static/lib/gfonts/gfonts.css', as: 'style' },
{ rel: 'stylesheet', href: 'static/lib/gfonts/gfonts.css' },
],
scripts: [],
},
body: {
scripts: [],
snippet: '',
},
};

View File

@@ -0,0 +1,31 @@
const path = require('path');
const rootDir = '..';
const srcDir = rootDir + '/src';
const configDir = srcDir + '/templates/config';
const coreConfigDir = configDir + '/core';
const installConfigDir = configDir + '/installation';
module.exports = {
src: path.resolve(__dirname, srcDir),
build: path.resolve(__dirname, rootDir),
html: require('./mediacms.config.html.js'),
pages: require('./mediacms.config.pages.js'),
window: {
MediaCMS: {
api: require(coreConfigDir + '/api.config.js'),
url: require(coreConfigDir + '/url.config.js'),
user: require(coreConfigDir + '/user.config.js'),
site: require(installConfigDir + '/site.config.js'),
pages: require(installConfigDir + '/pages.config.js'),
contents: require(installConfigDir + '/contents.config.js'),
features: require(installConfigDir + '/features.config.js'),
/*notifications: [
'Message one text',
'Message two text',
'Message three text'
],*/
},
},
postcssConfigFile: path.resolve(__dirname, './postcss.config.js'),
};

View File

@@ -0,0 +1,268 @@
const templates = require('./mediacms.config.templates');
const DEV_SAMPLE_DATA = {
profileId: process.env.MEDIACMS_USER_USERNAME,
media: {
videoId: process.env.MEDIACMS_VIDEO_ID,
audioId: process.env.MEDIACMS_AUDIO_ID,
imageId: process.env.MEDIACMS_IMAGE_ID,
pdfId: process.env.MEDIACMS_PDF_ID,
},
playlistId: process.env.MEDIACMS_PLAYLIST_ID,
};
const formatPage = (page) => {
const pageContentId = 'page-' + page.id;
const filename = page.filename ? page.filename : ('home' === page.id ? 'index' : page.id) + '.html';
const render = page.renderer
? page.renderer
: page.component
? 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' },
...(page.headLinks ? page.headLinks : []),
];
const bodyScripts = [
{ src: './static/lib/video-js/7.7.5/video.min.js' },
...(page.bodyScripts ? page.bodyScripts : []),
];
const ret = {
buildExclude: !!page.buildExclude,
title: page.title,
filename,
render,
html: {
head: {
links: headLinks,
},
body: {
scripts: bodyScripts,
snippet: page.snippet || templates.htmlBodySnippet({ id: pageContentId }),
},
},
window: { MediaCMS: page.global ? { ...page.global } : {} },
};
return ret;
};
const formatPageData = (page) => {
return formatPage({
...page,
});
};
const formatStaticPageData = (page) => {
const pageContentId = 'page-' + page.id;
return formatPage({
...page,
renderer: page.renderer
? page.renderer
: page.component
? templates.renderPageStaticContent({ page: { id: pageContentId, component: page.component } })
: undefined,
});
};
const PAGES = {
base: {
id: 'base',
title: 'Layout base',
renderer: templates.renderBase(),
},
index: { id: 'home', title: 'Home', component: 'HomePage' },
search: { id: 'search', title: 'Search results', component: 'SearchPage' },
latest: { id: 'latest', title: 'Recent uploads', component: 'LatestMediaPage' },
featured: { id: 'featured', title: 'Featured', component: 'FeaturedMediaPage' },
recommended: { id: 'recommended', title: 'Recommended', component: 'RecommendedMediaPage' },
members: { id: 'members', title: 'Members', component: 'MembersPage' },
history: { id: 'history', title: 'History', component: 'HistoryPage' },
liked: { id: 'liked', title: 'Liked media', component: 'LikedMediaPage' },
tags: { id: 'tags', title: 'Tags', component: 'TagsPage' },
categories: { id: 'categories', title: 'Categories', component: 'CategoriesPage' },
'manage-media': { id: 'manage-media', title: 'Manage media', component: 'ManageMediaPage' },
'manage-users': { id: 'manage-users', title: 'Manage users', component: 'ManageUsersPage' },
'manage-comments': { id: 'manage-comments', title: 'Manage comments', component: 'ManageCommentsPage' },
'add-media': {
id: 'add-media',
title: 'Add media',
renderer: templates.renderAddMediaPageContent(),
snippet: templates.htmlBodySnippetAddMediaPage(),
headLinks: [{ rel: 'preload', href: './static/lib/file-uploader/5.13.0/fine-uploader.min.js', as: 'script' }],
bodyScripts: [{ src: './static/lib/file-uploader/5.13.0/fine-uploader.min.js' }],
},
embed: {
id: 'embed',
title: 'Embedded player',
renderer: templates.renderEmbedPageContent({ page: { id: 'page-embed', component: 'EmbedPage' } }),
snippet: templates.htmlBodySnippetEmbedPage({ id: 'page-embed' }),
global: { mediaId: DEV_SAMPLE_DATA.media.videoId },
},
media: {
id: 'media',
title: 'Media',
component: 'MediaPage',
global: { mediaId: DEV_SAMPLE_DATA.media.videoId },
},
'media-video': {
buildExclude: true,
id: 'media-video',
title: 'Media - Video',
component: 'MediaVideoPage',
global: { mediaId: DEV_SAMPLE_DATA.media.videoId },
},
'media-audio': {
buildExclude: true,
id: 'media-audio',
title: 'Media - Audio',
component: 'MediaAudioPage',
global: { mediaId: DEV_SAMPLE_DATA.media.audioId },
},
'media-image': {
buildExclude: true,
id: 'media-image',
title: 'Media - Image',
component: 'MediaImagePage',
global: { mediaId: DEV_SAMPLE_DATA.media.imageId },
},
'media-pdf': {
buildExclude: true,
id: 'media-pdf',
title: 'Media - Pdf',
component: 'MediaPdfPage',
global: { mediaId: DEV_SAMPLE_DATA.media.pdfId },
},
playlist: {
id: 'playlist',
title: 'Playlist',
component: 'PlaylistPage',
global: { playlistId: DEV_SAMPLE_DATA.playlistId },
},
'profile-media': {
id: 'profile-media',
title: 'Profile - Media',
component: 'ProfileMediaPage',
global: { profileId: DEV_SAMPLE_DATA.profileId },
},
'profile-about': {
id: 'profile-about',
title: 'Profile - About',
component: 'ProfileAboutPage',
global: { profileId: DEV_SAMPLE_DATA.profileId },
},
'profile-playlists': {
id: 'profile-playlists',
title: 'Profile - Playlist',
component: 'ProfilePlaylistsPage',
global: { profileId: DEV_SAMPLE_DATA.profileId },
},
};
const STATIC_PAGES = {
error: {
buildExclude: true,
id: 'error',
title: 'Error',
renderer: templates.renderBase(),
snippet: templates.static.errorPage(),
},
about: {
id: 'about',
title: 'About',
renderer: templates.renderBase(),
snippet: templates.static.aboutPage(),
},
terms: {
buildExclude: true,
id: 'terms',
title: 'Terms',
renderer: templates.renderBase(),
snippet: templates.static.termsPage(),
},
};
const DEV_ONLY_STATIC_PAGES = {
'add-media-template': {
buildExclude: true,
id: 'add-media-template',
title: 'Add media - Template',
renderer: templates.renderAddMediaPageContent(),
snippet: templates.static.addMediaPageTemplate(),
headLinks: [{ rel: 'preload', href: './static/lib/file-uploader/5.13.0/fine-uploader.min.js', as: 'script' }],
bodyScripts: [{ src: './static/lib/file-uploader/5.13.0/fine-uploader.min.js' }],
},
'edit-media': {
buildExclude: true,
id: 'edit-media',
title: 'Edit media',
renderer: templates.renderBase(),
snippet: templates.static.editMediaPage(),
},
'edit-channel': {
buildExclude: true,
id: 'edit-channel',
title: 'Edit channel',
renderer: templates.renderBase(),
snippet: templates.static.editChannelPage(),
},
'edit-profile': {
buildExclude: true,
id: 'edit-profile',
title: 'Edit profile',
renderer: templates.renderBase(),
snippet: templates.static.editProfilePage(),
},
signin: {
buildExclude: true,
id: 'signin',
title: 'Sign in',
renderer: templates.renderBase(),
snippet: templates.static.signinPage(),
},
signout: {
buildExclude: true,
id: 'signout',
title: 'Sign out',
renderer: templates.renderBase(),
snippet: templates.static.signoutPage(),
},
register: {
buildExclude: true,
id: 'register',
title: 'Register',
renderer: templates.renderBase(),
snippet: templates.static.registerPage(),
},
'reset-password': {
buildExclude: true,
id: 'reset-password',
title: 'Reset password',
renderer: templates.renderBase(),
snippet: templates.static.resetPasswordPage(),
},
contact: {
buildExclude: true,
id: 'contact',
title: 'Contact us',
renderer: templates.renderBase(),
snippet: templates.static.contactPage(),
},
};
const pages = {};
for (let k in PAGES) {
pages[k] = formatPageData(PAGES[k]);
}
for (let k in STATIC_PAGES) {
pages[k] = formatStaticPageData(STATIC_PAGES[k]);
}
for (let k in DEV_ONLY_STATIC_PAGES) {
pages[k] = formatStaticPageData(DEV_ONLY_STATIC_PAGES[k]);
}
module.exports = pages;

View File

@@ -0,0 +1,45 @@
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const templatesPath = path.join(__dirname, './templates');
const staticTemplatesPath = path.join(__dirname, './templates/static');
const compileTmpl = (filename) =>
ejs.compile(fs.readFileSync(path.join(templatesPath, filename), 'utf8'), {
root: [templatesPath],
filename: path.join(templatesPath, filename),
outputFunctionName: 'echo',
});
const compileStaticTmpl = (filename) =>
ejs.compile(fs.readFileSync(path.join(staticTemplatesPath, filename), 'utf8'), {
root: [staticTemplatesPath],
filename: path.join(staticTemplatesPath, filename),
outputFunctionName: 'echo',
});
module.exports = {
htmlBodySnippet: compileTmpl('htmlBodySnippet.ejs'),
htmlBodySnippetEmbedPage: compileTmpl('htmlBodySnippetEmbedPage.ejs'),
htmlBodySnippetAddMediaPage: compileTmpl('htmlBodySnippetAddMediaPage.ejs'),
renderBase: compileTmpl('renderBase.ejs'),
renderPageContent: compileTmpl('renderPageContent.ejs'),
renderPageStaticContent: compileTmpl('renderPageStaticContent.ejs'),
renderEmbedPageContent: compileTmpl('renderEmbedPageContent.ejs'),
renderAddMediaPageContent: compileTmpl('renderAddMediaPageContent.ejs'),
static: {
errorPage: compileStaticTmpl('errorPage.html'),
aboutPage: compileStaticTmpl('aboutPage.html'),
termsPage: compileStaticTmpl('termsPage.html'),
contactPage: compileStaticTmpl('contactPage.html'),
signinPage: compileStaticTmpl('signinPage.html'),
signoutPage: compileStaticTmpl('signoutPage.html'),
registerPage: compileStaticTmpl('registerPage.html'),
resetPasswordPage: compileStaticTmpl('resetPasswordPage.html'),
editMediaPage: compileStaticTmpl('editMediaPage.html'),
editChannelPage: compileStaticTmpl('editChannelPage.html'),
editProfilePage: compileStaticTmpl('editProfilePage.html'),
addMediaPageTemplate: compileStaticTmpl('addMediaPageTemplate.html'),
},
};

View File

@@ -0,0 +1,10 @@
module.exports = (ctx) => {
const ret = {
map: ctx.env === 'development' ? ctx.map : false,
plugins: {
autoprefixer: {},
},
};
return ret;
};

View File

@@ -0,0 +1,6 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<% if (id) { %><div id="<%= id %>"></div><% } %>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,65 @@
<div id="add-media-page">
<div class="page-container">
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<main class="page-main">
<div class="page-main-inner">
<div class="media-uploader-wrap">
<div class="media-uploader-top-wrap">
<div class="media-uploader-top-left-wrap">
<h1>Upload media files</h1>
</div>
<div class="media-uploader-top-right-wrap"></div>
</div>
<script type="text/template" id="qq-template"> <div class="media-uploader-bottom-wrap qq-uploader-selector"> <div class="media-uploader-bottom-left-wrap"> <div class="media-drag-drop-wrap"> <div class="media-drag-drop-inner" qq-drop-area-text="Drop files here"> <div class="media-drag-drop-content"> <div class="media-drag-drop-content-inner"> <span><i class="material-icons">cloud_upload</i></span> <span>Drag and drop files</span> <span>or</span> <span class="browse-files-btn-wrap"><span class="qq-upload-button-selector">Browse your files</span></span><div class="qq-upload-drop-area-selector media-dropzone" qq-hide-dropzone><span class="qq-upload-drop-area-text-selector"></span></div> </div></div></div></div></div><div class="media-uploader-bottom-right-wrap"> <ul class="media-upload-items-list qq-upload-list-selector"> <li> <div class="media-upload-item-main"> <div class="media-upload-item-thumb"> <img class="qq-thumbnail-selector" qq-max-size="120" qq-server-scale alt=""/> <span class="media-upload-item-spinner qq-upload-spinner-selector"><i class="material-icons">autorenew</i></span> <button type="button" class="qq-upload-retry-selector retry-media-upload-item" aria-label="Retry"><i class="material-icons">refresh</i> Retry</button> </div><div class="media-upload-item-details"> <div class="media-upload-item-name"> <span class="media-upload-item-filename qq-upload-file-selector"></span> <input class="media-upload-item-filename-input qq-edit-filename-selector" tab-index="0" type="text"/> </div><div class="media-upload-item-details-bottom"> <div class="media-upload-item-progress-bar-container qq-progress-bar-container-selector"> <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="media-upload-item-progress-bar qq-progress-bar-selector"></div></div><span class="media-upload-item-upload-size qq-upload-size-selector"></span> <span role="status" class="media-upload-item-status-text qq-upload-status-text-selector"></span> </div><div class="media-upload-item-top-actions"> <span class="filename-edit qq-edit-filename-icon-selector" aria-label="Edit filename">Edit filename <i class="material-icons">create</i></span> <button type="button" class="delete-media-upload-item qq-upload-delete-selector" aria-label="Delete">Delete <i class="material-icons">delete</i></button> <button type="button" class="cancel-media-upload-item qq-upload-cancel-selector" aria-label="Cancel">Cancel <i class="material-icons">cancel</i></button> </div><div class="media-upload-item-bottom-actions"> <button type="button" class="continue-media-upload-item qq-upload-continue-selector" aria-label="Continue"><i class="material-icons">play_circle_outline</i> Continue</button> <button type="button" class="pause-media-upload-item qq-upload-pause-selector" aria-label="Pause"><i class="material-icons">pause_circle_outline</i> Pause</button> </div></div></div></li></ul> <dialog class="qq-alert-dialog-selector"> <div class="qq-dialog-message-selector"></div><div class="qq-dialog-buttons"> <button type="button" class="qq-cancel-button-selector">Close</button> </div></dialog> <dialog class="qq-confirm-dialog-selector"> <div class="qq-dialog-message-selector"></div><div class="qq-dialog-buttons"> <button type="button" class="qq-cancel-button-selector">No</button> <button type="button" class="qq-ok-button-selector">Yes</button> </div></dialog> <dialog class="qq-prompt-dialog-selector"> <div class="qq-dialog-message-selector"></div><input type="text"> <div class="qq-dialog-buttons"> <button type="button" class="qq-cancel-button-selector">Cancel</button> <button type="button" class="qq-ok-button-selector">Ok</button> </div></dialog> </div></div></script>
<div class="media-uploader"></div>
</div>
</div>
</main>
</div>
</div>
</div>
<script type='text/javascript'>
function csrfToken(){var a,b=null;if(document.cookie&&""!==document.cookie){var c=document.cookie.split(";");for(a=0;a<c.length;){var d=c[a].trim();if("csrftoken="===d.substring(0,10)){b=decodeURIComponent(d.substring(10));break}a+=1}}return b};
document.addEventListener("DOMContentLoaded", function(event) {
var default_concurrent_chunked_uploader = new qq.FineUploader({
debug: true,
element: document.querySelector('.media-uploader'),
request: {
endpoint: '/fu/upload/',
customHeaders: {
'X-CSRFToken': csrfToken('csrftoken'),
},
},
retry: {
enableAuto: true,
},
chunking: {
enabled: true,
concurrent: {
enabled: true,
},
success: {
endpoint: '/fu/upload/?done',
},
},
});
});
</script>

View File

@@ -0,0 +1,3 @@
<div class="page-main-wrap">
<% if (id) { %><div id="<%= id %>"></div><% } %>
</div>

View File

@@ -0,0 +1,5 @@
import { renderPage } from './static/js/utils/renderer';
import './static/css/AddMediaPage.scss';
renderPage();

View File

@@ -0,0 +1,3 @@
import { renderPage } from './static/js/utils/renderer';
renderPage();

View File

@@ -0,0 +1,15 @@
<% if (page) { %>
import { renderEmbedPage } from './static/js/utils/renderer';
<% if (page.component) { %>
import { <%= page.component %> } from './static/js/pages/<%= page.component %>';
<% if (page.id) { %>
renderEmbedPage( '<%= page.id %>', <%= page.component %> );
<% } %>
<% } %>
<% } %>

View File

@@ -0,0 +1,15 @@
<% if (page) { %>
import { renderPage } from './static/js/utils/renderer';
<% if (page.component) { %>
import { <%= page.component %> } from './static/js/pages/<%= page.component %>';
<% if (page.id) { %>
renderPage( '<%= page.id %>', <%= page.component %> );
<% } %>
<% } %>
<% } %>

View File

@@ -0,0 +1,15 @@
<% if (page) { %>
import { renderPage } from './static/js/utils/renderer';
<% if (page.component) { %>
import { <%= page.component %> } from './static/js/static-pages/<%= page.component %>';
<% if (page.id) { %>
renderPage( '<%= page.id %>', <%= page.component %> );
<% } %>
<% } %>
<% } %>

View File

@@ -0,0 +1,18 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="custom-page-wrapper">
<h2>About</h2>
<hr />
<p>
<a href="https://mediacms.io">MediaCMS</a> 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.
</p>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,144 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="media-uploader-wrap">
<div class="media-uploader-top-wrap">
<div class="media-uploader-top-left-wrap">
<h1>Upload media files</h1>
</div>
<div class="media-uploader-top-right-wrap"></div>
</div>
<div class="media-uploader">
<div class="media-uploader-bottom-wrap">
<div class="media-uploader-bottom-left-wrap">
<div class="media-drag-drop-wrap">
<div class="media-drag-drop-inner">
<div class="media-drag-drop-content">
<div class="media-drag-drop-content-inner">
<span><i class="material-icons">cloud_upload</i></span>
<span>Drag and drop files</span>
<span>or</span>
<span class="browse-files-btn-wrap">
<span
class="qq-upload-button-selector"
style="position: relative; overflow: hidden; direction: ltr"
>Browse your files<input
qq-button-id="9523a4a3-6688-40a5-b01d-7c72a79c7bcb"
title="file input"
multiple=""
type="file"
name="qqfile"
style="
position: absolute;
right: 0px;
top: 0px;
font-family: Arial;
font-size: 118px;
margin: 0px;
padding: 0px;
cursor: pointer;
opacity: 0;
height: 100%;
"
/></span>
</span>
<div class="qq-upload-drop-area-selector media-dropzone" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="media-uploader-bottom-right-wrap">
<ul class="media-upload-items-list qq-upload-list-selector">
<li>
<div class="media-upload-item-main">
<div class="media-upload-item-thumb">
<img class="qq-thumbnail-selector" alt="" />
<span class="media-upload-item-spinner qq-upload-spinner-selector"
><i class="material-icons">autorenew</i></span
>
<button type="button" class="qq-upload-retry-selector retry-media-upload-item" aria-label="Retry">
<i class="material-icons">refresh</i> Retry
</button>
</div>
<div class="media-upload-item-details">
<div class="media-upload-item-name">
<span class="media-upload-item-filename qq-upload-file-selector" title="">File #1</span>
<input
class="
media-upload-item-filename-input
qq-edit-filename-selector qq-edit-filename-icon-selector
"
tabindex="0"
type="text"
value="File #1"
/>
</div>
<div class="media-upload-item-details-bottom">
<div class="media-upload-item-progress-bar-container qq-progress-bar-container-selector">
<div
role="progressbar"
aria-valuenow="80"
aria-valuemin="0"
aria-valuemax="100"
class="media-upload-item-progress-bar qq-progress-bar-selector"
style="width: 80%"
></div>
</div>
<span class="media-upload-item-upload-size qq-upload-size-selector">80% of 7.5MB</span>
<span role="status" class="media-upload-item-status-text qq-upload-status-text-selector"
>Retrying 3/3...</span
>
</div>
<div class="media-upload-item-top-actions">
<span class="filename-edit qq-edit-filename-icon-selector" aria-label="Edit filename"
>Edit filename <i class="material-icons">create</i></span
>
<button
type="button"
class="delete-media-upload-item qq-upload-delete-selector"
aria-label="Delete"
>
Delete <i class="material-icons">delete</i>
</button>
<button
type="button"
class="cancel-media-upload-item qq-upload-cancel-selector"
aria-label="Cancel"
>
Cancel <i class="material-icons">cancel</i>
</button>
</div>
<div class="media-upload-item-bottom-actions">
<button
type="button"
class="continue-media-upload-item qq-upload-continue-selector"
aria-label="Continue"
>
<i class="material-icons">play_circle_outline</i> Continue
</button>
<button
type="button"
class="pause-media-upload-item qq-upload-pause-selector"
aria-label="Pause"
>
<i class="material-icons">pause_circle_outline</i> Pause
</button>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,27 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Contact us</h1>
<form enctype="multipart/form-data" action="" method="post" class="post-form">
<p>
<label for="id_from_email">Your email:</label>
<input type="email" name="from_email" required="" id="id_from_email" />
</p>
<p><label for="id_name">Your name:</label> <input type="text" name="name" id="id_name" /></p>
<p>
<label for="id_message">Please add your message here and submit:</label>
<textarea name="message" cols="40" rows="10" required="" id="id_message"></textarea>
</p>
<button class="primaryAction" type="submit">Submit</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,30 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Edit Channel</h1>
<form enctype="multipart/form-data" action="" method="post" class="post-form">
<div id="div_id_banner_logo" class="control-group">
<label for="id_banner_logo" class="control-label"> Banner logo </label>
<div class="controls">
Currently: <a href="#">*****</a>
<input type="checkbox" name="banner_logo-clear" id="banner_logo-clear_id" />
<label for="banner_logo-clear_id">Clear</label><br />
Change:
<input type="file" name="banner_logo" accept="image/*" class="clearablefileinput" id="id_banner_logo" />
</div>
</div>
<button class="primaryAction" type="submit">Update Channel</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,137 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Edit Media</h1>
<form enctype="multipart/form-data" action="" method="post" class="post-form">
<div id="div_id_title" class="control-group">
<label for="id_title" class="control-label"> Title </label>
<div class="controls">
<input
type="text"
name="title"
value="Media title..."
maxlength="100"
class="textinput textInput"
id="id_title"
/>
<p id="hint_id_title" class="help-block">media title</p>
</div>
</div>
<div id="div_id_category" class="control-group">
<label for="id_category" class="control-label"> Category </label>
<div class="controls">
<select name="category" class="selectmultiple" id="id_category" multiple="">
<option value="1">Art</option>
<option value="2">Documentary</option>
<option value="3">Experimental</option>
<option value="4">Film</option>
<option value="5">Music</option>
<option value="6">TV</option>
</select>
<p id="hint_id_category" class="help-block">Media can be part of one or more categories</p>
</div>
</div>
<div id="div_id_new_tags" class="control-group">
<label for="id_new_tags" class="control-label"> Tags </label>
<div class="controls">
<input type="text" name="new_tags" class="textinput textInput" id="id_new_tags" />
<p id="hint_id_new_tags" class="help-block">a comma separated list of new tags.</p>
</div>
</div>
<div id="div_id_add_date" class="control-group">
<label for="id_add_date" class="control-label"> Date produced </label>
<div class="controls">
<input type="text" name="add_date" value="2021-01-01 00:00:01" class="datetimeinput" id="id_add_date" />
</div>
</div>
<div id="div_id_uploaded_poster" class="control-group">
<label for="id_uploaded_poster" class="control-label"> Upload image </label>
<div class="controls">
<input
type="file"
name="uploaded_poster"
accept="image/*"
class="clearablefileinput"
id="id_uploaded_poster"
/>
<p id="hint_id_uploaded_poster" class="help-block">This image will characterize the media</p>
</div>
</div>
<div id="div_id_description" class="control-group">
<label for="id_description" class="control-label"> Description </label>
<div class="controls">
<textarea name="description" cols="40" rows="10" class="textarea" id="id_description"></textarea>
</div>
</div>
<div id="div_id_state" class="control-group">
<label for="id_state" class="control-label requiredField">
State<span class="asteriskField">*</span>
</label>
<div class="controls">
<select name="state" class="select" id="id_state">
<option value="private">Private</option>
<option value="public" selected="">Public</option>
<option value="unlisted">Unlisted</option>
</select>
<p id="hint_id_state" class="help-block">state of Media</p>
</div>
</div>
<div id="div_id_enable_comments" class="control-group">
<div class="controls">
<label for="id_enable_comments" class="checkbox">
<input
type="checkbox"
name="enable_comments"
class="checkboxinput"
id="id_enable_comments"
checked=""
/>
Enable comments
</label>
<p id="hint_id_enable_comments" class="help-block">Whether comments will be allowed for this media</p>
</div>
</div>
<div id="div_id_thumbnail_time" class="control-group">
<label for="id_thumbnail_time" class="control-label"> Thumbnail time </label>
<div class="controls">
<input
type="number"
name="thumbnail_time"
value="132.5"
step="any"
class="numberinput"
id="id_thumbnail_time"
/>
<p id="hint_id_thumbnail_time" class="help-block">Time on video that a thumbnail will be taken</p>
</div>
</div>
<div id="div_id_allow_download" class="control-group">
<div class="controls">
<label for="id_allow_download" class="checkbox">
<input
type="checkbox"
name="allow_download"
class="checkboxinput"
id="id_allow_download"
checked=""
/>
Allow download
</label>
<p id="hint_id_allow_download" class="help-block">Whether option to download media is shown</p>
</div>
</div>
<button class="primaryAction" type="submit">Update Media</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,43 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Edit Profile</h1>
<form enctype="multipart/form-data" action="" method="post" class="post-form">
<p>
<label for="id_name">Full name:</label>
<input type="text" name="name" value="" maxlength="250" required="" id="id_name" />
</p>
<p>
<label for="id_description">About me:</label>
<textarea name="description" cols="40" rows="10" id="id_description"></textarea>
</p>
<p>
<label for="id_email">Email address:</label>
<input type="email" name="email" value="" maxlength="254" id="id_email" />
</p>
<p>
<label for="id_logo">Logo:</label> Currently: <a href="#">*****</a>
<input type="checkbox" name="logo-clear" id="logo-clear_id" />
<label for="logo-clear_id">Clear</label><br />
Change:
<input type="file" name="logo" accept="image/*" id="id_logo" />
</p>
<p>
<label for="id_notification_on_comments"
>Whether you will receive email notifications for comments added to your content:</label
>
<input type="checkbox" name="notification_on_comments" id="id_notification_on_comments" checked="" />
</p>
<button class="primaryAction" type="submit">Update Profile</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,11 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="error-page-wrap">Error</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,45 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Sign Up</h1>
<p>Already have an account? Then please <a href="./signin.html">sign in</a>.</p>
<form class="signup" id="signup_form" method="post" action="#">
<input type="hidden" name="csrfmiddlewaretoken" value="" />
<p>
<label for="id_email">E-mail:</label>
<input type="email" name="email" id="id_email" required="" placeholder="E-mail address" />
</p>
<p>
<label for="id_username">Username:</label>
<input
type="text"
name="username"
id="id_username"
autofocus="autofocus"
placeholder="Username"
minlength="4"
maxlength="150"
required=""
/>
</p>
<p>
<label for="id_name">Name:</label>
<input type="text" name="name" id="id_name" maxlength="100" required="" />
</p>
<p>
<label for="id_password1">Password:</label>
<input type="password" name="password1" id="id_password1" required="" placeholder="Password" />
</p>
<button type="submit">Sign Up »</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,28 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Password Reset</h1>
<p>
Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset
it.
</p>
<form method="POST" action="#" class="password_reset">
<input type="hidden" name="csrfmiddlewaretoken" value="" />
<p>
<label for="id_email">E-mail:</label>
<input type="email" name="email" size="30" id="id_email" required="" placeholder="E-mail address" />
</p>
<input type="submit" value="Reset My Password" />
</form>
<p>Please contact us if you have any trouble resetting your password.</p>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,36 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Sign In</h1>
<p>If you have not created an account yet, then please <a href="./register.html">sign up</a> first.</p>
<form class="login" method="POST" action="#">
<input type="hidden" name="csrfmiddlewaretoken" value="" />
<p>
<label for="id_login">Login:</label>
<input
type="text"
name="login"
id="id_login"
required=""
autofocus="autofocus"
placeholder="Username or e-mail"
/>
</p>
<p>
<label for="id_password">Password:</label>
<input type="password" name="password" id="id_password" required="" placeholder="Password" />
</p>
<a class="button secondaryAction" href="./reset-password.html">Forgot Password?</a>
<button class="primaryAction" type="submit">Sign In</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,20 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="user-action-form-wrap">
<div class="user-action-form-inner">
<h1>Sign Out</h1>
<p>Are you sure you want to sign out?</p>
<form method="post" action="#">
<input type="hidden" name="csrfmiddlewaretoken" value="" />
<button type="submit">Sign Out</button>
</form>
</div>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

View File

@@ -0,0 +1,15 @@
<div id="app-header"></div>
<div id="app-sidebar"></div>
<div class="page-main-wrap">
<div class="page-main">
<div class="page-main-inner">
<div class="custom-page-wrapper">
<h2>Terms</h2>
<hr />
<p>Terms of service</p>
</div>
</div>
<div class="page-sidebar-content-overlay"></div>
</div>
</div>
<div id="app-footer"></div>

39075
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
frontend/package.json Executable file
View File

@@ -0,0 +1,53 @@
{
"name": "mediacms-frontend",
"version": "0.9.1",
"description": "",
"author": "",
"license": "",
"keywords": [],
"main": "index.js",
"scripts": {
"start": "mediacms-scripts development --config=./config/mediacms.config.js --host=0.0.0.0 --port=8088",
"dist": "mediacms-scripts rimraf ./dist && mediacms-scripts build --config=./config/mediacms.config.js --env=dist"
},
"browserslist": [
"cover 99.5%"
],
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.7",
"autoprefixer": "^10.2.6",
"babel-loader": "^8.2.2",
"compass-mixins": "^0.12.10",
"copy-webpack-plugin": "^9.0.0",
"core-js": "^3.14.0",
"css-loader": "^5.2.6",
"dotenv": "^10.0.0",
"ejs": "^3.1.6",
"ejs-compiled-loader": "^3.1.0",
"mediacms-scripts": "file:packages/scripts",
"postcss-loader": "^6.1.0",
"prettier": "^2.3.1",
"prop-types": "^15.7.2",
"sass": "^1.34.1",
"sass-loader": "^12.1.0",
"ts-loader": "^9.2.3",
"typescript": "^4.3.2",
"url-loader": "^4.1.1",
"webpack": "^5.38.1"
},
"dependencies": {
"axios": "^0.21.1",
"flux": "^4.0.1",
"mediacms-player": "file:packages/player",
"normalize.css": "^8.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.1"
}
}

View File

@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@@ -0,0 +1,18 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 1
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.md]
insert_final_newline = true
trim_trailing_whitespace = false

View File

@@ -0,0 +1,8 @@
{
"minify": true,
"options": [],
"feature-detects": [
"css/transforms",
"test/storage/localstorage"
]
}

View File

@@ -0,0 +1 @@
# mediacms-player

View File

@@ -0,0 +1,113 @@
import gzip from 'rollup-plugin-gzip';
import postcss from 'rollup-plugin-postcss';
import babel from 'rollup-plugin-babel';
import cleanup from 'rollup-plugin-cleanup';
// import { uglify } from "rollup-plugin-uglify";
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import visualizer from 'rollup-plugin-visualizer';
import json from '@rollup/plugin-json';
export default function rollup_builds(input_file, output_folder, pkg) {
const package_name = pkg.name;
const dependencies = pkg.dependencies;
const dependencies_names = !!dependencies ? Object.keys(pkg.dependencies) : [];
const esm_format = 'es';
const browser_format = 'umd';
const commonjs_format = 'cjs';
const postcss_config = {
extract: true,
modules: false, // Avoid adding prefixes to classnames (etc).
extensions: ['.css', '.sss', '.pcss', '.scss'],
};
const postcss_plugin = postcss(postcss_config);
const postcss_plugin_minimized = postcss({ ...postcss_config, minimize: true });
const commonjs_resolve_config = {
// pass custom options to the resolve plugin
customResolveOptions: { moduleDirectory: 'node_modules' },
};
function beautify_plugin() {
return cleanup(/*{
maxEmptyLines: 1,
sourcemap: false,
}*/);
}
function visualizer_plugin(name) {
return visualizer({
title: name,
filename: output_folder + '/visualizer/' + name + '.html',
});
}
function es_build(filename, visualize, bundle) {
const plugins = [postcss_plugin, json(), beautify_plugin()];
if (!!visualize) {
plugins.push(visualizer_plugin(filename));
}
return {
input: input_file,
external: !!bundle ? {} : dependencies_names,
output: [{ format: esm_format, file: filename }],
plugins: plugins,
};
}
function commonjs_build(filename, visualize, bundle) {
const plugins = [postcss_plugin, json(), resolve(commonjs_resolve_config), beautify_plugin()];
if (!!visualize) {
plugins.push(visualizer_plugin(filename));
}
return {
input: input_file,
external: !!bundle ? {} : dependencies_names,
output: [{ format: commonjs_format, file: filename }],
plugins: plugins,
};
}
function browser_build(filename, visualize, minimize, compact) {
const plugins = [
!!minimize ? postcss_plugin_minimized : postcss_plugin,
json(),
babel(),
resolve(),
commonjs(),
beautify_plugin(),
];
if (!!minimize) {
// plugins.push( uglify() );
if (!!compact) {
plugins.push(gzip());
}
}
if (!!visualize) {
plugins.push(visualizer_plugin(filename));
}
return {
input: input_file,
output: { name: package_name, format: browser_format, file: filename },
plugins: plugins,
};
}
return Object.freeze({
es: es_build,
browser: browser_build,
commonjs: commonjs_build,
});
}

View File

@@ -0,0 +1,11 @@
import rollup_builds from './includes/rollup_builds';
import pckg from '../package.json';
const dists = rollup_builds('./src/index.js', './out', pckg);
export default [
dists.browser('./dist/mediacms-player.js'),
// dists.browser("./dist/mediacms-player.js", true),
// dists.browser("./dist/mediacms-player.min.js", true, true),
// dists.browser("./dist/mediacms-player.min.js", true, true, true)
];

View File

@@ -0,0 +1,6 @@
import rollup_builds from './includes/rollup_builds';
import pckg from '../package.json';
const dists = rollup_builds('./src/index.js', './out', pckg);
export default [dists.browser('./dist/mediacms-player.js')];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

16686
frontend/packages/player/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
{
"name": "mediacms-player",
"version": "0.9.0",
"description": "",
"author": "",
"license": "",
"engines": {
"node": ">=14.17.0"
},
"keywords": [
"mediacms",
"media player",
"videojs"
],
"main": "./dist/mediacms-player.js",
"module": "./src/",
"browser": "./dist/mediacms-player.js",
"files": [
"dist"
],
"browserslist": [
"defaults"
],
"scripts": {
"start": "npx rollup -w -c config/rollup.config.js",
"build": "npx rollup -c config/rollup.config.build.js",
"clean:build": "node ./scripts/rmrf.js ./dist"
},
"peerDependencies": {
"video.js": "^7.12.3"
},
"dependencies": {
"mediacms-vjs-plugin": "file:../vjs-plugin"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"ajv": "^8.6.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"core-js": "^3.14.0",
"css-loader": "^5.2.6",
"global": "^4.4.0",
"json-loader": "^0.5.7",
"node-sass": "^6.0.0",
"postcss": "^8.3.2",
"rollup": "^2.51.2",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-gzip": "^2.5.0",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-visualizer": "^5.5.0",
"sass-loader": "^12.1.0",
"style-loader": "^2.0.0",
"trim-newlines": "^4.0.2"
}
}

View File

@@ -0,0 +1,21 @@
var fs = require('fs');
var path = require('path');
var rimraf = require('rimraf');
var cliArgs = process.argv.slice(2);
function rmdir_callback(err) {
if (err) {
throw err;
}
}
var i, dir;
for (i = 0; i < cliArgs.length; i++) {
dir = path.resolve(cliArgs[i]);
if (fs.existsSync(dir)) {
rimraf.sync(dir, {}, rmdir_callback);
}
}

View File

@@ -0,0 +1,453 @@
import 'mediacms-vjs-plugin/dist/mediacms-vjs-plugin.js';
import 'mediacms-vjs-plugin/dist/mediacms-vjs-plugin.css';
function isString(v) {
return 'string' === typeof v || v instanceof String;
}
function isArray(v) {
return !Array.isArray ? '[object Array]' === Object.prototype.toString.call(v) : Array.isArray(v);
}
function isBoolean(v) {
return 'boolean' === typeof v || v instanceof Boolean;
}
function ifBooleanElse(bol, els) {
return isBoolean(bol) ? bol : els;
}
const defaults = {
options: {
sources: [],
keyboardControls: !0,
enabledTouchControls: !0,
nativeDimensions: !1,
suppressNotSupportedError: !0,
poster: '',
loop: !1,
controls: !0,
preload: 'auto',
autoplay: !1,
bigPlayButton: !0,
liveui: !1,
controlBar: {
bottomBackground: !0,
progress: !0,
play: !0,
next: !1,
previous: !1,
volume: !0,
pictureInPicture: !0, // @link: https://docs.videojs.com/control-bar_picture-in-picture-toggle.js.html
fullscreen: !0,
theaterMode: !0,
time: !0,
},
cornerLayers: {
topLeft: null,
topRight: null,
bottomLeft: null,
bottomRight: null,
},
videoPreviewThumb: {},
subtitles: {
on: false,
default: null,
languages: [],
},
},
};
/**
* Filter plugin options values.
* @param {Object} opt Options object.
* @return {Object} Filtered/Validated options object.
*/
function filterPlayerOptions(domPlayer, opt) {
let k, x, j, i;
opt.sources = isArray(opt.sources) && opt.sources.length ? opt.sources : [];
opt.loop = ifBooleanElse(opt.loop, defaults.options.loop);
opt.controls = ifBooleanElse(opt.controls, defaults.options.controls);
if (opt.subtitles && opt.subtitles instanceof Object) {
opt.subtitles.default = void 0 !== opt.subtitles.default ? opt.subtitles.default : defaults.options.subtitles.default;
opt.subtitles.languages = isArray(opt.subtitles.languages)
? opt.subtitles.languages
: defaults.options.subtitles.languages;
opt.subtitles.on = ifBooleanElse(opt.subtitles.on, defaults.options.subtitles.on);
} else {
opt.subtitles.default = defaults.options.subtitles;
}
opt.autoplay =
'any' === opt.autoplay || 'play' === opt.autoplay || 'muted' === opt.autoplay
? opt.autoplay
: ifBooleanElse(opt.autoplay, defaults.options.autoplay);
// console.log(opt.autoplay);
opt.bigPlayButton = ifBooleanElse(opt.bigPlayButton, defaults.options.bigPlayButton);
opt.poster = isString(opt.poster) && '' !== opt.poster.trim() ? opt.poster : defaults.options.poster;
opt.preload =
isString(opt.preload) && -1 < ['auto', 'metadata', 'none'].indexOf(opt.preload.trim())
? opt.preload
: defaults.options.preload;
// Control bar options.
if (opt.controlBar && opt.controlBar instanceof Object && Object.keys(opt.controlBar).length) {
for (k in opt.controlBar) {
if (opt.controlBar.hasOwnProperty(k)) {
opt.controlBar[k] = ifBooleanElse(opt.controlBar[k], defaults.options.controlBar[k]);
}
}
}
// Corner layers.
if (opt.cornerLayers && opt.cornerLayers instanceof Object && Object.keys(opt.cornerLayers).length) {
for (k in opt.cornerLayers) {
if (opt.cornerLayers.hasOwnProperty(k)) {
if ('string' === typeof opt.cornerLayers[k]) {
opt.cornerLayers[k] = '' !== opt.cornerLayers[k] ? opt.cornerLayers[k] : defaults.options.cornerLayers[k];
} else if (Node.prototype.isPrototypeOf(opt.cornerLayers[k]) || !isNaN(opt.cornerLayers[k])) {
opt.cornerLayers[k] = opt.cornerLayers[k];
} else {
opt.cornerLayers[k] = opt.cornerLayers[k] || defaults.options.cornerLayers[k];
}
} else {
opt.cornerLayers[k] = defaults.options.cornerLayers[k];
}
}
}
opt.previewSprite = 'object' === typeof opt.previewSprite ? opt.previewSprite : {};
// Include HTML sources.
let obj;
let sources_el = domPlayer.querySelectorAll('source');
i = 0;
while (i < sources_el.length) {
if (void 0 !== sources_el[i].attributes.src) {
obj = {
src: sources_el[i].src,
};
if (void 0 !== sources_el[i].attributes.type) {
obj.type = sources_el[i].type;
}
x = 0;
while (x < opt.sources.length && obj.src !== opt.sources[x].src) {
x += 1;
}
if (x >= opt.sources.length) {
opt.sources.push(obj);
}
}
i += 1;
}
// Include HTML subtitle tracks.
let subs_el = domPlayer.querySelectorAll('track[kind="subtitles"]');
const subtitles_options = {
on: opt.subtitles.on,
default: null,
languages: [],
};
const languages = {};
function addSubtitle(track) {
track.src = void 0 !== track.src && null !== track.src ? track.src.toString().trim() : '';
track.srclang = void 0 !== track.srclang && null !== track.srclang ? track.srclang.toString().trim() : '';
if (track.src.length && track.srclang.length) {
track.label = void 0 !== track.label && null !== track.label ? track.label.toString().trim() : track.srclang;
if (void 0 !== languages[track.srclang]) {
languages[track.srclang].src = track.src;
languages[track.srclang].label = track.label;
} else {
subtitles_options.languages.push({
label: track.label,
src: track.src,
srclang: track.srclang,
});
languages[track.srclang] = subtitles_options.languages[subtitles_options.languages.length - 1];
}
if (void 0 !== track.default && null !== track.default) {
track.default = track.default.toString().trim();
if (!track.default.length || '1' === track.default || 'true' === track.default) {
subtitles_options.default = track.srclang;
}
}
}
}
i = 0;
while (i < subs_el.length) {
addSubtitle({
src: subs_el[i].getAttribute('src'),
srclang: subs_el[i].getAttribute('srclang'),
default: subs_el[i].getAttribute('default'),
label: subs_el[i].getAttribute('label'),
});
i += 1;
}
if (opt.subtitles.languages.length) {
i = 0;
while (i < opt.subtitles.languages.length) {
addSubtitle({
src: opt.subtitles.languages[i].src,
srclang: opt.subtitles.languages[i].srclang,
default: opt.subtitles.languages[i].default,
label: opt.subtitles.languages[i].label,
});
i += 1;
}
}
if (null !== opt.subtitles.default && void 0 !== languages[opt.subtitles.default]) {
subtitles_options.default = opt.subtitles.default;
}
if (null === subtitles_options.default && opt.subtitles.languages.length) {
subtitles_options.default = opt.subtitles.languages[0].srclang;
}
opt.subtitles = subtitles_options;
return opt;
}
/**
* Construct VideoJs options by player options.
* @param {Object} opt Plugin options.
* @param {Object} vjopt Initial VideoJs object.
* @return {Object} Final VideoJs object.
*/
function constructVideojsOptions(opt, vjopt) {
// {
// /*autoplay: false,
// controls: true,
// preload: "auto", // preload: "metadata",
// loop: false,
// bigPlayButton: true,*/
// // poster: "",
// // width: "",
// // height: "",
// // children: {}
// controlBar: {
// children: [],
// // children: {
// // bottomGradientComponent: true,
// // progressControl: true, // (hidden during live playback)
// // leftControls: true,
// // // playbackRateMenuButton: true, // (hidden, unless playback tech supports rate changes)
// // // chaptersButton: true, // (hidden, unless there are relevant tracks)
// // // descriptionsButton: true, // (hidden, unless there are relevant tracks)
// // // subtitlesButton: true, // (hidden, unless there are relevant tracks)
// // // captionsButton: true, // (hidden, unless there are relevant tracks)
// // // audioTrackButton: true, // (hidden, unless there are relevant tracks)
// // }
// // seekBar: false,
// // loadProgressBar: false,
// // mouseTimeDisplay: false,
// // playProgressBar: false,
// // liveDisplay: false, // (hidden during VOD playback)
// // remainingTimeDisplay: false,
// // customControlSpacer: false, // (has no UI)
// // playbackRateMenuButton: true, // (hidden, unless playback tech supports rate changes)
// // chaptersButton: true, // (hidden, unless there are relevant tracks)
// // descriptionsButton: true, // (hidden, unless there are relevant tracks)
// // subtitlesButton: true, // (hidden, unless there are relevant tracks)
// // captionsButton: true, // (hidden, unless there are relevant tracks)
// // audioTrackButton: true, // (hidden, unless there are relevant tracks)
// }
// }
vjopt.sources = opt.sources;
vjopt.loop = opt.loop;
vjopt.controls = opt.controls;
vjopt.autoplay = opt.autoplay;
vjopt.bigPlayButton = opt.bigPlayButton;
vjopt.poster = opt.poster;
vjopt.preload = opt.preload;
vjopt.suppressNotSupportedError = opt.suppressNotSupportedError;
// console.log( vjopt );
// console.log( opt );
return vjopt;
}
/**
* A wrapper/container class of MediaCMS VideoJs player.
* @param {DOM Node} domPlayer The video element in html.
* @param {Object} pluginOptions Plugin (genral player's) options.
* @param {Object} pluginState Plugin initial state values.
* @param {Function} pluginStateUpdateCallback The function will be called on plugin's state values update.
*/
export function MediaPlayer(
domPlayer,
pluginOptions,
pluginState,
videoResolutions,
videoPlaybackSpeeds,
pluginStateUpdateCallback,
onNextButtonClick,
onPrevButtonClick
) {
if (!Node.prototype.isPrototypeOf(domPlayer)) {
console.error('Invalid player DOM element', domPlayer); // TODO: Validate that element is <video> or <audio>.
return null;
}
function sourcesSrcs(urls) {
const ret = [];
let i = 0;
while (i < urls.length) {
if (!!urls[i]) {
ret.push(urls[i]); // @todo: Validate url file extension.
}
i += 1;
}
return ret;
}
function sourcesFormats(formats) {
const ret = [];
let i = 0;
while (i < formats.length) {
if (!!formats[i]) {
ret.push(formats[i]); // @todo: Validate format.
}
i += 1;
}
return ret;
}
let k,
i,
pluginVideoResolutions = {},
pluginVideoPlaybackSpeeds = {};
if (!!videoResolutions) {
for (k in videoResolutions) {
if (videoResolutions.hasOwnProperty(k)) {
if (
isArray(videoResolutions[k].url) &&
videoResolutions[k].url.length &&
isArray(videoResolutions[k].format) &&
videoResolutions[k].format.length
) {
pluginVideoResolutions[k] = {
title: k,
src: sourcesSrcs(videoResolutions[k].url),
format: sourcesFormats(videoResolutions[k].format),
};
}
}
}
}
if (!!videoPlaybackSpeeds) {
k = 0;
while (k < videoPlaybackSpeeds.length) {
pluginVideoPlaybackSpeeds[k] = {
title: 1 === videoPlaybackSpeeds[k] ? 'Normal' : videoPlaybackSpeeds[k],
speed: videoPlaybackSpeeds[k].toString(),
};
k += 1;
}
}
/*
* Filter options value.
*/
// console.log( '####################' );
// console.log( domPlayer );
// console.log( defaults.options );
// console.log( Object.keys(pluginOptions) );
pluginOptions = filterPlayerOptions(
domPlayer,
videojs.mergeOptions(
defaults.options,
pluginOptions && pluginOptions instanceof Object && Object.keys(pluginOptions).length ? pluginOptions : {}
)
);
// console.log( pluginOptions );
// console.log( '####################' );
/*
* Filter state value.
*/
// console.log( '####################' );
// console.log( pluginState );
// console.warn( pluginOptions.subtitles );
// console.log( pluginState );
if (null !== pluginOptions.subtitles.default && pluginOptions.subtitles.on) {
pluginState.theSelectedSubtitleOption = pluginOptions.subtitles.default;
}
// console.log( pluginState );
// console.log( pluginState );
// console.log( '####################' );
/*
* Initialize videojs player.
*/
const passOptions = constructVideojsOptions(pluginOptions, {
controlBar: {
children: [],
},
});
this.player = videojs(domPlayer, passOptions);
/*
* Call plugin.
*/
this.player.mediaCmsVjsPlugin(
domPlayer,
pluginOptions,
pluginState,
pluginVideoResolutions,
pluginVideoPlaybackSpeeds,
pluginStateUpdateCallback,
onNextButtonClick,
onPrevButtonClick
);
/*
* Public methods.
*/
this.isEnded = this.player.mediaCmsVjsPlugin().isEnded;
this.isFullscreen = this.player.mediaCmsVjsPlugin().isFullscreen;
this.isTheaterMode = this.player.mediaCmsVjsPlugin().isTheaterMode;
if (void 0 !== typeof window) {
window.HELP_IMPROVE_VIDEOJS = false;
}
}

View File

@@ -0,0 +1,3 @@
import { MediaPlayer } from './MediaPlayer';
export default MediaPlayer;

View File

@@ -0,0 +1,36 @@
module.exports = {
"extends": ["eslint:recommended"],
"env": { "es6": true, "browser": true, "node": true },
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018
},
"rules": {
"indent": ["error", "tab"],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
'no-empty': ["error", { "allowEmptyCatch": true }],
'no-constant-condition': ["error", { "checkLoops": false }],
},
"overrides": [{
"plugins": [ "@typescript-eslint" ],
"parser": "@typescript-eslint/parser",
"files": ["**/*.ts", "**/*.tsx"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"@typescript-eslint/no-var-requires": "off"
},
}]
}

View File

@@ -0,0 +1 @@
# mediacms-scripts

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env node
const spawn = require('cross-spawn');
('use strict');
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
const args = process.argv.slice(2);
const scriptIndex = args.findIndex(
(x) => x === 'build' || x === 'development' || x === 'analyzer' || x === 'rimraf' // TODO
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
if (['build', 'development', 'analyzer', 'rimraf'].includes(script)) {
const result = spawn.sync(
process.execPath,
nodeArgs.concat(require.resolve('./scripts/' + script)).concat(args.slice(scriptIndex + 1)),
{ stdio: 'inherit' }
);
if (result.signal) {
if (result.signal === 'SIGKILL') {
console.log(
'The build failed because the process exited too early. ' +
'This probably means the system ran out of memory or someone called ' +
'`kill -9` on the process.'
);
} else if (result.signal === 'SIGTERM') {
console.log(
'The build failed because the process exited too early. ' +
'Someone might have called `kill` or `killall`, or the system could ' +
'be shutting down.'
);
}
process.exit(1);
}
process.exit(result.status);
} else {
console.log('Unknown script "' + script + '".');
}

View File

@@ -0,0 +1,3 @@
import { buildCommonjs } from './helpers/buildCommonjs.js';
export default buildCommonjs('./src/index.ts', '.')('./dist/webpack-dev-env.js');

View File

@@ -0,0 +1,24 @@
import typescript from 'rollup-plugin-typescript2';
import resolve from '@rollup/plugin-node-resolve';
import visualizer from 'rollup-plugin-visualizer';
import cleanup from 'rollup-plugin-cleanup';
export function buildCommonjs(input_file, output_folder) {
return function (filename, visualize) {
const plugins = [
resolve({ customResolveOptions: { moduleDirectories: 'node_modules' } }),
typescript(),
cleanup({ comments: 'none' }),
];
if (visualize) {
plugins.push(visualizer({ title: filename, filename: output_folder + filename + '.html' }));
}
return {
input: input_file,
output: [{ format: 'cjs', file: filename }],
plugins: plugins,
};
};
}

View File

@@ -0,0 +1,3 @@
import { buildCommonjs } from './helpers/buildCommonjs.js';
export default buildCommonjs('./src/index.ts', './visualizer/')('./dist/webpack-dev-env.js', true);

View File

@@ -0,0 +1,3 @@
import { buildCommonjs } from './helpers/buildCommonjs.js';
export default buildCommonjs('./src/index.ts', '.')('./dist/webpack-dev-env.js');

View File

@@ -0,0 +1,782 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || from);
}
function bodySnippet(id) {
return '<div id="' + id + '"></div>';
}
var homePage = {
staticPage: true,
buildExclude: false,
title: 'Home',
filename: 'index.html',
html: {
head: {},
body: {
scripts: [],
snippet: bodySnippet('page-home'),
}
},
window: {},
render: 'import { renderPage } from \'./js/helpers\'; import { HomePage } from \'./js/pages/HomePage\'; renderPage( \'page-home\', HomePage );',
};
var errorPage = {
staticPage: true,
buildExclude: false,
title: 'Error',
filename: 'error.html',
html: {
head: {},
body: {
scripts: [],
snippet: bodySnippet('page-error'),
}
},
window: {},
render: 'import { renderPage } from \'./js/helpers\'; import { ErrorPage } from \'./js/pages/ErrorPage\'; renderPage( \'page-error\', ErrorPage );',
};
var pages = {
home: homePage,
error: errorPage,
};
var htmlHead = {
meta: [
{ charset: 'utf-8' },
{ content: 'ie=edge', 'http-equiv': 'x-ua-compatible' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
links: [],
scripts: [],
};
var htmlBody = {
scripts: [],
snippet: '',
};
var html = {
head: htmlHead,
body: htmlBody,
};
var config$3 = {
src: '',
build: '',
pages: pages,
html: html,
window: {},
postcssConfigFile: '',
};
/*const chunksCacheGroups_0 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_1 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
// priority: -10,
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
// test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
// test: /[\\/]node_modules[\\/](!MediaCmsPlayer)[\\/]/,
name: "_vendors",
// priority: -20,
chunks: "all",
enforce: true,
// reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_2 = {
commons: {
minChunks: 2,
// maxInitialRequests: 8, // @note: Tested values from 0 to 10, and changes applied with values 0, 4, 5, 6, 7, 8.
// minSize: 0,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_3 = {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
};*/
var config$2 = {
mode: 'production',
devtool: 'source-map',
optimization: {
runtimeChunk: false,
/*splitChunks: {
// minSize: 1000000,
chunks: 'all',
automaticNameDelimiter: '-',
},*/
/*splitChunks: {
// minSize: 1000000,
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_0,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_1,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_2,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_3,
},*/
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
},
},
}
};
/*const chunksCacheGroups_0 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};
const chunksCacheGroups_1 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
// priority: -10,
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
// test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
// test: /[\\/]node_modules[\\/](!MediaCmsPlayer)[\\/]/,
name: "_vendors",
// priority: -20,
chunks: "all",
enforce: true,
// reuseExistingChunk: true,
},
};
const chunksCacheGroups_2 = {
commons: {
minChunks: 2,
// maxInitialRequests: 8, // @note: Tested values from 0 to 10, and changes applied with values 0, 4, 5, 6, 7, 8.
// minSize: 0,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};
const chunksCacheGroups_3 = {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
};*/
var config$1 = {
mode: 'production',
optimization: {
minimize: true,
runtimeChunk: false,
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
},
},
}
};
/**
* @see {link: https://github.com/seeyoulater/html-beautify-webpack-plugin/blob/master/index.js}
*/
var prettify = require('html-prettify');
var HtmlWebpackPlugin$1 = require('html-webpack-plugin');
require('webpack/lib/WebpackError.js');
function htmlPluginDataFunction(pluginData, options, callback) {
pluginData.html = prettify(options.replace.reduce(function (res, item) { return res.replace(item instanceof RegExp ? new RegExp(item, 'gi') : item, ''); }, pluginData.html) /*,
options.config*/);
callback(null, pluginData);
}
var MyHtmlBeautifyWebpackPlugin = /** @class */ (function () {
function MyHtmlBeautifyWebpackPlugin() {
}
MyHtmlBeautifyWebpackPlugin.prototype.apply = function (compiler) {
var options = {
config: {
indent_size: 4,
indent_with_tabs: false,
html: {
end_with_newline: true,
indent_inner_html: true,
preserve_newlines: true,
max_preserve_newlines: 0,
}
},
replace: []
};
function tapAsyncCallback(pluginData, callback) {
return htmlPluginDataFunction(pluginData, options, callback);
}
function tapHookCallback(compilation) {
return HtmlWebpackPlugin$1.getHooks(compilation).beforeEmit.tapAsync('MyHtmlBeautifyWebpackPlugin', tapAsyncCallback);
}
compiler.hooks.compilation.tap('MyHtmlBeautifyWebpackPlugin', tapHookCallback);
};
return MyHtmlBeautifyWebpackPlugin;
}());
var fs = require('fs');
var path$1 = require('path');
var ejs = require('ejs');
var templatePath = path$1.join(__dirname, '../templates');
var sitemapTemplatePath = path$1.join(templatePath, 'sitemap.ejs');
var sitemapTemplate = ejs.compile(fs.readFileSync(sitemapTemplatePath, 'utf8'), { root: [templatePath], filename: sitemapTemplatePath, outputFunctionName: 'echo' });
function pagesConfig(pagesKeys) {
var pages = {};
if (-1 === pagesKeys.indexOf('sitemap')) {
pages.sitemap = {
staticPage: true,
buildExclude: true,
title: 'Sitemap',
filename: 'sitemap.html',
html: {
head: {},
body: {
scripts: [],
snippet: sitemapTemplate({ pages: __spreadArray(__spreadArray([], pagesKeys), Object.keys(pages)) }),
},
},
window: {},
render: ''
};
}
return pages;
}
var merge = require('lodash.merge');
function validateBoolean(value, defaultValue) {
if (defaultValue === void 0) { defaultValue = false; }
if (true === value || false === value) {
return value;
}
if (0 === value || 1 === value) {
return !!value;
}
return defaultValue;
}
function validateString(value, defaultValue) {
if (defaultValue === void 0) { defaultValue = ''; }
return value ? value : defaultValue;
}
function getArrayType(sourcesArr, pageArr) {
if (pageArr === void 0) { pageArr = []; }
if ((!sourcesArr || !sourcesArr.length) && (!pageArr || !pageArr.length)) {
return [];
}
if (sourcesArr && sourcesArr.length && pageArr && pageArr.length) {
return sourcesArr.concat(pageArr);
}
if (sourcesArr && sourcesArr.length) {
return sourcesArr;
}
return pageArr;
}
function formatPagesConfig(sources, pages) {
var ret = {};
for (var pk in pages) {
ret[pk] = {
staticPage: validateBoolean(pages[pk].staticPage, false),
buildExclude: validateBoolean(pages[pk].buildExclude, false),
title: validateString(pages[pk].title, sources.title),
filename: validateString(pages[pk].filename, sources.filename),
html: {
head: {
meta: getArrayType(sources.html.head.meta, pages[pk].html.head.meta),
links: getArrayType(sources.html.head.links, pages[pk].html.head.links),
scripts: getArrayType(sources.html.head.scripts, pages[pk].html.head.scripts),
},
body: {
scripts: getArrayType(sources.html.body.scripts, pages[pk].html.body.scripts),
snippet: validateString(pages[pk].html.body.snippet, sources.html.body.snippet),
},
},
window: merge({}, sources.window, pages[pk].window),
render: validateString(sources.render, pages[pk].render),
};
}
return ret;
}
var path = require('path');
var NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
// Webpack plugins.
var DefinePlugin = require('webpack').DefinePlugin;
var LimitChunkCountPlugin = require('webpack').optimize.LimitChunkCountPlugin;
var HtmlWebpackPlugin = require('html-webpack-plugin');
var VirtualModulesPlugin = require('webpack-virtual-modules');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var ProgressBarPlugin = require('progress-bar-webpack-plugin');
var CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
var CopyPlugin = require("copy-webpack-plugin");
var dotenv = require('dotenv').config({ path: path.resolve(__dirname + '../../../../.env') });
function webpackEntry(env, srcDir, pages) {
var ret = {};
for (var p in pages) {
if ('development' === env || !pages[p].buildExclude) {
ret[p] = path.resolve(srcDir + '/' + p + '.js');
}
}
return ret;
}
function webpackOutput(env, destinationDir, buildDir, chunkhash, hash) {
var ret = {
path: destinationDir,
filename: '',
};
var prefix = 'development' === env ? '' : buildDir;
var tmp;
if (undefined !== chunkhash) {
tmp = chunkhash.trim();
if ('' === tmp) {
throw Error('Invalid chunkhash argument value: ' + chunkhash);
}
ret.filename = (prefix || '') + '[name]-[chunkhash].js';
}
else if (undefined !== hash) {
tmp = hash.trim();
if ('' === tmp) {
throw Error('Invalid hash argument value: ' + hash);
}
ret.filename = (prefix || '') + '[name]-[hash].js';
}
else {
ret.filename = (prefix || '') + '[name].js';
}
return ret;
}
function webpackAlias() {
return {
// modernizr$: path.resolve(__dirname, "../../.modernizrrc"), // TODO: Enable this?
};
}
function webpackRules(env, srcDir, postcssConfigFile) {
return [{
test: /\.(jsx|js)?$/,
use: 'babel-loader'
},
{
test: /\.(tsx|ts)?$/,
use: 'ts-loader',
// exclude: /node_modules/,
// options: {
// compilerOptions: {
// "sourceMap": !isProduction,
// },
// },
},
{
test: /\.ejs$/,
use: {
loader: 'ejs-compiled-loader',
options: {
// beautify: true,
htmlmin: true,
// htmlminOptions: {
// removeComments: true,
// collapseWhitespace: true,
// preserveLineBreaks: true
// }
}
}
},
{
test: /\.(sa|sc|c)ss$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
// { loader: 'development' === env ? MiniCssExtractPlugin.loader : 'style-loader' }, // Use inline <style> tag.
{ loader: 'css-loader', options: { importLoaders: 1 } },
{ loader: 'postcss-loader', options: { postcssOptions: { config: postcssConfigFile } } },
{ loader: 'sass-loader' },
],
},
{
test: /\.module\.(sa|sc|c)ss$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
// { loader: 'development' === env ? MiniCssExtractPlugin.loader : 'style-loader' }, // Use inline <style> tag.
{ loader: 'css-loader', options: { importLoaders: 1, modules: true, onlyLocals: false } },
{ loader: 'postcss-loader', options: { postcssOptions: { config: postcssConfigFile } } },
{ loader: 'sass-loader' },
]
},
{
test: /\.(png|jpe?g|gif)(\?\S*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 1024,
fallback: 'file-loader',
name: function (file) {
return '.' + path.join(file.replace(srcDir, ''), '..').replace(/\\/g, '/') + '/[name].[ext]';
},
},
},
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
/*issuer: {
test: /\.jsx?$/
},*/
use: ['babel-loader', '@svgr/webpack', 'url-loader']
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader'
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'file-loader',
options: {
name: function (file) {
return '.' + path.join(file.replace(srcDir, ''), '..').replace(/\\/g, '/') + '/[name].[ext]';
},
}
}]
},
{
test: /\.modernizrrc.js$/,
use: 'modernizr-loader',
},
{
test: /\.modernizrrc(\.json)?$/,
use: ['modernizr-loader', 'json-loader'],
}];
}
function webpackPlugins(env, srcDir, pages, cssSrc) {
var ret = [
new DefinePlugin({ "process.env": JSON.stringify(dotenv.parsed) }),
new NodePolyfillPlugin(),
new MyHtmlBeautifyWebpackPlugin(),
];
if ('development' !== env) {
ret.push(new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../../../src/static/lib'),
to: path.resolve(__dirname, '../../../' + env + '/static/lib'),
},
{
from: path.resolve(__dirname, '../../../src/static/images'),
to: path.resolve(__dirname, '../../../' + env + '/static/images'),
},
{
from: path.resolve(__dirname, '../../../src/static/favicons'),
to: path.resolve(__dirname, '../../../' + env + '/static/favicons'),
},
{
from: path.resolve(__dirname, '../../../src/static/css/_extra.css'),
to: path.resolve(__dirname, '../../../' + env + '/static/css/_extra.css'),
},
],
}));
}
var virtualPages = {};
var file;
for (var k in pages) {
if ('production' !== env || !pages[k].buildExclude) {
file = path.resolve(srcDir + '/' + k + '.js');
if ((void 0 !== pages[k].staticPage && pages[k].staticPage) || void 0 === pages[k].render) {
virtualPages[file] = '';
}
else {
virtualPages[file] = pages[k].render;
}
}
if ('development' === env) {
// Export pages HTML files.
ret.push(new HtmlWebpackPlugin(__assign({ template: path.resolve(__dirname, '../templates/index.ejs'), hash: false, chunks: [k] }, pages[k])));
}
}
ret.push(new VirtualModulesPlugin(virtualPages));
ret.push(new MiniCssExtractPlugin({
ignoreOrder: true,
// filename: ! is_build ? '[name].css' : '[name].[hash].css',
// chunkFilename: ! is_build ? '[id].css' : '[id].[hash].css',
filename: cssSrc + '[name].css',
// chunkFilename: "../css/[id].css",
}));
if ('development' !== env) {
ret.push(new LimitChunkCountPlugin({ maxChunks: 1 }));
ret.push(new ProgressBarPlugin({
clear: false,
}));
}
if ('production' === env) {
ret.push(new CssMinimizerPlugin({
cache: true,
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
},
],
},
}));
}
return ret;
}
function generateConfig(env, config) {
var srcDir = config.src;
var buildDir = config.build + '/' + env + ('development' === env ? '' : '/static');
var cssbuild = './css/';
var jsbuild = './js/';
var configPages = config.pages;
var configPagesKeys = config.pages ? Object.keys(configPages) : [];
var defPages = pagesConfig(configPagesKeys);
var pages = formatPagesConfig({ title: '', filename: '', render: '', html: config.html, window: config.window }, __assign(__assign({}, configPages), defPages));
var ret = {
entry: webpackEntry(env, srcDir, pages),
output: 'development' === env ? webpackOutput(env, srcDir, void 0, void 0, void 0) : webpackOutput(env, buildDir, jsbuild, void 0, void 0),
plugins: webpackPlugins(env, srcDir, pages, cssbuild),
module: {
rules: webpackRules(env, srcDir, config.postcssConfigFile),
},
resolve: {
alias: webpackAlias(),
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
};
return ret;
}
var isAbsolutePath$2 = require('path').isAbsolute;
var webpack$2 = require('webpack');
var webpackFormatMessages$1 = require('webpack-format-messages');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var defaultOptions$2 = {
env: 'production',
host: '127.0.0.1',
port: 8888,
mode: 'static',
config: config$3,
};
function analyzer(analyzerOptions) {
if (analyzerOptions === void 0) { analyzerOptions = defaultOptions$2; }
var options = __assign(__assign({}, defaultOptions$2), analyzerOptions);
options.config = __assign(__assign({}, defaultOptions$2.config), analyzerOptions.config);
var config = generateConfig(options.env, options.config);
if (!isAbsolutePath$2(options.config.src)) {
throw Error('"src" is not an absolute path');
}
if (!isAbsolutePath$2(options.config.build)) {
throw Error('"build" is not an absolute path');
}
if (!isAbsolutePath$2(options.config.postcssConfigFile)) {
throw Error('"postcssConfigFile" is not an absolute path');
}
var analyzerConfig = {
analyzerMode: options.mode,
analyzerHost: options.host,
analyzerPort: options.port,
generateStatsFile: 'server' !== options.mode,
startAnalyzer: 'server' === options.mode,
statsFilename: 'analyzer-stats.json',
reportFilename: 'analyzer-report.html',
};
var compiler = 'dist' === options.env ? webpack$2(__assign(__assign({}, config$1), config)) : webpack$2(__assign(__assign({}, config$2), config));
var analyzer = new BundleAnalyzerPlugin(analyzerConfig);
analyzer.apply(compiler);
compiler.run(function (err, stats) {
if (err)
throw err;
var messages = webpackFormatMessages$1(stats);
if (!messages.errors.length && !messages.warnings.length) {
console.log('Compiled successfully!', '\n');
}
if (messages.errors.length) {
console.log('Failed to compile.', '\n');
for (var _i = 0, _a = messages.errors; _i < _a.length; _i++) {
var m = _a[_i];
console.log(m);
}
}
else if (messages.warnings.length) {
console.log('Compiled with warnings.', '\n');
for (var _b = 0, _c = messages.warnings; _b < _c.length; _b++) {
var m = _c[_b];
console.log(m);
}
}
});
}
var isAbsolutePath$1 = require('path').isAbsolute;
var webpack$1 = require('webpack');
var webpackFormatMessages = require('webpack-format-messages');
var defaultOptions$1 = {
env: 'production',
config: config$3,
};
function build(buildOptions) {
if (buildOptions === void 0) { buildOptions = defaultOptions$1; }
var options = __assign(__assign({}, defaultOptions$1), buildOptions);
options.config = __assign(__assign({}, defaultOptions$1.config), buildOptions.config);
if (!isAbsolutePath$1(options.config.src)) {
throw Error('"src" is not an absolute path');
}
if (!isAbsolutePath$1(options.config.build)) {
throw Error('"build" is not an absolute path');
}
if (!isAbsolutePath$1(options.config.postcssConfigFile)) {
throw Error('"postcssConfigFile" is not an absolute path');
}
var config = generateConfig(options.env, options.config);
var compiler = 'dist' === options.env ? webpack$1(__assign(__assign({}, config$1), config)) : webpack$1(__assign(__assign({}, config$2), config));
compiler.run(function (err, stats) {
if (err)
throw err;
var messages = webpackFormatMessages(stats);
if (!messages.errors.length && !messages.warnings.length) {
console.log('Compiled successfully!', '\n');
}
if (messages.errors.length) {
console.log('Failed to compile.', '\n');
for (var _i = 0, _a = messages.errors; _i < _a.length; _i++) {
var m = _a[_i];
console.log(m);
}
}
else if (messages.warnings.length) {
console.log('Compiled with warnings.', '\n');
for (var _b = 0, _c = messages.warnings; _b < _c.length; _b++) {
var m = _c[_b];
console.log(m);
}
}
});
}
var config = {
mode: 'development',
// devtool: 'eval',
// devtool: 'source-map',
// devtool: 'eval-cheap-source-map',
optimization: {
minimize: false
}
};
function configFunc(contentBase) {
return {
watchOptions: {
poll: true,
},
contentBase: contentBase,
compress: true,
hot: true
};
}
var isAbsolutePath = require('path').isAbsolute;
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var defaultOptions = {
env: 'development',
host: '0.0.0.0',
port: 8080,
config: config$3,
};
function dev(devOptions) {
if (devOptions === void 0) { devOptions = defaultOptions; }
var options = __assign(__assign({}, defaultOptions), devOptions);
options.config = __assign(__assign({}, defaultOptions.config), devOptions.config);
var config$1 = generateConfig(options.env, options.config);
if (!isAbsolutePath(options.config.src)) {
throw Error('"src" is not an absolute path');
}
if (!isAbsolutePath(options.config.build)) {
throw Error('"build" is not an absolute path');
}
if (!isAbsolutePath(options.config.postcssConfigFile)) {
throw Error('"postcssConfigFile" is not an absolute path');
}
var compilerConfig = __assign(__assign({}, config), config$1);
var serverOptions = configFunc(options.config.src);
WebpackDevServer.addDevServerEntrypoints(compilerConfig, serverOptions);
var compiler = webpack(compilerConfig);
var server = new WebpackDevServer(compiler, serverOptions);
server.listen(options.port, options.host, function (err) {
if (err)
throw err;
});
}
exports.analyzer = analyzer;
exports.build = build;
exports.dev = dev;

View File

@@ -0,0 +1,99 @@
import { Configuration } from 'webpack';
/*const chunksCacheGroups_0 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_1 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
// priority: -10,
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
// test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
// test: /[\\/]node_modules[\\/](!MediaCmsPlayer)[\\/]/,
name: "_vendors",
// priority: -20,
chunks: "all",
enforce: true,
// reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_2 = {
commons: {
minChunks: 2,
// maxInitialRequests: 8, // @note: Tested values from 0 to 10, and changes applied with values 0, 4, 5, 6, 7, 8.
// minSize: 0,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};*/
/*const chunksCacheGroups_3 = {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
};*/
export const config: Configuration = {
mode: 'production',
devtool: 'source-map',
optimization: {
runtimeChunk: false,
/*splitChunks: {
// minSize: 1000000,
chunks: 'all',
automaticNameDelimiter: '-',
},*/
/*splitChunks: {
// minSize: 1000000,
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_0,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_1,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_2,
},*/
/*splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: chunksCacheGroups_3,
},*/
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: '_commons',
priority: 1,
chunks: 'initial',
},
},
},
},
};

View File

@@ -0,0 +1,12 @@
import { Configuration } from 'webpack-dev-server';
export function configFunc(contentBase: string): Configuration {
return {
watchOptions: {
poll: true,
},
contentBase: contentBase,
compress: true,
hot: true,
};
}

View File

@@ -0,0 +1,11 @@
import { Configuration } from 'webpack';
export const config: Configuration = {
mode: 'development',
// devtool: 'eval',
// devtool: 'source-map',
// devtool: 'eval-cheap-source-map',
optimization: {
minimize: false,
},
};

View File

@@ -0,0 +1,73 @@
import { Configuration } from 'webpack';
/*const chunksCacheGroups_0 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};
const chunksCacheGroups_1 = {
commons: {
test: /[\\/]src[\\/]/,
name: "_commons",
// priority: -10,
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
// test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
// test: /[\\/]node_modules[\\/](!MediaCmsPlayer)[\\/]/,
name: "_vendors",
// priority: -20,
chunks: "all",
enforce: true,
// reuseExistingChunk: true,
},
};
const chunksCacheGroups_2 = {
commons: {
minChunks: 2,
// maxInitialRequests: 8, // @note: Tested values from 0 to 10, and changes applied with values 0, 4, 5, 6, 7, 8.
// minSize: 0,
name: "_commons",
chunks: "all",
enforce: true,
reuseExistingChunk: true,
},
};
const chunksCacheGroups_3 = {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "_commons",
priority: 1,
chunks: "initial",
},
};*/
export const config: Configuration = {
mode: 'production',
optimization: {
minimize: true,
runtimeChunk: false,
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: '_commons',
priority: 1,
chunks: 'initial',
},
},
},
},
};

View File

@@ -0,0 +1,114 @@
function bodySnippet(id:string) {
return '<div id="' + id + '"></div>';
}
interface ConfigHtmlHead{
meta?: { [key: string]: string }[],
links?: { [key: string]: string }[],
scripts?: { [key: string]: string }[],
}
interface ConfigHtmlBody{
scripts: { [key: string]: string }[],
snippet: string,
}
export interface ConfigHtml{
head: ConfigHtmlHead,
body: ConfigHtmlBody,
}
export interface ConfigPages{
[key: string]: ConfigPage
}
export interface ConfigWindow{
[key: string ]: unknown
}
export interface ConfigType {
src: string,
build: string,
html: ConfigHtml,
pages: ConfigPages,
window: ConfigWindow,
postcssConfigFile: string,
}
export interface ConfigPage{
staticPage: boolean,
buildExclude: boolean,
title: string,
filename: string,
html: ConfigHtml,
window: ConfigWindow,
render: string,
}
const homePage: ConfigPage = {
staticPage: true,
buildExclude: false,
title: 'Home',
filename: 'index.html',
html: {
head: {},
body: {
scripts: [],
snippet: bodySnippet('page-home'),
}
},
window: {},
render: 'import { renderPage } from \'./js/helpers\'; import { HomePage } from \'./js/pages/HomePage\'; renderPage( \'page-home\', HomePage );',
};
const errorPage: ConfigPage = {
staticPage: true,
buildExclude: false,
title: 'Error',
filename: 'error.html',
html: {
head: {},
body: {
scripts: [],
snippet: bodySnippet('page-error'),
}
},
window: {},
render: 'import { renderPage } from \'./js/helpers\'; import { ErrorPage } from \'./js/pages/ErrorPage\'; renderPage( \'page-error\', ErrorPage );',
};
const pages: { [key: string]: ConfigPage } = {
home: homePage,
error: errorPage,
};
const htmlHead: ConfigHtmlHead = {
meta: [
{ charset: 'utf-8' },
{ content: 'ie=edge', 'http-equiv': 'x-ua-compatible' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
links: [],
scripts: [],
};
const htmlBody: ConfigHtmlBody = {
scripts: [],
snippet: '',
};
const html: ConfigHtml = {
head: htmlHead,
body: htmlBody,
};
export const config : ConfigType = {
src: '',
build: '',
pages,
html,
window: {},
postcssConfigFile: '',
};
export default config;

View File

@@ -0,0 +1,21 @@
import { ConfigType } from '../config';
export interface DevOptionsType {
env: string,
host: string,
port: number,
config: ConfigType,
}
export interface BuildOptionsType {
env: string,
config: ConfigType,
}
export interface AnalyzerOptionsType {
env: string,
host: string,
port: number,
mode: string,
config: ConfigType,
}

View File

@@ -0,0 +1,69 @@
const merge = require('lodash.merge');
import { ConfigHtml, ConfigPages, ConfigWindow } from '../config';
function validateBoolean(value?: boolean | 0 | 1, defaultValue = false): boolean {
if (true === value || false === value) {
return value;
}
if (0 === value || 1 === value) {
return !!value;
}
return defaultValue;
}
function validateString(value?: string, defaultValue = ''): string {
return value ? value : defaultValue;
}
function getArrayType(sourcesArr?: Array<{ [key: string]: string }>, pageArr: Array<{ [key: string]: string }> = []): Array<{ [key: string]: string }> {
if ((!sourcesArr || !sourcesArr.length) && (!pageArr || !pageArr.length)) {
return [];
}
if (sourcesArr && sourcesArr.length && pageArr && pageArr.length) {
return sourcesArr.concat(pageArr);
}
if (sourcesArr && sourcesArr.length) {
return sourcesArr;
}
return pageArr;
}
function formatPagesConfig(sources: { title: string, filename: string, render: string, html: ConfigHtml, window: ConfigWindow }, pages: ConfigPages): ConfigPages {
const ret: ConfigPages = {};
for (const pk in pages) {
ret[pk] = {
staticPage: validateBoolean(pages[pk].staticPage, false),
buildExclude: validateBoolean(pages[pk].buildExclude, false),
title: validateString(pages[pk].title, sources.title),
filename: validateString(pages[pk].filename, sources.filename),
html: {
head: {
meta: getArrayType(sources.html.head.meta, pages[pk].html.head.meta),
links: getArrayType(sources.html.head.links, pages[pk].html.head.links),
scripts: getArrayType(sources.html.head.scripts, pages[pk].html.head.scripts),
},
body: {
scripts: getArrayType(sources.html.body.scripts, pages[pk].html.body.scripts),
snippet: validateString(pages[pk].html.body.snippet, sources.html.body.snippet),
},
},
window: merge({}, sources.window, pages[pk].window),
render: validateString(sources.render, pages[pk].render),
};
}
return ret;
}
export default formatPagesConfig;

View File

@@ -0,0 +1,308 @@
const path = require('path');
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
// Webpack plugins.
const { DefinePlugin } = require('webpack');
const { LimitChunkCountPlugin } = require('webpack').optimize;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VirtualModulesPlugin = require('webpack-virtual-modules');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");
var dotenv = require('dotenv').config({ path: path.resolve(__dirname + '../../../../.env') });
import MyHtmlBeautifyWebpackPlugin from '../webpack-plugins/MyHtmlBeautifyWebpackPlugin';
import { ConfigType, ConfigPages } from '../config';
import defaultPages from './pagesConfig';
import formatPagesConfig from './formatPagesConfig';
function webpackEntry(env: string, srcDir: string, pages: ConfigPages) {
const ret: { [key: string]: string } = {};
for (const p in pages) {
if ('development' === env || !pages[p].buildExclude) {
ret[p] = path.resolve(srcDir + '/' + p + '.js');
}
}
return ret;
}
function webpackOutput(env: string, destinationDir: string, buildDir?: string, chunkhash?: string, hash?: string) {
const ret = {
path: destinationDir,
filename: '',
};
const prefix = 'development' === env ? '' : buildDir;
let tmp;
if (undefined !== chunkhash) {
tmp = chunkhash.trim();
if ('' === tmp) {
throw Error('Invalid chunkhash argument value: ' + chunkhash);
}
ret.filename = (prefix || '') + '[name]-[chunkhash].js';
}
else if (undefined !== hash) {
tmp = hash.trim();
if ('' === tmp) {
throw Error('Invalid hash argument value: ' + hash);
}
ret.filename = (prefix || '') + '[name]-[hash].js';
}
else {
ret.filename = (prefix || '') + '[name].js';
}
return ret;
}
function webpackAlias() {
return {
// modernizr$: path.resolve(__dirname, "../../.modernizrrc"), // TODO: Enable this?
};
}
function webpackRules(env: string, srcDir: string, postcssConfigFile: string): any[] {
return [{
test: /\.(jsx|js)?$/,
use: 'babel-loader'
},
{
test: /\.(tsx|ts)?$/,
use: 'ts-loader',
// exclude: /node_modules/,
// options: {
// compilerOptions: {
// "sourceMap": !isProduction,
// },
// },
},
{
test: /\.ejs$/,
use: {
loader: 'ejs-compiled-loader',
options: {
// beautify: true,
htmlmin: true,
// htmlminOptions: {
// removeComments: true,
// collapseWhitespace: true,
// preserveLineBreaks: true
// }
}
}
},
{
test: /\.(sa|sc|c)ss$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
// { loader: 'development' === env ? MiniCssExtractPlugin.loader : 'style-loader' }, // Use inline <style> tag.
{ loader: 'css-loader', options: { importLoaders: 1 } },
{ loader: 'postcss-loader', options: { postcssOptions: { config: postcssConfigFile } } },
{ loader: 'sass-loader' },
],
},
{
test: /\.module\.(sa|sc|c)ss$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
// { loader: 'development' === env ? MiniCssExtractPlugin.loader : 'style-loader' }, // Use inline <style> tag.
{ loader: 'css-loader', options: { importLoaders: 1, modules: true, onlyLocals: false } },
{ loader: 'postcss-loader', options: { postcssOptions: { config: postcssConfigFile } } },
{ loader: 'sass-loader' },
]
},
{
test: /\.(png|jpe?g|gif)(\?\S*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 1024,
fallback: 'file-loader',
name: (file: string) => {
return '.' + path.join(file.replace(srcDir, ''), '..').replace(/\\/g, '/') + '/[name].[ext]';
},
},
},
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
/*issuer: {
test: /\.jsx?$/
},*/
use: ['babel-loader', '@svgr/webpack', 'url-loader']
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader'
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'file-loader',
options: {
name: (file: string) => {
return '.' + path.join(file.replace(srcDir, ''), '..').replace(/\\/g, '/') + '/[name].[ext]';
},
}
}]
},
{
test: /\.modernizrrc.js$/,
use: 'modernizr-loader',
},
{
test: /\.modernizrrc(\.json)?$/,
use: ['modernizr-loader', 'json-loader'],
}
];
}
function webpackPlugins(env: string, srcDir: string, pages: ConfigPages, cssSrc: string) {
const ret = [
new DefinePlugin({ "process.env": JSON.stringify(dotenv.parsed) }),
new NodePolyfillPlugin(),
new MyHtmlBeautifyWebpackPlugin(),
];
if ('development' !== env) {
ret.push(
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../../../src/static/lib'),
to: path.resolve(__dirname, '../../../' + env + '/static/lib'),
},
{
from: path.resolve(__dirname, '../../../src/static/images'),
to: path.resolve(__dirname, '../../../' + env + '/static/images'),
},
{
from: path.resolve(__dirname, '../../../src/static/favicons'),
to: path.resolve(__dirname, '../../../' + env + '/static/favicons'),
},
{
from: path.resolve(__dirname, '../../../src/static/css/_extra.css'),
to: path.resolve(__dirname, '../../../' + env + '/static/css/_extra.css'),
},
],
})
);
}
const virtualPages: { [key: string]: string } = {};
let file: string;
for (const k in pages) {
if ('production' !== env || !pages[k].buildExclude) {
file = path.resolve(srcDir + '/' + k + '.js');
if ((void 0 !== pages[k].staticPage && pages[k].staticPage) || void 0 === pages[k].render) {
virtualPages[file] = '';
} else {
virtualPages[file] = pages[k].render;
}
}
if ('development' === env) {
// Export pages HTML files.
ret.push(new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../templates/index.ejs'),
hash: false,
chunks: [k],
...pages[k],
}));
}
}
ret.push(new VirtualModulesPlugin(virtualPages));
ret.push(new MiniCssExtractPlugin({
ignoreOrder: true, // TODO: Remove it...
// filename: ! is_build ? '[name].css' : '[name].[hash].css',
// chunkFilename: ! is_build ? '[id].css' : '[id].[hash].css',
filename: cssSrc + '[name].css',
// chunkFilename: "../css/[id].css",
}));
if ('development' !== env) {
ret.push(new LimitChunkCountPlugin({ maxChunks: 1 }));
ret.push(
new ProgressBarPlugin({
clear: false,
})
);
}
if ('production' === env) {
ret.push(new CssMinimizerPlugin({
cache: true, // TODO: Ignore in Webpack 5. Use https://webpack.js.org/configuration/other-options/#cache.
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
},
],
},
}));
}
return ret;
}
export default function generateConfig(env: string, config: ConfigType) {
const srcDir = config.src;
const buildDir = config.build + '/' + env + ('development' === env ? '' : '/static');
const cssbuild = './css/';
const jsbuild = './js/';
const configPages = config.pages;
const configPagesKeys = config.pages ? Object.keys(configPages) : [];
const defPages = defaultPages(configPagesKeys);
const pages = formatPagesConfig(
{ title: '', filename: '', render: '', html: config.html, window: config.window },
{ ...configPages, ...defPages }
);
const ret = {
entry: webpackEntry(env, srcDir, pages),
output: 'development' === env ? webpackOutput(env, srcDir, void 0, void 0, void 0) : webpackOutput(env, buildDir, jsbuild, void 0, void 0),
plugins: webpackPlugins(env, srcDir, pages, cssbuild),
module: {
rules: webpackRules(env, srcDir, config.postcssConfigFile),
},
resolve: {
alias: webpackAlias(),
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
};
return ret;
}

View File

@@ -0,0 +1,37 @@
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const templatePath = path.join(__dirname, '../templates');
const sitemapTemplatePath = path.join(templatePath, 'sitemap.ejs');
const sitemapTemplate = ejs.compile(fs.readFileSync(sitemapTemplatePath, 'utf8'), { root: [templatePath], filename: sitemapTemplatePath, outputFunctionName: 'echo' });
import { ConfigPages } from '../config';
export default function pagesConfig(pagesKeys: string[]): ConfigPages {
const pages: ConfigPages = {};
if (-1 === pagesKeys.indexOf('sitemap')) {
pages.sitemap = {
staticPage: true,
buildExclude: true,
title: 'Sitemap',
filename: 'sitemap.html',
html: {
head: {},
body: {
scripts: [],
snippet: sitemapTemplate({ pages: [...pagesKeys, ...Object.keys(pages)] }),
},
},
window: {},
render: ''
};
}
return pages;
}

View File

@@ -0,0 +1,74 @@
/**
* @see {link: https://github.com/seeyoulater/html-beautify-webpack-plugin/blob/master/index.js}
*/
const prettify = require('html-prettify');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackError = require( 'webpack/lib/WebpackError.js' );
import { Compiler, Compilation } from 'webpack';
interface OptionsConfigHtml {
end_with_newline: boolean,
indent_inner_html: boolean,
preserve_newlines: boolean,
max_preserve_newlines: number,
}
interface OptionsConfig {
indent_size: number,
indent_with_tabs: boolean,
html: OptionsConfigHtml
}
interface Options{
config: OptionsConfig,
replace: Array<string|RegExp>
}
interface HtmlWebpackPluginArgs{
html: string,
plugin: typeof HtmlWebpackPlugin,
outputName: string
}
function htmlPluginDataFunction (pluginData: HtmlWebpackPluginArgs, options: Options, callback: (err:typeof WebpackError, arg1: HtmlWebpackPluginArgs) => void) {
pluginData.html = prettify(
options.replace.reduce( (res:string, item: string | RegExp) => res.replace( item instanceof RegExp ? new RegExp(item, 'gi') : item, '' ), pluginData.html )/*,
options.config*/
);
callback(null, pluginData);
}
export default class MyHtmlBeautifyWebpackPlugin {
apply(compiler: Compiler): void {
const options: Options = {
config: { // TODO: Remove it.
indent_size: 4,
indent_with_tabs: false,
html: {
end_with_newline: true,
indent_inner_html: true,
preserve_newlines: true,
max_preserve_newlines: 0,
}
},
replace: []
};
function tapAsyncCallback(pluginData: HtmlWebpackPluginArgs, callback: (err:typeof WebpackError, arg1: HtmlWebpackPluginArgs) => void ){
return htmlPluginDataFunction (pluginData, options, callback);
}
function tapHookCallback(compilation: Compilation){
return HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync( 'MyHtmlBeautifyWebpackPlugin', tapAsyncCallback );
}
compiler.hooks.compilation.tap( 'MyHtmlBeautifyWebpackPlugin', tapHookCallback );
}
}

10148
frontend/packages/scripts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
{
"name": "mediacms-scripts",
"version": "0.9.0",
"description": "",
"author": "",
"license": "",
"engines": {
"node": ">=14.17.0"
},
"files": [
"scripts",
"cli.js"
],
"scripts": {
"start": "npx rollup -w -c ./config/watch.config.js",
"build": "npx rollup -c ./config/build.config.js",
"visual": "npx rollup -c ./config/visual.config.js"
},
"bin": {
"mediacms-scripts": "./cli.js"
},
"types": "",
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.7",
"axios": "^0.21.1",
"babel-core": "^6.26.3",
"copy-webpack-plugin": "^9.0.0",
"cross-spawn": "^7.0.3",
"dotenv": "^10.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.51.2",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-visualizer": "^5.5.0",
"serialize-javascript": "^5.0.1",
"source-map-loader": "^3.0.0",
"ts-loader": "^9.2.3",
"typescript": "^4.3.2"
},
"dependencies": {
"@svgr/webpack": "^5.5.0",
"@types/webpack": "^5.28.0",
"@types/webpack-bundle-analyzer": "^4.4.0",
"@types/webpack-dev-server": "^3.11.4",
"autoprefixer": "^10.2.6",
"babel-loader": "^8.2.2",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.1",
"ejs": "^3.1.6",
"ejs-compiled-loader": "^3.1.0",
"ejs-loader": "^0.5.0",
"file-loader": "^6.2.0",
"html-prettify": "^1.0.3",
"html-webpack-plugin": "^5.3.1",
"json-loader": "^0.5.7",
"lodash.merge": "^4.6.2",
"mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"node-sass": "^6.0.0",
"postcss": "^8.3.2",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.0",
"postcss-modules": "^4.1.3",
"postcss-nested": "^5.0.5",
"postcss-scss": "^3.0.5",
"progress-bar-webpack-plugin": "^2.1.0",
"sass-loader": "^12.1.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^5.38.1",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-dev-server": "^3.11.2",
"webpack-format-messages": "^2.0.6",
"webpack-virtual-modules": "^0.4.3"
},
"peerDependenciesMeta": {},
"browserslist": {}
}

View File

@@ -0,0 +1,5 @@
const { analyzer } = require('../dist/webpack-dev-env.js');
const parseCliArgs = require('./utils/parseCliArgs.js');
const { validateAnalyzerOptions } = require('./utils/validateOptions.js');
const options = validateAnalyzerOptions(parseCliArgs(process.argv.slice(2)));
analyzer(options);

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