anilist logic improved basically (#189)

* removed double bs for id telling

* improved anilist logic, single episode anilist sync, anilist sync also avaiaiable with tmdb as provider, tmdb posters avaiabale with anilist as provider
This commit is contained in:
Seiike 2025-06-14 09:16:00 +02:00 committed by GitHub
parent b03ff287fe
commit 2026e43630
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 183 additions and 56 deletions

View file

@ -195,6 +195,50 @@ class AniListMutation {
}.resume()
}
func fetchCoverImage(
animeId: Int,
completion: @escaping (Result<String, Error>) -> Void
) {
let query = """
query ($id: Int) {
Media(id: $id, type: ANIME) {
coverImage { large }
}
}
"""
let variables = ["id": animeId]
let body: [String: Any] = ["query": query, "variables": variables]
guard let url = URL(string: "https://graphql.anilist.co"),
let httpBody = try? JSONSerialization.data(withJSONObject: body)
else {
completion(.failure(NSError(domain: "AniList", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL or payload"])))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
return completion(.failure(error))
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataDict = json["data"] as? [String: Any],
let media = dataDict["Media"] as? [String: Any],
let cover = media["coverImage"] as? [String: Any],
let imageUrl = cover["large"] as? String
else {
return completion(.failure(NSError(domain: "AniList", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed response"])))
}
completion(.success(imageUrl))
}
.resume()
}
private struct AniListMediaResponse: Decodable {
struct DataField: Decodable {
struct Media: Decodable { let idMal: Int? }

View file

@ -111,6 +111,11 @@ struct EpisodeCell: View {
.onDisappear { activeDownloadTask = nil }
.onChange(of: progress) { _ in updateProgress() }
.onChange(of: itemID) { _ in handleItemIDChange() }
.onChange(of: tmdbID) { _ in
isLoading = true
retryAttempts = 0
fetchEpisodeDetails()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
updateDownloadStatus()
@ -437,13 +442,31 @@ private extension EpisodeCell {
}
func markAsWatched() {
let userDefaults = UserDefaults.standard
let defaults = UserDefaults.standard
let totalTime = 1000.0
userDefaults.set(totalTime, forKey: "lastPlayedTime_\(episode)")
userDefaults.set(totalTime, forKey: "totalTime_\(episode)")
defaults.set(totalTime, forKey: "lastPlayedTime_\(episode)")
defaults.set(totalTime, forKey: "totalTime_\(episode)")
updateProgress()
if itemID > 0 {
let epNum = episodeID + 1
let newStatus = (epNum == totalEpisodes) ? "COMPLETED" : "CURRENT"
AniListMutation().updateAnimeProgress(
animeId: itemID,
episodeNumber: epNum,
status: newStatus
) { result in
switch result {
case .success:
Logger.shared.log("AniList sync: marked ep \(epNum) as \(newStatus)", type: "General")
case .failure(let err):
Logger.shared.log("AniList sync failed: \(err.localizedDescription)", type: "Error")
}
}
}
}
func resetProgress() {
let userDefaults = UserDefaults.standard
userDefaults.set(0.0, forKey: "lastPlayedTime_\(episode)")

View file

@ -759,6 +759,8 @@ struct MediaInfoView: View {
if savedCustomID != 0 {
itemID = savedCustomID
activeProvider = "AniList"
UserDefaults.standard.set("AniList", forKey: "metadataProviders")
} else {
fetchMetadataIDIfNeeded()
}
@ -798,29 +800,36 @@ struct MediaInfoView: View {
}
private func toggleSingleEpisodeWatchStatus() {
if let ep = episodeLinks.first {
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
guard let ep = episodeLinks.first else { return }
let lastPlayedKey = "lastPlayedTime_\(ep.href)"
let totalTimeKey = "totalTime_\(ep.href)"
let last = UserDefaults.standard.double(forKey: lastPlayedKey)
let total = UserDefaults.standard.double(forKey: totalTimeKey)
let progress = total > 0 ? last/total : 0
let watchedEp = ep.number
if progress <= 0.9 {
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(ep.href)")
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(ep.href)")
DropManager.shared.showDrop(
title: "Marked as Watched",
subtitle: "",
duration: 1.0,
icon: UIImage(systemName: "checkmark.circle.fill")
)
} else {
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(ep.href)")
UserDefaults.standard.set(0.0, forKey: "totalTime_\(ep.href)")
DropManager.shared.showDrop(
title: "Progress Reset",
subtitle: "",
duration: 1.0,
icon: UIImage(systemName: "arrow.counterclockwise")
)
if progress <= 0.9 {
UserDefaults.standard.set(99999999.0, forKey: lastPlayedKey)
UserDefaults.standard.set(99999999.0, forKey: totalTimeKey)
DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill"))
if let listID = itemID, listID > 0 {
AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: watchedEp, status: "CURRENT") { result in
switch result {
case .success:
Logger.shared.log("AniList sync: marked ep \(watchedEp) as CURRENT", type: "General")
case .failure(let err):
Logger.shared.log("AniList sync failed: \(err.localizedDescription)", type: "Error")
}
}
}
} else {
UserDefaults.standard.set(0.0, forKey: lastPlayedKey)
UserDefaults.standard.set(0.0, forKey: totalTimeKey)
DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise"))
if let listID = itemID, listID > 0 {
AniListMutation().updateAnimeProgress(animeId: listID, episodeNumber: 0, status: "CURRENT") { _ in }
}
}
}
@ -883,6 +892,8 @@ struct MediaInfoView: View {
private func handleAniListMatch(selectedID: Int) {
self.customAniListID = selectedID
self.itemID = selectedID
self.activeProvider = "AniList"
UserDefaults.standard.set("AniList", forKey: "metadataProviders")
UserDefaults.standard.set(selectedID, forKey: "custom_anilist_id_\(href)")
self.fetchDetails()
isMatchingPresented = false
@ -1181,7 +1192,40 @@ struct MediaInfoView: View {
}
}
private func fetchMetadataIDIfNeeded() {
private func fetchAniListPosterImageAndSet() {
guard let listID = itemID, listID > 0 else { return }
AniListMutation().fetchCoverImage(animeId: listID) { result in
switch result {
case .success(let urlString):
DispatchQueue.main.async {
let originalKey = "originalPoster_\(self.href)"
UserDefaults.standard.set(self.imageUrl, forKey: originalKey)
self.imageUrl = urlString
}
case .failure(let err):
Logger.shared.log("AniList poster fetch failed: \(err.localizedDescription)", type: "Error")
}
}
}
private func fetchAniListIDForSync() {
let cleaned = cleanTitle(title)
fetchItemID(byTitle: cleaned) { result in
switch result {
case .success(let id):
DispatchQueue.main.async {
if customAniListID == nil {
self.itemID = id
}
}
case .failure(let err):
Logger.shared.log("AniList syncID fetch failed: \(err.localizedDescription)", type: "Error")
}
}
}
func fetchMetadataIDIfNeeded() {
let order = metadataProvidersOrder
let cleanedTitle = cleanTitle(title)
@ -1198,8 +1242,21 @@ struct MediaInfoView: View {
self.itemID = id
self.activeProvider = "AniList"
UserDefaults.standard.set("AniList", forKey: "metadataProviders")
completion(true)
tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { tmdbId, tmdbType in
DispatchQueue.main.async {
guard let tmdbId = tmdbId, let tmdbType = tmdbType else {
completion(true)
return
}
self.tmdbID = tmdbId
self.tmdbType = tmdbType
self.fetchTMDBPosterImageAndSet()
completion(true)
}
}
}
case .failure(let error):
Logger.shared.log("Failed to fetch AniList ID for tracking: \(error)", type: "Error")
completion(false)
@ -1207,46 +1264,41 @@ struct MediaInfoView: View {
}
}
func fetchTMDB(completion: @escaping (Bool) -> Void) {
tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in
DispatchQueue.main.async {
if let id = id, let type = type {
self.tmdbID = id
self.tmdbType = type
self.activeProvider = "TMDB"
UserDefaults.standard.set("TMDB", forKey: "metadataProviders")
completion(true)
} else {
completion(false)
}
}
}
}
func tryProviders(_ index: Int) {
guard index < order.count else {
isError = true
return
}
let provider = order[index]
if provider == "AniList" {
switch provider {
case "AniList":
fetchAniList { success in
if !success {
tryProviders(index + 1)
}
}
} else if provider == "TMDB" {
fetchTMDB { success in
if !success {
tryProviders(index + 1)
case "TMDB":
tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in
DispatchQueue.main.async {
if let id = id, let type = type {
self.tmdbID = id
self.tmdbType = type
self.activeProvider = "TMDB"
UserDefaults.standard.set("TMDB", forKey: "metadataProviders")
self.fetchTMDBPosterImageAndSet()
} else {
tryProviders(index + 1)
}
}
}
} else {
default:
tryProviders(index + 1)
}
}
tryProviders(0)
fetchAniListIDForSync()
}
private func fetchItemID(byTitle title: String, completion: @escaping (Result<Int, Error>) -> Void) {

View file

@ -368,10 +368,9 @@
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
isa = PBXGroup;
children = (
1E0435F02DFCB86800FF6808 /* CustomMatching */,
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */,
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */,
);
path = MediaInfoView;
sourceTree = "<group>";
@ -609,6 +608,15 @@
path = Components;
sourceTree = "<group>";
};
1E0435F02DFCB86800FF6808 /* CustomMatching */ = {
isa = PBXGroup;
children = (
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */,
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */,
);
path = CustomMatching;
sourceTree = "<group>";
};
72443C832DC8046500A61321 /* DownloadUtils */ = {
isa = PBXGroup;
children = (