AutoCat/AutoCatCore/Services/VehicleRecordService/VehicleRecordService.swift

161 lines
5.5 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// VehicleRecordService.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
public actor VehicleRecordService {
let recordService: AudioRecordServiceProtocol
let locationService: LocationServiceProtocol
let settingsService: SettingsServiceProtocol
let validLetters = Constants.pnLettersMap.keys.map(String.init)
var url: URL?
var date = Date()
@AutoCancellable
var locationTask: Task<VehicleEventDto?,Error>?
var locationTaskActive = false
public init(
recordService: AudioRecordServiceProtocol,
locationService: LocationServiceProtocol,
settingsService: SettingsServiceProtocol
) {
self.recordService = recordService
self.locationService = locationService
self.settingsService = settingsService
}
func getPlateNumber(from recognizedText: String?) async -> String? {
guard let recognizedText else {
return nil
}
let trimmed = recognizedText
.replacingOccurrences(of: " ", with: "")
.uppercased()
.replacingOccurrences(of: "Ф", with: "В")
.replacingOccurrences(of: "НОЛЬ", with: "0")
.replacingOccurrences(of: "Э", with: "")
let recognizeAlternativeOrder = await settingsService.recognizeAlternativeOrder
let recognizeShortenedNumbers = await settingsService.recognizeShortenedNumbers
let defaultRegion = await settingsService.defaultRegion
var result = ""
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
result = String(trimmed[range])
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), recognizeAlternativeOrder {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), recognizeShortenedNumbers {
result = String(trimmed[range]) + defaultRegion
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), recognizeAlternativeOrder && recognizeShortenedNumbers {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + defaultRegion
}
if !result.isEmpty && valid(number: result) {
return result
} else {
return nil
}
}
func valid(number: String) -> Bool {
guard number.count >= 8 else { return false }
let first = String(number.prefix(1))
let second = number.substring(with: 4..<5)
let third = number.substring(with: 5..<6)
let digits = Int(number.substring(with: 1..<4))
let region = Int(number.substring(from: 6))
return self.validLetters.contains(first)
&& self.validLetters.contains(second)
&& self.validLetters.contains(third)
&& digits != nil
&& region != nil
&& region! < 1000
}
}
extension VehicleRecordService: VehicleRecordServiceProtocol {
public func requestPermissionsIfNeeded() async {
await recordService.requestRecordPermissions()
await recordService.requestRecognitionAuthorization()
}
public func startRecording() async throws {
date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
let url = try FileManager.default.url(for: fileName, in: Constants.audioRecordsFolder)
self.url = url
try await recordService.startRecording(to: url)
if !locationTaskActive {
locationTaskActive = true
locationTask = Task {
do {
let location = try await locationService.getRecentLocation()
locationTaskActive = false
return location
} catch {
locationTaskActive = false
return nil
}
}
}
}
public func stopRecording() async throws -> AudioRecordDto {
guard let url else {
await recordService.cancelRecording()
throw VehicleRecordError.emptyUrl
}
self.url = nil
await recordService.stopRecording()
async let recognitionTask = recordService.recognizeText(from: url)
async let durationTask = recordService.getDuration(from: url)
var location = try? await locationTask?.value
let (text, duration) = await (recognitionTask, try? durationTask)
// One location can be shared between multile records
// So, manually give each copy it's unique id
location?.id = UUID().uuidString
let record = AudioRecordDto(
path: url.lastPathComponent,
number: await getPlateNumber(from: text),
raw: text ?? "",
duration: duration ?? 0,
event: location
)
return record
}
public func cancelRecording() async {
await recordService.cancelRecording()
locationTask?.cancel()
locationTask = nil
url = nil
}
}