Archived
1
This commit is contained in:
Kim Wittenburg
2019-02-01 22:59:01 +01:00
parent ba472864df
commit 0a485ff42a
82 changed files with 3975 additions and 1822 deletions

345
TagTunes/TagTunesItem.swift Executable file
View 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
}