diff --git a/.swiftlint.yml b/.swiftlint.yml index a4e2ac1..e06f801 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -10,8 +10,8 @@ identifier_name: min_length: 1 allowed_symbols: ["_"] file_length: - error: 10000 - warning: 3000 + error: 100000 + warning: 10000 cyclomatic_complexity: warning: 50 error: 100 diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index ea97ff8..55c6591 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -45,11 +45,11 @@ struct GlobalState: Codable { /// nil indicates a default should be used, aka what is in settings. struct LocalState: Codable { - var progress: ProgressIndices = ProgressIndices() + var progress: ProgressIndices = .init() var mode: PageTurnMode? var backgroundColor: String? var aniListTracking: Bool = false - var scrollPos: CGPoint = CGPoint() + var scrollPos: CGPoint = .init() } struct Settings: Codable { @@ -1984,13 +1984,17 @@ class ComicMetadataViewController: UITableViewController { switch indexPath.row { case 0: cell.textLabel?.text = "Title"; cell.detailTextLabel?.text = comic.title case 1: cell.textLabel?.text = "AniList ID"; cell.detailTextLabel?.text = anId - case 2: cell.textLabel?.text = "Format"; cell.detailTextLabel?.text = "\(comic.format)" + case 2: cell.textLabel?.text = "Format"; cell.detailTextLabel?.text = comic.format != nil + ? "\(comic.format!)" : "" case 3: cell.textLabel?.text = "Original Language"; cell.detailTextLabel?.text = comic.originalLanguage case 4: cell.textLabel?.text = "Publication Demographic" - cell.detailTextLabel?.text = "\(comic.publicationDemographic)" + cell.detailTextLabel?.text = comic.publicationDemographic != nil + ? "\(comic.publicationDemographic!)" : "" case 5: cell.textLabel?.text = "Country of Origin"; cell.detailTextLabel?.text = comic.countryOfOrigin - case 6: cell.textLabel?.text = "Status"; cell.detailTextLabel?.text = "\(comic.status)" - case 7: cell.textLabel?.text = "Content Rating"; cell.detailTextLabel?.text = "\(comic.contentRating)" + case 6: cell.textLabel?.text = "Status"; cell.detailTextLabel?.text = comic.status != nil + ? "\(comic.status!)" : "" + case 7: cell.textLabel?.text = "Content Rating"; cell.detailTextLabel?.text = comic.contentRating != nil + ? "\(comic.contentRating!)" : "" case 8: cell.textLabel?.text = "First Release" cell.detailTextLabel?.text = "\(dateToString(comic.startReleaseDate))" case 9: cell.textLabel?.text = "Last Release" @@ -2365,7 +2369,7 @@ extension ViewController { children: aniListOptionItems, destructive: false, available: { [self] in - return comics[indexPath.item].metadata.anilistId != nil && aniListToken != nil || true + return comics[indexPath.item].metadata.anilistId != nil && aniListToken != nil }, action: { [weak self] in self?.dismiss(animated: false) @@ -2420,6 +2424,16 @@ extension ViewController { self.state[self.currentPath.lastPathComponent]!.aniListTracking = v self.saveLocalState() } + if action == .fetchMetadata { + if let id = comics[indexPath.item].metadata.anilistId { + AniListClient.fetchMetadata(id: UInt(id)) { result in + switch result { + case let .success(m): self.rebuildMetadata(m, indexPath: indexPath) + case .failure: break + } + } + } + } self.dismiss(animated: false) } ) @@ -2468,6 +2482,123 @@ extension ViewController { } } } + + private func rebuildMetadata(_ m: MediaMetadata, indexPath: IndexPath) { + let alert = UIAlertController( + title: "Failed to rebuild metadata", + message: "This is an alert.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + let endDate: Date! + if let d = m.endDate, let year = d.year, let month = d.month, let day = d.day { + endDate = Date(year: UInt16(year), month: UInt8(month), day: UInt8(day)) + } else { endDate = Date(year: 0, month: 0, day: 0) } + let startDate: Date! + if let d = m.startDate, let year = d.year, let month = d.month, let day = d.day { + startDate = Date(year: UInt16(year), month: UInt8(month), day: UInt8(day)) + } else { startDate = Date(year: 0, month: 0, day: 0) } + + let nSource = Source(from: m.source) + let nFormat = Format(from: m.format) + let nStatus = Status(from: m.status) + let pM = comics[indexPath.item].metadata + var volumes: [Offset] = [] + var currentChapters: [Offset] = [] + var currentImages: [Offset] = [] + var builder = FlatBufferBuilder(initialSize: 1024) + for volume in pM.volumes { + for chapter in volume.chapters { + for image in chapter.images { + let m_filename = builder.create(string: image.filename) + let imageMetadata = ImageMetadata.createImageMetadata( + &builder, + doublePage: image.doublePage, + filenameOffset: m_filename, + firstPage: image.firstPage, + size: image.size + ) + currentImages.append(imageMetadata) + } + let chapterName = builder.create(string: chapter.name) + let imagesOffset = builder.createVector(ofOffsets: currentImages) + let chapterMetadata = ChapterMetadata.createChapterMetadata( + &builder, + chapter: chapter.chapter, + nameOffset: chapterName, + imagesVectorOffset: imagesOffset + ) + currentChapters.append(chapterMetadata) + currentImages = [] + } + let volumeTitle = builder.create(string: volume.title) + let volumeURLOffset = builder.create(string: volume.archive) + let chaptersOffset = builder.createVector(ofOffsets: currentChapters) + let volumeMetadata = VolumeMetadata.createVolumeMetadata( + &builder, + volume: volume.volume, + titleOffset: volumeTitle, + archiveOffset: volumeURLOffset, + chaptersVectorOffset: chaptersOffset + ) + + volumes.append(volumeMetadata) + currentChapters = [] + } + + let title = builder.create(string: m.title?.english) + let originalLanguage = builder.create(string: pM.originalLanguage) + let countryOfOrigin = builder.create(string: pM.countryOfOrigin) + let description = builder.create(string: m.description) + let tags = builder.createVector(ofOffsets: []) + let volumeOffsets = builder.createVector(ofOffsets: volumes) + let metadata = Metadata.createMetadata( + &builder, + titleOffset: title, + format: nFormat, + originalLanguageOffset: originalLanguage, + countryOfOriginOffset: countryOfOrigin, + publicationDemographic: pM.publicationDemographic, + status: nStatus, + contentRating: pM.contentRating, + source: nSource, + startReleaseDate: startDate ?? Date(year: 0, month: 0, day: 0), + endReleaseDate: endDate ?? Date(year: 0, month: 0, day: 0), + tagsVectorOffset: tags, + descriptionOffset: description, + anilistId: pM.anilistId, + volumesVectorOffset: volumeOffsets, + imageCount: pM.imageCount, + ) + + builder.finish(offset: metadata) + do { + try builder.data.write(to: currentPath.appendingPathComponent(metadataFilename)) + } catch { + alert.message = String(format: "Failed to write metadata file at: \(metadataFilename)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + } + let metadataPath = currentPath.appendingPathComponent(metadataFilename) + guard let data = try? Data(contentsOf: metadataPath) else { + alert.message = String(format: "Failed to read metadata file at: \(metadataPath)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + return + } + var byteBuffer = ByteBuffer(data: data) + guard let newMetadata: Metadata = try? getCheckedRoot(byteBuffer: &byteBuffer) else { + alert.message = String(format: "Failed to load flatbuffer file at: \(metadataPath)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + return + } + comics[indexPath.item].metadata = newMetadata + } } extension ViewController: UIPopoverPresentationControllerDelegate { @@ -2545,6 +2676,32 @@ struct AniListResponse: Decodable { let data: T } +struct MediaMetadataData: Decodable { + // swiftlint:disable identifier_name + let Media: MediaMetadata + // swiftlint:enable identifier_name +} + +struct MediaDate: Decodable { + let year: Int? + let month: Int? + let day: Int? +} + +struct MediaMetadata: Decodable { + let description: String? + let endDate: MediaDate? + let startDate: MediaDate? + let format: String? + let title: MediaTitle? + let source: String? + let status: String? +} + +struct MediaTitle: Decodable { + let english: String? +} + struct ViewerData: Decodable { // swiftlint:disable identifier_name let Viewer: Viewer @@ -2670,6 +2827,96 @@ enum AniListClient { } } + static func fetchMetadata( + id: UInt, + completion: @escaping (Result) -> Void + ) { + let query = """ + query($mediaId: Int = \(id)) { + Media(id: $mediaId) { + description + endDate { + day + month + year + } + format + startDate { + day + month + year + } + title { + english + } + source + status + } + } + """ + fetch( + query: query + ) { (result: Result, Error>) in + switch result { + case let .success(m): + let metadata = m.data.Media + completion(.success(metadata)) + case let .failure(error): + completion(.failure(error)) + print("Error: \(error)") + } + } + } + + static func fetch( + query: String, + completion: @escaping (Result, Error>) -> Void + ) { + var request = URLRequest(url: endpoint) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + do { + request.httpBody = try JSONEncoder().encode(GraphQLBody(query: query)) + } catch { + completion(.failure(error)) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let http = response as? HTTPURLResponse else { + completion(.failure(AniListError.badResponse)) + return + } + + guard let data = data else { + completion(.failure(AniListError.noData)) + return + } + + guard (200 ... 299).contains(http.statusCode) else { + let body = String(data: data, encoding: .utf8) ?? "" + completion(.failure(AniListError.requestFailed(status: http.statusCode, body: body))) + return + } + + do { + let decoded = try JSONDecoder().decode(AniListResponse.self, from: data) + completion(.success(decoded)) + } catch { + completion(.failure(error)) + } + } + + task.resume() + } + static func fetchMediaStatus( accessToken: String, id: UInt, @@ -2824,3 +3071,65 @@ enum AniListClient { task.resume() } } + +extension Source { + init?(from string: String?) { + guard let string = string else { return nil } + guard let source = Source(from: string) else { return nil } + self = source + } + init?(from string: String) { + switch string.lowercased() { + case "ORIGINAL": self = .original + case "MANGA": self = .manga + case "LIGHT_NOVEL": self = .lightnovel + case "VISUAL_NOVEL": self = .visualnovel + case "VIDEO_GAME": self = .videogame + case "OTHER": self = .other + case "NOVEL": self = .novel + case "DOUJINSHI": self = .doujinshi + case "ANIME": self = .anime + case "WEB_NOVEL": self = .webnovel + case "LIVE_ACTION": self = .liveaction + case "GAME": self = .game + case "COMIC": self = .comic + case "MULTIMEDIA_PROJECT": self = .multimediaproject + case "PICTUREBOOK": self = .picturebook + default: return nil + } + } +} + +extension Format { + init?(from string: String?) { + guard let string = string else { return nil } + guard let format = Format(from: string) else { return nil } + self = format + } + init?(from string: String) { + switch string { + case "MANGA": self = .manga + case "NOVEL": self = .novel + case "ONE_SHOT": self = .oneshot + default: return nil + } + } +} + +extension Status { + init?(from string: String?) { + guard let string = string else { return nil } + guard let status = Status(from: string) else { return nil } + self = status + } + init?(from string: String) { + switch string { + case "FINISHED": self = .finished + case "RELEASING": self = .releasing + case "HIATUS": self = .hiatus + case "NOT_YET_RELEASED": self = .notyetreleased + case "CANCELLED": self = .cancelled + default: return nil + } + } +} diff --git a/ImageViewer/metadata.fbs b/ImageViewer/metadata.fbs index c3ef1ac..71c3692 100644 --- a/ImageViewer/metadata.fbs +++ b/ImageViewer/metadata.fbs @@ -49,13 +49,13 @@ table VolumeMetadata { table Metadata { title:string (required); - format:Format; + format:Format = null; original_language:string; country_of_origin:string; - publication_demographic:PublicationDemographic; - status:Status; - content_rating:ContentRating; - source:Source; + publication_demographic:PublicationDemographic = null; + status:Status = null; + content_rating:ContentRating = null; + source:Source = null; start_release_date:Date; end_release_date:Date; tags:[Tag]; diff --git a/ImageViewer/metadata_generated.swift b/ImageViewer/metadata_generated.swift index de4c597..237ebf0 100644 --- a/ImageViewer/metadata_generated.swift +++ b/ImageViewer/metadata_generated.swift @@ -455,15 +455,15 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia public var title: String! { let o = _accessor.offset(VT.title); return _accessor.string(at: o) } public var titleSegmentArray: [UInt8]! { return _accessor.getVector(at: VT.title) } - public var format: Format { let o = _accessor.offset(VT.format); return o == 0 ? .manga : Format(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .manga } + public var format: Format? { let o = _accessor.offset(VT.format); return o == 0 ? nil : Format(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } public var originalLanguage: String? { let o = _accessor.offset(VT.originalLanguage); return o == 0 ? nil : _accessor.string(at: o) } public var originalLanguageSegmentArray: [UInt8]? { return _accessor.getVector(at: VT.originalLanguage) } public var countryOfOrigin: String? { let o = _accessor.offset(VT.countryOfOrigin); return o == 0 ? nil : _accessor.string(at: o) } public var countryOfOriginSegmentArray: [UInt8]? { return _accessor.getVector(at: VT.countryOfOrigin) } - public var publicationDemographic: PublicationDemographic { let o = _accessor.offset(VT.publicationDemographic); return o == 0 ? .shounen : PublicationDemographic(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .shounen } - public var status: Status { let o = _accessor.offset(VT.status); return o == 0 ? .finished : Status(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .finished } - public var contentRating: ContentRating { let o = _accessor.offset(VT.contentRating); return o == 0 ? .safe : ContentRating(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .safe } - public var source: Source { let o = _accessor.offset(VT.source); return o == 0 ? .original : Source(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .original } + public var publicationDemographic: PublicationDemographic? { let o = _accessor.offset(VT.publicationDemographic); return o == 0 ? nil : PublicationDemographic(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var status: Status? { let o = _accessor.offset(VT.status); return o == 0 ? nil : Status(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var contentRating: ContentRating? { let o = _accessor.offset(VT.contentRating); return o == 0 ? nil : ContentRating(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var source: Source? { let o = _accessor.offset(VT.source); return o == 0 ? nil : Source(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } public var startReleaseDate: Date? { let o = _accessor.offset(VT.startReleaseDate); return o == 0 ? nil : _accessor.readBuffer(of: Date.self, at: o) } public var mutableStartReleaseDate: Date_Mutable? { let o = _accessor.offset(VT.startReleaseDate); return o == 0 ? nil : Date_Mutable(_accessor.bb, o: o + _accessor.position) } public var endReleaseDate: Date? { let o = _accessor.offset(VT.endReleaseDate); return o == 0 ? nil : _accessor.readBuffer(of: Date.self, at: o) } @@ -476,13 +476,13 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia public var imageCount: UInt32 { let o = _accessor.offset(VT.imageCount); return o == 0 ? 0 : _accessor.readBuffer(of: UInt32.self, at: o) } public static func startMetadata(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 15) } public static func add(title: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: title, at: VT.title) } - public static func add(format: Format, _ fbb: inout FlatBufferBuilder) { fbb.add(element: format.rawValue, def: 0, at: VT.format) } + public static func add(format: Format?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: format?.rawValue, at: VT.format) } public static func add(originalLanguage: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: originalLanguage, at: VT.originalLanguage) } public static func add(countryOfOrigin: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: countryOfOrigin, at: VT.countryOfOrigin) } - public static func add(publicationDemographic: PublicationDemographic, _ fbb: inout FlatBufferBuilder) { fbb.add(element: publicationDemographic.rawValue, def: 0, at: VT.publicationDemographic) } - public static func add(status: Status, _ fbb: inout FlatBufferBuilder) { fbb.add(element: status.rawValue, def: 0, at: VT.status) } - public static func add(contentRating: ContentRating, _ fbb: inout FlatBufferBuilder) { fbb.add(element: contentRating.rawValue, def: 0, at: VT.contentRating) } - public static func add(source: Source, _ fbb: inout FlatBufferBuilder) { fbb.add(element: source.rawValue, def: 0, at: VT.source) } + public static func add(publicationDemographic: PublicationDemographic?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: publicationDemographic?.rawValue, at: VT.publicationDemographic) } + public static func add(status: Status?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: status?.rawValue, at: VT.status) } + public static func add(contentRating: ContentRating?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: contentRating?.rawValue, at: VT.contentRating) } + public static func add(source: Source?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: source?.rawValue, at: VT.source) } public static func add(startReleaseDate: Date?, _ fbb: inout FlatBufferBuilder) { guard let startReleaseDate = startReleaseDate else { return }; fbb.create(struct: startReleaseDate, position: VT.startReleaseDate) } public static func add(endReleaseDate: Date?, _ fbb: inout FlatBufferBuilder) { guard let endReleaseDate = endReleaseDate else { return }; fbb.create(struct: endReleaseDate, position: VT.endReleaseDate) } public static func addVectorOf(tags: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: tags, at: VT.tags) } @@ -494,13 +494,13 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia public static func createMetadata( _ fbb: inout FlatBufferBuilder, titleOffset title: Offset, - format: Format = .manga, + format: Format? = nil, originalLanguageOffset originalLanguage: Offset = Offset(), countryOfOriginOffset countryOfOrigin: Offset = Offset(), - publicationDemographic: PublicationDemographic = .shounen, - status: Status = .finished, - contentRating: ContentRating = .safe, - source: Source = .original, + publicationDemographic: PublicationDemographic? = nil, + status: Status? = nil, + contentRating: ContentRating? = nil, + source: Source? = nil, startReleaseDate: Date? = nil, endReleaseDate: Date? = nil, tagsVectorOffset tags: Offset = Offset(),