Archived
1

Version 1.0 of TagTunes.

This commit is contained in:
Kim Wittenburg
2015-09-03 00:22:33 +02:00
parent 3ab655b6c1
commit 9966827f9c
55 changed files with 5235 additions and 65 deletions

View File

@@ -7,12 +7,23 @@
objects = {
/* Begin PBXBuildFile section */
3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */; };
3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */; };
3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */; };
3B489DC31B90B116002B7EB3 /* Artwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC01B90B116002B7EB3 /* Artwork.swift */; };
3B489DC41B90B116002B7EB3 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC11B90B116002B7EB3 /* Album.swift */; };
3B489DC51B90B116002B7EB3 /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC21B90B116002B7EB3 /* Track.swift */; };
3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC61B90B38C002B7EB3 /* iTunes.swift */; };
3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DCA1B90B3E3002B7EB3 /* iTunes.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */; };
3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7721B909B280025D550 /* AppDelegate.swift */; };
3B76C7751B909B280025D550 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7741B909B280025D550 /* ViewController.swift */; };
3B76C7751B909B280025D550 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7741B909B280025D550 /* MainViewController.swift */; };
3B76C7771B909B280025D550 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7761B909B280025D550 /* Assets.xcassets */; };
3B76C77A1B909B280025D550 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7781B909B280025D550 /* Main.storyboard */; };
3B76C7851B909B280025D550 /* TagTunesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7841B909B280025D550 /* TagTunesTests.swift */; };
3B76C7901B909B280025D550 /* TagTunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C78F1B909B280025D550 /* TagTunesUITests.swift */; };
3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */; };
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -33,9 +44,20 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = "<group>"; };
3B285DBE1B912AB700F0A2F1 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TrackTableCellView.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
3B489DC01B90B116002B7EB3 /* Artwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artwork.swift; sourceTree = "<group>"; };
3B489DC11B90B116002B7EB3 /* Album.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
3B489DC21B90B116002B7EB3 /* Track.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Track.swift; sourceTree = "<group>"; };
3B489DC61B90B38C002B7EB3 /* iTunes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iTunes.swift; sourceTree = "<group>"; };
3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TagTunes-Bridging-Header.h"; sourceTree = "<group>"; };
3B489DC91B90B3E3002B7EB3 /* iTunes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTunes.h; sourceTree = "<group>"; };
3B489DCA1B90B3E3002B7EB3 /* iTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iTunes.m; sourceTree = "<group>"; };
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AlbumTableCellView.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
3B76C76F1B909B280025D550 /* TagTunes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TagTunes.app; sourceTree = BUILT_PRODUCTS_DIR; };
3B76C7721B909B280025D550 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3B76C7741B909B280025D550 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
3B76C7741B909B280025D550 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
3B76C7761B909B280025D550 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3B76C7791B909B280025D550 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
3B76C77B1B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -45,6 +67,8 @@
3B76C78B1B909B280025D550 /* TagTunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagTunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B76C78F1B909B280025D550 /* TagTunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTunesUITests.swift; sourceTree = "<group>"; };
3B76C7911B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = "<group>"; };
3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKitPlus.framework; path = "../../../../Library/Developer/Xcode/DerivedData/TagTunes-ahlftzbggvvcneeglkkowfbohpzh/Build/Products/Debug/AppKitPlus.framework"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -52,6 +76,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -72,12 +97,22 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3B285DBD1B9128FB00F0A2F1 /* Ultil */ = {
isa = PBXGroup;
children = (
3B489DC91B90B3E3002B7EB3 /* iTunes.h */,
3B489DCA1B90B3E3002B7EB3 /* iTunes.m */,
);
name = Ultil;
sourceTree = "<group>";
};
3B76C7661B909B280025D550 = {
isa = PBXGroup;
children = (
3B76C7711B909B280025D550 /* TagTunes */,
3B76C7831B909B280025D550 /* TagTunesTests */,
3B76C78E1B909B280025D550 /* TagTunesUITests */,
3BBF710C1B95E02E00BB1EDB /* Frameworks */,
3B76C7701B909B280025D550 /* Products */,
);
sourceTree = "<group>";
@@ -95,11 +130,12 @@
3B76C7711B909B280025D550 /* TagTunes */ = {
isa = PBXGroup;
children = (
3B76C7721B909B280025D550 /* AppDelegate.swift */,
3B76C7741B909B280025D550 /* ViewController.swift */,
3B76C7761B909B280025D550 /* Assets.xcassets */,
3B76C7781B909B280025D550 /* Main.storyboard */,
3B285DBD1B9128FB00F0A2F1 /* Ultil */,
3B76C79D1B909B8C0025D550 /* Model */,
3B76C79F1B909B960025D550 /* View */,
3B76C79E1B909B910025D550 /* Controller */,
3B76C77B1B909B280025D550 /* Info.plist */,
3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */,
);
path = TagTunes;
sourceTree = "<group>";
@@ -122,6 +158,48 @@
path = TagTunesUITests;
sourceTree = "<group>";
};
3B76C79D1B909B8C0025D550 /* Model */ = {
isa = PBXGroup;
children = (
3B489DC11B90B116002B7EB3 /* Album.swift */,
3B489DC01B90B116002B7EB3 /* Artwork.swift */,
3B489DC61B90B38C002B7EB3 /* iTunes.swift */,
3B285DBE1B912AB700F0A2F1 /* Preferences.swift */,
3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */,
3B489DC21B90B116002B7EB3 /* Track.swift */,
);
name = Model;
sourceTree = "<group>";
};
3B76C79E1B909B910025D550 /* Controller */ = {
isa = PBXGroup;
children = (
3B76C7721B909B280025D550 /* AppDelegate.swift */,
3B76C7741B909B280025D550 /* MainViewController.swift */,
3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */,
);
name = Controller;
sourceTree = "<group>";
};
3B76C79F1B909B960025D550 /* View */ = {
isa = PBXGroup;
children = (
3B76C7781B909B280025D550 /* Main.storyboard */,
3B76C7761B909B280025D550 /* Assets.xcassets */,
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */,
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */,
);
name = View;
sourceTree = "<group>";
};
3BBF710C1B95E02E00BB1EDB /* Frameworks */ = {
isa = PBXGroup;
children = (
3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -184,6 +262,7 @@
3B76C7671B909B280025D550 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0700;
ORGANIZATIONNAME = "Kim Wittenburg";
TargetAttributes = {
@@ -251,8 +330,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3B76C7751B909B280025D550 /* ViewController.swift in Sources */,
3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */,
3B489DC31B90B116002B7EB3 /* Artwork.swift in Sources */,
3B76C7751B909B280025D550 /* MainViewController.swift in Sources */,
3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */,
3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */,
3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */,
3B489DC41B90B116002B7EB3 /* Album.swift in Sources */,
3B489DC51B90B116002B7EB3 /* Track.swift in Sources */,
3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */,
3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */,
3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */,
3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -383,11 +472,14 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = TagTunes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
@@ -395,11 +487,13 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = TagTunes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h";
};
name = Release;
};
@@ -474,6 +568,7 @@
3B76C7961B909B280025D550 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3B76C7971B909B280025D550 /* Build configuration list for PBXNativeTarget "TagTunesTests" */ = {
isa = XCConfigurationList;
@@ -482,6 +577,7 @@
3B76C7991B909B280025D550 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3B76C79A1B909B280025D550 /* Build configuration list for PBXNativeTarget "TagTunesUITests" */ = {
isa = XCConfigurationList;
@@ -490,6 +586,7 @@
3B76C79C1B909B280025D550 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};

147
TagTunes/Album.swift Normal file
View 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
}

View 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
}
}

