diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj
index 4cc2fa87..520a2580 100644
--- a/Demo/Demo.xcodeproj/project.pbxproj
+++ b/Demo/Demo.xcodeproj/project.pbxproj
@@ -379,8 +379,9 @@
452832E72044D28E00458375 /* Project object */ = {
isa = PBXProject;
attributes = {
+ BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1600;
- LastUpgradeCheck = 1200;
+ LastUpgradeCheck = 1600;
ORGANIZATIONNAME = "Fabrizio Duroni";
TargetAttributes = {
45966C942CBACF5000F841E7 = {
@@ -583,6 +584,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -645,6 +647,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -689,6 +692,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -726,6 +730,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "it.chicio.Demo-tvOS";
@@ -766,6 +771,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -806,6 +812,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "it.chicio.Demo-iOS";
@@ -830,6 +837,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Demo macOS/Preview Content\"";
DEVELOPMENT_TEAM = 5Y4K7JX2AU;
ENABLE_HARDENED_RUNTIME = YES;
@@ -867,6 +875,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Demo macOS/Preview Content\"";
DEVELOPMENT_TEAM = 5Y4K7JX2AU;
ENABLE_HARDENED_RUNTIME = YES;
@@ -914,6 +923,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -953,6 +963,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "it.chicio.Demo-watchOS";
diff --git a/ID3TagEditor.xcodeproj/project.pbxproj b/ID3TagEditor.xcodeproj/project.pbxproj
index 63696508..36ad07bd 100644
--- a/ID3TagEditor.xcodeproj/project.pbxproj
+++ b/ID3TagEditor.xcodeproj/project.pbxproj
@@ -1020,7 +1020,7 @@
SKIP_INSTALL = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 3;
- TVOS_DEPLOYMENT_TARGET = 12.0;
+ TVOS_DEPLOYMENT_TARGET = 15.0;
};
name = Debug;
};
@@ -1054,7 +1054,7 @@
SKIP_INSTALL = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 3;
- TVOS_DEPLOYMENT_TARGET = 12.0;
+ TVOS_DEPLOYMENT_TARGET = 15.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
diff --git a/README.md b/README.md
index f6f1c7f0..80b02346 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,13 @@ In the following screenshots you can find examples of the data extracted/updated
martinjbaker
+
+
+
+
+ fabiankr
+
+ |
@@ -78,15 +85,15 @@ In the following screenshots you can find examples of the data extracted/updated
BLeeEZ
- |
+
+
NCrusher74
- |
-
+
@@ -121,7 +128,8 @@ In the following screenshots you can find examples of the data extracted/updated
jverkoey
- |
+
+
diff --git a/Source/ID3TagEditor.swift b/Source/ID3TagEditor.swift
index a101f90b..c4c2e8c8 100644
--- a/Source/ID3TagEditor.swift
+++ b/Source/ID3TagEditor.swift
@@ -12,6 +12,7 @@ import Foundation
*/
public class ID3TagEditor {
private let id3TagParser: ID3TagParser
+ private let id3TagCreator: ID3TagCreator
private let mp3FileReader: Mp3FileReader
private let mp3FileWriter: Mp3FileWriter
private let mp3WithID3TagBuilder: Mp3WithID3TagBuilder
@@ -21,9 +22,10 @@ public class ID3TagEditor {
*/
public init() {
self.id3TagParser = ID3TagParserFactory.make()
+ self.id3TagCreator = ID3TagCreatorFactory.make()
self.mp3FileReader = Mp3FileReaderFactory.make()
self.mp3FileWriter = Mp3FileWriter()
- self.mp3WithID3TagBuilder = Mp3WithID3TagBuilder(id3TagCreator: ID3TagCreatorFactory.make(),
+ self.mp3WithID3TagBuilder = Mp3WithID3TagBuilder(id3TagCreator: id3TagCreator,
id3TagConfiguration: ID3TagConfiguration())
}
@@ -38,7 +40,10 @@ public class ID3TagEditor {
Could throw `CorruptedFile` if the file is corrupted.
*/
public func read(from path: String) throws -> ID3Tag? {
- let mp3 = try mp3FileReader.readID3TagFrom(path: path)
+ guard let mp3 = try mp3FileReader.readID3TagFrom(path: path) else {
+ return nil
+ }
+
return try self.id3TagParser.parse(mp3: mp3)
}
@@ -68,10 +73,9 @@ public class ID3TagEditor {
ID3 tag).
*/
public func write(tag: ID3Tag, to path: String, andSaveTo newPath: String? = nil) throws {
- let mp3 = try mp3FileReader.readFileFrom(path: path)
- let currentTag = try self.id3TagParser.parse(mp3: mp3)
- let mp3WithId3Tag = try mp3WithID3TagBuilder.build(mp3: mp3, newId3Tag: tag, currentId3Tag: currentTag)
- try mp3FileWriter.write(mp3: mp3WithId3Tag, path: newPath ?? path)
+ let currentId3TagData = try mp3FileReader.readID3TagFrom(path: path)
+ let newId3TagData = try id3TagCreator.create(id3Tag: tag)
+ try mp3FileWriter.write(newId3TagData: newId3TagData, currentId3TagData: currentId3TagData, fromPath: path, toPath: newPath ?? path)
}
/**
diff --git a/Source/Mp3/Mp3FileReader.swift b/Source/Mp3/Mp3FileReader.swift
index 6c376f0b..db33fd5a 100644
--- a/Source/Mp3/Mp3FileReader.swift
+++ b/Source/Mp3/Mp3FileReader.swift
@@ -10,11 +10,17 @@ import Foundation
class Mp3FileReader {
private let tagSizeParser: TagSizeParser
private let id3TagConfiguration: ID3TagConfiguration
+ private let tagVersionParser: TagVersionParser
+ private let tagPresence: TagPresence
init(tagSizeParser: TagSizeParser,
- id3TagConfiguration: ID3TagConfiguration) {
+ id3TagConfiguration: ID3TagConfiguration,
+ tagVersionParser: TagVersionParser,
+ tagPresence: TagPresence) {
self.tagSizeParser = tagSizeParser
self.id3TagConfiguration = id3TagConfiguration
+ self.tagVersionParser = tagVersionParser
+ self.tagPresence = tagPresence
}
/**
@@ -41,40 +47,54 @@ class Mp3FileReader {
- parameter path: the path to the mp3 file
- - returns: ID3 header data of the file
+ - returns: ID3 header data or nil, if a tag doesn't exists in the file.
- - throws: Could throw `InvalidFileFormat` if an mp3 file doesn't exists at the specified path, or if the file
- does not contain the entire ID3 header
+ - throws: Could throw `InvalidFileFormat` if an mp3 file doesn't exists at the specified path.
+ Could throw `CorruptedFile` if the file is corrupted.
*/
- func readID3TagFrom(path: String) throws -> Data {
+ func readID3TagFrom(path: String) throws -> Data? {
let validPath = URL(fileURLWithPath: path)
guard validPath.pathExtension.caseInsensitiveCompare("mp3") == ComparisonResult.orderedSame else {
throw ID3TagEditorError.invalidFileFormat
}
- guard let inputStream = InputStream(fileAtPath: path) else {
- throw ID3TagEditorError.corruptedFile
+ let readHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
+ defer {
+ if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
+ try? readHandle.close()
+ } else {
+ readHandle.closeFile()
+ }
}
- inputStream.open()
-
let headerSize = id3TagConfiguration.headerSize()
- let header = try read(bytesCount: headerSize, fromStream: inputStream)
- let headerData = Data(header) as NSData
+ let header = try read(bytesCount: headerSize, from: readHandle)
- let frameSize = tagSizeParser.parse(data: headerData)
- let frame = try read(bytesCount: Int(frameSize), fromStream: inputStream)
+ // Verify that there is a valid ID3 tag to parse the size from
+ let version = tagVersionParser.parse(mp3: header)
+ guard tagPresence.isTagPresentIn(mp3: header, version: version) else {
+ return nil
+ }
- let mp3 = header + frame
- return Data(mp3)
+ let frameSize = tagSizeParser.parse(data: header as NSData)
+ let frame = try read(bytesCount: Int(frameSize), from: readHandle)
+
+ return header + frame
}
- private func read(bytesCount: Int, fromStream stream: InputStream) throws -> [UInt8] {
- var buffer = [UInt8](repeating: 0, count: bytesCount)
- let result = stream.read(&buffer, maxLength: bytesCount)
- if result < bytesCount {
+ private func read(bytesCount: Int, from fileHandle: FileHandle) throws -> Data {
+ let result = try {
+ if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
+ return try fileHandle.read(upToCount: bytesCount)
+ } else {
+ return fileHandle.readData(ofLength: bytesCount)
+ }
+ }()
+
+ guard let result, result.count == bytesCount else {
throw ID3TagEditorError.corruptedFile
}
- return buffer
+
+ return result
}
}
diff --git a/Source/Mp3/Mp3FileReaderFactory.swift b/Source/Mp3/Mp3FileReaderFactory.swift
index d1a322c9..f72b5236 100644
--- a/Source/Mp3/Mp3FileReaderFactory.swift
+++ b/Source/Mp3/Mp3FileReaderFactory.swift
@@ -13,8 +13,10 @@ class Mp3FileReaderFactory {
let tagSizeParser = ID3TagSizeParser()
let id3TagConfiguration = ID3TagConfiguration()
let fileReader = Mp3FileReader(tagSizeParser: tagSizeParser,
- id3TagConfiguration: id3TagConfiguration)
-
+ id3TagConfiguration: id3TagConfiguration,
+ tagVersionParser: ID3TagVersionParser(),
+ tagPresence: ID3TagPresence(id3TagConfiguration: id3TagConfiguration))
+
return fileReader
}
}
diff --git a/Source/Mp3/Mp3FileWriter.swift b/Source/Mp3/Mp3FileWriter.swift
index 9e73e0d3..474b6788 100644
--- a/Source/Mp3/Mp3FileWriter.swift
+++ b/Source/Mp3/Mp3FileWriter.swift
@@ -8,9 +8,115 @@
import Foundation
class Mp3FileWriter {
- func write(mp3: Data, path: String) throws {
- try eventuallyCreateIntermediatesDirectoriesFor(path: path)
- try mp3.write(to: URL(fileURLWithPath: path))
+ func write(newId3TagData: Data, currentId3TagData: Data?, fromPath: String, toPath: String) throws {
+ let validPath = URL(fileURLWithPath: toPath)
+ guard validPath.pathExtension.caseInsensitiveCompare("mp3") == ComparisonResult.orderedSame else {
+ throw ID3TagEditorError.invalidFileFormat
+ }
+
+ // Create a temporary file for the new mp3
+ let temporaryPath = {
+ if toPath != fromPath {
+ return toPath
+ }
+
+ return FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).mp3").path
+ }()
+
+ defer {
+ if temporaryPath != toPath {
+ try? FileManager.default.removeItem(atPath: temporaryPath)
+ }
+ }
+
+ try eventuallyCreateIntermediatesDirectoriesFor(path: temporaryPath)
+ try newId3TagData.write(to: URL(fileURLWithPath: temporaryPath))
+
+ // Create file handles
+ let readHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: fromPath))
+ defer {
+ if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
+ try? readHandle.close()
+ } else {
+ readHandle.closeFile()
+ }
+ }
+
+ let writeHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: temporaryPath))
+ defer {
+ if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
+ try? writeHandle.close()
+ } else {
+ writeHandle.closeFile()
+ }
+ }
+
+ // Seek over the tag of the existing file, then copy the rest in chunks
+ if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
+ try writeHandle.seekToEnd()
+ } else {
+ writeHandle.seekToEndOfFile()
+ }
+
+ if let currentId3TagData = currentId3TagData {
+ if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
+ try readHandle.seek(toOffset: UInt64(currentId3TagData.count))
+ } else {
+ readHandle.seek(toFileOffset: UInt64(currentId3TagData.count))
+ }
+ }
+
+ var isFinished = false
+ while !isFinished {
+ let work = {
+ let chunk = try {
+ if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
+ return try readHandle.read(upToCount: 131072) // 128 KB
+ } else {
+ return readHandle.readData(ofLength: 131072) // 128 KB
+ }
+ }()
+
+ if let chunk, !chunk.isEmpty {
+ if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
+ try writeHandle.write(contentsOf: chunk)
+ } else {
+ writeHandle.write(chunk)
+ }
+ } else {
+ isFinished = true
+ }
+ }
+
+#if canImport(ObjectiveC)
+ // autoreleasepool is only needed in Objective-C environment (not on Linux)
+ try autoreleasepool(invoking: work)
+#else
+ try work()
+#endif
+ }
+
+ // Replace the file
+ if temporaryPath != toPath {
+#if os(Linux)
+ // For some reason the FileManager.replaceItemAt(_:withItemAt:) doesn't work on Linux and fails with `NSFileWriteUnknownError`
+ let backupPath = URL(fileURLWithPath: toPath).appendingPathExtension("tmp").path
+ try FileManager.default.copyItem(atPath: toPath, toPath: backupPath)
+ defer {
+ try? FileManager.default.removeItem(atPath: backupPath)
+ }
+
+ do {
+ try FileManager.default.removeItem(atPath: toPath)
+ try FileManager.default.copyItem(atPath: temporaryPath, toPath: toPath)
+ } catch {
+ try? FileManager.default.copyItem(atPath: backupPath, toPath: toPath)
+ throw error
+ }
+#else
+ _ = try FileManager.default.replaceItemAt(validPath, withItemAt: URL(fileURLWithPath: temporaryPath))
+#endif
+ }
}
private func eventuallyCreateIntermediatesDirectoriesFor(path: String) throws {
diff --git a/Source/Mp3/Mp3WithID3TagBuilder.swift b/Source/Mp3/Mp3WithID3TagBuilder.swift
index e68f96fc..ddf9efad 100644
--- a/Source/Mp3/Mp3WithID3TagBuilder.swift
+++ b/Source/Mp3/Mp3WithID3TagBuilder.swift
@@ -22,7 +22,9 @@ class Mp3WithID3TagBuilder {
tagSizeWithHeader = Int(validCurrentId3Tag.properties.size) + ID3TagConfiguration().headerSize()
}
var mp3WithTag = try id3TagCreator.create(id3Tag: newId3Tag)
- mp3WithTag.append(mp3.subdata(in: tagSizeWithHeader.. |