Version 1.0 of TagTunes.
147
TagTunes/Album.swift
Normal file
@@ -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
|
||||
}
|
||||
177
TagTunes/AlbumTableCellView.swift
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
106
TagTunes/Artwork.swift
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/128x128.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/16x16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/16x16@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/256x256.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/256x256@2x.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/32x32@2x.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/512x512.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
TagTunes/Assets.xcassets/AppIcon.appiconset/512x512@2x.png
Normal file
|
After Width: | Height: | Size: 694 KiB |
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
6
TagTunes/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
TagTunes/Assets.xcassets/Cross.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png
vendored
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png
vendored
Normal file
|
After Width: | Height: | Size: 55 KiB |
15
TagTunes/Assets.xcassets/PauseProgressFreestandingTemplate.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PauseProgressFreestandingTemplate.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 236>>stream
|
||||
xœÍ’1N1E{ŸÂ'ˆl''W@¢ ¢@Û`„f‘–b¹>ÎLⶃ
|
||||
EJüœïoGÊ®@È$ /.°‚ë¶ÀÏðŒ_ ø`Âw»ÅGx9ž<>Ç7¢¾ÿðúSI[Ÿod–@q<>Y%äTCXÔˆñR<>Ëf<C38B>™CÕ-¾<>J5oTÔâ´WYÒÑM;övû}$
|
||||
”G9g£i·n”S}úP#Ñgör;ˆØÝW¤C÷¤p3<70>45ÁRÔÖ¡ó2Xµ´ñ—¡¿g×Ï<C397>KuÚ5q{‚+:»C×ßóÁñž~õ«þ“ºo«P˜K
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150902100931Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150902100931Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000458 00000 n
|
||||
0000000318 00000 n
|
||||
0000000508 00000 n
|
||||
0000000552 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<ae74877cb59949374c065f7f1deb2d92><ae74877cb59949374c065f7f1deb2d92>]/Root 4 0 R/Size 6>>
|
||||
startxref
|
||||
757
|
||||
%%EOF
|
||||
23
TagTunes/Assets.xcassets/PreferenceTags.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-1.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags-2.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
TagTunes/Assets.xcassets/PreferenceTags.imageset/PreferencesTags.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
23
TagTunes/Assets.xcassets/PreferencesTags.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-1.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags-2.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
TagTunes/Assets.xcassets/PreferencesTags.imageset/PreferencesTags.png
vendored
Normal file
|
After Width: | Height: | Size: 558 KiB |
23
TagTunes/Assets.xcassets/SaveToITunes.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-1.png
vendored
Normal file
|
After Width: | Height: | Size: 781 KiB |
BIN
TagTunes/Assets.xcassets/SaveToITunes.imageset/SaveToITunes-2.png
vendored
Normal file
|
After Width: | Height: | Size: 781 KiB |
23
TagTunes/Assets.xcassets/TickBW.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/TickBW.imageset/TickBW-1.png
vendored
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
TagTunes/Assets.xcassets/TickBW.imageset/TickBW-2.png
vendored
Normal file
|
After Width: | Height: | Size: 41 KiB |
11
TagTunes/Assets.xcassets/iTunes.dataset/Contents.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"data" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
TagTunes/Assets.xcassets/iTunes.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/iTunes.imageset/iTunes-1.png
vendored
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Assets.xcassets/iTunes.imageset/iTunes-2.png
vendored
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png
vendored
Normal file
|
After Width: | Height: | Size: 825 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="6198" systemVersion="14A297b" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="8173.3" systemVersion="14F27" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6198"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8173.3"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@@ -21,7 +21,11 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW">
|
||||
<connections>
|
||||
<segue destination="d4o-tN-8wc" kind="show" id="Sol-gW-DTe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
@@ -155,6 +159,9 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
CA
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
@@ -641,20 +648,47 @@
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="TagTunes" customModuleProvider="target"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
<point key="canvasLocation" x="75" y="-81"/>
|
||||
</scene>
|
||||
<!--Window Controller - Window-->
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<window key="window" title="TagTunes" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<toolbar key="toolbar" implicitIdentifier="77DCE6FB-61CD-40EE-B6F9-02081D2DE8BE" autosavesConfiguration="NO" displayMode="iconAndLabel" sizeMode="regular" id="jCk-5k-5x7">
|
||||
<allowedToolbarItems>
|
||||
<toolbarItem implicitItemIdentifier="694E800B-64CF-416A-A82D-2E13BB23AEC4" label="Add Selection" paletteLabel="Add iTunes Selection" tag="-1" image="iTunes" id="vfK-go-3oR">
|
||||
<connections>
|
||||
<action selector="addITunesSelection:" target="Oky-zY-oP4" id="G9h-yp-J9e"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="573468FC-979E-4370-A98C-49CB9C00BE09" label="Save" paletteLabel="Save to iTunes" tag="-1" image="SaveToITunes" id="ax1-DR-t4v">
|
||||
<connections>
|
||||
<action selector="performSave:" target="Oky-zY-oP4" id="9BZ-UY-ffJ"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="85CB266F-AF83-4345-A711-06F6A276F0AA" label="Remove" paletteLabel="Remove" tag="-1" image="Cross" id="RU6-wj-p8A">
|
||||
<connections>
|
||||
<action selector="delete:" target="Oky-zY-oP4" id="SyO-WS-R4p"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="Bqq-8n-9JT"/>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="uaV-9O-6iw"/>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="vfK-go-3oR"/>
|
||||
<toolbarItem reference="Bqq-8n-9JT"/>
|
||||
<toolbarItem reference="ax1-DR-t4v"/>
|
||||
<toolbarItem reference="RU6-wj-p8A"/>
|
||||
</defaultToolbarItems>
|
||||
</toolbar>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
@@ -664,18 +698,233 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="dU3-ef-E5X">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="d4o-tN-8wc" sceneMemberID="viewController">
|
||||
<window key="window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="7rU-ut-IAI">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="294" y="362" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
|
||||
<connections>
|
||||
<binding destination="d4o-tN-8wc" name="title" keyPath="window.contentViewController.title" id="Iko-NW-ghs"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="qfM-ma-lyn" kind="relationship" relationship="window.shadowedContentViewController" id="gBt-Dq-ctT"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="qtI-W6-JfI" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-767" y="250"/>
|
||||
</scene>
|
||||
<!--Preferences View Controller-->
|
||||
<scene sceneID="9ZM-fo-zrI">
|
||||
<objects>
|
||||
<tabViewController tabStyle="toolbar" id="qfM-ma-lyn" customClass="PreferencesViewController" customModule="AppKitPlus" sceneMemberID="viewController">
|
||||
<tabViewItems>
|
||||
<tabViewItem image="NSPreferencesGeneral" id="YSn-2h-2B5"/>
|
||||
</tabViewItems>
|
||||
<viewControllerTransitionOptions key="transitionOptions" allowUserInteraction="YES"/>
|
||||
<tabView key="tabView" type="noTabsNoBorder" id="UTE-Sb-ieY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<font key="font" metaFont="message"/>
|
||||
<tabViewItems/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="qfM-ma-lyn" id="IHd-U1-spO"/>
|
||||
</connections>
|
||||
</tabView>
|
||||
<connections>
|
||||
<segue destination="tzd-4a-CRb" kind="relationship" relationship="tabItems" id="8pq-ZH-tzf"/>
|
||||
</connections>
|
||||
</tabViewController>
|
||||
<customObject id="5zh-wl-nwU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-767" y="687"/>
|
||||
</scene>
|
||||
<!--General-->
|
||||
<scene sceneID="RQU-Wx-xKw">
|
||||
<objects>
|
||||
<viewController title="General" id="tzd-4a-CRb" customClass="GeneralPreferencesViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="pOc-Vr-IET" customClass="PreferenceView" customModule="AppKitPlus">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="kl3-n8-T9b">
|
||||
<rect key="frame" x="18" y="92" width="105" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Save Artwork" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="q2l-4t-mL0">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="saveArtworkStateChanged:" target="tzd-4a-CRb" id="qQu-K2-wWk"/>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="saveArtwork" id="1An-Cl-B1c"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1fj-p7-sMs">
|
||||
<rect key="frame" x="335" y="58" width="101" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="89" id="WSP-Sc-NcB"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Choose…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="A5S-ps-EYW">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="chooseArtworkPath:" target="tzd-4a-CRb" id="v67-Ay-ckG"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="MSr-08-ucR">
|
||||
<rect key="frame" x="18" y="40" width="151" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Keep Search Results" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="uED-ee-Oc7">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="keepSearchResults" id="Nwn-Ik-Zdy"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QNf-Uy-bMT">
|
||||
<rect key="frame" x="30" y="20" width="402" height="14"/>
|
||||
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="If checked the search results are not removed if a result is added." id="Lnf-PQ-PX4">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<pathControl verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pT8-NA-x3W">
|
||||
<rect key="frame" x="20" y="64" width="313" height="22"/>
|
||||
<pathCell key="cell" selectable="YES" alignment="left" id="Sgq-Mk-WnH">
|
||||
<font key="font" metaFont="system"/>
|
||||
<url key="url" string="file:///Applications/"/>
|
||||
</pathCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="artworkTarget" id="wEo-ga-KAB"/>
|
||||
</connections>
|
||||
</pathControl>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="top" secondItem="MSr-08-ucR" secondAttribute="bottom" constant="8" symbolic="YES" id="3Vk-P7-72n"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="leading" secondItem="pOc-Vr-IET" secondAttribute="leading" constant="20" symbolic="YES" id="5VS-q6-Nyw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" constant="20" symbolic="YES" id="Np8-sr-aF0"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="baseline" secondItem="pT8-NA-x3W" secondAttribute="baseline" id="c2l-d6-00D"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="leading" id="dAH-m2-853"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="dCN-mx-bY1"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="trailing" constant="8" symbolic="YES" id="ejc-iL-QON"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="top" secondItem="pOc-Vr-IET" secondAttribute="top" constant="20" symbolic="YES" id="h13-nP-neh"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="top" secondItem="kl3-n8-T9b" secondAttribute="bottom" constant="8" symbolic="YES" id="qdu-oL-zyh"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="top" secondItem="pT8-NA-x3W" secondAttribute="bottom" constant="8" symbolic="YES" id="sSO-jr-zo1"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" id="tri-hX-smF"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="leading" secondItem="MSr-08-ucR" secondAttribute="leading" constant="12" id="tyS-Y5-FOc"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="width">
|
||||
<real key="value" value="450"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="height">
|
||||
<real key="value" value="128"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="artworkPathControl" destination="pT8-NA-x3W" id="qQf-u8-VJb"/>
|
||||
<outlet property="chooseArtworkButton" destination="1fj-p7-sMs" id="pA6-5i-JZI"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="RtZ-4O-Pgk" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="aSx-iH-PLA" customClass="Preferences" customModule="TagTunes" customModuleProvider="target"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1031" y="1051"/>
|
||||
</scene>
|
||||
<!--Main View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="XfG-lQ-9wD" customClass="MainViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="692" height="390"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<searchField wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JF0-Xb-DqY">
|
||||
<rect key="frame" x="20" y="348" width="652" height="22"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsWholeSearchString="YES" id="iQi-K0-yFr">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</searchFieldCell>
|
||||
<connections>
|
||||
<action selector="performSearch:" target="XfG-lQ-9wD" id="zNC-V3-DNP"/>
|
||||
</connections>
|
||||
</searchField>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="33" horizontalPageScroll="10" verticalLineScroll="33" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zXR-px-hjg">
|
||||
<rect key="frame" x="20" y="20" width="652" height="320"/>
|
||||
<clipView key="contentView" id="CyK-XI-Ggy">
|
||||
<rect key="frame" x="1" y="1" width="650" height="318"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" autosaveColumns="NO" rowHeight="31" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="16" outlineTableColumn="p3C-E5-sdk" id="1Vy-Gq-TWU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="650" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn width="647" minWidth="40" maxWidth="10000" id="p3C-E5-sdk">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
</tableHeaderCell>
|
||||
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="D7Q-Pc-p9W">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="XfG-lQ-9wD" id="z99-eh-ktB"/>
|
||||
<outlet property="delegate" destination="XfG-lQ-9wD" id="a5p-EA-EBG"/>
|
||||
</connections>
|
||||
</outlineView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="vEh-oN-xXy">
|
||||
<rect key="frame" x="1" y="303" width="650" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="iqM-dh-v73">
|
||||
<rect key="frame" x="224" y="17" width="15" height="102"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="zXR-px-hjg" firstAttribute="leading" secondItem="JF0-Xb-DqY" secondAttribute="leading" id="7Ue-RX-gnl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="JF0-Xb-DqY" secondAttribute="trailing" constant="20" symbolic="YES" id="KHs-FT-bYo"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zXR-px-hjg" secondAttribute="bottom" constant="20" symbolic="YES" id="QjD-YK-8tD"/>
|
||||
<constraint firstItem="JF0-Xb-DqY" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="20" symbolic="YES" id="aNE-yT-Pj9"/>
|
||||
<constraint firstItem="JF0-Xb-DqY" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" symbolic="YES" id="l3D-mn-F0D"/>
|
||||
<constraint firstItem="zXR-px-hjg" firstAttribute="top" secondItem="JF0-Xb-DqY" secondAttribute="bottom" constant="8" symbolic="YES" id="pKp-Ad-Sr5"/>
|
||||
<constraint firstItem="zXR-px-hjg" firstAttribute="trailing" secondItem="JF0-Xb-DqY" secondAttribute="trailing" id="r36-CQ-K22"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="outlineView" destination="1Vy-Gq-TWU" id="vRG-b2-ocW"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
<point key="canvasLocation" x="75" y="715"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Cross" width="245.75999450683594" height="245.75999450683594"/>
|
||||
<image name="NSPreferencesGeneral" width="32" height="32"/>
|
||||
<image name="SaveToITunes" width="512" height="512"/>
|
||||
<image name="iTunes" width="512" height="512"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
13
TagTunes/Error Handler.swift
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -21,7 +21,9 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>4242</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.music</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
754
TagTunes/MainViewController.swift
Normal file
@@ -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<iTunesTrack> {
|
||||
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<Album>()
|
||||
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<iTunesTrack>()
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
69
TagTunes/Preferences.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
TagTunes/PreferencesTabViewController.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
65
TagTunes/SearchResult.swift
Normal file
@@ -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
|
||||
}
|
||||
5
TagTunes/TagTunes-Bridging-Header.h
Normal file
@@ -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"
|
||||
129
TagTunes/Track.swift
Normal file
@@ -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
|
||||
}
|
||||
127
TagTunes/TrackTableCellView.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
522
TagTunes/iTunes.h
Normal file
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* iTunes.h
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
|
||||
@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
|
||||
|
||||
2306
TagTunes/iTunes.m
Normal file
177
TagTunes/iTunes.swift
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||