Skip to content

Commit

Permalink
Merge branch 'feature/importQueueUXandTestability' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeMatt committed Nov 28, 2024
2 parents 4b7762c + 4081da7 commit 4b85877
Show file tree
Hide file tree
Showing 22 changed files with 507 additions and 116 deletions.
38 changes: 29 additions & 9 deletions PVLibrary/Sources/PVLibrary/Importer/Models/ImportQueueItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import PVPrimitives
import Perception

// Enum to define the possible statuses of each import
public enum ImportStatus: String {
case queued
public enum ImportStatus: Int, CustomStringConvertible, CaseIterable {
case conflict // Indicates additional action needed by user after successful import

case partial //indicates the item is waiting for associated files before it could be processed
case processing
case success

case queued

case failure
case conflict // Indicates additional action needed by user after successful import

case success

public var description: String {
switch self {
Expand Down Expand Up @@ -55,16 +59,20 @@ public enum ProcessingState {
// ImportItem model to hold each file's metadata and progress
@Perceptible
public class ImportQueueItem: Identifiable, ObservableObject {

// TODO: Make this more generic with AnySystem, some System?
public typealias System = PVSystem //AnySystem

public let id = UUID()
public var url: URL
public var fileType: FileType
public var systems: [PVSystem] // Can be set to the specific system type
public var userChosenSystem: PVSystem?
public var systems: [System] // Can be set to the specific system type
public var userChosenSystem: (System)?
public var destinationUrl: URL?
public var errorValue: String?

//this is used when a single import has child items - e.g., m3u, cue, directory
public var childQueueItems:[ImportQueueItem]
public var childQueueItems: [ImportQueueItem]

// Observable status for individual imports
public var status: ImportStatus = .queued
Expand Down Expand Up @@ -94,7 +102,7 @@ public class ImportQueueItem: Identifiable, ObservableObject {
var md5: String?
}

public func targetSystem() -> PVSystem? {
public func targetSystem() -> (any SystemProtocol)? {
guard !systems.isEmpty else {
return nil
}
Expand All @@ -105,7 +113,7 @@ public class ImportQueueItem: Identifiable, ObservableObject {

if let chosenSystem = userChosenSystem {

var target:PVSystem? = nil
var target:(any SystemProtocol)? = nil

for system in systems {
if (chosenSystem.identifier == system.identifier) {
Expand Down Expand Up @@ -139,3 +147,15 @@ public class ImportQueueItem: Identifiable, ObservableObject {
return current
}
}

extension ImportQueueItem: Equatable {
public static func == (lhs: ImportQueueItem, rhs: ImportQueueItem) -> Bool {
return lhs.url == rhs.url
}
}

extension ImportQueueItem: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@
// Created by David Proskin on 11/3/24.
//

import PVPrimitives

protocol ArtworkImporting {
func setSystemsService(_ systemsService:GameImporterSystemsServicing)
func importArtworkItem(_ queueItem: ImportQueueItem) async -> PVGame?
// TODO: Make me more generic
// associatedtype MyGameImporterSystemsService: GameImporterSystemsServicing
typealias MyGameImporterSystemsService = GameImporterSystemsServicing

func setSystemsService(_ systemsService: MyGameImporterSystemsService)
func importArtworkItem(_ queueItem: ImportQueueItem) async -> MyGameImporterSystemsService.GameType?
}

class ArtworkImporter : ArtworkImporting {

private var gameImporterSystemsService:GameImporterSystemsServicing?
private var gameImporterSystemsService: (any GameImporterSystemsServicing)?

init() {

}

func setSystemsService(_ systemsService:GameImporterSystemsServicing) {
func setSystemsService(_ systemsService: MyGameImporterSystemsService) {
gameImporterSystemsService = systemsService
}

func importArtworkItem(_ queueItem: ImportQueueItem) async -> PVGame? {
func importArtworkItem(_ queueItem: ImportQueueItem) async -> MyGameImporterSystemsService.GameType? {
guard queueItem.fileType == .artwork else {
return nil
}
Expand Down Expand Up @@ -108,7 +114,7 @@ class ArtworkImporter : ArtworkImporting {
} catch {
ELOG("Couldn't update game \(onlyMatch.title) with new artwork URL")
}
return onlyMatch
return onlyMatch as? PVGame
} else {
ELOG("We got to the unlikely scenario where an extension is possibly a CD binary file, or belongs to a system, and had multiple games that matched the filename under more than one core.")
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import PVPrimitives

protocol CDFileHandling {
func findAssociatedBinFileNames(for cueFileItem: ImportQueueItem) throws -> [String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,41 @@ public typealias GameImporterFinishedImportingGameHandler = (_ md5Hash: String,
public typealias GameImporterFinishedGettingArtworkHandler = (_ artworkURL: String?) -> Void

public protocol GameImporting {

typealias ImportQueueItemType = ImportQueueItem

func initSystems() async

var importStatus: String { get }

var importQueue: [ImportQueueItem] { get }
var importQueue: [ImportQueueItemType] { get }

var processingState: ProcessingState { get }

func addImport(_ item: ImportQueueItem)
func addImports(forPaths paths: [URL])
func addImports(forPaths paths: [URL], targetSystem: AnySystem)

func removeImports(at offsets: IndexSet)
func startProcessing()

func sortImportQueueItems(_ importQueueItems: [ImportQueueItem]) -> [ImportQueueItem]
func sortImportQueueItems(_ importQueueItems: [ImportQueueItemType]) -> [ImportQueueItemType]

func importQueueContainsDuplicate(_ queue: [ImportQueueItem], ofItem queueItem: ImportQueueItem) -> Bool
func importQueueContainsDuplicate(_ queue: [ImportQueueItemType], ofItem queueItem: ImportQueueItemType) -> Bool

var importStartedHandler: GameImporterImportStartedHandler? { get set }
/// Closure called when import completes
var completionHandler: GameImporterCompletionHandler? { get set }
/// Closure called when a game finishes importing
var finishedImportHandler: GameImporterFinishedImportingGameHandler? { get set }
/// Closure called when artwork finishes downloading
var finishedArtworkHandler: GameImporterFinishedGettingArtworkHandler? { get set }

/// Spotlight Handerls
/// Closure called when spotlight completes
var spotlightCompletionHandler: GameImporterCompletionHandler? { get set }
/// Closure called when a game finishes importing
var spotlightFinishedImportHandler: GameImporterFinishedImportingGameHandler? { get set }
}


Expand Down Expand Up @@ -182,9 +201,9 @@ public final class GameImporter: GameImporting, ObservableObject {
public var processingState: ProcessingState = .idle // Observable state for processing status

internal var gameImporterFileService:GameImporterFileServicing
internal var gameImporterDatabaseService:GameImporterDatabaseServicing
internal var gameImporterSystemsService:GameImporterSystemsServicing
internal var gameImporterArtworkImporter:ArtworkImporting
internal var gameImporterDatabaseService:any GameImporterDatabaseServicing
internal var gameImporterSystemsService:any GameImporterSystemsServicing
internal var gameImporterArtworkImporter:any ArtworkImporting
internal var cdRomFileHandler:CDFileHandling

// MARK: - Paths
Expand All @@ -198,7 +217,7 @@ public final class GameImporter: GameImporting, ObservableObject {
/// Path to the BIOS directory
public var biosPath: URL { get { Paths.biosesPath }}

public var databaseService: GameImporterDatabaseServicing {
public var databaseService: any GameImporterDatabaseServicing {
return gameImporterDatabaseService
}

Expand Down Expand Up @@ -360,13 +379,13 @@ public final class GameImporter: GameImporting, ObservableObject {
}
}

public func addImports(forPaths paths: [URL], targetSystem: PVSystem) {
public func addImports(forPaths paths: [URL], targetSystem: AnySystem) {
importQueueLock.lock()
defer { importQueueLock.unlock() }

for path in paths {
var item = ImportQueueItem(url: path, fileType: .unknown)
item.userChosenSystem = targetSystem
item.userChosenSystem?.identifier = targetSystem.identifier
self.addImportItemToQueue(item)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,31 @@ import Perception
import SwiftUI

public protocol GameImporterDatabaseServicing {
// TODO: Make me more generic
// associatedtype GameType: PVGameLibraryEntry
typealias GameType = PVGame // PVGameLibraryEntry

func setOpenVGDB(_ vgdb: OpenVGDB)
func setRomsPath(url:URL)
func importGameIntoDatabase(queueItem: ImportQueueItem) async throws
func importBIOSIntoDatabase(queueItem: ImportQueueItem) async throws
func getUpdatedGameInfo(for game: PVGame, forceRefresh: Bool) -> PVGame
func getArtwork(forGame game: PVGame) async -> PVGame
func getUpdatedGameInfo(for game: GameType, forceRefresh: Bool) -> GameType
func getArtwork(forGame game: GameType) async -> GameType
}

extension CharacterSet {
var GameImporterDatabaseServiceCharset: CharacterSet {
GameImporterDatabaseServiceCharset
}
}
fileprivate let GameImporterDatabaseServiceCharset: CharacterSet = {
var c = CharacterSet.punctuationCharacters
c.remove(charactersIn: ",-+&.'")
return c
}()

class GameImporterDatabaseService : GameImporterDatabaseServicing {
static var charset: CharacterSet = {
var c = CharacterSet.punctuationCharacters
c.remove(charactersIn: ",-+&.'")
return c
}()


var romsPath:URL?
var openVGDB: OpenVGDB?
Expand Down Expand Up @@ -81,7 +92,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
await saveRelativePath(existingGame, partialPath: partialPath, file: queueItem.url)
} else {
DLOG("No existing game found, starting import to database")
try await self.importToDatabaseROM(forItem: queueItem, system: targetSystem, relatedFiles: nil)
try await self.importToDatabaseROM(forItem: queueItem, system: targetSystem as! AnySystem, relatedFiles: nil)
}
}

Expand Down Expand Up @@ -118,7 +129,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
}

/// Imports a ROM to the database
internal func importToDatabaseROM(forItem queueItem: ImportQueueItem, system: PVSystem, relatedFiles: [URL]?) async throws {
internal func importToDatabaseROM(forItem queueItem: ImportQueueItem, system: AnySystem, relatedFiles: [URL]?) async throws {

guard let _ = queueItem.destinationUrl else {
//how did we get here, throw?
Expand All @@ -134,6 +145,10 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {

DLOG("Creating game object with title: \(title), partialPath: \(partialPath)")

guard let system = RomDatabase.sharedInstance.object(ofType: PVSystem.self, wherePrimaryKeyEquals: system.identifier) else {
throw GameImporterError.noSystemMatched
}

let file = PVFile(withURL: queueItem.destinationUrl!)
let game = PVGame(withFile: file, system: system)
game.romPath = partialPath
Expand Down Expand Up @@ -371,7 +386,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing {
if resultsMaybe == nil || resultsMaybe!.isEmpty { //PVEmulatorConfiguration.supportedROMFileExtensions.contains(game.file.url.pathExtension.lowercased()) {
let fileName: String = game.file.url.lastPathComponent
// Remove any extraneous stuff in the rom name such as (U), (J), [T+Eng] etc
let nonCharRange: NSRange = (fileName as NSString).rangeOfCharacter(from: GameImporterDatabaseService.charset)
let nonCharRange: NSRange = (fileName as NSString).rangeOfCharacter(from: GameImporterDatabaseServiceCharset)
var gameTitleLen: Int
if nonCharRange.length > 0, nonCharRange.location > 1 {
gameTitleLen = nonCharRange.location - 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import PVSupport
import RealmSwift
import PVPrimitives

protocol GameImporterFileServicing {
func moveImportItem(toAppropriateSubfolder queueItem: ImportQueueItem) async throws
Expand Down Expand Up @@ -91,7 +92,7 @@ class GameImporterFileService : GameImporterFileServicing {

// MARK: - Utility

internal func moveChildImports(forQueueItem queueItem:ImportQueueItem, to destinationFolder:URL) async throws {
internal func moveChildImports(forQueueItem queueItem: ImportQueueItem, to destinationFolder: URL) async throws {
guard !queueItem.childQueueItems.isEmpty else {
return
}
Expand Down
Loading

0 comments on commit 4b85877

Please sign in to comment.