Compare commits

..

19 Commits
v1.9 ... v2.0

Author SHA1 Message Date
Markos Gogoulos
a3997bfb1c update versions for pre-commit (#741) 2023-03-14 14:09:52 +02:00
Markos Gogoulos
4b0718c43f version 2.0 2023-03-14 13:30:26 +02:00
Markos Gogoulos
91d8179fa0 improve formating on Readme 2023-03-14 13:15:17 +02:00
Markos Gogoulos
6532b19849 Update README.md 2023-03-14 13:13:55 +02:00
Markos Gogoulos
6ea8fd12a3 fix issue with uninitialized video player 2023-03-14 13:13:15 +02:00
Markos Gogoulos
d971bb955f pin versions (#718) 2023-02-17 12:46:45 +02:00
Markos Gogoulos
b52b008f89 enable cors for media dir (#701) 2023-02-17 11:51:48 +02:00
MICRUFUN
30cf5d7176 How to modify encode profiles (#682) 2023-01-03 12:33:38 +02:00
masavini
6fd9a7d37f remove zombie thumbnails (#657)
remove zombie thumbnails
2022-12-05 12:31:31 +02:00
Markos Gogoulos
9c6d13559b update doc 2022-11-29 17:24:13 +02:00
Markos Gogoulos
8ec97a8219 pre-commit yaml 2022-11-29 15:27:15 +02:00
Markos Gogoulos
de8f9ca718 fix pre-commit version 2022-11-29 14:55:00 +02:00
Markos Gogoulos
a4bedca4db fix pre-commit version 2022-11-29 14:53:05 +02:00
Kyle Maas
da565b3bfc Fix double slashes in URIs (#558) 2022-11-29 11:22:30 +02:00
Kyle Maas
239ff6cb60 Add meta description tag for search engines (#551) 2022-11-29 11:02:16 +02:00
masavini
da840b156d add api path to LOGIN_REQUIRED_IGNORE_PATHS (#483) 2022-11-29 10:59:36 +02:00
Markos Gogoulos
b08d493823 static files 2022-09-20 12:27:33 +00:00
masavini
25eaa35758 Fix admin user creation (#472)
add ADMIN_USER, ADMIN_PASSWORD and ADMIN_EMAIL
2022-09-20 15:18:25 +03:00
MrPercheul
cba2ed75ed Show comments in the Timebar (#442)
Show comments in the Timebar
2022-09-20 15:16:16 +03:00
42 changed files with 790 additions and 66 deletions

View File

@@ -8,8 +8,8 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.0
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

17
.github/workflows/pre-commit.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: pre-commit
on:
pull_request:
push:
branches:
- main
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,15 +1,15 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.5.4
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 23.1.0
hooks:
- id: black
language_version: python3

View File

@@ -1,11 +1,8 @@
# MediaCMS
[![Code Quality: Cpp](https://img.shields.io/lgtm/grade/python/g/mediacms-io/mediacms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/mediacms-io/mediacms/context:python)
[![Code Quality: Cpp](https://img.shields.io/lgtm/grade/javascript/g/mediacms-io/mediacms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/mediacms-io/mediacms/context:javascript)
<br/>
[![GitHub license](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://raw.githubusercontent.com/mediacms-io/mediacms/main/LICENSE.txt)
[![Releases](https://img.shields.io/github/v/release/mediacms-io/mediacms?color=green)](https://github.com/mediacms-io/mediacms/releases/)
[![DockerHub](https://img.shields.io/docker/pulls/mediacms/mediacms)](https://hub.docker.com/repository/docker/mediacms/mediacms/)
[![DockerHub](https://img.shields.io/docker/pulls/mediacms/mediacms)](https://hub.docker.com/r/mediacms/mediacms)

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ DEBUG = False
# PORTAL NAME, this is the portal title and
# is also shown on several places as emails
PORTAL_NAME = "MediaCMS"
PORTAL_DESCRIPTION = ""
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/London"
@@ -86,6 +87,7 @@ MAX_MEDIA_PER_PLAYLIST = 70
UPLOAD_MAX_SIZE = 800 * 1024 * 1000 * 5
MAX_CHARS_FOR_COMMENT = 10000 # so that it doesn't end up huge
TIMESTAMP_IN_TIMEBAR = False # shows timestamped comments in the timebar for videos
ALLOW_MENTION_IN_COMMENTS = False # allowing to mention other users with @ in the comments
# valid options: content, author
@@ -479,4 +481,5 @@ if GLOBAL_LOGIN_REQUIRED:
r'/accounts/login/$',
r'/accounts/logout/$',
r'/accounts/signup/$',
r'/api/v[0-9]+/',
]

View File

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

View File

@@ -18,6 +18,10 @@ services:
context: .
dockerfile: ./Dockerfile-dev
image: mediacms/mediacms-dev:latest
environment:
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ports:
- "80:80"
volumes:

View File

@@ -16,6 +16,7 @@
- [13. How To Add A Static Page To The Sidebar](#13-how-to-add-a-static-page-to-the-sidebar)
- [14. Add Google Analytics](#14-add-google-analytics)
- [15. Debugging email issues](#15-debugging-email-issues)
- [16. Frequently Asked Questions](#16-frequently-asked-questions)
## 1. Welcome
@@ -459,7 +460,9 @@ to be written
Through the admin section - http://your_installation/admin/
## 12. Video transcoding
Add / remove resolutions and profiles through http://your_installation/admin/encodeprofile
Add / remove resolutions and profiles by modifying the database table of `Encode profiles` through https://your_installation/admin/files/encodeprofile/
For example, the `Active` state of any profile can be toggled to enable or disable it.
## 13. How To Add A Static Page To The Sidebar
@@ -687,3 +690,41 @@ For example, while specifying wrong password for my Gmail account I get
```
SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 https://support.google.com/mail/?p=BadCredentials d4sm12687785wrc.34 - gsmtp')
```
## 16. Frequently Asked Questions
Video is playing but preview thumbnails are not showing for large video files
Chances are that the sprites file was not created correctly.
The output of files.tasks.produce_sprite_from_video() function in this case is something like this
```
convert-im6.q16: width or height exceeds limit `/tmp/img001.jpg' @ error/cache.c/OpenPixelCache/3912.
```
Solution: edit file `/etc/ImageMagick-6/policy.xml` and set bigger values for the lines that contain width and height. For example
```
<policy domain="resource" name="height" value="16000KP"/>
<policy domain="resource" name="width" value="16000KP"/>
```
Newly added video files now will be able to produce the sprites file needed for thumbnail previews. To re-run that task on existing videos, enter the Django shell
```
root@8433f923ccf5:/home/mediacms.io/mediacms# source /home/mediacms.io/bin/activate
root@8433f923ccf5:/home/mediacms.io/mediacms# python manage.py shell
Python 3.8.14 (default, Sep 13 2022, 02:23:58)
```
and run
```
In [1]: from files.models import Media
In [2]: from files.tasks import produce_sprite_from_video
In [3]: for media in Media.objects.filter(media_type='video', sprites=''):
...: produce_sprite_from_video(media.friendly_token)
```
this will re-create the sprites for videos that the task failed.

View File

@@ -54,6 +54,13 @@ docker-compose -f docker-compose-dev.yaml build
docker-compose -f docker-compose-dev.yaml up
```
An `admin` user is created during the installation process. Its attributes are defined in `docker-compose-dev.yaml`:
```
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
```
### Frontend application changes
Eg change `frontend/src/static/js/pages/HomePage.tsx` , dev application refreshes in a number of seconds (hot reloading) and I see the changes, once I'm happy I can run

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -7,6 +7,7 @@
- [Search media](#search-media)
- [Using Timestamps for sharing](#using-timestamps-for-sharing)
- [Mentionning users in comments](#Mentionning-users-in-comments)
- [Show comments in the Timebar](#Show-comments-in-the-Timebar)
- [Share media](#share-media)
- [Embed media](#embed-media)
- [Customize my profile options](#customize-my-profile-options)
@@ -234,6 +235,17 @@ Comments send with mentions will contain a link to the user page, and can be set
<img src="./images/Mention4.png"/>
</p>
## Show comments in the Timebar
When enabled, comments including a timestamp will also be displayed in the current video Timebar as a little colorful dot. The comment can be previewed by hovering the dot (left image) and it will be displayed on top of the video when reaching the correct time (right image).
Only comments with correct timestamps formats (HH:MM:SS or MM:SS) will be picked up and appear in the Timebar.
<p align="left">
<img src="./images/TimebarComments_Hover.png" height="180" alt="Comment preview on hover"/>
<img src="./images/TimebarComments_Hit.png" height="180" alt="Comment shown when the timestamp is reached "/>
</p>
## Search media
How search can be used

View File

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

View File

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

View File

@@ -305,7 +305,6 @@ def show_related_media_author(media, request, limit):
def show_related_media_calculated(media, request, limit):
"""Return a list of related media based on ML recommendations
A big todo!
"""

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0002_auto_20201201_0712'),
]

View File

@@ -1,3 +1,4 @@
import glob
import json
import logging
import os
@@ -313,7 +314,6 @@ class Media(models.Model):
self.__original_uploaded_poster = self.uploaded_poster
def save(self, *args, **kwargs):
if not self.title:
self.title = self.media_file.path.split("/")[-1]
@@ -371,7 +371,6 @@ class Media(models.Model):
# will run only when a poster is uploaded for the first time
if self.uploaded_poster and self.uploaded_poster != self.__original_uploaded_poster:
with open(self.uploaded_poster.path, "rb") as f:
# set this otherwise gets to infinite loop
self.__original_uploaded_poster = self.uploaded_poster
@@ -579,9 +578,7 @@ class Media(models.Model):
# attempt to break media file in chunks
if self.duration > settings.CHUNKIZE_VIDEO_DURATION and chunkize:
for profile in profiles:
if profile.extension == "gif":
profiles.remove(profile)
encoding = Encoding(media=self, profile=profile)
@@ -1405,6 +1402,13 @@ def media_file_delete(sender, instance, **kwargs):
helpers.rm_dir(p)
instance.user.update_user_media()
# remove extra zombie thumbnails
if instance.thumbnail:
thumbnails_path = os.path.dirname(instance.thumbnail.path)
thumbnails = glob.glob(f'{thumbnails_path}/{instance.uid.hex}.*')
for thumbnail in thumbnails:
helpers.rm_file(thumbnail)
@receiver(m2m_changed, sender=Media.category.through)
def media_m2m(sender, instance, **kwargs):

View File

@@ -268,7 +268,6 @@ def encode_media(
# return False
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
tf = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
tfpass = create_temp_file(suffix=".{0}".format(profile.extension), dir=temp_dir)
ffmpeg_commands = produce_ffmpeg_commands(

View File

@@ -5955,7 +5955,13 @@
}
},
"mediacms-vjs-plugin": {
"version": "file:packages/vjs-plugin"
"version": "file:packages/vjs-plugin",
"requires": {
"mediacms-vjs-plugin-font-icons": "file:packages/vjs-plugin-font-icons"
}
},
"mediacms-vjs-plugin-font-icons": {
"version": "file:packages/vjs-plugin-font-icons"
},
"memory-fs": {
"version": "0.4.1",

View File

@@ -8,6 +8,10 @@ import { PageActions, MediaPageActions } from '../../utils/actions/';
import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/';
import { PopupMain, UserThumbnail } from '../_shared';
import './videojs-markers.js';
import './videojs.markers.css';
import {enableMarkers, addMarker} from './videojs-markers_config.js'
import './Comments.scss';
const commentsText = {
@@ -426,6 +430,12 @@ export default function CommentsList(props) {
function onCommentsLoad() {
const retrievedComments = [...MediaPageStore.get('media-comments')];
const video = videojs('vjs_video_3');
if (MediaCMS.features.media.actions.timestampTimebar)
{
enableMarkers(video);
}
if (MediaCMS.features.media.actions.comment_mention === true)
{
@@ -433,14 +443,18 @@ export default function CommentsList(props) {
comment.text = setMentions(comment.text);
});
}
retrievedComments.forEach(comment => {
comment.text = setTimestampAnchors(comment.text);
});
displayCommentsRelatedAlert();
setComments(retrievedComments);
}
video.one('loadedmetadata', () => {
retrievedComments.forEach(comment => {
comment.text = setTimestampAnchorsAndMarkers(comment.text, video);
});
displayCommentsRelatedAlert();
setComments([...retrievedComments]);
});
setComments([...retrievedComments]);
}
function setMentions(text)
{
let sanitizedComment = text.split('@(_').join("<a href=\"/user/");
@@ -448,7 +462,7 @@ export default function CommentsList(props) {
return sanitizedComment.split('_]').join("</a>");
}
function setTimestampAnchors(text)
function setTimestampAnchorsAndMarkers(text, videoPlayer)
{
function wrapTimestampWithAnchor(match, string)
{
@@ -460,6 +474,10 @@ export default function CommentsList(props) {
s += m * parseInt(split.pop(), 10);
m *= 60;
}
if (MediaCMS.features.media.actions.timestampTimebar)
{
addMarker(videoPlayer, s, text);
}
searchParameters.set('t', s)
const wrapped = "<a href=\"" + MediaPageStore.get('media-url').split('?')[0] + "?" + searchParameters + "\">" + match + "</a>";

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,21 @@
Django==3.1.12
djangorestframework==3.12.2
django-allauth==0.44.0
psycopg2-binary==2.8.6
uwsgi==2.0.19.1
django-redis==4.12.1
celery==4.4.7
drf-yasg==1.20.0
Pillow==8.2.0
django-imagekit
markdown
django-filter
filetype
django-mptt
django-crispy-forms
django-imagekit==4.1.0
markdown==3.3.6
django-filter==21.1
filetype==1.0.10
django-mptt==0.13.4
django-crispy-forms==1.13.0
requests==2.25.0
django-celery-email
m3u8
django-ckeditor
django-celery-email==3.0.0
m3u8==1.0.0
django-ckeditor==6.2.0
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

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,8 @@
<link rel="canonical" href="{{FRONTEND_HOST}}{{media_object.get_absolute_url}}">
<meta name="description" content="{{PORTAL_DESCRIPTION}}">
<meta property="og:title" content="{{PORTAL_NAME}}">
<meta property="og:url" content="{{FRONTEND_HOST}}">
<meta property="og:type" content="website">

View File

@@ -22,6 +22,7 @@ MediaCMS.features = {
dislike: {% if CAN_DISLIKE_MEDIA %}true{% else %}false{% endif %},
download: true,
comment: true,
timestampTimebar: {% if TIMESTAMP_IN_TIMEBAR %}true{% else %}false{% endif %},
comment_mention: {% if CAN_MENTION_IN_COMMENTS %}true{% else %}false{% endif %},
save: true,
},

View File

@@ -2,7 +2,7 @@ MediaCMS.site = {
id: 'mediacms',
title: "{{PORTAL_NAME}}",
url: '{{FRONTEND_HOST}}',
api: '{{FRONTEND_HOST}}api/v1',
api: '{{FRONTEND_HOST}}/api/v1',
theme: {
mode: '{{DEFAULT_THEME}}',
switch: {

View File

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

View File

@@ -176,7 +176,6 @@ Sender email: %s\n
class UserList(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)

View File

@@ -1 +1 @@
1.6
2.0