Skip to content

Commit

Permalink
HTTP code check (PlayCover#839)
Browse files Browse the repository at this point in the history
* feat: IPA library http code check

* feat: less laggy IPA library loading images

* fix swiftlint
  • Loading branch information
TheMoonThatRises committed Mar 7, 2023
1 parent b6a60c8 commit 70dafc0
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 92 deletions.
3 changes: 1 addition & 2 deletions PlayCover/AppInstaller/Downloader.swift
Expand Up @@ -36,7 +36,6 @@ class DownloadApp {
let downloader = DownloadManager.shared

func start() {
if !NetworkVM.isConnectedToNetwork() { return }
if installVM.inProgress {
Log.shared.error(PlayCoverError.waitInstallation)
} else {
Expand All @@ -58,7 +57,7 @@ class DownloadApp {

if let url = url, url.isFileURL {
proceedInstall(url, deleteIPA: false)
} else {
} else if NetworkVM.urlAccessible(url: url, popup: true) {
proceedDownload()
}
}
Expand Down
31 changes: 18 additions & 13 deletions PlayCover/Model/ITunesResponse.swift
Expand Up @@ -59,19 +59,24 @@ struct ITunesResponse: Codable {
}

func getITunesData(_ itunesLookup: String) async -> ITunesResponse? {
if !NetworkVM.isConnectedToNetwork() { return nil }
guard let url = URL(string: itunesLookup) else { return nil }

do {
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let decoder = JSONDecoder()
let jsonResult: ITunesResponse = try decoder.decode(ITunesResponse.self, from: data)
if jsonResult.resultCount > 0 {
return jsonResult
}
} catch {
print("Error getting iTunes data from URL: \(itunesLookup): \(error)")
guard NetworkVM.isConnectedToNetwork(), let url = URL(string: itunesLookup) else {
return nil
}

return nil
return await withCheckedContinuation { continuation in
URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
do {
if error == nil, let data = data {
let decoder = JSONDecoder()
let jsonResult: ITunesResponse = try decoder.decode(ITunesResponse.self, from: data)
continuation.resume(returning: jsonResult.resultCount > 0 ? jsonResult : nil)
return
}
} catch {
print("Error getting iTunes data from URL: \(itunesLookup): \(error)")
}

continuation.resume(returning: nil)
}.resume()
}
}
47 changes: 39 additions & 8 deletions PlayCover/ViewModel/NetworkVM.swift
Expand Up @@ -15,14 +15,11 @@ class NetworkVM {
let needsConnection = flags.contains(.connectionRequired)
let result = (isReachable && !needsConnection)

if !result {
let networkToastExists = ToastVM.shared.toasts.contains { $0.toastType == .network }
if !networkToastExists {
ToastVM.shared.showToast(
toastType: .network,
toastDetails: NSLocalizedString("ipaLibrary.noNetworkConnection.toast", comment: "")
)
}
if !result && !ToastVM.shared.toasts.contains(where: { $0.toastType == .network }) {
ToastVM.shared.showToast(
toastType: .network,
toastDetails: NSLocalizedString("ipaLibrary.noNetworkConnection.toast", comment: "")
)
}

return result
Expand Down Expand Up @@ -60,4 +57,38 @@ class NetworkVM {
}
})
}

static func urlAccessible(url: URL?, popup: Bool = false) -> Bool {
if let url = url {
guard isConnectedToNetwork() else {
return false
}

let semaphore = DispatchSemaphore(value: 0)

var avaliable = false

var request = URLRequest(url: url)
request.httpMethod = "HEAD"

URLSession.shared.dataTask(with: request) { _, response, error in
defer { semaphore.signal() }

if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode != 200 {
if popup {
Log.shared.error("Unable to download: \(statusCode) " +
"\(HTTPURLResponse.localizedString(forStatusCode: statusCode))")
}
} else if error == nil {
avaliable = true
}
}.resume()

semaphore.wait()

return avaliable
} else {
return false
}
}
}
81 changes: 34 additions & 47 deletions PlayCover/ViewModel/StoreVM.swift
Expand Up @@ -7,7 +7,7 @@

import Foundation

class StoreVM: ObservableObject {
class StoreVM: ObservableObject, @unchecked Sendable {

static let shared = StoreVM()

Expand Down Expand Up @@ -84,49 +84,58 @@ class StoreVM: ObservableObject {
}

func resolveSources() {
if !NetworkVM.isConnectedToNetwork() { return }
guard NetworkVM.isConnectedToNetwork() else {
return
}

apps.removeAll()
for index in 0..<sources.endIndex {
sources[index].status = .checking
sources[index].status = .empty
Task {
if let url = URL(string: self.sources[index].source) {
if StoreVM.checkAvaliability(url: url) {
URLSession.shared.dataTask(with: URLRequest(url: url)) { jsonData, response, error in
guard error == nil,
((response as? HTTPURLResponse)?.statusCode ?? 200) == 200,
let jsonData = jsonData else {
Task { @MainActor in
self.sources[index].status = .badurl
}

return
}

do {
let contents = try String(contentsOf: url)
let jsonData = contents.data(using: .utf8)!
do {
let data: [StoreAppData] = try JSONDecoder().decode([StoreAppData].self, from: jsonData)
if data.count > 0 {
Task { @MainActor in
self.sources[index].status =
sources[0..<index].filter({
$0.source == sources[index].source && $0.id != sources[index].id
}).isEmpty ? .valid : .duplicate
self.appendAppData(data)
}
return
}
} catch {
let data: [StoreAppData] = try JSONDecoder().decode([StoreAppData].self,
from: jsonData)
if data.count > 0 {
Task { @MainActor in
self.sources[index].status = .badjson
self.sources[index].status = self.sources[0..<index].filter({
$0.source == self.sources[index].source && $0.id != self.sources[index].id
}).isEmpty ? .valid : .duplicate

self.appendAppData(data)
}
return
}
} catch {
Task { @MainActor in
self.sources[index].status = .badurl
self.sources[index].status = .badjson
}
return
}
}.resume()

Task { @MainActor in
self.sources[index].status = .checking
}

return
}

Task { @MainActor in
self.sources[index].status = .badurl
sources[index].status = .badurl
}
return
}
}

fetchApps()
}

Expand Down Expand Up @@ -160,28 +169,6 @@ class StoreVM: ObservableObject {
self.sources.append(data)
self.resolveSources()
}

static func checkAvaliability(url: URL) -> Bool {
var avaliable = true
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
URLSession(configuration: .default)
.dataTask(with: request) { _, response, error in
guard error == nil else {
print("Error:", error ?? "")
avaliable = false
return
}

guard (response as? HTTPURLResponse)?.statusCode == 200 else {
print("down")
avaliable = false
return
}
}
.resume()
return avaliable
}
}

struct StoreAppData: Codable, Equatable {
Expand Down
73 changes: 51 additions & 22 deletions PlayCover/Views/Settings/IPASourceSettings.swift
Expand Up @@ -128,12 +128,17 @@ struct SourceView: View {
popoverText: "preferences.popover.badurl",
showingPopover: $showingPopover)
case .checking:
EmptyView()
StatusBadgeView(imageName: "exclamationmark.circle.fill",
imageColor: .yellow,
popoverText: "preferences.popover.checking",
showingPopover: $showingPopover)
case .duplicate:
StatusBadgeView(imageName: "exclamationmark.circle.fill",
imageColor: .yellow,
popoverText: "preferences.popover.duplicate",
showingPopover: $showingPopover)
case .empty:
EmptyView()
case .valid:
StatusBadgeView(imageName: "checkmark.circle.fill",
imageColor: .green,
Expand Down Expand Up @@ -166,13 +171,13 @@ struct StatusBadgeView: View {
}

enum SourceValidation {
case badjson, badurl, checking, duplicate, valid
case badjson, badurl, checking, duplicate, valid, empty
}

struct AddSourceView: View {
@State var newSource = ""
@State var newSourceURL: URL?
@State var sourceValidationState = SourceValidation.checking
@State var sourceValidationState = SourceValidation.empty
@Binding var addSourceSheet: Bool
@EnvironmentObject var storeVM: StoreVM

Expand All @@ -194,12 +199,17 @@ struct AddSourceView: View {
Text("preferences.popover.badurl")
.font(.system(.subheadline))
case .checking:
EmptyView()
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.yellow)
Text("preferences.popover.checking")
.font(.system(.subheadline))
case .duplicate:
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.yellow)
Text("preferences.popover.duplicate")
.font(.system(.subheadline))
case .empty:
EmptyView()
case .valid:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Expand Down Expand Up @@ -243,36 +253,55 @@ struct AddSourceView: View {
}

func validateSource(_ source: String) {
sourceValidationState = .checking
guard NetworkVM.isConnectedToNetwork() else {
return
}

sourceValidationState = .empty

Task {
if let url = URL(string: source) {
newSourceURL = url
if StoreVM.checkAvaliability(url: newSourceURL!) {
do {
if newSourceURL!.scheme == nil {
newSourceURL = URL(string: "https://" + newSourceURL!.absoluteString)!

if newSourceURL!.scheme == nil {
newSourceURL = URL(string: "https://" + newSourceURL!.absoluteString)!
}

URLSession.shared.dataTask(with: URLRequest(url: newSourceURL!)) { jsonData, response, error in
guard error == nil,
((response as? HTTPURLResponse)?.statusCode ?? 200) == 200,
let jsonData = jsonData else {
Task { @MainActor in
self.sourceValidationState = .badurl
}
let (jsonData, _) = try await URLSession.shared.data(for: URLRequest(url: newSourceURL!))
do {
let data: [StoreAppData] = try JSONDecoder().decode([StoreAppData].self, from: jsonData)
if data.count > 0 {
return
}

do {
let data: [StoreAppData] = try JSONDecoder().decode([StoreAppData].self,
from: jsonData)
if data.count > 0 {
Task { @MainActor in
sourceValidationState = storeVM.sources.filter({
$0.source == source
}).isEmpty ? .valid : .duplicate
return
}
} catch {
sourceValidationState = .badjson
return
}
} catch {
sourceValidationState = .badurl
return
Task { @MainActor in
self.sourceValidationState = .badjson
}
}
}
}.resume()

sourceValidationState = .checking

return
}

Task { @MainActor in
self.sourceValidationState = .badurl
}
sourceValidationState = .badurl
return
}
}
}
Expand Down
1 change: 1 addition & 0 deletions PlayCover/en.lproj/Localizable.strings
Expand Up @@ -54,6 +54,7 @@
"preferences.popover.badurl" = "URL Invalid!";
"preferences.popover.badjson" = "JSON Invalid or Not Found!";
"preferences.popover.duplicate" = "Duplicate Link";
"preferences.popover.checking" = "Checking URL";
"preferences.textfield.url" = "Source URL...";
"preferences.tab.install" = "Install";
"preferences.toggle.showInstallPopup" = "Show install PlayTools popup";
Expand Down

0 comments on commit 70dafc0

Please sign in to comment.