From 5a9e399451148740ad399e1a780df1e1f705a170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Fri, 4 Oct 2024 16:01:38 +0200 Subject: [PATCH] Avoid crashes with media compositions missing a main chapter (#1038) --- Demo/Sources/Model/Media.swift | 2 +- .../CoreBusiness/Model/MediaComposition.swift | 25 ++++------ .../CoreBusiness/Model/MediaMetadata.swift | 46 +++++++++++-------- .../MediaCompositionTests.swift | 27 ----------- .../MediaMetadataTests.swift | 15 ++++++ 5 files changed, 51 insertions(+), 64 deletions(-) delete mode 100644 Tests/CoreBusinessTests/MediaCompositionTests.swift diff --git a/Demo/Sources/Model/Media.swift b/Demo/Sources/Model/Media.swift index 28bbd223e..b10db22b0 100644 --- a/Demo/Sources/Model/Media.swift +++ b/Demo/Sources/Model/Media.swift @@ -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)) diff --git a/Sources/CoreBusiness/Model/MediaComposition.swift b/Sources/CoreBusiness/Model/MediaComposition.swift index 2a4d5cf0c..e8ed1645c 100644 --- a/Sources/CoreBusiness/Model/MediaComposition.swift +++ b/Sources/CoreBusiness/Model/MediaComposition.swift @@ -9,8 +9,8 @@ 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 } @@ -18,12 +18,6 @@ public struct MediaComposition: Decodable { /// 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? @@ -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 } } } diff --git a/Sources/CoreBusiness/Model/MediaMetadata.swift b/Sources/CoreBusiness/Model/MediaMetadata.swift index e2d0e868f..4faa7b6fd 100644 --- a/Sources/CoreBusiness/Model/MediaMetadata.swift +++ b/Sources/CoreBusiness/Model/MediaMetadata.swift @@ -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 @@ -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 } @@ -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 } @@ -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) } @@ -63,6 +81,7 @@ public struct MediaMetadata { } self.mediaComposition = mediaComposition self.mediaCompositionUrl = mediaCompositionResponse.response.url + self.mainChapter = mainChapter self.resource = resource self.dataProvider = dataProvider } @@ -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, @@ -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 @@ -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) { @@ -115,7 +132,7 @@ extension MediaMetadata: AssetMetadata { } var description: String? { - mediaComposition.mainChapter.description + mainChapter.description } var episodeInformation: EpisodeInformation? { @@ -137,23 +154,12 @@ 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) @@ -161,7 +167,7 @@ extension MediaMetadata: AssetMetadata { } 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) diff --git a/Tests/CoreBusinessTests/MediaCompositionTests.swift b/Tests/CoreBusinessTests/MediaCompositionTests.swift deleted file mode 100644 index 782eb8594..000000000 --- a/Tests/CoreBusinessTests/MediaCompositionTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Nimble -import XCTest - -final class MediaCompositionTests: XCTestCase { - func testMainChapter() { - let mediaComposition = Mock.mediaComposition() - expect(mediaComposition.mainChapter.urn).to(equal(mediaComposition.chapterUrn)) - } - - func testChapters() { - let mediaComposition = Mock.mediaComposition(.mixed) - expect(mediaComposition.chapters.count).to(equal(10)) - } - - func testAudioChapterRemoval() { - let mediaComposition = Mock.mediaComposition(.audioChapters) - expect(mediaComposition.chapters).to(beEmpty()) - } -} diff --git a/Tests/CoreBusinessTests/MediaMetadataTests.swift b/Tests/CoreBusinessTests/MediaMetadataTests.swift index 906b3edfd..e5a87fa2e 100644 --- a/Tests/CoreBusinessTests/MediaMetadataTests.swift +++ b/Tests/CoreBusinessTests/MediaMetadataTests.swift @@ -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())