NCrusher74 / SwiftTaggerID3

A Swift library for reading and editing ID3 tags
Apache License 2.0
5 stars 2 forks source link

Compiler Error - No 'Read' method #17

Closed kamleshgk closed 3 years ago

kamleshgk commented 3 years ago

Hello @NCrusher74 ,

I'm trying a demo macOS project and i tried adding the code to read ID3 attributes. But i get a compiler error.

### Value of type 'Mp3File' has no member 'read'

I want that all the exiting ID3 attributes in the mp3 file should not be overwritten. So in order to retain the existing attributes, the readme file says we have to use the 'read' method. But why is it not available to use?

let mp3Url = URL(fileURLWithPath: "/path/to/file.mp3")
let mp3File = try Mp3File(location: mp3Url)
var tag = try mp3File.read()

print(tag.album)
print(tag.artist)
print(tag.trackNumber)
NCrusher74 commented 3 years ago

Hi, I apologize, the README was out of date. The method you actually need is tag() instead of read(). I've updated the README accordingly.

Thank you for bringing this to my attention.

let mp3Url = URL(fileURLWithPath: "/path/to/file.mp3")
let mp3File = try Mp3File(location: mp3Url)
var tag = try mp3File.tag()

print(tag.album)
print(tag.artist)
print(tag.trackNumber)
kamleshgk commented 3 years ago

Hi @NCrusher74 Thanks for the quick revert. I had to make some changes. I had to create a local clone and made some changes to get rid of the OSX 14.0 check for UniformTypeIdentifier API and changed references to OSX 11.0 to OSX 10.12. I think you really need to get rid of the 11.0 limitation. As lot of software is written to support previous 2-3 versions of macOS. There are other ways of checking if the file is of type .mp3 To make your repo useful, i would recommend to get rid of the 11.0 API and the check. Do think about it. I have already made changes in a new branch, if you want, i can submit a PR. Let me know.

So after testing my code, and when we write to a new file and you can notice the differences here Screen Shot 2021-03-17 at 11 14 22 AM

Only the fields i edited are changed and all other attributes are retained - which is fantastic. But some reason the thumbnail also has some changes. But it does not affect the thumbnail. I can still see my old thumbnails.

Also am having trouble reading and writing the 'comment' attribute. Can you update the readme on that too? Thanks for the great work on your repo.

`        let mp3Url = URL(fileURLWithPath: "/Users/kamyfc/Music/KamySongs/CFLO 12.19/Meduza - Piece Of Your Heart (CFLO No Stupid Talking Edit).mp3")

    do {
        let mp3File = try Mp3File(location: mp3Url)
        var tag = try mp3File.tag()

        print(tag.album ?? "")
        print(tag.artist ?? "")
        print(tag.title ?? "")
        print(tag.composer ?? "")
        //print(tag.comment ?? "")

        tag.album = "djcflo.com KAM"
        tag.artist = "Meduza KAM"
        tag.title = "Piece Of Your Heart Kam"
        tag.composer = "CFLO Kam"
        //tag[comment description: "Description", language: SwiftLanguageAndLocaleCodes.ISO6392Code] = ""
        //tag[description = "Description",  = .eng] = "Comment Content"
        //tag[comment description .eng, "Description"] = "Comment Content"

        let outputUrl = URL(fileURLWithPath: "/Users/kamyfc/Desktop/Meduza.mp3")
        try mp3File.write(
            tag: &tag,
            version: .v2_4,
            outputLocation: outputUrl)
    }
    catch {
        print(error.localizedDescription)
    }`
NCrusher74 commented 3 years ago

Hi. It's been a while since I worked on SwiftTaggerID3 and you know what they say, any code you haven't looked at for more than 10 weeks might as well have been written by someone else. So I've had to spend this morning re-familiarizing myself with what I'd written.

This is the structure of the APIC (attached picture) frame header:

 <Header for 'Attached picture', ID: "APIC">
 Text encoding      $xx
 MIME type          <text string> $00
 Picture type       $xx
 Description        <text string according to encoding> $00 (00)
 Picture data       <binary data>

To begin to troubleshoot, I have to figure out WHERE we are in the frame at the point where your hex values begin.

Looking at the first section of changed/red hex:

E0 00 10 4A 46 49 46

I notice the E0 is right after FF D8 FF. The "magic number" value for JPEG "ff d8 ff e0", so that E0 is actually part of the jpg magic number that is at the beginning of all image data.

Which tells us that everything prior to the Picture data <binary data> part of the frame has already been interpreted before the hex data begins. So we know where we are in the frame now. We're at the beginning of the actual image data.

After I've extracted and interpreted all the frame header data, I "set aside" the image itself as either an NSImage or UIImage, depending on platform:

        if let image = NativeImage(data: data) {
            self.image = image
        } else {
            throw FrameError.InvalidImageData
        }

In case you're wondering what NativeImage is, it's a platform-independent way of working with images:

#if os(macOS)
import Cocoa
public typealias NativeImage = NSImage

#elseif os(iOS)
import UIKit
public typealias NativeImage = UIImage

