Test jikan API filler implementation

This commit is contained in:
scigward 2025-08-16 21:41:16 +03:00 committed by GitHub
parent 5755bb961a
commit e2f7f92d47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -22,6 +22,7 @@ struct EpisodeCell: View {
let showPosterURL: String?
let tmdbID: Int?
let seasonNumber: Int?
let malID: Int? // Add MAL ID property
let isMultiSelectMode: Bool
let isSelected: Bool
@ -53,11 +54,32 @@ struct EpisodeCell: View {
@Environment(\.colorScheme) private var colorScheme
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
//Filler state & cache
// Filler state
@State private var isFiller: Bool = false
private static var fillerCache: [String: (fetchedAt: Date, episodes: Set<Int>)] = [:]
private static let fillerCacheQueue = DispatchQueue(label: "sora.filler.cache.queue", attributes: .concurrent)
private static let fillerCacheTTL: TimeInterval = 60 * 60 * 24
// Jikan API cache
private static var jikanCache: [Int: (fetchedAt: Date, episodes: [JikanEpisode])] = [:]
private static let jikanCacheQueue = DispatchQueue(label: "sora.jikan.cache.queue", attributes: .concurrent)
private static let jikanCacheTTL: TimeInterval = 60 * 60 * 24 * 7 // 1 week
private static var inProgressMALIDs: Set<Int> = []
private static let inProgressQueue = DispatchQueue(label: "sora.jikan.inprogress.queue")
// Jikan data models
private struct JikanResponse: Decodable {
let data: [JikanEpisode]
let pagination: Pagination
}
private struct Pagination: Decodable {
let last_visible_page: Int
let has_next_page: Bool
}
private struct JikanEpisode: Decodable {
let mal_id: Int
let filler: Bool
let recap: Bool
}
init(
episodeIndex: Int,
@ -76,7 +98,8 @@ struct EpisodeCell: View {
onTap: @escaping (String) -> Void,
onMarkAllPrevious: @escaping () -> Void,
tmdbID: Int? = nil,
seasonNumber: Int? = nil
seasonNumber: Int? = nil,
malID: Int? = nil // Add MAL ID parameter
) {
self.episodeIndex = episodeIndex
self.episode = episode
@ -94,6 +117,7 @@ struct EpisodeCell: View {
self.onMarkAllPrevious = onMarkAllPrevious
self.tmdbID = tmdbID
self.seasonNumber = seasonNumber
self.malID = malID // Initialize MAL ID
let isLightMode = (UserDefaults.standard.string(forKey: "selectedAppearance") == "light") ||
((UserDefaults.standard.string(forKey: "selectedAppearance") == "system") &&
@ -538,8 +562,8 @@ private extension EpisodeCell {
} else {
fetchAnimeEpisodeDetails()
}
// Fetch filler info in parallel with episode details
fetchFillerInfo()
// Fetch filler info using Jikan API
fetchJikanFillerInfo()
}
}
@ -959,69 +983,121 @@ private extension EpisodeCell {
}.resume()
}
func fetchFillerInfo() {
let raw = parentTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !raw.isEmpty else { return }
var slug = raw.lowercased()
slug = slug.replacingOccurrences(of: " ", with: "-")
slug = slug.components(separatedBy: CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted).joined()
let epNum = self.episodeID + 1
// MARK: - Jikan Filler Implementation
private func fetchJikanFillerInfo() {
guard let malID = malID else { return }
let episodeNumber = episodeID + 1
// Check cache first
var cachedEpisodes: Set<Int>? = nil
Self.fillerCacheQueue.sync {
if let entry = Self.fillerCache[slug], Date().timeIntervalSince(entry.fetchedAt) < Self.fillerCacheTTL {
var cachedEpisodes: [JikanEpisode]? = nil
Self.jikanCacheQueue.sync {
if let entry = Self.jikanCache[malID], Date().timeIntervalSince(entry.fetchedAt) < Self.jikanCacheTTL {
cachedEpisodes = entry.episodes
}
}
if let set = cachedEpisodes {
DispatchQueue.main.async {
self.isFiller = set.contains(epNum)
}
if let episodes = cachedEpisodes {
updateFillerStatus(episodes: episodes)
return
}
// Not in cache or expired, fetch from API
guard let url = URL(string: "https://sora-filler-episodes-api.jmcrafter26.workers.dev/\(slug)") else { return }
// Prevent duplicate requests
var shouldFetch = false
Self.inProgressQueue.sync {
if !Self.inProgressMALIDs.contains(malID) {
Self.inProgressMALIDs.insert(malID)
shouldFetch = true
}
}
URLSession.shared.dataTask(with: url) { data, response, error in
var episodesSet = Set<Int>()
defer {
// Cache the result (even if empty) and update UI
Self.fillerCacheQueue.async(flags: .barrier) {
Self.fillerCache[slug] = (fetchedAt: Date(), episodes: episodesSet)
if !shouldFetch { return }
// Fetch all pages
fetchAllJikanPages(malID: malID) { episodes in
// Update cache
if let episodes = episodes {
Self.jikanCacheQueue.async(flags: .barrier) {
Self.jikanCache[malID] = (Date(), episodes)
}
// Update UI
DispatchQueue.main.async {
self.isFiller = episodesSet.contains(epNum)
self.updateFillerStatus(episodes: episodes)
}
}
// Handle API errors or empty responses
// Remove from in-progress set
Self.inProgressQueue.async {
Self.inProgressMALIDs.remove(malID)
}
}
}
private func fetchAllJikanPages(malID: Int, completion: @escaping ([JikanEpisode]?) -> Void) {
let firstPageURL = URL(string: "https://api.jikan.moe/v4/anime/\(malID)/episodes?page=1")!
URLSession.shared.dataTask(with: firstPageURL) { data, response, error in
guard let data = data, error == nil else {
completion(nil)
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let fillerArray = json["fillerEpisodes"] as? [String],
!fillerArray.isEmpty {
let fillerString = fillerArray[0]
let numbers = fillerString.split(separator: ",").compactMap {
Int($0.trimmingCharacters(in: .whitespaces))
let firstPageResponse = try JSONDecoder().decode(JikanResponse.self, from: data)
var allEpisodes = firstPageResponse.data
let totalPages = firstPageResponse.pagination.last_visible_page
if totalPages <= 1 {
completion(allEpisodes)
return
}
// Fetch remaining pages
let group = DispatchGroup()
var episodesFromPages: [Int: [JikanEpisode]] = [:]
for page in 2...totalPages {
group.enter()
// Add delay to respect rate limits
DispatchQueue.global().asyncAfter(deadline: .now() + Double(page-1) * 0.3) {
let url = URL(string: "https://api.jikan.moe/v4/anime/\(malID)/episodes?page=\(page)")!
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data, error == nil else { return }
do {
let response = try JSONDecoder().decode(JikanResponse.self, from: data)
episodesFromPages[page] = response.data
} catch {
// Ignore page error
}
}.resume()
}
episodesSet = Set(numbers)
}
group.notify(queue: .global()) {
// Combine episodes in order
for page in 2...totalPages {
if let episodes = episodesFromPages[page] {
allEpisodes.append(contentsOf: episodes)
}
}
completion(allEpisodes)
}
} catch {
print("Filler parse error: \(error)")
completion(nil)
}
}.resume()
}
private func updateFillerStatus(episodes: [JikanEpisode]) {
let episodeNumber = episodeID + 1
guard episodeNumber <= episodes.count else { return }
let jikanEpisode = episodes[episodeNumber - 1]
isFiller = jikanEpisode.filler || jikanEpisode.recap
}
func handleFetchFailure(error: Error) {
Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error")