NCrusher74 / SwiftTagger

A simple library for reading and editing metadata and chapters of Mp4 and Mp3 audio files
Apache License 2.0
9 stars 4 forks source link

SwiftTagger

If you find this library helpful, please consider donating

SwiftTagger combines the SwiftTaggerMP4 and SwiftTaggerID3 libraries into a comprehensive audio file tagging and chaptering library, including bridging the gaps where a tag exists in ID3 but not for Mp4 and vice versa.

You can find SwiftTaggerMP4 here: You can find SwiftTaggerID3 here:

Usage

Add dependency to your package.swift manifest:

let package = Package(
    name: "YourMetadataEditor",
    dependencies: [
        .package(
            name: "SwiftTagger",
            url: "https://github.com/NCrusher74/SwiftTagger",
            from: "1.0.0"
        ),
    ],
    targets: [
        .target(
            name: "YourMetadataEditor",
                dependencies: [
                    .product(name: "SwiftTaggerID3", package: "SwiftTagger"),
                    .product(name: "SwiftTaggerMP4", package: "SwiftTagger"),
                ]
        )
    ]
)

Reading metadata:

let source = try AudioFile(location: file.url)

print(source.album)
print(source.artist)
print(source.trackNumber.track)
print(source.trackNumber.totalTracks)
print(source["FreeformTag"]) // prints freeform tag with the descriptor "FreeformTag"
print(source.involvementCreditsList[.accounting])
print(source.musicianCreditsList[.actor])

Editing metadata:

var source = try AudioFile(location: file.url)

source.album = "New Album Title"
source.artist = "New Artist"
source.trackNumber.track = 3
source.trackNumber.totalTracks = 17
source.compilation = true
source.titleKeywords = ["Title", "Keywords"]

// adding credits list tags - edits TIPL frame for ID3, or creates a freeform atom for the involvement role for MP4
source.involvementCreditsList[.adaptation] = "Involved Person"

// edits TMCL frame for ID3, or adds performer name to performers tag for MP4
source.musicianCreditsList[.actress] = "Actress Name"

// adding freeform tags
source["MyFreeformTagDescription"] = "New Tag Content" // works for ID3 or MP4

// Only works for ID3, as MP4 only allows one comment atom and one lyrics atom
source.id3Tag[comment: "MyCommentDescription", .eng] = "New Comment in English"
source.id3Tag[lyrics: "My Lyrics Description", .eng] = "New Lyrics in English"
source.id3Tag[userDefinedUrl: "My Custom URL"] = "http://url.com"

Reading Chapter Data:

let source = try AudioFile(location: file.url)

for chapter in source.chapterList {
    print(chapter)
}

// (startTime is in milliseconds)
// (startTime: 0, title: "Chapter 1")
// (startTime: 10000, title: "Chapter 2")
// (startTime: 25000, title: "Chapter 3")
// (startTime: 43000, title: "Chapter 4")
// (startTime: 64000, title: "Chapter 5")
// (startTime: 81000, title: "Chapter 6")

Editing Chapter Data:

var source = try AudioFile(location: file.url)

try source.removeAllChapters()

// or

source.removeChapter(startTime: 64000) // removes chapter at startTime 64000
source.addChapter(at: 95000, title: "Chapter 7")

// To edit the title of an existing chapter, add a chapter at the same startTime with a different title.
// To edit the startTime of an existing chapter, remove the old chapter and create a new one

Writing the new file:

var source = try AudioFile(location: file.url)

source.album = "New Album Title"
source.artist = "New Artist"
source.trackNumber.track = 3
source.trackNumber.totalTracks = 17
source.compilation = true
source.addChapter(at: 95000, title: "Chapter 7")

try source.write(outputLocation: output.url)

Handled metadata items:

If a frame is specified but not an atom (or vice versa) it is because the item exists for ID3 but not for MP4. Instead, a freeform atom or user-defined text frame will be created for it.

A-C

D-F

G-L

M-N

P

R-S

T-Z

Limitations, spec-noncompliance, and known issues:

SwiftTagger tries to stick pretty close to the requirements of the ID3 and MP4 documented specs, but there are a few places where it deviates, either because the spec is silly, or compliance would be more cumbersome to achieve than can be justified by the author's needs, or compliance would make the usage of SwiftTagger too convoluted. These deviations are:

A note about compatibility

Each tagging library or app handles things slightly differently. Some will parse values of a tag called "recording date" to a field called "release date". Some will write the value for "publisher" to a field called "label" even though there are separate atoms for each of these.

Each one also has its own unique descriptor that it may recognize for a freeform ---- atom or TXXX frame fulfilling a certain purpose, while another app uses a different descriptor altogether.

For example:

The point being: compatiblity with all other tagging apps and libraries is impossible.

SwiftTagger takes a literalist approach to tags whenever there is ambiguity.

If a tag is called releaseDate it's going to write to the frame or atom for releaseDate not the frame/atom for recordingDate. If desired, which frame or atom SwiftTagger writes to can be adjusted in the accessor for that frame/atom.

If you wanted recordingDate to write to the releaseDate atom, or vice versa, you would change this in Accessors.R-S.swift:

public var recordingDateTime: Date? {
    get {
        switch library {
            case .id3: return id3Tag.recordingDateTime
            case .mp4: return mp4Tag.recordingDate
        }
    }
    set {
        switch library {
            case .id3: id3Tag.recordingDateTime = newValue
            case .mp4: mp4Tag.recordingDate = newValue
        }
    }
}

to this:

public var recordingDateTime: Date? {
    get {
        switch library {
            case .id3: return id3Tag.releaseDateTime
            case .mp4: return mp4Tag.releaseDate
        }
    }
    set {
        switch library {
            case .id3: id3Tag.releaseDateTime = newValue
            case .mp4: mp4Tag.releasegDate = newValue
        }
    }
}

If you wanted the MP4 freeform atom for initialKey to have a different descriptor, you would edit the "Initial Key" string in the initialKey accessor Accessors.E-I.swift:

public var initialKey: KeySignature? {
    get {
        switch library {
            case .id3: return id3Tag.initialKey
            case .mp4:
                if let string = mp4Tag["Initial key"] { // <- edit this string to the desired descriptor
                    return KeySignature(rawValue: string)
                } else {
                    return nil
            }
        }
    }
    set {
        switch library {
            case .id3: id3Tag.initialKey = newValue
            case .mp4: mp4Tag["Initial key"] = newValue?.rawValue // <- edit this to the desired descriptor
        }
    }
}

("Initial key" was chosen for this particular value because that is the string used in MediaInfo for the ID3 tag, so using it as the freeform descriptor for MP4 means the result looks identical to ID3 in MediaInfo. When in doubt, the author tends to default to compatibility with MediaInfo, as it's a widely used and recognized resource.)