Skip to content

Commit

Permalink
Avoid crashes with media compositions missing a main chapter (#1038)
Browse files Browse the repository at this point in the history
  • Loading branch information
defagos authored Oct 4, 2024
1 parent 07ee4de commit 5a9e399
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Demo/Sources/Model/Media.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ struct Media: Hashable {
source: self,
trackerAdapters: [
DemoTracker.adapter { metadata in
DemoTracker.Metadata(title: metadata.mediaComposition.mainChapter.title)
DemoTracker.Metadata(title: metadata.mainChapter.title)
}
],
configuration: .init(position: at(startTime))
Expand Down
25 changes: 9 additions & 16 deletions Sources/CoreBusiness/Model/MediaComposition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,15 @@ public struct MediaComposition: Decodable {
enum CodingKeys: String, CodingKey {
case _analyticsData = "analyticsData"
case _analyticsMetadata = "analyticsMetadata"
case chapters = "chapterList"
case chapterUrn
case _chapters = "chapterList"
case episode
case show
}

/// The URN of the chapter to be played.
public let chapterUrn: String

/// The available chapters.
public var chapters: [Chapter] {
guard mainChapter.mediaType == .video else { return [] }
return _chapters.filter { $0.fullLengthUrn == chapterUrn && $0.mediaType == mainChapter.mediaType }
}

/// The related show.
public let show: Show?

Expand All @@ -40,22 +34,21 @@ public struct MediaComposition: Decodable {
_analyticsMetadata ?? [:]
}

var allChapters: [Chapter] {
[mainChapter] + chapters
}

// swiftlint:disable:next discouraged_optional_collection
private let _analyticsData: [String: String]?

// swiftlint:disable:next discouraged_optional_collection
private let _analyticsMetadata: [String: String]?

private let _chapters: [Chapter]
private let chapters: [Chapter]
}

public extension MediaComposition {
/// The main chapter.
var mainChapter: Chapter {
_chapters.first { $0.urn == chapterUrn }!
extension MediaComposition {
func chapters(relatedTo chapter: Chapter) -> [Chapter] {
chapters.filter { $0.fullLengthUrn == chapter.urn && $0.mediaType == chapter.mediaType }
}

func chapter(for urn: String) -> Chapter? {
chapters.first { $0.urn == urn }
}
}
46 changes: 26 additions & 20 deletions Sources/CoreBusiness/Model/MediaMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public struct MediaMetadata {
/// The URL at which the playback context was retrieved.
public let mediaCompositionUrl: URL?

/// The main chapter.
public let mainChapter: MediaComposition.Chapter

/// The resource to be played.
public let resource: MediaComposition.Resource

Expand All @@ -34,9 +37,22 @@ public struct MediaMetadata {
resource.streamType
}

/// The available chapters.
public var chapters: [Chapter] {
guard mainChapter.mediaType == .video else { return [] }
return mediaComposition.chapters(relatedTo: mainChapter).map { chapter in
.init(
identifier: chapter.urn,
title: chapter.title,
imageSource: .url(imageUrl(for: chapter)),
timeRange: chapter.timeRange
)
}
}

/// The consolidated comScore analytics data.
var analyticsData: [String: String] {
var analyticsData = mediaComposition.mainChapter.analyticsData
var analyticsData = mainChapter.analyticsData
guard !analyticsData.isEmpty else { return [:] }
analyticsData.merge(mediaComposition.analyticsData) { _, new in new }
analyticsData.merge(resource.analyticsData) { _, new in new }
Expand All @@ -45,7 +61,7 @@ public struct MediaMetadata {

/// The consolidated Commanders Act analytics data.
var analyticsMetadata: [String: String] {
var analyticsMetadata = mediaComposition.mainChapter.analyticsMetadata
var analyticsMetadata = mainChapter.analyticsMetadata
guard !analyticsMetadata.isEmpty else { return [:] }
analyticsMetadata.merge(mediaComposition.analyticsMetadata) { _, new in new }
analyticsMetadata.merge(resource.analyticsMetadata) { _, new in new }
Expand All @@ -54,7 +70,9 @@ public struct MediaMetadata {

init(mediaCompositionResponse: MediaCompositionResponse, dataProvider: DataProvider) throws {
let mediaComposition = mediaCompositionResponse.mediaComposition
let mainChapter = mediaComposition.mainChapter
guard let mainChapter = mediaComposition.chapter(for: mediaComposition.chapterUrn) else {
throw DataError.noResourceAvailable
}
if let blockingReason = mainChapter.blockingReason {
throw DataError.blocked(withMessage: blockingReason.description)
}
Expand All @@ -63,6 +81,7 @@ public struct MediaMetadata {
}
self.mediaComposition = mediaComposition
self.mediaCompositionUrl = mediaCompositionResponse.response.url
self.mainChapter = mainChapter
self.resource = resource
self.dataProvider = dataProvider
}
Expand All @@ -79,7 +98,7 @@ extension MediaMetadata: AssetMetadata {
title: title,
subtitle: subtitle,
description: description,
imageSource: .url(imageUrl(for: mediaComposition.mainChapter)),
imageSource: .url(imageUrl(for: mainChapter)),
viewport: viewport,
episodeInformation: episodeInformation,
chapters: chapters,
Expand All @@ -88,7 +107,6 @@ extension MediaMetadata: AssetMetadata {
}

var title: String {
let mainChapter = mediaComposition.mainChapter
guard mainChapter.contentType != .livestream else { return mainChapter.title }
if let show = mediaComposition.show {
return show.title
Expand All @@ -99,7 +117,6 @@ extension MediaMetadata: AssetMetadata {
}

var subtitle: String? {
let mainChapter = mediaComposition.mainChapter
guard mainChapter.contentType != .livestream else { return nil }
if let show = mediaComposition.show {
if Self.areRedundant(chapter: mainChapter, show: show) {
Expand All @@ -115,7 +132,7 @@ extension MediaMetadata: AssetMetadata {
}

var description: String? {
mediaComposition.mainChapter.description
mainChapter.description
}

var episodeInformation: EpisodeInformation? {
Expand All @@ -137,31 +154,20 @@ extension MediaMetadata: AssetMetadata {
}
}

private var chapters: [Chapter] {
mediaComposition.chapters.map { chapter in
.init(
identifier: chapter.urn,
title: chapter.title,
imageSource: .url(imageUrl(for: chapter)),
timeRange: chapter.timeRange
)
}
}

private var timeRanges: [TimeRange] {
blockedTimeRanges + creditsTimeRanges
}

private var blockedTimeRanges: [TimeRange] {
mediaComposition.mainChapter.segments
mainChapter.segments
.filter { $0.blockingReason != nil }
.map { segment in
TimeRange(kind: .blocked, start: segment.timeRange.start, end: segment.timeRange.end)
}
}

private var creditsTimeRanges: [TimeRange] {
mediaComposition.mainChapter.timeIntervals.map { interval in
mainChapter.timeIntervals.map { interval in
switch interval.kind {
case .openingCredits:
TimeRange(kind: .credits(.opening), start: interval.timeRange.start, end: interval.timeRange.end)
Expand Down
27 changes: 0 additions & 27 deletions Tests/CoreBusinessTests/MediaCompositionTests.swift

This file was deleted.

15 changes: 15 additions & 0 deletions Tests/CoreBusinessTests/MediaMetadataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ final class MediaMetadataTests: XCTestCase {
expect(metadata.episodeInformation).to(beNil())
}

func testMainChapter() throws {
let metadata = try Self.metadata(.onDemand)
expect(metadata.mainChapter.urn).to(equal(metadata.mediaComposition.chapterUrn))
}

func testChapters() throws {
let metadata = try Self.metadata(.mixed)
expect(metadata.chapters.count).to(equal(10))
}

func testAudioChapterRemoval() throws {
let metadata = try Self.metadata(.audioChapters)
expect(metadata.chapters).to(beEmpty())
}

func testAnalytics() throws {
let metadata = try Self.metadata(.onDemand)
expect(metadata.analyticsData).notTo(beEmpty())
Expand Down

0 comments on commit 5a9e399

Please sign in to comment.