#endif

So I'm initializing an NSImage instance with whatever data remains after peeling away the frame header data.

        if let image = NSImage(data: data) {
            self.image = image
        } else {
            throw FrameError.InvalidImageData
        }

At encoding time, I put the frame back together like this:

    override var contentData: Data {
        var data = Data()
        // append encoding byte
        let encoding = String.Encoding.isoLatin1
        data.append(encoding.encodingByte)

       // determine format based on file extension
        // encode and append a format or MIME-type string according to version requirements
        var formatString = String()
        switch version {
            case .v2_2:
                if self.imageFormat == .jpg {
                    formatString = "jpg"
                } else if self.imageFormat == .png {
                    formatString = "png"
                }
                data.append(formatString.encodedISOLatin1)
            case .v2_3, .v2_4:
                /// These versions require a MIME-type string
                if self.imageFormat == .jpg {
                    formatString = "image/jpeg"
                } else if self.imageFormat == .png {
                    formatString = "image/png"
                }
                data.append(formatString.encodeNullTerminatedString(encoding))
        }

        // append image type byte
        data.append(self.imageType.rawValue.beData)

        // encode and append image Description
        if let description = self.descriptionString {
            data.append(description.encodeNullTerminatedString(.isoLatin1))
        } else {
            data.append(self.imageType.pictureDescription.encodeNullTerminatedString(.isoLatin1))
        }

       // append image data 
        if self.imageFormat == .jpg {
            if let image = self.image {
                data.append(image.jpgData) // <-- this is probably where the changes are creeping in
            }
        } else {
            if let image = self.image {
                data.append(image.pngData)
            }
        }
        return data
    }

This is where NSImage and UIImage differ. UIImage has functionality for this sort of conversion from UIImage to Data a lot more cleanly laid out:

    var jpgData: Data {
        if let data = self.jpegData(compressionQuality: 1.0) {
            return data
        } else {
            return Data()
        }
    }

With NSImage/Cocoa, however, you have to fudge it with a few more steps:

public extension NSImage {
   /// Converts an `NSImage` to jpg data
    var jpgData: Data {
        if let tiff = tiffRepresentation,
           let bitmap = NSBitmapImageRep(data: tiff),
           let data = bitmap.representation(using: .jpeg, properties: [.compressionFactor: 1.0]) {
            return data
        } else {
            // Not sure if this situation can ever happen anyway.
            // If it does turn out to be possible, this should be turned into a thrown error instead.
            fatalError("Unable to convert image to JPG.")
        }
    }
}

So, this is where the change to the image data is probably happening.

The good news is: it's another easy fix! Replace ImageFrame.swift with this:

https://gist.github.com/NCrusher74/fb556f51f5a83bb0ee95824b62b713de

and see if it helps! Good luck!

NCrusher74 commented 3 years ago

Oh, sorry, I just realized I hadn't answered your question about the Comment frame:

This is what it should look like:

tag[comment: "Comment", .eng] // to read
[comment: "Comment", .eng] = "Comment Content" // to write

I will update the README accordingly.

NCrusher74 commented 3 years ago

FYI I have now edited this library, as well as SwiftTaggerMP4 and SwiftTagger, to remove the UniformTypeIdentifier usage. It took me a few days to get it done, because I have other projects that use these libraries and they had to be adapted accordingly, but as of right now, you should be able to use this directly instead of as a local clone.

kamleshgk commented 3 years ago

Hi @NCrusher74

It works beautifully. Thank you for quickly making these changes. The Image data does not get edited now. Screen Shot 2021-03-18 at 10 36 55 AM

Just one minor thing - how can i edit the actual comment - the one with the "Comment(eng)" in the screenshot?

Thanks again.

NCrusher74 commented 3 years ago

I can't say for certain, but I suspect the comment with the Comment(eng) is simply a comment with no description string (the description string is optional, and if none is available, the frame is just encoded with a null terminator.

Each tag can have multiple comments (and multiple lyrics, and multiple user-defined text/user-defined url) frames, but only one each with the same description and language. And since the description string is optional, you can have one without any description string as well.

So what you would do to recreate what is seen in your screenshot is this:

tag[comment: "Comment", .eng] = "New Comment Content Kam"
tag[nil, .eng] = "djcflo.com/edits"
tag[comment: "iTunNORM", .eng] = // whatever
tag[comment: "iTunPGAP", .eng] = 0
tag[comment: "iTunSMPB", .eng] = // whatever

And like I said, you can do the same with lyrics (USLT), user-defined text (TXXX), and user-defined url (WXXX) frames. Lyrics frames require a language, the same as comment frames do. If you want, I can make the language parameter optional, in which case, SwiftTagger would simply set the language to "undetermined" (und)

kamleshgk commented 3 years ago

Hi @NCrusher74 Thanks. That works.

Thanks for all your help. I appreciate the quick fixes and now your library is ready for macOS apps. If i catch any more issues, will post here.

You may close the issue. Best

NCrusher74 commented 3 years ago

You're very welcome. Best of luck with your project!