Remove dead scan path and tighten discovery API
This commit is contained in:
parent
4a3b336643
commit
4cdf8b64a8
@ -334,7 +334,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
let accessMethod = sourceAccessMethod
|
let accessMethod = sourceAccessMethod
|
||||||
let discoveryTask = Task.detached(priority: .userInitiated) {
|
let discoveryTask = Task.detached(priority: .userInitiated) {
|
||||||
do {
|
do {
|
||||||
_ = try await accessMethod.discoverItems(for: source, mode: mode) { item in
|
try await accessMethod.discoverItems(for: source, mode: mode) { item in
|
||||||
continuation.yield(item)
|
continuation.yield(item)
|
||||||
}
|
}
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
@ -630,46 +630,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleEnrichedItem(_ enrichedItem: MinecraftContentItem, for sourceID: URL) {
|
|
||||||
var previousItem: MinecraftContentItem?
|
|
||||||
|
|
||||||
updateSource(sourceID) { source in
|
|
||||||
guard let index = source.rawItems.firstIndex(where: { $0.id == enrichedItem.id }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
previousItem = source.rawItems[index]
|
|
||||||
source.rawItems[index] = enrichedItem
|
|
||||||
source.indexedDetailCount += 1
|
|
||||||
|
|
||||||
if source.indexedDetailCount < source.indexedItemCount {
|
|
||||||
source.scanStatus = "Loaded details for \(source.indexedDetailCount) of \(source.indexedItemCount) items..."
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMetadataUpdate(
|
|
||||||
for: enrichedItem,
|
|
||||||
previousItem: previousItem,
|
|
||||||
in: &source,
|
|
||||||
sourceID: sourceID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSizedItem(_ sizedItem: MinecraftContentItem, for sourceID: URL) {
|
|
||||||
updateSource(sourceID) { source in
|
|
||||||
guard let index = source.rawItems.firstIndex(where: { $0.id == sizedItem.id }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
source.rawItems[index].sizeBytes = sizedItem.sizeBytes
|
|
||||||
source.rawItems[index].sizeLoaded = sizedItem.sizeLoaded
|
|
||||||
|
|
||||||
if source.isScanning {
|
|
||||||
source.scanStatus = "Calculating sizes for \(source.rawItems.filter(\.sizeLoaded).count) of \(source.indexedItemCount) items..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func rebuildNormalizedIndex(for sourceID: URL) {
|
private func rebuildNormalizedIndex(for sourceID: URL) {
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
|
let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
|
||||||
@ -921,39 +881,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return "Failed to scan device library."
|
return "Failed to scan device library."
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleDiscoveredItem(_ item: MinecraftContentItem, in source: inout MinecraftSource, sourceID: URL) {
|
|
||||||
guard isLogicalPackType(item.contentType) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let identity = packMetadata(for: item, sourceRootURL: source.folderURL).identity
|
|
||||||
refreshLogicalPack(identity: identity, in: &source, sourceID: sourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleMetadataUpdate(
|
|
||||||
for item: MinecraftContentItem,
|
|
||||||
previousItem: MinecraftContentItem?,
|
|
||||||
in source: inout MinecraftSource,
|
|
||||||
sourceID: URL
|
|
||||||
) {
|
|
||||||
if isLogicalPackType(item.contentType) {
|
|
||||||
let newIdentity = packMetadata(for: item, sourceRootURL: source.folderURL).identity
|
|
||||||
let previousIdentity = previousItem.map { packMetadata(for: $0, sourceRootURL: source.folderURL).identity }
|
|
||||||
|
|
||||||
if let previousIdentity, previousIdentity != newIdentity {
|
|
||||||
refreshLogicalPack(identity: previousIdentity, in: &source, sourceID: sourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshLogicalPack(identity: newIdentity, in: &source, sourceID: sourceID)
|
|
||||||
refreshWorldRelationships(in: &source, filteringTo: item.contentType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.contentType == .world {
|
|
||||||
refreshWorldRelationship(for: item, in: &source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) {
|
private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) {
|
||||||
guard let index = sources.firstIndex(where: { $0.id == sourceID }) else {
|
guard let index = sources.firstIndex(where: { $0.id == sourceID }) else {
|
||||||
return
|
return
|
||||||
@ -987,135 +914,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshLogicalPack(identity: PackIdentity, in source: inout MinecraftSource, sourceID: URL) {
|
|
||||||
let matchingItems = source.rawItems.filter { item in
|
|
||||||
guard isLogicalPackType(item.contentType) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return packMetadata(for: item, sourceRootURL: source.folderURL).identity == identity
|
|
||||||
}
|
|
||||||
|
|
||||||
source.logicalPacks.removeAll { $0.id == identity }
|
|
||||||
source.packInstances.removeAll { $0.logicalPackID == identity }
|
|
||||||
|
|
||||||
guard !matchingItems.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let representativeItem = matchingItems.reduce(matchingItems[0]) { current, candidate in
|
|
||||||
shouldPreferPackItem(candidate, over: current) ? candidate : current
|
|
||||||
}
|
|
||||||
let representativeMetadata = packMetadata(for: representativeItem, sourceRootURL: source.folderURL)
|
|
||||||
|
|
||||||
source.logicalPacks.append(
|
|
||||||
LogicalPack(
|
|
||||||
id: identity,
|
|
||||||
contentType: identity.type,
|
|
||||||
displayName: representativeItem.displayName,
|
|
||||||
uuid: representativeMetadata.uuid,
|
|
||||||
version: representativeMetadata.version,
|
|
||||||
representativeItemID: representativeItem.id,
|
|
||||||
instanceItemIDs: matchingItems.map(\.id).sorted {
|
|
||||||
$0.path.localizedStandardCompare($1.path) == .orderedAscending
|
|
||||||
},
|
|
||||||
isSuspicious: identity.isSuspicious
|
|
||||||
)
|
|
||||||
)
|
|
||||||
source.logicalPacks.sort {
|
|
||||||
let nameOrder = $0.displayName.localizedStandardCompare($1.displayName)
|
|
||||||
if nameOrder != .orderedSame {
|
|
||||||
return nameOrder == .orderedAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
return $0.id.id.localizedStandardCompare($1.id.id) == .orderedAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
let rawWorlds = source.rawItems.filter { $0.contentType == .world }
|
|
||||||
source.packInstances.append(
|
|
||||||
contentsOf: matchingItems.map { item in
|
|
||||||
PackInstance(
|
|
||||||
id: item.id,
|
|
||||||
itemID: item.id,
|
|
||||||
sourceID: sourceID,
|
|
||||||
logicalPackID: identity,
|
|
||||||
origin: packOrigin(for: item),
|
|
||||||
hostWorldItemID: hostWorldItemID(for: item, in: rawWorlds)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
source.packInstances.sort {
|
|
||||||
$0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshWorldRelationships(in source: inout MinecraftSource, filteringTo type: MinecraftContentType? = nil) {
|
|
||||||
let worlds = source.rawItems.filter { $0.contentType == .world }
|
|
||||||
for world in worlds {
|
|
||||||
guard type == nil || world.packReferences.contains(where: { $0.type == type }) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshWorldRelationship(for: world, in: &source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshWorldRelationship(for world: MinecraftContentItem, in source: inout MinecraftSource) {
|
|
||||||
source.worldPackRelationships.removeAll { $0.worldItemID == world.id }
|
|
||||||
source.logicalWorlds.removeAll { $0.itemID == world.id }
|
|
||||||
|
|
||||||
let logicalPacksByID = Dictionary(uniqueKeysWithValues: source.logicalPacks.map { ($0.id, $0) })
|
|
||||||
var usedPackIDs = Set<PackIdentity>()
|
|
||||||
var unresolvedReferences: [ContentPackReference] = []
|
|
||||||
var relationships: [WorldPackRelationship] = []
|
|
||||||
|
|
||||||
for reference in world.packReferences {
|
|
||||||
let referenceIdentity = PackIdentity(
|
|
||||||
type: reference.type,
|
|
||||||
uuid: reference.uuid,
|
|
||||||
version: reference.version,
|
|
||||||
fallbackName: reference.name,
|
|
||||||
fallbackLocationHint: world.folderName
|
|
||||||
)
|
|
||||||
let resolvedID = logicalPacksByID[referenceIdentity]?.id
|
|
||||||
|
|
||||||
if let resolvedID {
|
|
||||||
usedPackIDs.insert(resolvedID)
|
|
||||||
} else {
|
|
||||||
unresolvedReferences.append(reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
relationships.append(
|
|
||||||
WorldPackRelationship(
|
|
||||||
worldItemID: world.id,
|
|
||||||
logicalPackID: resolvedID,
|
|
||||||
reference: reference
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
source.worldPackRelationships.append(contentsOf: relationships)
|
|
||||||
source.logicalWorlds.append(
|
|
||||||
LogicalWorld(
|
|
||||||
id: world.id,
|
|
||||||
itemID: world.id,
|
|
||||||
usedPackIDs: usedPackIDs.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending },
|
|
||||||
unresolvedReferences: unresolvedReferences
|
|
||||||
)
|
|
||||||
)
|
|
||||||
let rawItemsByID = Dictionary(uniqueKeysWithValues: source.rawItems.map { ($0.id, $0) })
|
|
||||||
source.logicalWorlds.sort {
|
|
||||||
guard
|
|
||||||
let lhs = rawItemsByID[$0.itemID],
|
|
||||||
let rhs = rawItemsByID[$1.itemID]
|
|
||||||
else {
|
|
||||||
return $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
return WorldScanner.sortItems(lhs, rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func runConnectedDeviceRefreshLoop() async {
|
private func runConnectedDeviceRefreshLoop() async {
|
||||||
while !Task.isCancelled && !isShuttingDown {
|
while !Task.isCancelled && !isShuttingDown {
|
||||||
if hasActiveConnectedDeviceScan {
|
if hasActiveConnectedDeviceScan {
|
||||||
|
|||||||
@ -103,7 +103,7 @@ struct AppleMobileDeviceSourceAccess: ConnectedDeviceSourceAccessMethod {
|
|||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
mode: SourceDiscoveryMode,
|
mode: SourceDiscoveryMode,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) async throws -> [MinecraftContentItem] {
|
) async throws {
|
||||||
_ = mode
|
_ = mode
|
||||||
guard case .connectedDevice(_, let container) = source.origin else {
|
guard case .connectedDevice(_, let container) = source.origin else {
|
||||||
throw SourceAccessError.accessFailed(
|
throw SourceAccessError.accessFailed(
|
||||||
@ -141,8 +141,6 @@ struct AppleMobileDeviceSourceAccess: ConnectedDeviceSourceAccessMethod {
|
|||||||
for item in items {
|
for item in items {
|
||||||
onDiscovered(item)
|
onDiscovered(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ protocol SourceAccessMethod: Sendable {
|
|||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
mode: SourceDiscoveryMode,
|
mode: SourceDiscoveryMode,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) async throws -> [MinecraftContentItem]
|
) async throws
|
||||||
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem
|
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem
|
||||||
nonisolated func loadPreviewAssets(for item: MinecraftContentItem, in source: MinecraftSource) async -> MinecraftContentItem
|
nonisolated func loadPreviewAssets(for item: MinecraftContentItem, in source: MinecraftSource) async -> MinecraftContentItem
|
||||||
nonisolated func loadPreviewAssets(for items: [MinecraftContentItem], in source: MinecraftSource) async -> [MinecraftContentItem]
|
nonisolated func loadPreviewAssets(for items: [MinecraftContentItem], in source: MinecraftSource) async -> [MinecraftContentItem]
|
||||||
@ -54,11 +54,10 @@ extension SourceAccessMethod {
|
|||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
mode: SourceDiscoveryMode,
|
mode: SourceDiscoveryMode,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) async throws -> [MinecraftContentItem] {
|
) async throws {
|
||||||
_ = source
|
_ = source
|
||||||
_ = mode
|
_ = mode
|
||||||
_ = onDiscovered
|
_ = onDiscovered
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
||||||
@ -157,8 +156,8 @@ struct SourceAccessCoordinator: SourceAccessMethod {
|
|||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
mode: SourceDiscoveryMode,
|
mode: SourceDiscoveryMode,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) async throws -> [MinecraftContentItem] {
|
) async throws {
|
||||||
return try await accessMethod(for: source).discoverItems(
|
try await accessMethod(for: source).discoverItems(
|
||||||
for: source,
|
for: source,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
onDiscovered: onDiscovered
|
onDiscovered: onDiscovered
|
||||||
|
|||||||
@ -48,7 +48,7 @@ struct LocalFolderSourceAccess: SourceAccessMethod {
|
|||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
mode: SourceDiscoveryMode,
|
mode: SourceDiscoveryMode,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) async throws -> [MinecraftContentItem] {
|
) async throws {
|
||||||
guard case .localFolder(let bookmarkData) = source.origin else {
|
guard case .localFolder(let bookmarkData) = source.origin else {
|
||||||
throw SourceAccessError.accessFailed(
|
throw SourceAccessError.accessFailed(
|
||||||
reason: "No local-folder access method is configured for this source type."
|
reason: "No local-folder access method is configured for this source type."
|
||||||
@ -83,15 +83,16 @@ struct LocalFolderSourceAccess: SourceAccessMethod {
|
|||||||
|
|
||||||
if case .reconcile = mode,
|
if case .reconcile = mode,
|
||||||
let snapshot = source.snapshot {
|
let snapshot = source.snapshot {
|
||||||
return try discoverItemsByReconcilingCache(
|
try discoverItemsByReconcilingCache(
|
||||||
for: source,
|
for: source,
|
||||||
snapshot: snapshot,
|
snapshot: snapshot,
|
||||||
resolvedURL: resolvedURL,
|
resolvedURL: resolvedURL,
|
||||||
onDiscovered: onDiscovered
|
onDiscovered: onDiscovered
|
||||||
)
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return try WorldScanner.discoverItems(in: resolvedURL, onDiscovered: onDiscovered)
|
_ = try WorldScanner.discoverItems(in: resolvedURL, onDiscovered: onDiscovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
|
||||||
@ -137,7 +138,7 @@ struct LocalFolderSourceAccess: SourceAccessMethod {
|
|||||||
snapshot: SourceSnapshot,
|
snapshot: SourceSnapshot,
|
||||||
resolvedURL: URL,
|
resolvedURL: URL,
|
||||||
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
|
||||||
) throws -> [MinecraftContentItem] {
|
) throws {
|
||||||
let currentCollections = Dictionary(
|
let currentCollections = Dictionary(
|
||||||
uniqueKeysWithValues: WorldScanner.collectionSnapshots(in: resolvedURL).map { ($0.folderName, $0) }
|
uniqueKeysWithValues: WorldScanner.collectionSnapshots(in: resolvedURL).map { ($0.folderName, $0) }
|
||||||
)
|
)
|
||||||
@ -182,7 +183,6 @@ struct LocalFolderSourceAccess: SourceAccessMethod {
|
|||||||
for item in reconciledItems {
|
for item in reconciledItems {
|
||||||
onDiscovered(item)
|
onDiscovered(item)
|
||||||
}
|
}
|
||||||
return reconciledItems
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated private func topLevelCollectionName(for item: MinecraftContentItem, sourceRootURL: URL) -> String? {
|
nonisolated private func topLevelCollectionName(for item: MinecraftContentItem, sourceRootURL: URL) -> String? {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user