Maybe work now

This commit is contained in:
scigward 2025-08-17 01:34:10 +03:00
parent 5755bb961a
commit b8792dd06f
2 changed files with 114 additions and 48 deletions

View file

@ -22,6 +22,7 @@ struct EpisodeCell: View {
let showPosterURL: String?
let tmdbID: Int?
let seasonNumber: Int?
let malID: Int? // MAL ID for the series
let isMultiSelectMode: Bool
let isSelected: Bool
@ -53,11 +54,25 @@ 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]
}
private struct JikanEpisode: Decodable {
let mal_id: Int
let filler: Bool
}
init(
episodeIndex: Int,
@ -76,7 +91,8 @@ struct EpisodeCell: View {
onTap: @escaping (String) -> Void,
onMarkAllPrevious: @escaping () -> Void,
tmdbID: Int? = nil,
seasonNumber: Int? = nil
seasonNumber: Int? = nil,
malID: Int? = nil // MAL ID parameter
) {
self.episodeIndex = episodeIndex
self.episode = episode
@ -94,6 +110,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 +555,8 @@ private extension EpisodeCell {
} else {
fetchAnimeEpisodeDetails()
}
// Fetch filler info in parallel with episode details
fetchFillerInfo()
// Fetch filler info using Jikan API
fetchJikanFillerInfo()
}
}
@ -959,69 +976,108 @@ 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
// Jikan Filler Implementation
private func fetchJikanFillerInfo() {
guard let malID = malID else {
Logger.shared.log("MAL ID not available for filler info", type: "Debug")
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 {
Logger.shared.log("Using cached filler info for MAL ID: \(malID)", type: "Debug")
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 {
Logger.shared.log("Fetch already in progress for MAL ID: \(malID)", type: "Debug")
return
}
Logger.shared.log("Fetching filler info for MAL ID: \(malID)", type: "Debug")
// Fetch all pages
fetchAllJikanPages(malID: malID) { episodes in
// Update cache
if let episodes = episodes {
Logger.shared.log("Successfully fetched filler info for MAL ID: \(malID)", type: "Debug")
Self.jikanCacheQueue.async(flags: .barrier) {
Self.jikanCache[malID] = (Date(), episodes)
}
// Update UI
DispatchQueue.main.async {
self.isFiller = episodesSet.contains(epNum)
self.updateFillerStatus(episodes: episodes)
}
} else {
Logger.shared.log("Failed to fetch filler info for MAL ID: \(malID)", type: "Error")
}
// 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 url = URL(string: "https://api.jikan.moe/v4/anime/\(malID)/episodes")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
Logger.shared.log("Jikan API request failed: \(error?.localizedDescription ?? "Unknown error")", type: "Error")
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))
}
episodesSet = Set(numbers)
}
let response = try JSONDecoder().decode(JikanResponse.self, from: data)
completion(response.data)
} catch {
print("Filler parse error: \(error)")
Logger.shared.log("Failed to parse Jikan response: \(error)", type: "Error")
completion(nil)
}
}.resume()
}
private func updateFillerStatus(episodes: [JikanEpisode]) {
let episodeNumber = episodeID + 1
guard episodeNumber > 0, episodeNumber <= episodes.count else {
Logger.shared.log("Episode \(episodeNumber) not found in Jikan response", type: "Debug")
isFiller = false // Defensive: always set to false if out of bounds
return
}
let jikanEpisode = episodes[episodeNumber - 1]
isFiller = jikanEpisode.filler
if jikanEpisode.filler {
Logger.shared.log("Marking episode \(episodeNumber) as filler", type: "Debug")
} else {
Logger.shared.log("Episode \(episodeNumber) is not filler", type: "Debug")
}
}
func handleFetchFailure(error: Error) {
Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error")

View file

@ -9,7 +9,7 @@ import SwiftUI
struct AnilistMatchPopupView: View {
let seriesTitle: String
let onSelect: (Int, String) -> Void
let onSelect: (Int, String, Int?) -> Void // id, title, malId
@State private var results: [[String: Any]] = []
@State private var isLoading = true
@ -54,7 +54,9 @@ struct AnilistMatchPopupView: View {
Button(action: {
if let id = result["id"] as? Int {
let title = result["title"] as? String ?? seriesTitle
onSelect(id, title)
let malId = result["mal_id"] as? Int
Logger.shared.log("Selected AniList ID: \(id), MAL ID: \(malId?.description ?? "nil")", type: "AnilistMatch")
onSelect(id, title, malId)
dismiss()
}
}) {
@ -86,6 +88,11 @@ struct AnilistMatchPopupView: View {
.font(.caption)
.foregroundStyle(.secondary)
}
if let malId = result["mal_id"] as? Int {
Text("MAL ID: \(malId)")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Spacer()
@ -153,7 +160,8 @@ struct AnilistMatchPopupView: View {
Button("Cancel", role: .cancel) { }
Button("Save") {
if let idInt = Int(manualIDText.trimmingCharacters(in: .whitespaces)) {
onSelect(idInt, seriesTitle)
Logger.shared.log("Manual AniList ID: \(idInt), MAL ID: nil", type: "AnilistMatch")
onSelect(idInt, seriesTitle, nil)
dismiss()
}
}
@ -170,6 +178,7 @@ struct AnilistMatchPopupView: View {
Page(page: 1, perPage: 6) {
media(search: "\(seriesTitle)", type: ANIME) {
id
idMal
title {
romaji
english
@ -204,6 +213,7 @@ struct AnilistMatchPopupView: View {
let cover = (media["coverImage"] as? [String: Any])?["large"] as? String
return [
"id": media["id"] ?? 0,
"mal_id": media["idMal"] as? Int, // <-- MAL ID
"title": titleInfo?["romaji"] ?? "Unknown",
"title_english": titleInfo?["english"] as Any,
"cover": cover as Any
@ -212,4 +222,4 @@ struct AnilistMatchPopupView: View {
}
}.resume()
}
}
}