From 1810800f5092ee60606912abd5eb1df33682b0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 20 Jan 2026 17:10:52 +0100 Subject: [PATCH 1/4] Remove title inference based on folder names for raw archives --- CHANGELOG.md | 1 + .../Streamer/Parser/Audio/AudioParser.swift | 2 +- .../Streamer/Parser/Image/ImageParser.swift | 10 +--- .../Toolkit/Extensions/Container.swift | 21 -------- TestApp/Sources/Library/LibraryService.swift | 21 ++++++-- .../Toolkit/Extensions/ContainerTests.swift | 53 ------------------- 6 files changed, 20 insertions(+), 88 deletions(-) delete mode 100644 Tests/StreamerTests/Toolkit/Extensions/ContainerTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a35e8919a..f8c2dc68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file. Take a look #### Streamer * The EPUB manifest item `id` attribute is no longer exposed in `Link.properties`. +* Removed title inference based on folder names within image and audio archives. Use the archive's filename instead. ### Fixed diff --git a/Sources/Streamer/Parser/Audio/AudioParser.swift b/Sources/Streamer/Parser/Audio/AudioParser.swift index 0d9a461fc..821415a60 100644 --- a/Sources/Streamer/Parser/Audio/AudioParser.swift +++ b/Sources/Streamer/Parser/Audio/AudioParser.swift @@ -76,7 +76,7 @@ public final class AudioParser: PublicationParser { await makeBuilder( container: asset.container, readingOrder: readingOrder, - title: asset.container.guessTitle(ignoring: ignores) + title: nil ) } } diff --git a/Sources/Streamer/Parser/Image/ImageParser.swift b/Sources/Streamer/Parser/Image/ImageParser.swift index e7f7fb018..8d55922cf 100644 --- a/Sources/Streamer/Parser/Image/ImageParser.swift +++ b/Sources/Streamer/Parser/Image/ImageParser.swift @@ -53,8 +53,7 @@ public final class ImageParser: PublicationParser { let container = SingleResourceContainer(publication: asset) return makeBuilder( container: container, - readingOrder: [(container.entry, asset.format)], - fallbackTitle: nil + readingOrder: [(container.entry, asset.format)] ) } @@ -68,14 +67,12 @@ public final class ImageParser: PublicationParser { // Parse ComicInfo.xml metadata if present let comicInfo = await parseComicInfo(from: asset.container, warnings: warnings) - let fallbackTitle = asset.container.guessTitle(ignoring: ignores) return await makeReadingOrder(for: asset.container) .flatMap { readingOrder in makeBuilder( container: asset.container, readingOrder: readingOrder, - fallbackTitle: fallbackTitle, comicInfo: comicInfo ) } @@ -131,7 +128,6 @@ public final class ImageParser: PublicationParser { private func makeBuilder( container: Container, readingOrder: [(AnyURL, Format)], - fallbackTitle: String?, comicInfo: ComicInfo? = nil ) -> Result { guard !readingOrder.isEmpty else { @@ -175,10 +171,6 @@ public final class ImageParser: PublicationParser { metadata.conformsTo = [.divina] metadata.layout = .fixed - if metadata.localizedTitle == nil, let fallbackTitle = fallbackTitle { - metadata.localizedTitle = .nonlocalized(fallbackTitle) - } - // Apply center page layout for double-page spreads if let pages = comicInfo?.pages { for pageInfo in pages where pageInfo.doublePage == true { diff --git a/Sources/Streamer/Toolkit/Extensions/Container.swift b/Sources/Streamer/Toolkit/Extensions/Container.swift index 87766875a..9e69dce98 100644 --- a/Sources/Streamer/Toolkit/Extensions/Container.swift +++ b/Sources/Streamer/Toolkit/Extensions/Container.swift @@ -57,25 +57,4 @@ extension Container { return .success(entries) } - - /// Guesses a publication title from a list of resource HREFs. - /// - /// If the HREFs contain a single root directory, we assume it is the - /// title. This is often the case for example with CBZ files. - func guessTitle(ignoring: (AnyURL) -> Bool = { _ in false }) -> String? { - var title: String? - - for url in entries { - if ignoring(url) { - continue - } - let segments = url.pathSegments - guard segments.count > 1, title == nil || title == segments.first else { - return nil - } - title = segments.first - } - - return title - } } diff --git a/TestApp/Sources/Library/LibraryService.swift b/TestApp/Sources/Library/LibraryService.swift index b0307d6e3..1d9e965ad 100644 --- a/TestApp/Sources/Library/LibraryService.swift +++ b/TestApp/Sources/Library/LibraryService.swift @@ -116,17 +116,24 @@ final class LibraryService: Loggable { } let (pub, format) = try await openPublication(at: url, allowUserInteraction: false, sender: sender) + let title = pub.metadata.title ?? url.url.deletingPathExtension().lastPathComponent let coverPath = try await importCover(of: pub) if let file = url.fileURL { url = try moveToDocuments( from: file, - title: pub.metadata.title ?? file.lastPathSegment, + title: title, format: format ) } - return try await insertBook(at: url, publication: pub, mediaType: format.mediaType, coverPath: coverPath) + return try await insertBook( + at: url, + publication: pub, + mediaType: format.mediaType, + title: title, + coverPath: coverPath + ) } /// Fulfills the given `url` if it's a DRM license file. @@ -177,13 +184,19 @@ final class LibraryService: Loggable { } /// Inserts the given `book` in the bookshelf. - private func insertBook(at url: AbsoluteURL, publication: Publication, mediaType: MediaType?, coverPath: String?) async throws -> Book { + private func insertBook( + at url: AbsoluteURL, + publication: Publication, + mediaType: MediaType?, + title: String, + coverPath: String? + ) async throws -> Book { // Makes the URL relative to the Documents/ folder if possible. let url: AnyURL = Paths.documents.relativize(url)?.anyURL ?? url.anyURL let book = Book( identifier: publication.metadata.identifier, - title: publication.metadata.title ?? url.lastPathSegment ?? "Untitled", + title: title, authors: publication.metadata.authors .map(\.name) .joined(separator: ", "), diff --git a/Tests/StreamerTests/Toolkit/Extensions/ContainerTests.swift b/Tests/StreamerTests/Toolkit/Extensions/ContainerTests.swift deleted file mode 100644 index a0dd77af0..000000000 --- a/Tests/StreamerTests/Toolkit/Extensions/ContainerTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright 2026 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import ReadiumShared -@testable import ReadiumStreamer -import XCTest - -class ContainerTests: XCTestCase { - func testGuessTitleWithoutDirectories() { - let container = TestContainer(hrefs: ["a.txt", "b.png"]) - XCTAssertNil(container.guessTitle()) - } - - func testGuessTitleWithOneRootDirectory() { - let container = TestContainer(hrefs: ["Root%20Directory/b.png", "Root%20Directory/dir/c.png"]) - XCTAssertEqual(container.guessTitle(), "Root Directory") - } - - func testGuessTitleWithOneRootDirectoryButRootFiles() { - let container = TestContainer(hrefs: ["a.txt", "Root%20Directory/b.png", "Root%20Directory/dir/c.png"]) - XCTAssertNil(container.guessTitle()) - } - - func testGuessTitleWithOneRootDirectoryIgnoringRootFile() { - let container = TestContainer(hrefs: [".hidden", "Root%20Directory/b.png", "Root%20Directory/dir/c.png"]) - XCTAssertEqual(container.guessTitle(ignoring: { url in url.lastPathSegment == ".hidden" }), "Root Directory") - } - - func testGuessTitleWithSeveralDirectories() { - let container = TestContainer(hrefs: ["a.txt", "dir1/b.png", "dir2/c.png"]) - XCTAssertNil(container.guessTitle()) - } - - func testGuessTitleIgnoresSingleFiles() { - let container = TestContainer(hrefs: ["single"]) - XCTAssertNil(container.guessTitle()) - } -} - -private struct TestContainer: Container { - init(hrefs: [String]) { - entries = Set(hrefs.map { AnyURL(string: $0)! }) - } - - let entries: Set - - let sourceURL: (any AbsoluteURL)? = nil - - subscript(url: any URLConvertible) -> (any Resource)? { nil } -} From e770a60cdb0e1e9550b1a55d842f3b44dc4f4a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 20 Jan 2026 17:25:38 +0100 Subject: [PATCH 2/4] Use a generic device for GitHub actions --- .github/workflows/checks.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 031cd4990..e55c4ef20 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,8 +6,6 @@ on: pull_request: env: - platform: ${{ 'iOS Simulator' }} - device: ${{ 'iPhone 17' }} commit_sha: ${{ github.sha }} DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer @@ -34,11 +32,11 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build-for-testing -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build-for-testing -scheme "$scheme" -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi - name: Test run: | set -eo pipefail - xcodebuild test-without-building -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild test-without-building -scheme "$scheme" -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi navigator-ui-tests: name: Navigator UI Tests @@ -55,7 +53,7 @@ jobs: run: | set -eo pipefail make navigator-ui-tests-project - xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi lint: name: Lint @@ -111,7 +109,7 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi int-spm: name: Integration (Swift Package Manager) @@ -139,7 +137,7 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi int-carthage: name: Integration (Carthage) @@ -167,5 +165,5 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi From 5f0ac79d799cc83e2449de84161878509bb50fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 20 Jan 2026 17:29:36 +0100 Subject: [PATCH 3/4] Fix tests --- Tests/StreamerTests/Parser/Audio/AudioParserTests.swift | 5 ----- Tests/StreamerTests/Parser/Image/ImageParserTests.swift | 5 ----- 2 files changed, 10 deletions(-) diff --git a/Tests/StreamerTests/Parser/Audio/AudioParserTests.swift b/Tests/StreamerTests/Parser/Audio/AudioParserTests.swift index c1ec0eee0..2d7cda111 100644 --- a/Tests/StreamerTests/Parser/Audio/AudioParserTests.swift +++ b/Tests/StreamerTests/Parser/Audio/AudioParserTests.swift @@ -77,11 +77,6 @@ class AudioParserTests: XCTestCase { XCTAssertNil(publication.linkWithRel(.cover)) } - func testComputeTitleFromArchiveRootDirectory() async throws { - let publication = try await parser.parse(asset: zabAsset, warnings: nil).get().build() - XCTAssertEqual(publication.metadata.title, "Test Audiobook") - } - func testHasNoPositions() async throws { let publication = try await parser.parse(asset: zabAsset, warnings: nil).get().build() let result = try await publication.positions().get() diff --git a/Tests/StreamerTests/Parser/Image/ImageParserTests.swift b/Tests/StreamerTests/Parser/Image/ImageParserTests.swift index 172c4780d..fbb87e234 100644 --- a/Tests/StreamerTests/Parser/Image/ImageParserTests.swift +++ b/Tests/StreamerTests/Parser/Image/ImageParserTests.swift @@ -86,11 +86,6 @@ class ImageParserTests: XCTestCase { XCTAssertEqual(publication.readingOrder.first, cover) } - func testComputeTitleFromArchiveRootDirectory() async throws { - let publication = try await parser.parse(asset: cbzAsset, warnings: nil).get().build() - XCTAssertEqual(publication.metadata.title, "Cory Doctorow's Futuristic Tales of the Here and Now") - } - func testPositions() async throws { let publication = try await parser.parse(asset: cbzAsset, warnings: nil).get().build() From b22f01062c12f7b318d574e3ad34a50e68daee7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 20 Jan 2026 17:36:27 +0100 Subject: [PATCH 4/4] Revert "Use a generic device for GitHub actions" This reverts commit e770a60cdb0e1e9550b1a55d842f3b44dc4f4a18. --- .github/workflows/checks.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e55c4ef20..031cd4990 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,6 +6,8 @@ on: pull_request: env: + platform: ${{ 'iOS Simulator' }} + device: ${{ 'iPhone 17' }} commit_sha: ${{ github.sha }} DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer @@ -32,11 +34,11 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build-for-testing -scheme "$scheme" -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build-for-testing -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi - name: Test run: | set -eo pipefail - xcodebuild test-without-building -scheme "$scheme" -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild test-without-building -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi navigator-ui-tests: name: Navigator UI Tests @@ -53,7 +55,7 @@ jobs: run: | set -eo pipefail make navigator-ui-tests-project - xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi lint: name: Lint @@ -109,7 +111,7 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi int-spm: name: Integration (Swift Package Manager) @@ -137,7 +139,7 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi int-carthage: name: Integration (Carthage) @@ -165,5 +167,5 @@ jobs: - name: Build run: | set -eo pipefail - xcodebuild build -scheme TestApp -destination "generic/platform=iOS" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi