Skip to content
This repository was archived by the owner on Dec 28, 2024. It is now read-only.

EnkaAPI // +Sputnik, handling data fetch process. #171

Merged
merged 2 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Packages/HBEnkaAPI/Sources/HBEnkaAPI/DefaultsKeys_EnkaImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,32 @@ extension Defaults.Keys {
default: EnkaHSR.EnkaDB()!,
suite: .enkaSuite
)
public static let lastEnkaQueryDate = Key<[String: Date]>(
"lastEnkaDBDataCheckDate",
default: [:],
suite: .enkaSuite
)
public static let queriedEnkaProfiles = Key<[String: EnkaHSR.QueryRelated.DetailInfo]>(
"lastEnkaDBDataCheckDate",
default: [:],
suite: .enkaSuite
)
public static let defaultDBQueryHost = Key<EnkaHSR.HostType>(
"defaultDBQueryHost",
default: EnkaHSR.HostType.enkaGlobal,
suite: .enkaSuite
)
}
#endif

// MARK: - EnkaHSR.EnkaDB + _DefaultsSerializable

extension EnkaHSR.EnkaDB: _DefaultsSerializable {}

// MARK: - EnkaHSR.QueryRelated.DetailInfo + _DefaultsSerializable

extension EnkaHSR.QueryRelated.DetailInfo: _DefaultsSerializable {}

// MARK: - EnkaHSR.HostType + _DefaultsSerializable

