Skip to content

Commit cc9cdf8

Browse files
committed
Add metadata API request usage tracking
1 parent b7b1810 commit cc9cdf8

3 files changed

Lines changed: 106 additions & 4 deletions

File tree

Sources/StreetViewWander/AppMain.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,28 @@ struct ContentView: View {
175175
.lineLimit(2)
176176
.font(.system(size: 13, weight: .semibold))
177177
.foregroundStyle(model.errorText == nil ? Color.white.opacity(0.86) : Color(red: 1, green: 0.82, blue: 0.76))
178+
.layoutPriority(1)
179+
180+
Spacer(minLength: 8)
181+
182+
Text(metadataUsageStatus)
183+
.lineLimit(1)
184+
.font(.system(size: 12, weight: .bold))
185+
.foregroundStyle(.white.opacity(0.72))
178186
}
179187
.padding(.horizontal, 14)
180188
.padding(.vertical, 10)
181189
.background(.black.opacity(0.58), in: RoundedRectangle(cornerRadius: 8))
182190
.frame(maxWidth: .infinity, alignment: .leading)
183191
}
184192

193+
private var metadataUsageStatus: String {
194+
if let remaining = model.metadataRequestsRemaining {
195+
return "Metadata API \(formatCount(model.metadataRequestsUsed))/\(formatCount(model.metadataRequestLimit)) · \(formatCount(remaining)) left"
196+
}
197+
return "Metadata API used \(formatCount(model.metadataRequestsUsed)) · no limit"
198+
}
199+
185200
private var sidePanel: some View {
186201
HStack {
187202
Spacer()
@@ -382,6 +397,32 @@ struct SettingsView: View {
382397
.textFieldStyle(.roundedBorder)
383398
}
384399

400+
Divider()
401+
402+
VStack(alignment: .leading, spacing: 10) {
403+
Text("Metadata API Request Count")
404+
.font(.system(size: 13, weight: .semibold))
405+
406+
HStack(spacing: 16) {
407+
requestMetric("Total", model.metadataRequestLimit > 0 ? formatCount(model.metadataRequestLimit) : "No limit")
408+
requestMetric("Used", formatCount(model.metadataRequestsUsed))
409+
requestMetric("Remaining", model.metadataRequestsRemaining.map(formatCount) ?? "No limit")
410+
}
411+
412+
HStack(spacing: 10) {
413+
TextField("Total request limit", value: $model.metadataRequestLimit, format: .number)
414+
.textFieldStyle(.roundedBorder)
415+
.frame(width: 180)
416+
Text("0 means no limit")
417+
.font(.system(size: 12))
418+
.foregroundStyle(.secondary)
419+
Spacer()
420+
Button("Reset Used Count") {
421+
model.resetMetadataRequestUsage()
422+
}
423+
}
424+
}
425+
385426
HStack {
386427
Button("Import .env") {
387428
model.importEnvFile()
@@ -396,6 +437,18 @@ struct SettingsView: View {
396437
.padding(20)
397438
.frame(width: 520)
398439
}
440+
441+
private func requestMetric(_ label: String, _ value: String) -> some View {
442+
VStack(alignment: .leading, spacing: 3) {
443+
Text(label)
444+
.font(.system(size: 11, weight: .medium))
445+
.foregroundStyle(.secondary)
446+
Text(value)
447+
.font(.system(size: 13, weight: .bold))
448+
.foregroundStyle(.primary)
449+
}
450+
.frame(maxWidth: .infinity, alignment: .leading)
451+
}
399452
}
400453

401454
private func panelHeader(_ text: String) -> some View {
@@ -420,6 +473,10 @@ private func formatCoord(_ value: Double) -> String {
420473
String(format: "%.5f", value)
421474
}
422475

476+
private func formatCount(_ value: Int) -> String {
477+
NumberFormatter.localizedString(from: NSNumber(value: value), number: .decimal)
478+
}
479+
423480
private func mapsURL(for panorama: Panorama) -> URL {
424481
var components = URLComponents(string: "https://www.google.com/maps/search/")!
425482
components.queryItems = [

Sources/StreetViewWander/WanderModel.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ final class WanderModel: ObservableObject {
1111
static let metadataAPIKey = "metadataAPIKey"
1212
static let selectedContinentId = "selectedContinentId"
1313
static let selectedCountryId = "selectedCountryId"
14+
static let metadataRequestLimit = "metadataRequestLimit"
15+
static let metadataRequestsUsed = "metadataRequestsUsed"
1416
}
1517

1618
@Published var browserAPIKey: String {
@@ -37,13 +39,26 @@ final class WanderModel: ObservableObject {
3739
}
3840
}
3941
}
42+
@Published var metadataRequestLimit = 0 {
43+
didSet {
44+
if metadataRequestLimit < 0 {
45+
metadataRequestLimit = 0
46+
}
47+
defaults.set(metadataRequestLimit, forKey: DefaultsKey.metadataRequestLimit)
48+
}
49+
}
4050

4151
@Published private(set) var panorama: Panorama?
4252
@Published private(set) var history: [HistoryEntry] = []
4353
@Published private(set) var locationOptions = LocationOptions.empty
4454
@Published private(set) var isLoading = false
4555
@Published private(set) var statusText = "Add API keys in Settings, then pick a random place."
4656
@Published private(set) var errorText: String?
57+
@Published private(set) var metadataRequestsUsed = 0 {
58+
didSet {
59+
defaults.set(metadataRequestsUsed, forKey: DefaultsKey.metadataRequestsUsed)
60+
}
61+
}
4762
@Published var activePanel: Panel = .none
4863
@Published var isSettingsPresented = false
4964

@@ -78,6 +93,8 @@ final class WanderModel: ObservableObject {
7893
self.metadataAPIKey = keychainStore.string(for: DefaultsKey.metadataAPIKey) ?? legacyMetadataAPIKey ?? ""
7994
self.selectedContinentId = defaults.string(forKey: DefaultsKey.selectedContinentId) ?? ""
8095
self.selectedCountryId = defaults.string(forKey: DefaultsKey.selectedCountryId) ?? ""
96+
self.metadataRequestLimit = max(0, defaults.integer(forKey: DefaultsKey.metadataRequestLimit))
97+
self.metadataRequestsUsed = max(0, defaults.integer(forKey: DefaultsKey.metadataRequestsUsed))
8198

8299
persistSecret(browserAPIKey, key: DefaultsKey.browserAPIKey)
83100
persistSecret(metadataAPIKey, key: DefaultsKey.metadataAPIKey)
@@ -92,6 +109,13 @@ final class WanderModel: ObservableObject {
92109
!metadataAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
93110
}
94111

112+
var metadataRequestsRemaining: Int? {
113+
guard metadataRequestLimit > 0 else {
114+
return nil
115+
}
116+
return max(metadataRequestLimit - metadataRequestsUsed, 0)
117+
}
118+
95119
var selectedScopeLabel: String {
96120
if let country = locationOptions.countries.first(where: { $0.id == selectedCountryId }) {
97121
return country.label
@@ -162,7 +186,10 @@ final class WanderModel: ObservableObject {
162186
let next = try await panoramaFinder.findRandomPanorama(
163187
metadataAPIKey: metadataAPIKey,
164188
selection: selection,
165-
recentContinents: recentContinents
189+
recentContinents: recentContinents,
190+
onMetadataRequest: { [weak self] in
191+
await self?.recordMetadataRequest()
192+
}
166193
)
167194
panorama = next
168195
history = try historyStore.append(next)
@@ -177,6 +204,14 @@ final class WanderModel: ObservableObject {
177204
}
178205
}
179206

207+
func resetMetadataRequestUsage() {
208+
metadataRequestsUsed = 0
209+
}
210+
211+
private func recordMetadataRequest() {
212+
metadataRequestsUsed += 1
213+
}
214+
180215
private func persistSecret(_ value: String, key: String) {
181216
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
182217
if trimmed.isEmpty {

Sources/StreetViewWanderCore/PanoramaFinder.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public actor PanoramaFinder {
4646
public func findRandomPanorama(
4747
metadataAPIKey: String,
4848
selection: SearchSelection,
49-
recentContinents: [String] = []
49+
recentContinents: [String] = [],
50+
onMetadataRequest: (@Sendable () async -> Void)? = nil
5051
) async throws -> Panorama {
5152
let apiKey = metadataAPIKey.trimmingCharacters(in: .whitespacesAndNewlines)
5253
guard !apiKey.isEmpty else {
@@ -65,7 +66,11 @@ public actor PanoramaFinder {
6566
globalPlan: globalPlan,
6667
attempt: attempt
6768
)
68-
let metadata = try await metadata(apiKey: apiKey, location: candidate.requestedLocation)
69+
let metadata = try await metadata(
70+
apiKey: apiKey,
71+
location: candidate.requestedLocation,
72+
onRequest: onMetadataRequest
73+
)
6974
lastStatus = metadata.errorMessage ?? metadata.status
7075

7176
if metadata.status == "OK",
@@ -98,7 +103,11 @@ public actor PanoramaFinder {
98103
throw PanoramaFinderError.noPanoramaFound(SearchSampler.maxAttempts, lastStatus)
99104
}
100105

101-
private func metadata(apiKey: String, location: PanoramaLocation) async throws -> PanoramaMetadata {
106+
private func metadata(
107+
apiKey: String,
108+
location: PanoramaLocation,
109+
onRequest: (@Sendable () async -> Void)?
110+
) async throws -> PanoramaMetadata {
102111
var components = URLComponents(string: "https://maps.googleapis.com/maps/api/streetview/metadata")
103112
components?.queryItems = [
104113
URLQueryItem(name: "key", value: apiKey),
@@ -111,6 +120,7 @@ public actor PanoramaFinder {
111120
throw PanoramaFinderError.googleResponseInvalid
112121
}
113122

123+
await onRequest?()
114124
let (data, response) = try await session.data(from: url)
115125
guard let http = response as? HTTPURLResponse else {
116126
throw PanoramaFinderError.googleResponseInvalid

0 commit comments

Comments
 (0)