diff --git a/tests/videodinges/factories.py b/tests/videodinges/factories.py
index 0867ab2..fffccd8 100644
--- a/tests/videodinges/factories.py
+++ b/tests/videodinges/factories.py
@@ -34,6 +34,13 @@ def create(model: Type[T], **kwargs) -> T:
if model is models.Upload:
return _create_with_defaults(models.Upload, kwargs, file=SimpleUploadedFile('some_file.txt', b'some contents'))
+ if model is models.Track:
+ return _create_with_defaults(models.Track, kwargs,
+ video=lambda: create(models.Video),
+ lang='en',
+ upload=lambda: create(models.Upload)
+ )
+
raise NotImplementedError('Factory for %s not implemented' % model)
diff --git a/tests/videodinges/models/test_track.py b/tests/videodinges/models/test_track.py
new file mode 100644
index 0000000..3bb0efa
--- /dev/null
+++ b/tests/videodinges/models/test_track.py
@@ -0,0 +1,96 @@
+from django.db.utils import IntegrityError
+
+from django.test import TestCase
+
+from tests.videodinges import factories, UploadMixin
+from videodinges.models import Track, Video, Upload
+
+
+class TrackTestCase(UploadMixin, TestCase):
+ def setUp(self):
+ super().setUp()
+ self.video = Video.objects.create(title='Title', slug='slug', description='Description')
+
+ def test_model_is_created_with_required_fields(self):
+ Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
+ track = Track.objects.all()[0]
+ self.assertEqual(track.video.slug, 'slug')
+ self.assertEqual(track.default, False)
+ self.assertEqual(track.kind, 'subtitles')
+ self.assertEqual(track.lang, 'en')
+ self.assertEqual(track.label, None)
+ self.assertEqual(track.upload.file.name, 'some_file.txt')
+
+ def test_model_is_created_with_nonrequired_fields(self):
+ Track.objects.create(
+ video=self.video,
+ lang='en',
+ upload=factories.create(Upload),
+ default=True,
+ label='Something',
+ kind='chapters',
+ )
+ track = Track.objects.all()[0]
+ self.assertEqual(track.video.slug, 'slug')
+ self.assertEqual(track.default, True)
+ self.assertEqual(track.kind, 'chapters')
+ self.assertEqual(track.lang, 'en')
+ self.assertEqual(track.label, 'Something')
+ self.assertEqual(track.upload.file.name, 'some_file.txt')
+
+ def test_can_create_two_models(self):
+ model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
+ model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
+ self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
+
+ self.assertEqual(model1.video, self.video)
+ self.assertEqual(model1.default, False)
+ self.assertEqual(model1.kind, 'subtitles')
+ self.assertEqual(model1.lang, 'en')
+
+ self.assertEqual(model2.video, self.video)
+ self.assertEqual(model2.default, False)
+ self.assertEqual(model2.kind, 'subtitles')
+ self.assertEqual(model2.lang, 'nl')
+
+ def test_can_create_two_models_with_one_default(self):
+ model1 = Track.objects.create(video=self.video, default=True, lang='en', upload=factories.create(Upload))
+ model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
+ self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
+
+ self.assertEqual(model1.video, self.video)
+ self.assertEqual(model1.default, True)
+ self.assertEqual(model1.kind, 'subtitles')
+ self.assertEqual(model1.lang, 'en')
+
+ self.assertEqual(model2.video, self.video)
+ self.assertEqual(model2.default, False)
+ self.assertEqual(model2.kind, 'subtitles')
+ self.assertEqual(model2.lang, 'nl')
+
+ def test_cannot_set_default_twice(self):
+ video = factories.create(Video)
+ Track.objects.create(video=video, default=True, lang='en', upload=factories.create(Upload))
+ with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
+ Track.objects.create(video=video, default=True, lang='nl', upload=factories.create(Upload))
+
+ def test_cannot_set_two_subtitles_with_same_lang(self):
+ video = factories.create(Video)
+ Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
+ with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
+ Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
+
+ def test_can_create_two_models_with_same_lang_but_different_kind(self):
+ model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
+ model2 = Track.objects.create(video=self.video, lang='en', kind='chapters', upload=factories.create(Upload))
+ self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
+
+ self.assertEqual(model1.video, self.video)
+ self.assertEqual(model1.default, False)
+ self.assertEqual(model1.kind, 'subtitles')
+ self.assertEqual(model1.lang, 'en')
+
+ self.assertEqual(model2.video, self.video)
+ self.assertEqual(model2.default, False)
+ self.assertEqual(model2.kind, 'chapters')
+ self.assertEqual(model2.lang, 'en')
diff --git a/tests/videodinges/models/test_transcoding.py b/tests/videodinges/models/test_transcoding.py
index 9e0fcbc..af4aa56 100644
--- a/tests/videodinges/models/test_transcoding.py
+++ b/tests/videodinges/models/test_transcoding.py
@@ -8,6 +8,7 @@ from videodinges.models import Transcoding, Video, qualities, transcoding_types,
class TranscodingTestCase(TestCase):
def setUp(self):
+ super().setUp()
video = Video.objects.create(title='Title', slug='slug', description='Description')
Transcoding.objects.create(video=video, quality=qualities[0].name, type=str(transcoding_types[0]), url='https://some_url')
diff --git a/tests/videodinges/views/test_video.py b/tests/videodinges/views/test_video.py
index b5dedb8..4804f36 100644
--- a/tests/videodinges/views/test_video.py
+++ b/tests/videodinges/views/test_video.py
@@ -246,3 +246,191 @@ class VideoTestCase(UploadMixin, TestCase):
)
+class VideoWithTrackTestCase(UploadMixin, TestCase):
+ def setUp(self):
+ super().setUp()
+ self.client = Client()
+ self.video = factories.create(
+ models.Video,
+ title='Vid 1',
+ slug='vid-1',
+ default_quality='480p',
+ )
+ factories.create(
+ models.Transcoding,
+ video=self.video,
+ quality='480p',
+ type='video/webm; codecs="vp9, opus"',
+ url='http://480p.webm',
+ )
+ factories.create(
+ models.Transcoding,
+ video=self.video,
+ quality='480p',
+ type='video/mp4',
+ url='http://480p.mp4',
+ )
+
+ def test_video_view_renders_track_properly(self):
+
+ factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ """""",
+ content,
+ )
+
+ def test_video_view_renders_multiple_tracks_properly(self):
+
+ track_en = factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ )
+
+ track_nl = factories.create(
+ models.Track,
+ video=self.video,
+ lang='nl',
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ f"""""",
+ content,
+ )
+
+
+ def test_video_view_renders_default_track_properly(self):
+
+ factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ default=True
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ """""",
+ content,
+ )
+
+ def test_video_view_renders_multiple_tracks_properly_with_one_default(self):
+
+ track_en = factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ default=True
+ )
+
+ track_nl = factories.create(
+ models.Track,
+ video=self.video,
+ lang='nl',
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ f"""""",
+ content,
+ )
+
+ def test_video_view_renders_track_label(self):
+
+ factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ label='Some Label',
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ """""",
+ content,
+ )
+
+
+ def test_video_view_renders_track_kind(self):
+
+ factories.create(
+ models.Track,
+ video=self.video,
+ lang='en',
+ kind='captions',
+ )
+
+ resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
+
+ self.assertEqual(resp.status_code, 200)
+
+ content:str = resp.content.decode(resp.charset)
+
+ self.assertInHTML(
+ """""",
+ content,
+ )
diff --git a/videodinges/admin.py b/videodinges/admin.py
index bd352ae..8736f49 100644
--- a/videodinges/admin.py
+++ b/videodinges/admin.py
@@ -1,3 +1,5 @@
+from typing import Iterable
+
from django import forms
from django.contrib import admin
from django.core.exceptions import ValidationError
@@ -17,16 +19,37 @@ class TranscodingsForm(forms.ModelForm):
self.add_error('upload', validation_error)
return cleaned_data
+class TrackInlineFormset(forms.BaseInlineFormSet):
+ forms: Iterable[forms.ModelForm]
+ def clean(self):
+ cleaned_data = super().clean()
+ default_cnt = 0
+ for form in self.forms:
+ try:
+ if form.cleaned_data['default'] is True:
+ default_cnt += 1
+ except AttributeError:
+ pass
+ if default_cnt > 1:
+ form.add_error('default', ValidationError('Can set only one track as default'))
+ return cleaned_data
+
class TranscodingsInline(admin.StackedInline):
model = models.Transcoding
form = TranscodingsForm
fields = ['quality', 'type', 'url', 'upload']
extra = 0
+class TracksInline(admin.StackedInline):
+ model = models.Track
+ formset = TrackInlineFormset
+ fields = ('default', 'kind', 'lang', 'label', 'upload')
+ extra = 0
+
class VideoAdmin(admin.ModelAdmin):
model = models.Video
fields = ['title', 'description', 'slug', 'poster', 'og_image', 'created_at', 'default_quality']
- inlines = [TranscodingsInline]
+ inlines = (TranscodingsInline, TracksInline)
list_display = ('title', 'slug', 'created_at', 'updated_at')
ordering = ('-created_at', )
diff --git a/videodinges/models.py b/videodinges/models.py
index eba1104..885bb70 100644
--- a/videodinges/models.py
+++ b/videodinges/models.py
@@ -108,6 +108,33 @@ class Transcoding(models.Model):
]
db_table = 'transcodings'
+class Track(models.Model):
+ KINDS = (
+ 'subtitles',
+ 'captions',
+ 'descriptions',
+ 'chapters',
+ 'metadata',
+ )
+ id = models.AutoField(primary_key=True)
+ video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='tracks')
+ default = models.BooleanField(default=False)
+ kind = models.CharField(choices=((kind, kind) for kind in KINDS), max_length=128, default=KINDS[0])
+ lang = models.CharField(max_length=128)
+ label = models.CharField(max_length=128, blank=True, null=True)
+ upload = models.OneToOneField(Upload, on_delete=models.PROTECT)
+
+ def __str__(self):
+ return '%s_%s' % (self.kind, self.lang)
+
+ class Meta:
+ unique_together = ('video', 'kind', 'lang')
+ constraints = (
+ constraints.UniqueConstraint(
+ fields=('video',), condition=Q(default=True), name='only_one_default_per_video'),
+ )
+ db_table = 'tracks'
+
def get_quality_by_name(name: str) -> Optional[Quality]:
for quality in qualities:
if quality.name == name:
diff --git a/videodinges/templates/video.html.j2 b/videodinges/templates/video.html.j2
index 102a51e..57f16c4 100644
--- a/videodinges/templates/video.html.j2
+++ b/videodinges/templates/video.html.j2
@@ -11,6 +11,9 @@
diff --git a/videodinges/views.py b/videodinges/views.py
index 39bad45..0e2a938 100644
--- a/videodinges/views.py
+++ b/videodinges/views.py
@@ -51,6 +51,16 @@ def video(request: HttpRequest, slug: str) -> HttpResponse:
template_data['qualities'] = qualities.keys()
+ template_data['tracks'] = [
+ {
+ 'default': track.default,
+ 'src': track.upload.file.url,
+ 'srclang': track.lang,
+ 'kind': track.kind,
+ 'label': track.label or track.lang,
+ } for track in video.tracks.all()
+ ]
+
return render(request, 'video.html.j2', template_data, using='jinja2')
def index(request: HttpRequest) -> HttpResponse: