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}}