Skip to content

Commit d3e9031

Browse files
fix: Add duration from encodejob to video (#1212)
* added migration to add duration in video from encode jobs * fixed attribute error * fixed formatting issues * added condition to migrate video duration data * fixed failing test * meeting linter * fixed failing test for refresh status * added backpopulation of duration in the new encoded videos * fixed attribute errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed video id for backpopulation * reverted removing update_status * added exception handling for literal evaluation and added null check for messages in refactor * isolated back-population of new encode jobs * fixed boundary error * fixed more unbound variables errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent faa92aa commit d3e9031

File tree

6 files changed

+96
-19
lines changed

6 files changed

+96
-19
lines changed

cloudsync/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from mitol.transcoding.api import media_convert_job
2020

2121
from odl_video import logging
22+
from ui.api import get_duration_from_encode_job
2223
from ui.constants import VideoStatus
2324
from ui.encodings import EncodingNames
2425
from ui.models import (
@@ -78,6 +79,7 @@ def process_transcode_results(results: dict) -> None:
7879
elif "FILE_GROUP" in group_type:
7980
process_mp4_outputs(group.get("outputDetails", []), video)
8081

82+
video.duration = get_duration_from_encode_job(results)
8183
video.update_status(VideoStatus.COMPLETE)
8284

8385
# Ensure content_type and object_id are set for the EncodeJob

cloudsync/api_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_refresh_status_video_job_othererror(mocker, status):
139139
mocker.patch("cloudsync.api.boto3", MockBoto)
140140
error = Exception("unexpected exception")
141141
mocker.patch(
142-
"cloudsync.api.media_convert_job", return_value=MockClientMC(error=error)
142+
"cloudsync.api.get_media_convert_job", return_value=MockClientMC(error=error)
143143
)
144144
with pytest.raises(Exception):
145145
api.refresh_status(video)

ui/api.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,7 @@ def post_video_to_edx(video_files):
9393
for edx_endpoint in edx_endpoints:
9494
try:
9595
edx_endpoint.refresh_access_token()
96-
encode_job = (
97-
video_files[0]
98-
.video.encode_jobs.filter(state=models.EncodeJob.State.COMPLETED)
99-
.first()
100-
)
101-
duration = get_duration_from_encode_job(encode_job)
96+
duration = video_files[0].video.duration
10297
video_key = str(video_files[0].video.key)
10398
resp = requests.post(
10499
edx_endpoint.full_api_url,
@@ -163,11 +158,7 @@ def update_video_on_edx(video_key, encoded_videos=None):
163158
payload = {
164159
"edx_video_id": str(video.key),
165160
"client_video_id": video.title,
166-
"duration": get_duration_from_encode_job(
167-
video.encode_jobs.filter(
168-
state=models.EncodeJob.State.COMPLETED
169-
).first()
170-
),
161+
"duration": video.duration,
171162
"status": "updated",
172163
}
173164
if encoded_videos:
@@ -197,8 +188,8 @@ def get_duration_from_encode_job(encode_job):
197188
duration: float
198189
"""
199190
duration = 0.0
200-
if encode_job and encode_job.message:
201-
if output_groups := encode_job.message.get("outputGroupDetails", []):
191+
if encode_job:
192+
if output_groups := encode_job.get("outputGroupDetails", []):
202193
# Get the first output group
203194
output_group = output_groups[0]
204195
if outputs := output_group.get("outputDetails", []):

ui/api_test.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
from ui.factories import (
1919
CollectionEdxEndpointFactory,
2020
CollectionFactory,
21-
EncodeJobFactory,
22-
VideoFactory,
2321
VideoFileFactory,
2422
)
2523

@@ -378,9 +376,7 @@ def test_get_duration_from_encode_job():
378376
"""
379377
get_duration_from_encode_job should return duration from video's encode_jobs message body
380378
"""
381-
video = VideoFactory(status="Complete")
382-
encode_job = EncodeJobFactory(video=video)
383-
encode_job.message = {
379+
encode_job = {
384380
"id": "1711563064503-e5qdnh",
385381
"outputGroupDetails": [
386382
{

ui/migrations/0036_video_duration.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Generated by Django 4.2.21 on 2025-05-23 01:10
2+
3+
from ast import literal_eval
4+
from django.db import migrations, models, connection
5+
6+
7+
def migrate_video_duration(apps, schema_editor):
8+
"""
9+
Migrate video duration from the old encode job if data exists.
10+
"""
11+
12+
db_alias = schema_editor.connection.alias
13+
EncodeJob = apps.get_model("ui", "EncodeJob")
14+
Video = apps.get_model("ui", "Video")
15+
16+
if (
17+
"dj_elastictranscoder_encodejob"
18+
in schema_editor.connection.introspection.table_names()
19+
):
20+
with connection.cursor() as cursor:
21+
cursor.execute(
22+
"""
23+
SELECT e1.object_id, e1.message
24+
FROM dj_elastictranscoder_encodejob e1
25+
INNER JOIN (
26+
SELECT object_id, MAX(created_at) as max_created_at
27+
FROM dj_elastictranscoder_encodejob
28+
WHERE state = 4
29+
GROUP BY object_id
30+
) e2 ON e1.object_id = e2.object_id AND e1.created_at = e2.max_created_at
31+
WHERE e1.state = 4
32+
"""
33+
)
34+
rows = cursor.fetchall()
35+
36+
for row in rows:
37+
video_id, message = row
38+
try:
39+
# Attempt to parse the message as a literal
40+
message = literal_eval(message)
41+
except (ValueError, SyntaxError, TypeError):
42+
# If parsing fails, skip this row
43+
continue
44+
45+
if duration := (
46+
message
47+
and isinstance(message, dict)
48+
and message.get("Output", {}).get("Duration")
49+
):
50+
Video.objects.using(db_alias).filter(id=video_id).update(
51+
duration=duration
52+
)
53+
54+
encode_jobs = EncodeJob.objects.using(db_alias).filter(state=4)
55+
for job in encode_jobs:
56+
video_id = job.object_id
57+
if not video_id:
58+
continue
59+
# Fetch the video object
60+
video = Video.objects.using(db_alias).get(id=video_id)
61+
if video and (not video.duration):
62+
if output_groups := job.message.get("outputGroupDetails", []):
63+
# Get the first output group
64+
output_group = output_groups[0]
65+
if outputs := output_group.get("outputDetails", []):
66+
# Get the first output
67+
output = outputs[0]
68+
duration_in_ms = output.get("durationInMs", 0)
69+
# Convert milliseconds to seconds
70+
duration = duration_in_ms / 1000.0
71+
video.duration = duration
72+
video.save(update_fields=["duration"])
73+
74+
75+
class Migration(migrations.Migration):
76+
dependencies = [
77+
("ui", "0035_encodejob"),
78+
]
79+
80+
operations = [
81+
migrations.AddField(
82+
model_name="video",
83+
name="duration",
84+
field=models.FloatField(default=0.0, null=True),
85+
),
86+
migrations.RunPython(migrate_video_duration, migrations.RunPython.noop),
87+
]

ui/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ class Video(TimestampedModel):
351351
is_logged_in_only = models.BooleanField(null=False, default=False)
352352
custom_order = models.IntegerField(null=True, blank=True)
353353
schedule_retranscode = models.BooleanField(default=False)
354+
duration = models.FloatField(null=True, default=0.0)
354355

355356
objects = VideoManager()
356357

0 commit comments

Comments
 (0)