Compare commits

...

14 Commits
v1.6 ... v1.7

Author SHA1 Message Date
Markos Gogoulos
f8376c5c58 static files (#432) 2022-03-23 19:04:59 +02:00
MrPercheul
e7ae2833d9 Timestamp (#430)
* Test

Messing around to figure out docker and whatnot

* Revert "Test"

This reverts commit 43b76932c5.

* Update docker dev yaml

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

* Timestamp on play

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

* Timestamp when sharing DRAFT

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

* Checkbox and update share url

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

* Cleaning before PR

removing un-necessary modified  files

* Clean up before PR - remove statics

* Timestamp in comments

Parse the comments to wrap timestamps with an appropriate anchor

* Forgotten comments and console.logs

* Last touch for PR

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

* Screenshot

Adding the screenshot for the user_docs

* PR amends

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

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

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

* Framerate fix

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

* Introduction of minimum bitrate modifier

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

* Introduction of minimum bitrate modifier

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

* Deinterlacing and better filter logic
2021-10-01 17:48:52 +03:00
Markos Gogoulos
4480fa7de1 documentation, admin section, plus fix of a typo on models (#293)
* documentation, admin section, plus fix of a typo on models
2021-09-27 15:07:17 +03:00
Markos Gogoulos
32e07035f3 add tests for new uploads (#291)
* add tests for new uploads
2021-09-23 18:34:27 +03:00
39 changed files with 519 additions and 28326 deletions

4
.coveragerc Normal file
View File

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

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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
View 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()

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -68,7 +68,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:13
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -70,7 +70,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:13
volumes:
- ../postgres_data/:/var/lib/postgresql/data/
restart: always

View File

@@ -90,7 +90,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:13
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -66,7 +66,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -62,7 +62,7 @@ services:
depends_on:
- migrations
db:
image: postgres
image: postgres:13
volumes:
- ../postgres_data:/var/lib/postgresql/data/
restart: always

View File

@@ -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
@@ -648,3 +649,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')
```

BIN
docs/images/Demo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

BIN
docs/images/Demo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/images/Demo3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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

View File

@@ -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
@@ -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"),
}
@@ -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],

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

View File

@@ -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

Binary file not shown.

BIN
fixtures/small_video.mp4 Normal file

Binary file not shown.

BIN
fixtures/test_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

28320
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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) {

View File

@@ -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'));

View File

@@ -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>
);
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -12,5 +12,3 @@ pytest-cov
pytest-django
pytest-factoryboy
Faker
selenium
webdriver-manager

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

View File

@@ -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

View 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")