View File

@@ -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
View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

View File

@@ -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"
}
],

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PauseProgressFreestandingTemplate.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,29 @@
%PDF-1.4
%âãÏÓ
1 0 obj <</Filter/FlateDecode/Length 236>>stream
xœÍ1N1 E{ŸÂ'ˆl''W@¢ ¢@Û­`„fb¹>ÎLⶃ
EJüœïoGÊ®@È$ /.°‚ë¶À ÏðŒ_ ø`Âw»ÅGx9ž<>Ç7¢¾ÿðúSI[Ÿod@q<>Y%äTC­ˆñR<>Ëf<C38B>™CÕ-¾<>J 5oTÔâ´WYÒÑM;övû}$
”G9g£i·n”S}úP#Ñgör;ˆØÝW¤C÷¤p3<70>45ÁRÔÖ¡ó2Xµ´ñ—¡¿g×Ï<C397>K5q{+:»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

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,11 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"data" : [
{
"idiom" : "universal"
}
]
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

View File

@@ -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>

View 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 {
}

View File

@@ -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>

View 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
}
}

View 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")
}
}
}

View 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
}
}
}
}

View 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
}

View 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
View 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
}

View 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
}
}
}

View File

@@ -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
View 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 players 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 CDs 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 tracks 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

File diff suppressed because it is too large Load Diff

177
TagTunes/iTunes.swift Normal file
View 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
}
}