Skip to content

OVS Video resources#3042

Open
mbertrand wants to merge 4 commits intomainfrom
mb/ovs_video_resources
Open

OVS Video resources#3042
mbertrand wants to merge 4 commits intomainfrom
mb/ovs_video_resources

Conversation

@mbertrand
Copy link
Member

@mbertrand mbertrand commented Mar 11, 2026

What are the relevant tickets?

Closes https://github.com/mitodl/hq/issues/10396

Description (What does it do?)

  • Adds an ETL pipeline to ingest videos and playlists from ODL Video Service.

Screenshots (if appropriate):

  • Desktop screenshots
  • Mobile width screenshots

How can this be tested?

@github-actions
Copy link

github-actions bot commented Mar 11, 2026

OpenAPI Changes

Show/hide 63 changes: 0 error, 1 warning, 62 info
63 changes: 0 error, 1 warning, 62 info
warning	[response-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/delete/
		added the new 'ovs' enum value to the 'source' response property for the response status '200'
		Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v0/vector_learning_resources_search/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v0/vector_learning_resources_search/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/featured/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/featured/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/featured/{id}/
		added the required property '/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/featured/{id}/
		added the required property '/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/
		added the required property '/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/
		added the required property '/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learning_resources/{id}/learning_paths/
		added the required property '/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learning_resources/{id}/learning_paths/
		added the required property '/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/similar/
		added the required property '/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/similar/
		added the required property '/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learning_resources/{id}/userlists/
		added the required property '/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learning_resources/{id}/userlists/
		added the required property '/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/vector_similar/
		added the required property '/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{id}/vector_similar/
		added the required property '/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources_search/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources_search/
		added the required property 'results/items/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/learning_resources_user_subscription/subscribe/
		added the new 'ovs' enum value to the request property 'platform/items/' (media type: application/x-www-form-urlencoded)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/learning_resources_user_subscription/subscribe/
		added the new 'ovs' enum value to the request property 'platform/items/' (media type: multipart/form-data)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/learning_resources_user_subscription/subscribe/
		added the new 'ovs' enum value to the request property 'platform/items/' (media type: application/json)

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learningpaths/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learningpaths/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/learningpaths/{learning_resource_id}/items/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '201' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/learningpaths/{learning_resource_id}/items/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '201' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learningpaths/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learningpaths/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learningpaths/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/learningpaths/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/podcasts/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/podcasts/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/podcasts/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/podcasts/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/userlists/{userlist_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/userlists/{userlist_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/userlists/{userlist_id}/items/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '201' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/userlists/{userlist_id}/items/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '201' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/userlists/{userlist_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/userlists/{userlist_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/userlists/{userlist_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API PATCH /api/v1/userlists/{userlist_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/video_playlists/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/video_playlists/{learning_resource_id}/items/
		added the required property 'results/items/resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/video_playlists/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/video_playlists/{learning_resource_id}/items/{id}/
		added the required property 'resource/allOf[#/components/schemas/LearningResource]/oneOf[#/components/schemas/VideoResource]/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/videos/
		added the required property 'results/items/video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/videos/
		added the required property 'results/items/video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/videos/{id}/
		added the required property 'video/allOf[#/components/schemas/Video]/caption_urls' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/videos/{id}/
		added the required property 'video/allOf[#/components/schemas/Video]/cover_image_url' to the response with the '200' status

info	[request-parameter-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/
		added the new enum value 'ovs' to the 'query' request parameter 'source'

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/
		added the new 'ovs' enum value to the request property 'source' (media type: multipart/form-data)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/
		added the new 'ovs' enum value to the request property 'source' (media type: application/x-www-form-urlencoded)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/
		added the new 'ovs' enum value to the request property 'source' (media type: application/json)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/delete/
		added the new 'ovs' enum value to the request property 'source' (media type: application/json)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/delete/
		added the new 'ovs' enum value to the request property 'source' (media type: application/x-www-form-urlencoded)

info	[request-property-enum-value-added] at head/openapi/specs/v1.yaml	
	in API POST /api/v1/webhooks/content_files/delete/
		added the new 'ovs' enum value to the request property 'source' (media type: multipart/form-data)


Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Copilot AI review requested due to automatic review settings March 19, 2026 17:32
@mbertrand mbertrand force-pushed the mb/ovs_video_resources branch from f1b6925 to 97a325f Compare March 19, 2026 17:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for ingesting ODL Video Service (OVS) videos/collections into the learning resources system, including scheduled ETL runs and transcript backpopulation, and exposes the new platform + video fields via the API/OpenAPI.

Changes:

  • Introduces an OVS ETL (extract/transform/load) that groups videos into playlists (collections) and loads them into LearningResource/Video/VideoPlaylist.
  • Adds transcript-fetching job for OVS videos based on caption URLs, plus Celery beat schedules and a management command to backpopulate/delete.
  • Extends the data model + OpenAPI + generated frontend API types to include ovs platform and Video.caption_urls / Video.cover_image_url.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test_json/ovs_videos.json Adds a fixture-style OVS API response used by OVS ETL tests.
openapi/specs/v1.yaml Adds ovs to platform enums and adds caption_urls/cover_image_url to Video schemas.
openapi/specs/v0.yaml Same as v1 for the v0 spec.
main/settings_course_etl.py Adds OVS_API_BASE_URL setting used by the OVS extractor.
main/settings_celery.py Schedules periodic OVS video ingestion and transcript update tasks.
learning_resources/tasks_test.py Adds task-level tests for invoking the new OVS ETL/transcript functions.
learning_resources/tasks.py Adds Celery tasks get_ovs_data and get_ovs_transcripts.
learning_resources/models.py Adds caption_urls and cover_image_url fields to the Video detail model.
learning_resources/migrations/0106_video_caption_url_video_cover_image_url.py Adds DB fields for Video.caption_urls and Video.cover_image_url.
learning_resources/migrations/0106_resource_category_choices_index.py Removes a prior 0106 migration (replaced in this PR).
learning_resources/management/commands/backpopulate_ovs_data.py Adds a management command to fetch/delete OVS data and (optionally) transcripts.
learning_resources/etl/pipelines.py Wires a new ovs_etl pipeline into the ETL pipeline registry.
learning_resources/etl/ovs_test.py Adds comprehensive unit tests for OVS extract/transform and transcript fetching.
learning_resources/etl/ovs.py Implements OVS extraction (pagination), transform (collection grouping), and transcript fetching.
learning_resources/etl/loaders_test.py Adds loader tests for OVS playlist loading/unpublishing behavior; updates playlist unpublish assertions.
learning_resources/etl/loaders.py Adds load_ovs_playlist/load_ovs_playlists; adjusts playlist image handling and bulk-unpublish IDs.
learning_resources/etl/constants.py Adds ovs to ETLSource enum.
learning_resources/constants.py Adds ovs to PlatformType enum.
frontends/api/src/generated/v1/api.ts Regenerates TS client types/enums to include ovs + the new video fields.
frontends/api/src/generated/v0/api.ts Same as v1 for the v0 generated client.
data_fixtures/migrations/0024_add_ovs_platform_offeror.py Adds a data migration to create the ovs platform record.
data_fixtures/fixtures/platforms.json Adds ovs platform entry to fixture data.

You can also share your feedback on Copilot code review. Take the survey.

@mbertrand mbertrand added Needs Review An open Pull Request that is ready for review and removed Work in Progress labels Mar 19, 2026
@shanbady shanbady self-requested a review March 19, 2026 18:28
Copy link
Contributor

@shanbady shanbady left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. just left a question



class NullableURLField(serializers.URLField):
"""URLField that returns None for empty strings"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why this was necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compromise between precommit insisting this field should be blankable but not nullable, and copilot review saying it should be null instead of a blank string.

@shanbady shanbady added Waiting on author and removed Needs Review An open Pull Request that is ready for review labels Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants