Stuff…
This commit is contained in:
345
TagTunes/TagTunesItem.swift
Executable file
345
TagTunes/TagTunesItem.swift
Executable file
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// TagTunesItem.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.01.16.
|
||||
// Copyright © 2016 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import SearchAPI
|
||||
|
||||
/// This enum holds errors that may occur during saving.
|
||||
public enum SaveError: ErrorType {
|
||||
|
||||
/// This error indicates that the artwork could not be downloaded.
|
||||
case ArtworkDownloadFailed
|
||||
|
||||
/// This constant indicates that there was probably a error downloading the
|
||||
/// artwork. This is judged by the artwork's resolution.
|
||||
case LowResArtwork
|
||||
|
||||
}
|
||||
|
||||
/// A `TagTunesItem` (*item* for short) represents a `SearchAPIEntity` in the
|
||||
/// TagTunes application. There are two classes that conform to this protocol:
|
||||
/// `TagTunesEntityItem` (*entity*) and `TagTunesGroupItem` (*group*). The
|
||||
/// general structure is as follows:
|
||||
///
|
||||
/// - A group contains zero or more entities.
|
||||
/// - An entity can be contained in a group (but doesn't have to).
|
||||
/// - An entity contains zero or more associated tracks.
|
||||
public protocol TagTunesItem: class {
|
||||
|
||||
/// The `SearchAPIEntity` represented by the item. The `entity` should not
|
||||
/// change after initialization.
|
||||
var entity: SearchAPIEntity { get }
|
||||
|
||||
/// The `TagTunesTrack`s associated with the item. For groups this is the
|
||||
/// union of the `associatedTracks` of its children.
|
||||
var associatedTracks: Set<TagTunesTrack> { get }
|
||||
|
||||
/// Adds the specified `tracks` to the item's `associatedTracks`. For goups
|
||||
/// this sorts the `tracks` into its children. How the tracks are sorted is
|
||||
/// determined by the concrete group.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tracks: The `TagTunesTrack`s to be associated with the item.
|
||||
///
|
||||
/// - returns: For groups: The tracks that couldn't be sorted.
|
||||
/// For entities: An empty `Set`.
|
||||
func addAssociatedTracks<S: SequenceType where S.Generator.Element == TagTunesTrack>(tracks: S) -> Set<TagTunesTrack>
|
||||
|
||||
/// Removes all `associatedTracks` from the item.
|
||||
func clearAssociatedTracks()
|
||||
|
||||
/// Returns whether the item's `associatedTracks` already contain up-to-date
|
||||
/// tags. This value should respect the user's `Preferences`.
|
||||
var saved: Bool { get }
|
||||
|
||||
}
|
||||
|
||||
public func ==(lhs: TagTunesItem, rhs: TagTunesItem) -> Bool {
|
||||
return lhs.entity == rhs.entity
|
||||
}
|
||||
|
||||
/// Represents an entity item. An entity item directly corresponds to a file (for
|
||||
/// example a song, a movie, or a podcast episode). An entity may have a parent.
|
||||
/// An entity's parent does not usually correspond to a file but rather is an
|
||||
/// abstract grouping criterion (for example a album or a podcast).
|
||||
///
|
||||
/// `TagTunesEntityItem` is an abstract class that can not be initialized
|
||||
/// directly. Instead one of the concrete subclasses (like `SongItem`) should be
|
||||
/// used.
|
||||
public class TagTunesEntityItem: TagTunesItem {
|
||||
|
||||
public let entity: SearchAPIEntity
|
||||
|
||||
/// The entity's parent. The parent may change if the entity is added to a
|
||||
/// different group. The parent is nil for entities that do not belong to a
|
||||
/// group (such as movies or apps).
|
||||
public weak var parentItem: TagTunesGroupItem?
|
||||
|
||||
public var associatedTracks = Set<TagTunesTrack>() {
|
||||
willSet {
|
||||
for track in associatedTracks {
|
||||
track.entity = nil
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
for track in associatedTracks {
|
||||
track.entity = self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a new `TagTunesEntityItem` with the specified `entity`.
|
||||
///
|
||||
/// The class `TagTunesEntityItem` can not be initialized directly. Instead
|
||||
/// use one of the concrete subclasses like `SongItem`.
|
||||
public init(entity: SearchAPIEntity) {
|
||||
self.entity = entity
|
||||
assert(self.dynamicType != TagTunesEntityItem.self, "TagTunesEntityItem must not be initialized directly.")
|
||||
}
|
||||
|
||||
/// Initializes a new `TagTunesEntityItem` with the specified `entity` and
|
||||
/// `parentItem`. The newly initialized entity is **not** added to the
|
||||
/// `parentItem` automatically.
|
||||
///
|
||||
/// The class `TagTunesEntityItem` can not be initialized directly. Instead
|
||||
/// use one of the concrete subclasses like `SongItem`.
|
||||
public convenience init(entity: SearchAPIEntity, parentItem: TagTunesGroupItem?) {
|
||||
self.init(entity: entity)
|
||||
self.parentItem = parentItem
|
||||
}
|
||||
|
||||
public func addAssociatedTracks<S: SequenceType where S.Generator.Element == TagTunesTrack>(tracks: S) -> Set<TagTunesTrack> {
|
||||
associatedTracks.unionInPlace(tracks)
|
||||
return []
|
||||
}
|
||||
|
||||
public func clearAssociatedTracks() {
|
||||
associatedTracks.removeAll()
|
||||
}
|
||||
|
||||
public var saved: Bool {
|
||||
fatalError("Must override property saved.")
|
||||
}
|
||||
|
||||
/// Returns the value for the specified `tag`. This method should **not**
|
||||
/// act depending on the user's preferences.
|
||||
///
|
||||
/// - returns: The value for the specified `tag` or `nil` if such a value
|
||||
/// does not exist.
|
||||
///
|
||||
/// - throws: Errors that occur when reading the tag. This may for example
|
||||
/// happen if an artwork can not be downloaded.
|
||||
public func valueForTag(tag: Tag) throws -> AnyObject! {
|
||||
fatalError("Must override valueForTag(_:)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Represents a group item. A group item is the parent of zero or more entities.
|
||||
///
|
||||
/// A group usually does not directly correspond to a specific file but rather is
|
||||
/// an abstract grouping concept. A group would for example be an album (see
|
||||
/// class `AlbumItem`) with its children being the songs stored on disk.
|
||||
///
|
||||
/// Since groups can have children associated with them it may be neccessary to
|
||||
/// load those children sometime after the group has been initialized. The
|
||||
/// `TagTunesGroupItem` class offers the method `beginLoadingChildren()` to deal
|
||||
/// with those cases. See the documentation on that method for details.
|
||||
///
|
||||
/// `TagTunesGroupItem` is an abstract class that can not be initialized
|
||||
/// directly. Instead one of the concrete subclasses (like `AlbumItem`) should be
|
||||
/// used.
|
||||
public class TagTunesGroupItem: TagTunesItem {
|
||||
|
||||
/// This notification is posted by `TagTunesGroupItem`s when their state
|
||||
/// changes. Listening for this notification is the recommended way to react
|
||||
/// to groups starting or finishing loading their children.
|
||||
///
|
||||
/// The `object` associated with the respective notification is the group
|
||||
/// item whose state changed.
|
||||
public static let StateChangedNotificationName = "TagTunesGroupItemStateChangedNotificationName"
|
||||
|
||||
/// The state of a group item.
|
||||
public enum LoadingState {
|
||||
|
||||
/// The group is currently not active. This state is set before and after
|
||||
/// a group loads its children.
|
||||
case Normal
|
||||
|
||||
/// The group is currently loading its children.
|
||||
case Loading(NSURLSessionTask)
|
||||
|
||||
/// This state is basically equivalent to the `Normal` state except that
|
||||
/// indicates that the previous attempt to load the item's children
|
||||
/// failed.
|
||||
case Error(ErrorType)
|
||||
}
|
||||
|
||||
/// The URL session used to load children for groups.
|
||||
private static let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
|
||||
|
||||
public let entity: SearchAPIEntity
|
||||
|
||||
/// The group's children. It is recommended that subclasses install a
|
||||
/// property observer on this property and sort the children appropriately.
|
||||
public internal(set) var children = [TagTunesEntityItem]()
|
||||
|
||||
/// The state of the group. This state indicates whether the group is
|
||||
/// currently loading its children or not. See `LoadingState` for details.
|
||||
public private(set) var loadingState: LoadingState = .Normal {
|
||||
didSet {
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(TagTunesGroupItem.StateChangedNotificationName, object: self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new group representing the specified `entity`.
|
||||
///
|
||||
/// The class `TagTunesGroupItem` can not be initialized directly. Instead
|
||||
/// use one of the concrete subclasses like `AlbumItem`.
|
||||
public init(entity: SearchAPIEntity) {
|
||||
self.entity = entity
|
||||
assert(self.dynamicType != TagTunesGroupItem.self, "TagTunesGroupItem must not be initialized directly.")
|
||||
}
|
||||
|
||||
public var associatedTracks: Set<TagTunesTrack> {
|
||||
return children.reduce(Set()) { $0.union($1.associatedTracks) }
|
||||
}
|
||||
|
||||
public func addAssociatedTracks<S : SequenceType where S.Generator.Element == TagTunesTrack>(tracks: S) -> Set<TagTunesTrack> {
|
||||
fatalError("Must override addAssociatedTracks(_:)")
|
||||
}
|
||||
|
||||
/// Sorts the specified `track` into the children of the group. If there is
|
||||
/// no child representing the specified `childEntity` a new one is created.
|
||||
///
|
||||
/// In contrast to the `addAssociatedTracks(_:)` method this one can be
|
||||
/// called before the group has loaded its children. Tracks associated by
|
||||
/// this method are preserved when a group loads its children.
|
||||
///
|
||||
/// This method is especially useful if you want to associate the specified
|
||||
/// `track` with the `childEntity` but it is not clear whether the group did
|
||||
/// already load its children.
|
||||
///
|
||||
/// This method must be implemented by subclasses.
|
||||
public func addAssociatedTrack(track: TagTunesTrack, forChildEntity childEntity: SearchAPIEntity) {
|
||||
fatalError("Must override addAssociatedTrack:forChildEntity:")
|
||||
}
|
||||
|
||||
public func clearAssociatedTracks() {
|
||||
for child in children {
|
||||
child.clearAssociatedTracks()
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether all children have the same artist as the group itself.
|
||||
///
|
||||
/// This must be implemented by subclasses.
|
||||
public var hasCommonArtist: Bool {
|
||||
fatalError("Must override property hasCommonArtist")
|
||||
}
|
||||
|
||||
/// Starts a network request to load the group's children. This method
|
||||
/// returns immediately.
|
||||
///
|
||||
/// It is possible to monitor the loading of children through the group's
|
||||
/// `loadingState`. Whenever that state changes a
|
||||
/// `TagTunesGroupItem.StateChangedNotificationName` notification is posted
|
||||
/// with the respective group as the notification's `object`.
|
||||
///
|
||||
/// Normally this method should only be invoked once per group. A second
|
||||
/// invocation will still send the network request but the group will not
|
||||
/// change because its children are already loaded. The only exception to
|
||||
/// this rule is if the first request failed (as determined by the
|
||||
/// `LoadingState.Error` case). In that case you can *retry* by invoking this
|
||||
/// method again.
|
||||
public func beginLoadingChildren() {
|
||||
if case .Loading = loadingState {
|
||||
return
|
||||
}
|
||||
var request = SearchAPIRequest(lookupRequestWithID: entity.id)
|
||||
request.entity = self.dynamicType.childEntityType
|
||||
request.country = Preferences.sharedPreferences.iTunesStore
|
||||
if Preferences.sharedPreferences.useEnglishTags {
|
||||
request.language = .English
|
||||
}
|
||||
request.maximumNumberOfResults = 200
|
||||
let task = TagTunesGroupItem.urlSession.dataTaskWithURL(request.URL) { (data, response, error) -> Void in
|
||||
if let theError = error {
|
||||
self.loadingState = .Error(theError)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let result = try SearchAPIResult(data: data!)
|
||||
self.processLookupResult(result)
|
||||
self.loadingState = .Normal
|
||||
} catch let error as NSError {
|
||||
self.loadingState = .Error(error)
|
||||
}
|
||||
}
|
||||
loadingState = .Loading(task)
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/// Returns the `EntityType` of the group's children. This is used to
|
||||
/// construct an appropriate lookup request.
|
||||
///
|
||||
/// This property must be overridden by subclasses.
|
||||
internal class var childEntityType: SearchAPIRequest.EntityType {
|
||||
fatalError("Must override property childEntityType")
|
||||
}
|
||||
|
||||
/// Called when the group did finish loading its children. Subclasses must
|
||||
/// implement this method and should modify the `children` property to
|
||||
/// represent the `result` of the lookup.
|
||||
internal func processLookupResult(result: SearchAPIResult) {
|
||||
fatalError("Must override processLookupResult(_:)")
|
||||
}
|
||||
|
||||
/// Immediately stops loading the group's children and sets the
|
||||
/// `loadingState` to `Error(NSUserCancelledError)`.
|
||||
public func cancelLoadingChildren() {
|
||||
if case let .Loading(task) = loadingState {
|
||||
task.cancel()
|
||||
}
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||
loadingState = .Error(error)
|
||||
}
|
||||
|
||||
public var saved: Bool {
|
||||
for child in children {
|
||||
if !child.saved {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TagTunesEntityItem: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return Int(entity.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TagTunesGroupItem: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return Int(entity.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func ==(lhs: TagTunesEntityItem, rhs: TagTunesEntityItem) -> Bool {
|
||||
return lhs.entity.id == rhs.entity.id
|
||||
}
|
||||
|
||||
public func ==(lhs: TagTunesGroupItem, rhs: TagTunesGroupItem) -> Bool {
|
||||
return lhs.entity.id == rhs.entity.id
|
||||
}
|
||||
Reference in New Issue
Block a user