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/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() 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 } -}