Merge branch 'bump-project'

Work of last month to get this project running for the first release.
This commit is contained in:
2020-05-21 17:17:21 +02:00
16 changed files with 487 additions and 169 deletions

View File

@@ -3,20 +3,20 @@ import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "videodinges.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "videodinges.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError: except ImportError:
# The above import may fail for some other reason. Ensure that the # The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other # issue is really that Django is missing to avoid masking other
# exceptions on Python 2. # exceptions on Python 2.
try: try:
import django import django
except ImportError: except ImportError:
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " "available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?" "forget to activate a virtual environment?"
) )
raise raise
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View File

@@ -1,3 +1,3 @@
Django==1.11 Django==3.0.*
pkg-resources==0.0.0 pytz
pytz==2018.7 Jinja2==2.11.*

0
static/.placeholder Normal file
View File

24
static/js/video.js Normal file
View File

@@ -0,0 +1,24 @@
function init() {
var hash = document.location.hash;
var res = hash.match(/t=([0-9]+)/);
if (res) {
var vids = document.getElementsByTagName('video');
// only first video
var vid = vids[0];
vid.currentTime = res[1];
vid.autoplay = true;
}
}
init();
function vidTimeInUrl(el) {
var vids = document.getElementsByTagName('video');
// only first video
var vid = vids[0];
var currentTime = vid.currentTime;
var href = el.href + "#t=" + parseInt(currentTime);
el.href = href;
return true;
}

0
tmp/.placeholder Normal file
View File

0
uploads/.placeholder Normal file
View File

34
videodinges/admin.py Normal file
View File

@@ -0,0 +1,34 @@
from django import forms
from django.contrib import admin
from django.core.exceptions import ValidationError
from . import models
class TranscodingsForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if not cleaned_data['url'] and not cleaned_data['upload']:
validation_error = ValidationError('Either url or upload must be given', code='invalid')
self.add_error('url', validation_error)
self.add_error('upload', validation_error)
if cleaned_data['url'] and cleaned_data['upload']:
validation_error = ValidationError('Cannot fill both url and upload', code='invalid')
self.add_error('url', validation_error)
self.add_error('upload', validation_error)
return cleaned_data
class TranscodingsInline(admin.StackedInline):
model = models.Transcoding
form = TranscodingsForm
fields = ['quality', 'type', 'url', 'upload']
extra = 0
class VideoAdmin(admin.ModelAdmin):
model = models.Video
fields = ['title', 'description', 'slug', 'poster', 'og_image', 'created_at', 'default_quality']
inlines = [TranscodingsInline]
list_display = ('title', 'slug', 'created_at', 'updated_at')
ordering = ('-created_at', )
admin.site.register(models.Video, VideoAdmin)
admin.site.register(models.Upload)

View File

@@ -1,32 +1,115 @@
import os
from datetime import datetime from datetime import datetime
from collections import namedtuple from typing import NamedTuple, Optional, Union
from django.db import models
Quality = namedtuple('Quality', ['name', 'width', 'height', 'priority']) from django.db import models
from django.db.models import constraints
from django.db.models.query_utils import Q
class Quality(NamedTuple):
name: str
width: int
height: int
priority: int
class TranscodingType(NamedTuple):
name: str
short_name: str
def __str__(self):
return self.name
qualities = ( qualities = (
Quality(name='360p', width=640, height=360, priority=1), Quality(name='360p', width=640, height=360, priority=1),
Quality(name='480p', width=853, height=480, priority=2), Quality(name='480p', width=853, height=480, priority=2),
Quality(name='480p', width=1280, height=720, priority=2), Quality(name='720p', width=1280, height=720, priority=2),
Quality(name='1080p', width=1920, height=1080, priority=1), Quality(name='1080p', width=1920, height=1080, priority=1),
) )
transcoding_types = (
TranscodingType(name='video/webm', short_name='vp8'),
TranscodingType(name='video/webm; codecs="vp8, vorbis"', short_name='vp8'),
TranscodingType(name='video/webm; codecs="vp9, opus"', short_name='vp9'),
TranscodingType(name='video/mp4', short_name='h.264'),
TranscodingType(name='video/mp4; codecs="avc1.64001f,mp4a.40.2"', short_name='h.264'),
)
class Upload(models.Model):
id = models.AutoField(primary_key=True)
file = models.FileField()
def __str__(self):
return os.path.basename(self.file.path)
class Meta:
db_table = 'uploads'
class Video(models.Model): class Video(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
title = models.CharField() title = models.CharField(max_length=256)
slug = models.CharField() slug = models.CharField(max_length=256, unique=True)
description = models.TextField() description = models.TextField()
created_at = models.DateTimeField(default=datetime.now) poster = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True, related_name='video_poster')
updated_at = models.DateTimeField(default=datetime.now) og_image = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True, related_name='video_og_image')
default_quality = models.CharField(
choices=((quality.name, quality.name) for quality in qualities),
max_length=128,
blank=True,
null=True
)
created_at = models.DateTimeField(default=datetime.now)
updated_at = models.DateTimeField(default=datetime.now)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None): def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.updated_at = datetime.now() self.updated_at = datetime.now()
super().save(force_insert, force_update, using, update_fields) super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title
class Meta:
indexes = [models.Index(fields=['slug']), models.Index(fields=['created_at'])]
db_table = 'videos'
class Transcoding(models.Model): class Transcoding(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
video = models.ForeignKey(Video, on_delete=models.CASCADE) video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='transcodings')
quality = models.CharField(choices=(quality.name, quality.name) for quality in qualities) quality = models.CharField(choices=((quality.name, quality.name) for quality in qualities), max_length=128)
file = models.FileField() type = models.CharField(choices=((str(type_), str(type_)) for type_ in transcoding_types), max_length=128)
upload = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True)
url = models.CharField(max_length=256, null=True, blank=True, unique=True)
def __str__(self):
return self.quality
@property
def quality_obj(self):
return get_quality_by_name(self.quality)
class Meta:
unique_together = ('video', 'quality', 'type')
constraints = [
constraints.CheckConstraint(
check=Q(upload__isnull=False) | Q(url__isnull=False),
name='upload_or_url_is_filled'
),
constraints.CheckConstraint(
check=~(Q(upload__isnull=False) & Q(url__isnull=False)),
name='upload_and_url_cannot_both_be_filled'
),
]
db_table = 'transcodings'
def get_quality_by_name(name: str) -> Optional[Quality]:
for quality in qualities:
if quality.name == name:
return quality
def get_short_name_of_transcoding_type(transcoding_type: Union[str, TranscodingType]) -> str:
if isinstance(transcoding_type, str):
for type_ in transcoding_types:
if type_.name == transcoding_type:
transcoding_type = type_
if isinstance(transcoding_type, TranscodingType):
return transcoding_type.short_name

View File

@@ -1,120 +0,0 @@
"""
Django settings for videodinges project.
Generated by 'django-admin startproject' using Django 1.11.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$9059r$6diz+m+z91(sn_mx3yarj%h)j_+2z$6j8yp+(-h-1c'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'videodinges.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'videodinges.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'

View File

@@ -0,0 +1,4 @@
try:
from .localsettings import *
except ImportError:
from .defaultsettings import *

View File

@@ -0,0 +1,141 @@
"""
Django settings for videodinges project.
Generated by 'django-admin startproject' using Django 1.11.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$9059r$6diz+m+z91(sn_mx3yarj%h)j_+2z$6j8yp+(-h-1c'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'videodinges',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'videodinges.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [os.path.join(BASE_DIR, 'videodinges', 'templates')],
'APP_DIRS': False,
'OPTIONS': {},
},
]
WSGI_APPLICATION = 'videodinges.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/uploads/'
FILE_UPLOAD_HANDLERS = ['django.core.files.uploadhandler.TemporaryFileUploadHandler']
FILE_UPLOAD_MAX_MEMORY_SIZE = 2147483648 # 2GB
FILE_UPLOAD_TEMP_DIR = os.path.join(BASE_DIR, 'tmp') # probably default /tmp is too small for video files
URL_BASE = '' # usefull to prefix the application URL on deployment

View File

@@ -0,0 +1,15 @@
"""
Copy this file to localsettings.py to make local overrides.
BEWARE to always import defaultsettings as well if activate this file.
"""
from .defaultsettings import *
DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'videos', # database name
'USER': 'videos',
'PASSWORD': 'v3r7s3cr3t',
'HOST': 'localhost',
'PORT': '5432',
}

View File

