|
| 1 | +// |
| 2 | +// Copyright 2024 Readium Foundation. All rights reserved. |
| 3 | +// Use of this source code is governed by the BSD-style license |
| 4 | +// available in the top-level LICENSE file of the project. |
| 5 | +// |
| 6 | + |
| 7 | +import AVFoundation |
| 8 | +import Foundation |
| 9 | +import ReadiumShared |
| 10 | +import UIKit |
| 11 | + |
| 12 | +/// Implements a strategy to augment a `Manifest` of an audio publication with additional metadata and |
| 13 | +/// cover, for example by looking into the audio files metadata. |
| 14 | +public protocol AudioPublicationManifestAugmentor { |
| 15 | + func augment(_ baseManifest: Manifest, using fetcher: Fetcher) -> AudioPublicationAugmentedManifest |
| 16 | +} |
| 17 | + |
| 18 | +public struct AudioPublicationAugmentedManifest { |
| 19 | + var manifest: Manifest |
| 20 | + var cover: UIImage? |
| 21 | +} |
| 22 | + |
| 23 | +/// An `AudioPublicationManifestAugmentor` using AVFoundation to retrieve the audio metadata. |
| 24 | +/// |
| 25 | +/// It will only work for local publications (file://). |
| 26 | +public final class AVAudioPublicationManifestAugmentor: AudioPublicationManifestAugmentor { |
| 27 | + public init() {} |
| 28 | + |
| 29 | + public func augment(_ manifest: Manifest, using fetcher: Fetcher) -> AudioPublicationAugmentedManifest { |
| 30 | + let avAssets = manifest.readingOrder.map { link in |
| 31 | + fetcher.get(link).file |
| 32 | + .map { AVURLAsset(url: $0.url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) } |
| 33 | + } |
| 34 | + var manifest = manifest |
| 35 | + manifest.readingOrder = zip(manifest.readingOrder, avAssets).map { link, avAsset in |
| 36 | + guard let avAsset = avAsset else { return link } |
| 37 | + var link = link |
| 38 | + link.title = avAsset.metadata.filter([.commonIdentifierTitle]).first(where: { $0.stringValue }) |
| 39 | + link.duration = avAsset.duration.seconds |
| 40 | + return link |
| 41 | + } |
| 42 | + let avMetadata = avAssets.compactMap { $0?.metadata }.reduce([], +) |
| 43 | + var metadata = manifest.metadata |
| 44 | + metadata.localizedTitle = avMetadata.filter([.commonIdentifierTitle, .id3MetadataAlbumTitle]).first(where: { $0.stringValue })?.localizedString ?? manifest.metadata.localizedTitle |
| 45 | + metadata.localizedSubtitle = avMetadata.filter([.id3MetadataSubTitle, .iTunesMetadataTrackSubTitle]).first(where: { $0.stringValue })?.localizedString |
| 46 | + metadata.modified = avMetadata.filter([.commonIdentifierLastModifiedDate]).first(where: { $0.dateValue }) |
| 47 | + metadata.published = avMetadata.filter([.commonIdentifierCreationDate, .id3MetadataDate]).first(where: { $0.dateValue }) |
| 48 | + metadata.languages = avMetadata.filter([.commonIdentifierLanguage, .id3MetadataLanguage]).compactMap(\.stringValue).removingDuplicates() |
| 49 | + metadata.subjects = avMetadata.filter([.commonIdentifierSubject]).compactMap(\.stringValue).removingDuplicates().map { Subject(name: $0) } |
| 50 | + metadata.authors = avMetadata.filter([.commonIdentifierAuthor, .iTunesMetadataAuthor]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } |
| 51 | + metadata.artists = avMetadata.filter([.commonIdentifierArtist, .id3MetadataOriginalArtist, .iTunesMetadataArtist, .iTunesMetadataOriginalArtist]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } |
| 52 | + metadata.illustrators = avMetadata.filter([.iTunesMetadataAlbumArtist]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } |
| 53 | + metadata.contributors = avMetadata.filter([.commonIdentifierContributor]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } |
| 54 | + metadata.publishers = avMetadata.filter([.commonIdentifierPublisher, .id3MetadataPublisher, .iTunesMetadataPublisher]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } |
| 55 | + metadata.description = avMetadata.filter([.commonIdentifierDescription, .iTunesMetadataDescription]).first?.stringValue |
| 56 | + metadata.duration = avAssets.reduce(0) { duration, avAsset in |
| 57 | + guard let duration = duration, let avAsset = avAsset else { return nil } |
| 58 | + return duration + avAsset.duration.seconds |
| 59 | + } |
| 60 | + manifest.metadata = metadata |
| 61 | + let cover = avMetadata.filter([.commonIdentifierArtwork, .id3MetadataAttachedPicture, .iTunesMetadataCoverArt]).first(where: { $0.dataValue.flatMap(UIImage.init(data:)) }) |
| 62 | + return .init(manifest: manifest, cover: cover) |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +private extension [AVMetadataItem] { |
| 67 | + func filter(_ identifiers: [AVMetadataIdentifier]) -> [AVMetadataItem] { |
| 68 | + identifiers.flatMap { AVMetadataItem.metadataItems(from: self, filteredByIdentifier: $0) } |
| 69 | + } |
| 70 | +} |
0 commit comments