Locations manifest, reorganized instances settings

This commit is contained in:
Arkadiusz Fal
2022-07-01 23:28:32 +02:00
parent 6f62f14adf
commit 4fcf57d755
28 changed files with 686 additions and 214 deletions

View File

@@ -5,37 +5,50 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
static var bridge = AccountsBridge()
let id: String
let instanceID: String
let app: VideosApp
let instanceID: String?
var name: String?
let url: String
let username: String
let password: String?
var token: String?
let anonymous: Bool
let country: String?
let region: String?
init(
id: String? = nil,
app: VideosApp? = nil,
instanceID: String? = nil,
name: String? = nil,
url: String? = nil,
username: String? = nil,
password: String? = nil,
token: String? = nil,
anonymous: Bool = false
anonymous: Bool = false,
country: String? = nil,
region: String? = nil
) {
self.anonymous = anonymous
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
self.instanceID = instanceID ?? UUID().uuidString
self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString)
self.app = app ?? .invidious
self.instanceID = instanceID
self.name = name
self.url = url ?? ""
self.username = username ?? ""
self.token = token
self.password = password ?? ""
self.country = country
self.region = region
}
var instance: Instance! {
Defaults[.instances].first { $0.id == instanceID }
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app, name: url, apiURL: url)
}
var isPublic: Bool {
instanceID.isNil
}
var shortUsername: String {

View File

@@ -3,7 +3,7 @@ import Siesta
import SwiftUI
final class AccountValidator: Service {
let app: Binding<VideosApp>
let app: Binding<VideosApp?>
let url: String
let account: Account!
@@ -13,8 +13,10 @@ final class AccountValidator: Service {
var isValidating: Binding<Bool>
var error: Binding<String?>?
private var appsToValidateInstance = VideosApp.allCases
init(
app: Binding<VideosApp>,
app: Binding<VideosApp?>,
url: String,
account: Account? = nil,
id: Binding<String>,
@@ -54,82 +56,128 @@ final class AccountValidator: Service {
}
}
func instanceValidationResource(_ app: VideosApp) -> Resource {
switch app {
case .invidious:
return resource("/api/v1/videos/dQw4w9WgXcQ")
case .piped:
return resource("/streams/dQw4w9WgXcQ")
}
}
func validateInstance() {
reset()
neverGonnaGiveYouUp
guard let app = appsToValidateInstance.popLast() else { return }
tryValidatingUsing(app)
}
func tryValidatingUsing(_ app: VideosApp) {
instanceValidationResource(app)
.load()
.onSuccess { response in
guard self.url == self.formObjectID.wrappedValue else {
return
}
guard !response.json.isEmpty else {
guard let app = self.appsToValidateInstance.popLast() else {
self.isValid.wrappedValue = false
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
return
}
self.tryValidatingUsing(app)
return
}
let json = response.json.dictionaryValue
let author = self.app.wrappedValue == .invidious ? json["author"] : json["uploader"]
let author = app == .invidious ? json["author"] : json["uploader"]
if author == "Rick Astley" {
self.app.wrappedValue = app
self.isValid.wrappedValue = true
self.error?.wrappedValue = nil
} else {
self.isValid.wrappedValue = false
}
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
}
.onFailure { error in
guard self.url == self.formObjectID.wrappedValue else {
return
}
self.isValid.wrappedValue = false
self.error?.wrappedValue = error.userMessage
}
.onCompletion { _ in
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
if self.appsToValidateInstance.isEmpty {
self.isValidating.wrappedValue = false
self.isValidated.wrappedValue = true
self.isValid.wrappedValue = false
self.error?.wrappedValue = error.userMessage
} else {
guard let app = self.appsToValidateInstance.popLast() else { return }
self.tryValidatingUsing(app)
}
}
}
func validateAccount() {
reset()
accountRequest
.onSuccess { response in
guard self.account!.username == self.formObjectID.wrappedValue else {
return
}
guard let request = accountRequest else {
isValid.wrappedValue = false
isValidated.wrappedValue = true
isValidating.wrappedValue = false
switch self.app.wrappedValue {
case .invidious:
self.isValid.wrappedValue = true
case .piped:
let error = response.json.dictionaryValue["error"]?.string
let token = response.json.dictionaryValue["token"]?.string
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
self.error!.wrappedValue = error
}
}
.onFailure { _ in
guard self.account!.username == self.formObjectID.wrappedValue else {
return
}
return
}
self.isValid.wrappedValue = false
request.onSuccess { response in
guard self.account!.username == self.formObjectID.wrappedValue else {
return
}
.onCompletion { _ in
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
switch self.app.wrappedValue {
case .invidious:
self.isValid.wrappedValue = true
case .piped:
let error = response.json.dictionaryValue["error"]?.string
let token = response.json.dictionaryValue["token"]?.string
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
self.error!.wrappedValue = error
default:
return
}
}
.onFailure { _ in
guard self.account!.username == self.formObjectID.wrappedValue else {
return
}
self.isValid.wrappedValue = false
}
.onCompletion { _ in
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
}
}
var accountRequest: Request {
var accountRequest: Request? {
switch app.wrappedValue {
case .invidious:
return feed.load()
case .piped:
return login.request(.post, json: ["username": account.username, "password": account.password])
default:
return nil
}
}
func reset() {
appsToValidateInstance = VideosApp.allCases
app.wrappedValue = nil
isValid.wrappedValue = false
isValidated.wrappedValue = false
isValidating.wrappedValue = false

View File

@@ -12,7 +12,7 @@ struct AccountsBridge: Defaults.Bridge {
return [
"id": value.id,
"instanceID": value.instanceID,
"instanceID": value.instanceID ?? "",
"name": value.name ?? "",
"apiURL": value.url,
"username": value.username,

View File

@@ -8,6 +8,8 @@ final class AccountsModel: ObservableObject {
@Published private var invidious = InvidiousAPI()
@Published private var piped = PipedAPI()
@Published var publicAccount: Account?
private var cancellables = [AnyCancellable]()
var all: [Account] {
@@ -70,7 +72,7 @@ final class AccountsModel: ObservableObject {
piped.setAccount(account)
}
Defaults[.lastAccountID] = account.anonymous ? nil : account.id
Defaults[.lastAccountID] = account.anonymous ? (account.isPublic ? "public" : nil) : account.id
Defaults[.lastInstanceID] = account.instanceID
}

View File

@@ -0,0 +1,104 @@
import Defaults
import Foundation
import Siesta
import SwiftyJSON
final class InstancesManifest: Service, ObservableObject {
static let builtinManifestUrl = "https://r.yattee.stream/manifest.json"
static let shared = InstancesManifest()
@Published var instances = [ManifestedInstance]()
init() {
super.init()
configure {
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
}
configureTransformer(
manifestURL,
requestMethods: [.get]
) { (content: Entity<JSON>
) -> [ManifestedInstance] in
guard let instances = content.json.dictionaryValue["instances"] else { return [] }
return instances.arrayValue.compactMap(self.extractInstance)
}
}
func setPublicAccount(_ country: String?, accounts: AccountsModel, asCurrent: Bool = true) {
guard let country = country else {
accounts.publicAccount = nil
if asCurrent {
accounts.setCurrent(nil)
}
return
}
instancesList.load().onSuccess { response in
if let instances: [ManifestedInstance] = response.typedContent() {
guard let instance = instances.filter { $0.country == country }.randomElement() else { return }
let account = instance.anonymousAccount
accounts.publicAccount = account
if asCurrent {
accounts.setCurrent(account)
}
}
}
}
func changePublicAccount(_ accounts: AccountsModel, settings: SettingsModel) {
instancesList.load().onSuccess { response in
if let instances: [ManifestedInstance] = response.typedContent() {
let countryInstances = instances.filter { $0.country == Defaults[.countryOfPublicInstances] }
let region = countryInstances.first?.region ?? "Europe"
var regionInstances = instances.filter { $0.region == region }
if let publicAccountUrl = accounts.publicAccount?.url {
regionInstances = regionInstances.filter { $0.url.absoluteString != publicAccountUrl }
}
guard let instance = regionInstances.randomElement() else {
settings.presentAlert(title: "Could not change location", message: "No locations available at the moment")
return
}
let account = instance.anonymousAccount
accounts.publicAccount = account
accounts.setCurrent(account)
}
}
}
func extractInstance(from json: JSON) -> ManifestedInstance? {
guard let app = json["app"].string,
let videosApp = VideosApp(rawValue: app.lowercased()),
let region = json["region"].string,
let country = json["country"].string,
let flag = json["flag"].string,
let url = json["url"].url else { return nil }
return ManifestedInstance(
app: videosApp,
country: country,
region: region,
flag: flag,
url: url
)
}
var manifestURL: String {
var url = Defaults[.instancesManifest]
if url.isEmpty {
url = Self.builtinManifestUrl
}
return url
}
var instancesList: Resource {
resource(absoluteURL: manifestURL)
}
}

View File

@@ -0,0 +1,30 @@
import Foundation
struct ManifestedInstance: Identifiable, Hashable {
let id = UUID().uuidString
let app: VideosApp
let country: String
let region: String
let flag: String
let url: URL
var instance: Instance {
.init(app: app, name: "Public - \(country)", apiURL: url.absoluteString)
}
var location: String {
"\(flag) \(country)"
}
var anonymousAccount: Account {
.init(
id: UUID().uuidString,
app: app,
name: location,
url: url.absoluteString,
anonymous: true,
country: country,
region: region
)
}
}

View File

@@ -221,8 +221,9 @@ final class NavigationModel: ObservableObject {
#endif
}
func presentAlert(title: String, message: String) {
alert = Alert(title: Text(title), message: Text(message))
func presentAlert(title: String, message: String? = nil) {
let message = message.isNil ? nil : Text(message!)
alert = Alert(title: Text(title), message: message)
presentingAlert = true
}

18
Model/SettingsModel.swift Normal file
View File

@@ -0,0 +1,18 @@
import Foundation
import SwiftUI
class SettingsModel: ObservableObject {
@Published var presentingAlert = false
@Published var alert = Alert(title: Text("Error"))
func presentAlert(title: String, message: String? = nil) {
let message = message.isNil ? nil : Text(message!)
alert = Alert(title: Text(title), message: message)
presentingAlert = true
}
func presentAlert(_ alert: Alert) {
self.alert = alert
presentingAlert = true
}
}