mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-21 13:57:57 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ddfae7c95 | ||
|
|
f1969e4637 | ||
|
|
9e7a7a7482 | ||
|
|
6e478e6e82 | ||
|
|
e06deed3b8 | ||
|
|
f8376c5c58 | ||
|
|
e7ae2833d9 | ||
|
|
fb0f3ee739 | ||
|
|
c0701de047 | ||
|
|
0d4918a715 | ||
|
|
8093c4ccb5 | ||
|
|
2dbd97cb22 | ||
|
|
6b6662420f | ||
|
|
f1a1e342db | ||
|
|
738247c32b | ||
|
|
f974d78270 | ||
|
|
28031f07e5 | ||
|
|
4480fa7de1 | ||
|
|
32e07035f3 |
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
*bento4*
|
||||
*/migrations/*
|
||||
5
.github/workflows/python.yml
vendored
5
.github/workflows/python.yml
vendored
@@ -29,7 +29,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Run Django Tests
|
||||
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
|
||||
|
||||
# Run with coverage, saves report on htmlcov dir
|
||||
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc
|
||||
|
||||
- name: Tear down the Stack
|
||||
run: docker-compose -f docker-compose-dev.yaml down
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
cli-tool/.env
|
||||
frontend/package-lock.json
|
||||
media_files/encoded/
|
||||
media_files/original/
|
||||
media_files/hls/
|
||||
@@ -14,4 +16,4 @@ static/mptt/
|
||||
static/rest_framework/
|
||||
static/drf-yasg
|
||||
cms/local_settings.py
|
||||
deploy/docker/local_settings.py
|
||||
deploy/docker/local_settings.py
|
||||
@@ -9,7 +9,7 @@ repos:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
Wordgames.gr - https://www.wordgames.gr
|
||||
Yiannis Stergiou - ys.stergiou@gmail.com
|
||||
Markos Gogoulos - mgogoulos@gmail.com
|
||||
|
||||
Contributors
|
||||
|
||||
Swift Ugandan - swiftugandan@gmail.com
|
||||
|
||||
Please see https://github.com/mediacms-io/mediacms/graphs/contributors for complete list of contributors to this repository!
|
||||
10
cli-tool/README.md
Normal file
10
cli-tool/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## MediaCMS CLI Tool
|
||||
This is the CLI tool to interact with the API of your installation/instance of MediaCMS.
|
||||
|
||||
### How to configure and use the tools
|
||||
- Make sure that you have all the required installations (`cli-tool/requirements.txt`)installed. To install it -
|
||||
- Create a new virtualenv using any python virtualenv manager.
|
||||
- Then activate the virtualenv and enter `pip install -r requirements.txt`.
|
||||
- Create an .env file in this folder (`mediacms/cli-tool/`)
|
||||
- Run the cli tool using the command `python cli.py login`. This will authenticate you and store necessary creds for further authentications.
|
||||
- To check the credentials and necessary setup, run `python cli.py whoami`. This will show your details.
|
||||
167
cli-tool/cli.py
Normal file
167
cli-tool/cli.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import click
|
||||
import requests
|
||||
from decouple import config
|
||||
from rich import print
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
print("Welcome to the CLI Tool of [bold blue]MediaCMS![/bold blue]", ":thumbs_up:")
|
||||
|
||||
|
||||
BASE_URL = 'https://demo.mediacms.io/api/v1'
|
||||
AUTH_KEY = ''
|
||||
USERNAME = ''
|
||||
EMAIL = ''
|
||||
|
||||
|
||||
def set_envs():
|
||||
with open('.env', 'r') as file:
|
||||
if not file.read(1):
|
||||
print("Use the Login command to set your credential environment variables")
|
||||
else:
|
||||
global AUTH_KEY, USERNAME, EMAIL
|
||||
AUTH_KEY = config('AUTH_KEY')
|
||||
USERNAME = config('USERNAME')
|
||||
EMAIL = config('EMAIL')
|
||||
|
||||
|
||||
set_envs()
|
||||
|
||||
|
||||
@click.group()
|
||||
def apis():
|
||||
"""A CLI wrapper for the MediaCMS API endpoints."""
|
||||
|
||||
|
||||
@apis.command()
|
||||
def login():
|
||||
"""Login to your account."""
|
||||
|
||||
email = input('Enter your email address: ')
|
||||
password = input('Enter your password: ')
|
||||
|
||||
data = {
|
||||
"email": f"{email}",
|
||||
"password": f"{password}",
|
||||
}
|
||||
|
||||
response = requests.post(url=f'{BASE_URL}/login', data=data)
|
||||
if response.status_code == 200:
|
||||
username = json.loads(response.text)["username"]
|
||||
with open(".env", "w") as file:
|
||||
file.writelines(f'AUTH_KEY={json.loads(response.text)["token"]}\n')
|
||||
file.writelines(f'EMAIL={json.loads(response.text)["email"]}\n')
|
||||
file.writelines(f'USERNAME={json.loads(response.text)["username"]}\n')
|
||||
print(f"Welcome to MediaCMS [bold blue]{username}[/bold blue]. Your auth creds have been suceesfully stored in the .env file", ":v:")
|
||||
else:
|
||||
print(f'Error: {"non_field_errors":["User not found."]}')
|
||||
|
||||
|
||||
@apis.command()
|
||||
def upload_media():
|
||||
"""Upload media to the server"""
|
||||
|
||||
headers = {'authorization': f'Token {AUTH_KEY}'}
|
||||
|
||||
path = input('Enter the location of the file or directory where multiple files are present: ')
|
||||
|
||||
if os.path.isdir(path):
|
||||
for filename in os.listdir(path):
|
||||
files = {}
|
||||
abs = os.path.abspath("{path}/{filename}")
|
||||
files['media_file'] = open(f'{abs}', 'rb')
|
||||
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
|
||||
if response.status_code == 201:
|
||||
print("[bold blue]{filename}[/bold blue] successfully uploaded!")
|
||||
else:
|
||||
print(f'Error: {response.text}')
|
||||
|
||||
else:
|
||||
files = {}
|
||||
files['media_file'] = open(f'{os.path.abspath(path)}', 'rb')
|
||||
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
|
||||
if response.status_code == 201:
|
||||
print("[bold blue]{filename}[/bold blue] successfully uploaded!")
|
||||
else:
|
||||
print(f'Error: {response.text}')
|
||||
|
||||
|
||||
@apis.command()
|
||||
def my_media():
|
||||
"""List all my media"""
|
||||
|
||||
headers = {'authorization': f'Token {AUTH_KEY}'}
|
||||
response = requests.get(url=f'{BASE_URL}/media?author={USERNAME}', headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
data_json = json.loads(response.text)
|
||||
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
table.add_column("Name of the media")
|
||||
table.add_column("Media Type")
|
||||
table.add_column("State")
|
||||
|
||||
for data in data_json['results']:
|
||||
table.add_row(data['title'], data['media_type'], data['state'])
|
||||
console.print(table)
|
||||
|
||||
else:
|
||||
print(f'Could not get the media: {response.text}')
|
||||
|
||||
|
||||
@apis.command()
|
||||
def whoami():
|
||||
"""Shows the details of the authorized user"""
|
||||
headers = {'authorization': f'Token {AUTH_KEY}'}
|
||||
response = requests.get(url=f'{BASE_URL}/whoami', headers=headers)
|
||||
for data, value in json.loads(response.text).items():
|
||||
print(data, ' : ', value)
|
||||
|
||||
|
||||
@apis.command()
|
||||
def categories():
|
||||
"""List all categories."""
|
||||
response = requests.get(url=f'{BASE_URL}/categories')
|
||||
if response.status_code == 200:
|
||||
data_json = json.loads(response.text)
|
||||
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
table.add_column("Category")
|
||||
table.add_column("Description")
|
||||
|
||||
for data in data_json:
|
||||
table.add_row(data['title'], data['description'])
|
||||
|
||||
console.print(table)
|
||||
else:
|
||||
print(f'Could not get the categories: {response.text}')
|
||||
|
||||
|
||||
@apis.command()
|
||||
def encodings():
|
||||
"""List all encoding profiles"""
|
||||
response = requests.get(url=f'{BASE_URL}/encode_profiles/')
|
||||
if response.status_code == 200:
|
||||
data_json = json.loads(response.text)
|
||||
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
table.add_column("Name")
|
||||
table.add_column("Extension")
|
||||
table.add_column("Resolution")
|
||||
table.add_column("Codec")
|
||||
table.add_column("Description")
|
||||
|
||||
for data in data_json:
|
||||
table.add_row(data['name'], data['extension'], str(data['resolution']), data['codec'], data['description'])
|
||||
console.print(table)
|
||||
else:
|
||||
print(f'Could not get the encodings: {response.text}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
apis()
|
||||
4
cli-tool/requirements.txt
Normal file
4
cli-tool/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
click
|
||||
python-decouple
|
||||
requests
|
||||
rich
|
||||
@@ -3,6 +3,7 @@ from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
from django.conf import settings
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||
app = Celery("cms")
|
||||
@@ -14,5 +15,8 @@ app.conf.beat_schedule = app.conf.CELERY_BEAT_SCHEDULE
|
||||
app.conf.broker_transport_options = {"visibility_timeout": 60 * 60 * 24} # 1 day
|
||||
# http://docs.celeryproject.org/en/latest/getting-started/brokers/redis.html#redis-caveats
|
||||
|
||||
# setting this to settings.py file only is not respected. Setting here too
|
||||
app.conf.task_always_eager = settings.CELERY_TASK_ALWAYS_EAGER
|
||||
|
||||
|
||||
app.conf.worker_prefetch_multiplier = 1
|
||||
|
||||
@@ -27,23 +27,8 @@ services:
|
||||
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
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data/:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data/:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -62,7 +62,7 @@ services:
|
||||
depends_on:
|
||||
- migrations
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- ../postgres_data:/var/lib/postgresql/data/
|
||||
restart: always
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [12. Video transcoding](#12-video-transcoding)
|
||||
- [13. How To Add A Static Page To The Sidebar](#13-how-to-add-a-static-page-to-the-sidebar)
|
||||
- [14. Add Google Analytics](#14-add-google-analytics)
|
||||
- [15. Debugging email issues](#15-debugging-email-issues)
|
||||
|
||||
|
||||
## 1. Welcome
|
||||
@@ -435,7 +436,10 @@ ADMINS_NOTIFICATIONS = {
|
||||
- MEDIA_ADDED: a media is added
|
||||
- MEDIA_REPORTED: the report for a media was hit
|
||||
|
||||
### 5.23 Configure only member access to media
|
||||
|
||||
- Make the portal workflow public, but at the same time set `GLOBAL_LOGIN_REQUIRED = True` so that only logged in users can see content.
|
||||
- You can either set `REGISTER_ALLOWED = False` if you want to add members yourself or checkout options on "django-allauth settings" that affects registration in `cms/settings.py`. Eg set the portal invite only, or set email confirmation as mandatory, so that you control who registers.
|
||||
|
||||
## 6. Manage pages
|
||||
to be written
|
||||
@@ -648,3 +652,38 @@ Instructions contributed by @alberto98fx
|
||||
- ./templates/tracking.html://home/mediacms.io/mediacms/templates/tracking.html
|
||||
|
||||
```
|
||||
|
||||
## 15. Debugging email issues
|
||||
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
|
||||
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
|
||||
|
||||
Enter the Django shell, example if you're using the Single Server installation:
|
||||
|
||||
```bash
|
||||
source /home/mediacms.io/bin/activate
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
and inside the shell
|
||||
|
||||
```bash
|
||||
from django.core.mail import EmailMessage
|
||||
from django.conf import settings
|
||||
|
||||
settings.EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
|
||||
email = EmailMessage(
|
||||
'title',
|
||||
'msg',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
['recipient@email.com'],
|
||||
)
|
||||
email.send(fail_silently=False)
|
||||
```
|
||||
|
||||
You have the chance to either receive the email (in this case it will be sent to recipient@email.com) otherwise you will see the error.
|
||||
For example, while specifying wrong password for my Gmail account I get
|
||||
|
||||
```
|
||||
SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 https://support.google.com/mail/?p=BadCredentials d4sm12687785wrc.34 - gsmtp')
|
||||
```
|
||||
|
||||
@@ -19,6 +19,26 @@ to be written
|
||||
API is documented using Swagger - checkout ot http://your_installation/swagger - example https://demo.mediacms.io/swagger/
|
||||
This page allows you to login to perform authenticated actions - it will also use your session if logged in.
|
||||
|
||||
|
||||
An example of working with Python requests library:
|
||||
|
||||
```
|
||||
import requests
|
||||
|
||||
auth = ('user' ,'password')
|
||||
upload_url = "https://domain/api/v1/media"
|
||||
title = 'x title'
|
||||
description = 'x description'
|
||||
media_file = '/tmp/file.mp4'
|
||||
|
||||
requests.post(
|
||||
url=upload_url,
|
||||
files={'media_file': open(media_file,'rb')},
|
||||
data={'title': title, 'description': description},
|
||||
auth=auth
|
||||
)
|
||||
```
|
||||
|
||||
## 4. How to contribute
|
||||
Before you send a PR, make sure your code is properly formatted. For that, use `pre-commit install` to install a pre-commit hook and run `pre-commit run --all` and fix everything before you commit. This pre-commit will check for your code lint everytime you commit a code.
|
||||
|
||||
|
||||
BIN
docs/images/Demo1.png
Normal file
BIN
docs/images/Demo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
BIN
docs/images/Demo2.png
Normal file
BIN
docs/images/Demo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/images/Demo3.png
Normal file
BIN
docs/images/Demo3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -5,6 +5,7 @@
|
||||
- [Downloading media](#downloading-media)
|
||||
- [Adding captions/subtitles](#adding-captionssubtitles)
|
||||
- [Search media](#search-media)
|
||||
- [Using Timestamps for sharing](#using-timestamps-for-sharing)
|
||||
- [Share media](#share-media)
|
||||
- [Embed media](#embed-media)
|
||||
- [Customize my profile options](#customize-my-profile-options)
|
||||
@@ -195,6 +196,30 @@ You can now watch the captions/subtitles play back in the video player - and tog
|
||||
<img src="./images/CC-display.png"/>
|
||||
</p>
|
||||
|
||||
## Using Timestamps for sharing
|
||||
|
||||
### Using Timestamp in the URL
|
||||
|
||||
An additional GET parameter 't' can be added in video URL's to start the video at the given time. The starting time has to be given in seconds.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Demo1.png"/>
|
||||
</p>
|
||||
|
||||
Additionally the share button has an option to generate the URL with the timestamp at current second the video is.
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Demo2.png"/>
|
||||
</p>
|
||||
|
||||
### Using Timestamp in the comments
|
||||
|
||||
Comments can also include timestamps. They are automatically detected upon posting the comment, and will be in the form of an hyperlink link in the comment. The timestamps in the comments have to follow the format HH:MM:SS or MM:SS
|
||||
|
||||
<p align="left">
|
||||
<img src="./images/Demo3.png"/>
|
||||
</p>
|
||||
|
||||
## Search media
|
||||
How search can be used
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ def media_file_info(input_file):
|
||||
- `video_bitrate`: Bitrate of the video stream in kBit/s
|
||||
- `video_width`: Width in pixels
|
||||
- `video_height`: Height in pixels
|
||||
- `interlaced` : True if the video is interlaced
|
||||
- `video_codec`: Video codec
|
||||
- `audio_duration`: Duration of the audio in `s.msec`
|
||||
- `audio_sample_rate`: Audio sample rate in Hz
|
||||
@@ -329,7 +330,7 @@ def media_file_info(input_file):
|
||||
except ValueError:
|
||||
hms, msec = duration_str.split(",")
|
||||
|
||||
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
|
||||
total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
|
||||
video_duration = total_dur + float("0." + msec)
|
||||
else:
|
||||
# fallback to format, eg for webm
|
||||
@@ -374,6 +375,10 @@ def media_file_info(input_file):
|
||||
video_frame_rate_n = video_frame_rate[0]
|
||||
video_frame_rate_d = video_frame_rate[2]
|
||||
|
||||
interlaced = False
|
||||
if video_info.get("field_order") in ("tt", "tb", "bt", "bb"):
|
||||
interlaced = True
|
||||
|
||||
ret = {
|
||||
"filename": input_file,
|
||||
"file_size": file_size,
|
||||
@@ -390,7 +395,7 @@ def media_file_info(input_file):
|
||||
"color_space": video_info.get("color_space"),
|
||||
"color_transfer": video_info.get("color_space"),
|
||||
"color_primaries": video_info.get("color_primaries"),
|
||||
"field_order": video_info.get("field_order"),
|
||||
"interlaced": interlaced,
|
||||
"display_aspect_ratio": video_info.get("display_aspect_ratio"),
|
||||
"sample_aspect_ratio": video_info.get("sample_aspect_ratio"),
|
||||
}
|
||||
@@ -404,7 +409,7 @@ def media_file_info(input_file):
|
||||
hms, msec = duration_str.split(".")
|
||||
except ValueError:
|
||||
hms, msec = duration_str.split(",")
|
||||
total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
|
||||
total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
|
||||
audio_duration = total_dur + float("0." + msec)
|
||||
else:
|
||||
# fallback to format, eg for webm
|
||||
@@ -490,6 +495,7 @@ def get_base_ffmpeg_command(
|
||||
encoder,
|
||||
audio_encoder,
|
||||
target_fps,
|
||||
interlaced,
|
||||
target_height,
|
||||
target_rate,
|
||||
target_rate_audio,
|
||||
@@ -508,6 +514,7 @@ def get_base_ffmpeg_command(
|
||||
encoder {str} -- video encoder
|
||||
audio_encoder {str} -- audio encoder
|
||||
target_fps {fractions.Fraction} -- target FPS
|
||||
interlaced {bool} -- true if interlaced
|
||||
target_height {int} -- height
|
||||
target_rate {int} -- target bitrate in kbps
|
||||
target_rate_audio {int} -- audio target bitrate
|
||||
@@ -523,6 +530,27 @@ def get_base_ffmpeg_command(
|
||||
if target_fps < 1:
|
||||
target_fps = 1
|
||||
|
||||
filters = []
|
||||
|
||||
if interlaced:
|
||||
filters.append("yadif")
|
||||
|
||||
target_width = round(target_height * 16 / 9)
|
||||
scale_filter_opts = [
|
||||
f"if(lt(iw\\,ih)\\,{target_height}\\,{target_width})",
|
||||
f"if(lt(iw\\,ih)\\,{target_width}\\,{target_height})",
|
||||
"force_original_aspect_ratio=decrease",
|
||||
"force_divisible_by=2",
|
||||
"flags=lanczos",
|
||||
]
|
||||
scale_filter_str = "scale=" + ":".join(scale_filter_opts)
|
||||
filters.append(scale_filter_str)
|
||||
|
||||
fps_str = f"fps=fps={target_fps}"
|
||||
filters.append(fps_str)
|
||||
|
||||
filters_str = ",".join(filters)
|
||||
|
||||
base_cmd = [
|
||||
settings.FFMPEG_COMMAND,
|
||||
"-y",
|
||||
@@ -531,9 +559,7 @@ def get_base_ffmpeg_command(
|
||||
"-c:v",
|
||||
encoder,
|
||||
"-filter:v",
|
||||
"scale=-2:" + str(target_height) + ",fps=fps=" + str(target_fps),
|
||||
# always convert to 4:2:0 -- FIXME: this could be also 4:2:2
|
||||
# but compatibility will suffer
|
||||
filters_str,
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
]
|
||||
@@ -716,6 +742,8 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
||||
elif enc_type == "crf":
|
||||
passes = [2]
|
||||
|
||||
interlaced = media_info.get("interlaced")
|
||||
|
||||
cmds = []
|
||||
for pass_number in passes:
|
||||
cmds.append(
|
||||
@@ -727,6 +755,7 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi
|
||||
encoder=encoder,
|
||||
audio_encoder=AUDIO_ENCODERS[codec],
|
||||
target_fps=target_fps,
|
||||
interlaced=interlaced,
|
||||
target_height=resolution,
|
||||
target_rate=target_rate,
|
||||
target_rate_audio=AUDIO_BITRATES[codec],
|
||||
|
||||
18
files/migrations/0003_auto_20210927_1245.py
Normal file
18
files/migrations/0003_auto_20210927_1245.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.12 on 2021-09-27 11:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('files', '0002_auto_20201201_0712'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='media',
|
||||
name='reported_times',
|
||||
field=models.IntegerField(default=0, help_text='how many time a media is reported'),
|
||||
),
|
||||
]
|
||||
@@ -209,7 +209,7 @@ class Media(models.Model):
|
||||
help_text="Rating category, if media Rating is allowed",
|
||||
)
|
||||
|
||||
reported_times = models.IntegerField(default=0, help_text="how many time a Medis is reported")
|
||||
reported_times = models.IntegerField(default=0, help_text="how many time a media is reported")
|
||||
|
||||
search = SearchVectorField(
|
||||
null=True,
|
||||
@@ -395,11 +395,11 @@ class Media(models.Model):
|
||||
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
|
||||
|
||||
items = [
|
||||
helpers.clean_query(self.title),
|
||||
self.title,
|
||||
self.user.username,
|
||||
self.user.email,
|
||||
self.user.name,
|
||||
helpers.clean_query(self.description),
|
||||
self.description,
|
||||
a_tags,
|
||||
b_tags,
|
||||
]
|
||||
@@ -407,6 +407,8 @@ class Media(models.Model):
|
||||
text = " ".join(items)
|
||||
text = " ".join([token for token in text.lower().split(" ") if token not in STOP_WORDS])
|
||||
|
||||
text = helpers.clean_query(text)
|
||||
|
||||
sql_code = """
|
||||
UPDATE {db_table} SET search = to_tsvector(
|
||||
'{config}', '{text}'
|
||||
|
||||
BIN
fixtures/medium_video.mp4
Normal file
BIN
fixtures/medium_video.mp4
Normal file
Binary file not shown.
BIN
fixtures/small_video.mp4
Normal file
BIN
fixtures/small_video.mp4
Normal file
Binary file not shown.
BIN
fixtures/test_image.png
Normal file
BIN
fixtures/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
28320
frontend/package-lock.json
generated
28320
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -368,8 +368,36 @@ export default function CommentsList(props) {
|
||||
const [displayComments, setDisplayComments] = useState(false);
|
||||
|
||||
function onCommentsLoad() {
|
||||
displayCommentsRelatedAlert();
|
||||
setComments([...MediaPageStore.get('media-comments')]);
|
||||
const retrievedComments = [...MediaPageStore.get('media-comments')];
|
||||
|
||||
retrievedComments.forEach(comment => {
|
||||
comment.text = setTimestampAnchors(comment.text);
|
||||
});
|
||||
|
||||
displayCommentsRelatedAlert();
|
||||
setComments(retrievedComments);
|
||||
}
|
||||
|
||||
function setTimestampAnchors(text)
|
||||
{
|
||||
function wrapTimestampWithAnchor(match, string)
|
||||
{
|
||||
let split = match.split(':'), s = 0, m = 1;
|
||||
let searchParameters = new URLSearchParams(window.location.search);
|
||||
|
||||
while (split.length > 0)
|
||||
{
|
||||
s += m * parseInt(split.pop(), 10);
|
||||
m *= 60;
|
||||
}
|
||||
|
||||
searchParameters.set('t', s)
|
||||
const wrapped = "<a href=\"" + MediaPageStore.get('media-url').split('?')[0] + "?" + searchParameters + "\">" + match + "</a>";
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g');
|
||||
return text.replace(timeRegex , wrapTimestampWithAnchor);
|
||||
}
|
||||
|
||||
function onCommentSubmit(commentId) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon } from '../_shared/';
|
||||
|
||||
export function MediaDislikeIcon() {
|
||||
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-liked-media'));
|
||||
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
|
||||
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-disliked-media'));
|
||||
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-dislikes'), false));
|
||||
|
||||
function updateStateValues() {
|
||||
setDislikedMedia(MediaPageStore.get('user-disliked-media'));
|
||||
|
||||
@@ -182,9 +182,27 @@ function updateDimensions() {
|
||||
};
|
||||
}
|
||||
|
||||
function getTimestamp() {
|
||||
const videoPlayer = document.getElementsByTagName("video");
|
||||
return videoPlayer[0]?.currentTime;
|
||||
}
|
||||
|
||||
function ToHHMMSS (timeInt) {
|
||||
let sec_num = parseInt(timeInt, 10);
|
||||
let hours = Math.floor(sec_num / 3600);
|
||||
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
let seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {hours = "0"+hours;}
|
||||
if (minutes < 10) {minutes = "0"+minutes;}
|
||||
if (seconds < 10) {seconds = "0"+seconds;}
|
||||
return hours >= 1 ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
export function MediaShareOptions(props) {
|
||||
const containerRef = useRef(null);
|
||||
const shareOptionsInnerRef = useRef(null);
|
||||
const mediaUrl = MediaPageStore.get('media-url');
|
||||
|
||||
const [inlineSlider, setInlineSlider] = useState(null);
|
||||
const [sliderButtonsVisible, setSliderButtonsVisible] = useState({ prev: false, next: false });
|
||||
@@ -192,6 +210,12 @@ export function MediaShareOptions(props) {
|
||||
const [dimensions, setDimensions] = useState(updateDimensions());
|
||||
const [shareOptions] = useState(ShareOptions());
|
||||
|
||||
const [timestamp, setTimestamp] = useState(0);
|
||||
const [formattedTimestamp, setFormattedTimestamp] = useState(0);
|
||||
const [startAtSelected, setStartAtSelected] = useState(false);
|
||||
|
||||
const [shareMediaLink, setShareMediaLink] = useState(mediaUrl);
|
||||
|
||||
function onWindowResize() {
|
||||
setDimensions(updateDimensions());
|
||||
}
|
||||
@@ -219,6 +243,17 @@ export function MediaShareOptions(props) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateStartAtCheckbox() {
|
||||
setStartAtSelected(!startAtSelected);
|
||||
updateShareMediaLink();
|
||||
}
|
||||
|
||||
function updateShareMediaLink()
|
||||
{
|
||||
const newLink = startAtSelected ? mediaUrl : mediaUrl + "&t=" + Math.trunc(timestamp);
|
||||
setShareMediaLink(newLink);
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
inlineSlider.nextSlide();
|
||||
updateSlider();
|
||||
@@ -243,6 +278,10 @@ export function MediaShareOptions(props) {
|
||||
useEffect(() => {
|
||||
PageStore.on('window_resize', onWindowResize);
|
||||
MediaPageStore.on('copied_media_link', onCompleteCopyMediaLink);
|
||||
|
||||
const localTimestamp = getTimestamp();
|
||||
setTimestamp(localTimestamp);
|
||||
setFormattedTimestamp(ToHHMMSS(localTimestamp));
|
||||
|
||||
return () => {
|
||||
PageStore.removeListener('window_resize', onWindowResize);
|
||||
@@ -273,10 +312,22 @@ export function MediaShareOptions(props) {
|
||||
</div>
|
||||
<div className="copy-field">
|
||||
<div>
|
||||
<input type="text" readOnly value={MediaPageStore.get('media-url')} />
|
||||
<input type="text" readOnly value={shareMediaLink} />
|
||||
<button onClick={onClickCopyMediaLink}>COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="start-at">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="start-at-checkbox"
|
||||
id="id-start-at-checkbox"
|
||||
checked={startAtSelected}
|
||||
onChange={updateStartAtCheckbox}
|
||||
/>
|
||||
Start at {formattedTimestamp}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,13 @@ export function VideoPlayer(props) {
|
||||
document.addEventListener('visibilitychange', initPlayer);
|
||||
}
|
||||
|
||||
player.player.one('loadedmetadata', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paramT = Number(urlParams.get('t'));
|
||||
const timestamp = !isNaN(paramT) ? paramT : 0;
|
||||
player.player.currentTime(timestamp);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsetPlayer();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# should be run as root and only on Ubuntu 18/20, Debian Buster versions!
|
||||
# should be run as root and only on Ubuntu 18/20, Debian 10/11 (Buster/Bullseye) versions!
|
||||
echo "Welcome to the MediacMS installation!";
|
||||
|
||||
if [ `id -u` -ne 0 ]
|
||||
@@ -22,7 +22,7 @@ done
|
||||
|
||||
|
||||
osVersion=$(lsb_release -d)
|
||||
if [[ $osVersion == *"Ubuntu 20"* ]] || [[ $osVersion == *"Ubuntu 18"* ]] || [[ $osVersion == *"buster"* ]]; then
|
||||
if [[ $osVersion == *"Ubuntu 20"* ]] || [[ $osVersion == *"Ubuntu 18"* ]] || [[ $osVersion == *"buster"* ]] || [[ $osVersion == *"bullseye"* ]]; then
|
||||
echo 'Performing system update and dependency installation, this will take a few minutes'
|
||||
apt-get update && apt-get -y upgrade && apt-get install python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip imagemagick python3-certbot-nginx certbot wget xz-utils -y
|
||||
else
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||
os.environ.setdefault("TESTING", "True")
|
||||
# os.environ.setdefault("TESTING", "True")
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
@@ -12,5 +12,3 @@ pytest-cov
|
||||
pytest-django
|
||||
pytest-factoryboy
|
||||
Faker
|
||||
selenium
|
||||
webdriver-manager
|
||||
@@ -28,6 +28,6 @@ django-celery-email
|
||||
m3u8
|
||||
|
||||
django-ckeditor
|
||||
django-debug-toolbar
|
||||
django-debug-toolbar==3.2.4
|
||||
|
||||
django-login-required-middleware==0.6.1
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def test_X(self):
|
||||
# add new file, check it is added and more (eg for videos it is transcoded etc)
|
||||
pass
|
||||
48
tests/api/test_new_media.py
Normal file
48
tests/api/test_new_media.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import uuid
|
||||
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from files.models import Encoding, Media
|
||||
from files.tests import create_account
|
||||
|
||||
API_V1_LOGIN_URL = '/api/v1/login'
|
||||
|
||||
|
||||
class TestX(TestCase):
|
||||
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.password = 'this_is_a_fake_password'
|
||||
self.user = create_account(password=self.password)
|
||||
|
||||
def test_file_upload(self):
|
||||
client = Client()
|
||||
client.login(username=self.user, password=self.password)
|
||||
|
||||
# use both ways, form + API to upload a new media file
|
||||
# while video transcoding through ffmpeg takes place asynchronously
|
||||
# (through celery workers), inside tests ffmpeg runs synchronously
|
||||
# because celery is started with setting task_always_eager
|
||||
# practically this means that this testing will take some time, but
|
||||
# ensures that video transcoding completes well
|
||||
with open('fixtures/small_video.mp4', 'rb') as fp:
|
||||
client.post('/api/v1/media', {'title': 'small video file test', 'media_file': fp})
|
||||
|
||||
with open('fixtures/test_image.png', 'rb') as fp:
|
||||
client.post('/api/v1/media', {'title': 'image file test', 'media_file': fp})
|
||||
|
||||
with open('fixtures/medium_video.mp4', 'rb') as fp:
|
||||
client.post('/fu/upload/', {'qqfile': fp, 'qqfilename': 'medium_video.mp4', 'qquuid': str(uuid.uuid4())})
|
||||
|
||||
self.assertEqual(Media.objects.all().count(), 3, "Problem with file upload")
|
||||
|
||||
# by default the portal_workflow is public, so anything uploaded gets public
|
||||
self.assertEqual(Media.objects.filter(state='public').count(), 3, "Expected all media to be public, as per the default portal workflow")
|
||||
self.assertEqual(Media.objects.filter(media_type='video', encoding_status='success').count(), 2, "Encoding did not finish well")
|
||||
self.assertEqual(Media.objects.filter(media_type='video').count(), 2, "Media identification failed")
|
||||
self.assertEqual(Media.objects.filter(media_type='image').count(), 1, "Media identification failed")
|
||||
self.assertEqual(Media.objects.filter(user=self.user).count(), 3, "User assignment failed")
|
||||
|
||||
# using the provided EncodeProfiles, these two files should produce 9 Encoding objects.
|
||||
# if new EncodeProfiles are added and enabled, this will break!
|
||||
self.assertEqual(Encoding.objects.filter(status='success').count(), 9, "Not all video transcodings finished well")
|
||||
Reference in New Issue
Block a user