Merge pull request #1198 from kiwix/1193-change-library-endpoint

Library endpoint changes
This commit is contained in:
Kelson 2025-05-10 20:35:23 +02:00 committed by GitHub
commit ecc9ba1556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 67 additions and 23 deletions

View File

@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface OPDSParser : NSObject
- (nonnull instancetype)init;
- (BOOL)parseData:(nonnull NSData *)data NS_REFINED_FOR_SWIFT;
- (BOOL)parseData:(nonnull NSData *)data using: (nonnull NSString *)urlHost NS_REFINED_FOR_SWIFT;
- (nonnull NSSet *)getZimFileIDs NS_REFINED_FOR_SWIFT;
- (nullable ZimFileMetaData *)getZimFileMetaData:(nonnull NSUUID *)identifier NS_REFINED_FOR_SWIFT;

View File

@ -39,7 +39,7 @@
return self;
}
- (BOOL)parseData:(nonnull NSData *)data {
- (BOOL)parseData:(nonnull NSData *)data using: (nonnull NSString *)urlHost {
try {
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (content == nil) {
@ -47,7 +47,7 @@
}
std::shared_ptr<kiwix::Manager> manager = std::make_shared<kiwix::Manager>(self.library);
return manager->readOpds([content cStringUsingEncoding:NSUTF8StringEncoding],
[@"https://library.kiwix.org" cStringUsingEncoding:NSUTF8StringEncoding]);
[urlHost cStringUsingEncoding:NSUTF8StringEncoding]);
} catch (std::exception) {
return false;
}

View File

@ -15,7 +15,7 @@
protocol Parser {
var zimFileIDs: Set<UUID> { get }
func parse(data: Data) throws
func parse(data: Data, urlHost: String) throws
func getMetaData(id: UUID) -> ZimFileMetaData?
}
@ -24,8 +24,8 @@ extension OPDSParser: Parser {
__getZimFileIDs() as? Set<UUID> ?? Set<UUID>()
}
func parse(data: Data) throws {
if !self.__parseData(data) {
func parse(data: Data, urlHost: String) throws {
if !self.__parseData(data, using: urlHost.removingSuffix("/")) {
throw LibraryRefreshError.parse
}
}
@ -42,7 +42,7 @@ extension OPDSParser: Parser {
struct DeletingParser: Parser {
let zimFileIDs: Set<UUID> = .init()
func parse(data: Data) throws {
func parse(data: Data, urlHost: String) throws {
}
func getMetaData(id: UUID) -> ZimFileMetaData? {

View File

@ -21,6 +21,11 @@ extension String {
guard hasPrefix(value) else { return self }
return String(dropFirst(value.count))
}
func removingSuffix(_ value: String) -> String {
guard hasSuffix(value) else { return self }
return String(dropLast(value.count))
}
func replacingRegex(
matching pattern: String,

View File

@ -95,4 +95,25 @@ extension URL {
components.scheme = "zim"
return components.url ?? self
}
/// Remove the defined components one by one if found
/// - Parameter pathComponents: eg: /package/details/more can be defined as: ["package", "details", "more"]
/// - Returns: the modified url
func trim(pathComponents: [String]) -> URL {
var result = self
for component in pathComponents.reversed() where component == result.lastPathComponent {
result = result.deletingLastPathComponent()
}
return result
}
/// Removes everything after ? or &
/// - Returns: the modified URL, or the same if it fails to find the components
func withoutQueryParams() -> URL {
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
return self
}
components.queryItems = nil
return components.url ?? self
}
}

View File

@ -171,8 +171,9 @@ final class LibraryRefreshViewModelTest: XCTestCase {
func testNewZimFileAndProperties() async throws {
let zimFileID = UUID()
HTTPTestingURLProtocol.handler = { urlProtocol in
let responseTestURL = URL(string: "https://response-testing.com/catalog/v2/entries?count=-1")!
let response = HTTPURLResponse(
url: URL.mock(),
url: responseTestURL,
statusCode: 200, httpVersion: nil, headerFields: [:]
)!
let data = self.makeOPDSData(zimFileID: zimFileID).data(using: .utf8)!
@ -211,7 +212,7 @@ final class LibraryRefreshViewModelTest: XCTestCase {
XCTAssertNil(zimFile.faviconData)
XCTAssertEqual(
zimFile.faviconURL,
URL(string: "https://library.kiwix.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
URL(string: "https://response-testing.com/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
)
XCTAssertEqual(zimFile.fileDescription, "A selection of the best 50,000 Wikipedia articles")
XCTAssertEqual(zimFile.fileID, zimFileID)

View File

@ -22,7 +22,7 @@ final class OPDSParserTests: XCTestCase {
XCTExpectFailure("Requires work in dependency to resolve the issue.")
let content = "Invalid OPDS Data"
XCTAssertThrowsError(
try OPDSParser().parse(data: content.data(using: .utf8)!)
try OPDSParser().parse(data: content.data(using: .utf8)!, urlHost: "")
)
}
@ -31,7 +31,7 @@ final class OPDSParserTests: XCTestCase {
let incompatibleEncodings: [String.Encoding] = [.unicode, .utf16, .utf32]
try incompatibleEncodings.forEach { encoding in
XCTAssertThrowsError(
try OPDSParser().parse(data: content.data(using: encoding)!),
try OPDSParser().parse(data: content.data(using: encoding)!, urlHost: ""),
"parsing with enconding \(encoding.description) should fail"
)
}
@ -73,10 +73,16 @@ final class OPDSParserTests: XCTestCase {
</entry>
</feed>
"""
// Parse data
let responseTestURL = URL(string: "https://resp-test.org/")!
let parser = OPDSParser()
XCTAssertNoThrow(try parser.parse(data: content.data(using: .utf8)!))
XCTAssertNoThrow(
try parser.parse(
data: content.data(using: .utf8)!,
urlHost: responseTestURL.absoluteString
)
)
// check one zim file is populated
let zimFileID = UUID(uuidString: "1ec90eab-5724-492b-9529-893959520de4")!
@ -108,7 +114,7 @@ final class OPDSParserTests: XCTestCase {
)
XCTAssertEqual(
metadata.faviconURL,
URL(string: "https://library.kiwix.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
URL(string: "https://resp-test.org/catalog/v2/illustration/1ec90eab-5724-492b-9529-893959520de4/")
)
XCTAssertEqual(metadata.flavor, "maxi")
}

View File

@ -43,5 +43,11 @@ final class URLContentPathTests: XCTestCase {
"widgets.wp.com/likes/master.html?ver=20240530"
])
}
func test_trimming() {
let inputURL = URL(string: "https://library.kiwix.org/catalog/v2/entries?count=-1")!
let expectedURL = URL(string: "https://library.kiwix.org/")!
XCTAssertEqual(inputURL.withoutQueryParams().trim(pathComponents: ["catalog", "v2", "entries"]), expectedURL)
}
}

View File

@ -95,7 +95,7 @@ final class LibraryViewModel: ObservableObject {
private var insertionCount = 0
private var deletionCount = 0
private static let catalogURL = URL(string: "https://library.kiwix.org/catalog/v2/entries?count=-1")!
private static let catalogURL = URL(string: "https://opds.library.kiwix.org/v2/entries?count=-1")!
@MainActor
init(
@ -131,7 +131,7 @@ final class LibraryViewModel: ObservableObject {
process.state = .inProgress
// refresh library
guard let data = try await fetchData() else {
guard case (var data, let responseURL)? = try await fetchData() else {
// this is the case when we have no new data (304 http)
// but we still need to refresh the memory only stored
// zimfile categories to languages dictionary
@ -156,7 +156,7 @@ final class LibraryViewModel: ObservableObject {
return
}
}
let parser = try await parse(data: data)
let parser = try await parse(data: data, urlHost: responseURL)
// delete all old ISO Lang Code entries if needed, by passing in an empty parser
if defaults[.libraryUsingOldISOLangCodes] {
try await process(parser: DeletingParser())
@ -252,7 +252,7 @@ final class LibraryViewModel: ObservableObject {
}
}
private func fetchData() async throws -> Data? {
private func fetchData() async throws -> (Data, URL)? {
do {
var request = URLRequest(url: Self.catalogURL, timeoutInterval: 20)
request.allHTTPHeaderFields = ["If-None-Match": defaults[.libraryETag]]
@ -260,19 +260,20 @@ final class LibraryViewModel: ObservableObject {
guard let response = response as? HTTPURLResponse else { return nil }
switch response.statusCode {
case 200:
let responseURL = response.url ?? Self.catalogURL
if let eTag = response.allHeaderFields["Etag"] as? String {
defaults[.libraryETag] = eTag
}
// OK to process further
os_log("Retrieved OPDS Data, size: %llu bytes", log: Log.OPDS, type: .info, data.count)
return data
return (data, responseURL)
case 304:
return nil // already downloaded
default:
throw LibraryRefreshError.retrieve(description: "HTTP Status \(response.statusCode).")
}
} catch {
os_log("Error retrieving OPDS Data: %s", log: Log.OPDS, type: .error)
os_log("Error retrieving OPDS Data: %s", log: Log.OPDS, type: .error, error.localizedDescription)
if let error = error as? LibraryRefreshError {
throw error
} else {
@ -281,11 +282,15 @@ final class LibraryViewModel: ObservableObject {
}
}
private func parse(data: Data) async throws -> OPDSParser {
private func parse(data: Data, urlHost: URL) async throws -> OPDSParser {
try await withCheckedThrowingContinuation { continuation in
let parser = OPDSParser()
do {
try parser.parse(data: data)
let urlHostString = urlHost
.withoutQueryParams()
.trim(pathComponents: ["catalog", "v2", "entries"])
.absoluteString
try parser.parse(data: data, urlHost: urlHostString)
continuation.resume(returning: parser)
} catch {
continuation.resume(throwing: error)

View File

@ -63,7 +63,7 @@ struct Favicon_Previews: PreviewProvider {
category: .wikipedia,
imageData: nil,
imageURL: URL(
string: "https://library.kiwix.org/meta?name=favicon&content=wikipedia_en_climate_change_maxi_2021-12"
string: "https://opds.library.kiwix.org/v2/illustration/e82e6816-a2dc-a7f0-2d15-58d24709db93/?size=48"
)!
).frame(width: 200, height: 200).previewLayout(.sizeThatFits)
Favicon(