mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-06 17:50:03 +00:00
Test jikan API filler implementation
This commit is contained in:
parent
5755bb961a
commit
e2f7f92d47
1 changed files with 118 additions and 42 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue