Video Duration: The duration of a video is now shown on thumbnails and bellow the video player
Asset nodes now have a new field called "properties.duration_seconds". This holds a copy of the duration stored on the referenced video file and stays in sync using eve hooks. To migrate existing duration times from files to nodes you need to run the following: ./manage.py maintenance reconcile_node_video_duration -ag There are 2 more maintenance commands to be used to determine if there are any missing durations in either files or nodes: find_video_files_without_duration find_video_nodes_without_duration FFProbe is now used to detect what duration a video file has. Reviewed by Sybren.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from unittest import mock
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
from pillar.api.utils import random_etag, utcnow
|
||||
from pillar.tests import AbstractPillarTest
|
||||
from pillar.tests import common_test_data as ctd
|
||||
|
||||
@@ -250,3 +253,149 @@ class UpgradeAttachmentUsageTest(AbstractPillarTest):
|
||||
},
|
||||
node['properties']['attachments'],
|
||||
'The link should have been removed from the attachment')
|
||||
|
||||
|
||||
class ReconcileNodeDurationTest(AbstractPillarTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
self.pid, _ = self.ensure_project_exists()
|
||||
self.fake_now = utcnow()
|
||||
|
||||
# Already correct. Should not be touched.
|
||||
self.node_id0 = self._create_video_node(file_duration=123, node_duration=123)
|
||||
# Out of sync, should be updated
|
||||
self.node_id1 = self._create_video_node(file_duration=3661, node_duration=15)
|
||||
self.node_id2 = self._create_video_node(file_duration=432, node_duration=5)
|
||||
self.node_id3 = self._create_video_node(file_duration=222)
|
||||
# No file duration. Should not be touched
|
||||
self.node_id4 = self._create_video_node()
|
||||
# No file. Should not be touched
|
||||
self.node_id5 = self._create_video_node(include_file=False)
|
||||
# Wrong node type. Should not be touched
|
||||
self.image_node_id = self._create_image_node()
|
||||
|
||||
def id_to_original_dict(*nids):
|
||||
with self.app.app_context():
|
||||
nodes_coll = self.app.db('nodes')
|
||||
return dict(((nid, nodes_coll.find_one({'_id': nid})) for nid in nids))
|
||||
|
||||
self.orig_nodes = id_to_original_dict(
|
||||
self.node_id0,
|
||||
self.node_id1,
|
||||
self.node_id2,
|
||||
self.node_id3,
|
||||
self.node_id4,
|
||||
self.node_id5,
|
||||
self.image_node_id,
|
||||
)
|
||||
|
||||
def test_reconcile_all(self):
|
||||
from pillar.cli.maintenance import reconcile_node_video_duration
|
||||
|
||||
with self.app.app_context():
|
||||
with mock.patch('pillar.api.utils.utcnow') as mock_utcnow:
|
||||
mock_utcnow.return_value = self.fake_now
|
||||
|
||||
reconcile_node_video_duration(all_nodes=True, go=False) # Dry run
|
||||
self.assertAllUnchanged()
|
||||
|
||||
reconcile_node_video_duration(all_nodes=True, go=True)
|
||||
self.assertUnChanged(
|
||||
self.node_id0,
|
||||
self.node_id4,
|
||||
self.image_node_id,
|
||||
)
|
||||
self.assertUpdated(self.node_id1, duration_seconds=3661)
|
||||
self.assertUpdated(self.node_id2, duration_seconds=432)
|
||||
self.assertUpdated(self.node_id3, duration_seconds=222)
|
||||
|
||||
def test_reconcile_some(self):
|
||||
from pillar.cli.maintenance import reconcile_node_video_duration
|
||||
|
||||
with self.app.app_context():
|
||||
with mock.patch('pillar.api.utils.utcnow') as mock_utcnow:
|
||||
mock_utcnow.return_value = self.fake_now
|
||||
|
||||
to_reconcile = [str(self.node_id0), str(self.node_id1), str(self.node_id2), str(self.node_id5)]
|
||||
reconcile_node_video_duration(nodes_to_update=to_reconcile, go=False) # Dry run
|
||||
self.assertAllUnchanged()
|
||||
|
||||
reconcile_node_video_duration(nodes_to_update=to_reconcile, go=True)
|
||||
self.assertUnChanged(
|
||||
self.node_id0,
|
||||
self.node_id3,
|
||||
self.node_id4,
|
||||
self.image_node_id,
|
||||
)
|
||||
self.assertUpdated(self.node_id1, duration_seconds=3661)
|
||||
self.assertUpdated(self.node_id2, duration_seconds=432)
|
||||
|
||||
def assertUpdated(self, nid, duration_seconds):
|
||||
nodes_coll = self.app.db('nodes')
|
||||
new_node = nodes_coll.find_one({'_id': nid})
|
||||
orig_node = self.orig_nodes[nid]
|
||||
self.assertNotEqual(orig_node['_etag'], new_node['_etag'])
|
||||
self.assertEquals(self.fake_now, new_node['_updated'])
|
||||
self.assertEquals(duration_seconds, new_node['properties']['duration_seconds'])
|
||||
|
||||
def assertAllUnchanged(self):
|
||||
self.assertUnChanged(*self.orig_nodes.keys())
|
||||
|
||||
def assertUnChanged(self, *node_ids):
|
||||
nodes_coll = self.app.db('nodes')
|
||||
for nid in node_ids:
|
||||
new_node = nodes_coll.find_one({'_id': nid})
|
||||
orig_node = self.orig_nodes[nid]
|
||||
self.assertEquals(orig_node, new_node)
|
||||
|
||||
def _create_video_node(self, file_duration=None, node_duration=None, include_file=True):
|
||||
file_id, _ = self.ensure_file_exists(file_overrides={
|
||||
'_id': ObjectId(),
|
||||
'content_type': 'video/mp4',
|
||||
'variations': [
|
||||
{'format': 'mp4',
|
||||
'duration': file_duration
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
node = {
|
||||
'name': 'Just a node name',
|
||||
'project': self.pid,
|
||||
'description': '',
|
||||
'node_type': 'asset',
|
||||
'_etag': random_etag(),
|
||||
}
|
||||
props = {'status': 'published',
|
||||
'content_type': 'video',
|
||||
'order': 0}
|
||||
if node_duration is not None:
|
||||
props['duration_seconds'] = node_duration
|
||||
if include_file:
|
||||
props['file'] = file_id
|
||||
return self.create_node({
|
||||
'properties': props,
|
||||
**node})
|
||||
|
||||
def _create_image_node(self):
|
||||
file_id, _ = self.ensure_file_exists(file_overrides={
|
||||
'_id': ObjectId(),
|
||||
'variations': [
|
||||
{'format': 'jpeg'},
|
||||
],
|
||||
})
|
||||
|
||||
node = {
|
||||
'name': 'Just a node name',
|
||||
'project': self.pid,
|
||||
'description': '',
|
||||
'node_type': 'asset',
|
||||
'_etag': random_etag(),
|
||||
}
|
||||
props = {'status': 'published',
|
||||
'file': file_id,
|
||||
'content_type': 'image',
|
||||
'order': 0}
|
||||
return self.create_node({
|
||||
'properties': props,
|
||||
**node})
|
||||
|
Reference in New Issue
Block a user