182 lines
7.5 KiB
Swift
Executable File
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
|
|
}
|
|
|
|
}
|