import Foundation struct BookRemote: Codable, Identifiable, Hashable { var id: String { isbn13 ?? isbn10 ?? title + (authors.first ?? "") } let remoteId: String? let title: String let authors: [String] let publishedYear: Int? let isbn10: String? let isbn13: String? let coverImageUrl: URL? let language: String? let description: String? let pageCount: Int? let categories: [String] let publisher: String? let sourceLocale: String? let readingStatus: ReadingStatus? let readingProgress: Double? init( remoteId: String? = nil, title: String, authors: [String] = [], publishedYear: Int? = nil, isbn10: String? = nil, isbn13: String? = nil, coverImageUrl: URL? = nil, language: String? = nil, description: String? = nil, pageCount: Int? = nil, categories: [String] = [], publisher: String? = nil, sourceLocale: String? = nil, readingStatus: ReadingStatus? = nil, readingProgress: Double? = nil ) { self.remoteId = remoteId self.title = title self.authors = authors self.publishedYear = publishedYear self.isbn10 = isbn10 self.isbn13 = isbn13 self.coverImageUrl = coverImageUrl self.language = language self.description = description self.pageCount = pageCount self.categories = categories self.publisher = publisher self.sourceLocale = sourceLocale self.readingStatus = readingStatus self.readingProgress = readingProgress } } struct BookSearchResponse: Decodable { let items: [BookRemote] init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // Endpoint 1: /isbn returns { data: { tr: {...}, en: {...} } } if let localeMap = try? container.decode([String: RawBook].self, forKey: .data) { self.items = localeMap.map { locale, raw in raw.toBook(locale: locale) } return } // Endpoint 2/3: /title and /filter returns { data: [{ locale, items: [...] }] } if let groups = try? container.decode([LocaleGroup].self, forKey: .data) { self.items = groups.flatMap { group in group.items.map { $0.toBook(locale: group.locale) } } return } self.items = [] } private enum CodingKeys: String, CodingKey { case data } } private struct LocaleGroup: Decodable { let locale: String let items: [RawBook] } private struct RawBook: Decodable { let asin: String? let title: String? let authorName: String? let author: String? let isbn: String? let thumbImage: String? let image: String? let date: String? let publisher: String? let page: Int? let description: String? let categories: [String]? let locale: String? func toBook(locale: String?) -> BookRemote { let coverRaw = thumbImage ?? image let authorsText = authorName ?? author ?? "" let authors = authorsText .replacingOccurrences(of: "[Yazar]", with: "") .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } let isbnClean = isbn?.filter { $0.isNumber || $0.uppercased() == "X" } let isbn10 = isbnClean?.count == 10 ? isbnClean : nil let isbn13 = isbnClean?.count == 13 ? isbnClean : nil return BookRemote( remoteId: asin, title: title ?? "Untitled", authors: authors, publishedYear: Self.extractYear(from: date), isbn10: isbn10, isbn13: isbn13, coverImageUrl: coverRaw.flatMap(URL.init(string:)), language: locale, description: description, pageCount: page, categories: categories ?? [], publisher: publisher, sourceLocale: locale ) } private static func extractYear(from value: String?) -> Int? { guard let value else { return nil } let digits = value.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() guard digits.count >= 4 else { return nil } return Int(String(digits.prefix(4))) } }