@@ -0,0 +1,10 @@
<html>
<body>
<h1>Video's</h1>
<ul>
{% for video in videos %}
<li><a href="{{ video.slug }}.html">{{ video.title }}</a></li>
{% endfor %}
</ul>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<html>
<head>
{# <link rel="stylesheet" href="style.css" type="text/css" media="screen" /> #}
{% if og_image %}
<meta property="og:image" content="{{og_image}}" />
{% endif %}
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
<video width="{{ width }}" height="{{ height }}" {% if poster %}poster="{{ poster }}" {% endif %}controls="controls">
{% for source in sources %}
<source src="{{ source.src }}" type='{{ source.type }}' />
{% endfor %}
You need a browser that understands HTML5 video and supports {% for i in used_codecs %}{{ i }}{% if not loop.last %} or {% endif %}{% endfor %} codecs.
</video><br />
<p>
{% for quality in qualities %}
{% if quality == current_quality %}
<strong>{{ quality }} versie</strong>
{% else %}
<a href="{{ slug }}.html?quality={{ quality }}" onclick="vidTimeInUrl(this);">{{ quality }} versie</a>
{% endif %}
{% if not loop.last %}|{% endif %}
{% endfor %}
</p>
<p>
{{ description|safe }}
</p>
<div id="commenter-container" data-object-id="welmers-video-{{ slug }}">
<div class="commenter-count-container"><span class="commenter-count">0</span> comments total</div>
</div>
<script data-container="commenter-container" src="//www.welmers.net/commenter/js/commenter.js" type="text/javascript"></script>
<script src="static/js/video.js" type="text/javascript"></script>
</body>
</html>

View File

@@ -1,21 +1,35 @@
"""videodinges URL Configuration """videodinges URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/ https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples: Examples:
Function views Function views
1. Add an import: from my_app import views 1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views Class-based views
1. Add an import: from other_app.views import Home 1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin from django.contrib import admin
from django.urls import include
urlpatterns = [ from . import views
url(r'^admin/', admin.site.urls),
_urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.index),
url(r'^(?P<slug>[\w-]+).html', views.video)
] ]
_urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.URL_BASE:
urlpatterns = [url(r'^{}/'.format(settings.URL_BASE), include(_urlpatterns))]
else:
urlpatterns = _urlpatterns

77
videodinges/views.py Normal file
View File

@@ -0,0 +1,77 @@
from collections import defaultdict
from typing import List, Dict, Any
from django.http import HttpResponse, HttpRequest, Http404
from django.shortcuts import render
from . import models
def video(request: HttpRequest, slug: str) -> HttpResponse:
try:
video = models.Video.objects.get(slug=slug)
except models.Video.DoesNotExist:
raise Http404('Video not found')
template_data = dict(
og_image=video.og_image.file.url if video.og_image else None,
title=video.title,
poster=video.poster.file.url if video.poster else None,
description=video.description,
slug=video.slug
)
qualities = _get_qualities(video)
try:
# find quality specified by URL param
quality = qualities[request.GET['quality']]
except:
# find quality specified by default quality specified for video
try:
quality = qualities[video.default_quality]
except:
# take default first quality
quality = next(iter(qualities.values()))
template_data.update(
width=quality[0].quality_obj.width,
height=quality[0].quality_obj.height,
current_quality=quality[0].quality_obj.name
)
template_data['sources'] = [
{
'src': _url_for(transcoding),
'type': transcoding.type,
}
for transcoding in quality ]
template_data['used_codecs'] = [
models.get_short_name_of_transcoding_type(transcoding.type)
for transcoding in quality
]
template_data['qualities'] = qualities.keys()
return render(request, 'video.html.j2', template_data, using='jinja2')
def index(request: HttpRequest) -> HttpResponse:
videos = models.Video.objects.order_by('-created_at').all()
return render(request, 'index.html.j2', dict(videos=videos), using='jinja2')
def _get_dict_from_models_with_fields(model, *fields: str) -> Dict[str, Any]:
ret = {}
for field in fields:
ret[field] = model.__dict__[field]
return ret
def _get_qualities(video: models.Video) -> Dict[str, List[models.Transcoding]]:
transcodings: List[models.Transcoding] = video.transcodings.order_by('quality').all()
qualities = defaultdict(list)
for transcoding in transcodings:
qualities[transcoding.quality_obj.name].append(transcoding)
return dict(qualities)
def _url_for(transcoding: models.Transcoding) -> str:
if transcoding.url:
return transcoding.url
elif transcoding.upload:
return transcoding.upload.file.url