Archived
1
This repository has been archived on 2020-06-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tagtunes/TagTunes/LookupQueue.swift
Kim Wittenburg 0a485ff42a Stuff…
2019-02-01 22:59:01 +01:00

182 lines
7.5 KiB
Swift
Executable File

//
// LookupController.swift
// TagTunes
//
// Created by Kim Wittenburg on 07.03.16.
// Copyright © 2016 Kim Wittenburg. All rights reserved.
//
import SearchAPI
import AppKitPlus
/// The state of a lookup process.
public enum LookupState {
/// The track has not been processed for lookup. This is the initial state.
case Unprocessed
/// The track is being prepared for lookup. During preparation the track's id
/// is read.
case Preparing
/// The lookup is currently searching for the track's metadata.
case Searching
/// The track was found. The associated `Track` represents the found metadata
/// of the track.
case Found(Track)
/// The track has been looked up but was not found.
case NotFound
/// The track is not qualified for lookup because its id could not be read. The
/// associated value contains an error description identifying the reason why
/// the track's id could not be read.
case Unqualified(ErrorType)
/// During the lookup an error occured. This state is also used if the lookup
/// was cancelled by the user.
case Error(ErrorType?)
}
// TODO: Documentation
// DOCUMENTATION: All delegate methods are executed on main thread.
protocol LookupQueueDelegate: class {
/// Invoked before the `lookupController` will process the specified
/// `tracks`. This method may be invoked multiple times before
/// `lookupControllerDiDFinishLookup(_:)` is invoked, but it is guaranteed
/// that it will be invoked at least once. For more information see the
/// documentation for `LookupController`.
func lookupQueue(lookupQueue: LookupQueue, willBeginLookupForTracks tracks: [TagTunesTrack])
/// Invoked when the `lookupController` completes the lookup process for the
/// specified `tracks`. This method is called for tracks that could be
/// successfully looked up or that could not be looked up. There can also be
/// both in one invocation of this method. Use the `lookupState` of the
/// `TagTunesTrack` instances to get information about the lookup result for
/// individual tracks.
///
/// This method may be called multiple times in a row. Every track that has
/// been part of the `tracks` array of a
/// `lookupController(_:willBeginLookupForTracks:)` message will be part of
/// a invocation of this method exactly once.
///
/// - parameters:
/// - lookupController: The controller that did the lookup.
/// - tracks: The tracks that have been processed by the
/// `lookupController`. The order of the tracks has no meaning.
func lookupQueue(lookupQueue: LookupQueue, completedLookupForTracks tracks: [TagTunesTrack])
/// Invoked after the `lookupController` finished looking up all enqueued
/// tracks.
func lookupQueueDidFinishLookup(lookupQueue: LookupQueue)
}
/// The lookup controller looks up tracks' metadata online based on the iTunes
/// store ID embedded into the a file. To be notified about the lookup progress
/// set the controller's `delegate` property. There are some valid assumptions
/// you can make about the order in which the delgate methods are invoked:
///
/// 1. `lookupController(_:willBeginLookupForTracks:)` is the first of the
/// methods to be invoked.
/// 2. Every `TagTunesTrack` that is part of the `tracks` in
/// `lookupController(_:willBeginLookupForTracks:)` will be part of the
/// `tracks` of `lookupController(_:completedLookupForTracks:)` or.
/// 4. `lookupControllerDidFinishLookup(_:)` may only be called after (1) and (2)
/// are satisfied.
/// 5. After `lookupControllerDidFinishLookup(_:)` the next delegate message will
/// be `lookupController(_:willBeginLookupForTracks:)`.
///
/// The calls to `lookupController(_:willBeginLookupForTracks:)` and
/// `lookupControllerDidFinishLookup(_:)` are **not** balanced. There may be
/// multiple invokations of the former with only a single invokation of the
/// latter.
class LookupQueue: OperationQueue {
static let globalQueue = LookupQueue()
// TODO: Is there an alternative to two delegates
/// The lookup controller's delegate.
weak var lookupDelegate: LookupQueueDelegate?
var lookupOperation: LookupOperation?
private var aggregatedTracks = [TagTunesTrack]()
/// Enqueues the specified `tracks` for lookup. The point in time where the
/// `tracks` will actually be looked up is not defined. It may be when this
/// method returns or at some later point. To get notified when the lookup
/// actually starts implement the delgate method
/// `lookupController(_:willBeginLookupForTracks:)`.
///
/// - note: There is no correspondance between the specified `tracks` an the
/// tracks passed to the delegate methods. If there is currently a
/// lookup in progress the lookup controller might collect the
/// enqueued tracks and batch-process them.
func enqueueTracksForLookup<S: SequenceType where S.Generator.Element == TagTunesTrack>(tracks: S) {
aggregatedTracks.appendContentsOf(tracks)
for track in tracks {
let preparationOperation = LookupPreparationOperation(track: track)
addOperation(preparationOperation)
}
beginLookupIfPossible()
}
// DOCUMENTATION: Must execute on main thread
// FIXME: ? Can there be two lookup operations?
private func beginLookupIfPossible() {
// TODO: "Finished Lookup" delegate call
if lookupOperation == nil && !aggregatedTracks.isEmpty {
lookupOperation = LookupOperation(tracks: aggregatedTracks)
lookupOperation?.addObserver(BlockObserver(startHandler: { operation in
dispatch_sync(dispatch_get_main_queue()) {
// TODO: Is dispatch_sync ok?
let lookupOperation = operation as! LookupOperation
self.lookupDelegate?.lookupQueue(self, willBeginLookupForTracks: lookupOperation.tracks)
}
}, produceHandler: nil, finishHandler: { (operation, errors) in
dispatch_sync(dispatch_get_main_queue()) {
// TODO: Is this a retain cycle?
// TODO: Is dispatch_sync ok?
self.lookupOperation(operation, finishedWithErrors: errors)
}
}))
aggregatedTracks = []
for operation in operations where operation is LookupPreparationOperation {
lookupOperation?.addDependency(operation)
}
addOperation(lookupOperation!)
}
}
// DOCUMENTATION: Must execute on main thread.
private func lookupOperation(operation: Operation, finishedWithErrors errors: [ErrorType]) {
let lookupOperation = operation as! LookupOperation
self.lookupDelegate?.lookupQueue(self, completedLookupForTracks: lookupOperation.tracks)
self.lookupOperation = nil
if aggregatedTracks.isEmpty {
lookupDelegate?.lookupQueueDidFinishLookup(self)
} else {
beginLookupIfPossible()
}
}
/// Cancels the lookup. This will do three things:
///
/// 1. Stop any running or pending network request. Set the `lookupState` of
/// all tracks to `NSCocoaError.UserCancelledError`.
/// 2. Send the delegate a `lookupController(_:completedLookupForTracks:)`
/// message.
/// 3. Send the delegate a `lookupControllerDidFinishLookup(_:)` message.
func cancelLookup() {
// TODO: Implementation
}
}