extension EnkaHSR.HostType: _DefaultsSerializable {}
46 changes: 36 additions & 10 deletions Packages/HBEnkaAPI/Sources/HBEnkaAPI/EnkaDBModels/EnkaDB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension EnkaHSR {
// MARK: Lifecycle

public init(
locTag: String,
locTag: String = Locale.langCodeForEnkaAPI,
locTable: EnkaHSR.DBModels.LocTable,
profileAvatars: EnkaHSR.DBModels.ProfileAvatarDict,
characters: EnkaHSR.DBModels.CharacterDict,
Expand All @@ -33,29 +33,55 @@ extension EnkaHSR {
self.weapons = weapons
}

public init?(
locTag: String = Locale.langCodeForEnkaAPI,
locTables: EnkaHSR.DBModels.RawLocTables,
profileAvatars: EnkaHSR.DBModels.ProfileAvatarDict,
characters: EnkaHSR.DBModels.CharacterDict,
meta: EnkaHSR.DBModels.Meta,
skillRanks: EnkaHSR.DBModels.SkillRanksDict,
artifacts: EnkaHSR.DBModels.ArtifactsDict,
skills: EnkaHSR.DBModels.SkillsDict,
skillTrees: EnkaHSR.DBModels.SkillTreesDict,
weapons: EnkaHSR.DBModels.WeaponsDict
) {
let langTag = Self.sanitizeLangTag(locTag)
self.langTag = langTag
guard let langTable = locTables[langTag] else { return nil }
self.locTable = langTable
self.profileAvatars = profileAvatars
self.characters = characters
self.meta = meta
self.skillRanks = skillRanks
self.artifacts = artifacts
self.skills = skills
self.skillTrees = skillTrees
self.weapons = weapons
}

/// Use bundled resources to initiate an EnkaDB instance.
public init?(locTag: String = Locale.langCodeForEnkaAPI) {
do {
let locTables = try EnkaHSR.JSONTypes.locTable.bundledJSONData
let locTables = try EnkaHSR.JSONType.locTable.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.RawLocTables.self)
guard let locTableSpecified = locTables[locTag] else { return nil }
self.langTag = Self.sanitizeLangTag(locTag)
self.locTable = locTableSpecified
self.profileAvatars = try EnkaHSR.JSONTypes.profileAvatarIcons.bundledJSONData
self.profileAvatars = try EnkaHSR.JSONType.profileAvatarIcons.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.ProfileAvatarDict.self)
self.characters = try EnkaHSR.JSONTypes.characters.bundledJSONData
self.characters = try EnkaHSR.JSONType.characters.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.CharacterDict.self)
self.meta = try EnkaHSR.JSONTypes.metadata.bundledJSONData
self.meta = try EnkaHSR.JSONType.metadata.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.Meta.self)
self.skillRanks = try EnkaHSR.JSONTypes.skillRanks.bundledJSONData
self.skillRanks = try EnkaHSR.JSONType.skillRanks.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.SkillRanksDict.self)
self.artifacts = try EnkaHSR.JSONTypes.artifacts.bundledJSONData
self.artifacts = try EnkaHSR.JSONType.artifacts.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.ArtifactsDict.self)
self.skills = try EnkaHSR.JSONTypes.skills.bundledJSONData
self.skills = try EnkaHSR.JSONType.skills.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.SkillsDict.self)
self.skillTrees = try EnkaHSR.JSONTypes.skillTrees.bundledJSONData
self.skillTrees = try EnkaHSR.JSONType.skillTrees.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.SkillTreesDict.self)
self.weapons = try EnkaHSR.JSONTypes.weapons.bundledJSONData
self.weapons = try EnkaHSR.JSONType.weapons.bundledJSONData
.assertedParseAs(EnkaHSR.DBModels.WeaponsDict.self)
} catch {
print("\n\(error)\n")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// (c) 2023 and onwards Pizza Studio (GPL v3.0 License).
// ====================
// This code is released under the GPL v3.0 License (SPDX-License-Identifier: GPL-3.0)

import Defaults
import Foundation

// MARK: - Enka Sputnik

#if !os(watchOS)
extension EnkaHSR {
public class Sputnik {
// MARK: Lifecycle

private init() {}

// MARK: Public

public static let shared = EnkaHSR.Sputnik()

public func getEnkaDB() async throws -> EnkaHSR.EnkaDB {
// Read charloc and charmap from UserDefault
let storedDB = Defaults[.enkaDBData]

let enkaDataExpired = Calendar.current.date(
byAdding: .hour,
value: 2,
to: Defaults[.lastEnkaDBDataCheckDate]
)! < Date()

let needUpdate = enkaDataExpired

if !needUpdate {
return storedDB
} else {
let host = Defaults[.defaultDBQueryHost]
async let newDB = try EnkaHSR.EnkaDB(
locTables: Self.fetchEnkaDBData(
from: host, type: .locTable,
decodingTo: EnkaHSR.DBModels.RawLocTables.self
),
profileAvatars: Self.fetchEnkaDBData(
from: host, type: .profileAvatarIcons,
decodingTo: EnkaHSR.DBModels.ProfileAvatarDict.self
),
characters: Self.fetchEnkaDBData(
from: host, type: .characters,
decodingTo: EnkaHSR.DBModels.CharacterDict.self
),
meta: Self.fetchEnkaDBData(
from: host, type: .metadata,
decodingTo: EnkaHSR.DBModels.Meta.self
),
skillRanks: Self.fetchEnkaDBData(
from: host, type: .skillRanks,
decodingTo: EnkaHSR.DBModels.SkillRanksDict.self
),
artifacts: Self.fetchEnkaDBData(
from: host, type: .artifacts,
decodingTo: EnkaHSR.DBModels.ArtifactsDict.self
),
skills: Self.fetchEnkaDBData(
from: host, type: .skills,
decodingTo: EnkaHSR.DBModels.SkillsDict.self
),
skillTrees: Self.fetchEnkaDBData(
from: host, type: .skillTrees,
decodingTo: EnkaHSR.DBModels.SkillTreesDict.self
),
weapons: Self.fetchEnkaDBData(
from: host, type: .weapons,
decodingTo: EnkaHSR.DBModels.WeaponsDict.self
)
)
guard let newDB = try await newDB else {
throw EnkaHSR.QueryRelated.Exception
.enkaDBOnlineFetchFailure(details: "Language Tag Matching Error.")
}

Defaults[.enkaDBData] = newDB

Defaults[.lastEnkaDBDataCheckDate] = Date()

return newDB
}
}
}
}

// MARK: - Fetch Errors

extension EnkaHSR.Sputnik {
public enum DataFetchError: Error {
case charMapInvalid
case charLocInvalid
}
}

extension EnkaHSR.Sputnik {
/// 从 Enka Networks 获取游戏内玩家展柜资讯。
/// - Parameters:
/// - uid: 用户UID
/// - completion: 资料
public static func fetchEnkaProfile(
_ uid: String,
dateWhenNextRefreshable: Date? = nil
) async throws
-> EnkaHSR.QueryRelated.QueriedProfile {
if let date = dateWhenNextRefreshable, date > Date() {
print(
"PLAYER DETAIL FETCH 刷新太快了,请在\(date.timeIntervalSinceReferenceDate - Date().timeIntervalSinceReferenceDate)秒后刷新"
)
throw EnkaHSR.QueryRelated.Exception.refreshTooFast(dateWhenRefreshable: date)
} else {
let enkaOfficial = EnkaHSR.HostType.profileQueryURLHeader + uid
// swiftlint:disable force_unwrapping
let url = URL(string: enkaOfficial)!
// swiftlint:enable force_unwrapping
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let requestResult = try JSONDecoder().decode(
EnkaHSR.QueryRelated.QueriedProfile.self,
from: data
)
return requestResult
}
}

/// 从 EnkaNetwork 获取具体单笔 EnkaDB 子类型资料
/// - Parameters:
/// - completion: 资料
static func fetchEnkaDBData<T: Codable>(
from serverType: EnkaHSR.HostType = .enkaGlobal,
type dataType: EnkaHSR.JSONType,
decodingTo: T.Type
) async throws
-> T {
var dataToParse = Data([])
do {
let (data, _) = try await URLSession.shared.data(
for: URLRequest(url: serverType.enkaDBSourceURL(type: dataType))
)
dataToParse = data
} catch {
if serverType != .enkaGlobal {
let (data, _) = try await URLSession.shared.data(
for: URLRequest(url: EnkaHSR.HostType.enkaGlobal.enkaDBSourceURL(type: dataType))
)
dataToParse = data
} else {
print(error.localizedDescription)
throw error
}
}
do {
let requestResult = try JSONDecoder().decode(T.self, from: dataToParse)
return requestResult
} catch {
if dataToParse.isEmpty {
print("// DEBUG: Data Fetch Failed: \(dataType.rawValue).json")
} else {
print("// DEBUG: Data Parse Failed: \(dataType.rawValue).json")
}
print(error.localizedDescription)
throw error
}
}
}

#endif
47 changes: 44 additions & 3 deletions Packages/HBEnkaAPI/Sources/HBEnkaAPI/HBEnkaAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ extension EnkaHSR {
public typealias LifePath = DBModels.LifePath
}

// MARK: - EnkaHSR.JSONTypes
// MARK: - EnkaHSR.JSONType

extension EnkaHSR {
public enum JSONTypes: String, CaseIterable {
public enum JSONType: String, CaseIterable {
case profileAvatarIcons = "honker_avatars" // Player Account Profile Picture
case characters = "honker_characters"
case metadata = "honker_meta"
Expand Down Expand Up @@ -84,7 +84,7 @@ extension Data? {
}
}

// EnkaAPI LangCode
// MARK: - EnkaAPI LangCode

extension Locale {
public static var langCodeForEnkaAPI: String {
Expand All @@ -104,3 +104,44 @@ extension Locale {
return languageCode
}
}

extension EnkaHSR {
public enum HostType: Int, Codable, RawRepresentable, Hashable {
case mainlandChina = 0
case enkaGlobal = 1

// MARK: Public

public static var profileQueryURLHeader: String {
// MicroGG 目前不支持星穹铁道的资料查询。
"https://enka.network/api/hsr/uid/"
}

public var enkaDBSourceURLHeader: String {
switch self {
case .mainlandChina: return "https://gitcode.net/SHIKISUEN/Enka-API-docs/-/raw/master/"
case .enkaGlobal: return "https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/"
}
}

public var profileQueryURLHeader: String {
Self.profileQueryURLHeader
}

public func enkaDBSourceURL(type: EnkaHSR.JSONType) -> URL {
// swiftlint:disable force_unwrapping
let urlStr = "\(enkaDBSourceURLHeader)store/hsr/\(type.rawValue).json"
return .init(string: urlStr)!
// swiftlint:enable force_unwrapping
}
}
}

extension EnkaHSR.QueryRelated {
public enum Exception: Error {
case enkaDBOnlineFetchFailure(details: String)
case enkaProfileQueryFailure(message: String)
case refreshTooFast(dateWhenRefreshable: Date)
case dataInvalid
}
}
Loading
Loading