From 0d9ff05bb69a79b8fbc53e5889fb151ee44e127a Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:53:36 +0100 Subject: [PATCH] does even work yay --- Sora.xcodeproj/project.pbxproj | 16 ++ Sora/Utils/Extensions/URLSession.swift | 32 ++- Sora/Utils/Loaders/JSController.swift | 42 +++- .../EpisodeCell/CircularProgressBar.swift | 36 ++++ .../EpisodeCell/EpisodeCell.swift | 119 +++++++++++ Sora/Views/MediaInfoView/MediaInfoView.swift | 193 ++++++++++-------- 6 files changed, 352 insertions(+), 86 deletions(-) create mode 100644 Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift create mode 100644 Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index 245421c..b424ad7 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; }; 133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; }; 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; }; + 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; + 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -38,6 +40,8 @@ 133D7C872D2BE2640075467E /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; 133D7C892D2BE2640075467E /* Modules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = ""; }; 133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = ""; }; + 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; + 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,6 +109,7 @@ 133D7C7F2D2BE2630075467E /* MediaInfoView */ = { isa = PBXGroup; children = ( + 138AA1B52D2D66EC0021F9DF /* EpisodeCell */, 133D7C802D2BE2630075467E /* MediaInfoView.swift */, ); path = MediaInfoView; @@ -152,6 +157,15 @@ path = Loaders; sourceTree = ""; }; + 138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = { + isa = PBXGroup; + children = ( + 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */, + 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */, + ); + path = EpisodeCell; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -233,12 +247,14 @@ 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */, 133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */, + 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, 133D7C942D2BE2640075467E /* JSController.swift in Sources */, 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, + 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 7f3b1cc..90fc6fa 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -8,10 +8,40 @@ import Foundation extension URLSession { + static let userAgents = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.92", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Android 14; Mobile; rv:123.0) Gecko/123.0 Firefox/123.0", + "Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0" + ] + + static let randomUserAgent: String = { + userAgents.randomElement() ?? userAgents[0] + }() + static let custom: URLSession = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + "User-Agent": randomUserAgent ] return URLSession(configuration: configuration) }() diff --git a/Sora/Utils/Loaders/JSController.swift b/Sora/Utils/Loaders/JSController.swift index 1115934..e15bf23 100644 --- a/Sora/Utils/Loaders/JSController.swift +++ b/Sora/Utils/Loaders/JSController.swift @@ -36,7 +36,7 @@ class JSController: ObservableObject { return } - URLSession.custom.dataTask(with: url) { [weak self] data, response, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -69,4 +69,44 @@ class JSController: ObservableObject { } }.resume() } + + func fetchDetails(url: String, completion: @escaping ([MediaItem]) -> Void) { + guard let url = URL(string: url) else { + completion([]) + return + } + + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } + + if let error = error { + print("Network error: \(error)") + DispatchQueue.main.async { completion([]) } + return + } + + guard let data = data, let html = String(data: data, encoding: .utf8) else { + print("Failed to decode HTML") + DispatchQueue.main.async { completion([]) } + return + } + + if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"), + let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] { + let resultItems = results.map { item in + MediaItem( + description: item["description"] ?? "", + aliases: item["aliases"] ?? "", + airdate: item["airdate"] ?? "" + ) + } + DispatchQueue.main.async { + completion(resultItems) + } + } else { + print("Failed to parse results") + DispatchQueue.main.async { completion([]) } + } + }.resume() + } } diff --git a/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift new file mode 100644 index 0000000..57cb55b --- /dev/null +++ b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift @@ -0,0 +1,36 @@ +// +// CircularProgressBar.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI + +struct CircularProgressBar: View { + var progress: Double + + var body: some View { + ZStack { + Circle() + .stroke(lineWidth: 5.0) + .opacity(0.3) + .foregroundColor(Color.accentColor) + + Circle() + .trim(from: 0.0, to: CGFloat(min(progress, 1.0))) + .stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round)) + .foregroundColor(Color.accentColor) + .rotationEffect(Angle(degrees: 270.0)) + .animation(.linear, value: progress) + + if progress >= 0.90 { + Image(systemName: "checkmark") + .font(.system(size: 12)) + } else { + Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0)) + .font(.system(size: 12)) + } + } + } +} diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift new file mode 100644 index 0000000..a24ff25 --- /dev/null +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -0,0 +1,119 @@ +// +// EpisodeCell.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI +import Kingfisher + +struct EpisodeCell: View { + let episode: String + let episodeID: Int + let imageUrl: String + let progress: Double + let itemID: Int + + @State private var episodeTitle: String = "" + @State private var episodeImageUrl: String = "" + @State private var isLoading: Bool = true + + var body: some View { + HStack { + ZStack { + KFImage(URL(string: episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl)) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: 100, height: 56) + .cornerRadius(8) + + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + + VStack(alignment: .leading) { + Text("Episode \(episodeID + 1)") + .font(.system(size: 15)) + if !episodeTitle.isEmpty { + Text(episodeTitle) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + } + + Spacer() + + CircularProgressBar(progress: progress) + .frame(width: 40, height: 40) + } + .onAppear { + fetchEpisodeDetails() + } + } + + func fetchEpisodeDetails() { + let cacheKey = "episodeDetails_\(itemID)_\(episodeID)" + + if let cachedData = UserDefaults.standard.data(forKey: cacheKey) { + parseEpisodeDetails(data: cachedData) + return + } + + guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else { + isLoading = false + return + } + + URLSession.custom.dataTask(with: url) { data, _, error in + if let error = error { + print("Failed to fetch episode details: \(error)") + DispatchQueue.main.async { + self.isLoading = false + } + return + } + + guard let data = data else { + print("No data received") + DispatchQueue.main.async { + self.isLoading = false + } + return + } + + UserDefaults.standard.set(data, forKey: cacheKey) + self.parseEpisodeDetails(data: data) + }.resume() + } + + func parseEpisodeDetails(data: Data) { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonObject as? [String: Any], + let episodes = json["episodes"] as? [String: Any], + let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any], + let title = episodeDetails["title"] as? [String: String], + let image = episodeDetails["image"] as? String else { + print("Invalid response format") + DispatchQueue.main.async { + self.isLoading = false + } + return + } + + DispatchQueue.main.async { + self.episodeTitle = title["en"] ?? "" + self.episodeImageUrl = image + self.isLoading = false + } + } catch { + print("Failed to parse JSON: \(error)") + DispatchQueue.main.async { + self.isLoading = false + } + } + } +} diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 81f6c59..47fcb0b 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -8,6 +8,13 @@ import SwiftUI import Kingfisher +struct MediaItem: Identifiable { + let id = UUID() + let description: String + let aliases: String + let airdate: String +} + struct MediaInfoView: View { let title: String let imageUrl: String @@ -25,104 +32,122 @@ struct MediaInfoView: View { @AppStorage("externalPlayer") private var externalPlayer: String = "Default" + @ObservedObject var jsController = JSController() + var body: some View { Group { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 10) { - KFImage(URL(string: imageUrl)) - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.system(size: 17)) - .fontWeight(.bold) + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: imageUrl)) + .resizable() + .aspectRatio(2/3, contentMode: .fill) + .cornerRadius(10) + .frame(width: 150, height: 225) - if !aliases.isEmpty && aliases != title { - Text(aliases) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - Spacer() - - HStack(alignment: .center, spacing: 12) { - Text(module.metadata.sourceName) - .font(.system(size: 13)) - .padding(4) - .background(Capsule().fill(Color.accentColor.opacity(0.4))) - - Button(action: { - }) { - Image(systemName: "ellipsis.circle") - .resizable() - .frame(width: 20, height: 20) - } - - Button(action: { - }) { - Image(systemName: "safari") - .resizable() - .frame(width: 20, height: 20) - } - } - } - } - - if !synopsis.isEmpty { - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center) { - Text("Synopsis") - .font(.system(size: 18)) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 17)) .fontWeight(.bold) + if !aliases.isEmpty && aliases != title { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + Spacer() - Button(action: { - showFullSynopsis.toggle() - }) { - Text(showFullSynopsis ? "Less" : "More") - .font(.system(size: 14)) + HStack(alignment: .center, spacing: 12) { + Text(module.metadata.sourceName) + .font(.system(size: 13)) + .padding(4) + .background(Capsule().fill(Color.accentColor.opacity(0.4))) + + Button(action: { + }) { + Image(systemName: "ellipsis.circle") + .resizable() + .frame(width: 20, height: 20) + } + + Button(action: { + }) { + Image(systemName: "safari") + .resizable() + .frame(width: 20, height: 20) + } } } - - Text(synopsis) - .lineLimit(showFullSynopsis ? nil : 4) - .font(.system(size: 14)) - } - } - - HStack { - Button(action: { - }) { - HStack { - Image(systemName: "play.fill") - .foregroundColor(.primary) - Text("Start Watching") - .font(.headline) - .foregroundColor(.primary) - } - .padding() - .frame(maxWidth: .infinity) - .background(Color.accentColor) - .cornerRadius(10) } - Button(action: { - }) { - Image(systemName: "bookmark") - .resizable() - .frame(width: 20, height: 27) + if !synopsis.isEmpty { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center) { + Text("Synopsis") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + Button(action: { + showFullSynopsis.toggle() + }) { + Text(showFullSynopsis ? "Less" : "More") + .font(.system(size: 14)) + } + } + + Text(synopsis) + .lineLimit(showFullSynopsis ? nil : 4) + .font(.system(size: 14)) + } + } + + HStack { + Button(action: { + }) { + HStack { + Image(systemName: "play.fill") + .foregroundColor(.primary) + Text("Start Watching") + .font(.headline) + .foregroundColor(.primary) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.accentColor) + .cornerRadius(10) + } + + Button(action: { + }) { + Image(systemName: "bookmark") + .resizable() + .frame(width: 20, height: 27) + } } } + .padding() + .navigationBarTitleDisplayMode(.inline) + .navigationBarTitle(title) + .navigationViewStyle(StackNavigationViewStyle()) } - .padding() - .navigationBarTitleDisplayMode(.inline) - .navigationBarTitle(title) - .navigationViewStyle(StackNavigationViewStyle()) + } + } + .onAppear { + jsController.fetchDetails(url: href) { items in + if let item = items.first { + print("Fetched item: \(item)") + self.synopsis = item.description + self.aliases = item.aliases + self.airdate = item.airdate + } + self.isLoading = false } } }