External Audio Provider
External audio provider#
External APIs (audio search engines) can be used as a data source for custom audio clip categories.
The provider object must conform to AudioProvider, by implementing a general trending endpoint for the initial load, a search endpoint for queries as well as a get endpoint for retrieving individual songs.
Pagination is handled via the query, offset, and AudioProviderResult.hasMore parameters.
If your service does not support pagination, you can ignore the query, offset arguments and pass false as AudioProviderResult.hasMore.
class ExternalAudioProvider: AudioProvider {func trending(offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {// Load audio clips from local/remote resource and call completion handler}func search(query: String, offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {// Load audio clips from local/remote resource and call completion handler}func get(identifier: String, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {// Load an audio clip from local/remote resource and call completion handler}}
A custom category can be added to the existing categories in a familiar way. Instead of passing an audioClips array, you should provide an audioProvider object.
let previewURL = Bundle.main.url(forResource: "external_provider", withExtension: "png")!let customAudioCategory = AudioProviderCategory(title: "ExternalProvider", imageURL: previewURL, audioProvider: ExternalAudioProvider())let configuration = Configuration { builder inlet assetCatalog = AssetCatalog.defaultItemsassetCatalog.audioClips.append(customAudioCategory)builder.assetCatalog = assetCatalog}
Example audio provider#
The example below shows an implementation of an AudioProvider for an arbitrary web service.
class WebServiceAudioProvider: AudioProvider {private static let baseURL = "https://web.service/api/audio"private enum Parameters: String {case query = "q"}func trending(offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {let task = URLSession.shared.dataTask(with: request(path: "trending", query: nil)) { data, response, error inself.parse(data: data, response: response, error: error, completion: completion)}task.resume()}func search(query: String, offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {let task = URLSession.shared.dataTask(with: request(path: "search", query: nil)) { data, response, error inself.parse(data: data, response: response, error: error, completion: completion)}task.resume()}func get(identifier: String, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {let task = URLSession.shared.dataTask(with: requestSong(identifier: identifier)) { data, response, error inself.parseSong(data: data, response: response, error: error, completion: completion)}task.resume()}private func request(path: String, query: String?) -> URLRequest {var urlComponents = URLComponents(string: WebServiceAudioProvider.baseURL)!if let query = query {urlComponents.queryItems?.append(URLQueryItem(name: Parameters.query.rawValue, value: query))}let request = URLRequest(url: urlComponents.url!.appendingPathComponent(path))return request}private func requestSong(identifier: String) -> URLRequest {let url = URL(string: WebServiceAudioProvider.baseURL)!.appendingPathComponent(identifier)let request = URLRequest(url: url)return request}private func parse(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {if let error = error {completion(.failure(error))return}guard let data = data else {completion(.success(AudioProviderResult(audioClips: [])))return}do {let response = try JSONDecoder().decode(WebServiceAudioResponse.self, from: data)let result = AudioProviderResult(response: response)completion(.success(result))} catch {completion(.failure(error))}}private func parseSong(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {if let error = error {completion(.failure(error))return}guard let data = data else {completion(.success(AudioProviderResult(audioClips: [])))return}do {let response = try JSONDecoder().decode(WebServiceAudioClip.self, from: data)let audioClip = AudioClip(audio: response)let result = AudioProviderResult(audioClips: [audioClip].compactMap { $0 })completion(.success(result))} catch {completion(.failure(error))}}}struct WebServiceAudioClip: Decodable {let id: Stringlet url: URL}struct WebServiceAudioResponse: Decodable {let songs: [WebServiceAudioClip]}extension AudioClip {convenience init(audio: WebServiceAudioClip) {self.init(identifier: audio.id, audioURL: audio.url, resolver: WebServiceAssetResovler.identifier)}}extension AudioProviderResult {convenience init(response: WebServiceAudioResponse) {let audioClips = response.songs.map(AudioClip.init)self.init(audioClips: audioClips)}}class WebServiceAssetResovler: AssetResolver {/// The identifier of this resolver.static let identifier = "web_service_resolver"/// The `WebServiceAudioProvider` responsible for fetching the song for deserialization.public let provider: WebServiceAudioProvider/// Initializes a new `WebServiceAudioProvider` from a provider.////// - Parameters:/// - provider: The `WebServiceAudioProvider` responsible for fetching the song for deserialization.public required init(provider: WebServiceAudioProvider) {self.provider = provider}func deserialize(from data: [String: String], completion: @escaping (ResolvableAsset?) -> Void) {guard let id = data["id"] else {completion(nil)return}provider.get(identifier: id) { result inswitch result {case .success(let result):if let audioClip = result.audioClips.first {completion(audioClip)} else {completion(nil)}returncase .failure:completion(nil)return}}}func serialize(_ asset: ResolvableAsset) -> [String: String]? {if let audioClip = asset as? AudioClip {return ["id": audioClip.identifier]}return nil}}