fethica / FRadioPlayer

A simple radio player framework for iOS, macOS, tvOS.

Home Page:https://fethica.github.io/FRadioPlayer/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

  1. Remove the @objc from the delegate protocol, I renamed to last function to iTunesMetaDataDidChange :
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!

  1. you need to remove the optional calls from the delegate functions, for example:
delegate?.radioPlayer?(self, metadataDidChange: rawValue)
// Become
delegate?.radioPlayer(self, metadataDidChange: rawValue)
  1. Make all MetaData properties public:
public struct MetaData {
    public let artworkURL: URL?
    public let duration: Int?
    public let price: Double?
    public let iTunesURL: URL?
}
  1. 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.