-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for standalone audio files and their metadata (#414)
- Loading branch information
Showing
6 changed files
with
125 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
Sources/Streamer/Parser/Audio/AudioPublicationManifestAugmentor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// | ||
// Copyright 2024 Readium Foundation. All rights reserved. | ||
// Use of this source code is governed by the BSD-style license | ||
// available in the top-level LICENSE file of the project. | ||
// | ||
|
||
import AVFoundation | ||
import Foundation | ||
import ReadiumShared | ||
import UIKit | ||
|
||
/// Implements a strategy to augment a `Manifest` of an audio publication with additional metadata and | ||
/// cover, for example by looking into the audio files metadata. | ||
public protocol AudioPublicationManifestAugmentor { | ||
func augment(_ baseManifest: Manifest, using fetcher: Fetcher) -> AudioPublicationAugmentedManifest | ||
} | ||
|
||
public struct AudioPublicationAugmentedManifest { | ||
var manifest: Manifest | ||
var cover: UIImage? | ||
} | ||
|
||
/// An `AudioPublicationManifestAugmentor` using AVFoundation to retrieve the audio metadata. | ||
/// | ||
/// It will only work for local publications (file://). | ||
public final class AVAudioPublicationManifestAugmentor: AudioPublicationManifestAugmentor { | ||
public init() {} | ||
|
||
public func augment(_ manifest: Manifest, using fetcher: Fetcher) -> AudioPublicationAugmentedManifest { | ||
let avAssets = manifest.readingOrder.map { link in | ||
fetcher.get(link).file | ||
.map { AVURLAsset(url: $0.url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) } | ||
} | ||
var manifest = manifest | ||
manifest.readingOrder = zip(manifest.readingOrder, avAssets).map { link, avAsset in | ||
guard let avAsset = avAsset else { return link } | ||
var link = link | ||
link.title = avAsset.metadata.filter([.commonIdentifierTitle]).first(where: { $0.stringValue }) | ||
link.duration = avAsset.duration.seconds | ||
return link | ||
} | ||
let avMetadata = avAssets.compactMap { $0?.metadata }.reduce([], +) | ||
var metadata = manifest.metadata | ||
metadata.localizedTitle = avMetadata.filter([.commonIdentifierTitle, .id3MetadataAlbumTitle]).first(where: { $0.stringValue })?.localizedString ?? manifest.metadata.localizedTitle | ||
metadata.localizedSubtitle = avMetadata.filter([.id3MetadataSubTitle, .iTunesMetadataTrackSubTitle]).first(where: { $0.stringValue })?.localizedString | ||
metadata.modified = avMetadata.filter([.commonIdentifierLastModifiedDate]).first(where: { $0.dateValue }) | ||
metadata.published = avMetadata.filter([.commonIdentifierCreationDate, .id3MetadataDate]).first(where: { $0.dateValue }) | ||
metadata.languages = avMetadata.filter([.commonIdentifierLanguage, .id3MetadataLanguage]).compactMap(\.stringValue).removingDuplicates() | ||
metadata.subjects = avMetadata.filter([.commonIdentifierSubject]).compactMap(\.stringValue).removingDuplicates().map { Subject(name: $0) } | ||
metadata.authors = avMetadata.filter([.commonIdentifierAuthor, .iTunesMetadataAuthor]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } | ||
metadata.artists = avMetadata.filter([.commonIdentifierArtist, .id3MetadataOriginalArtist, .iTunesMetadataArtist, .iTunesMetadataOriginalArtist]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } | ||
metadata.illustrators = avMetadata.filter([.iTunesMetadataAlbumArtist]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } | ||
metadata.contributors = avMetadata.filter([.commonIdentifierContributor]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } | ||
metadata.publishers = avMetadata.filter([.commonIdentifierPublisher, .id3MetadataPublisher, .iTunesMetadataPublisher]).compactMap(\.stringValue).removingDuplicates().map { Contributor(name: $0) } | ||
metadata.description = avMetadata.filter([.commonIdentifierDescription, .iTunesMetadataDescription]).first?.stringValue | ||
metadata.duration = avAssets.reduce(0) { duration, avAsset in | ||
guard let duration = duration, let avAsset = avAsset else { return nil } | ||
return duration + avAsset.duration.seconds | ||
} | ||
manifest.metadata = metadata | ||
let cover = avMetadata.filter([.commonIdentifierArtwork, .id3MetadataAttachedPicture, .iTunesMetadataCoverArt]).first(where: { $0.dataValue.flatMap(UIImage.init(data:)) }) | ||
return .init(manifest: manifest, cover: cover) | ||
} | ||
} | ||
|
||
private extension [AVMetadataItem] { | ||
func filter(_ identifiers: [AVMetadataIdentifier]) -> [AVMetadataItem] { | ||
identifiers.flatMap { AVMetadataItem.metadataItems(from: self, filteredByIdentifier: $0) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters