Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 0.65.3
--------------

- show resource_category in channel pages (#3264)
- use youtube etl for playlists and ocw for videos (#3257)
- Vector search pagination fix for empty hybrid searches (#3251)

Version 0.65.2 (Released April 30, 2026)
--------------

Expand Down
59 changes: 59 additions & 0 deletions frontends/main/src/app-pages/ChannelPage/ChannelSearch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,63 @@ describe("ChannelSearch", () => {
await user.click(screen.getByRole("button", { name: "Search" }))
expect(location.current.searchParams.get("q")).toBe("woof")
})

test.each([
{ channelType: ChannelTypeEnum.Topic },
{ channelType: ChannelTypeEnum.Department },
{ channelType: ChannelTypeEnum.Unit },
])(
"Shows Resource Category facet only when resource_type_group=learning_material ($channelType)",
async ({ channelType }) => {
const { channel } = setMockApiResponses({
channelPatch: { channel_type: channelType },
search: {
count: 700,
metadata: {
aggregations: {
resource_type_group: [
{ key: "course", doc_count: 100 },
{ key: "learning_material", doc_count: 200 },
],
resource_category: [
{ key: "Course", doc_count: 100 },
{ key: "Video", doc_count: 100 },
],
topic: [{ key: "physics", doc_count: 100 }],
department: [{ key: "1", doc_count: 100 }],
certification_type: [{ key: "micromasters", doc_count: 100 }],
delivery: [{ key: "online", doc_count: 100 }],
offered_by: [{ key: "ocw", doc_count: 100 }],
},
suggestions: [],
},
},
})

const {
view: { unmount },
} = renderWithProviders(<ChannelPage />, {
url: `/c/${channel.channel_type}/${channel.name}/`,
})

const facetsContainer = await screen.findByTestId("facets-container")
await within(facetsContainer).findByText("Certificate")
expect(
within(facetsContainer).queryByText("Resource Category"),
).toBeNull()

unmount()

// Re-render with resource_type_group=learning_material
renderWithProviders(<ChannelPage />, {
url: `/c/${channel.channel_type}/${channel.name}/?resource_type_group=learning_material`,
})

expect(
await within(await screen.findByTestId("facets-container")).findByText(
"Resource Category",
),
).toBeInTheDocument()
},
)
})
3 changes: 3 additions & 0 deletions frontends/main/src/app-pages/ChannelPage/searchRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const getConstantSearchParams = (searchFilter?: string) => {
const FACETS_BY_CHANNEL_TYPE: Record<ChannelTypeEnum, string[]> = {
[ChannelTypeEnum.Topic]: [
"free",
"resource_category",
"resource_type",
"certification_type",
"delivery",
Expand All @@ -38,6 +39,7 @@ const FACETS_BY_CHANNEL_TYPE: Record<ChannelTypeEnum, string[]> = {
],
[ChannelTypeEnum.Department]: [
"free",
"resource_category",
"resource_type",
"certification_type",
"topic",
Expand All @@ -46,6 +48,7 @@ const FACETS_BY_CHANNEL_TYPE: Record<ChannelTypeEnum, string[]> = {
],
[ChannelTypeEnum.Unit]: [
"free",
"resource_category",
"resource_type",
"topic",
"certification_type",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ const FeaturedVideo: React.FC<FeaturedVideoProps> = ({
totalVideos,
totalTime,
}) => {
const imageUrl = video.image?.url ?? null
const imageUrl =
video.image?.url ?? video.content_files?.[0]?.image_src ?? null

const duration = video.video?.duration
? formatDurationClockTime(video.video.duration)
: null
Expand Down Expand Up @@ -231,7 +233,9 @@ const FeaturedVideo: React.FC<FeaturedVideoProps> = ({
</Link>
</FeaturedTitle>
{description && (
<FeaturedDescription>{description}</FeaturedDescription>
<FeaturedDescription
dangerouslySetInnerHTML={{ __html: description }}
/>
)}
</TextSide>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,12 @@ export const EpisodeItem: React.FC<EpisodeItemProps> = ({
{episode.title}
</EpisodeTitleLink>

<EpisodeDescription variant="body2">
{episode.description}
</EpisodeDescription>
<EpisodeDescription
variant="body2"
dangerouslySetInnerHTML={{
__html: episode.description ?? "",
}}
/>
</EpisodeInfo>

<EpisodeRight>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,11 @@ type VideoCardProps = {

const VideoCard: React.FC<VideoCardProps> = ({ resource, href }) => {
const [imgError, setImgError] = useState(false)
const imageUrl =
!imgError && resource.image?.url ? resource.image.url : PLACEHOLDER_IMG
const imageUrl = !imgError
? (resource?.image?.url ??
resource.content_files?.[0]?.image_src ??
PLACEHOLDER_IMG)
: PLACEHOLDER_IMG
const description = resource.description ?? ""
const duration = resource.video?.duration
? formatDurationClockTime(resource.video.duration)
Expand Down Expand Up @@ -171,7 +174,7 @@ const VideoCard: React.FC<VideoCardProps> = ({ resource, href }) => {
</CardTitleRow>
<CardMetaRow>
<CardMetaGroup>
<CardMetaValue>{description}</CardMetaValue>
<CardMetaValue dangerouslySetInnerHTML={{ __html: description }} />
</CardMetaGroup>
</CardMetaRow>
</CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,13 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({

const sources = useMemo(
() =>
video ? resolveVideoSources(video.video?.streaming_url, video.url) : [],
video
? resolveVideoSources(
video.video?.streaming_url,
video.url,
video.content_files?.[0]?.youtube_id,
)
: [],
[video],
)

Expand Down Expand Up @@ -523,6 +529,7 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
sources[0]?.type === "video/youtube"
? undefined
: (video?.video?.cover_image_url ??
video?.content_files?.[0]?.image_src ??
video?.image?.url ??
undefined)
}
Expand All @@ -532,10 +539,12 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
ariaLabel={`Video: ${videoTitleLabel}`}
ariaDescribedBy="video-description"
/>
) : video?.image?.url ? (
) : video?.image?.url || video?.content_files?.[0]?.image_src ? (
<ThumbnailWrapper>
<Image
src={video.image.url}
src={
(video?.image?.url ?? video?.content_files?.[0]?.image_src)!
}
alt={videoThumbnailAlt}
fill
sizes="100vw"
Expand All @@ -556,9 +565,10 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
<BorderLine />

{!isLoading && video?.description && (
<DescriptionText id="video-description">
{video?.description}
</DescriptionText>
<DescriptionText
id="video-description"
dangerouslySetInnerHTML={{ __html: video.description }}
/>
)}

{!isLoading && !video?.description && (
Expand Down Expand Up @@ -627,7 +637,10 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
const itemDuration = item.video?.duration
? formatDurationClockTime(item.video.duration)
: null
const imageUrl = item.image?.url ?? null
const imageUrl =
item.image?.url ??
item.content_files?.[0]?.image_src ??
null
const itemTopicNames = (item.topics ?? [])
.map((topic) => topic.name)
.filter(Boolean)
Expand Down Expand Up @@ -660,9 +673,11 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
{item.title}
</MoreFromItemTitle>
{item.description && (
<MoreFromItemMeta>
{item.description}
</MoreFromItemMeta>
<MoreFromItemMeta
dangerouslySetInnerHTML={{
__html: item.description,
}}
/>
)}
</MoreFromTextSide>
</MoreFromItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ const VideoSeriesDetailPage: React.FC<VideoSeriesDetailPageProps> = ({

const sources = useMemo(
() =>
video ? resolveVideoSources(video.video?.streaming_url, video.url) : [],
video
? resolveVideoSources(
video.video?.streaming_url,
video.url,
video.content_files?.[0]?.youtube_id,
)
: [],
[video],
)

Expand Down Expand Up @@ -274,9 +280,10 @@ const VideoSeriesDetailPage: React.FC<VideoSeriesDetailPageProps> = ({

{/* Description */}
{!isLoading && video?.description && (
<Styled.DescriptionText id="video-description">
{video.description}
</Styled.DescriptionText>
<Styled.DescriptionText
id="video-description"
dangerouslySetInnerHTML={{ __html: video.description }}
/>
)}

{!isLoading && !video?.description && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe("resolveVideoSources", () => {
const result = resolveVideoSources(
"https://cdn.example.com/video/index.m3u8",
null,
null,
)
expect(result).toEqual([
{
Expand All @@ -18,6 +19,7 @@ describe("resolveVideoSources", () => {
const result = resolveVideoSources(
"https://cdn.example.com/video/manifest.mpd",
null,
null,
)
expect(result).toEqual([
{
Expand All @@ -31,6 +33,7 @@ describe("resolveVideoSources", () => {
const result = resolveVideoSources(
"https://cdn.example.com/video/clip.mp4",
null,
null,
)
expect(result).toEqual([
{ src: "https://cdn.example.com/video/clip.mp4", type: "video/mp4" },
Expand All @@ -41,6 +44,7 @@ describe("resolveVideoSources", () => {
const result = resolveVideoSources(
"https://cdn.example.com/video/stream",
"https://www.youtube.com/watch?v=abc",
null,
)
// streamingUrl takes precedence over pageUrl
expect(result).toEqual([
Expand All @@ -52,30 +56,55 @@ describe("resolveVideoSources", () => {
const result = resolveVideoSources(
null,
"https://www.youtube.com/watch?v=abc123",
null,
)
expect(result).toEqual([
{ src: "https://www.youtube.com/watch?v=abc123", type: "video/youtube" },
])
})

it("returns YouTube source when pageUrl is a youtu.be short link", () => {
const result = resolveVideoSources(null, "https://youtu.be/abc123")
const result = resolveVideoSources(null, "https://youtu.be/abc123", null)
expect(result).toEqual([
{ src: "https://youtu.be/abc123", type: "video/youtube" },
])
})

it("returns an empty array when both arguments are null", () => {
expect(resolveVideoSources(null, null)).toEqual([])
it("returns YouTube source from youtubeId when no streaming URL", () => {
const result = resolveVideoSources(null, null, "dQw4w9WgXcQ")
expect(result).toEqual([
{
src: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
type: "video/youtube",
},
])
})

it("returns an empty array when both arguments are undefined", () => {
expect(resolveVideoSources(undefined, undefined)).toEqual([])
it("prefers youtubeId over a non-YouTube pageUrl", () => {
const result = resolveVideoSources(
null,
"https://ocw.mit.edu/some-video",
"dQw4w9WgXcQ",
)
expect(result).toEqual([
{
src: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
type: "video/youtube",
},
])
})

it("returns an empty array when there is no streaming URL and pageUrl is not a YouTube link", () => {
expect(resolveVideoSources(null, "https://ocw.mit.edu/some-video")).toEqual(
[],
)
it("returns an empty array when all arguments are null", () => {
expect(resolveVideoSources(null, null, null)).toEqual([])
})

it("returns an empty array when all arguments are undefined", () => {
expect(resolveVideoSources(undefined, undefined, undefined)).toEqual([])
})

it("returns an empty array when there is no streaming URL, no youtubeId, and pageUrl is not a YouTube link", () => {
expect(
resolveVideoSources(null, "https://ocw.mit.edu/some-video", null),
).toEqual([])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { VideoJsSource } from "./VideoJsPlayer"
export function resolveVideoSources(
streamingUrl: string | null | undefined,
pageUrl: string | null | undefined,
youtubeId: string | null | undefined,
): VideoJsSource[] {
if (streamingUrl) {
// HLS
Expand All @@ -24,6 +25,15 @@ export function resolveVideoSources(
return [{ src: streamingUrl, type: "video/mp4" }]
}

if (youtubeId) {
return [
{
src: `https://www.youtube.com/watch?v=${youtubeId}`,
type: "video/youtube",
},
]
}

if (pageUrl && /youtube\.com|youtu\.be/.test(pageUrl)) {
return [{ src: pageUrl, type: "video/youtube" }]
}
Expand Down
Loading
Loading