diff --git a/TagTunes.xcodeproj/project.pbxproj b/TagTunes.xcodeproj/project.pbxproj index 7677b1b..c7f1d6e 100644 --- a/TagTunes.xcodeproj/project.pbxproj +++ b/TagTunes.xcodeproj/project.pbxproj @@ -7,12 +7,23 @@ objects = { /* Begin PBXBuildFile section */ + 3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */; }; + 3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */; }; + 3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */; }; + 3B489DC31B90B116002B7EB3 /* Artwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC01B90B116002B7EB3 /* Artwork.swift */; }; + 3B489DC41B90B116002B7EB3 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC11B90B116002B7EB3 /* Album.swift */; }; + 3B489DC51B90B116002B7EB3 /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC21B90B116002B7EB3 /* Track.swift */; }; + 3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC61B90B38C002B7EB3 /* iTunes.swift */; }; + 3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DCA1B90B3E3002B7EB3 /* iTunes.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */; }; 3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7721B909B280025D550 /* AppDelegate.swift */; }; - 3B76C7751B909B280025D550 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7741B909B280025D550 /* ViewController.swift */; }; + 3B76C7751B909B280025D550 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7741B909B280025D550 /* MainViewController.swift */; }; 3B76C7771B909B280025D550 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7761B909B280025D550 /* Assets.xcassets */; }; 3B76C77A1B909B280025D550 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7781B909B280025D550 /* Main.storyboard */; }; 3B76C7851B909B280025D550 /* TagTunesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7841B909B280025D550 /* TagTunesTests.swift */; }; 3B76C7901B909B280025D550 /* TagTunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C78F1B909B280025D550 /* TagTunesUITests.swift */; }; + 3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */; }; + 3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -33,9 +44,20 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = ""; }; + 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TrackTableCellView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 3B489DC01B90B116002B7EB3 /* Artwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artwork.swift; sourceTree = ""; }; + 3B489DC11B90B116002B7EB3 /* Album.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; }; + 3B489DC21B90B116002B7EB3 /* Track.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Track.swift; sourceTree = ""; }; + 3B489DC61B90B38C002B7EB3 /* iTunes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iTunes.swift; sourceTree = ""; }; + 3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TagTunes-Bridging-Header.h"; sourceTree = ""; }; + 3B489DC91B90B3E3002B7EB3 /* iTunes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTunes.h; sourceTree = ""; }; + 3B489DCA1B90B3E3002B7EB3 /* iTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iTunes.m; sourceTree = ""; }; + 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AlbumTableCellView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 3B76C76F1B909B280025D550 /* TagTunes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TagTunes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3B76C7721B909B280025D550 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 3B76C7741B909B280025D550 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 3B76C7741B909B280025D550 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 3B76C7761B909B280025D550 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3B76C7791B909B280025D550 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 3B76C77B1B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -45,6 +67,8 @@ 3B76C78B1B909B280025D550 /* TagTunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagTunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B76C78F1B909B280025D550 /* TagTunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTunesUITests.swift; sourceTree = ""; }; 3B76C7911B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = ""; }; + 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKitPlus.framework; path = "../../../../Library/Developer/Xcode/DerivedData/TagTunes-ahlftzbggvvcneeglkkowfbohpzh/Build/Products/Debug/AppKitPlus.framework"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -52,6 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,12 +97,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3B285DBD1B9128FB00F0A2F1 /* Ultil */ = { + isa = PBXGroup; + children = ( + 3B489DC91B90B3E3002B7EB3 /* iTunes.h */, + 3B489DCA1B90B3E3002B7EB3 /* iTunes.m */, + ); + name = Ultil; + sourceTree = ""; + }; 3B76C7661B909B280025D550 = { isa = PBXGroup; children = ( 3B76C7711B909B280025D550 /* TagTunes */, 3B76C7831B909B280025D550 /* TagTunesTests */, 3B76C78E1B909B280025D550 /* TagTunesUITests */, + 3BBF710C1B95E02E00BB1EDB /* Frameworks */, 3B76C7701B909B280025D550 /* Products */, ); sourceTree = ""; @@ -95,11 +130,12 @@ 3B76C7711B909B280025D550 /* TagTunes */ = { isa = PBXGroup; children = ( - 3B76C7721B909B280025D550 /* AppDelegate.swift */, - 3B76C7741B909B280025D550 /* ViewController.swift */, - 3B76C7761B909B280025D550 /* Assets.xcassets */, - 3B76C7781B909B280025D550 /* Main.storyboard */, + 3B285DBD1B9128FB00F0A2F1 /* Ultil */, + 3B76C79D1B909B8C0025D550 /* Model */, + 3B76C79F1B909B960025D550 /* View */, + 3B76C79E1B909B910025D550 /* Controller */, 3B76C77B1B909B280025D550 /* Info.plist */, + 3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */, ); path = TagTunes; sourceTree = ""; @@ -122,6 +158,48 @@ path = TagTunesUITests; sourceTree = ""; }; + 3B76C79D1B909B8C0025D550 /* Model */ = { + isa = PBXGroup; + children = ( + 3B489DC11B90B116002B7EB3 /* Album.swift */, + 3B489DC01B90B116002B7EB3 /* Artwork.swift */, + 3B489DC61B90B38C002B7EB3 /* iTunes.swift */, + 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */, + 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */, + 3B489DC21B90B116002B7EB3 /* Track.swift */, + ); + name = Model; + sourceTree = ""; + }; + 3B76C79E1B909B910025D550 /* Controller */ = { + isa = PBXGroup; + children = ( + 3B76C7721B909B280025D550 /* AppDelegate.swift */, + 3B76C7741B909B280025D550 /* MainViewController.swift */, + 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */, + ); + name = Controller; + sourceTree = ""; + }; + 3B76C79F1B909B960025D550 /* View */ = { + isa = PBXGroup; + children = ( + 3B76C7781B909B280025D550 /* Main.storyboard */, + 3B76C7761B909B280025D550 /* Assets.xcassets */, + 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */, + 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */, + ); + name = View; + sourceTree = ""; + }; + 3BBF710C1B95E02E00BB1EDB /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -184,6 +262,7 @@ 3B76C7671B909B280025D550 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0700; ORGANIZATIONNAME = "Kim Wittenburg"; TargetAttributes = { @@ -251,8 +330,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3B76C7751B909B280025D550 /* ViewController.swift in Sources */, + 3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */, + 3B489DC31B90B116002B7EB3 /* Artwork.swift in Sources */, + 3B76C7751B909B280025D550 /* MainViewController.swift in Sources */, 3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */, + 3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */, + 3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */, + 3B489DC41B90B116002B7EB3 /* Album.swift in Sources */, + 3B489DC51B90B116002B7EB3 /* Track.swift in Sources */, + 3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */, + 3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */, + 3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */, + 3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -383,11 +472,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = TagTunes/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; @@ -395,11 +487,13 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = TagTunes/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h"; }; name = Release; }; @@ -474,6 +568,7 @@ 3B76C7961B909B280025D550 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 3B76C7971B909B280025D550 /* Build configuration list for PBXNativeTarget "TagTunesTests" */ = { isa = XCConfigurationList; @@ -482,6 +577,7 @@ 3B76C7991B909B280025D550 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 3B76C79A1B909B280025D550 /* Build configuration list for PBXNativeTarget "TagTunesUITests" */ = { isa = XCConfigurationList; @@ -490,6 +586,7 @@ 3B76C79C1B909B280025D550 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/TagTunes/Album.swift b/TagTunes/Album.swift new file mode 100644 index 0000000..54dd640 --- /dev/null +++ b/TagTunes/Album.swift @@ -0,0 +1,147 @@ +// +// Album.swift +// Tag for iTunes +// +// Created by Kim Wittenburg on 29.05.15. +// Copyright (c) 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +/// Represents an album of the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public class Album: iTunesType { + + // MARK: Properties + + public let id: iTunesId + + public let name: String + + public let censoredName: String + + public let viewURL: NSURL + + public let artwork: Artwork + + public let trackCount: Int + + public let releaseDate: NSDate + + public let genre: String + + public let artistName: String + + public private(set) var tracks = [Track]() + + // MARK: Initializers + + internal init(id: iTunesId, name: String, censoredName: String, viewURL: NSURL, artwork: Artwork, trackCount: Int, releaseDate: NSDate, genre: String, artistName: String) { + self.id = id + self.name = name + self.censoredName = censoredName + self.viewURL = viewURL + self.artwork = artwork + self.trackCount = trackCount + self.releaseDate = releaseDate + self.genre = genre + self.artistName = artistName + } + + public required init(data: [iTunesAPI.Field : AnyObject]) { + id = data[.CollectionId] as! UInt + name = data[.CollectionName] as! String + censoredName = data[.CollectionCensoredName] as! String + viewURL = NSURL(string: data[.CollectionViewUrl] as! String)! + + artwork = Artwork(data: data) + trackCount = data[.TrackCount] as! Int + releaseDate = iTunesAPI.sharedDateFormatter.dateFromString(data[.ReleaseDate] as! String)! + genre = data[.PrimaryGenreName] as! String + if let artistName = data[.CollectionArtistName] as? String { + self.artistName = artistName + } else { + artistName = data[.ArtistName] as! String + } + } + + public static var requiredFields: [iTunesAPI.Field] { + return [.CollectionId, .CollectionName, .CollectionCensoredName, .CollectionViewUrl, .TrackCount, .ReleaseDate, .PrimaryGenreName, .ArtistName] + } + + // MARK: Methods + + /// Adds a track to the album. + internal func addTrack(newTrack: Track) { + newTrack.album = self + var index = 0 + for track in tracks { + if newTrack.discNumber < track.discNumber { + break + } else if newTrack.discNumber == track.discNumber { + if newTrack.trackNumber <= track.trackNumber { + break + } + } + ++index + } + tracks.insert(newTrack, atIndex: index) + } + + /// Returns wether all tracks in the album have the same artist name as the + /// album itself. + public var hasSameArtistNameAsTracks: Bool { + for track in tracks { + if artistName != track.artistName { + return false + } + } + return true + } + + /// Saves the album to iTunes. + public func save() { + for track in tracks { + track.save() + } + } + + /// Saves the album's artwork to the directory specified in the user's + /// preferences (See `Preferences` for details). + public func saveArtwork() throws { + let url = Preferences.sharedPreferences.artworkTarget + try artwork.saveToURL(url, filename: name) + } + + /// Returns `true` if all tracks of the album are saved. + public var saved: Bool { + for track in tracks { + if !track.saved { + return false + } + } + return true + } + + +} + +extension Album: CustomStringConvertible { + + public var description: String { + return "\"\(name)\" (\(tracks.count) tracks)" + } + +} + +extension Album: Hashable { + + public var hashValue: Int { + return Int(id) + } + +} + +public func ==(lhs: Album, rhs: Album) -> Bool { + return lhs === rhs +} \ No newline at end of file diff --git a/TagTunes/AlbumTableCellView.swift b/TagTunes/AlbumTableCellView.swift new file mode 100644 index 0000000..d32670d --- /dev/null +++ b/TagTunes/AlbumTableCellView.swift @@ -0,0 +1,177 @@ +// +// AlbumTableCellView.swift +// TagTunes +// +// Created by Kim Wittenburg on 28.08.15. +// Copyright © 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa +import AppKitPlus + +/// A table cell view to represent an `Album`. This view can be initialized using +/// `initWithFrame`. +public class AlbumTableCellView: AdvancedTableCellView { + + // MARK: Types + + private struct Constants { + static let secondaryImageViewWidth: CGFloat = 17 + } + + // MARK: Properties + + /// Returns the receiver's `imageView` as an `AsyncImageView`. This property + /// replaces the `imageView` property. + /// + /// Intended to be used as accessory view. + @IBOutlet public var asyncImageView: AsyncImageView! = { + let imageView = AsyncImageView() + imageView.imageScaling = .ScaleProportionallyUpOrDown + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + /// Intended to be used as accessory view. + /// + /// Contains the *tick* image for saved albums. + @IBOutlet public lazy var secondaryImageView: NSImageView! = { + let secondaryImageView = NSImageView() + secondaryImageView.image = NSImage(named: "Tick") + secondaryImageView.imageScaling = .ScaleProportionallyDown + secondaryImageView.translatesAutoresizingMaskIntoConstraints = false + return secondaryImageView + }() + + /// Intended to be used as accessory view. + /// + /// Displayed for loading albums. + @IBOutlet public lazy var loadingIndicator: NSProgressIndicator! = { + let loadingIndicator = NSProgressIndicator() + loadingIndicator.style = .SpinningStyle + loadingIndicator.indeterminate = true + loadingIndicator.controlSize = .SmallControlSize + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false + return loadingIndicator + }() + + /// Intended to be used as accessory view. + /// + /// Displayed for search results. + @IBOutlet public lazy var button: NSButton! = { + let button = NSButton() + button.setButtonType(NSButtonType.MomentaryPushInButton) + button.bezelStyle = NSBezelStyle.RoundedBezelStyle + button.controlSize = .SmallControlSize + button.setContentHuggingPriority(NSLayoutPriorityDefaultHigh, forOrientation: .Horizontal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + @IBOutlet public lazy var errorButton: NSButton! = { + let errorButton = NSButton() + errorButton.setButtonType(NSButtonType.MomentaryChangeButton) + errorButton.bezelStyle = NSBezelStyle.ShadowlessSquareBezelStyle + errorButton.bordered = false + errorButton.imagePosition = .ImageOnly + errorButton.image = NSImage(named: NSImageNameCaution) + errorButton.translatesAutoresizingMaskIntoConstraints = false + return errorButton + }() + + // MARK: Initializers + + override public func setupView() { + style = .Subtitle + primaryTextField?.font = NSFont.boldSystemFontOfSize(0) + setLeftAccessoryView(asyncImageView, withConstraints: [NSLayoutConstraint( + item: asyncImageView, + attribute: .Width, + relatedBy: .Equal, + toItem: asyncImageView, + attribute: .Height, + multiplier: 1, + constant: 0)]) + super.setupView() + } + + // MARK: Overrides + + /// The receiver's imageView. + /// + /// - attention: Use `asyncImageView` instead. + override public var imageView: NSImageView? { + set { + asyncImageView = newValue as? AsyncImageView + } + get { + return asyncImageView + } + } + + // MARK: Methods + + /// Configures the receiver to display the specified `album`. + /// + /// - parameters: + /// - album: The album to be displayed. + /// - loading: `true` if a loading indicator should be displayed at the + /// album view. + public func setupForAlbum(album: Album, loading: Bool, error: NSError?) { + textField?.stringValue = album.name + secondaryTextField?.stringValue = album.artistName + asyncImageView.downloadImageFromURL(album.artwork.hiResURL) + if loading { + textField?.textColor = NSColor.disabledControlTextColor() + rightAccessoryView = loadingIndicator + } else if error != nil { + textField?.textColor = NSColor.redColor() + rightAccessoryView = errorButton + } else { + textField?.textColor = NSColor.controlTextColor() + if album.saved { + let aspectRatioConstraint = NSLayoutConstraint( + item: secondaryImageView, + attribute: .Width, + relatedBy: .Equal, + toItem: secondaryImageView, + attribute: .Height, + multiplier: 1, + constant: 0) + let widthConstraint = NSLayoutConstraint( + item: secondaryImageView, + attribute: .Width, + relatedBy: .Equal, + toItem: nil, + attribute: .Width, + multiplier: 1, + constant: 17) + setRightAccessoryView(secondaryImageView, withConstraints: [aspectRatioConstraint, widthConstraint]) + } else { + rightAccessoryView = nil + } + } + } + + /// Configures the receiver to display the specified `searchResult`. + /// + /// - parameters: + /// - searchResult: The search result to be displayed. + /// - selectable: `true` if the search result can be selected, `false` + /// otherwise. + public func setupForSearchResult(searchResult: SearchResult, selectable: Bool) { + textField?.stringValue = searchResult.name + textField?.textColor = NSColor.controlTextColor() + secondaryTextField?.stringValue = searchResult.artistName + asyncImageView.downloadImageFromURL(searchResult.artwork.hiResURL) + if selectable { + button.title = NSLocalizedString("Select", comment: "Button title for 'selecting a search result'") + button.enabled = true + } else { + button.title = NSLocalizedString("Added", comment: "Button title for a search result that is already present") + button.enabled = false + } + rightAccessoryView = button + } + +} diff --git a/TagTunes/AppDelegate.swift b/TagTunes/AppDelegate.swift index ef31f70..3cdfa83 100644 --- a/TagTunes/AppDelegate.swift +++ b/TagTunes/AppDelegate.swift @@ -9,18 +9,15 @@ import Cocoa @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { +internal class AppDelegate: NSObject, NSApplicationDelegate { - - - func applicationDidFinishLaunching(aNotification: NSNotification) { - // Insert code here to initialize your application + internal func applicationDidFinishLaunching(aNotification: NSNotification) { + Preferences.sharedPreferences.initializeDefaultValues() } - - func applicationWillTerminate(aNotification: NSNotification) { - // Insert code here to tear down your application + + internal func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool { + return true } - } diff --git a/TagTunes/Artwork.swift b/TagTunes/Artwork.swift new file mode 100644 index 0000000..527d58b --- /dev/null +++ b/TagTunes/Artwork.swift @@ -0,0 +1,106 @@ +// +// Artwork.swift +// Tag for iTunes +// +// Created by Kim Wittenburg on 30.05.15. +// Copyright (c) 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +/// Represents an Artwork from the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public class Artwork: iTunesType { + + // MARK: Properties + + /// The URL for the artwork, sized to 60x60 pixels. + public let url60: NSURL + + /// The URL for the artwork, sized to 100x100 pixels. + public let url100: NSURL + + /// The URL for the artwork with full resolution. + /// + /// - note: This URL is aquired by using an unofficial hack. If Apple changes + /// the way the full resolution artworks are stored this URL may be + /// `nil`. + public let hiResURL: NSURL! + + // MARK: Initializers + + public required init(data: [iTunesAPI.Field: AnyObject]) { + url60 = NSURL(string: data[.ArtworkUrl60] as! String)! + url100 = NSURL(string: data[.ArtworkUrl100] as! String)! + var hiResURLString = (data[.ArtworkUrl100] as! String) + let hotSpotRange = hiResURLString.rangeOfString("100x100", options: .BackwardsSearch, range: nil, locale: nil)! + hiResURLString.replaceRange(hotSpotRange, with: "1500x1500") + hiResURL = NSURL(string: hiResURLString) + } + + public static var requiredFields: [iTunesAPI.Field] { + return [.ArtworkUrl60, .ArtworkUrl100] + } + + // MARK: Methods + + /// Saves the high resolution artwork to the specified URL. If there is + /// alread a file at the specified URL it will be overwritten. + /// + /// - parameters: + /// - url: The URL of the directory the artwork is going to be saved to. + /// This must be a valid file URL. + /// - filename: The filename to be used (without the file extension). + public func saveToURL(url: NSURL, filename: String) throws { + let directory = url.filePathURL!.path! + let filePath = directory.stringByAppendingString("/\(filename).tiff") + + try NSFileManager.defaultManager().createDirectoryAtPath(directory, withIntermediateDirectories: true, attributes: nil) + let _ = NSFileManager.defaultManager().createFileAtPath(filePath, contents: hiResImage?.TIFFRepresentation, attributes: nil) + } + + // MARK: Calculated Properties + + private var cachedImage60: NSImage? + + /// Returns an `NSImage` instance of the image located at `url60`. + /// + /// - attention: The first time this method is called the current thread will + /// be blocked until the image is loaded. + public var image60: NSImage? { + if cachedImage60 == nil { + cachedImage60 = NSImage(byReferencingURL: url60) + } + return cachedImage60 + } + + private var cachedImage100: NSImage? + + /// Returns an `NSImage` instance of the image located at `url100`. + /// + /// - attention: The first time this method is called the current thread will + /// be blocked until the image is loaded. + public var image100: NSImage? { + if cachedImage100 == nil { + cachedImage100 = NSImage(byReferencingURL: url100) + } + return cachedImage100 + } + + private var cachedHiResImage: NSImage? + + /// Returns an `NSImage` instance of the image located at `hiResURL`. + /// + /// - attention: The first time this method is called the current thread will + /// be blocked until the image is loaded. + public var hiResImage: NSImage? { + if hiResURL == nil { + return nil + } + if cachedHiResImage == nil { + cachedHiResImage = NSImage(byReferencingURL: hiResURL) + } + return cachedHiResImage + } + +} diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128.png new file mode 100644 index 0000000..6a7433a Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128@2x.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128@2x.png new file mode 100644 index 0000000..2accf88 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/128x128@2x.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16.png new file mode 100644 index 0000000..7ecd10a Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16@2x.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16@2x.png new file mode 100644 index 0000000..3757d92 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/16x16@2x.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256.png new file mode 100644 index 0000000..c7453d5 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256@2x.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256@2x.png new file mode 100644 index 0000000..68f2732 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/256x256@2x.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32.png new file mode 100644 index 0000000..1ffbee6 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32@2x.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32@2x.png new file mode 100644 index 0000000..7f24371 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/32x32@2x.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512.png new file mode 100644 index 0000000..6eea786 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512@2x.png b/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512@2x.png new file mode 100644 index 0000000..f8ef9a4 Binary files /dev/null and b/TagTunes/Assets.xcassets/AppIcon.appiconset/512x512@2x.png differ diff --git a/TagTunes/Assets.xcassets/AppIcon.appiconset/Contents.json b/TagTunes/Assets.xcassets/AppIcon.appiconset/Contents.json index 2db2b1c..30fb232 100644 --- a/TagTunes/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/TagTunes/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,53 +1,63 @@ { "images" : [ { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "16x16.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "16x16@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "32x32.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "32x32@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "128x128.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "128x128@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "256x256.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "256x256@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "512x512.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "512x512@2x.png", "scale" : "2x" } ], diff --git a/TagTunes/Assets.xcassets/Contents.json b/TagTunes/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/TagTunes/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/Cross.imageset/Contents.json b/TagTunes/Assets.xcassets/Cross.imageset/Contents.json new file mode 100644 index 0000000..994e581 --- /dev/null +++ b/TagTunes/Assets.xcassets/Cross.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Cross.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Cross-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Cross-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png b/TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png new file mode 100644 index 0000000..db8eb36 Binary files /dev/null and b/TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png differ diff --git a/TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png b/TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png new file mode 100644 index 0000000..db8eb36 Binary files /dev/null and b/TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png differ diff --git a/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/Contents.json b/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/Contents.json new file mode 100644 index 0000000..cbee736 --- /dev/null +++ b/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PauseProgressFreestandingTemplate.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/PauseProgressFreestandingTemplate.pdf b/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/PauseProgressFreestandingTemplate.pdf new file mode 100644 index 0000000..72c9322 --- /dev/null +++ b/TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/PauseProgressFreestandingTemplate.pdf @@ -0,0 +1,29 @@ +%PDF-1.4 +% +1 0 obj <>stream +x͒1N1 E{'l''W@@ۭ`fb>L +EJoG@$ /. _ `wGx97SI[od@qY%TCXԈRfC-J 5oTWYM;v}$ +G9ginS}P#gr;WCp345R֡2X񗡿gϐKu5q{+:C~oPK +endstream +endobj +3 0 obj<>/Parent 2 0 R/MediaBox[0 0 1024 1024]>> +endobj +2 0 obj<> +endobj +4 0 obj<> +endobj +5 0 obj<> +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000458 00000 n +0000000318 00000 n +0000000508 00000 n +0000000552 00000 n +trailer +<]/Root 4 0 R/Size 6>> +startxref +757 +%%EOF diff --git a/TagTunes/Assets.xcassets/PreferenceTags.imageset/Contents.json b/TagTunes/Assets.xcassets/PreferenceTags.imageset/Contents.json new file mode 100644 index 0000000..67dfe8f --- /dev/null +++ b/TagTunes/Assets.xcassets/PreferenceTags.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PreferencesTags.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PreferencesTags-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PreferencesTags-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-1.png b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-1.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-1.png differ diff --git a/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-2.png b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-2.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-2.png differ diff --git a/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags.png b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags.png differ diff --git a/TagTunes/Assets.xcassets/PreferencesTags.imageset/Contents.json b/TagTunes/Assets.xcassets/PreferencesTags.imageset/Contents.json new file mode 100644 index 0000000..67dfe8f --- /dev/null +++ b/TagTunes/Assets.xcassets/PreferencesTags.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PreferencesTags.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PreferencesTags-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PreferencesTags-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-1.png b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-1.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-1.png differ diff --git a/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-2.png b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-2.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-2.png differ diff --git a/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags.png b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags.png new file mode 100644 index 0000000..2519041 Binary files /dev/null and b/TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags.png differ diff --git a/TagTunes/Assets.xcassets/SaveToITunes.imageset/Contents.json b/TagTunes/Assets.xcassets/SaveToITunes.imageset/Contents.json new file mode 100644 index 0000000..0b5a487 --- /dev/null +++ b/TagTunes/Assets.xcassets/SaveToITunes.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "SaveToITunes.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SaveToITunes-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SaveToITunes-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-1.png b/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-1.png new file mode 100644 index 0000000..bcdb8da Binary files /dev/null and b/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-1.png differ diff --git a/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-2.png b/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-2.png new file mode 100644 index 0000000..bcdb8da Binary files /dev/null and b/TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-2.png differ diff --git a/TagTunes/Assets.xcassets/TickBW.imageset/Contents.json b/TagTunes/Assets.xcassets/TickBW.imageset/Contents.json new file mode 100644 index 0000000..0ca0f7a --- /dev/null +++ b/TagTunes/Assets.xcassets/TickBW.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "TickBW.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "TickBW-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "TickBW-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-1.png b/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-1.png new file mode 100644 index 0000000..c1b16de Binary files /dev/null and b/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-1.png differ diff --git a/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-2.png b/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-2.png new file mode 100644 index 0000000..c1b16de Binary files /dev/null and b/TagTunes/Assets.xcassets/TickBW.imageset/TickBW-2.png differ diff --git a/TagTunes/Assets.xcassets/iTunes.dataset/Contents.json b/TagTunes/Assets.xcassets/iTunes.dataset/Contents.json new file mode 100644 index 0000000..6baffea --- /dev/null +++ b/TagTunes/Assets.xcassets/iTunes.dataset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "data" : [ + { + "idiom" : "universal" + } + ] +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/iTunes.imageset/Contents.json b/TagTunes/Assets.xcassets/iTunes.imageset/Contents.json new file mode 100644 index 0000000..4e7e267 --- /dev/null +++ b/TagTunes/Assets.xcassets/iTunes.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "iTunes-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "iTunes-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "iTunes.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-1.png b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-1.png new file mode 100644 index 0000000..2f88824 Binary files /dev/null and b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-1.png differ diff --git a/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-2.png b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-2.png new file mode 100644 index 0000000..2f88824 Binary files /dev/null and b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes-2.png differ diff --git a/TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png new file mode 100644 index 0000000..2f88824 Binary files /dev/null and b/TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png differ diff --git a/TagTunes/Base.lproj/Main.storyboard b/TagTunes/Base.lproj/Main.storyboard index f4ec4ad..f68d6cd 100644 --- a/TagTunes/Base.lproj/Main.storyboard +++ b/TagTunes/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -21,7 +21,11 @@ - + + + + + @@ -155,6 +159,9 @@ + +CA + @@ -641,20 +648,47 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -664,18 +698,233 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + diff --git a/TagTunes/Error Handler.swift b/TagTunes/Error Handler.swift new file mode 100644 index 0000000..1433b75 --- /dev/null +++ b/TagTunes/Error Handler.swift @@ -0,0 +1,13 @@ +// +// Error Handler.swift +// TagTunes +// +// Created by Kim Wittenburg on 30.08.15. +// Copyright © 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +class Error_Handler: T { + +} diff --git a/TagTunes/Info.plist b/TagTunes/Info.plist index ea0858a..d6a8f1a 100644 --- a/TagTunes/Info.plist +++ b/TagTunes/Info.plist @@ -21,7 +21,9 @@ CFBundleSignature ???? CFBundleVersion - 1 + 4242 + LSApplicationCategoryType + public.app-category.music LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/TagTunes/MainViewController.swift b/TagTunes/MainViewController.swift new file mode 100644 index 0000000..e22344c --- /dev/null +++ b/TagTunes/MainViewController.swift @@ -0,0 +1,754 @@ +// +// ViewController.swift +// TagTunes +// +// Created by Kim Wittenburg on 28.08.15. +// Copyright © 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa +import AppKitPlus + +internal class MainViewController: NSViewController { + + // MARK: Types + + private struct OutlineViewConstants { + struct ViewIdentifiers { + static let simpleTableCellViewIdentifier = "SimpleTableCellViewIdentifier" + static let centeredTableCellViewIdentifier = "CenteredTableCellViewIdentifier" + static let albumTableCellViewIdentifier = "AlbumTableCellViewIdentifier" + static let trackTableCellViewIdentifier = "TrackTableCellViewIdentifier" + } + + struct Items { + static let loadingItem: AnyObject = "LoadingItem" + static let noResultsItem: AnyObject = "NoResultsItem" + + static let searchResultsHeaderItem: AnyObject = MainViewController.Section.SearchResults.rawValue + static let albumsHeaderItem: AnyObject = MainViewController.Section.Albums.rawValue + static let unsortedTracksHeaderItem: AnyObject = MainViewController.Section.UnsortedTracks.rawValue + } + + static let pasteboardType = "public.item.tagtunes" + } + + internal enum Section: String { + case SearchResults = "SearchResults" + case Albums = "Albums" + case UnsortedTracks = "UnsortedTracks" + + static func isHeaderItem(item: AnyObject) -> Bool { + if let itemAsString = item as? String { + return Section(rawValue: itemAsString) != nil + } else { + return false + } + } + } + + // MARK: IBOutlets + + @IBOutlet private weak var outlineView: NSOutlineView! + + // MARK: Properties + + /// Used for searching and loading search results + private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue()) + + /// The URL task currently loading the search results + private var searchTask: NSURLSessionDataTask? + + /// If `true` the search section is displayed at the top of the + /// `outlineView`. + internal var showsSearch: Bool = false + + /// `true` if there is currently a search in progress. + internal var searching: Bool { + return searchTask != nil + } + + /// The error that occured during searching, if any. + internal private(set) var searchError: NSError? + + /// The URL tasks currently loading the tracks for the respective albums. + private var trackTasks = [Album: NSURLSessionDataTask]() + + /// Errors that occured during loading the tracks for the respective album. + private var trackErrors = [Album: NSError]() + + // MARK: Overrides + + override internal func viewDidLoad() { + super.viewDidLoad() + outlineView.setDraggingSourceOperationMask(.Move, forLocal: true) + outlineView.registerForDraggedTypes([OutlineViewConstants.pasteboardType]) + } + + // MARK: Outline View Content + + internal private(set) var searchResults = [SearchResult]() + + internal private(set) var albums = [Album]() + + internal private(set) var unsortedTracks = [iTunesTrack]() + + /// Returns all `iTunesTrack` objects that are somewhere down the outline + /// view. + private var allITunesTracks: Set { + return Set(unsortedTracks).union(albums.flatMap({ $0.tracks.flatMap { $0.associatedTracks } })) + } + + /// Returns all contents of the outline view. + /// + /// This property is regenerated every time it is queried. If you need to + /// access it a lot of times it is recommended to chache it into a local + /// variable. + private var outlineViewContents: [AnyObject] { + var contents = [AnyObject]() + if showsSearch { + contents.append(OutlineViewConstants.Items.searchResultsHeaderItem) + if !searchResults.isEmpty { + contents.extend(searchResults as [AnyObject]) + } else if searching { + contents.append(OutlineViewConstants.Items.loadingItem) + } else if let error = searchError { + contents.append(error) + } else { + contents.append(OutlineViewConstants.Items.noResultsItem) + } + if !albums.isEmpty { + contents.append(OutlineViewConstants.Items.albumsHeaderItem) + } + } + contents.extend(albums as [AnyObject]) + if !unsortedTracks.isEmpty { + contents.append(OutlineViewConstants.Items.unsortedTracksHeaderItem) + contents.extend(unsortedTracks as [AnyObject]) + } + return contents + } + + /// Returns the section of the specified row or `nil` if the row is not a + /// valid row. + internal func sectionOfRow(row: Int) -> Section? { + if row < 0 { + return nil + } + var relativeRow = row + if showsSearch { + let searchRelatedItemCount = 1 + (searchResults.isEmpty ? 1 : searchResults.count) + if relativeRow < searchRelatedItemCount { + return .SearchResults + } else { + relativeRow -= searchRelatedItemCount + } + } + var maxRow = outlineView.numberOfRows + if !unsortedTracks.isEmpty { + maxRow -= unsortedTracks.count + 1 + } + if relativeRow < maxRow { + return .Albums + } else { + relativeRow -= maxRow + } + if relativeRow < unsortedTracks.count + 1 { + return .UnsortedTracks + } + return nil + } + + /// Returns the section the specified item resides in or `nil` if the item is + /// not part of the outline view's contents. + internal func sectionOfItem(item: AnyObject) -> Section? { + if let album = item as? Album where albums.contains(album) { + return .Albums + } else if let track = item as? Track where albums.contains(track.album) { + return .Albums + } else if let track = item as? iTunesTrack { + if let parentTrack = outlineView.parentForItem(track) as? Track where albums.contains(parentTrack.album) { + return .Albums + } else { + return unsortedTracks.contains(track) ? .UnsortedTracks : nil + } + } else if item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError { + return .SearchResults + } else if let string = item as? String { + return Section(rawValue: string) + } else { + return nil + } + } + + /// Returns `true` if the specified `album` is currently loading its tracks. + internal func isAlbumLoading(album: Album) -> Bool { + return trackTasks[album] != nil + } + + // MARK: Searching + + /// Starts a search for the specified search term. Calling this method + internal func beginSearchForTerm(term: String) { + searchTask?.cancel() + searchResults.removeAll() + if let url = iTunesAPI.createAlbumSearchURLForTerm(term) { + showsSearch = true + searchTask = urlSession.dataTaskWithURL(url, completionHandler: processSearchResults) + searchTask?.resume() + } else { + showsSearch = false + } + outlineView.reloadData() + } + + /// Processes the data returned from a network request into the + /// `searchResults`. + private func processSearchResults(data: NSData?, response: NSURLResponse?, error: NSError?) { + searchTask = nil + if let theError = error { + searchError = theError + } else if let theData = data { + do { + let searchResults = try iTunesAPI.parseAPIData(theData).map { SearchResult(representedAlbum: $0) } + self.searchResults = searchResults + } catch let error as NSError { + searchError = error + } + } + showsSearch = true + outlineView.reloadData() + } + + /// Adds the search result at the specified `row` to the albums section and + /// begins loading its tracks. + internal func selectSearchResultAtRow(row: Int) { + guard sectionOfRow(row) == .SearchResults else { + return + } + let searchResult = outlineView.itemAtRow(row) as! SearchResult + if !Preferences.sharedPreferences.keepSearchResults { + searchResults.removeAll() + showsSearch = false + } + var albumAlreadyPresent = false + for album in albums { + if album == searchResult { + albumAlreadyPresent = true + } + } + if !albumAlreadyPresent { + albums.append(beginLoadingTracksForSearchResult(searchResult)) + } + outlineView.reloadData() + } + + // MARK: Albums + + private func beginLoadingTracksForSearchResult(searchResult: SearchResult) -> Album { + let album = Album(searchResult: searchResult) + let url = iTunesAPI.createAlbumLookupURLForId(album.id) + let task = urlSession.dataTaskWithURL(url) { (data, response, var error) -> Void in + self.trackTasks[album] = nil + do { + if let theData = data { + let newAlbum = try iTunesAPI.parseAPIData(theData)[0] + let index = self.albums.indexOf(album)! + self.albums.removeAtIndex(index) + self.albums.insert(newAlbum, atIndex: index) + } + } catch let theError as NSError { + error = theError + } catch _ { + // Will never happen + } + self.trackErrors[album] = error + self.outlineView.reloadData() + } + trackTasks[album] = task + task.resume() + return album + } + + func cancelLoadingTracksForAlbum(album: Album) { + trackTasks[album]?.cancel() + trackTasks[album] = nil + } + + private func saveTracks(tracks: [Track: [iTunesTrack]]) { + let numberOfTracks = tracks.reduce(0) { (count: Int, element: (key: Track, value: [iTunesTrack])) -> Int in + return count + element.value.count + } + let progress = NSProgress(totalUnitCount: Int64(numberOfTracks)) + NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving tracks…", comment: "Alert message indicating that the selected tracks are currently being saved") + NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription + for (parentTrack, targetTracks) in tracks { + for track in targetTracks { + parentTrack.saveToTrack(track) + ++progress.completedUnitCount + NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription + } + } + } + + private func saveArtworks(tracks: [Track: [iTunesTrack]]) { + var albums = Set() + for (track, _) in tracks { + albums.insert(track.album) + } + + let progress = NSProgress(totalUnitCount: Int64(albums.count)) + var errorCount = 0 + NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving artworks…", comment: "Alert message indicating that the artworks for the selected tracks are currently being saved") + NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription + for album in albums { + do { + try album.saveArtwork() + } catch _ { + ++errorCount + } + ++progress.completedUnitCount + NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription + } + if errorCount > 0 { + dispatch_sync(dispatch_get_main_queue()) { + let alert = NSAlert() + if errorCount == 1 { + alert.messageText = NSLocalizedString("1 artwork could not be saved.", comment: "Error message indicating that one of the artworks could not be saved.") + } else { + alert.messageText = String(format: NSLocalizedString("%d artworks could not be saved.", comment: "Error message indicating that n artworks could not be saved."), errorCount) + } + alert.informativeText = NSLocalizedString("Please check your privileges for the folder you set in the preferences and try again.", comment: "Informative text for 'artwork(s) could not be saved' errors") + alert.alertStyle = .WarningAlertStyle + alert.addButtonWithTitle("OK") + alert.beginSheetModalForWindow(self.view.window!, completionHandler: nil) + } + } + } + + // MARK: Actions + + /// Adds the current iTunes selection + @IBAction internal func addITunesSelection(sender: AnyObject?) { + if !iTunes.running { + let alert = NSAlert() + alert.messageText = NSLocalizedString("iTunes is not running", comment: "Error message informing the user that iTunes is not currently running.") + alert.informativeText = NSLocalizedString("Please launch iTunes and try again.", comment: "Informative text for the 'iTunes is not running' error") + alert.addButtonWithTitle(NSLocalizedString("OK", comment: "Button title")) + alert.beginSheetModalForWindow(view.window!, completionHandler: nil) + } else if let selection = iTunes.selection.get() as? [iTunesTrack] { + let newTracks = Set(selection).subtract(allITunesTracks) + unsortedTracks.extend(newTracks) + outlineView.reloadData() + } + } + + /// Begins to search for the `sender`'s `stringValue`. + @IBAction internal func performSearch(sender: AnyObject?) { + if let searchTerm = sender?.stringValue { + beginSearchForTerm(searchTerm) + } + } + + /// Selects the search result associated with the `sender`'s row (as + /// determined by `NSOutlineView.rowForView`) and adds it to the list of + /// albums. + @IBAction private func selectSearchResult(sender: AnyObject?) { + if let view = sender as? NSView { + let row = outlineView.rowForView(view) + selectSearchResultAtRow(row) + } + } + + /// Saves the selected items to iTunes. The saving process will be reported + /// to the user in a progress sheet. + @IBAction internal func performSave(sender: AnyObject?) { + var itemsToBeSaved = [Track: [iTunesTrack]]() + for row in outlineView.selectedRowIndexes where sectionOfRow(row) == .Albums { + let item = outlineView.itemAtRow(row) + if let album = item as? Album { + for track in album.tracks where !track.associatedTracks.isEmpty { + itemsToBeSaved[track] = track.associatedTracks + } + } else if let track = item as? Track { + itemsToBeSaved[track] = track.associatedTracks + } else if let track = item as? iTunesTrack { + if let parentTrack = outlineView.parentForItem(track) as? Track { + if itemsToBeSaved[parentTrack] != nil { + itemsToBeSaved[parentTrack]?.append(track) + } else { + itemsToBeSaved[parentTrack] = [track] + } + } + } + } + guard !itemsToBeSaved.isEmpty else { + return + } + let progress = NSProgress(totalUnitCount: 100) + progress.beginProgressSheetModalForWindow(self.view.window!) { + reponse in + self.outlineView.reloadData() + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { + if Preferences.sharedPreferences.saveArtwork { + progress.becomeCurrentWithPendingUnitCount(90) + } else { + progress.becomeCurrentWithPendingUnitCount(100) + } + self.saveTracks(itemsToBeSaved) + progress.resignCurrent() + if Preferences.sharedPreferences.saveArtwork { + progress.becomeCurrentWithPendingUnitCount(10) + self.saveArtworks(itemsToBeSaved) + progress.resignCurrent() + } + } + } + + /// Removes the selected items from the outline view. + @IBAction internal func delete(sender: AnyObject?) { + let items = outlineView.selectedRowIndexes.map { ($0, outlineView.itemAtRow($0)) } + for (row, item) in items { + if sectionOfRow(row)! != .SearchResults { + if let album = item as? Album { + cancelLoadingTracksForAlbum(album) + albums.removeElement(album) + } else if let track = item as? Track { + track.associatedTracks = [] + } else if let track = item as? iTunesTrack { + if let parentTrack = outlineView.parentForItem(track) as? Track { + parentTrack.associatedTracks.removeElement(track) + } else { + unsortedTracks.removeElement(track) + } + } + } + } + outlineView.reloadData() + } + + /// Action that should be triggered from a view inside the outline view. If + /// `sender` is not an `NSError` instance the item at the row associated with + /// the `sender` (as determined by `NSOutlineView.rowForView`) should be a + /// `NSError` or `Album` instance for this method to work correctly. + @IBAction private func showErrorDetails(sender: AnyObject?) { + var error: NSError + if let theError = sender as? NSError { + error = theError + } else if let view = sender as? NSView { + let row = outlineView.rowForView(view) + let item = outlineView.itemAtRow(row) + if let theError = item as? NSError { + error = theError + } else if let album = item as? Album { + if let theError = trackErrors[album] { + error = theError + } else { + return + } + } else { + return + } + } else { + return + } + presentError(error, modalForWindow: view.window!, delegate: nil, didPresentSelector: nil, contextInfo: nil) + } + +} + +// MARK: - User Interface Validations + +extension MainViewController: NSUserInterfaceValidations { + + func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool { + if anItem.action() == "performSave:" { + for row in outlineView.selectedRowIndexes { + return sectionOfRow(row) == .Albums + } + } else if anItem.action() == "addITunesSelection:" { + guard iTunes.running else { + return false + } + return !(iTunes.selection.get() as! [AnyObject]).isEmpty + } else if anItem.action() == "delete:" { + for row in outlineView.selectedRowIndexes { + if sectionOfRow(row) != .SearchResults { + return true + } + } + } + return false + } + +} + +// MARK: - Outline View + +extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate { + + func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int { + if item == nil { + return outlineViewContents.count + } else if let album = item as? Album { + return album.tracks.count + } else if let track = item as? Track { + return track.associatedTracks.count + } else { + return 0 + } + } + + func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject { + if item == nil { + return outlineViewContents[index] + } else if let album = item as? Album { + return album.tracks[index] + } else if let track = item as? Track { + return track.associatedTracks[index] + } else { + return "" + } + } + + func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool { + return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0 + } + + func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool { + return Section.isHeaderItem(item) + } + + func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool { + return !(self.outlineView(outlineView, isGroupItem: item) || item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError) + } + + func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat { + if item is Album || item is SearchResult { + return 39 + } else if let track = item as? Track { + return track.album.hasSameArtistNameAsTracks ? 24 : 31 + } else if item === OutlineViewConstants.Items.loadingItem { + return 39 + } else if item === OutlineViewConstants.Items.noResultsItem || item is NSError { + return 32 + } else if Section.isHeaderItem(item) { + return 24 + } else { + return 17 + } + } + + func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? { + if item === OutlineViewConstants.Items.searchResultsHeaderItem { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView + if view == nil { + view = AdvancedTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier + } + view?.style = .Simple + view?.textField?.font = NSFont.boldSystemFontOfSize(0) + view?.textField?.textColor = NSColor.disabledControlTextColor() + view?.textField?.stringValue = NSLocalizedString("Search Results", comment: "Header name for the seach results section") + return view + } + if item === OutlineViewConstants.Items.albumsHeaderItem { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView + if view == nil { + view = AdvancedTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier + } + view?.style = .Simple + view?.textField?.font = NSFont.boldSystemFontOfSize(0) + view?.textField?.textColor = NSColor.disabledControlTextColor() + view?.textField?.stringValue = NSLocalizedString("Albums", comment: "Header name for the albums section") + return view + } + if item === OutlineViewConstants.Items.unsortedTracksHeaderItem { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView + if view == nil { + view = AdvancedTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier + } + view?.style = .Simple + view?.textField?.font = NSFont.boldSystemFontOfSize(0) + view?.textField?.textColor = NSColor.disabledControlTextColor() + view?.textField?.stringValue = NSLocalizedString("Unsorted Tracks", comment: "Header name for the unsorted tracks section") + return view + } + if item === OutlineViewConstants.Items.loadingItem { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView + if view == nil { + view = CenteredTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier + } + view?.setupForLoading() + return view + } + if item === OutlineViewConstants.Items.noResultsItem { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView + if view == nil { + view = CenteredTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier + } + view?.setupForMessage(NSLocalizedString("No Results", comment: "Message informing the user that the search didn't return any results")) + return view + } + if let error = item as? NSError { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView + if view == nil { + view = CenteredTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier + } + view?.button?.target = self + view?.button?.action = "showErrorDetails:" + view?.setupForError(error, errorMessage: NSLocalizedString("Failed to load results", comment: "Error message informing the user that an error occured during searching.")) + return view + } + if let album = item as? Album { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView + if view == nil { + view = AlbumTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier + } + view?.setupForAlbum(album, loading: isAlbumLoading(album), error: trackErrors[album]) + view?.errorButton?.target = self + view?.errorButton?.action = "showErrorDetails:" + return view + } + if let searchResult = item as? SearchResult { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView + if view == nil { + view = AlbumTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier + } + view?.button.target = self + view?.button.action = "selectSearchResult:" + let selectable = albums.filter { $0.id == searchResult.id }.isEmpty + view?.setupForSearchResult(searchResult, selectable: selectable) + return view + } + if let track = item as? Track { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier, owner: nil) as? TrackTableCellView + if view == nil { + view = TrackTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier + } + view?.setupForTrack(track) + return view + } + if let track = item as? iTunesTrack { + var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView + if view == nil { + view = AdvancedTableCellView() + view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier + } + view?.style = .Simple + view?.textField?.font = NSFont.systemFontOfSize(0) + view?.textField?.textColor = NSColor.textColor() + view?.textField?.stringValue = track.name + return view + } + return nil + } + + func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool { + var rows = [Int]() + var containsValidItems = false + for item in items { + let row = outlineView.rowForItem(item) + rows.append(row) + if sectionOfRow(row) != .SearchResults { + containsValidItems = true + } + } + if !containsValidItems { + return false + } + let data = NSKeyedArchiver.archivedDataWithRootObject(rows) + pasteboard.declareTypes([OutlineViewConstants.pasteboardType], owner: nil) + pasteboard.setData(data, forType: OutlineViewConstants.pasteboardType) + return true + } + + func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation { + let firstUnsortedRow = outlineViewContents.count - (unsortedTracks.isEmpty ? 0 : unsortedTracks.count+1) + // Drop in the 'unsorted' section + if item == nil && index >= firstUnsortedRow || item === OutlineViewConstants.Items.unsortedTracksHeaderItem { + outlineView.setDropItem(nil, dropChildIndex: outlineViewContents.count) + return .Every + } + // Drop on iTunesTrack item or between items + if index != NSOutlineViewDropOnItemIndex || item is iTunesTrack { + return .None + } + // Drop on header row + if item != nil && self.outlineView(outlineView, isGroupItem: item!) { + return .None + } + // Drop in 'search results' section + let row = outlineView.rowForItem(item) + if sectionOfRow(row) == .SearchResults { + return .None + } + if let album = item as? Album where isAlbumLoading(album) || trackErrors[album] != nil { + return .None + } + return .Every + } + + func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool { + guard let data = info.draggingPasteboard().dataForType(OutlineViewConstants.pasteboardType), draggedRows = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [Int] else { + return false + } + // Get the dragged tracks and remove them from their previous location + var draggedTracks = Set() + for row in draggedRows { + if sectionOfRow(row) != .SearchResults { + let item = outlineView.itemAtRow(row) + if let album = item as? Album { + for track in album.tracks { + draggedTracks.unionInPlace(track.associatedTracks) + track.associatedTracks.removeAll() + } + } else if let track = item as? Track { + draggedTracks.unionInPlace(track.associatedTracks) + track.associatedTracks.removeAll() + } else if let track = item as? iTunesTrack { + draggedTracks.insert(track) + if let parentTrack = outlineView.parentForItem(track) as? Track { + parentTrack.associatedTracks.removeElement(track) + } else { + unsortedTracks.removeElement(track) + } + } + } + } + + // Add the dragged tracks to the new target + if let targetTrack = item as? Track { + targetTrack.associatedTracks.extend(draggedTracks) + } else if let targetAlbum = item as? Album { + for draggedTrack in draggedTracks { + var inserted = false + for track in targetAlbum.tracks { + if (draggedTrack.discNumber == track.discNumber || draggedTrack.discNumber == 0) && draggedTrack.trackNumber == track.trackNumber { + track.associatedTracks.append(draggedTrack) + inserted = true + break + } + } + if !inserted { + unsortedTracks.append(draggedTrack) + } + } + } else { + unsortedTracks.extend(draggedTracks) + } + outlineView.reloadData() + return true + } + +} + diff --git a/TagTunes/Preferences.swift b/TagTunes/Preferences.swift new file mode 100644 index 0000000..9e44c3a --- /dev/null +++ b/TagTunes/Preferences.swift @@ -0,0 +1,69 @@ +// +// Preferences.swift +// TagTunes +// +// Created by Kim Wittenburg on 29.08.15. +// Copyright © 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +/// A custom interface for the `NSUserDefaults`. It is recommended to use this +/// class insted of accessing the user defaults directly to prevent errors due +/// to misspelled strings. +/// +/// All properties in this class are KCO compliant. +@objc public class Preferences: NSObject { + + public static var sharedPreferences = Preferences() + + /// Initializes the default preferences. This method must be called the very + /// first time the application is launched. It is perfectly valid to call + /// this method every time the application launches. Existing values are not + /// overridden. + public func initializeDefaultValues() { + let initialArtworkFolder = NSURL.fileURLWithPath(NSHomeDirectory(), isDirectory: true) + NSUserDefaults.standardUserDefaults().registerDefaults([ + "Save Artwork": false, + "Keep Search Results": false, + "Remove Saved Albums": false]) + if NSUserDefaults.standardUserDefaults().URLForKey("Artwork Target") == nil { + NSUserDefaults.standardUserDefaults().setURL(initialArtworkFolder, forKey: "Artwork Target") + } + } + + /// If `true` the album artwork should be saved to the `artworkTarget` URL + /// when an item is saved. + public dynamic var saveArtwork: Bool { + set { + NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: "Save Artwork") + } + get { + return NSUserDefaults.standardUserDefaults().boolForKey("Save Artwork") + } + } + + /// The URL of the folder album artwork is saved to. + /// + /// The URL must be a valid file URL pointing to a directory. + public dynamic var artworkTarget: NSURL { + set { + NSUserDefaults.standardUserDefaults().setURL(newValue, forKey: "Artwork Target") + } + get { + return NSUserDefaults.standardUserDefaults().URLForKey("Artwork Target")! + } + } + + /// If `true` the search results are not removed from the main outline view + /// when the user selects a result. + public dynamic var keepSearchResults: Bool { + set { + NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: "Keep Search Results") + } + get { + return NSUserDefaults.standardUserDefaults().boolForKey("Keep Search Results") + } + } + +} diff --git a/TagTunes/PreferencesTabViewController.swift b/TagTunes/PreferencesTabViewController.swift new file mode 100644 index 0000000..e303cfc --- /dev/null +++ b/TagTunes/PreferencesTabViewController.swift @@ -0,0 +1,46 @@ +// +// PreferencesTabViewController.swift +// Harmony +// +// Created by Kim Wittenburg on 26.01.15. +// Copyright (c) 2015 Das Code Kollektiv. All rights reserved. +// + +import Cocoa + +internal class GeneralPreferencesViewController: NSViewController { + + // MARK: IBOutlets + + @IBOutlet internal weak var artworkPathControl: NSPathControl! + @IBOutlet internal weak var chooseArtworkButton: NSButton! + + override internal func viewDidLoad() { + super.viewDidLoad() + artworkPathControl.URL = Preferences.sharedPreferences.artworkTarget + saveArtworkStateChanged(self) + } + + @IBAction internal func saveArtworkStateChanged(sender: AnyObject) { + artworkPathControl.enabled = Preferences.sharedPreferences.saveArtwork + chooseArtworkButton.enabled = Preferences.sharedPreferences.saveArtwork + } + + @IBAction internal func chooseArtworkPath(sender: AnyObject) { + let openPanel = NSOpenPanel() + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + openPanel.canCreateDirectories = true + openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title prompting the user to choose a directory") + openPanel.beginSheetModalForWindow(view.window!) { + result in + if result == NSModalResponseOK { + Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL! + self.artworkPathControl.URL = openPanel.URL + } + } + } + + + +} \ No newline at end of file diff --git a/TagTunes/SearchResult.swift b/TagTunes/SearchResult.swift new file mode 100644 index 0000000..c110469 --- /dev/null +++ b/TagTunes/SearchResult.swift @@ -0,0 +1,65 @@ +// +// SearchResult.swift +// TagTunes +// +// Created by Kim Wittenburg on 31.08.15. +// Copyright © 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +/// Represents an `Album` returned fromm the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public class SearchResult: Equatable { + + public let id: iTunesId + + public let name: String + + public let censoredName: String + + public let viewURL: NSURL + + public let artwork: Artwork + + public let trackCount: Int + + public let releaseDate: NSDate + + public let genre: String + + public let artistName: String + + public init(representedAlbum: Album) { + id = representedAlbum.id + name = representedAlbum.name + censoredName = representedAlbum.censoredName + viewURL = representedAlbum.viewURL + artwork = representedAlbum.artwork + trackCount = representedAlbum.trackCount + releaseDate = representedAlbum.releaseDate + genre = representedAlbum.genre + artistName = representedAlbum.artistName + } + +} + +extension Album { + + public convenience init(searchResult: SearchResult) { + self.init(id: searchResult.id, name: searchResult.name, censoredName: searchResult.censoredName, viewURL: searchResult.viewURL, artwork: searchResult.artwork, trackCount: searchResult.trackCount, releaseDate: searchResult.releaseDate, genre: searchResult.genre, artistName: searchResult.artistName) + } + +} + +public func ==(lhs: SearchResult, rhs: SearchResult) -> Bool { + return lhs.id == rhs.id +} + +public func ==(lhs: SearchResult, rhs: Album) -> Bool { + return lhs.id == rhs.id +} + +public func ==(lhs: Album, rhs: SearchResult) -> Bool { + return lhs.id == rhs.id +} \ No newline at end of file diff --git a/TagTunes/TagTunes-Bridging-Header.h b/TagTunes/TagTunes-Bridging-Header.h new file mode 100644 index 0000000..b5f46bc --- /dev/null +++ b/TagTunes/TagTunes-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "iTunes.h" \ No newline at end of file diff --git a/TagTunes/Track.swift b/TagTunes/Track.swift new file mode 100644 index 0000000..1b658e6 --- /dev/null +++ b/TagTunes/Track.swift @@ -0,0 +1,129 @@ +// +// Track.swift +// Tag for iTunes +// +// Created by Kim Wittenburg on 30.05.15. +// Copyright (c) 2015 Kim Wittenburg. All rights reserved. +// + +import Cocoa + +/// Represents a track of the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public class Track: iTunesType { + + // MARK: Properties + + public let id: iTunesId + + public let name: String + + public let censoredName: String + + public let artistName: String + + public let releaseDate: NSDate + + public let trackNumber: Int + + public let trackCount: Int + + public let discNumber: Int + + public let discCount: Int + + public let genre: String + + public internal(set) weak var album: Album! + + /// These tracks will be changed, if `save()` is called. + public var associatedTracks = [iTunesTrack]() + + // MARK: Initializers + + public required init(data: [iTunesAPI.Field: AnyObject]) { + id = data[.TrackId] as! UInt + name = data[.TrackName] as! String + censoredName = data[.TrackCensoredName] as! String + artistName = data[.ArtistName] as! String + releaseDate = iTunesAPI.sharedDateFormatter.dateFromString(data[.ReleaseDate] as! String)! + trackNumber = data[.TrackNumber] as! Int + trackCount = data[.TrackCount] as! Int + discNumber = data[.DiscNumber] as! Int + discCount = data[.DiscCount] as! Int + genre = data[.PrimaryGenreName] as! String + } + + public static var requiredFields: [iTunesAPI.Field] { + return [.TrackId, .TrackName, .TrackCensoredName, .ArtistName, .ReleaseDate, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount, .PrimaryGenreName] + } + + // MARK: Methods + + /// Saves the track. This means that its properties are applied to every + /// `iTunesTrack` in its `associatedTracks`. + /// + /// This method respects the user's preferences (See `Preferences` class). + public func save() { + saveToTracks(associatedTracks) + } + + /// Applies the receiver's properties to the specified tracks. + /// + /// This method respects the user's preferences (See `Preferences` class). + public func saveToTracks(tracks: [iTunesTrack]) { + for track in tracks { + saveToTrack(track) + } + } + + /// Applies the receiver's properties to the specified `track`. + /// + /// This method respects the user's preferences (See `Preferences` class). + public func saveToTrack(track: iTunesTrack) { + let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate) + track.name = name + track.artist = artistName + track.year = components.year + track.trackNumber = trackNumber + track.trackCount = trackCount + track.discNumber = discNumber + track.discCount = discCount + track.genre = genre + track.album = album?.name + track.albumArtist = album?.artistName + track.sortName = "" + track.sortAlbum = "" + track.sortAlbumArtist = "" + track.sortArtist = "" + track.composer = "" + track.sortComposer = "" + track.comment = "" + track.artworks().removeAllObjects() + } + + /// Returns `true` if all `associatedTrack`s contain the same values as the + /// `Track` instance. + public var saved: Bool { + let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate) + for track in associatedTracks { + guard track.name == name && track.artist == artistName && track.year == components.year && track.trackNumber == trackNumber && track.trackCount == trackCount && track.discNumber == discNumber && track.discCount == discCount && track.genre == genre && track.album == album.name && track.albumArtist == album.artistName && track.composer == "" else { + return false + } + } + return true + } + +} + +extension Track: Hashable { + + public var hashValue: Int { + return Int(id) + } + +} + +public func ==(lhs: Track, rhs: Track) -> Bool { + return lhs.id == rhs.id +} diff --git a/TagTunes/TrackTableCellView.swift b/TagTunes/TrackTableCellView.swift new file mode 100644 index 0000000..1cd6005 --- /dev/null +++ b/TagTunes/TrackTableCellView.swift @@ -0,0 +1,127 @@ +// +// TrackTableCellView.swift +// Harmony +// +// Created by Kim Wittenburg on 21.01.15. +// Copyright (c) 2015 Das Code Kollektiv. All rights reserved. +// + +import Cocoa +import AppKitPlus + +/// A table cell view to display information for a `Track`. This class should +/// only be initialized from a nib or storyboard. +public class TrackTableCellView: AdvancedTableCellView { + + // MARK: Properties + + /// An outlet do display the track number. This acts as a secondary label. + /// The text color is automatically adjusted based on the `backgroundStyle` + /// of the receiver. + /// + /// Intended to be used as accessory view + @IBOutlet public lazy var trackNumberTextField: NSTextField? = { + let trackNumberTextField = NSTextField() + trackNumberTextField.bordered = false + trackNumberTextField.drawsBackground = false + trackNumberTextField.selectable = false + trackNumberTextField.lineBreakMode = .ByTruncatingTail + trackNumberTextField.font = NSFont.systemFontOfSize(0) + trackNumberTextField.textColor = NSColor.secondaryLabelColor() + trackNumberTextField.translatesAutoresizingMaskIntoConstraints = false + return trackNumberTextField + }() + + /// Intended to be used as accessory view. + /// + /// This property replaces `imageView`. + @IBOutlet public lazy var savedImageView: NSImageView! = { + let secondaryImageView = NSImageView() + secondaryImageView.imageScaling = .ScaleProportionallyDown + secondaryImageView.translatesAutoresizingMaskIntoConstraints = false + return secondaryImageView + }() + + // MARK: Intitializers + + override public func setupView() { + leftAccessoryView = trackNumberTextField + super.setupView() + } + + // MARK: Overrides + + override public var backgroundStyle: NSBackgroundStyle { + get { + return super.backgroundStyle + } + set { + super.backgroundStyle = newValue + let trackNumberCell = self.trackNumberTextField?.cell as? NSTextFieldCell + trackNumberCell?.backgroundStyle = newValue + + switch newValue { + case .Light: + trackNumberTextField?.textColor = NSColor.secondaryLabelColor() + case .Dark: + trackNumberTextField?.textColor = NSColor.secondarySelectedControlColor() + default: + break + } + } + } + + override public var imageView: NSImageView? { + set { + savedImageView = newValue + } + get { + return savedImageView + } + } + + // MARK: Methods + + /// Sets up the receiver to display `track`. + public func setupForTrack(track: Track) { + style = track.album.hasSameArtistNameAsTracks ? .Simple : .CompactSubtitle + + textField?.stringValue = track.name + if track.associatedTracks.isEmpty { + textField?.textColor = NSColor.disabledControlTextColor() + } else if track.associatedTracks.count > 1 || track.associatedTracks[0].name != track.name { + textField?.textColor = NSColor.redColor() + } else { + textField?.textColor = NSColor.controlTextColor() + } + secondaryTextField?.stringValue = track.artistName + trackNumberTextField?.stringValue = "\(track.trackNumber)" + if track.associatedTracks.isEmpty { + imageView?.image = NSImage(named: "TickBW") + } else { + imageView?.image = NSImage(named: "Tick") + } + if track.saved { + let aspectRatioConstraint = NSLayoutConstraint( + item: savedImageView, + attribute: .Width, + relatedBy: .Equal, + toItem: savedImageView, + attribute: .Height, + multiplier: 1, + constant: 0) + let widthConstraint = NSLayoutConstraint( + item: savedImageView, + attribute: .Width, + relatedBy: .Equal, + toItem: nil, + attribute: .Width, + multiplier: 1, + constant: 17) + setRightAccessoryView(savedImageView, withConstraints: [aspectRatioConstraint, widthConstraint]) + } else { + rightAccessoryView = nil + } + } + +} diff --git a/TagTunes/ViewController.swift b/TagTunes/ViewController.swift deleted file mode 100644 index d2e3412..0000000 --- a/TagTunes/ViewController.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ViewController.swift -// TagTunes -// -// Created by Kim Wittenburg on 28.08.15. -// Copyright © 2015 Kim Wittenburg. All rights reserved. -// - -import Cocoa - -class ViewController: NSViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - } - - override var representedObject: AnyObject? { - didSet { - // Update the view, if already loaded. - } - } - - -} - diff --git a/TagTunes/iTunes.h b/TagTunes/iTunes.h new file mode 100644 index 0000000..afcfe40 --- /dev/null +++ b/TagTunes/iTunes.h @@ -0,0 +1,522 @@ +/* + * iTunes.h + */ + +#import +#import + + +@class iTunesPrintSettings, iTunesApplication, iTunesItem, iTunesAirPlayDevice, iTunesArtwork, iTunesEncoder, iTunesEQPreset, iTunesPlaylist, iTunesAudioCDPlaylist, iTunesLibraryPlaylist, iTunesRadioTunerPlaylist, iTunesSource, iTunesTrack, iTunesAudioCDTrack, iTunesFileTrack, iTunesSharedTrack, iTunesURLTrack, iTunesUserPlaylist, iTunesFolderPlaylist, iTunesVisual, iTunesWindow, iTunesBrowserWindow, iTunesEQWindow, iTunesPlaylistWindow; + +enum iTunesEKnd { + iTunesEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */, + iTunesEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */, + iTunesEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */ +}; +typedef enum iTunesEKnd iTunesEKnd; + +enum iTunesEnum { + iTunesEnumStandard = 'lwst' /* Standard PostScript error handling */, + iTunesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ +}; +typedef enum iTunesEnum iTunesEnum; + +enum iTunesEPlS { + iTunesEPlSStopped = 'kPSS', + iTunesEPlSPlaying = 'kPSP', + iTunesEPlSPaused = 'kPSp', + iTunesEPlSFastForwarding = 'kPSF', + iTunesEPlSRewinding = 'kPSR' +}; +typedef enum iTunesEPlS iTunesEPlS; + +enum iTunesERpt { + iTunesERptOff = 'kRpO', + iTunesERptOne = 'kRp1', + iTunesERptAll = 'kAll' +}; +typedef enum iTunesERpt iTunesERpt; + +enum iTunesEVSz { + iTunesEVSzSmall = 'kVSS', + iTunesEVSzMedium = 'kVSM', + iTunesEVSzLarge = 'kVSL' +}; +typedef enum iTunesEVSz iTunesEVSz; + +enum iTunesESrc { + iTunesESrcLibrary = 'kLib', + iTunesESrcIPod = 'kPod', + iTunesESrcAudioCD = 'kACD', + iTunesESrcMP3CD = 'kMCD', + iTunesESrcRadioTuner = 'kTun', + iTunesESrcSharedLibrary = 'kShd', + iTunesESrcUnknown = 'kUnk' +}; +typedef enum iTunesESrc iTunesESrc; + +enum iTunesESrA { + iTunesESrAAlbums = 'kSrL' /* albums only */, + iTunesESrAAll = 'kAll' /* all text fields */, + iTunesESrAArtists = 'kSrR' /* artists only */, + iTunesESrAComposers = 'kSrC' /* composers only */, + iTunesESrADisplayed = 'kSrV' /* visible text fields */, + iTunesESrASongs = 'kSrS' /* song names only */ +}; +typedef enum iTunesESrA iTunesESrA; + +enum iTunesESpK { + iTunesESpKNone = 'kNon', + iTunesESpKBooks = 'kSpA', + iTunesESpKFolder = 'kSpF', + iTunesESpKGenius = 'kSpG', + iTunesESpKITunesU = 'kSpU', + iTunesESpKLibrary = 'kSpL', + iTunesESpKMovies = 'kSpI', + iTunesESpKMusic = 'kSpZ', + iTunesESpKPodcasts = 'kSpP', + iTunesESpKPurchasedMusic = 'kSpM', + iTunesESpKTVShows = 'kSpT' +}; +typedef enum iTunesESpK iTunesESpK; + +enum iTunesEVdK { + iTunesEVdKNone = 'kNon' /* not a video or unknown video kind */, + iTunesEVdKHomeVideo = 'kVdH' /* home video track */, + iTunesEVdKMovie = 'kVdM' /* movie track */, + iTunesEVdKMusicVideo = 'kVdV' /* music video track */, + iTunesEVdKTVShow = 'kVdT' /* TV show track */ +}; +typedef enum iTunesEVdK iTunesEVdK; + +enum iTunesERtK { + iTunesERtKUser = 'kRtU' /* user-specified rating */, + iTunesERtKComputed = 'kRtC' /* iTunes-computed rating */ +}; +typedef enum iTunesERtK iTunesERtK; + +enum iTunesEAPD { + iTunesEAPDComputer = 'kAPC', + iTunesEAPDAirPortExpress = 'kAPX', + iTunesEAPDAppleTV = 'kAPT', + iTunesEAPDAirPlayDevice = 'kAPO', + iTunesEAPDUnknown = 'kAPU' +}; +typedef enum iTunesEAPD iTunesEAPD; + + + +/* + * Standard Suite + */ + +@interface iTunesPrintSettings : SBObject + +@property (readonly) NSInteger copies; // the number of copies of a document to be printed +@property (readonly) BOOL collating; // Should printed copies be collated? +@property (readonly) NSInteger startingPage; // the first page of the document to be printed +@property (readonly) NSInteger endingPage; // the last page of the document to be printed +@property (readonly) NSInteger pagesAcross; // number of logical pages laid across a physical page +@property (readonly) NSInteger pagesDown; // number of logical pages laid out down a physical page +@property (readonly) iTunesEnum errorHandling; // how errors are handled +@property (copy, readonly) NSDate *requestedPrintTime; // the time at which the desktop printer should print the document +@property (copy, readonly) NSArray *printerFeatures; // printer specific options +@property (copy, readonly) NSString *faxNumber; // for fax number +@property (copy, readonly) NSString *targetPrinter; // for target printer + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) +- (void) close; // Close an object +- (void) delete; // Delete an element from an object +- (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) +- (BOOL) exists; // Verify if an object exists +- (void) open; // open the specified object(s) +- (void) playOnce:(BOOL)once; // play the current track or the specified track or file. + +@end + + + +/* + * iTunes Suite + */ + +// The application program +@interface iTunesApplication : SBApplication + +- (SBElementArray *) AirPlayDevices; +- (SBElementArray *) browserWindows; +- (SBElementArray *) encoders; +- (SBElementArray *) EQPresets; +- (SBElementArray *) EQWindows; +- (SBElementArray *) playlistWindows; +- (SBElementArray *) sources; +- (SBElementArray *) visuals; +- (SBElementArray *) windows; + +@property (readonly) BOOL AirPlayEnabled; // is AirPlay currently enabled? +@property (readonly) BOOL converting; // is a track currently being converted? +@property (copy) NSArray *currentAirPlayDevices; // the currently selected AirPlay device(s) +@property (copy) iTunesEncoder *currentEncoder; // the currently selected encoder (MP3, AIFF, WAV, etc.) +@property (copy) iTunesEQPreset *currentEQPreset; // the currently selected equalizer preset +@property (copy, readonly) iTunesPlaylist *currentPlaylist; // the playlist containing the currently targeted track +@property (copy, readonly) NSString *currentStreamTitle; // the name of the current song in the playing stream (provided by streaming server) +@property (copy, readonly) NSString *currentStreamURL; // the URL of the playing stream or streaming web site (provided by streaming server) +@property (copy, readonly) iTunesTrack *currentTrack; // the current targeted track +@property (copy) iTunesVisual *currentVisual; // the currently selected visual plug-in +@property BOOL EQEnabled; // is the equalizer enabled? +@property BOOL fixedIndexing; // true if all AppleScript track indices should be independent of the play order of the owning playlist. +@property BOOL frontmost; // is iTunes the frontmost application? +@property BOOL fullScreen; // are visuals displayed using the entire screen? +@property (copy, readonly) NSString *name; // the name of the application +@property BOOL mute; // has the sound output been muted? +@property double playerPosition; // the player’s position within the currently playing track in seconds. +@property (readonly) iTunesEPlS playerState; // is iTunes stopped, paused, or playing? +@property (copy, readonly) SBObject *selection; // the selection visible to the user +@property NSInteger soundVolume; // the sound output volume (0 = minimum, 100 = maximum) +@property (copy, readonly) NSString *version; // the version of iTunes +@property BOOL visualsEnabled; // are visuals currently being displayed? +@property iTunesEVSz visualSize; // the size of the displayed visual +@property (copy, readonly) NSString *iAdIdentifier; // the iAd identifier + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) +- (void) run; // run iTunes +- (void) quit; // quit iTunes +- (iTunesTrack *) add:(NSArray *)x to:(SBObject *)to; // add one or more files to a playlist +- (void) backTrack; // reposition to beginning of current track or go to previous track if already at start of current track +- (iTunesTrack *) convert:(NSArray *)x; // convert one or more files or tracks +- (void) fastForward; // skip forward in a playing track +- (void) nextTrack; // advance to the next track in the current playlist +- (void) pause; // pause playback +- (void) playOnce:(BOOL)once; // play the current track or the specified track or file. +- (void) playpause; // toggle the playing/paused state of the current track +- (void) previousTrack; // return to the previous track in the current playlist +- (void) resume; // disable fast forward/rewind and resume playback, if playing. +- (void) rewind; // skip backwards in a playing track +- (void) stop; // stop playback +- (void) update; // update the specified iPod +- (void) eject; // eject the specified iPod +- (void) subscribe:(NSString *)x; // subscribe to a podcast feed +- (void) updateAllPodcasts; // update all subscribed podcast feeds +- (void) updatePodcast; // update podcast feed +- (void) openLocation:(NSString *)x; // Opens a Music Store or audio stream URL + +@end + +// an item +@interface iTunesItem : SBObject + +@property (copy, readonly) SBObject *container; // the container of the item +- (NSInteger) id; // the id of the item +@property (readonly) NSInteger index; // The index of the item in internal application order. +@property (copy) NSString *name; // the name of the item +@property (copy, readonly) NSString *persistentID; // the id of the item as a hexadecimal string. This id does not change over time. +@property (copy) NSDictionary *properties; // every property of the item + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) +- (void) close; // Close an object +- (void) delete; // Delete an element from an object +- (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) +- (BOOL) exists; // Verify if an object exists +- (void) open; // open the specified object(s) +- (void) playOnce:(BOOL)once; // play the current track or the specified track or file. +- (void) reveal; // reveal and select a track or playlist + +@end + +// an AirPlay device +@interface iTunesAirPlayDevice : iTunesItem + +@property (readonly) BOOL active; // is the device currently being played to? +@property (readonly) BOOL available; // is the device currently available? +@property (readonly) iTunesEAPD kind; // the kind of the device +@property (copy, readonly) NSString *networkAddress; // the network (MAC) address of the device +- (BOOL) protected; // is the device password- or passcode-protected? +@property BOOL selected; // is the device currently selected? +@property (readonly) BOOL supportsAudio; // does the device support audio playback? +@property (readonly) BOOL supportsVideo; // does the device support video playback? +@property NSInteger soundVolume; // the output volume for the device (0 = minimum, 100 = maximum) + + +@end + +// a piece of art within a track +@interface iTunesArtwork : iTunesItem + +@property (copy) NSImage *data; // data for this artwork, in the form of a picture +@property (copy) NSString *objectDescription; // description of artwork as a string +@property (readonly) BOOL downloaded; // was this artwork downloaded by iTunes? +@property (copy, readonly) NSNumber *format; // the data format for this piece of artwork +@property NSInteger kind; // kind or purpose of this piece of artwork +@property (copy) NSData *rawData; // data for this artwork, in original format + + +@end + +// converts a track to a specific file format +@interface iTunesEncoder : iTunesItem + +@property (copy, readonly) NSString *format; // the data format created by the encoder + + +@end + +// equalizer preset configuration +@interface iTunesEQPreset : iTunesItem + +@property double band1; // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB) +@property double band2; // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB) +@property double band3; // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB) +@property double band4; // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB) +@property double band5; // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB) +@property double band6; // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB) +@property double band7; // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB) +@property double band8; // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB) +@property double band9; // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB) +@property double band10; // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB) +@property (readonly) BOOL modifiable; // can this preset be modified? +@property double preamp; // the equalizer preamp level (-12.0 dB to +12.0 dB) +@property BOOL updateTracks; // should tracks which refer to this preset be updated when the preset is renamed or deleted? + + +@end + +// a list of songs/streams +@interface iTunesPlaylist : iTunesItem + +- (SBElementArray *) tracks; + +@property (readonly) NSInteger duration; // the total length of all songs (in seconds) +@property (copy) NSString *name; // the name of the playlist +@property (copy, readonly) iTunesPlaylist *parent; // folder which contains this playlist (if any) +@property BOOL shuffle; // play the songs in this playlist in random order? +@property (readonly) NSInteger size; // the total size of all songs (in bytes) +@property iTunesERpt songRepeat; // playback repeat mode +@property (readonly) iTunesESpK specialKind; // special playlist kind +@property (copy, readonly) NSString *time; // the length of all songs in MM:SS format +@property (readonly) BOOL visible; // is this playlist visible in the Source list? + +- (void) moveTo:(SBObject *)to; // Move playlist(s) to a new location +- (iTunesTrack *) searchFor:(NSString *)for_ only:(iTunesESrA)only; // search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes. + +@end + +// a playlist representing an audio CD +@interface iTunesAudioCDPlaylist : iTunesPlaylist + +- (SBElementArray *) audioCDTracks; + +@property (copy) NSString *artist; // the artist of the CD +@property BOOL compilation; // is this CD a compilation album? +@property (copy) NSString *composer; // the composer of the CD +@property NSInteger discCount; // the total number of discs in this CD’s album +@property NSInteger discNumber; // the index of this CD disc in the source album +@property (copy) NSString *genre; // the genre of the CD +@property NSInteger year; // the year the album was recorded/released + + +@end + +// the master music library playlist +@interface iTunesLibraryPlaylist : iTunesPlaylist + +- (SBElementArray *) fileTracks; +- (SBElementArray *) URLTracks; +- (SBElementArray *) sharedTracks; + + +@end + +// the radio tuner playlist +@interface iTunesRadioTunerPlaylist : iTunesPlaylist + +- (SBElementArray *) URLTracks; + + +@end + +// a music source (music library, CD, device, etc.) +@interface iTunesSource : iTunesItem + +- (SBElementArray *) audioCDPlaylists; +- (SBElementArray *) libraryPlaylists; +- (SBElementArray *) playlists; +- (SBElementArray *) radioTunerPlaylists; +- (SBElementArray *) userPlaylists; + +@property (readonly) long long capacity; // the total size of the source if it has a fixed size +@property (readonly) long long freeSpace; // the free space on the source if it has a fixed size +@property (readonly) iTunesESrc kind; + +- (void) update; // update the specified iPod +- (void) eject; // eject the specified iPod + +@end + +// playable audio source +@interface iTunesTrack : iTunesItem + +- (SBElementArray *) artworks; + +@property (copy) NSString *album; // the album name of the track +@property (copy) NSString *albumArtist; // the album artist of the track +@property NSInteger albumRating; // the rating of the album for this track (0 to 100) +@property (readonly) iTunesERtK albumRatingKind; // the rating kind of the album rating for this track +@property (copy) NSString *artist; // the artist/source of the track +@property (readonly) NSInteger bitRate; // the bit rate of the track (in kbps) +@property double bookmark; // the bookmark time of the track in seconds +@property BOOL bookmarkable; // is the playback position for this track remembered? +@property NSInteger bpm; // the tempo of this track in beats per minute +@property (copy) NSString *category; // the category of the track +@property (copy) NSString *comment; // freeform notes about the track +@property BOOL compilation; // is this track from a compilation album? +@property (copy) NSString *composer; // the composer of the track +@property (readonly) NSInteger databaseID; // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data. +@property (copy, readonly) NSDate *dateAdded; // the date the track was added to the playlist +@property (copy) NSString *objectDescription; // the description of the track +@property NSInteger discCount; // the total number of discs in the source album +@property NSInteger discNumber; // the index of the disc containing this track on the source album +@property (readonly) double duration; // the length of the track in seconds +@property BOOL enabled; // is this track checked for playback? +@property (copy) NSString *episodeID; // the episode ID of the track +@property NSInteger episodeNumber; // the episode number of the track +@property (copy) NSString *EQ; // the name of the EQ preset of the track +@property double finish; // the stop time of the track in seconds +@property BOOL gapless; // is this track from a gapless album? +@property (copy) NSString *genre; // the music/audio genre (category) of the track +@property (copy) NSString *grouping; // the grouping (piece) of the track. Generally used to denote movements within a classical work. +@property (readonly) BOOL iTunesU; // is this track an iTunes U episode? +@property (copy, readonly) NSString *kind; // a text description of the track +@property (copy) NSString *longDescription; +@property (copy) NSString *lyrics; // the lyrics of the track +@property (copy, readonly) NSDate *modificationDate; // the modification date of the content of this track +@property NSInteger playedCount; // number of times this track has been played +@property (copy) NSDate *playedDate; // the date and time this track was last played +@property (readonly) BOOL podcast; // is this track a podcast episode? +@property NSInteger rating; // the rating of this track (0 to 100) +@property (readonly) iTunesERtK ratingKind; // the rating kind of this track +@property (copy, readonly) NSDate *releaseDate; // the release date of this track +@property (readonly) NSInteger sampleRate; // the sample rate of the track (in Hz) +@property NSInteger seasonNumber; // the season number of the track +@property BOOL shufflable; // is this track included when shuffling? +@property NSInteger skippedCount; // number of times this track has been skipped +@property (copy) NSDate *skippedDate; // the date and time this track was last skipped +@property (copy) NSString *show; // the show name of the track +@property (copy) NSString *sortAlbum; // override string to use for the track when sorting by album +@property (copy) NSString *sortArtist; // override string to use for the track when sorting by artist +@property (copy) NSString *sortAlbumArtist; // override string to use for the track when sorting by album artist +@property (copy) NSString *sortName; // override string to use for the track when sorting by name +@property (copy) NSString *sortComposer; // override string to use for the track when sorting by composer +@property (copy) NSString *sortShow; // override string to use for the track when sorting by show name +@property (readonly) long long size; // the size of the track (in bytes) +@property double start; // the start time of the track in seconds +@property (copy, readonly) NSString *time; // the length of the track in MM:SS format +@property NSInteger trackCount; // the total number of tracks on the source album +@property NSInteger trackNumber; // the index of the track on the source album +@property BOOL unplayed; // is this track unplayed? +@property iTunesEVdK videoKind; // kind of video track +@property NSInteger volumeAdjustment; // relative volume adjustment of the track (-100% to 100%) +@property NSInteger year; // the year the track was recorded/released + + +@end + +// a track on an audio CD +@interface iTunesAudioCDTrack : iTunesTrack + +@property (copy, readonly) NSURL *location; // the location of the file represented by this track + + +@end + +// a track representing an audio file (MP3, AIFF, etc.) +@interface iTunesFileTrack : iTunesTrack + +@property (copy) NSURL *location; // the location of the file represented by this track + +- (void) refresh; // update file track information from the current information in the track’s file + +@end + +// a track residing in a shared library +@interface iTunesSharedTrack : iTunesTrack + + +@end + +// a track representing a network stream +@interface iTunesURLTrack : iTunesTrack + +@property (copy) NSString *address; // the URL for this track + +- (void) download; // download podcast episode + +@end + +// custom playlists created by the user +@interface iTunesUserPlaylist : iTunesPlaylist + +- (SBElementArray *) fileTracks; +- (SBElementArray *) URLTracks; +- (SBElementArray *) sharedTracks; + +@property BOOL shared; // is this playlist shared? +@property (readonly) BOOL smart; // is this a Smart Playlist? + + +@end + +// a folder that contains other playlists +@interface iTunesFolderPlaylist : iTunesUserPlaylist + + +@end + +// a visual plug-in +@interface iTunesVisual : iTunesItem + + +@end + +// any window +@interface iTunesWindow : iTunesItem + +@property NSRect bounds; // the boundary rectangle for the window +@property (readonly) BOOL closeable; // does the window have a close box? +@property (readonly) BOOL collapseable; // does the window have a collapse (windowshade) box? +@property BOOL collapsed; // is the window collapsed? +@property NSPoint position; // the upper left position of the window +@property (readonly) BOOL resizable; // is the window resizable? +@property BOOL visible; // is the window visible? +@property (readonly) BOOL zoomable; // is the window zoomable? +@property BOOL zoomed; // is the window zoomed? + + +@end + +// the main iTunes window +@interface iTunesBrowserWindow : iTunesWindow + +@property BOOL minimized; // is the small player visible? +@property (copy, readonly) SBObject *selection; // the selected songs +@property (copy) iTunesPlaylist *view; // the playlist currently displayed in the window + + +@end + +// the iTunes equalizer window +@interface iTunesEQWindow : iTunesWindow + +@property BOOL minimized; // is the small EQ window visible? + + +@end + +// a sub-window showing a single playlist +@interface iTunesPlaylistWindow : iTunesWindow + +@property (copy, readonly) SBObject *selection; // the selected songs +@property (copy, readonly) iTunesPlaylist *view; // the playlist displayed in the window + + +@end + diff --git a/TagTunes/iTunes.m b/TagTunes/iTunes.m new file mode 100644 index 0000000..bfd9d82 --- /dev/null +++ b/TagTunes/iTunes.m @@ -0,0 +1,2306 @@ +/* + * iTunes.m + */ + +#include "iTunes.h" + + + + +/* + * Standard Suite + */ + +@implementation iTunesPrintSettings + + +- (NSInteger) copies +{ + id v = [[self propertyWithCode:'lwcp'] get]; + return [v integerValue]; +} + +- (BOOL) collating +{ + id v = [[self propertyWithCode:'lwcl'] get]; + return [v boolValue]; +} + +- (NSInteger) startingPage +{ + id v = [[self propertyWithCode:'lwfp'] get]; + return [v integerValue]; +} + +- (NSInteger) endingPage +{ + id v = [[self propertyWithCode:'lwlp'] get]; + return [v integerValue]; +} + +- (NSInteger) pagesAcross +{ + id v = [[self propertyWithCode:'lwla'] get]; + return [v integerValue]; +} + +- (NSInteger) pagesDown +{ + id v = [[self propertyWithCode:'lwld'] get]; + return [v integerValue]; +} + +- (iTunesEnum) errorHandling +{ + id v = [[self propertyWithCode:'lweh'] get]; + return [v enumCodeValue]; +} + +- (NSDate *) requestedPrintTime +{ + return [[self propertyWithCode:'lwqt'] get]; +} + +- (NSArray *) printerFeatures +{ + return [[self propertyWithCode:'lwpf'] get]; +} + +- (NSString *) faxNumber +{ + return [[self propertyWithCode:'faxn'] get]; +} + +- (NSString *) targetPrinter +{ + return [[self propertyWithCode:'trpr'] get]; +} + + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme +{ + [self sendEvent:'aevt' id:'pdoc' parameters:'pdlg', [NSNumber numberWithBool:printDialog], 'prdt', withProperties, 'pKnd', [NSAppleEventDescriptor descriptorWithEnumCode:kind], 'pThm', theme, 0]; +} + +- (void) close +{ + [self sendEvent:'core' id:'clos' parameters:0]; +} + +- (void) delete +{ + [self sendEvent:'core' id:'delo' parameters:0]; +} + +- (SBObject *) duplicateTo:(SBObject *)to +{ + id result__ = [self sendEvent:'core' id:'clon' parameters:'insh', to, 0]; + return result__; +} + +- (BOOL) exists +{ + id result__ = [self sendEvent:'core' id:'doex' parameters:0]; + return [result__ boolValue]; +} + +- (void) open +{ + [self sendEvent:'aevt' id:'odoc' parameters:0]; +} + +- (void) playOnce:(BOOL)once +{ + [self sendEvent:'hook' id:'Play' parameters:'POne', [NSNumber numberWithBool:once], 0]; +} + + +@end + + + + +/* + * iTunes Suite + */ + +@implementation iTunesApplication + +typedef struct { NSString *name; FourCharCode code; } classForCode_t; +static const classForCode_t classForCodeData__[] = { + { @"iTunesPrintSettings", 'pset' }, + { @"iTunesAirPlayDevice", 'cAPD' }, + { @"iTunesApplication", 'capp' }, + { @"iTunesArtwork", 'cArt' }, + { @"iTunesAudioCDPlaylist", 'cCDP' }, + { @"iTunesAudioCDTrack", 'cCDT' }, + { @"iTunesBrowserWindow", 'cBrW' }, + { @"iTunesEncoder", 'cEnc' }, + { @"iTunesEQPreset", 'cEQP' }, + { @"iTunesEQWindow", 'cEQW' }, + { @"iTunesFileTrack", 'cFlT' }, + { @"iTunesFolderPlaylist", 'cFoP' }, + { @"iTunesItem", 'cobj' }, + { @"iTunesLibraryPlaylist", 'cLiP' }, + { @"iTunesPlaylist", 'cPly' }, + { @"iTunesPlaylistWindow", 'cPlW' }, + { @"iTunesRadioTunerPlaylist", 'cRTP' }, + { @"iTunesSharedTrack", 'cShT' }, + { @"iTunesSource", 'cSrc' }, + { @"iTunesTrack", 'cTrk' }, + { @"iTunesURLTrack", 'cURT' }, + { @"iTunesUserPlaylist", 'cUsP' }, + { @"iTunesVisual", 'cVis' }, + { @"iTunesWindow", 'cwin' }, + { nil, 0 } +}; + +- (NSDictionary *) classNamesForCodes +{ + static NSMutableDictionary *dict__; + + if (!dict__) @synchronized([self class]) { + if (!dict__) { + dict__ = [[NSMutableDictionary alloc] init]; + const classForCode_t *p; + for (p = classForCodeData__; p->name != nil; ++p) + [dict__ setObject:p->name forKey:[NSNumber numberWithInt:p->code]]; + } } + return dict__; +} + +typedef struct { FourCharCode code; NSString *name; } codeForPropertyName_t; +static const codeForPropertyName_t codeForPropertyNameData__[] = { + { 'lwcp', @"copies" }, + { 'lwcl', @"collating" }, + { 'lwfp', @"startingPage" }, + { 'lwlp', @"endingPage" }, + { 'lwla', @"pagesAcross" }, + { 'lwld', @"pagesDown" }, + { 'lweh', @"errorHandling" }, + { 'lwqt', @"requestedPrintTime" }, + { 'lwpf', @"printerFeatures" }, + { 'faxn', @"faxNumber" }, + { 'trpr', @"targetPrinter" }, + { 'pAct', @"active" }, + { 'pAva', @"available" }, + { 'pKnd', @"kind" }, + { 'pMAC', @"networkAddress" }, + { 'pPro', @"protected" }, + { 'selc', @"selected" }, + { 'pAud', @"supportsAudio" }, + { 'pVid', @"supportsVideo" }, + { 'pVol', @"soundVolume" }, + { 'pAPE', @"AirPlayEnabled" }, + { 'pCnv', @"converting" }, + { 'pAPD', @"currentAirPlayDevices" }, + { 'pEnc', @"currentEncoder" }, + { 'pEQP', @"currentEQPreset" }, + { 'pPla', @"currentPlaylist" }, + { 'pStT', @"currentStreamTitle" }, + { 'pStU', @"currentStreamURL" }, + { 'pTrk', @"currentTrack" }, + { 'pVis', @"currentVisual" }, + { 'pEQ ', @"EQEnabled" }, + { 'pFix', @"fixedIndexing" }, + { 'pisf', @"frontmost" }, + { 'pFSc', @"fullScreen" }, + { 'pnam', @"name" }, + { 'pMut', @"mute" }, + { 'pPos', @"playerPosition" }, + { 'pPlS', @"playerState" }, + { 'sele', @"selection" }, + { 'pVol', @"soundVolume" }, + { 'vers', @"version" }, + { 'pVsE', @"visualsEnabled" }, + { 'pVSz', @"visualSize" }, + { 'piAD', @"iAdIdentifier" }, + { 'pPCT', @"data" }, + { 'pDes', @"objectDescription" }, + { 'pDlA', @"downloaded" }, + { 'pFmt', @"format" }, + { 'pKnd', @"kind" }, + { 'pRaw', @"rawData" }, + { 'pArt', @"artist" }, + { 'pAnt', @"compilation" }, + { 'pCmp', @"composer" }, + { 'pDsC', @"discCount" }, + { 'pDsN', @"discNumber" }, + { 'pGen', @"genre" }, + { 'pYr ', @"year" }, + { 'pLoc', @"location" }, + { 'pMin', @"minimized" }, + { 'sele', @"selection" }, + { 'pPly', @"view" }, + { 'pFmt', @"format" }, + { 'pEQ1', @"band1" }, + { 'pEQ2', @"band2" }, + { 'pEQ3', @"band3" }, + { 'pEQ4', @"band4" }, + { 'pEQ5', @"band5" }, + { 'pEQ6', @"band6" }, + { 'pEQ7', @"band7" }, + { 'pEQ8', @"band8" }, + { 'pEQ9', @"band9" }, + { 'pEQ0', @"band10" }, + { 'pMod', @"modifiable" }, + { 'pEQA', @"preamp" }, + { 'pUTC', @"updateTracks" }, + { 'pMin', @"minimized" }, + { 'pLoc', @"location" }, + { 'pcls', @"objectClass" }, + { 'ctnr', @"container" }, + { 'ID ', @"id" }, + { 'pidx', @"index" }, + { 'pnam', @"name" }, + { 'pPIS', @"persistentID" }, + { 'pALL', @"properties" }, + { 'pDur', @"duration" }, + { 'pnam', @"name" }, + { 'pPlP', @"parent" }, + { 'pShf', @"shuffle" }, + { 'pSiz', @"size" }, + { 'pRpt', @"songRepeat" }, + { 'pSpK', @"specialKind" }, + { 'pTim', @"time" }, + { 'pvis', @"visible" }, + { 'sele', @"selection" }, + { 'pPly', @"view" }, + { 'capa', @"capacity" }, + { 'frsp', @"freeSpace" }, + { 'pKnd', @"kind" }, + { 'pAlb', @"album" }, + { 'pAlA', @"albumArtist" }, + { 'pAlR', @"albumRating" }, + { 'pARk', @"albumRatingKind" }, + { 'pArt', @"artist" }, + { 'pBRt', @"bitRate" }, + { 'pBkt', @"bookmark" }, + { 'pBkm', @"bookmarkable" }, + { 'pBPM', @"bpm" }, + { 'pCat', @"category" }, + { 'pCmt', @"comment" }, + { 'pAnt', @"compilation" }, + { 'pCmp', @"composer" }, + { 'pDID', @"databaseID" }, + { 'pAdd', @"dateAdded" }, + { 'pDes', @"objectDescription" }, + { 'pDsC', @"discCount" }, + { 'pDsN', @"discNumber" }, + { 'pDur', @"duration" }, + { 'enbl', @"enabled" }, + { 'pEpD', @"episodeID" }, + { 'pEpN', @"episodeNumber" }, + { 'pEQp', @"EQ" }, + { 'pStp', @"finish" }, + { 'pGpl', @"gapless" }, + { 'pGen', @"genre" }, + { 'pGrp', @"grouping" }, + { 'pTIU', @"iTunesU" }, + { 'pKnd', @"kind" }, + { 'pLds', @"longDescription" }, + { 'pLyr', @"lyrics" }, + { 'asmo', @"modificationDate" }, + { 'pPlC', @"playedCount" }, + { 'pPlD', @"playedDate" }, + { 'pTPc', @"podcast" }, + { 'pRte', @"rating" }, + { 'pRtk', @"ratingKind" }, + { 'pRlD', @"releaseDate" }, + { 'pSRt', @"sampleRate" }, + { 'pSeN', @"seasonNumber" }, + { 'pSfa', @"shufflable" }, + { 'pSkC', @"skippedCount" }, + { 'pSkD', @"skippedDate" }, + { 'pShw', @"show" }, + { 'pSAl', @"sortAlbum" }, + { 'pSAr', @"sortArtist" }, + { 'pSAA', @"sortAlbumArtist" }, + { 'pSNm', @"sortName" }, + { 'pSCm', @"sortComposer" }, + { 'pSSN', @"sortShow" }, + { 'pSiz', @"size" }, + { 'pStr', @"start" }, + { 'pTim', @"time" }, + { 'pTrC', @"trackCount" }, + { 'pTrN', @"trackNumber" }, + { 'pUnp', @"unplayed" }, + { 'pVdK', @"videoKind" }, + { 'pAdj', @"volumeAdjustment" }, + { 'pYr ', @"year" }, + { 'pURL', @"address" }, + { 'pShr', @"shared" }, + { 'pSmt', @"smart" }, + { 'pbnd', @"bounds" }, + { 'hclb', @"closeable" }, + { 'pWSh', @"collapseable" }, + { 'wshd', @"collapsed" }, + { 'ppos', @"position" }, + { 'prsz', @"resizable" }, + { 'pvis', @"visible" }, + { 'iszm', @"zoomable" }, + { 'pzum', @"zoomed" }, + { 0, nil } +}; + +- (NSDictionary *) codesForPropertyNames +{ + static NSMutableDictionary *dict__; + + if (!dict__) @synchronized([self class]) { + if (!dict__) { + dict__ = [[NSMutableDictionary alloc] init]; + const codeForPropertyName_t *p; + for (p = codeForPropertyNameData__; p->name != nil; ++p) + [dict__ setObject:[NSNumber numberWithInt:p->code] forKey:p->name]; + } } + return dict__; +} + + +- (SBElementArray *) AirPlayDevices +{ + return [self elementArrayWithCode:'cAPD']; +} + + +- (SBElementArray *) browserWindows +{ + return [self elementArrayWithCode:'cBrW']; +} + + +- (SBElementArray *) encoders +{ + return [self elementArrayWithCode:'cEnc']; +} + + +- (SBElementArray *) EQPresets +{ + return [self elementArrayWithCode:'cEQP']; +} + + +- (SBElementArray *) EQWindows +{ + return [self elementArrayWithCode:'cEQW']; +} + + +- (SBElementArray *) playlistWindows +{ + return [self elementArrayWithCode:'cPlW']; +} + + +- (SBElementArray *) sources +{ + return [self elementArrayWithCode:'cSrc']; +} + + +- (SBElementArray *) visuals +{ + return [self elementArrayWithCode:'cVis']; +} + + +- (SBElementArray *) windows +{ + return [self elementArrayWithCode:'cwin']; +} + + + +- (BOOL) AirPlayEnabled +{ + id v = [[self propertyWithCode:'pAPE'] get]; + return [v boolValue]; +} + +- (BOOL) converting +{ + id v = [[self propertyWithCode:'pCnv'] get]; + return [v boolValue]; +} + +- (NSArray *) currentAirPlayDevices +{ + return [[self propertyWithCode:'pAPD'] get]; +} + +- (void) setCurrentAirPlayDevices: (NSArray *) currentAirPlayDevices +{ + [[self propertyWithCode:'pAPD'] setTo:currentAirPlayDevices]; +} + +- (iTunesEncoder *) currentEncoder +{ + return (iTunesEncoder *) [self propertyWithClass:[iTunesEncoder class] code:'pEnc']; +} + +- (void) setCurrentEncoder: (iTunesEncoder *) currentEncoder +{ + [[self propertyWithClass:[iTunesEncoder class] code:'pEnc'] setTo:currentEncoder]; +} + +- (iTunesEQPreset *) currentEQPreset +{ + return (iTunesEQPreset *) [self propertyWithClass:[iTunesEQPreset class] code:'pEQP']; +} + +- (void) setCurrentEQPreset: (iTunesEQPreset *) currentEQPreset +{ + [[self propertyWithClass:[iTunesEQPreset class] code:'pEQP'] setTo:currentEQPreset]; +} + +- (iTunesPlaylist *) currentPlaylist +{ + return (iTunesPlaylist *) [self propertyWithClass:[iTunesPlaylist class] code:'pPla']; +} + +- (NSString *) currentStreamTitle +{ + return [[self propertyWithCode:'pStT'] get]; +} + +- (NSString *) currentStreamURL +{ + return [[self propertyWithCode:'pStU'] get]; +} + +- (iTunesTrack *) currentTrack +{ + return (iTunesTrack *) [self propertyWithClass:[iTunesTrack class] code:'pTrk']; +} + +- (iTunesVisual *) currentVisual +{ + return (iTunesVisual *) [self propertyWithClass:[iTunesVisual class] code:'pVis']; +} + +- (void) setCurrentVisual: (iTunesVisual *) currentVisual +{ + [[self propertyWithClass:[iTunesVisual class] code:'pVis'] setTo:currentVisual]; +} + +- (BOOL) EQEnabled +{ + id v = [[self propertyWithCode:'pEQ '] get]; + return [v boolValue]; +} + +- (void) setEQEnabled: (BOOL) EQEnabled +{ + id v = [NSNumber numberWithBool:EQEnabled]; + [[self propertyWithCode:'pEQ '] setTo:v]; +} + +- (BOOL) fixedIndexing +{ + id v = [[self propertyWithCode:'pFix'] get]; + return [v boolValue]; +} + +- (void) setFixedIndexing: (BOOL) fixedIndexing +{ + id v = [NSNumber numberWithBool:fixedIndexing]; + [[self propertyWithCode:'pFix'] setTo:v]; +} + +- (BOOL) frontmost +{ + id v = [[self propertyWithCode:'pisf'] get]; + return [v boolValue]; +} + +- (void) setFrontmost: (BOOL) frontmost +{ + id v = [NSNumber numberWithBool:frontmost]; + [[self propertyWithCode:'pisf'] setTo:v]; +} + +- (BOOL) fullScreen +{ + id v = [[self propertyWithCode:'pFSc'] get]; + return [v boolValue]; +} + +- (void) setFullScreen: (BOOL) fullScreen +{ + id v = [NSNumber numberWithBool:fullScreen]; + [[self propertyWithCode:'pFSc'] setTo:v]; +} + +- (NSString *) name +{ + return [[self propertyWithCode:'pnam'] get]; +} + +- (BOOL) mute +{ + id v = [[self propertyWithCode:'pMut'] get]; + return [v boolValue]; +} + +- (void) setMute: (BOOL) mute +{ + id v = [NSNumber numberWithBool:mute]; + [[self propertyWithCode:'pMut'] setTo:v]; +} + +- (double) playerPosition +{ + id v = [[self propertyWithCode:'pPos'] get]; + return [v doubleValue]; +} + +- (void) setPlayerPosition: (double) playerPosition +{ + id v = [NSNumber numberWithDouble:playerPosition]; + [[self propertyWithCode:'pPos'] setTo:v]; +} + +- (iTunesEPlS) playerState +{ + id v = [[self propertyWithCode:'pPlS'] get]; + return [v enumCodeValue]; +} + +- (SBObject *) selection +{ + return (SBObject *) [self propertyWithClass:[SBObject class] code:'sele']; +} + +- (NSInteger) soundVolume +{ + id v = [[self propertyWithCode:'pVol'] get]; + return [v integerValue]; +} + +- (void) setSoundVolume: (NSInteger) soundVolume +{ + id v = [NSNumber numberWithInteger:soundVolume]; + [[self propertyWithCode:'pVol'] setTo:v]; +} + +- (NSString *) version +{ + return [[self propertyWithCode:'vers'] get]; +} + +- (BOOL) visualsEnabled +{ + id v = [[self propertyWithCode:'pVsE'] get]; + return [v boolValue]; +} + +- (void) setVisualsEnabled: (BOOL) visualsEnabled +{ + id v = [NSNumber numberWithBool:visualsEnabled]; + [[self propertyWithCode:'pVsE'] setTo:v]; +} + +- (iTunesEVSz) visualSize +{ + id v = [[self propertyWithCode:'pVSz'] get]; + return [v enumCodeValue]; +} + +- (void) setVisualSize: (iTunesEVSz) visualSize +{ + id v = [NSAppleEventDescriptor descriptorWithEnumCode:visualSize]; + [[self propertyWithCode:'pVSz'] setTo:v]; +} + +- (NSString *) iAdIdentifier +{ + return [[self propertyWithCode:'piAD'] get]; +} + + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme +{ + [self sendEvent:'aevt' id:'pdoc' parameters:'pdlg', [NSNumber numberWithBool:printDialog], 'prdt', withProperties, 'pKnd', [NSAppleEventDescriptor descriptorWithEnumCode:kind], 'pThm', theme, 0]; +} + +- (void) run +{ + [self sendEvent:'aevt' id:'oapp' parameters:0]; +} + +- (void) quit +{ + [self sendEvent:'aevt' id:'quit' parameters:0]; +} + +- (iTunesTrack *) add:(NSArray *)x to:(SBObject *)to +{ + id result__ = [self sendEvent:'hook' id:'Add ' parameters:'----', x, 'insh', to, 0]; + return result__; +} + +- (void) backTrack +{ + [self sendEvent:'hook' id:'Back' parameters:0]; +} + +- (iTunesTrack *) convert:(NSArray *)x +{ + id result__ = [self sendEvent:'hook' id:'Conv' parameters:'----', x, 0]; + return result__; +} + +- (void) fastForward +{ + [self sendEvent:'hook' id:'Fast' parameters:0]; +} + +- (void) nextTrack +{ + [self sendEvent:'hook' id:'Next' parameters:0]; +} + +- (void) pause +{ + [self sendEvent:'hook' id:'Paus' parameters:0]; +} + +- (void) playOnce:(BOOL)once +{ + [self sendEvent:'hook' id:'Play' parameters:'POne', [NSNumber numberWithBool:once], 0]; +} + +- (void) playpause +{ + [self sendEvent:'hook' id:'PlPs' parameters:0]; +} + +- (void) previousTrack +{ + [self sendEvent:'hook' id:'Prev' parameters:0]; +} + +- (void) resume +{ + [self sendEvent:'hook' id:'Resu' parameters:0]; +} + +- (void) rewind +{ + [self sendEvent:'hook' id:'Rwnd' parameters:0]; +} + +- (void) stop +{ + [self sendEvent:'hook' id:'Stop' parameters:0]; +} + +- (void) update +{ + [self sendEvent:'hook' id:'Updt' parameters:0]; +} + +- (void) eject +{ + [self sendEvent:'hook' id:'Ejct' parameters:0]; +} + +- (void) subscribe:(NSString *)x +{ + [self sendEvent:'hook' id:'pSub' parameters:'----', x, 0]; +} + +- (void) updateAllPodcasts +{ + [self sendEvent:'hook' id:'Updp' parameters:0]; +} + +- (void) updatePodcast +{ + [self sendEvent:'hook' id:'Upd1' parameters:0]; +} + +- (void) openLocation:(NSString *)x +{ + [self sendEvent:'GURL' id:'GURL' parameters:'----', x, 0]; +} + + +@end + + +@implementation iTunesItem + + +- (SBObject *) container +{ + return (SBObject *) [self propertyWithClass:[SBObject class] code:'ctnr']; +} + +- (NSInteger) id +{ + id v = [[self propertyWithCode:'ID '] get]; + return [v integerValue]; +} + +- (NSInteger) index +{ + id v = [[self propertyWithCode:'pidx'] get]; + return [v integerValue]; +} + +- (NSString *) name +{ + return [[self propertyWithCode:'pnam'] get]; +} + +- (void) setName: (NSString *) name +{ + [[self propertyWithCode:'pnam'] setTo:name]; +} + +- (NSString *) persistentID +{ + return [[self propertyWithCode:'pPIS'] get]; +} + +- (NSDictionary *) properties +{ + return [[self propertyWithCode:'pALL'] get]; +} + +- (void) setProperties: (NSDictionary *) properties +{ + [[self propertyWithCode:'pALL'] setTo:properties]; +} + + +- (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme +{ + [self sendEvent:'aevt' id:'pdoc' parameters:'pdlg', [NSNumber numberWithBool:printDialog], 'prdt', withProperties, 'pKnd', [NSAppleEventDescriptor descriptorWithEnumCode:kind], 'pThm', theme, 0]; +} + +- (void) close +{ + [self sendEvent:'core' id:'clos' parameters:0]; +} + +- (void) delete +{ + [self sendEvent:'core' id:'delo' parameters:0]; +} + +- (SBObject *) duplicateTo:(SBObject *)to +{ + id result__ = [self sendEvent:'core' id:'clon' parameters:'insh', to, 0]; + return result__; +} + +- (BOOL) exists +{ + id result__ = [self sendEvent:'core' id:'doex' parameters:0]; + return [result__ boolValue]; +} + +- (void) open +{ + [self sendEvent:'aevt' id:'odoc' parameters:0]; +} + +- (void) playOnce:(BOOL)once +{ + [self sendEvent:'hook' id:'Play' parameters:'POne', [NSNumber numberWithBool:once], 0]; +} + +- (void) reveal +{ + [self sendEvent:'hook' id:'Revl' parameters:0]; +} + + +@end + + +@implementation iTunesAirPlayDevice + + +- (BOOL) active +{ + id v = [[self propertyWithCode:'pAct'] get]; + return [v boolValue]; +} + +- (BOOL) available +{ + id v = [[self propertyWithCode:'pAva'] get]; + return [v boolValue]; +} + +- (iTunesEAPD) kind +{ + id v = [[self propertyWithCode:'pKnd'] get]; + return [v enumCodeValue]; +} + +- (NSString *) networkAddress +{ + return [[self propertyWithCode:'pMAC'] get]; +} + +- (BOOL) protected +{ + id v = [[self propertyWithCode:'pPro'] get]; + return [v boolValue]; +} + +- (BOOL) selected +{ + id v = [[self propertyWithCode:'selc'] get]; + return [v boolValue]; +} + +- (void) setSelected: (BOOL) selected +{ + id v = [NSNumber numberWithBool:selected]; + [[self propertyWithCode:'selc'] setTo:v]; +} + +- (BOOL) supportsAudio +{ + id v = [[self propertyWithCode:'pAud'] get]; + return [v boolValue]; +} + +- (BOOL) supportsVideo +{ + id v = [[self propertyWithCode:'pVid'] get]; + return [v boolValue]; +} + +- (NSInteger) soundVolume +{ + id v = [[self propertyWithCode:'pVol'] get]; + return [v integerValue]; +} + +- (void) setSoundVolume: (NSInteger) soundVolume +{ + id v = [NSNumber numberWithInteger:soundVolume]; + [[self propertyWithCode:'pVol'] setTo:v]; +} + + + +@end + + +@implementation iTunesArtwork + + +- (NSImage *) data +{ + return [[self propertyWithCode:'pPCT'] get]; +} + +- (void) setData: (NSImage *) data +{ + [[self propertyWithCode:'pPCT'] setTo:data]; +} + +- (NSString *) objectDescription +{ + return [[self propertyWithCode:'pDes'] get]; +} + +- (void) setObjectDescription: (NSString *) objectDescription +{ + [[self propertyWithCode:'pDes'] setTo:objectDescription]; +} + +- (BOOL) downloaded +{ + id v = [[self propertyWithCode:'pDlA'] get]; + return [v boolValue]; +} + +- (NSNumber *) format +{ + return [[self propertyWithCode:'pFmt'] get]; +} + +- (NSInteger) kind +{ + id v = [[self propertyWithCode:'pKnd'] get]; + return [v integerValue]; +} + +- (void) setKind: (NSInteger) kind +{ + id v = [NSNumber numberWithInteger:kind]; + [[self propertyWithCode:'pKnd'] setTo:v]; +} + +- (NSData *) rawData +{ + id v = [[self propertyWithCode:'pRaw'] get]; + return [v data]; +} + +- (void) setRawData: (NSData *) rawData +{ + id v = [NSAppleEventDescriptor descriptorWithDescriptorType:'tdta' data:rawData]; + [[self propertyWithCode:'pRaw'] setTo:v]; +} + + + +@end + + +@implementation iTunesEncoder + + +- (NSString *) format +{ + return [[self propertyWithCode:'pFmt'] get]; +} + + + +@end + + +@implementation iTunesEQPreset + + +- (double) band1 +{ + id v = [[self propertyWithCode:'pEQ1'] get]; + return [v doubleValue]; +} + +- (void) setBand1: (double) band1 +{ + id v = [NSNumber numberWithDouble:band1]; + [[self propertyWithCode:'pEQ1'] setTo:v]; +} + +- (double) band2 +{ + id v = [[self propertyWithCode:'pEQ2'] get]; + return [v doubleValue]; +} + +- (void) setBand2: (double) band2 +{ + id v = [NSNumber numberWithDouble:band2]; + [[self propertyWithCode:'pEQ2'] setTo:v]; +} + +- (double) band3 +{ + id v = [[self propertyWithCode:'pEQ3'] get]; + return [v doubleValue]; +} + +- (void) setBand3: (double) band3 +{ + id v = [NSNumber numberWithDouble:band3]; + [[self propertyWithCode:'pEQ3'] setTo:v]; +} + +- (double) band4 +{ + id v = [[self propertyWithCode:'pEQ4'] get]; + return [v doubleValue]; +} + +- (void) setBand4: (double) band4 +{ + id v = [NSNumber numberWithDouble:band4]; + [[self propertyWithCode:'pEQ4'] setTo:v]; +} + +- (double) band5 +{ + id v = [[self propertyWithCode:'pEQ5'] get]; + return [v doubleValue]; +} + +- (void) setBand5: (double) band5 +{ + id v = [NSNumber numberWithDouble:band5]; + [[self propertyWithCode:'pEQ5'] setTo:v]; +} + +- (double) band6 +{ + id v = [[self propertyWithCode:'pEQ6'] get]; + return [v doubleValue]; +} + +- (void) setBand6: (double) band6 +{ + id v = [NSNumber numberWithDouble:band6]; + [[self propertyWithCode:'pEQ6'] setTo:v]; +} + +- (double) band7 +{ + id v = [[self propertyWithCode:'pEQ7'] get]; + return [v doubleValue]; +} + +- (void) setBand7: (double) band7 +{ + id v = [NSNumber numberWithDouble:band7]; + [[self propertyWithCode:'pEQ7'] setTo:v]; +} + +- (double) band8 +{ + id v = [[self propertyWithCode:'pEQ8'] get]; + return [v doubleValue]; +} + +- (void) setBand8: (double) band8 +{ + id v = [NSNumber numberWithDouble:band8]; + [[self propertyWithCode:'pEQ8'] setTo:v]; +} + +- (double) band9 +{ + id v = [[self propertyWithCode:'pEQ9'] get]; + return [v doubleValue]; +} + +- (void) setBand9: (double) band9 +{ + id v = [NSNumber numberWithDouble:band9]; + [[self propertyWithCode:'pEQ9'] setTo:v]; +} + +- (double) band10 +{ + id v = [[self propertyWithCode:'pEQ0'] get]; + return [v doubleValue]; +} + +- (void) setBand10: (double) band10 +{ + id v = [NSNumber numberWithDouble:band10]; + [[self propertyWithCode:'pEQ0'] setTo:v]; +} + +- (BOOL) modifiable +{ + id v = [[self propertyWithCode:'pMod'] get]; + return [v boolValue]; +} + +- (double) preamp +{ + id v = [[self propertyWithCode:'pEQA'] get]; + return [v doubleValue]; +} + +- (void) setPreamp: (double) preamp +{ + id v = [NSNumber numberWithDouble:preamp]; + [[self propertyWithCode:'pEQA'] setTo:v]; +} + +- (BOOL) updateTracks +{ + id v = [[self propertyWithCode:'pUTC'] get]; + return [v boolValue]; +} + +- (void) setUpdateTracks: (BOOL) updateTracks +{ + id v = [NSNumber numberWithBool:updateTracks]; + [[self propertyWithCode:'pUTC'] setTo:v]; +} + + + +@end + + +@implementation iTunesPlaylist + + +- (SBElementArray *) tracks +{ + return [self elementArrayWithCode:'cTrk']; +} + + + +- (NSInteger) duration +{ + id v = [[self propertyWithCode:'pDur'] get]; + return [v integerValue]; +} + +- (NSString *) name +{ + return [[self propertyWithCode:'pnam'] get]; +} + +- (void) setName: (NSString *) name +{ + [[self propertyWithCode:'pnam'] setTo:name]; +} + +- (iTunesPlaylist *) parent +{ + return (iTunesPlaylist *) [self propertyWithClass:[iTunesPlaylist class] code:'pPlP']; +} + +- (BOOL) shuffle +{ + id v = [[self propertyWithCode:'pShf'] get]; + return [v boolValue]; +} + +- (void) setShuffle: (BOOL) shuffle +{ + id v = [NSNumber numberWithBool:shuffle]; + [[self propertyWithCode:'pShf'] setTo:v]; +} + +- (NSInteger) size +{ + id v = [[self propertyWithCode:'pSiz'] get]; + return [v integerValue]; +} + +- (iTunesERpt) songRepeat +{ + id v = [[self propertyWithCode:'pRpt'] get]; + return [v enumCodeValue]; +} + +- (void) setSongRepeat: (iTunesERpt) songRepeat +{ + id v = [NSAppleEventDescriptor descriptorWithEnumCode:songRepeat]; + [[self propertyWithCode:'pRpt'] setTo:v]; +} + +- (iTunesESpK) specialKind +{ + id v = [[self propertyWithCode:'pSpK'] get]; + return [v enumCodeValue]; +} + +- (NSString *) time +{ + return [[self propertyWithCode:'pTim'] get]; +} + +- (BOOL) visible +{ + id v = [[self propertyWithCode:'pvis'] get]; + return [v boolValue]; +} + + +- (void) moveTo:(SBObject *)to +{ + [self sendEvent:'core' id:'move' parameters:'insh', to, 0]; +} + +- (iTunesTrack *) searchFor:(NSString *)for_ only:(iTunesESrA)only +{ + id result__ = [self sendEvent:'hook' id:'Srch' parameters:'pTrm', for_, 'pAre', [NSAppleEventDescriptor descriptorWithEnumCode:only], 0]; + return result__; +} + + +@end + + +@implementation iTunesAudioCDPlaylist + + +- (SBElementArray *) audioCDTracks +{ + return [self elementArrayWithCode:'cCDT']; +} + + + +- (NSString *) artist +{ + return [[self propertyWithCode:'pArt'] get]; +} + +- (void) setArtist: (NSString *) artist +{ + [[self propertyWithCode:'pArt'] setTo:artist]; +} + +- (BOOL) compilation +{ + id v = [[self propertyWithCode:'pAnt'] get]; + return [v boolValue]; +} + +- (void) setCompilation: (BOOL) compilation +{ + id v = [NSNumber numberWithBool:compilation]; + [[self propertyWithCode:'pAnt'] setTo:v]; +} + +- (NSString *) composer +{ + return [[self propertyWithCode:'pCmp'] get]; +} + +- (void) setComposer: (NSString *) composer +{ + [[self propertyWithCode:'pCmp'] setTo:composer]; +} + +- (NSInteger) discCount +{ + id v = [[self propertyWithCode:'pDsC'] get]; + return [v integerValue]; +} + +- (void) setDiscCount: (NSInteger) discCount +{ + id v = [NSNumber numberWithInteger:discCount]; + [[self propertyWithCode:'pDsC'] setTo:v]; +} + +- (NSInteger) discNumber +{ + id v = [[self propertyWithCode:'pDsN'] get]; + return [v integerValue]; +} + +- (void) setDiscNumber: (NSInteger) discNumber +{ + id v = [NSNumber numberWithInteger:discNumber]; + [[self propertyWithCode:'pDsN'] setTo:v]; +} + +- (NSString *) genre +{ + return [[self propertyWithCode:'pGen'] get]; +} + +- (void) setGenre: (NSString *) genre +{ + [[self propertyWithCode:'pGen'] setTo:genre]; +} + +- (NSInteger) year +{ + id v = [[self propertyWithCode:'pYr '] get]; + return [v integerValue]; +} + +- (void) setYear: (NSInteger) year +{ + id v = [NSNumber numberWithInteger:year]; + [[self propertyWithCode:'pYr '] setTo:v]; +} + + + +@end + + +@implementation iTunesLibraryPlaylist + + +- (SBElementArray *) fileTracks +{ + return [self elementArrayWithCode:'cFlT']; +} + + +- (SBElementArray *) URLTracks +{ + return [self elementArrayWithCode:'cURT']; +} + + +- (SBElementArray *) sharedTracks +{ + return [self elementArrayWithCode:'cShT']; +} + + + + +@end + + +@implementation iTunesRadioTunerPlaylist + + +- (SBElementArray *) URLTracks +{ + return [self elementArrayWithCode:'cURT']; +} + + + + +@end + + +@implementation iTunesSource + + +- (SBElementArray *) audioCDPlaylists +{ + return [self elementArrayWithCode:'cCDP']; +} + + +- (SBElementArray *) libraryPlaylists +{ + return [self elementArrayWithCode:'cLiP']; +} + + +- (SBElementArray *) playlists +{ + return [self elementArrayWithCode:'cPly']; +} + + +- (SBElementArray *) radioTunerPlaylists +{ + return [self elementArrayWithCode:'cRTP']; +} + + +- (SBElementArray *) userPlaylists +{ + return [self elementArrayWithCode:'cUsP']; +} + + + +- (long long) capacity +{ + id v = [[self propertyWithCode:'capa'] get]; + return [v longLongValue]; +} + +- (long long) freeSpace +{ + id v = [[self propertyWithCode:'frsp'] get]; + return [v longLongValue]; +} + +- (iTunesESrc) kind +{ + id v = [[self propertyWithCode:'pKnd'] get]; + return [v enumCodeValue]; +} + + +- (void) update +{ + [self sendEvent:'hook' id:'Updt' parameters:0]; +} + +- (void) eject +{ + [self sendEvent:'hook' id:'Ejct' parameters:0]; +} + + +@end + + +@implementation iTunesTrack + + +- (SBElementArray *) artworks +{ + return [self elementArrayWithCode:'cArt']; +} + + + +- (NSString *) album +{ + return [[self propertyWithCode:'pAlb'] get]; +} + +- (void) setAlbum: (NSString *) album +{ + [[self propertyWithCode:'pAlb'] setTo:album]; +} + +- (NSString *) albumArtist +{ + return [[self propertyWithCode:'pAlA'] get]; +} + +- (void) setAlbumArtist: (NSString *) albumArtist +{ + [[self propertyWithCode:'pAlA'] setTo:albumArtist]; +} + +- (NSInteger) albumRating +{ + id v = [[self propertyWithCode:'pAlR'] get]; + return [v integerValue]; +} + +- (void) setAlbumRating: (NSInteger) albumRating +{ + id v = [NSNumber numberWithInteger:albumRating]; + [[self propertyWithCode:'pAlR'] setTo:v]; +} + +- (iTunesERtK) albumRatingKind +{ + id v = [[self propertyWithCode:'pARk'] get]; + return [v enumCodeValue]; +} + +- (NSString *) artist +{ + return [[self propertyWithCode:'pArt'] get]; +} + +- (void) setArtist: (NSString *) artist +{ + [[self propertyWithCode:'pArt'] setTo:artist]; +} + +- (NSInteger) bitRate +{ + id v = [[self propertyWithCode:'pBRt'] get]; + return [v integerValue]; +} + +- (double) bookmark +{ + id v = [[self propertyWithCode:'pBkt'] get]; + return [v doubleValue]; +} + +- (void) setBookmark: (double) bookmark +{ + id v = [NSNumber numberWithDouble:bookmark]; + [[self propertyWithCode:'pBkt'] setTo:v]; +} + +- (BOOL) bookmarkable +{ + id v = [[self propertyWithCode:'pBkm'] get]; + return [v boolValue]; +} + +- (void) setBookmarkable: (BOOL) bookmarkable +{ + id v = [NSNumber numberWithBool:bookmarkable]; + [[self propertyWithCode:'pBkm'] setTo:v]; +} + +- (NSInteger) bpm +{ + id v = [[self propertyWithCode:'pBPM'] get]; + return [v integerValue]; +} + +- (void) setBpm: (NSInteger) bpm +{ + id v = [NSNumber numberWithInteger:bpm]; + [[self propertyWithCode:'pBPM'] setTo:v]; +} + +- (NSString *) category +{ + return [[self propertyWithCode:'pCat'] get]; +} + +- (void) setCategory: (NSString *) category +{ + [[self propertyWithCode:'pCat'] setTo:category]; +} + +- (NSString *) comment +{ + return [[self propertyWithCode:'pCmt'] get]; +} + +- (void) setComment: (NSString *) comment +{ + [[self propertyWithCode:'pCmt'] setTo:comment]; +} + +- (BOOL) compilation +{ + id v = [[self propertyWithCode:'pAnt'] get]; + return [v boolValue]; +} + +- (void) setCompilation: (BOOL) compilation +{ + id v = [NSNumber numberWithBool:compilation]; + [[self propertyWithCode:'pAnt'] setTo:v]; +} + +- (NSString *) composer +{ + return [[self propertyWithCode:'pCmp'] get]; +} + +- (void) setComposer: (NSString *) composer +{ + [[self propertyWithCode:'pCmp'] setTo:composer]; +} + +- (NSInteger) databaseID +{ + id v = [[self propertyWithCode:'pDID'] get]; + return [v integerValue]; +} + +- (NSDate *) dateAdded +{ + return [[self propertyWithCode:'pAdd'] get]; +} + +- (NSString *) objectDescription +{ + return [[self propertyWithCode:'pDes'] get]; +} + +- (void) setObjectDescription: (NSString *) objectDescription +{ + [[self propertyWithCode:'pDes'] setTo:objectDescription]; +} + +- (NSInteger) discCount +{ + id v = [[self propertyWithCode:'pDsC'] get]; + return [v integerValue]; +} + +- (void) setDiscCount: (NSInteger) discCount +{ + id v = [NSNumber numberWithInteger:discCount]; + [[self propertyWithCode:'pDsC'] setTo:v]; +} + +- (NSInteger) discNumber +{ + id v = [[self propertyWithCode:'pDsN'] get]; + return [v integerValue]; +} + +- (void) setDiscNumber: (NSInteger) discNumber +{ + id v = [NSNumber numberWithInteger:discNumber]; + [[self propertyWithCode:'pDsN'] setTo:v]; +} + +- (double) duration +{ + id v = [[self propertyWithCode:'pDur'] get]; + return [v doubleValue]; +} + +- (BOOL) enabled +{ + id v = [[self propertyWithCode:'enbl'] get]; + return [v boolValue]; +} + +- (void) setEnabled: (BOOL) enabled +{ + id v = [NSNumber numberWithBool:enabled]; + [[self propertyWithCode:'enbl'] setTo:v]; +} + +- (NSString *) episodeID +{ + return [[self propertyWithCode:'pEpD'] get]; +} + +- (void) setEpisodeID: (NSString *) episodeID +{ + [[self propertyWithCode:'pEpD'] setTo:episodeID]; +} + +- (NSInteger) episodeNumber +{ + id v = [[self propertyWithCode:'pEpN'] get]; + return [v integerValue]; +} + +- (void) setEpisodeNumber: (NSInteger) episodeNumber +{ + id v = [NSNumber numberWithInteger:episodeNumber]; + [[self propertyWithCode:'pEpN'] setTo:v]; +} + +- (NSString *) EQ +{ + return [[self propertyWithCode:'pEQp'] get]; +} + +- (void) setEQ: (NSString *) EQ +{ + [[self propertyWithCode:'pEQp'] setTo:EQ]; +} + +- (double) finish +{ + id v = [[self propertyWithCode:'pStp'] get]; + return [v doubleValue]; +} + +- (void) setFinish: (double) finish +{ + id v = [NSNumber numberWithDouble:finish]; + [[self propertyWithCode:'pStp'] setTo:v]; +} + +- (BOOL) gapless +{ + id v = [[self propertyWithCode:'pGpl'] get]; + return [v boolValue]; +} + +- (void) setGapless: (BOOL) gapless +{ + id v = [NSNumber numberWithBool:gapless]; + [[self propertyWithCode:'pGpl'] setTo:v]; +} + +- (NSString *) genre +{ + return [[self propertyWithCode:'pGen'] get]; +} + +- (void) setGenre: (NSString *) genre +{ + [[self propertyWithCode:'pGen'] setTo:genre]; +} + +- (NSString *) grouping +{ + return [[self propertyWithCode:'pGrp'] get]; +} + +- (void) setGrouping: (NSString *) grouping +{ + [[self propertyWithCode:'pGrp'] setTo:grouping]; +} + +- (BOOL) iTunesU +{ + id v = [[self propertyWithCode:'pTIU'] get]; + return [v boolValue]; +} + +- (NSString *) kind +{ + return [[self propertyWithCode:'pKnd'] get]; +} + +- (NSString *) longDescription +{ + return [[self propertyWithCode:'pLds'] get]; +} + +- (void) setLongDescription: (NSString *) longDescription +{ + [[self propertyWithCode:'pLds'] setTo:longDescription]; +} + +- (NSString *) lyrics +{ + return [[self propertyWithCode:'pLyr'] get]; +} + +- (void) setLyrics: (NSString *) lyrics +{ + [[self propertyWithCode:'pLyr'] setTo:lyrics]; +} + +- (NSDate *) modificationDate +{ + return [[self propertyWithCode:'asmo'] get]; +} + +- (NSInteger) playedCount +{ + id v = [[self propertyWithCode:'pPlC'] get]; + return [v integerValue]; +} + +- (void) setPlayedCount: (NSInteger) playedCount +{ + id v = [NSNumber numberWithInteger:playedCount]; + [[self propertyWithCode:'pPlC'] setTo:v]; +} + +- (NSDate *) playedDate +{ + return [[self propertyWithCode:'pPlD'] get]; +} + +- (void) setPlayedDate: (NSDate *) playedDate +{ + [[self propertyWithCode:'pPlD'] setTo:playedDate]; +} + +- (BOOL) podcast +{ + id v = [[self propertyWithCode:'pTPc'] get]; + return [v boolValue]; +} + +- (NSInteger) rating +{ + id v = [[self propertyWithCode:'pRte'] get]; + return [v integerValue]; +} + +- (void) setRating: (NSInteger) rating +{ + id v = [NSNumber numberWithInteger:rating]; + [[self propertyWithCode:'pRte'] setTo:v]; +} + +- (iTunesERtK) ratingKind +{ + id v = [[self propertyWithCode:'pRtk'] get]; + return [v enumCodeValue]; +} + +- (NSDate *) releaseDate +{ + return [[self propertyWithCode:'pRlD'] get]; +} + +- (NSInteger) sampleRate +{ + id v = [[self propertyWithCode:'pSRt'] get]; + return [v integerValue]; +} + +- (NSInteger) seasonNumber +{ + id v = [[self propertyWithCode:'pSeN'] get]; + return [v integerValue]; +} + +- (void) setSeasonNumber: (NSInteger) seasonNumber +{ + id v = [NSNumber numberWithInteger:seasonNumber]; + [[self propertyWithCode:'pSeN'] setTo:v]; +} + +- (BOOL) shufflable +{ + id v = [[self propertyWithCode:'pSfa'] get]; + return [v boolValue]; +} + +- (void) setShufflable: (BOOL) shufflable +{ + id v = [NSNumber numberWithBool:shufflable]; + [[self propertyWithCode:'pSfa'] setTo:v]; +} + +- (NSInteger) skippedCount +{ + id v = [[self propertyWithCode:'pSkC'] get]; + return [v integerValue]; +} + +- (void) setSkippedCount: (NSInteger) skippedCount +{ + id v = [NSNumber numberWithInteger:skippedCount]; + [[self propertyWithCode:'pSkC'] setTo:v]; +} + +- (NSDate *) skippedDate +{ + return [[self propertyWithCode:'pSkD'] get]; +} + +- (void) setSkippedDate: (NSDate *) skippedDate +{ + [[self propertyWithCode:'pSkD'] setTo:skippedDate]; +} + +- (NSString *) show +{ + return [[self propertyWithCode:'pShw'] get]; +} + +- (void) setShow: (NSString *) show +{ + [[self propertyWithCode:'pShw'] setTo:show]; +} + +- (NSString *) sortAlbum +{ + return [[self propertyWithCode:'pSAl'] get]; +} + +- (void) setSortAlbum: (NSString *) sortAlbum +{ + [[self propertyWithCode:'pSAl'] setTo:sortAlbum]; +} + +- (NSString *) sortArtist +{ + return [[self propertyWithCode:'pSAr'] get]; +} + +- (void) setSortArtist: (NSString *) sortArtist +{ + [[self propertyWithCode:'pSAr'] setTo:sortArtist]; +} + +- (NSString *) sortAlbumArtist +{ + return [[self propertyWithCode:'pSAA'] get]; +} + +- (void) setSortAlbumArtist: (NSString *) sortAlbumArtist +{ + [[self propertyWithCode:'pSAA'] setTo:sortAlbumArtist]; +} + +- (NSString *) sortName +{ + return [[self propertyWithCode:'pSNm'] get]; +} + +- (void) setSortName: (NSString *) sortName +{ + [[self propertyWithCode:'pSNm'] setTo:sortName]; +} + +- (NSString *) sortComposer +{ + return [[self propertyWithCode:'pSCm'] get]; +} + +- (void) setSortComposer: (NSString *) sortComposer +{ + [[self propertyWithCode:'pSCm'] setTo:sortComposer]; +} + +- (NSString *) sortShow +{ + return [[self propertyWithCode:'pSSN'] get]; +} + +- (void) setSortShow: (NSString *) sortShow +{ + [[self propertyWithCode:'pSSN'] setTo:sortShow]; +} + +- (long long) size +{ + id v = [[self propertyWithCode:'pSiz'] get]; + return [v longLongValue]; +} + +- (double) start +{ + id v = [[self propertyWithCode:'pStr'] get]; + return [v doubleValue]; +} + +- (void) setStart: (double) start +{ + id v = [NSNumber numberWithDouble:start]; + [[self propertyWithCode:'pStr'] setTo:v]; +} + +- (NSString *) time +{ + return [[self propertyWithCode:'pTim'] get]; +} + +- (NSInteger) trackCount +{ + id v = [[self propertyWithCode:'pTrC'] get]; + return [v integerValue]; +} + +- (void) setTrackCount: (NSInteger) trackCount +{ + id v = [NSNumber numberWithInteger:trackCount]; + [[self propertyWithCode:'pTrC'] setTo:v]; +} + +- (NSInteger) trackNumber +{ + id v = [[self propertyWithCode:'pTrN'] get]; + return [v integerValue]; +} + +- (void) setTrackNumber: (NSInteger) trackNumber +{ + id v = [NSNumber numberWithInteger:trackNumber]; + [[self propertyWithCode:'pTrN'] setTo:v]; +} + +- (BOOL) unplayed +{ + id v = [[self propertyWithCode:'pUnp'] get]; + return [v boolValue]; +} + +- (void) setUnplayed: (BOOL) unplayed +{ + id v = [NSNumber numberWithBool:unplayed]; + [[self propertyWithCode:'pUnp'] setTo:v]; +} + +- (iTunesEVdK) videoKind +{ + id v = [[self propertyWithCode:'pVdK'] get]; + return [v enumCodeValue]; +} + +- (void) setVideoKind: (iTunesEVdK) videoKind +{ + id v = [NSAppleEventDescriptor descriptorWithEnumCode:videoKind]; + [[self propertyWithCode:'pVdK'] setTo:v]; +} + +- (NSInteger) volumeAdjustment +{ + id v = [[self propertyWithCode:'pAdj'] get]; + return [v integerValue]; +} + +- (void) setVolumeAdjustment: (NSInteger) volumeAdjustment +{ + id v = [NSNumber numberWithInteger:volumeAdjustment]; + [[self propertyWithCode:'pAdj'] setTo:v]; +} + +- (NSInteger) year +{ + id v = [[self propertyWithCode:'pYr '] get]; + return [v integerValue]; +} + +- (void) setYear: (NSInteger) year +{ + id v = [NSNumber numberWithInteger:year]; + [[self propertyWithCode:'pYr '] setTo:v]; +} + + + +@end + + +@implementation iTunesAudioCDTrack + + +- (NSURL *) location +{ + return [[self propertyWithCode:'pLoc'] get]; +} + + + +@end + + +@implementation iTunesFileTrack + + +- (NSURL *) location +{ + return [[self propertyWithCode:'pLoc'] get]; +} + +- (void) setLocation: (NSURL *) location +{ + [[self propertyWithCode:'pLoc'] setTo:location]; +} + + +- (void) refresh +{ + [self sendEvent:'hook' id:'Rfrs' parameters:0]; +} + + +@end + + +@implementation iTunesSharedTrack + + + +@end + + +@implementation iTunesURLTrack + + +- (NSString *) address +{ + return [[self propertyWithCode:'pURL'] get]; +} + +- (void) setAddress: (NSString *) address +{ + [[self propertyWithCode:'pURL'] setTo:address]; +} + + +- (void) download +{ + [self sendEvent:'hook' id:'Dwnl' parameters:0]; +} + + +@end + + +@implementation iTunesUserPlaylist + + +- (SBElementArray *) fileTracks +{ + return [self elementArrayWithCode:'cFlT']; +} + + +- (SBElementArray *) URLTracks +{ + return [self elementArrayWithCode:'cURT']; +} + + +- (SBElementArray *) sharedTracks +{ + return [self elementArrayWithCode:'cShT']; +} + + + +- (BOOL) shared +{ + id v = [[self propertyWithCode:'pShr'] get]; + return [v boolValue]; +} + +- (void) setShared: (BOOL) shared +{ + id v = [NSNumber numberWithBool:shared]; + [[self propertyWithCode:'pShr'] setTo:v]; +} + +- (BOOL) smart +{ + id v = [[self propertyWithCode:'pSmt'] get]; + return [v boolValue]; +} + + + +@end + + +@implementation iTunesFolderPlaylist + + + +@end + + +@implementation iTunesVisual + + + +@end + + +@implementation iTunesWindow + + +- (NSRect) bounds +{ + id v = [[self propertyWithCode:'pbnd'] get]; + return [v rectValue]; +} + +- (void) setBounds: (NSRect) bounds +{ + id v = [NSValue valueWithRect:bounds]; + [[self propertyWithCode:'pbnd'] setTo:v]; +} + +- (BOOL) closeable +{ + id v = [[self propertyWithCode:'hclb'] get]; + return [v boolValue]; +} + +- (BOOL) collapseable +{ + id v = [[self propertyWithCode:'pWSh'] get]; + return [v boolValue]; +} + +- (BOOL) collapsed +{ + id v = [[self propertyWithCode:'wshd'] get]; + return [v boolValue]; +} + +- (void) setCollapsed: (BOOL) collapsed +{ + id v = [NSNumber numberWithBool:collapsed]; + [[self propertyWithCode:'wshd'] setTo:v]; +} + +- (NSPoint) position +{ + id v = [[self propertyWithCode:'ppos'] get]; + return [v pointValue]; +} + +- (void) setPosition: (NSPoint) position +{ + id v = [NSValue valueWithPoint:position]; + [[self propertyWithCode:'ppos'] setTo:v]; +} + +- (BOOL) resizable +{ + id v = [[self propertyWithCode:'prsz'] get]; + return [v boolValue]; +} + +- (BOOL) visible +{ + id v = [[self propertyWithCode:'pvis'] get]; + return [v boolValue]; +} + +- (void) setVisible: (BOOL) visible +{ + id v = [NSNumber numberWithBool:visible]; + [[self propertyWithCode:'pvis'] setTo:v]; +} + +- (BOOL) zoomable +{ + id v = [[self propertyWithCode:'iszm'] get]; + return [v boolValue]; +} + +- (BOOL) zoomed +{ + id v = [[self propertyWithCode:'pzum'] get]; + return [v boolValue]; +} + +- (void) setZoomed: (BOOL) zoomed +{ + id v = [NSNumber numberWithBool:zoomed]; + [[self propertyWithCode:'pzum'] setTo:v]; +} + + + +@end + + +@implementation iTunesBrowserWindow + + +- (BOOL) minimized +{ + id v = [[self propertyWithCode:'pMin'] get]; + return [v boolValue]; +} + +- (void) setMinimized: (BOOL) minimized +{ + id v = [NSNumber numberWithBool:minimized]; + [[self propertyWithCode:'pMin'] setTo:v]; +} + +- (SBObject *) selection +{ + return (SBObject *) [self propertyWithClass:[SBObject class] code:'sele']; +} + +- (iTunesPlaylist *) view +{ + return (iTunesPlaylist *) [self propertyWithClass:[iTunesPlaylist class] code:'pPly']; +} + +- (void) setView: (iTunesPlaylist *) view +{ + [[self propertyWithClass:[iTunesPlaylist class] code:'pPly'] setTo:view]; +} + + + +@end + + +@implementation iTunesEQWindow + + +- (BOOL) minimized +{ + id v = [[self propertyWithCode:'pMin'] get]; + return [v boolValue]; +} + +- (void) setMinimized: (BOOL) minimized +{ + id v = [NSNumber numberWithBool:minimized]; + [[self propertyWithCode:'pMin'] setTo:v]; +} + + + +@end + + +@implementation iTunesPlaylistWindow + + +- (SBObject *) selection +{ + return (SBObject *) [self propertyWithClass:[SBObject class] code:'sele']; +} + +- (iTunesPlaylist *) view +{ + return (iTunesPlaylist *) [self propertyWithClass:[iTunesPlaylist class] code:'pPly']; +} + + + +@end + + diff --git a/TagTunes/iTunes.swift b/TagTunes/iTunes.swift new file mode 100644 index 0000000..f448f69 --- /dev/null +++ b/TagTunes/iTunes.swift @@ -0,0 +1,177 @@ +// +// iTunes.swift +// Harmony +// +// Created by Kim Wittenburg on 14.04.15. +// Copyright (c) 2015 Das Code Kollektiv. All rights reserved. +// + +import Foundation +import AppKitPlus + +/// The Cocoa Scripting Bridge interface of iTunes. +public let iTunes = iTunesApplication(bundleIdentifier: "com.apple.iTunes")! + +/// An ID as returned from the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) +public typealias iTunesId = UInt + + +/// This struct contains static helper objects and methods to work with the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public struct iTunesAPI { + + // MARK: Types + + /// Error types indicating error responses from the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). + public enum Error: ErrorType { + case InvalidCountryCode + case InvalidLanguageCode + case UnknownError + } + + /// Contains constants identifying the fields in a response from the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). + public enum Field: String { + case WrapperType = "wrapperType" + case Kind = "kind" + + case TrackId = "trackId" + case TrackName = "trackName" + case TrackCensoredName = "trackCensoredName" + case ArtistName = "artistName" + case ReleaseDate = "releaseDate" + case TrackNumber = "trackNumber" + case TrackCount = "trackCount" + case DiscNumber = "discNumber" + case DiscCount = "discCount" + case PrimaryGenreName = "primaryGenreName" + + case CollectionId = "collectionId" + case CollectionName = "collectionName" + case CollectionCensoredName = "collectionCensoredName" + case CollectionViewUrl = "collectionViewUrl" + case CollectionArtistName = "collectionArtistName" + + case ArtworkUrl60 = "artworkUrl60" + case ArtworkUrl100 = "artworkUrl100" + } + + // MARK: Static Properties and Functions + + /// This formatter is configured to be used to parse dates returned from the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) + internal static let sharedDateFormatter: NSDateFormatter = { + let dateFormatter = NSDateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + return dateFormatter + }() + + /// Processes the data returned from a request to the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). + /// The `data` has to be in a valid JSON format. See `NSJSONSerialization` + /// for details. + /// + /// Currently only tracks and albums are supported. If there are any other + /// entries in the specified data this function will raise an exception. + /// + /// - throws: Parsing errors and `iTUnesAPI.Error` constants. + /// - returns: An array of albums populated with all associated tracks in the + /// specified data. + public static func parseAPIData(data: NSData) throws -> [Album] { + guard let parsedData = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else { + throw Error.UnknownError + } + // Handle API Errors + if let errorMessage = parsedData["errorMessage"] as? String { + switch errorMessage { + case "Invalid value(s) for key(s): [country]": + throw Error.InvalidCountryCode + case "Invalid value(s) for key(s): [language]": + throw Error.InvalidLanguageCode + default: + throw Error.UnknownError + } + } + // Parse API Results + var albums = [iTunesId: Album]() + if let results = parsedData["results"] as? [[String: AnyObject]] { + for result in results { + let convertedResult = result.filter({ (key, value) -> Bool in Field(rawValue: key) != nil }).map({ (Field(rawValue: $0)!, $1) + }) + let albumId = convertedResult[.CollectionId] as! iTunesId + if albums[albumId] == nil { + albums[albumId] = Album(data: convertedResult) + } + if isTrack(convertedResult) { + albums[albumId]?.addTrack(Track(data: convertedResult)) + } + } + } + return Array(albums.values) + } + + private static func isTrack(data: [Field: AnyObject]) -> Bool { + return data[.WrapperType] as! String == "track" && data[.Kind] as! String == "song" + } + + /// Creates an URL that searches the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) + /// for albums matching a specific `term`. + /// + /// This function respects the user's preferences (See `Preferences` class). + /// + /// - returns: The query URL or `nil` if `term` is invalid. + public static func createAlbumSearchURLForTerm(term: String) -> NSURL? { + var searchTerm = term.stringByReplacingOccurrencesOfString(" ", withString: "+") + searchTerm = CFURLCreateStringByAddingPercentEscapes(nil, searchTerm, nil, nil, CFStringBuiltInEncodings.UTF8.rawValue) as String + if searchTerm.isEmpty { + return nil + } + return NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)&media=music&entity=album&limit=10&country=de&lang=de_DE") + } + + /// Creates an URL that looks up all tracks that belong to the album with the + /// specified `id` in the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). + /// + /// This function respects the user's preferences (See `Preferences` class). + public static func createAlbumLookupURLForId(id: iTunesId) -> NSURL { + return NSURL(string: "http://itunes.apple.com/lookup?id=\(id)&entity=song&country=de&lang=de_DE&limit=200")! + } +} + +/// Defines a type that can be parsed from a result of the +/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). +public protocol iTunesType { + + /// Initializes the receiver with the specified data. The receiver may use + /// all or only some of the values. + /// + /// This method requires `data` to contain the expected formats. If data + /// contains no data or data in an invalid format for an expected key, this + /// method raises an exception. To check wether a specified dictionary is + /// valid use `canInitializeFromData`. + init(data: [iTunesAPI.Field: AnyObject]) + + /// Returns all fields that are required to initialize an instance of the + /// receiving type. + static var requiredFields: [iTunesAPI.Field] { get } + +} + +extension iTunesType { + + /// Returns wether the specified `data` can be used to initialize a instance + /// of the receiving type. + public static func canInitializeFromData(data: [iTunesAPI.Field: AnyObject]) -> Bool { + for field in requiredFields { + if data[field] == nil { + return false + } + } + return true + } + +}