Additional Metadata
Leftboot opened this issue · comments
I am attempting to get the duration of a track and using the " - " however it only returns track name and artist. I have tried putting it in the function for additional meta data and doesn't seem to work. I am fairly new to swift so I know I am missing something! (I learnt wayyyyyyyy back on Pascal and C...with no plus.)
While poking around in the API the search (specifically the struct Key.results) is useful in other places of my app as I want to incorporate a "buy" screen. It feels silly to implement a new method/function to duplicate so what would be best practice to send this information to my MainVC?
Thank-you,
Ryan
PS This app has been a lifesaver and great teaching tool so far.
Hi @Leftboot ,
Thanks for the feedback and using the library.
Yes, you can directly update the FRadioAPI
class if you are importing the source code into your project, if you are using a dependency manager, this will overwrite your changes in the future when you update the framework with pod update
or carthage update
.
To answer your question, you can add a wrapper struct
to hold more data from the iTunes API:
public struct MetaData {
let artworkURL: URL?
let duration: Int?
let price: Double?
let iTunesURL: URL?
// You can add more if you want
}
Then in the FRadioAPI
, add the keys, parse the results and change completion type from URL?
to MetaData?
:
internal struct FRadioAPI {
// MARK: - Util methods
// Change completionHandler result type to MetaData?
static func getArtwork(for metadata: String, size: Int, completionHandler: @escaping (_ artworkURL: MetaData?) -> ()) {
guard !metadata.isEmpty, metadata != " - ", let url = getURL(with: metadata) else {
completionHandler(nil)
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
guard error == nil, let data = data else {
completionHandler(nil)
return
}
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
guard let parsedResult = json as? [String: Any],
let results = parsedResult[Keys.results] as? Array<[String: Any]>,
let result = results.first,
var artwork = result[Keys.artwork] as? String else {
completionHandler(nil)
return
}
if size != 100, size > 0 {
artwork = artwork.replacingOccurrences(of: "100x100", with: "\(size)x\(size)")
}
let artworkURL = URL(string: artwork)
// parse the results
let duration = result[Keys.duration] as? Int
let price = result[Keys.price] as? Double
let stringURL = result[Keys.url] as? String
let iTunesURL = URL(string: stringURL ?? "")
let metaData = MetaData(artworkURL: artworkURL, duration: duration, price: price, iTunesURL: iTunesURL)
completionHandler(metaData)
}).resume()
}
// ....
private struct Keys {
// Request
static let term = "term"
static let entity = "entity"
// Response
static let results = "results"
static let artwork = "artworkUrl100"
// Additional keys
static let duration = "trackTimeMillis"
static let price = "trackPrice"
static let url = "trackViewUrl"
}
}
You need also to change the completion type in FRadioAPI
call:
// line 400 FRadioPlayer
FRadioAPI.getArtwork(for: rawValue, size: artworkSize, completionHandler: { [unowned self] metaData in
DispatchQueue.main.async {
print(metaData)
self.delegate?.radioPlayer?(self, artworkDidChange: metaData?.artworkURL)
}
})
You will need to update the delegate func definition to handle a MetaData
object instead of a URL
.
You can also explore the Codable
protocol to easily decode the JSON, I may update this in the future and pass more infos from the iTunes API.
Hope this helps!
Cheers.
Wow! Thank-you.
I almost made it! However when it came to the delegate (I think) the wheels fell off. I followed your suggestion and when it didn't work I only have artworkURL, duration and iTunesURL in my MetaData struct and keys.
The api is giving me the error: Cannot convert value of type 'MetaData' to expected argument type 'URL?'
Line 393 to 406, with errors:
private func shouldGetArtwork(for rawValue: String?, _ enabled: Bool) {
guard enabled else { return }
guard let rawValue = rawValue else {
self.delegate?.radioPlayer?(self, artworkDidChange: nil) "_**Type of expression is ambiguous without more context"**_
return
}
FRadioAPI.getArtwork(for: rawValue, size: artworkSize, completionHandler: { [unowned self] metaData in
DispatchQueue.main.async {
print(metaData) **_"Expression implicitly coerced from 'URL?' to 'Any'"_**
self.delegate?.radioPlayer?(self, artworkDidChange: metaData?.artworkURL) "**_Value of type 'URL' has no member 'artworkURL"
}_**
})
The delegate was changed to:
func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: MetaData?)
as I was getting: " Method cannot be a member of an @objc protocol because the type of the parameter 2 cannot be represented in Objective-C"
That didn't silence the warning.
I also did try
@objc optional func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: MetaData.artworkURL?)
But this gave me: "Let 'artworkURL' is not a member type of 'MetaData'"
Yes, that's because the struct
are not compatible with Objective-C, I did some testing and you should:
- Remove the
@objc
from the delegate protocol, I renamed to last function toiTunesMetaDataDidChange
:
public protocol FRadioPlayerDelegate: class {
/**
Called when player changes state
- parameter player: FRadioPlayer
- parameter state: FRadioPlayerState
*/
func radioPlayer(_ player: FRadioPlayer, playerStateDidChange state: FRadioPlayerState)
/**
Called when the player changes the playing state
- parameter player: FRadioPlayer
- parameter state: FRadioPlaybackState
*/
func radioPlayer(_ player: FRadioPlayer, playbackStateDidChange state: FRadioPlaybackState)
/**
Called when player changes the current player item
- parameter player: FRadioPlayer
- parameter url: Radio URL
*/
func radioPlayer(_ player: FRadioPlayer, itemDidChange url: URL?)
/**
Called when player item changes the timed metadata value, it uses (separatedBy: " - ") to get the artist/song name, if you want more control over the raw metadata, consider using `metadataDidChange rawValue` instead
- parameter player: FRadioPlayer
- parameter artistName: The artist name
- parameter trackName: The track name
*/
func radioPlayer(_ player: FRadioPlayer, metadataDidChange artistName: String?, trackName: String?)
/**
Called when player item changes the timed metadata value
- parameter player: FRadioPlayer
- parameter rawValue: metadata raw value
*/
func radioPlayer(_ player: FRadioPlayer, metadataDidChange rawValue: String?)
/**
Called when the player gets the artwork for the playing song
- parameter player: FRadioPlayer
- parameter metaData: meta data from iTunes
*/
func radioPlayer(_ player: FRadioPlayer, iTunesMetaDataDidChange metaData: MetaData?)
}
This will make all the protocol definitions required!
- you need to remove the optional calls from the delegate functions, for example:
delegate?.radioPlayer?(self, metadataDidChange: rawValue)
// Become
delegate?.radioPlayer(self, metadataDidChange: rawValue)
- Make all
MetaData
propertiespublic
:
public struct MetaData {
public let artworkURL: URL?
public let duration: Int?
public let price: Double?
public let iTunesURL: URL?
}
- Update your delegate implementation in the
ViewController
:
func radioPlayer(_ player: FRadioPlayer, iTunesMetaDataDidChange metaData: MetaData?) {
guard let metaData = metaData, let artworkURL = metaData.artworkURL, let data = try? Data(contentsOf: artworkURL) else {
artworkImageView.image = stations[selectedIndex].image
return
}
track?.image = UIImage(data: data)
artworkImageView.image = track?.image
updateNowPlaying(with: track)
}
Man I so appreciate this! Only one last error I wasn't able to fix. On line 403 I am getting this error and I cannot wrap my head around it. (It was the same as last time.)
Cannot convert value of type 'URL?' to expected argument type 'MetaData?'
line 403 is:
self.delegate?.radioPlayer(self, iTunesMetaDataDidChange: metaData?.artworkURL)
I am sure I missed something small. My struct is as you suggested (with public properties) and artworkURL is in there.
Yes, you need only to pass the metaData
instead of metaData?.artworkURL
self.delegate?.radioPlayer(self, iTunesMetaDataDidChange: metaData)
I removed the "?" but not artwork. Thank-you again! Works like a charm.