velody/packages/apple/VelodyPersistence/Tests/VelodyPersistenceTests/MP3EmbeddedArtworkExtractorTests.swift
2026-05-31 01:20:56 +02:00

114 lines
3.6 KiB
Swift

import CryptoKit
import Foundation
import XCTest
@testable import VelodyPersistence
final class MP3EmbeddedArtworkExtractorTests: XCTestCase {
func testExtractorReadsEmbeddedPNGArtworkFromID3APICFrame() throws {
let fileManager = FileManager.default
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true
)
let fileURL = tempDirectory.appendingPathComponent("artwork-test.mp3")
let extractor = MP3EmbeddedArtworkExtractor()
let artworkData = sampleArtworkData()
defer {
try? fileManager.removeItem(at: tempDirectory)
}
try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
try makeMP3FileWithEmbeddedArtwork(
artworkData: artworkData,
mimeType: "image/png",
at: fileURL
)
let extractedArtwork = try XCTUnwrap(
extractor.extractArtwork(from: fileURL)
)
XCTAssertEqual(extractedArtwork.data, artworkData)
XCTAssertEqual(extractedArtwork.mimeType, "image/png")
XCTAssertEqual(extractedArtwork.width, 1)
XCTAssertEqual(extractedArtwork.height, 1)
XCTAssertEqual(extractedArtwork.sha256, sha256Hex(artworkData))
}
func testExtractorReturnsNilWhenMP3HasNoEmbeddedArtwork() throws {
let fileManager = FileManager.default
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true
)
let fileURL = tempDirectory.appendingPathComponent("no-artwork.mp3")
let extractor = MP3EmbeddedArtworkExtractor()
defer {
try? fileManager.removeItem(at: tempDirectory)
}
try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
try Data([
0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]).write(to: fileURL)
XCTAssertNil(try extractor.extractArtwork(from: fileURL))
}
}
private func makeMP3FileWithEmbeddedArtwork(
artworkData: Data,
mimeType: String,
at fileURL: URL
) throws {
let mimeTypeData = Data(mimeType.utf8) + Data([0x00])
let frameBody = Data([0x00]) // ISO-8859-1 encoding
+ mimeTypeData
+ Data([0x03]) // front cover
+ Data([0x00]) // empty description
+ artworkData
let frameSize = UInt32(frameBody.count)
let frameHeader = Data("APIC".utf8)
+ Data([
UInt8((frameSize >> 24) & 0xff),
UInt8((frameSize >> 16) & 0xff),
UInt8((frameSize >> 8) & 0xff),
UInt8(frameSize & 0xff),
0x00,
0x00,
])
let tagBody = frameHeader + frameBody
let tagSize = synchsafeBytes(for: tagBody.count)
let id3Header = Data([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]) + tagSize
let mp3Bytes = id3Header + tagBody + Data([
0xff, 0xfb, 0x90, 0x64, 0x00, 0x00, 0x00, 0x00,
])
try mp3Bytes.write(to: fileURL)
}
private func synchsafeBytes(for value: Int) -> Data {
Data([
UInt8((value >> 21) & 0x7f),
UInt8((value >> 14) & 0x7f),
UInt8((value >> 7) & 0x7f),
UInt8(value & 0x7f),
])
}
private func sampleArtworkData() -> Data {
Data(
base64Encoded:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg=="
)!
}
private func sha256Hex(_ data: Data) -> String {
SHA256.hash(data: data)
.map { String(format: "%02x", $0) }
.joined()
}