mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-19 07:32:08 +00:00
added Trakt loggin
This commit is contained in:
parent
d5abd35beb
commit
b9cac9c91b
6 changed files with 587 additions and 51 deletions
|
|
@ -64,12 +64,25 @@ struct SoraApp: App {
|
|||
return
|
||||
}
|
||||
|
||||
AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in
|
||||
if success {
|
||||
Logger.shared.log("Token exchange successful")
|
||||
} else {
|
||||
Logger.shared.log("Token exchange failed", type: "Error")
|
||||
switch url.host {
|
||||
case "anilist":
|
||||
AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in
|
||||
if success {
|
||||
Logger.shared.log("AniList token exchange successful")
|
||||
} else {
|
||||
Logger.shared.log("AniList token exchange failed", type: "Error")
|
||||
}
|
||||
}
|
||||
case "trakt":
|
||||
TraktToken.exchangeAuthorizationCodeForToken(code: code) { success in
|
||||
if success {
|
||||
Logger.shared.log("Trakt token exchange successful")
|
||||
} else {
|
||||
Logger.shared.log("Trakt token exchange failed", type: "Error")
|
||||
}
|
||||
}
|
||||
default:
|
||||
Logger.shared.log("Unknown authentication service", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift
Normal file
34
Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Trakt-Login.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TraktLogin {
|
||||
static let clientID = "6ec81bf19deb80fdfa25652eef101576ca6aaa0dc016d36079b2de413d71c369"
|
||||
static let redirectURI = "sora://trakt"
|
||||
|
||||
static let authorizationEndpoint = "https://trakt.tv/oauth/authorize"
|
||||
|
||||
static func authenticate() {
|
||||
let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code"
|
||||
guard let url = URL(string: urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url, options: [:]) { success in
|
||||
if success {
|
||||
Logger.shared.log("Safari opened successfully", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Failed to open Safari", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Cannot open URL", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
168
Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift
Normal file
168
Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
// Trakt-Token.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class TraktToken {
|
||||
static let clientID = "6ec81bf19deb80fdfa25652eef101576ca6aaa0dc016d36079b2de413d71c369"
|
||||
static let clientSecret = "17cd92f71da3be9d755e2d8a6506fb3c3ecee19a247a6f0120ce2fb1f359850b"
|
||||
static let redirectURI = "sora://trakt"
|
||||
|
||||
static let tokenEndpoint = "https://api.trakt.tv/oauth/token"
|
||||
static let serviceName = "me.cranci.sora.TraktToken"
|
||||
static let accessTokenKey = "TraktAccessToken"
|
||||
static let refreshTokenKey = "TraktRefreshToken"
|
||||
|
||||
static let authSuccessNotification = Notification.Name("TraktAuthenticationSuccess")
|
||||
static let authFailureNotification = Notification.Name("TraktAuthenticationFailure")
|
||||
|
||||
private static func saveToKeychain(key: String, data: String) -> Bool {
|
||||
let tokenData = data.data(using: .utf8)!
|
||||
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: tokenData
|
||||
]
|
||||
|
||||
return SecItemAdd(addQuery as CFDictionary, nil) == errSecSuccess
|
||||
}
|
||||
|
||||
static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) {
|
||||
guard let url = URL(string: tokenEndpoint) else {
|
||||
Logger.shared.log("Invalid token endpoint URL", type: "Error")
|
||||
handleFailure(error: "Invalid token endpoint URL", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyData: [String: Any] = [
|
||||
"code": code,
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret,
|
||||
"redirect_uri": redirectURI,
|
||||
"grant_type": "authorization_code"
|
||||
]
|
||||
|
||||
processTokenRequest(request: request, bodyData: bodyData, completion: completion)
|
||||
}
|
||||
|
||||
static func refreshAccessToken(completion: @escaping (Bool) -> Void) {
|
||||
guard let refreshToken = getRefreshToken() else {
|
||||
handleFailure(error: "No refresh token available", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: tokenEndpoint) else {
|
||||
handleFailure(error: "Invalid token endpoint URL", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyData: [String: Any] = [
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret,
|
||||
"redirect_uri": redirectURI,
|
||||
"grant_type": "refresh_token"
|
||||
]
|
||||
|
||||
processTokenRequest(request: request, bodyData: bodyData, completion: completion)
|
||||
}
|
||||
|
||||
private static func processTokenRequest(request: URLRequest, bodyData: [String: Any], completion: @escaping (Bool) -> Void) {
|
||||
var request = request
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: bodyData)
|
||||
} catch {
|
||||
handleFailure(error: "Failed to create request body", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
handleFailure(error: error.localizedDescription, completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
handleFailure(error: "No data received", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let accessToken = json["access_token"] as? String,
|
||||
let refreshToken = json["refresh_token"] as? String {
|
||||
|
||||
let accessSuccess = saveToKeychain(key: accessTokenKey, data: accessToken)
|
||||
let refreshSuccess = saveToKeychain(key: refreshTokenKey, data: refreshToken)
|
||||
|
||||
if accessSuccess && refreshSuccess {
|
||||
NotificationCenter.default.post(name: authSuccessNotification, object: nil)
|
||||
completion(true)
|
||||
} else {
|
||||
handleFailure(error: "Failed to save tokens to keychain", completion: completion)
|
||||
}
|
||||
} else {
|
||||
let errorMessage = (json["error"] as? String) ?? "Unexpected response"
|
||||
handleFailure(error: errorMessage, completion: completion)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
handleFailure(error: "Failed to parse response: \(error.localizedDescription)", completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private static func handleFailure(error: String, completion: @escaping (Bool) -> Void) {
|
||||
Logger.shared.log(error, type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error])
|
||||
completion(false)
|
||||
}
|
||||
|
||||
private static func getRefreshToken() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: refreshTokenKey,
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = result as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
131
Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift
Normal file
131
Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// TraktPushUpdates.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class TraktMutation {
|
||||
let apiURL = URL(string: "https://api.trakt.tv")!
|
||||
|
||||
func getTokenFromKeychain() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: TraktToken.serviceName,
|
||||
kSecAttrAccount as String: TraktToken.accessTokenKey,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = item as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
enum ExternalIDType {
|
||||
case imdb(String)
|
||||
case tmdb(Int)
|
||||
|
||||
var dictionary: [String: Any] {
|
||||
switch self {
|
||||
case .imdb(let id):
|
||||
return ["imdb": id]
|
||||
case .tmdb(let id):
|
||||
return ["tmdb": id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markAsWatched(type: String, externalID: ExternalIDType, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool,
|
||||
sendTraktUpdates == false {
|
||||
return
|
||||
}
|
||||
|
||||
guard let userToken = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"])))
|
||||
return
|
||||
}
|
||||
|
||||
let endpoint = "/sync/history"
|
||||
let body: [String: Any]
|
||||
|
||||
switch type {
|
||||
case "movie":
|
||||
body = [
|
||||
"movies": [
|
||||
[
|
||||
"ids": externalID.dictionary
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
case "episode":
|
||||
guard let episode = episodeNumber, let season = seasonNumber else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing episode or season number"])))
|
||||
return
|
||||
}
|
||||
|
||||
body = [
|
||||
"shows": [
|
||||
[
|
||||
"ids": externalID.dictionary,
|
||||
"seasons": [
|
||||
[
|
||||
"number": season,
|
||||
"episodes": [
|
||||
["number": episode]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
default:
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid content type"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiURL.appendingPathComponent(endpoint))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("2", forHTTPHeaderField: "trakt-api-version")
|
||||
request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key")
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
completion(.failure(NSError(domain: "", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Unexpected response or status code"])))
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log("Successfully updated watch status on Trakt", type: "Debug")
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,18 @@ import Kingfisher
|
|||
|
||||
struct SettingsViewTrackers: View {
|
||||
@AppStorage("sendPushUpdates") private var isSendPushUpdates = true
|
||||
|
||||
@State private var status: String = "You are not logged in"
|
||||
@State private var isLoggedIn: Bool = false
|
||||
@State private var username: String = ""
|
||||
@State private var isLoading: Bool = false
|
||||
@State private var anilistStatus: String = "You are not logged in"
|
||||
@State private var isAnilistLoggedIn: Bool = false
|
||||
@State private var anilistUsername: String = ""
|
||||
@State private var isAnilistLoading: Bool = false
|
||||
@State private var profileColor: Color = .accentColor
|
||||
|
||||
@AppStorage("sendTraktUpdates") private var isSendTraktUpdates = true
|
||||
@State private var traktStatus: String = "You are not logged in"
|
||||
@State private var isTraktLoggedIn: Bool = false
|
||||
@State private var traktUsername: String = ""
|
||||
@State private var isTraktLoading: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("AniList"), footer: Text("Sora and cranci1 are not affiliated with AniList in any way.\n\nNote that progresses update may not be 100% acurate.")) {
|
||||
|
|
@ -36,31 +41,82 @@ struct SettingsViewTrackers: View {
|
|||
Text("AniList.co")
|
||||
.font(.title2)
|
||||
}
|
||||
if isLoading {
|
||||
|
||||
if isAnilistLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
if isLoggedIn {
|
||||
if isAnilistLoggedIn {
|
||||
HStack(spacing: 0) {
|
||||
Text("Logged in as ")
|
||||
Text(username)
|
||||
Text(anilistUsername)
|
||||
.foregroundColor(profileColor)
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
} else {
|
||||
Text(status)
|
||||
Text(anilistStatus)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
if isLoggedIn {
|
||||
Toggle("Sync progresses", isOn: $isSendPushUpdates)
|
||||
|
||||
if isAnilistLoggedIn {
|
||||
Toggle("Sync anime progress", isOn: $isSendPushUpdates)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
Button(isLoggedIn ? "Log Out from AniList.co" : "Log In with AniList.co") {
|
||||
if isLoggedIn {
|
||||
logout()
|
||||
|
||||
Button(isAnilistLoggedIn ? "Log Out from AniList" : "Log In with AniList") {
|
||||
if isAnilistLoggedIn {
|
||||
logoutAniList()
|
||||
} else {
|
||||
login()
|
||||
loginAniList()
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
Section(header: Text("Trakt"), footer: Text("Sora and cranci1 are not affiliated with Trakt in any way.\n\nNote that progress updates may not be 100% accurate.")) {
|
||||
HStack() {
|
||||
KFImage(URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png"))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 80, height: 80)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.clipShape(Rectangle())
|
||||
.cornerRadius(10)
|
||||
Text("Trakt.tv")
|
||||
.font(.title2)
|
||||
}
|
||||
|
||||
if isTraktLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
if isTraktLoggedIn {
|
||||
HStack(spacing: 0) {
|
||||
Text("Logged in as ")
|
||||
Text(traktUsername)
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
} else {
|
||||
Text(traktStatus)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
if isTraktLoggedIn {
|
||||
Toggle("Sync watch progress", isOn: $isSendTraktUpdates)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
|
||||
Button(isTraktLoggedIn ? "Log Out from Trakt" : "Log In with Trakt") {
|
||||
if isTraktLoggedIn {
|
||||
logoutTrakt()
|
||||
} else {
|
||||
loginTrakt()
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
|
|
@ -68,7 +124,8 @@ struct SettingsViewTrackers: View {
|
|||
}
|
||||
.navigationTitle("Trackers")
|
||||
.onAppear {
|
||||
updateStatus()
|
||||
updateAniListStatus()
|
||||
updateTraktStatus()
|
||||
setupNotificationObservers()
|
||||
}
|
||||
.onDisappear {
|
||||
|
|
@ -76,54 +133,167 @@ struct SettingsViewTrackers: View {
|
|||
}
|
||||
}
|
||||
|
||||
func removeNotificationObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: AniListToken.authSuccessNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: AniListToken.authFailureNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: TraktToken.authSuccessNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: TraktToken.authFailureNotification, object: nil)
|
||||
}
|
||||
|
||||
func setupNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(forName: AniListToken.authSuccessNotification, object: nil, queue: .main) { _ in
|
||||
self.status = "Authentication successful!"
|
||||
self.updateStatus()
|
||||
self.anilistStatus = "Authentication successful!"
|
||||
self.updateAniListStatus()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: AniListToken.authFailureNotification, object: nil, queue: .main) { notification in
|
||||
if let error = notification.userInfo?["error"] as? String {
|
||||
self.status = "Login failed: \(error)"
|
||||
self.anilistStatus = "Login failed: \(error)"
|
||||
} else {
|
||||
self.status = "Login failed with unknown error"
|
||||
self.anilistStatus = "Login failed with unknown error"
|
||||
}
|
||||
self.isLoggedIn = false
|
||||
self.isLoading = false
|
||||
self.isAnilistLoggedIn = false
|
||||
self.isAnilistLoading = false
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: TraktToken.authSuccessNotification, object: nil, queue: .main) { _ in
|
||||
self.traktStatus = "Authentication successful!"
|
||||
self.updateTraktStatus()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: TraktToken.authFailureNotification, object: nil, queue: .main) { notification in
|
||||
if let error = notification.userInfo?["error"] as? String {
|
||||
self.traktStatus = "Login failed: \(error)"
|
||||
} else {
|
||||
self.traktStatus = "Login failed with unknown error"
|
||||
}
|
||||
self.isTraktLoggedIn = false
|
||||
self.isTraktLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func removeNotificationObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: AniListToken.authSuccessNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: AniListToken.authFailureNotification, object: nil)
|
||||
func loginTrakt() {
|
||||
traktStatus = "Starting authentication..."
|
||||
isTraktLoading = true
|
||||
TraktLogin.authenticate()
|
||||
}
|
||||
|
||||
func login() {
|
||||
status = "Starting authentication..."
|
||||
isLoading = true
|
||||
func logoutTrakt() {
|
||||
removeTraktTokenFromKeychain()
|
||||
traktStatus = "You are not logged in"
|
||||
isTraktLoggedIn = false
|
||||
traktUsername = ""
|
||||
}
|
||||
|
||||
func updateTraktStatus() {
|
||||
if let token = getTraktTokenFromKeychain() {
|
||||
isTraktLoggedIn = true
|
||||
fetchTraktUserInfo(token: token)
|
||||
} else {
|
||||
isTraktLoggedIn = false
|
||||
traktStatus = "You are not logged in"
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTraktUserInfo(token: String) {
|
||||
isTraktLoading = true
|
||||
let userInfoURL = URL(string: "https://api.trakt.tv/users/settings")!
|
||||
var request = URLRequest(url: userInfoURL)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("2", forHTTPHeaderField: "trakt-api-version")
|
||||
request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key")
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isTraktLoading = false
|
||||
if let error = error {
|
||||
self.traktStatus = "Error: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.traktStatus = "No data received"
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let user = json["user"] as? [String: Any],
|
||||
let username = user["username"] as? String {
|
||||
self.traktUsername = username
|
||||
self.traktStatus = "Logged in as \(username)"
|
||||
}
|
||||
} catch {
|
||||
self.traktStatus = "Failed to parse response"
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func getTraktTokenFromKeychain() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: TraktToken.serviceName,
|
||||
kSecAttrAccount as String: TraktToken.accessTokenKey,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = item as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func removeTraktTokenFromKeychain() {
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: TraktToken.serviceName,
|
||||
kSecAttrAccount as String: TraktToken.accessTokenKey
|
||||
]
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let refreshDeleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: TraktToken.serviceName,
|
||||
kSecAttrAccount as String: TraktToken.refreshTokenKey
|
||||
]
|
||||
SecItemDelete(refreshDeleteQuery as CFDictionary)
|
||||
}
|
||||
|
||||
func loginAniList() {
|
||||
anilistStatus = "Starting authentication..."
|
||||
isAnilistLoading = true
|
||||
AniListLogin.authenticate()
|
||||
}
|
||||
|
||||
func logout() {
|
||||
func logoutAniList() {
|
||||
removeTokenFromKeychain()
|
||||
status = "You are not logged in"
|
||||
isLoggedIn = false
|
||||
username = ""
|
||||
anilistStatus = "You are not logged in"
|
||||
isAnilistLoggedIn = false
|
||||
anilistUsername = ""
|
||||
profileColor = .primary
|
||||
}
|
||||
|
||||
func updateStatus() {
|
||||
func updateAniListStatus() {
|
||||
if let token = getTokenFromKeychain() {
|
||||
isLoggedIn = true
|
||||
isAnilistLoggedIn = true
|
||||
fetchUserInfo(token: token)
|
||||
} else {
|
||||
isLoggedIn = false
|
||||
status = "You are not logged in"
|
||||
isAnilistLoggedIn = false
|
||||
anilistStatus = "You are not logged in"
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUserInfo(token: String) {
|
||||
isLoading = true
|
||||
isAnilistLoading = true
|
||||
let userInfoURL = URL(string: "https://graphql.anilist.co")!
|
||||
var request = URLRequest(url: userInfoURL)
|
||||
request.httpMethod = "POST"
|
||||
|
|
@ -146,22 +316,22 @@ struct SettingsViewTrackers: View {
|
|||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
|
||||
} catch {
|
||||
status = "Failed to serialize request"
|
||||
anilistStatus = "Failed to serialize request"
|
||||
Logger.shared.log("Failed to serialize request", type: "Error")
|
||||
isLoading = false
|
||||
isAnilistLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
isLoading = false
|
||||
isAnilistLoading = false
|
||||
if let error = error {
|
||||
status = "Error: \(error.localizedDescription)"
|
||||
anilistStatus = "Error: \(error.localizedDescription)"
|
||||
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
status = "No data received"
|
||||
anilistStatus = "No data received"
|
||||
Logger.shared.log("No data received", type: "Error")
|
||||
return
|
||||
}
|
||||
|
|
@ -173,15 +343,15 @@ struct SettingsViewTrackers: View {
|
|||
let options = viewer["options"] as? [String: Any],
|
||||
let colorName = options["profileColor"] as? String {
|
||||
|
||||
username = name
|
||||
anilistUsername = name
|
||||
profileColor = colorFromName(colorName)
|
||||
status = "Logged in as \(name)"
|
||||
anilistStatus = "Logged in as \(name)"
|
||||
} else {
|
||||
status = "Unexpected response format!"
|
||||
anilistStatus = "Unexpected response format!"
|
||||
Logger.shared.log("Unexpected response format!", type: "Error")
|
||||
}
|
||||
} catch {
|
||||
status = "Failed to parse response: \(error.localizedDescription)"
|
||||
anilistStatus = "Failed to parse response: \(error.localizedDescription)"
|
||||
Logger.shared.log("Failed to parse response: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@
|
|||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
|
||||
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; };
|
||||
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; };
|
||||
13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E62FC12DABC5830007E259 /* Trakt-Login.swift */; };
|
||||
13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E62FC32DABC58C0007E259 /* Trakt-Token.swift */; };
|
||||
13E62FC72DABFE900007E259 /* TraktPushUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E62FC62DABFE900007E259 /* TraktPushUpdates.swift */; };
|
||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; };
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
|
|
@ -114,6 +117,9 @@
|
|||
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
13E62FC12DABC5830007E259 /* Trakt-Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Login.swift"; sourceTree = "<group>"; };
|
||||
13E62FC32DABC58C0007E259 /* Trakt-Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Token.swift"; sourceTree = "<group>"; };
|
||||
13E62FC62DABFE900007E259 /* TraktPushUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraktPushUpdates.swift; sourceTree = "<group>"; };
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -399,6 +405,7 @@
|
|||
13E62FBF2DABC3A20007E259 /* Trakt */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13E62FC52DABFE810007E259 /* Mutations */,
|
||||
13E62FC02DABC3A90007E259 /* Auth */,
|
||||
);
|
||||
path = Trakt;
|
||||
|
|
@ -407,10 +414,20 @@
|
|||
13E62FC02DABC3A90007E259 /* Auth */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13E62FC12DABC5830007E259 /* Trakt-Login.swift */,
|
||||
13E62FC32DABC58C0007E259 /* Trakt-Token.swift */,
|
||||
);
|
||||
path = Auth;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13E62FC52DABFE810007E259 /* Mutations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13E62FC62DABFE900007E259 /* TraktPushUpdates.swift */,
|
||||
);
|
||||
path = Mutations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -549,9 +566,12 @@
|
|||
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
||||
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
||||
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
||||
13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */,
|
||||
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
|
||||
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
|
||||
13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */,
|
||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||
13E62FC72DABFE900007E259 /* TraktPushUpdates.swift in Sources */,
|
||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
|
||||
13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue