Implement store client
This commit is contained in:
parent
02369ffa4c
commit
b903f9c2b2
|
@ -8,8 +8,8 @@
|
|||
import Foundation
|
||||
|
||||
protocol HTTPRequest {
|
||||
var endpoint: HTTPEndpoint { get }
|
||||
var method: HTTPMethod { get }
|
||||
var endpoint: HTTPEndpoint { get }
|
||||
var headers: [String: String] { get }
|
||||
var payload: HTTPPayload? { get }
|
||||
}
|
||||
|
|
|
@ -20,9 +20,13 @@ extension HTTPResponse {
|
|||
|
||||
switch decoder {
|
||||
case .json:
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
case .propertyList:
|
||||
return try PropertyListDecoder().decode(type, from: data)
|
||||
let decoder = JSONDecoder()
|
||||
decoder.userInfo = [.init(rawValue: "data")!: data]
|
||||
return try decoder.decode(type, from: data)
|
||||
case .xml:
|
||||
let decoder = PropertyListDecoder()
|
||||
decoder.userInfo = [.init(rawValue: "data")!: data]
|
||||
return try decoder.decode(type, from: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +34,7 @@ extension HTTPResponse {
|
|||
extension HTTPResponse {
|
||||
enum Decoder {
|
||||
case json
|
||||
case propertyList
|
||||
case xml
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
//
|
||||
// StoreClient.swift
|
||||
// IPATool
|
||||
//
|
||||
// Created by Majd Alfhaily on 22.05.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol StoreClientInterface {
|
||||
func authenticate(email: String, password: String, code: String?, completion: @escaping (Result<StoreResponse.Account, Error>) -> Void)
|
||||
func item(identifier: String, directoryServicesIdentifier: String, completion: @escaping (Result<StoreResponse.Item, Error>) -> Void)
|
||||
}
|
||||
|
||||
extension StoreClientInterface {
|
||||
func authenticate(email: String,
|
||||
password: String,
|
||||
code: String? = nil,
|
||||
completion: @escaping (Result<StoreResponse.Account, Swift.Error>) -> Void) {
|
||||
authenticate(email: email, password: password, code: code, completion: completion)
|
||||
}
|
||||
|
||||
func authenticate(email: String, password: String, code: String? = nil) throws -> StoreResponse.Account {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: Result<StoreResponse.Account, Error>?
|
||||
|
||||
authenticate(email: email, password: password, code: code) {
|
||||
result = $0
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
switch result {
|
||||
case .none:
|
||||
throw StoreClient.Error.timeout
|
||||
case let .failure(error):
|
||||
throw error
|
||||
case let .success(result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func item(identifier: String, directoryServicesIdentifier: String) throws -> StoreResponse.Item {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: Result<StoreResponse.Item, Error>?
|
||||
|
||||
item(identifier: identifier, directoryServicesIdentifier: directoryServicesIdentifier) {
|
||||
result = $0
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
switch result {
|
||||
case .none:
|
||||
throw StoreClient.Error.timeout
|
||||
case let .failure(error):
|
||||
throw error
|
||||
case let .success(result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class StoreClient: StoreClientInterface {
|
||||
private let httpClient: HTTPClient
|
||||
|
||||
init(httpClient: HTTPClient) {
|
||||
self.httpClient = httpClient
|
||||
}
|
||||
|
||||
func authenticate(email: String, password: String, code: String?, completion: @escaping (Result<StoreResponse.Account, Swift.Error>) -> Void) {
|
||||
authenticate(email: email,
|
||||
password: password,
|
||||
code: code,
|
||||
isFirstAttempt: true,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
private func authenticate(email: String,
|
||||
password: String,
|
||||
code: String?,
|
||||
isFirstAttempt: Bool,
|
||||
completion: @escaping (Result<StoreResponse.Account, Swift.Error>) -> Void) {
|
||||
let request = StoreRequest.authenticate(email: email, password: password, code: code)
|
||||
|
||||
httpClient.send(request) { [weak self] result in
|
||||
switch result {
|
||||
case let .success(response):
|
||||
do {
|
||||
let decoded = try response.decode(StoreResponse.self, as: .xml)
|
||||
|
||||
switch decoded {
|
||||
case let .account(account):
|
||||
completion(.success(account))
|
||||
case .item:
|
||||
completion(.failure(Error.invalidResponse))
|
||||
case let .failure(error):
|
||||
switch error {
|
||||
case StoreResponse.Error.invalidCredentials:
|
||||
if isFirstAttempt {
|
||||
return self?.authenticate(email: email,
|
||||
password: password,
|
||||
code: code,
|
||||
isFirstAttempt: false,
|
||||
completion: completion) ?? ()
|
||||
}
|
||||
|
||||
completion(.failure(error))
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case let .failure(error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(identifier: String, directoryServicesIdentifier: String, completion: @escaping (Result<StoreResponse.Item, Swift.Error>) -> Void) {
|
||||
let request = StoreRequest.download(appIdentifier: identifier, directoryServicesIdentifier: directoryServicesIdentifier)
|
||||
|
||||
httpClient.send(request) { result in
|
||||
switch result {
|
||||
case let .success(response):
|
||||
do {
|
||||
let decoded = try response.decode(StoreResponse.self, as: .xml)
|
||||
|
||||
switch decoded {
|
||||
case let .item(item):
|
||||
completion(.success(item))
|
||||
case .account:
|
||||
completion(.failure(Error.invalidResponse))
|
||||
case let .failure(error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case let .failure(error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreClient {
|
||||
enum Error: Swift.Error {
|
||||
case timeout
|
||||
case invalidResponse
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// StoreEndpoint.swift
|
||||
// IPATool
|
||||
//
|
||||
// Created by Majd Alfhaily on 22.05.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StoreEndpoint {
|
||||
case authenticate(prefix: String, guid: String)
|
||||
case download(guid: String)
|
||||
}
|
||||
|
||||
extension StoreEndpoint: HTTPEndpoint {
|
||||
var url: URL {
|
||||
var components = URLComponents(string: path)!
|
||||
components.scheme = "https"
|
||||
components.host = host
|
||||
return components.url!
|
||||
}
|
||||
|
||||
private var host: String {
|
||||
switch self {
|
||||
case let .authenticate(prefix, _):
|
||||
return "\(prefix)-buy.itunes.apple.com"
|
||||
case .download:
|
||||
return "p25-buy.itunes.apple.com"
|
||||
}
|
||||
}
|
||||
|
||||
private var path: String {
|
||||
switch self {
|
||||
case let .authenticate(_, guid):
|
||||
return "/WebObjects/MZFinance.woa/wa/authenticate?guid=\(guid)"
|
||||
case let .download(guid):
|
||||
return "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct?guid=\(guid)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// StoreRequest.swift
|
||||
// IPATool
|
||||
//
|
||||
// Created by Majd Alfhaily on 22.05.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StoreRequest {
|
||||
case authenticate(email: String, password: String, code: String? = nil)
|
||||
case download(appIdentifier: String, directoryServicesIdentifier: String)
|
||||
}
|
||||
|
||||
extension StoreRequest: HTTPRequest {
|
||||
var endpoint: HTTPEndpoint {
|
||||
switch self {
|
||||
case let .authenticate(_, _, code):
|
||||
return StoreEndpoint.authenticate(prefix: (code == nil) ? "p25" : "p71", guid: guid)
|
||||
case .download:
|
||||
return StoreEndpoint.download(guid: guid)
|
||||
}
|
||||
}
|
||||
|
||||
var method: HTTPMethod {
|
||||
return .post
|
||||
}
|
||||
|
||||
var headers: [String: String] {
|
||||
var headers: [String: String] = [
|
||||
"User-Agent": "Configurator/2.0 (Macintosh; OS X 10.12.6; 16G29) AppleWebKit/2603.3.8",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
]
|
||||
|
||||
switch self {
|
||||
case .authenticate:
|
||||
break
|
||||
case let .download(_, directoryServicesIdentifier):
|
||||
headers["X-Dsid"] = directoryServicesIdentifier
|
||||
headers["iCloud-DSID"] = directoryServicesIdentifier
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
var payload: HTTPPayload? {
|
||||
switch self {
|
||||
case let .authenticate(email, password, code):
|
||||
return .xml([
|
||||
"appleId": email,
|
||||
"attempt": "\(code == nil ? "4" : "2")",
|
||||
"createSession": "true",
|
||||
"guid": guid,
|
||||
"password": "\(password)\(code ?? "")",
|
||||
"rmp": "0",
|
||||
"why": "signIn"
|
||||
])
|
||||
case let .download(appIdentifier, _):
|
||||
return .xml([
|
||||
"creditDisplay": "",
|
||||
"guid": guid,
|
||||
"salableAdamId": "\(appIdentifier)"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreRequest {
|
||||
// This identifier is calculated by reading the MAC address of the device and stripping the nonalphabetic characters from the string.
|
||||
// https://stackoverflow.com/a/31838376
|
||||
private var guid: String {
|
||||
let MAC_ADDRESS_LENGTH = 6
|
||||
let bsds: [String] = ["en0", "en1"]
|
||||
var bsd: String = bsds[0]
|
||||
|
||||
var length : size_t = 0
|
||||
var buffer : [CChar]
|
||||
|
||||
var bsdIndex = Int32(if_nametoindex(bsd))
|
||||
if bsdIndex == 0 {
|
||||
bsd = bsds[1]
|
||||
bsdIndex = Int32(if_nametoindex(bsd))
|
||||
guard bsdIndex != 0 else { fatalError("Could not read MAC address") }
|
||||
}
|
||||
|
||||
let bsdData = Data(bsd.utf8)
|
||||
var managementInfoBase = [CTL_NET, AF_ROUTE, 0, AF_LINK, NET_RT_IFLIST, bsdIndex]
|
||||
|
||||
guard sysctl(&managementInfoBase, 6, nil, &length, nil, 0) >= 0 else { fatalError("Could not read MAC address") }
|
||||
|
||||
buffer = [CChar](unsafeUninitializedCapacity: length, initializingWith: {buffer, initializedCount in
|
||||
for x in 0..<length { buffer[x] = 0 }
|
||||
initializedCount = length
|
||||
})
|
||||
|
||||
guard sysctl(&managementInfoBase, 6, &buffer, &length, nil, 0) >= 0 else { fatalError("Could not read MAC address") }
|
||||
|
||||
let infoData = Data(bytes: buffer, count: length)
|
||||
let indexAfterMsghdr = MemoryLayout<if_msghdr>.stride + 1
|
||||
let rangeOfToken = infoData[indexAfterMsghdr...].range(of: bsdData)!
|
||||
let lower = rangeOfToken.upperBound
|
||||
let upper = lower + MAC_ADDRESS_LENGTH
|
||||
let macAddressData = infoData[lower..<upper]
|
||||
let addressBytes = macAddressData.map{ String(format:"%02x", $0) }
|
||||
return addressBytes.joined().uppercased()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// StoreResponse.swift
|
||||
// IPATool
|
||||
//
|
||||
// Created by Majd Alfhaily on 22.05.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StoreResponse {
|
||||
case failure(error: Swift.Error)
|
||||
case account(Account)
|
||||
case item(Item)
|
||||
}
|
||||
|
||||
extension StoreResponse {
|
||||
struct Account {
|
||||
let firstName: String
|
||||
let lastName: String
|
||||
let directoryServicesIdentifier: String
|
||||
}
|
||||
|
||||
struct Item {
|
||||
let url: URL
|
||||
let md5: String
|
||||
let signatures: [Signature]
|
||||
let metadata: [String: Any]
|
||||
}
|
||||
|
||||
enum Error: Int, Swift.Error {
|
||||
case unknownError = 0
|
||||
case genericError = 5002
|
||||
case codeRequired = 1
|
||||
case invalidLicense = 9610
|
||||
case invalidCredentials = -5000
|
||||
case invalidAccount = 5001
|
||||
case invalidItem = -10000
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreResponse: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let error = try container.decodeIfPresent(String.self, forKey: .error)
|
||||
let message = try container.decodeIfPresent(String.self, forKey: .message)
|
||||
|
||||
if container.contains(.account) {
|
||||
let directoryServicesIdentifier = try container.decode(String.self, forKey: .directoryServicesIdentifier)
|
||||
let accountContainer = try container.nestedContainer(keyedBy: AccountInfoCodingKeys.self, forKey: .account)
|
||||
let addressContainer = try accountContainer.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address)
|
||||
let firstName = try addressContainer.decode(String.self, forKey: .firstName)
|
||||
let lastName = try addressContainer.decode(String.self, forKey: .lastName)
|
||||
|
||||
self = .account(.init(firstName: firstName, lastName: lastName, directoryServicesIdentifier: directoryServicesIdentifier))
|
||||
} else if let items = try container.decodeIfPresent([Item].self, forKey: .items), let item = items.first {
|
||||
self = .item(item)
|
||||
} else if let error = error, !error.isEmpty {
|
||||
self = .failure(error: Error(rawValue: Int(error) ?? 0) ?? .unknownError)
|
||||
} else {
|
||||
switch message {
|
||||
case "Your account information was entered incorrectly.":
|
||||
self = .failure(error: Error.invalidCredentials)
|
||||
case "An Apple ID verification code is required to sign in. Type your password followed by the verification code shown on your other devices.":
|
||||
self = .failure(error: Error.codeRequired)
|
||||
default:
|
||||
self = .failure(error: Error.unknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case directoryServicesIdentifier = "dsPersonId"
|
||||
case message = "customerMessage"
|
||||
case items = "songList"
|
||||
case error = "failureType"
|
||||
case account = "accountInfo"
|
||||
}
|
||||
|
||||
private enum AccountInfoCodingKeys: String, CodingKey {
|
||||
case address
|
||||
}
|
||||
|
||||
private enum AddressCodingKeys: String, CodingKey {
|
||||
case firstName
|
||||
case lastName
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreResponse.Item: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let md5 = try container.decode(String.self, forKey: .md5)
|
||||
|
||||
guard let key = CodingUserInfoKey(rawValue: "data"),
|
||||
let data = decoder.userInfo[key] as? Data,
|
||||
let json = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
||||
let items = json["songList"] as? [[String: Any]],
|
||||
let item = items.first(where: { $0["md5"] as? String == md5 }),
|
||||
let metadata = item["metadata"] as? [String: Any]
|
||||
else { throw StoreResponse.Error.invalidItem }
|
||||
|
||||
let absoluteUrl = try container.decode(String.self, forKey: .url)
|
||||
|
||||
self.md5 = md5
|
||||
self.metadata = metadata
|
||||
self.signatures = try container.decode([Signature].self, forKey: .signatures)
|
||||
|
||||
if let url = URL(string: absoluteUrl) {
|
||||
self.url = url
|
||||
} else {
|
||||
let context = DecodingError.Context(codingPath: [CodingKeys.url], debugDescription: "URL contains illegal characters: \(absoluteUrl).")
|
||||
throw DecodingError.keyNotFound(CodingKeys.url, context)
|
||||
}
|
||||
}
|
||||
|
||||
struct Signature: Decodable {
|
||||
let id: Int
|
||||
let sinf: Data
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "URL"
|
||||
case metadata
|
||||
case md5
|
||||
case signatures = "sinfs"
|
||||
}
|
||||
}
|
|
@ -42,7 +42,9 @@ final class iTunesClient: iTunesClientInterface {
|
|||
}
|
||||
|
||||
func lookup(bundleIdentifier: String, completion: @escaping (Result<iTunesResponse.Result, Swift.Error>) -> Void) {
|
||||
httpClient.send(iTunesRequest.lookup(bundleIdentifier: bundleIdentifier)) { result in
|
||||
let request = iTunesRequest.lookup(bundleIdentifier: bundleIdentifier)
|
||||
|
||||
httpClient.send(request) { result in
|
||||
switch result {
|
||||
case let .success(response):
|
||||
do {
|
||||
|
|
|
@ -12,14 +12,14 @@ enum iTunesRequest {
|
|||
}
|
||||
|
||||
extension iTunesRequest: HTTPRequest {
|
||||
var endpoint: HTTPEndpoint {
|
||||
return iTunesEndpoint.lookup
|
||||
}
|
||||
|
||||
var method: HTTPMethod {
|
||||
return .get
|
||||
}
|
||||
|
||||
var endpoint: HTTPEndpoint {
|
||||
return iTunesEndpoint.lookup
|
||||
}
|
||||
|
||||
var payload: HTTPPayload? {
|
||||
switch self {
|
||||
case let .lookup(bundleIdentifier):
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
A9CA48E5265926DB00BC09D5 /* iTunesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48E4265926DB00BC09D5 /* iTunesRequest.swift */; };
|
||||
A9CA48E72659273700BC09D5 /* iTunesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48E62659273700BC09D5 /* iTunesEndpoint.swift */; };
|
||||
A9CA48E92659281F00BC09D5 /* iTunesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48E82659281F00BC09D5 /* iTunesResponse.swift */; };
|
||||
A9CA48EC26592DD100BC09D5 /* StoreClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48EB26592DD100BC09D5 /* StoreClient.swift */; };
|
||||
A9CA48EE26592DD800BC09D5 /* StoreEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48ED26592DD800BC09D5 /* StoreEndpoint.swift */; };
|
||||
A9CA48F026592DE100BC09D5 /* StoreRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48EF26592DE100BC09D5 /* StoreRequest.swift */; };
|
||||
A9CA48F22659317E00BC09D5 /* StoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48F12659317E00BC09D5 /* StoreResponse.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
@ -52,6 +56,10 @@
|
|||
A9CA48E4265926DB00BC09D5 /* iTunesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iTunesRequest.swift; sourceTree = "<group>"; };
|
||||
A9CA48E62659273700BC09D5 /* iTunesEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iTunesEndpoint.swift; sourceTree = "<group>"; };
|
||||
A9CA48E82659281F00BC09D5 /* iTunesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iTunesResponse.swift; sourceTree = "<group>"; };
|
||||
A9CA48EB26592DD100BC09D5 /* StoreClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreClient.swift; sourceTree = "<group>"; };
|
||||
A9CA48ED26592DD800BC09D5 /* StoreEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreEndpoint.swift; sourceTree = "<group>"; };
|
||||
A9CA48EF26592DE100BC09D5 /* StoreRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreRequest.swift; sourceTree = "<group>"; };
|
||||
A9CA48F12659317E00BC09D5 /* StoreResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreResponse.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -88,6 +96,7 @@
|
|||
A9CA48CB2658FBEB00BC09D5 /* Commands */,
|
||||
A9CA48E1265926BF00BC09D5 /* iTunes */,
|
||||
A9CA48D02659138600BC09D5 /* Networking */,
|
||||
A9CA48EA265929E000BC09D5 /* Store */,
|
||||
A9CA48CE2658FC7400BC09D5 /* IPATool.swift */,
|
||||
A9CA48C12658F93F00BC09D5 /* main.swift */,
|
||||
);
|
||||
|
@ -127,6 +136,17 @@
|
|||
path = iTunes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A9CA48EA265929E000BC09D5 /* Store */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A9CA48EB26592DD100BC09D5 /* StoreClient.swift */,
|
||||
A9CA48ED26592DD800BC09D5 /* StoreEndpoint.swift */,
|
||||
A9CA48EF26592DE100BC09D5 /* StoreRequest.swift */,
|
||||
A9CA48F12659317E00BC09D5 /* StoreResponse.swift */,
|
||||
);
|
||||
path = Store;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -196,13 +216,17 @@
|
|||
A9CA48D626591AC200BC09D5 /* HTTPClient.swift in Sources */,
|
||||
A9CA48DA26591CE200BC09D5 /* HTTPMethod.swift in Sources */,
|
||||
A9CA48E3265926D200BC09D5 /* iTunesClient.swift in Sources */,
|
||||
A9CA48F026592DE100BC09D5 /* StoreRequest.swift in Sources */,
|
||||
A9CA48D826591B0400BC09D5 /* URLSession.swift in Sources */,
|
||||
A9CA48E5265926DB00BC09D5 /* iTunesRequest.swift in Sources */,
|
||||
A9CA48D2265913B300BC09D5 /* HTTPRequest.swift in Sources */,
|
||||
A9CA48C22658F93F00BC09D5 /* main.swift in Sources */,
|
||||
A9CA48EC26592DD100BC09D5 /* StoreClient.swift in Sources */,
|
||||
A9CA48E92659281F00BC09D5 /* iTunesResponse.swift in Sources */,
|
||||
A9CA48EE26592DD800BC09D5 /* StoreEndpoint.swift in Sources */,
|
||||
A9CA48DC26591CF100BC09D5 /* HTTPPayload.swift in Sources */,
|
||||
A9CA48E72659273700BC09D5 /* iTunesEndpoint.swift in Sources */,
|
||||
A9CA48F22659317E00BC09D5 /* StoreResponse.swift in Sources */,
|
||||
A9CA48CD2658FBF700BC09D5 /* Download.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
Loading…
Reference in New Issue