feat: anilist metadata fetch

This commit is contained in:
2026-08-04 01:48:03 +02:00
parent 9a34d4e952
commit c3b04e0a6b
4 changed files with 338 additions and 29 deletions
+2 -2
View File
@@ -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
+316 -7
View File
@@ -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<T: Decodable>: 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<MediaMetadata, Error>) -> 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<AniListResponse<MediaMetadataData>, 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<T: Decodable>(
query: String,
completion: @escaping (Result<AniListResponse<T>, 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) ?? "<no body>"
completion(.failure(AniListError.requestFailed(status: http.statusCode, body: body)))
return
}
do {
let decoded = try JSONDecoder().decode(AniListResponse<T>.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
}
}
}
+5 -5
View File
@@ -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];
+15 -15
View File
@@ -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(),