Skip to content

Commit

Permalink
Merge pull request #572 from rgoldberg/561-itunes-search
Browse files Browse the repository at this point in the history
Fix iTunes Search API issues
  • Loading branch information
rgoldberg authored Oct 14, 2024
2 parents ac3599d + a44655a commit bf627cc
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ArgumentParser

extension Mas {
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
/// https://performance-partners.apple.com/search-api
struct Home: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Opens MAS Preview app page in a browser"
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation

extension Mas {
/// Displays app details. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
/// https://performance-partners.apple.com/search-api
struct Info: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Display app information from the Mac App Store"
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Open.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private let masScheme = "macappstore"

extension Mas {
/// Opens app page in MAS app. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
/// https://performance-partners.apple.com/search-api
struct Open: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Opens app page in AppStore.app"
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ArgumentParser

extension Mas {
/// Search the Mac App Store using the iTunes Search API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
/// https://performance-partners.apple.com/search-api
struct Search: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Search for apps from the Mac App Store"
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Vendor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ArgumentParser

extension Mas {
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup
/// https://performance-partners.apple.com/search-api
struct Vendor: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Opens vendor's app page in a browser"
Expand Down
4 changes: 2 additions & 2 deletions Sources/mas/Controllers/MasStoreSearch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class MasStoreSearch: StoreSearch {
// into the App Store. Instead, we'll make an educated guess that it matches the currently
// selected locale in macOS. This obviously isn't always going to match, but it's probably
// better than passing no "country" at all to the iTunes Search API.
// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
// https://performance-partners.apple.com/search-api
private let country: String?
private let networkManager: NetworkManager

Expand All @@ -40,7 +40,7 @@ class MasStoreSearch: StoreSearch {
func search(for appName: String) -> Promise<[SearchResult]> {
// Search for apps for compatible platforms, in order of preference.
// Macs with Apple Silicon can run iPad and iPhone apps.
var entities = [Entity.macSoftware]
var entities = [Entity.desktopSoftware]
if SysCtlSystemCommand.isAppleSilicon {
entities += [.iPadSoftware, .iPhoneSoftware]
}
Expand Down
64 changes: 42 additions & 22 deletions Sources/mas/Controllers/StoreSearch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,73 @@ protocol StoreSearch {
}

enum Entity: String {
case desktopSoftware
case macSoftware
case iPadSoftware
case iPhoneSoftware = "software"
}

private enum URLAction {
case lookup
case search

var queryItemName: String {
switch self {
case .lookup:
return "id"
case .search:
return "term"
}
}
}

// MARK: - Common methods
extension StoreSearch {
/// Builds the search URL for an app.
///
/// - Parameter appName: MAS app identifier.
/// - Returns: URL for the search service or nil if appName can't be encoded.
func searchURL(for appName: String, inCountry country: String?, ofEntity entity: Entity = .macSoftware) -> URL? {
guard var components = URLComponents(string: "https://itunes.apple.com/search") else {
return nil
}

components.queryItems = [
URLQueryItem(name: "media", value: "software"),
URLQueryItem(name: "entity", value: entity.rawValue),
URLQueryItem(name: "term", value: appName),
]

if let country {
components.queryItems!.append(URLQueryItem(name: "country", value: country))
}

return components.url
/// - Parameter searchTerm: term for which to search in MAS.
/// - Returns: URL for the search service or nil if searchTerm can't be encoded.
func searchURL(
for searchTerm: String,
inCountry country: String?,
ofEntity entity: Entity = .desktopSoftware
) -> URL? {
url(.search, searchTerm, inCountry: country, ofEntity: entity)
}

/// Builds the lookup URL for an app.
///
/// - Parameter appID: MAS app identifier.
/// - Returns: URL for the lookup service or nil if appID can't be encoded.
func lookupURL(forAppID appID: AppID, inCountry country: String?) -> URL? {
guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else {
func lookupURL(
forAppID appID: AppID,
inCountry country: String?,
ofEntity entity: Entity = .desktopSoftware
) -> URL? {
url(.lookup, String(appID), inCountry: country, ofEntity: entity)
}

private func url(
_ action: URLAction,
_ queryItemValue: String,
inCountry country: String?,
ofEntity entity: Entity = .desktopSoftware
) -> URL? {
guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else {
return nil
}

components.queryItems = [
URLQueryItem(name: "id", value: "\(appID)"),
URLQueryItem(name: "entity", value: "desktopSoftware"),
URLQueryItem(name: "media", value: "software"),
URLQueryItem(name: "entity", value: entity.rawValue),
]

if let country {
components.queryItems!.append(URLQueryItem(name: "country", value: country))
}

components.queryItems!.append(URLQueryItem(name: action.queryItemName, value: queryItemValue))

return components.url
}
}
7 changes: 3 additions & 4 deletions Tests/masTests/Controllers/MasStoreSearchSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@ public class MasStoreSearchSpec: QuickSpec {
}
describe("url string") {
it("contains the app name") {
let appName = "myapp"
expect {
MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString
MasStoreSearch().searchURL(for: "myapp", inCountry: "US")?.absoluteString
}
== "https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US"
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp"
}
it("contains the encoded app name") {
expect {
MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString
}
== "https://itunes.apple.com/search?media=software&entity=macSoftware&term=My%20App&country=US"
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App"
}
}
describe("store") {
Expand Down

0 comments on commit bf627cc

Please sign in to comment.