├── .gitmodules ├── src ├── Music Library Exporter │ ├── Supporting Files │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── icon_16x16.png │ │ │ │ ├── icon_32x32.png │ │ │ │ ├── icon_128x128.png │ │ │ │ ├── icon_16x16@2x.png │ │ │ │ ├── icon_256x256.png │ │ │ │ ├── icon_32x32@2x.png │ │ │ │ ├── icon_512x512.png │ │ │ │ ├── icon_128x128@2x.png │ │ │ │ ├── icon_256x256@2x.png │ │ │ │ ├── icon_512x512@2x.png │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ ├── Music_Library_Exporter.entitlements │ │ ├── Info.plist │ │ └── Credits.rtf │ ├── main.m │ ├── PlaylistsView │ │ ├── CheckBoxTableCellView.m │ │ ├── PopupButtonTableCellView.m │ │ ├── CheckBoxTableCellView.h │ │ ├── PopupButtonTableCellView.h │ │ └── PlaylistsViewController.h │ ├── PreferencesWindow │ │ ├── PreferencesWindowController.h │ │ ├── PreferencesWindowController.m │ │ └── PreferencesWindow.xib │ ├── AppDelegate.h │ ├── ConfigurationView │ │ ├── HourNumberFormatter.h │ │ ├── HourNumberFormatter.m │ │ └── ConfigurationViewController.h │ ├── HelperAppManager.h │ └── HelperAppManager.m ├── Common │ ├── SwiftCompatFix │ │ ├── Bridging-Header.h │ │ └── Empty.swift │ ├── Filter │ │ ├── MediaItem │ │ │ ├── MediaItemFiltering.h │ │ │ ├── MediaItemKindFilter.h │ │ │ ├── MediaItemFilterGroup.h │ │ │ ├── MediaItemKindFilter.m │ │ │ └── MediaItemFilterGroup.m │ │ └── Playlist │ │ │ ├── PlaylistFiltering.h │ │ │ ├── PlaylistMasterFilter.h │ │ │ ├── PlaylistMasterFilter.m │ │ │ ├── PlaylistIDFilter.h │ │ │ ├── PlaylistParentIDFilter.h │ │ │ ├── PlaylistKindFilter.h │ │ │ ├── PlaylistDistinguishedKindFilter.h │ │ │ ├── PlaylistFilterGroup.h │ │ │ ├── PlaylistIDFilter.m │ │ │ ├── PlaylistParentIDFilter.m │ │ │ ├── PlaylistKindFilter.m │ │ │ ├── PlaylistDistinguishedKindFilter.m │ │ │ └── PlaylistFilterGroup.m │ ├── Serializer │ │ ├── MediaItemSerializerDelegate.h │ │ ├── MediaEntityRepository.h │ │ ├── PathMapper.h │ │ ├── PlaylistSerializerDelegate.h │ │ ├── LibrarySerializer.h │ │ ├── MediaItemSerializer.h │ │ ├── MediaEntityRepository.m │ │ ├── PlaylistSerializer.h │ │ ├── PathMapper.m │ │ ├── LibrarySerializer.m │ │ └── PlaylistSerializer.m │ ├── Utils.h │ ├── Defines.m │ ├── Export │ │ ├── ExportManagerDelegate.h │ │ └── ExportManager.h │ ├── Sorter │ │ ├── MediaItemSorter.h │ │ ├── SorterDefines.h │ │ ├── MediaItemSorter.m │ │ └── SorterDefines.m │ ├── PlaylistTree │ │ ├── PlaylistTreeGenerator.h │ │ ├── PlaylistTreeNode.h │ │ ├── PlaylistTreeNode.m │ │ └── PlaylistTreeGenerator.m │ ├── Configuration │ │ ├── DirectoryBookmarkHandler.h │ │ ├── ScheduleConfiguration.h │ │ ├── UserDefaultsExportConfiguration.h │ │ ├── ExportConfiguration.h │ │ ├── DirectoryBookmarkHandler.m │ │ ├── ScheduleConfiguration.m │ │ └── UserDefaultsExportConfiguration.m │ ├── SentryHandler.h │ ├── Utils.m │ ├── Logger.h │ ├── Defines.h │ └── SentryHandler.m ├── Config │ ├── Common │ │ ├── Version.xcconfig │ │ ├── Sentry.base.xcconfig │ │ ├── Swift.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Signing.xcconfig │ │ └── Base.xcconfig │ └── Schemes │ │ ├── music-library-exporter.xcconfig │ │ ├── Music Library Exporter.xcconfig │ │ └── Music Library Exporter Helper.xcconfig ├── Music Library Exporter.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ ├── Music Library Exporter Helper.xcscheme │ │ ├── Music Library Exporter.xcscheme │ │ └── music-library-exporter.xcscheme ├── Music Library Exporter Helper │ ├── HelperAppDelegate.h │ ├── main.m │ ├── Supporting Files │ │ ├── Music_Library_Exporter_Helper.entitlements │ │ └── Info.plist │ ├── DirectoryPermissionsWindow │ │ ├── DirectoryPermissionsWindowController.h │ │ ├── DirectoryPermissionsWindowController.m │ │ └── DirectoryPermissionsWindow.xib │ ├── ExportScheduler.h │ └── HelperAppDelegate.m ├── music-library-exporter │ ├── CLIManager.h │ ├── CLIDefines.h │ ├── main.m │ ├── ArgParser.h │ └── CLIDefines.m └── 3rd │ └── OrderedDictionary.h ├── Examples └── local.music-library-exporter.plist ├── .gitignore └── scripts ├── mle-increment-build-number └── mle-sentry-upload /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ArgumentParser"] 2 | path = src/3rd/ArgumentParser 3 | url = https://github.com/mysteriouspants/ArgumentParser.git 4 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Common/SwiftCompatFix/Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // This file is a workaround for `Could not find or use auto-linked library 'swiftCompatibility..': library 'swiftCompatibility..' not found` build failures. 2 | -------------------------------------------------------------------------------- /src/Config/Common/Version.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Version.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | VERSION_BUILD=42 9 | CURRENT_PROJECT_VERSION=1.2.2 10 | -------------------------------------------------------------------------------- /src/Config/Common/Sentry.base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Sentry.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-13. 6 | // 7 | 8 | SENTRY_MAIN_DSN = '@""' 9 | SENTRY_HELPER_DSN = '@""' 10 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylekingcdn/music-library-exporter/HEAD/src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Config/Common/Swift.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Swift.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2025-07-13. 6 | // 7 | 8 | SWIFT_VERSION = 6.0 9 | CLANG_ENABLE_MODULES = YES 10 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 11 | SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone 12 | -------------------------------------------------------------------------------- /src/Common/SwiftCompatFix/Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Empty.swift 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2025-07-13. 6 | // 7 | 8 | import Foundation 9 | 10 | // This file is a workaround for `Could not find or use auto-linked library 'swiftCompatibility..': library 'swiftCompatibility..' not found` build failures. 11 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Music Library Exporter/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-25. 6 | // 7 | 8 | #import 9 | 10 | int main(int argc, const char * argv[]) { 11 | @autoreleasepool { 12 | // Setup code that might create autoreleased objects goes here. 13 | } 14 | return NSApplicationMain(argc, argv); 15 | } 16 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/HelperAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // HelperAppDelegate.h 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-01-26. 6 | // 7 | 8 | #import 9 | 10 | @interface HelperAppDelegate : NSObject 11 | 12 | 13 | #pragma mark - Initializers 14 | 15 | - (instancetype)init; 16 | 17 | 18 | @end 19 | 20 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PlaylistsView/CheckBoxTableCellView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CheckBoxTableCellView.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-10. 6 | // 7 | 8 | #import "CheckBoxTableCellView.h" 9 | 10 | @implementation CheckBoxTableCellView 11 | 12 | - (void)drawRect:(NSRect)dirtyRect { 13 | 14 | [super drawRect:dirtyRect]; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-01-26. 6 | // 7 | 8 | #import 9 | 10 | int main(int argc, const char * argv[]) { 11 | @autoreleasepool { 12 | // Setup code that might create autoreleased objects goes here. 13 | } 14 | return NSApplicationMain(argc, argv); 15 | } 16 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PlaylistsView/PopupButtonTableCellView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PopupButtonTableCellView.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-10. 6 | // 7 | 8 | #import "PopupButtonTableCellView.h" 9 | 10 | @implementation PopupButtonTableCellView 11 | 12 | - (void)drawRect:(NSRect)dirtyRect { 13 | 14 | [super drawRect:dirtyRect]; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PlaylistsView/CheckBoxTableCellView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CheckBoxTableCellView.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-10. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface CheckBoxTableCellView : NSTableCellView 13 | 14 | @property (nullable, assign) IBOutlet NSButton* checkbox; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /src/Common/Filter/MediaItem/MediaItemFiltering.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemFiltering.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibMediaItem; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol MediaItemFiltering 15 | 16 | - (BOOL)filterPassesForItem:(ITLibMediaItem*)item; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistFiltering.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistFiltering.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibPlaylist; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol PlaylistFiltering 15 | 16 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PlaylistsView/PopupButtonTableCellView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PopupButtonTableCellView.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-10. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PopupButtonTableCellView : NSTableCellView 13 | 14 | @property (nullable, assign) IBOutlet NSPopUpButton* button; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /src/Common/Serializer/MediaItemSerializerDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportManagerDelegate.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @protocol MediaItemSerializerDelegate 13 | @optional 14 | 15 | - (void)serializedItems:(NSUInteger)serialized ofTotal:(NSUInteger)total; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /src/Common/Serializer/MediaEntityRepository.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaEntityRepository.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibMediaEntity; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface MediaEntityRepository : NSObject 15 | 16 | - (instancetype)init; 17 | 18 | - (nullable NSNumber*)getIDForEntity:(ITLibMediaEntity*)entity; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /src/Common/Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-18. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface Utils : NSObject 15 | 16 | + (nullable NSString*)hexStringForPersistentId:(nullable NSNumber*)persistentId; 17 | 18 | + (PlaylistSortOrderType)playlistSortOrderForTitle:(nullable NSString*)title; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistMasterFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistMasterFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | #import "PlaylistFiltering.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface PlaylistMasterFilter : NSObject 15 | 16 | - (instancetype)init; 17 | 18 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /src/Common/Defines.m: -------------------------------------------------------------------------------- 1 | // 2 | // Defines.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import "Defines.h" 9 | 10 | 11 | @implementation Defines 12 | 13 | 14 | NSString* const __MLE__AppGroupIdentifier = @"group.9YLM7HTV6V.com.MusicLibraryExporter"; 15 | 16 | NSString* const __MLE__AppBundleIdentifier = @"com.kylekingcdn.MusicLibraryExporter"; 17 | NSString* const __MLE__HelperBundleIdentifier = @"com.kylekingcdn.MusicLibraryExporter.MusicLibraryExporterHelper"; 18 | 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /src/Common/Serializer/PathMapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // PathMapper.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PathMapper : NSObject 13 | 14 | @property (copy,nullable) NSString* searchString; 15 | @property (copy,nullable) NSString* replaceString; 16 | 17 | @property BOOL addLocalhostPrefix; 18 | 19 | - (instancetype)init; 20 | - (NSString*)mapPath:(NSURL*)path; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "7b5e54f81ac1ebaa640945691cb38c371b637198701f04fba811702fc8e7067e", 3 | "pins" : [ 4 | { 5 | "identity" : "sentry-cocoa", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/getsentry/sentry-cocoa.git", 8 | "state" : { 9 | "revision" : "ca92efeb24b10052cd2a79e5205f42c5a16770ec", 10 | "version" : "8.53.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /src/Common/Serializer/PlaylistSerializerDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportManagerDelegate.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibPlaylist; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol PlaylistSerializerDelegate 15 | @optional 16 | 17 | - (void)serializedPlaylists:(NSUInteger)serialized ofTotal:(NSUInteger)total; 18 | 19 | - (void)excludedPlaylist:(ITLibPlaylist*)playlist; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PreferencesWindow/PreferencesWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowController.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-24. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PreferencesWindowController : NSWindowController 13 | 14 | 15 | #pragma mark - Initializers 16 | 17 | - (instancetype)init; 18 | 19 | 20 | #pragma mark - Mutators 21 | 22 | - (IBAction)setCrashReportingEnabled:(id)sender; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistMasterFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistMasterFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistMasterFilter.h" 9 | 10 | #import 11 | 12 | @implementation PlaylistMasterFilter 13 | 14 | - (instancetype)init { 15 | 16 | if (self = [super init]) { 17 | 18 | return self; 19 | } 20 | else { 21 | return nil; 22 | } 23 | } 24 | 25 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist { 26 | 27 | return playlist.master == NO; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Music_Library_Exporter.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.9YLM7HTV6V.com.MusicLibraryExporter 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Music Library Exporter/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-25. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : NSObject 11 | 12 | - (IBAction)showConfigurationView:(id)sender; 13 | 14 | - (IBAction)showPlaylistsView:(id)sender; 15 | - (IBAction)hidePlaylistsView:(id)sender; 16 | 17 | - (IBAction)showPreferencesWindow:(id)sender; 18 | 19 | - (NSInteger)launchCount; 20 | - (BOOL)isFirstLaunch; 21 | - (void)incrementLaunchCount; 22 | 23 | - (void)showCrashReportingPermissionsWindow; 24 | 25 | @end 26 | 27 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/Supporting Files/Music_Library_Exporter_Helper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.9YLM7HTV6V.com.MusicLibraryExporter 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Common/Export/ExportManagerDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportManagerDelegate.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol ExportManagerDelegate 15 | @optional 16 | 17 | - (void)exportStateChangedFrom:(ExportState)oldState toState:(ExportState)newState; 18 | 19 | - (void)exportedItems:(NSUInteger)exportedItems ofTotal:(NSUInteger)totalItems; 20 | - (void)exportedPlaylists:(NSUInteger)exportedPlaylists ofTotal:(NSUInteger)totalPlaylists; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistIDFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistIDFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | #import "PlaylistFiltering.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface PlaylistIDFilter : NSObject 15 | 16 | - (instancetype)init; 17 | - (instancetype)initWithExcludedIDs:(NSSet*)excludedIDs; 18 | 19 | - (void)addExcludedID:(NSNumber*)playlistID; 20 | - (void)removeExcludedID:(NSNumber*)playlistID; 21 | 22 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /src/Music Library Exporter/ConfigurationView/HourNumberFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // HourNumberFormatter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-08. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface HourNumberFormatter : NSNumberFormatter 13 | 14 | 15 | #pragma mark - Initializers 16 | 17 | - (instancetype)init; 18 | 19 | 20 | #pragma mark - Accessors 21 | 22 | - (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString * _Nullable * _Nullable)newString errorDescription:(NSString * _Nullable * _Nullable)error; 23 | 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistParentIDFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistParentIDFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | #import "PlaylistFiltering.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface PlaylistParentIDFilter : NSObject 15 | 16 | - (instancetype)init; 17 | - (instancetype)initWithExcludedIDs:(NSSet*)excludedIDs; 18 | 19 | - (void)addExcludedID:(NSNumber*)playlistID; 20 | - (void)removeExcludedID:(NSNumber*)playlistID; 21 | 22 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /src/Common/Serializer/LibrarySerializer.h: -------------------------------------------------------------------------------- 1 | // 2 | // LibrarySerializer.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibrary; 11 | @class OrderedDictionary; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface LibrarySerializer : NSObject 16 | 17 | - (instancetype) init; 18 | 19 | @property (copy, nullable) NSString* persistentID; 20 | @property (copy, nullable) NSString* musicLibraryDir; 21 | 22 | - (OrderedDictionary*)serializeLibrary:(ITLibrary*)library withItems:(OrderedDictionary*)items andPlaylists:(NSArray*)playlists; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistKindFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistKindFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "PlaylistFiltering.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface PlaylistKindFilter : NSObject 16 | 17 | - (instancetype)init; 18 | - (instancetype)initWithKinds:(NSSet*)kinds; 19 | 20 | - (instancetype)initWithBaseKinds; 21 | 22 | - (void)addKind:(ITLibPlaylistKind)kind; 23 | - (void)removeKind:(ITLibPlaylistKind)kind; 24 | 25 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /src/Music Library Exporter/HelperAppManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // HelperAppManager.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import 9 | 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface HelperAppManager : NSObject 14 | 15 | 16 | #pragma mark - Initializers 17 | 18 | - (instancetype)init; 19 | 20 | 21 | #pragma mark - Accessors 22 | 23 | - (BOOL)isHelperRegisteredWithSystem; 24 | - (NSString*)errorForHelperRegistration:(BOOL)registerFlag; 25 | 26 | 27 | #pragma mark - Mutators 28 | 29 | - (BOOL)registerHelperWithSystem:(BOOL)flag; 30 | - (void)updateHelperRegistrationWithScheduleEnabled:(BOOL)scheduleEnabled; 31 | 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /Examples/local.music-library-exporter.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | local.music-library-exporter 7 | ProgramArguments 8 | 9 | /usr/local/bin/music-library-exporter 10 | export 11 | 12 | RunAtLoad 13 | 14 | StandardErrorPath 15 | /tmp/local.music-library-exporter.log 16 | StandardOutPath 17 | /tmp/local.music-library-exporter.log 18 | StartInterval 19 | 3600 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Common/Filter/MediaItem/MediaItemKindFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemKindFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "MediaItemFiltering.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MediaItemKindFilter : NSObject 16 | 17 | - (instancetype)init; 18 | - (instancetype)initWithKinds:(NSSet*)kinds; 19 | 20 | - (instancetype)initWithBaseKinds; 21 | 22 | - (void)addKind:(ITLibMediaItemMediaKind)kind; 23 | - (void)removeKind:(ITLibMediaItemMediaKind)kind; 24 | 25 | - (BOOL)filterPassesForItem:(ITLibMediaItem*)item; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Gcc Patch 26 | /*.gcno 27 | 28 | # Sentry DSN/keys 29 | /src/Config/Common/Sentry.xcconfig 30 | /scripts/.sentry.keys 31 | 32 | # Script logs 33 | /scripts/mle-sentry-upload.log 34 | -------------------------------------------------------------------------------- /src/Common/Sorter/MediaItemSorter.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemSorter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "Defines.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MediaItemSorter : NSObject 16 | 17 | @property (nullable, nonatomic, copy) NSString* sortProperty; 18 | @property (readonly) PlaylistSortOrderType sortOrder; 19 | 20 | #pragma mark - Initializers 21 | 22 | - (instancetype)initWithSortProperty:(nullable NSString*)sortProperty andSortOrder:(PlaylistSortOrderType)sortOrder; 23 | 24 | #pragma mark - Accessors 25 | 26 | - (NSArray*)sortItems:(NSArray*)items; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /src/Common/PlaylistTree/PlaylistTreeGenerator.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistTreeGenerator.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-13. 6 | // 7 | 8 | #import 9 | 10 | @class PlaylistTreeNode; 11 | @class PlaylistFilterGroup; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface PlaylistTreeGenerator : NSObject 16 | 17 | @property BOOL flattenFolders; 18 | @property (nullable, weak) PlaylistFilterGroup* filters; 19 | 20 | @property (nonnull, copy) NSDictionary* customSortProperties; 21 | @property (nonnull, copy) NSDictionary* customSortOrders; 22 | 23 | - (instancetype)init; 24 | - (instancetype)initWithFilters:(PlaylistFilterGroup*)filters; 25 | 26 | - (nullable PlaylistTreeNode*)generateTreeWithError:(NSError**)error; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistDistinguishedKindFilter.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistDistinguishedKindFilter.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "PlaylistFiltering.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface PlaylistDistinguishedKindFilter : NSObject 16 | 17 | - (instancetype)init; 18 | - (instancetype)initWithKinds:(NSSet*)kinds; 19 | 20 | - (instancetype)initWithBaseKinds; 21 | - (instancetype)initWithInternalKinds; 22 | 23 | - (void)addKind:(ITLibDistinguishedPlaylistKind)kind; 24 | - (void)removeKind:(ITLibDistinguishedPlaylistKind)kind; 25 | 26 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /src/Common/Filter/MediaItem/MediaItemFilterGroup.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemFilterGroup.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibMediaItem; 11 | @protocol MediaItemFiltering; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MediaItemFilterGroup : NSObject 16 | 17 | - (instancetype)init; 18 | - (instancetype)initWithFilters:(NSArray*>*)filters; 19 | - (instancetype)initWithBaseFilters; 20 | 21 | - (NSArray*>*)filters; 22 | - (void)setFilters:(NSArray*>*)filters; 23 | 24 | - (void)addFilter:(NSObject*)filter; 25 | - (void)removeFilter:(NSObject*)filter; 26 | 27 | - (BOOL)filtersPassForItem:(ITLibMediaItem*)item; 28 | 29 | @end 30 | 31 | NS_ASSUME_NONNULL_END 32 | -------------------------------------------------------------------------------- /src/Common/Configuration/DirectoryBookmarkHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryBookmarkHandler.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-15. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface DirectoryBookmarkHandler : NSObject 13 | 14 | #pragma mark - Initializers 15 | 16 | - (instancetype)init; 17 | - (instancetype)initWithUserDefaultsKey:(NSString*)defaultsKey; 18 | 19 | #pragma mark - Accessors 20 | 21 | - (nullable NSData*)bookmarkDataFromDefaults; 22 | - (nullable NSURL*)urlFromDefaultsAndReturnError:(NSError**)error; 23 | - (nullable NSURL*)urlFromDefaultsWithFilename:(NSString*)filename andReturnError:(NSError**)error; 24 | 25 | #pragma mark - Mutators 26 | 27 | - (void)saveBookmarkDataToDefaults:(nullable NSData*)bookmarkData; 28 | - (BOOL)saveURLToDefaults:(nullable NSURL*)url; 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /src/Config/Common/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | #include "Config/Common/Base.xcconfig" 9 | 10 | // Architectures 11 | ONLY_ACTIVE_ARCH = YES 12 | 13 | // Build Options 14 | DEBUG_INFORMATION_FORMAT = dwarf 15 | ENABLE_TESTABILITY = YES 16 | 17 | // Signing 18 | CODE_SIGN_IDENTITY = $(CODE_SIGN_IDENTITY_DEBUG) 19 | PROVISIONING_PROFILE_SPECIFIER = $(PROVISIONING_PROFILE_SPECIFIER_DEBUG) 20 | 21 | // Apple Clang - Code Generation 22 | GCC_OPTIMIZATION_LEVEL = 0 23 | 24 | // Apple Clang - Preprocessing 25 | ENABLE_NS_ASSERTIONS = $(inherited) 26 | GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(GCC_PREPROCESSOR_DEFINITIONS_BASE) $(inherited) 27 | 28 | // User-Defined 29 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE 30 | SENTRY_ENABLED = 0 31 | SENTRY_ENVIRONMENT = debug 32 | HELPER_REGISTRATION_ENABLED = 0 33 | MLE_LOG_LEVEL = MLE_LOG_LEVEL_DEBUG 34 | -------------------------------------------------------------------------------- /src/Config/Common/Release.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Release.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | #include "Config/Common/Base.xcconfig" 9 | 10 | // Architectures 11 | ONLY_ACTIVE_ARCH = NO 12 | 13 | // Build Options 14 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 15 | ENABLE_TESTABILITY = NO 16 | 17 | // Signing 18 | CODE_SIGN_IDENTITY = $(CODE_SIGN_IDENTITY_RELEASE) 19 | PROVISIONING_PROFILE_SPECIFIER = $(PROVISIONING_PROFILE_SPECIFIER_RELEASE) 20 | 21 | // Apple Clang - Code Generation 22 | GCC_OPTIMIZATION_LEVEL = $(inherited) 23 | 24 | // Apple Clang - Preprocessing 25 | ENABLE_NS_ASSERTIONS = NO 26 | GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS_BASE) $(inherited) 27 | 28 | // User-Defined 29 | MTL_ENABLE_DEBUG_INFO = NO 30 | SENTRY_ENABLED = 1 31 | SENTRY_ENVIRONMENT = production 32 | HELPER_REGISTRATION_ENABLED = 1 33 | MLE_LOG_LEVEL = MLE_LOG_LEVEL_INFO 34 | -------------------------------------------------------------------------------- /src/Common/SentryHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // SentryHandler.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-22. 6 | // 7 | 8 | #import 9 | 10 | @class SentrySDK; 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface SentryHandler : NSObject 16 | 17 | 18 | #pragma mark - Properties 19 | 20 | @property (nullable, readonly) SentrySDK* sentrySdk; 21 | 22 | 23 | #pragma mark - Initializers 24 | 25 | - (instancetype)init; 26 | 27 | 28 | #pragma mark - Accessors 29 | 30 | + (SentryHandler*)sharedSentryHandler; 31 | 32 | - (BOOL)userHasEnabledCrashReporting; 33 | - (BOOL)userHasBeenPromptedForCrashReportingPermissions; 34 | 35 | 36 | #pragma mark - Mutators 37 | 38 | - (void)setupSentry; 39 | - (void)restartSentry; 40 | 41 | - (void)setUserHasBeenPromptedForCrashReportingPermissions:(BOOL)flag; 42 | 43 | + (void)setCrashReportingEnabled:(BOOL)flag; 44 | 45 | 46 | 47 | @end 48 | 49 | NS_ASSUME_NONNULL_END 50 | -------------------------------------------------------------------------------- /src/Common/Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-18. 6 | // 7 | 8 | #import "Utils.h" 9 | 10 | #import 11 | 12 | #import "Logger.h" 13 | 14 | 15 | @implementation Utils 16 | 17 | + (nullable NSString*)hexStringForPersistentId:(nullable NSNumber*)persistentId { 18 | 19 | if (persistentId == nil) { 20 | return nil; 21 | } 22 | 23 | return [NSString stringWithFormat:@"%016llX", persistentId.unsignedLongLongValue]; 24 | } 25 | 26 | + (PlaylistSortOrderType)playlistSortOrderForTitle:(nullable NSString*)title { 27 | 28 | if (title == nil) { 29 | return PlaylistSortOrderNull; 30 | } 31 | 32 | if ([title isEqualToString:@"Ascending"]) { 33 | return PlaylistSortOrderAscending; 34 | } 35 | else if ([title isEqualToString:@"Descending"]) { 36 | return PlaylistSortOrderDescending; 37 | } 38 | 39 | return PlaylistSortOrderNull; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /src/Common/Serializer/MediaItemSerializer.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemSerializer.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | #import "MediaItemSerializerDelegate.h" 10 | 11 | @class ITLibMediaItem; 12 | @class MediaEntityRepository; 13 | @class MediaItemFilterGroup; 14 | @class PathMapper; 15 | @class OrderedDictionary; 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @interface MediaItemSerializer : NSObject 20 | 21 | @property (nullable, weak) NSObject* delegate; 22 | 23 | @property (nullable, weak) MediaItemFilterGroup* itemFilters; 24 | @property (nullable, weak) PathMapper* pathMapper; 25 | 26 | - (instancetype) init; 27 | - (instancetype) initWithEntityRepository:(MediaEntityRepository*)entityRepository; 28 | 29 | - (OrderedDictionary*)serializeItems:(NSArray*)items; 30 | - (OrderedDictionary*)serializeItem:(ITLibMediaItem*)item; 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/DirectoryPermissionsWindow/DirectoryPermissionsWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryPermissionsWindowController.h 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-02-13. 6 | // 7 | 8 | #import 9 | 10 | @class ExportConfiguration; 11 | @class ScheduleConfiguration; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface DirectoryPermissionsWindowController : NSWindowController 16 | 17 | 18 | #pragma mark - Initializers 19 | 20 | - (instancetype)init; 21 | - (instancetype)initWithExportConfiguration:(ExportConfiguration*)exportConfiguration 22 | andScheduleConfiguration:(ScheduleConfiguration*)scheduleConfiguration; 23 | 24 | 25 | #pragma mark - Mutators 26 | 27 | - (IBAction)chooseOutputDirectory:(id)sender; 28 | 29 | - (void)showIncorrectDirectoryAlert; 30 | - (void)showAutomaticExportsDisabledDirectoryAlert; 31 | 32 | - (void)requestOutputDirectoryPermissions; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /src/Config/Schemes/music-library-exporter.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // music-library-exporter.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | // Linking 9 | OTHER_LDFLAGS = -ObjC $(inherited) 10 | 11 | // Search Paths 12 | HEADER_SEARCH_PATHS = $(SRCROOT)/3rd/ArgumentParser/ArgumentParser $(inherited) 13 | 14 | // Signing 15 | CODE_SIGN_ENTITLEMENTS = $(MLE_CLITOOL_CODE_SIGN_ENTITLEMENTS) 16 | CODE_SIGN_IDENTITY_DEBUG = $(MLE_CLITOOL_CODE_SIGN_IDENTITY_DEBUG) 17 | CODE_SIGN_IDENTITY_RELEASE = $(MLE_CLITOOL_CODE_SIGN_IDENTITY_RELEASE) 18 | PROVISIONING_PROFILE_SPECIFIER_DEBUG = $(MLE_CLITOOL_PROVISIONING_PROFILE_SPECIFIER_DEBUG) 19 | PROVISIONING_PROFILE_SPECIFIER_RELEASE = $(MLE_CLITOOL_PROVISIONING_PROFILE_SPECIFIER_RELEASE) 20 | 21 | // User-Defined 22 | SENTRY_ENABLED = 0 23 | MLE_LOG_LEVEL = MLE_LOG_LEVEL_WARNING 24 | 25 | // Apple Clang - Preprocessing 26 | GCC_PREPROCESSOR_DEFINITIONS = OUTPUT_DIRECTORY_BOOKMARK_KEY=@\"${OUTPUT_DIRECTORY_BOOKMARK_KEY}\" CLI_VERSION=@\"$(CURRENT_PROJECT_VERSION)-$(VERSION_BUILD)\" $(inherited) 27 | -------------------------------------------------------------------------------- /src/Music Library Exporter/ConfigurationView/HourNumberFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // HourNumberFormatter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-08. 6 | // 7 | 8 | #import "HourNumberFormatter.h" 9 | 10 | @implementation HourNumberFormatter 11 | 12 | 13 | #pragma mark - Initializers 14 | 15 | - (instancetype)init { 16 | 17 | if (self = [super init]) { 18 | 19 | return self; 20 | } 21 | else { 22 | return nil; 23 | } 24 | } 25 | 26 | 27 | #pragma mark - Accessors 28 | 29 | - (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString * _Nullable * _Nullable)newString errorDescription:(NSString * _Nullable * _Nullable)error { 30 | 31 | if (partialString.length == 0) { 32 | return YES; 33 | } 34 | 35 | NSScanner *scanner = [NSScanner scannerWithString:partialString]; 36 | 37 | if ([scanner scanInt:NULL] && [scanner isAtEnd]) { 38 | 39 | int partialStringInt = [partialString intValue]; 40 | if (partialStringInt > 0 && partialStringInt <= 24) { 41 | return YES; 42 | } 43 | } 44 | 45 | return NO; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /src/Common/Serializer/MediaEntityRepository.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaEntityRepository.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import "MediaEntityRepository.h" 9 | 10 | #import 11 | 12 | @implementation MediaEntityRepository { 13 | 14 | NSUInteger _currentEntityID; 15 | 16 | NSMutableDictionary* _entityIDs; 17 | } 18 | 19 | - (instancetype)init { 20 | 21 | if (self = [super init]) { 22 | 23 | _currentEntityID = 1; 24 | _entityIDs = [NSMutableDictionary dictionary]; 25 | 26 | return self; 27 | } 28 | else { 29 | return nil; 30 | } 31 | } 32 | 33 | - (nullable NSNumber*)getIDForEntity:(ITLibMediaEntity*)entity { 34 | 35 | if (entity == nil) { 36 | return nil; 37 | } 38 | 39 | NSNumber* entityID = [_entityIDs objectForKey:entity.persistentID]; 40 | 41 | // not stored yet 42 | if (entityID == nil) { 43 | entityID = [NSNumber numberWithUnsignedInteger:_currentEntityID++]; 44 | [_entityIDs setObject:entityID forKey:entity.persistentID]; 45 | } 46 | 47 | return entityID; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistFilterGroup.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistFilterGroup.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import 9 | 10 | @class ITLibPlaylist; 11 | @class PlaylistParentIDFilter; 12 | 13 | @protocol PlaylistFiltering; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | @interface PlaylistFilterGroup : NSObject 18 | 19 | - (instancetype)init; 20 | - (instancetype)initWithFilters:(NSArray*>*)filters; 21 | - (instancetype)initWithBaseFiltersAndIncludeInternal:(BOOL)includeInternal andFlattenPlaylists:(BOOL)flatten; 22 | 23 | - (NSArray*>*)filters; 24 | - (void)setFilters:(NSArray*>*)filters; 25 | 26 | - (void)addFilter:(NSObject*)filter; 27 | - (void)removeFilter:(NSObject*)filter; 28 | 29 | - (nullable PlaylistParentIDFilter*)addFiltersForExcludedIDs:(NSSet*)excludedIDs andFlattenPlaylists:(BOOL)flatten; 30 | 31 | - (BOOL)filtersPassForPlaylist:(ITLibPlaylist*)playlist; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/ExportScheduler.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportScheduler.h 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | 12 | @class ExportConfiguration; 13 | @class ScheduleConfiguration; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | @interface ExportScheduler : NSObject 18 | 19 | 20 | #pragma mark - Initializers 21 | 22 | - (instancetype)init; 23 | - (instancetype)initWithExportConfiguration:(ExportConfiguration*)exportConfiguration 24 | andScheduleConfiguration:(ScheduleConfiguration*)scheduleConfiguration; 25 | 26 | 27 | #pragma mark - Accessors 28 | 29 | - (nullable NSDate*)determineNextExportDate; 30 | 31 | + (NSString*)getCurrentPowerSource; 32 | + (BOOL)isSystemRunningOnBattery; 33 | 34 | + (BOOL)isMainAppRunning; 35 | 36 | - (BOOL)isOutputDirectoryBookmarkValid; 37 | 38 | - (ExportDeferralReason)reasonToDeferExport; 39 | 40 | 41 | #pragma mark - Mutators 42 | 43 | - (void)activateScheduler; 44 | - (void)deactivateScheduler; 45 | 46 | - (void)updateSchedule; 47 | 48 | - (void)requestOutputDirectoryPermissions; 49 | - (void)requestOutputDirectoryPermissionsIfRequired; 50 | 51 | 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /src/music-library-exporter/CLIManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLIManager.h 3 | // music-library-exporter 4 | // 5 | // Created by Kyle King on 2021-02-17. 6 | // 7 | 8 | #import 9 | 10 | #import "CLIDefines.h" 11 | #import "ExportManagerDelegate.h" 12 | 13 | @class ExportConfiguration; 14 | @class PlaylistTreeNode; 15 | 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @interface CLIManager : NSObject 20 | 21 | extern NSErrorDomain const __MLE_ErrorDomain_CLIManager; 22 | 23 | typedef NS_ENUM(NSUInteger, CLIManagerErrorCode) { 24 | CLIManagerErrorUknown = 0, 25 | CLIManagerErrorInvalidOutputPath, 26 | CLIManagerErrorInvalidMusicMediaDirectory, 27 | CLIManagerErrorInvalidRemapping, 28 | }; 29 | 30 | 31 | # pragma mark - Properties 32 | 33 | @property (readonly) CLICommandKind command; 34 | 35 | @property (nullable, readonly) ExportConfiguration* configuration; 36 | 37 | 38 | #pragma mark - Initializers 39 | 40 | - (instancetype)init; 41 | 42 | 43 | #pragma mark - Accessors 44 | 45 | - (void)printHelp; 46 | - (void)printVersion; 47 | - (void)printPlaylists; 48 | 49 | 50 | #pragma mark - Mutators 51 | 52 | - (BOOL)setupAndReturnError:(NSError**)error; 53 | 54 | - (BOOL)exportLibraryAndReturnError:(NSError**)error; 55 | 56 | 57 | @end 58 | 59 | NS_ASSUME_NONNULL_END 60 | -------------------------------------------------------------------------------- /scripts/mle-increment-build-number: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"; 4 | PROJECT_DIR="$(dirname "${SCRIPT_DIR}")"; 5 | 6 | VERSION_CONFIG_PATH="${PROJECT_DIR}/src/Config/Common/Version.xcconfig"; 7 | 8 | main() { 9 | 10 | if [[ ! -f "${VERSION_CONFIG_PATH}" ]]; then 11 | echo -e "error - failed to find version config file: ${VERSION_CONFIG_PATH}"; 12 | exit 1; 13 | fi; 14 | 15 | VERSION_BUILD_OLD=$(cat ${VERSION_CONFIG_PATH} | grep -m 1 "VERSION_BUILD=" | cut -f2 -d"="); 16 | 17 | if [[ -z "${VERSION_BUILD_OLD}" ]]; then 18 | echo -e "error - failed to parse VERSION_BUILD from version config file: ${VERSION_CONFIG_PATH}"; 19 | exit 1; 20 | fi; 21 | 22 | # increment variable 23 | VERSION_BUILD_NEW=$(( ${VERSION_BUILD_OLD} + 1 )); 24 | 25 | # sed search/replace expressions for version assignment 26 | VER_EXP_I="VERSION_BUILD=${VERSION_BUILD_OLD}"; 27 | VER_EXP_F="VERSION_BUILD=${VERSION_BUILD_NEW}"; 28 | 29 | sed -i ".temp" "s/${VER_EXP_I}/${VER_EXP_F}/g" "${VERSION_CONFIG_PATH}" && rm "${VERSION_CONFIG_PATH}.temp"; 30 | 31 | echo -e "Increased build number from ${VERSION_BUILD_OLD} to ${VERSION_BUILD_NEW} in $(basename "${VERSION_CONFIG_PATH}")"; 32 | } 33 | 34 | main "${@}" 2>&1 | tee -a '/tmp/mle-increment-build-number.log'; 35 | 36 | -------------------------------------------------------------------------------- /src/Common/Sorter/SorterDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // SorterDefines.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-17. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface SorterDefines : NSObject 13 | 14 | #pragma mark - Accessors 15 | 16 | // Properties that should support sorting 17 | + (NSArray*)allProperties; 18 | + (NSSet*)allPropertiesSet; 19 | 20 | // Names of properties to use for frontend, logging, etc. 21 | + (NSDictionary*)propertyNames; 22 | 23 | // Substitute properties to use when the value of a given property is empty/nil 24 | + (NSDictionary*)propertySubstitutions; 25 | 26 | // Alternative properties to compare/sort when the value is identical for both provided instances 27 | + (NSDictionary*)fallbackSortProperties; 28 | 29 | // Default alternatives to use when there is no corresponding array in the `fallbackSortProperties` dictionary 30 | + (NSArray*)defaultFallbackSortProperties; 31 | 32 | // Maps old properties to their new values 33 | + (NSDictionary*)migratedProperties; 34 | 35 | + (nullable NSString*)nameForProperty:(NSString*)property; 36 | 37 | + (NSArray*)substitutionsForProperty:(NSString*)property; 38 | 39 | + (NSArray*)fallbackPropertiesForProperty:(NSString*)property; 40 | 41 | @end 42 | 43 | NS_ASSUME_NONNULL_END 44 | -------------------------------------------------------------------------------- /src/Common/Configuration/ScheduleConfiguration.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduleConfiguration.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import 9 | 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ScheduleConfiguration : NSObject 14 | 15 | 16 | #pragma mark - Initializers 17 | 18 | - (instancetype)init; 19 | 20 | 21 | #pragma mark - Accessors 22 | 23 | - (NSDictionary*)defaultValues; 24 | 25 | - (BOOL)scheduleEnabled; 26 | - (NSTimeInterval)scheduleInterval; 27 | 28 | - (nullable NSDate*)lastExportedAt; 29 | - (nullable NSDate*)nextExportAt; 30 | 31 | - (BOOL)skipOnBattery; 32 | 33 | - (void)dumpProperties; 34 | 35 | 36 | #pragma mark - Mutators 37 | 38 | - (void)loadPropertiesFromUserDefaults; 39 | 40 | - (void)setScheduleEnabled:(BOOL)flag; 41 | - (void)setScheduleInterval:(NSTimeInterval)interval; 42 | 43 | - (void)setLastExportedAt:(nullable NSDate*)timestamp; 44 | - (void)setNextExportAt:(nullable NSDate*)timestamp; 45 | 46 | - (void)setSkipOnBattery:(BOOL)flag; 47 | 48 | @end 49 | 50 | extern NSString* const ScheduleConfigurationKeyScheduleEnabled; 51 | extern NSString* const ScheduleConfigurationKeyScheduleInterval; 52 | extern NSString* const ScheduleConfigurationKeyLastExportedAt; 53 | extern NSString* const ScheduleConfigurationKeyNextExportAt; 54 | extern NSString* const ScheduleConfigurationKeySkipOnBattery; 55 | 56 | NS_ASSUME_NONNULL_END 57 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistIDFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistIDFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistIDFilter.h" 9 | 10 | #import 11 | 12 | #import "Utils.h" 13 | 14 | @implementation PlaylistIDFilter { 15 | 16 | NSMutableSet* _excludedIDs; 17 | } 18 | 19 | - (instancetype)init { 20 | 21 | if (self = [super init]) { 22 | 23 | _excludedIDs = [NSMutableSet set]; 24 | 25 | return self; 26 | } 27 | else { 28 | return nil; 29 | } 30 | } 31 | 32 | - (instancetype)initWithExcludedIDs:(NSSet*)excludedIDs { 33 | 34 | if (self = [self init]) { 35 | 36 | _excludedIDs = [excludedIDs mutableCopy]; 37 | 38 | return self; 39 | } 40 | else { 41 | return nil; 42 | } 43 | } 44 | 45 | - (void)addExcludedID:(NSNumber*)playlistID { 46 | 47 | [_excludedIDs addObject:[Utils hexStringForPersistentId:playlistID]]; 48 | } 49 | 50 | - (void)removeExcludedID:(NSNumber*)playlistID { 51 | 52 | [_excludedIDs removeObject:[Utils hexStringForPersistentId:playlistID]]; 53 | } 54 | 55 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist { 56 | 57 | // excluded IDs contains the playlist's persistent ID 58 | if ([_excludedIDs containsObject:[Utils hexStringForPersistentId:playlist.persistentID]]) { 59 | return NO; 60 | } 61 | else { 62 | return YES; 63 | } 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /src/Common/Export/ExportManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportManager.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | #import "ExportManagerDelegate.h" 12 | #import "MediaItemSerializerDelegate.h" 13 | #import "PlaylistSerializerDelegate.h" 14 | 15 | @class ExportConfiguration; 16 | @class OrderedDictionary; 17 | 18 | NS_ASSUME_NONNULL_BEGIN 19 | 20 | @interface ExportManager : NSObject 21 | 22 | extern NSErrorDomain const __MLE_ErrorDomain_ExportManager; 23 | 24 | typedef NS_ENUM(NSUInteger, ExportManagerErrorCode) { 25 | ExportManagerErrorMusicMediaLocationUnset = 0, 26 | ExportManagerErrorOutputDirectoryInvalid, 27 | ExportManagerErrorRemappingInvalid, 28 | ExportManagerErrorBusyState, 29 | ExportManagerErrorUnitialized, 30 | ExportManagerErrorWriteError, 31 | }; 32 | 33 | #pragma mark - Properties 34 | 35 | @property (nullable, weak) NSObject* delegate; 36 | 37 | @property (readonly) ExportState state; 38 | @property (nullable,copy) NSURL* outputFileURL; 39 | 40 | 41 | #pragma mark - Initializers 42 | 43 | - (instancetype)init; 44 | - (instancetype)initWithConfiguration:(ExportConfiguration*)configuration; 45 | 46 | 47 | #pragma mark - Mutators 48 | 49 | - (BOOL)exportLibraryWithError:(NSError**)error; 50 | 51 | 52 | @end 53 | 54 | NS_ASSUME_NONNULL_END 55 | -------------------------------------------------------------------------------- /src/Common/Serializer/PlaylistSerializer.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistSerializer.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "PlaylistSerializerDelegate.h" 12 | 13 | @class ITLibMediaItem; 14 | @class ITLibPlaylist; 15 | @class MediaEntityRepository; 16 | @class MediaItemFilterGroup; 17 | @class OrderedDictionary; 18 | @class PlaylistFilterGroup; 19 | 20 | NS_ASSUME_NONNULL_BEGIN 21 | 22 | @interface PlaylistSerializer : NSObject 23 | 24 | @property (nullable, weak) NSObject* delegate; 25 | 26 | @property BOOL flattenFolders; 27 | 28 | @property (nullable, weak) PlaylistFilterGroup* playlistFilters; 29 | @property (nullable, weak) MediaItemFilterGroup* itemFilters; 30 | 31 | @property (weak) NSDictionary* playlistCustomSortProperties; 32 | @property (weak) NSDictionary* playlistCustomSortOrders; 33 | 34 | - (instancetype) init; 35 | - (instancetype) initWithEntityRepository:(MediaEntityRepository*)entityRepository; 36 | 37 | - (NSArray*)serializePlaylists:(NSArray*)playlists; 38 | - (OrderedDictionary*)serializePlaylist:(ITLibPlaylist*)playlist; 39 | 40 | - (NSArray*)serializePlaylistItems:(NSArray*)items; 41 | 42 | + (NSString*)describePlaylistKind:(ITLibPlaylistKind)kind; 43 | 44 | @end 45 | 46 | NS_ASSUME_NONNULL_END 47 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleAllowMixedLocalizations 6 | 7 | ITSAppUsesNonExemptEncryption 8 | 9 | NSHumanReadableCopyright 10 | Copyright © 2022 Kyle King 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIconFile 16 | 17 | CFBundleIdentifier 18 | $(PRODUCT_BUNDLE_IDENTIFIER) 19 | CFBundleInfoDictionaryVersion 20 | 6.0 21 | CFBundleName 22 | $(PRODUCT_NAME) 23 | CFBundlePackageType 24 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 25 | CFBundleShortVersionString 26 | $(CURRENT_PROJECT_VERSION) 27 | CFBundleVersion 28 | $(VERSION_BUILD) 29 | LSApplicationCategoryType 30 | public.app-category.music 31 | LSMinimumSystemVersion 32 | $(MACOSX_DEPLOYMENT_TARGET) 33 | NSMainNibFile 34 | MainMenu 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Common/Filter/MediaItem/MediaItemKindFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemKindFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "MediaItemKindFilter.h" 9 | 10 | @implementation MediaItemKindFilter { 11 | 12 | NSMutableSet* _includedKinds; 13 | } 14 | 15 | - (instancetype)init { 16 | 17 | if (self = [super init]) { 18 | 19 | _includedKinds = [NSMutableSet set]; 20 | 21 | return self; 22 | } 23 | else { 24 | return nil; 25 | } 26 | } 27 | 28 | - (instancetype)initWithKinds:(NSSet*)kinds { 29 | 30 | if (self = [self init]) { 31 | 32 | _includedKinds = [kinds mutableCopy]; 33 | 34 | return self; 35 | } 36 | else { 37 | return nil; 38 | } 39 | } 40 | 41 | - (instancetype)initWithBaseKinds { 42 | 43 | NSMutableSet* baseKinds = [NSMutableSet set]; 44 | [baseKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibMediaItemMediaKindSong]]; 45 | 46 | return [self initWithKinds:baseKinds]; 47 | } 48 | 49 | - (void)addKind:(ITLibMediaItemMediaKind)kind { 50 | 51 | [_includedKinds addObject:[NSNumber numberWithUnsignedInteger:kind]]; 52 | } 53 | 54 | - (void)removeKind:(ITLibMediaItemMediaKind)kind { 55 | 56 | [_includedKinds removeObject:[NSNumber numberWithUnsignedInteger:kind]]; 57 | } 58 | 59 | - (BOOL)filterPassesForItem:(ITLibMediaItem*)item { 60 | 61 | return [_includedKinds containsObject:[NSNumber numberWithUnsignedInteger:item.mediaKind]]; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /src/Common/Logger.h: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-17. 6 | // 7 | 8 | #import 9 | 10 | #if ( defined(MLE_LOG_LEVEL_NONE) && MLE_LOG_LEVEL_NONE ) 11 | # undef MLE_LOG_LEVEL_DEBUG 12 | # undef MLE_LOG_LEVEL_INFO 13 | # undef MLE_LOG_LEVEL_WARNING 14 | # undef MLE_LOG_LEVEL_ERROR 15 | #endif 16 | 17 | #if ( !defined(MLE_LOG_LEVEL_NONE) && !defined(MLE_LOG_LEVEL_DEBUG) && !defined(MLE_LOG_LEVEL_INFO) && !defined(MLE_LOG_LEVEL_WARNING) && !defined(MLE_LOG_LEVEL_ERROR) ) 18 | # define MLE_LOG_LEVEL_WARNING 1 19 | #endif 20 | 21 | #define _MLE_LogWithLevel(level,fmt,...) NSLog(fmt, ## __VA_ARGS__) 22 | 23 | #if ( MLE_LOG_LEVEL_DEBUG ) 24 | # define MLE_Log_Debug(fmt,...) _MLE_LogWithLevel(Debug, fmt, ## __VA_ARGS__) 25 | #else 26 | # define MLE_Log_Debug(...) 27 | #endif 28 | 29 | #if ( MLE_LOG_LEVEL_DEBUG || MLE_LOG_LEVEL_INFO ) 30 | # define MLE_Log_Info(fmt,...) _MLE_LogWithLevel(Info, fmt, ## __VA_ARGS__) 31 | #else 32 | # define MLE_Log_Info(...) 33 | #endif 34 | 35 | #if ( MLE_LOG_LEVEL_DEBUG || MLE_LOG_LEVEL_INFO || MLE_LOG_LEVEL_WARNING ) 36 | # define MLE_Log_Warning(fmt,...) _MLE_LogWithLevel(Warning, fmt, ## __VA_ARGS__) 37 | #else 38 | # define MLE_Log_Warning(...) 39 | #endif 40 | 41 | #if ( MLE_LOG_LEVEL_DEBUG || MLE_LOG_LEVEL_INFO || MLE_LOG_LEVEL_WARNING || MLE_LOG_LEVEL_ERROR ) 42 | # define MLE_Log_Error(fmt,...) _MLE_LogWithLevel(Error, fmt, ## __VA_ARGS__) 43 | #else 44 | # define MLE_Log_Error(...) 45 | #endif 46 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleAllowMixedLocalizations 6 | 7 | ITSAppUsesNonExemptEncryption 8 | 9 | NSHumanReadableCopyright 10 | Copyright © 2022 Kyle King 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIconFile 16 | 17 | CFBundleIdentifier 18 | $(PRODUCT_BUNDLE_IDENTIFIER) 19 | CFBundleInfoDictionaryVersion 20 | 6.0 21 | CFBundleName 22 | $(PRODUCT_NAME) 23 | CFBundlePackageType 24 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 25 | CFBundleShortVersionString 26 | $(CURRENT_PROJECT_VERSION) 27 | CFBundleVersion 28 | $(VERSION_BUILD) 29 | LSApplicationCategoryType 30 | public.app-category.music 31 | LSBackgroundOnly 32 | 33 | LSMinimumSystemVersion 34 | $(MACOSX_DEPLOYMENT_TARGET) 35 | NSMainNibFile 36 | MainMenu 37 | NSPrincipalClass 38 | NSApplication 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistParentIDFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistParentIDFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistParentIDFilter.h" 9 | 10 | #import 11 | 12 | #import "Utils.h" 13 | 14 | @implementation PlaylistParentIDFilter { 15 | 16 | NSMutableSet* _excludedIDs; 17 | } 18 | 19 | - (instancetype)init { 20 | 21 | if (self = [super init]) { 22 | 23 | _excludedIDs = [NSMutableSet set]; 24 | 25 | return self; 26 | } 27 | else { 28 | return nil; 29 | } 30 | } 31 | 32 | - (instancetype)initWithExcludedIDs:(NSSet*)excludedIDs { 33 | 34 | if (self = [self init]) { 35 | 36 | _excludedIDs = [excludedIDs mutableCopy]; 37 | 38 | return self; 39 | } 40 | else { 41 | return nil; 42 | } 43 | } 44 | 45 | - (void)addExcludedID:(NSNumber*)playlistID { 46 | 47 | [_excludedIDs addObject:[Utils hexStringForPersistentId:playlistID]]; 48 | } 49 | 50 | - (void)removeExcludedID:(NSNumber*)playlistID { 51 | 52 | [_excludedIDs removeObject:[Utils hexStringForPersistentId:playlistID]]; 53 | } 54 | 55 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist { 56 | 57 | NSString* parentPlaylistID = [Utils hexStringForPersistentId:playlist.parentID]; 58 | 59 | // excluded IDs contains the playlist's parent persistent ID 60 | if (parentPlaylistID != nil && [_excludedIDs containsObject:parentPlaylistID]) { 61 | return NO; 62 | } 63 | else { 64 | return YES; 65 | } 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /src/Common/PlaylistTree/PlaylistTreeNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistTreeNode.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-08. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "Defines.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface PlaylistTreeNode : NSObject 16 | 17 | 18 | #pragma mark - Properties 19 | 20 | @property NSArray* children; 21 | 22 | @property (nullable, nonatomic, copy) NSString* customSortProperty; 23 | @property (nonatomic, assign) PlaylistSortOrderType customSortOrder; 24 | 25 | @property (nullable, readonly, nonatomic, copy) NSString* playlistPersistentHexID; 26 | @property (nullable, readonly, nonatomic, copy) NSString* playlistParentPersistentHexID; 27 | @property (nullable, readonly, nonatomic, copy) NSString* playlistName; 28 | @property (readonly, nonatomic, assign) ITLibDistinguishedPlaylistKind playlistDistinguishedKind; 29 | @property (readonly, nonatomic, assign) ITLibPlaylistKind playlistKind; 30 | @property (readonly, nonatomic, assign, getter=isMaster) BOOL playlistMaster; 31 | 32 | 33 | #pragma mark - Initializers 34 | 35 | - (instancetype)init; 36 | 37 | + (PlaylistTreeNode*)nodeWithPlaylist:(nullable ITLibPlaylist*)playlist; 38 | + (PlaylistTreeNode*)nodeWithPlaylist:(nullable ITLibPlaylist*)playlist andChildren:(NSArray*)childNodes; 39 | 40 | 41 | #pragma mark - Accessors 42 | 43 | - (NSString*)kindDescription; 44 | - (NSString*)itemsDescription; 45 | 46 | 47 | @end 48 | 49 | 50 | NS_ASSUME_NONNULL_END 51 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistKindFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistKindFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistKindFilter.h" 9 | 10 | @implementation PlaylistKindFilter { 11 | 12 | NSMutableSet* _includedKinds; 13 | } 14 | 15 | - (instancetype)init { 16 | 17 | if (self = [super init]) { 18 | 19 | _includedKinds = [NSMutableSet set]; 20 | 21 | return self; 22 | } 23 | else { 24 | return nil; 25 | } 26 | } 27 | 28 | - (instancetype)initWithKinds:(NSSet*)kinds { 29 | 30 | if (self = [self init]) { 31 | 32 | _includedKinds = [kinds mutableCopy]; 33 | 34 | return self; 35 | } 36 | else { 37 | return nil; 38 | } 39 | } 40 | 41 | - (instancetype)initWithBaseKinds { 42 | 43 | NSMutableSet* baseKinds = [NSMutableSet set]; 44 | [baseKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibPlaylistKindRegular]]; 45 | [baseKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibPlaylistKindSmart]]; 46 | 47 | return [self initWithKinds:baseKinds]; 48 | } 49 | 50 | - (void)addKind:(ITLibPlaylistKind)kind { 51 | 52 | [_includedKinds addObject:[NSNumber numberWithUnsignedInteger:kind]]; 53 | } 54 | 55 | - (void)removeKind:(ITLibPlaylistKind)kind { 56 | 57 | [_includedKinds removeObject:[NSNumber numberWithUnsignedInteger:kind]]; 58 | } 59 | 60 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist { 61 | 62 | return [_includedKinds containsObject:[NSNumber numberWithUnsignedInteger:playlist.kind]]; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /src/Config/Schemes/Music Library Exporter.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Music Library Exporter.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | #include "Config/Common/Sentry.xcconfig" 9 | #include "Config/Common/Swift.xcconfig" 10 | 11 | // Deployment 12 | COMBINE_HIDPI_IMAGES = YES 13 | SKIP_INSTALL = $(INHERITED) 14 | 15 | // Linking 16 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks 17 | 18 | // Packaging 19 | INFOPLIST_FILE = Music Library Exporter/Supporting Files/Info.plist 20 | PRODUCT_BUNDLE_IDENTIFIER = com.kylekingcdn.MusicLibraryExporter 21 | 22 | // Signing 23 | CODE_SIGN_ENTITLEMENTS = $(MLE_MAINAPP_CODE_SIGN_ENTITLEMENTS) 24 | CODE_SIGN_IDENTITY_DEBUG = $(MLE_MAINAPP_CODE_SIGN_IDENTITY_DEBUG) 25 | CODE_SIGN_IDENTITY_RELEASE = $(MLE_MAINAPP_CODE_SIGN_IDENTITY_RELEASE) 26 | PROVISIONING_PROFILE_SPECIFIER_DEBUG = $(MLE_MAINAPP_PROVISIONING_PROFILE_SPECIFIER_DEBUG) 27 | PROVISIONING_PROFILE_SPECIFIER_RELEASE = $(MLE_MAINAPP_PROVISIONING_PROFILE_SPECIFIER_RELEASE) 28 | 29 | // Asset Catalog Compiler - Options 30 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon 31 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor 32 | 33 | // Apple Clang - Preprocessing 34 | GCC_PREPROCESSOR_DEFINITIONS = OUTPUT_DIRECTORY_BOOKMARK_KEY=@\"${OUTPUT_DIRECTORY_BOOKMARK_KEY}\" HELPER_REGISTRATION_ENABLED=$(HELPER_REGISTRATION_ENABLED) SENTRY_DSN=${SENTRY_MAIN_DSN} $(inherited) 35 | 36 | // Swift 37 | SWIFT_OBJC_BRIDGING_HEADER = Common/SwiftCompatFix/Bridging-Header.h 38 | 39 | // User-Defined 40 | OUTPUT_DIRECTORY_BOOKMARK_KEY = 'OutputDirectoryBookmarkMain' 41 | -------------------------------------------------------------------------------- /src/music-library-exporter/CLIDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLIDefines.h 3 | // music-library-exporter 4 | // 5 | // Created by Kyle King on 2021-02-15. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | 13 | @interface CLIDefines : NSObject 14 | 15 | typedef NS_ENUM(NSUInteger, CLICommandKind) { 16 | CLICommandKindHelp = 0, 17 | CLICommandKindVersion, 18 | CLICommandKindPrint, 19 | CLICommandKindExport, 20 | CLICommandKindUnknown, 21 | }; 22 | 23 | typedef NS_ENUM(NSUInteger, CLIOptionKind) { 24 | 25 | // - shared - // 26 | 27 | CLIOptionKindHelp = 0, 28 | 29 | CLIOptionKindVersion, 30 | 31 | CLIOptionKindReadPrefs, 32 | 33 | CLIOptionKindFlatten, 34 | CLIOptionKindExcludeInternal, 35 | CLIOptionKindExcludeIds, 36 | 37 | // - export only - // 38 | 39 | CLIOptionKindMusicMediaDirectory, 40 | 41 | CLIOptionKindSort, 42 | CLIOptionKindRemapSearch, 43 | CLIOptionKindRemapReplace, 44 | CLIOptionKindRemapLocalhostPrefix, 45 | CLIOptionKindOutputPath, 46 | 47 | 48 | CLIOptionKind_MAX, 49 | }; 50 | 51 | + (nullable NSString*)nameForCommand:(CLICommandKind)command; 52 | + (nullable NSString*)nameForOption:(CLIOptionKind)option; 53 | 54 | + (NSArray*)commandNames; 55 | 56 | + (NSArray*)optionsForCommand:(CLICommandKind)command; 57 | + (NSArray*)requiredOptionsForCommand:(CLICommandKind)command; 58 | 59 | + (nullable NSString*)signatureFormatForCommand:(CLICommandKind)command; 60 | + (nullable NSString*)signatureFormatForOption:(CLIOptionKind)option; 61 | 62 | + (NSURL*)fileUrlForAppPreferences; 63 | 64 | @end 65 | 66 | NS_ASSUME_NONNULL_END 67 | -------------------------------------------------------------------------------- /src/Config/Schemes/Music Library Exporter Helper.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Music Library Exporter Helper.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | #include "Config/Common/Sentry.xcconfig" 9 | #include "Config/Common/Swift.xcconfig" 10 | 11 | // Deployment 12 | COMBINE_HIDPI_IMAGES = YES 13 | SKIP_INSTALL = YES 14 | 15 | // Linking 16 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks 17 | 18 | // Packaging 19 | INFOPLIST_FILE = Music Library Exporter Helper/Supporting Files/Info.plist 20 | PRODUCT_BUNDLE_IDENTIFIER = com.kylekingcdn.MusicLibraryExporter.MusicLibraryExporterHelper 21 | 22 | // Signing 23 | CODE_SIGN_ENTITLEMENTS = $(MLE_HELPERAPP_CODE_SIGN_ENTITLEMENTS) 24 | CODE_SIGN_IDENTITY_DEBUG = $(MLE_HELPERAPP_CODE_SIGN_IDENTITY_DEBUG) 25 | CODE_SIGN_IDENTITY_RELEASE = $(MLE_HELPERAPP_CODE_SIGN_IDENTITY_RELEASE) 26 | PROVISIONING_PROFILE_SPECIFIER_DEBUG = $(MLE_HELPERAPP_PROVISIONING_PROFILE_SPECIFIER_DEBUG) 27 | PROVISIONING_PROFILE_SPECIFIER_RELEASE = $(MLE_HELPERAPP_PROVISIONING_PROFILE_SPECIFIER_RELEASE) 28 | 29 | // Asset Catalog Compiler - Options 30 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon 31 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor 32 | 33 | // Apple Clang - Preprocessing 34 | GCC_PREPROCESSOR_DEFINITIONS = OUTPUT_DIRECTORY_BOOKMARK_KEY=@\"${OUTPUT_DIRECTORY_BOOKMARK_KEY}\" HELPER_REGISTRATION_ENABLED=$(HELPER_REGISTRATION_ENABLED) SENTRY_DSN=${SENTRY_HELPER_DSN} $(inherited) 35 | 36 | // Swift 37 | SWIFT_OBJC_BRIDGING_HEADER = Common/SwiftCompatFix/Bridging-Header.h 38 | 39 | // User-Defined 40 | OUTPUT_DIRECTORY_BOOKMARK_KEY = 'OutputDirectoryBookmarkHelper' 41 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Config/Common/Signing.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Signing.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | // Shared 9 | CODE_SIGN_STYLE = Manual 10 | DEVELOPMENT_TEAM = 9YLM7HTV6V 11 | ENABLE_HARDENED_RUNTIME = YES 12 | 13 | // Entitlements 14 | MLE_MAINAPP_CODE_SIGN_ENTITLEMENTS = Music Library Exporter/Supporting Files/Music_Library_Exporter.entitlements 15 | MLE_HELPERAPP_CODE_SIGN_ENTITLEMENTS = Music Library Exporter Helper/Supporting Files/Music_Library_Exporter_Helper.entitlements 16 | MLE_CLITOOL_CODE_SIGN_ENTITLEMENTS = 17 | 18 | // Code Signing Identity 19 | MLE_MAINAPP_CODE_SIGN_IDENTITY_DEBUG = Apple Development: Kyle King (Y4WV8H5R72) 20 | MLE_MAINAPP_CODE_SIGN_IDENTITY_RELEASE = 3rd Party Mac Developer Application: Kyle King (9YLM7HTV6V) 21 | MLE_HELPERAPP_CODE_SIGN_IDENTITY_DEBUG = Apple Development: Kyle King (Y4WV8H5R72) 22 | MLE_HELPERAPP_CODE_SIGN_IDENTITY_RELEASE = 3rd Party Mac Developer Application: Kyle King (9YLM7HTV6V) 23 | MLE_CLITOOL_CODE_SIGN_IDENTITY_DEBUG = Apple Development: Kyle King (Y4WV8H5R72) 24 | MLE_CLITOOL_CODE_SIGN_IDENTITY_RELEASE = 3rd Party Mac Developer Application: Kyle King (9YLM7HTV6V) 25 | 26 | // Provisioning Profile 27 | MLE_MAINAPP_PROVISIONING_PROFILE_SPECIFIER_DEBUG = Music Library Exporter - Development 28 | MLE_MAINAPP_PROVISIONING_PROFILE_SPECIFIER_RELEASE = Music Library Exporter 29 | MLE_HELPERAPP_PROVISIONING_PROFILE_SPECIFIER_DEBUG = Music Library Exporter Helper - Development 30 | MLE_HELPERAPP_PROVISIONING_PROFILE_SPECIFIER_RELEASE = Music Library Exporter Helper 31 | MLE_CLITOOL_PROVISIONING_PROFILE_SPECIFIER_DEBUG = 32 | MLE_CLITOOL_PROVISIONING_PROFILE_SPECIFIER_RELEASE = 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PreferencesWindow/PreferencesWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowController.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-24. 6 | // 7 | 8 | #import "PreferencesWindowController.h" 9 | 10 | #import "Logger.h" 11 | 12 | #if SENTRY_ENABLED == 1 13 | #import "SentryHandler.h" 14 | #endif 15 | 16 | 17 | @interface PreferencesWindowController () 18 | 19 | @property (weak) IBOutlet NSButton* crashReportingCheckBox; 20 | 21 | @end 22 | 23 | 24 | @implementation PreferencesWindowController 25 | 26 | - (instancetype)init { 27 | 28 | if (self = [super initWithWindowNibName:@"PreferencesWindow"]) { 29 | 30 | _crashReportingCheckBox = nil; 31 | 32 | return self; 33 | } 34 | else { 35 | return nil; 36 | } 37 | } 38 | 39 | - (void)windowDidLoad { 40 | 41 | [super windowDidLoad]; 42 | 43 | BOOL sentryEnabled = NO; 44 | BOOL crashReportingEnabled = NO; 45 | 46 | #if SENTRY_ENABLED == 1 47 | sentryEnabled = YES; 48 | crashReportingEnabled = [[SentryHandler sharedSentryHandler] userHasEnabledCrashReporting]; 49 | #endif 50 | 51 | [_crashReportingCheckBox setEnabled:sentryEnabled]; 52 | [_crashReportingCheckBox setState:(crashReportingEnabled ? NSControlStateValueOn : NSControlStateValueOff)]; 53 | } 54 | 55 | - (IBAction)setCrashReportingEnabled:(id)sender { 56 | 57 | #if SENTRY_ENABLED == 1 58 | NSControlStateValue flagState = [sender state]; 59 | BOOL flag = (flagState == NSControlStateValueOn); 60 | 61 | MLE_Log_Info(@"PreferencesWindowController [setCrashReportingEnabled:%@]", (flag ? @"YES" : @"NO")); 62 | 63 | [SentryHandler setCrashReportingEnabled:flag]; 64 | #endif 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /src/Common/Configuration/UserDefaultsExportConfiguration.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsExportConfiguration.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-01. 6 | // 7 | 8 | #import 9 | 10 | #import "ExportConfiguration.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface UserDefaultsExportConfiguration : ExportConfiguration 16 | 17 | 18 | #pragma mark - Initializers 19 | 20 | - (instancetype)init; 21 | - (instancetype)initWithOutputDirectoryBookmarkKey:(NSString*)outputDirectoryBookmarkKey; 22 | 23 | #pragma mark - Mutators 24 | 25 | - (void)setMusicLibraryPath:(NSString*)musicLibraryPath; 26 | 27 | - (void)setGeneratedPersistentLibraryId:(NSString*)generatedPersistentLibraryId; 28 | 29 | - (void)setOutputDirectoryUrl:(nullable NSURL*)dirUrl; 30 | - (void)setOutputDirectoryPath:(nullable NSString*)dirPath; 31 | - (void)setOutputFileName:(NSString*)fileName; 32 | 33 | - (void)setRemapRootDirectory:(BOOL)flag; 34 | - (void)setRemapRootDirectoryOriginalPath:(NSString*)originalPath; 35 | - (void)setRemapRootDirectoryMappedPath:(NSString*)mappedPath; 36 | - (void)setRemapRootDirectoryLocalhostPrefix:(BOOL)flag; 37 | 38 | - (void)setFlattenPlaylistHierarchy:(BOOL)flag; 39 | - (void)setIncludeInternalPlaylists:(BOOL)flag; 40 | 41 | - (void)setExcludedPlaylistPersistentIds:(NSSet*)excludedIds; 42 | - (void)addExcludedPlaylistPersistentId:(NSString*)playlistId; 43 | - (void)removeExcludedPlaylistPersistentId:(NSString*)playlistId; 44 | 45 | - (void)setCustomSortPropertyDict:(NSDictionary*)dict; 46 | - (void)setCustomSortOrderDict:(NSDictionary*)dict; 47 | 48 | - (void)loadPropertiesFromUserDefaults; 49 | 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /src/Common/Defines.h: -------------------------------------------------------------------------------- 1 | // 2 | // Defines.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import 9 | 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface Defines : NSObject 14 | 15 | extern NSString* const __MLE__AppGroupIdentifier; 16 | extern NSString* const __MLE__AppBundleIdentifier; 17 | extern NSString* const __MLE__HelperBundleIdentifier; 18 | 19 | @end 20 | 21 | typedef NS_ENUM(NSUInteger, ExportState) { 22 | ExportStopped = 0, 23 | ExportPreparing, 24 | ExportGeneratingTracks, 25 | ExportGeneratingPlaylists, 26 | ExportGeneratingLibrary, 27 | ExportWritingToDisk, 28 | ExportFinished, 29 | ExportError 30 | }; 31 | 32 | static NSString *_Nonnull const ExportStateNames[] = { 33 | @"Stopped", 34 | @"Preparing", 35 | @"Generating tracks", 36 | @"Generating playlists", 37 | @"Generating library", 38 | @"Saving to disk", 39 | @"Finished", 40 | @"Error", 41 | }; 42 | 43 | typedef NS_ENUM(NSUInteger, ExportDeferralReason) { 44 | ExportDeferralOnBatteryReason = 0, 45 | ExportDeferralMainAppOpenReason, 46 | ExportDeferralErrorReason, 47 | ExportDeferralUnknownReason, 48 | ExportNoDeferralReason, 49 | }; 50 | 51 | static NSString *_Nonnull const ExportDeferralReasonNames[] = { 52 | @"Running on battery", 53 | @"Main app open", 54 | @"Error", 55 | @"Unknown", 56 | @"Not deferred", 57 | }; 58 | 59 | typedef NS_ENUM(NSUInteger, PlaylistSortModeType) { 60 | PlaylistSortDefaultMode = 0, 61 | PlaylistSortCustomMode, 62 | }; 63 | 64 | typedef NS_ENUM(NSUInteger, PlaylistSortOrderType) { 65 | PlaylistSortOrderAscending = 0, 66 | PlaylistSortOrderDescending, 67 | PlaylistSortOrderNull, 68 | }; 69 | 70 | static NSString *_Nullable const PlaylistSortOrderNames[] = { 71 | @"Ascending", 72 | @"Descending", 73 | nil, 74 | }; 75 | 76 | NS_ASSUME_NONNULL_END 77 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PlaylistsView/PlaylistsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistsViewController.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-10. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | 12 | @class ExportConfiguration; 13 | @class PlaylistTreeNode; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | @interface PlaylistsViewController : NSViewController 18 | 19 | typedef NS_ENUM(NSUInteger, TableColumnType) { 20 | NullColumn = 0, 21 | TitleColumn, 22 | KindColumn, 23 | ItemsColumn, 24 | SortingColumn 25 | }; 26 | 27 | 28 | #pragma mark - Initializers 29 | 30 | - (instancetype)init; 31 | - (instancetype)initWithExportConfiguration:(ExportConfiguration*)exportConfiguration; 32 | 33 | 34 | #pragma mark - Accessors 35 | 36 | + (TableColumnType)columnWithIdentifier:(NSString*)columnIdentifier; 37 | 38 | + (nullable NSString*)cellViewIdentifierForColumn:(TableColumnType)column; 39 | + (nullable NSString*)cellTitleForColumn:(TableColumnType)column andNode:(PlaylistTreeNode*)node; 40 | 41 | + (nullable NSString*)playlistSortPropertyForMenuItemTag:(NSInteger)tag; 42 | + (PlaylistSortOrderType)playlistSortOrderForMenuItemTag:(NSInteger)tag; 43 | 44 | + (NSInteger)menuItemTagForPlaylistSortProperty:(nullable NSString*)sortProperty; 45 | + (NSInteger)menuItemTagForPlaylistSortOrder:(PlaylistSortOrderType)sortOrder; 46 | 47 | - (BOOL)isNodeParentExcluded:(nullable PlaylistTreeNode*)node; 48 | - (BOOL)isNodeExcluded:(nullable PlaylistTreeNode*)node; 49 | 50 | - (nullable PlaylistTreeNode*)playlistNodeForCellView:(NSView*)cellView; 51 | 52 | - (void)updateSortingButton:(NSPopUpButton*)button forNode:(PlaylistTreeNode*)node; 53 | 54 | 55 | #pragma mark - Mutators 56 | 57 | - (void)initPlaylistNodes; 58 | 59 | - (IBAction)setPlaylistExcludedForCellView:(id)sender; 60 | 61 | - (IBAction)setPlaylistSorting:(id)sender; 62 | 63 | 64 | @end 65 | 66 | NS_ASSUME_NONNULL_END 67 | -------------------------------------------------------------------------------- /src/music-library-exporter/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // music-library-exporter 4 | // 5 | // Created by Kyle King on 2021-01-18. 6 | // 7 | 8 | #import 9 | 10 | #import "CLIManager.h" 11 | #import "ExportConfiguration.h" 12 | 13 | int main(int argc, const char * argv[]) { 14 | 15 | @autoreleasepool { 16 | 17 | CLIManager* cliManager = [[CLIManager alloc] init]; 18 | 19 | // parse args and load configuration 20 | NSError* setupError; 21 | if (![cliManager setupAndReturnError:&setupError]) { 22 | if (setupError) { 23 | fprintf(stderr, "%s\n", setupError.localizedDescription.UTF8String); 24 | } 25 | return 1; 26 | } 27 | 28 | // cliManager won't always be initialized (e.g. for help command) 29 | if (cliManager.configuration) { 30 | [cliManager.configuration dumpProperties]; 31 | } 32 | 33 | // handle command 34 | NSError* commandError; 35 | BOOL commandSuccess = YES; 36 | switch (cliManager.command) { 37 | 38 | case CLICommandKindExport: { 39 | commandSuccess = [cliManager exportLibraryAndReturnError:&commandError]; 40 | break; 41 | } 42 | 43 | case CLICommandKindPrint: { 44 | [cliManager printPlaylists]; 45 | break; 46 | } 47 | 48 | case CLICommandKindHelp: { 49 | [cliManager printHelp]; 50 | break; 51 | } 52 | 53 | case CLICommandKindVersion: { 54 | [cliManager printVersion]; 55 | break; 56 | } 57 | 58 | // This is included despite setup throwing an error even if it is the case. 59 | // This allows for potential IDE warnings for any added command types in the future. 60 | case CLICommandKindUnknown: { break; } 61 | } 62 | 63 | if (!commandSuccess) { 64 | if (commandError) { 65 | fprintf(stderr, "%s\n", commandError.localizedDescription.UTF8String); 66 | } 67 | return 1; 68 | } 69 | } 70 | 71 | return 0; 72 | } 73 | -------------------------------------------------------------------------------- /scripts/mle-sentry-upload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"; 4 | KEYS_PATH="${SCRIPT_DIR}/.sentry.keys"; 5 | 6 | read_keys() { 7 | 8 | # keys file not found, exit without failing 9 | if [[ ! -f "${KEYS_PATH}" ]]; then 10 | exit 0; 11 | fi; 12 | 13 | source "${KEYS_PATH}"; 14 | 15 | if [[ -z "${SENTRY_ORG}" ]]; then 16 | echo -e "error - SENTRY_ORG not set"; 17 | exit -1; 18 | elif [[ -z "${SENTRY_PROJECT}" ]]; then 19 | echo -e "error - SENTRY_PROJECT not set"; 20 | exit -1; 21 | elif [[ -z "${SENTRY_AUTH_TOKEN}" ]]; then 22 | echo -e "error - SENTRY_AUTH_TOKEN not set"; 23 | exit -1; 24 | fi; 25 | } 26 | 27 | upload_dsyms() { 28 | 29 | echo -e "\nUploading debug symbols to sentry - $(date)\n"; 30 | 31 | if [[ -z "${ARCHIVE_DSYMS_PATH}" ]]; then 32 | echo "error - ARCHIVE_DSYMS_PATH is unset!"; 33 | exit -1; 34 | elif [[ ! -d "${ARCHIVE_DSYMS_PATH}" ]]; then 35 | echo "error - directory for ARCHIVE_DSYMS_PATH doesn't exist: '${ARCHIVE_DSYMS_PATH}'"; 36 | exit -1; 37 | fi; 38 | 39 | sentry-cli upload-dif --force-foreground "${ARCHIVE_DSYMS_PATH}"; 40 | 41 | echo; 42 | } 43 | 44 | create_release() { 45 | 46 | echo -e "\nCreating a sentry release - $(date)\n"; 47 | 48 | if [[ -z "${CURRENT_PROJECT_VERSION}" ]]; then 49 | echo "error - CURRENT_PROJECT_VERSION is unset!"; 50 | exit -1; 51 | elif [[ -z "${VERSION_BUILD}" ]]; then 52 | echo "error - VERSION_BUILD is unset!"; 53 | exit -1; 54 | fi; 55 | 56 | SENTRY_RELEASE_VERSION="${PRODUCT_BUNDLE_IDENTIFIER}@${CURRENT_PROJECT_VERSION}+${VERSION_BUILD}"; 57 | 58 | sentry-cli releases new "${SENTRY_RELEASE_VERSION}"; 59 | sentry-cli releases set-commits --auto "${SENTRY_RELEASE_VERSION}"; 60 | sentry-cli releases finalize "${SENTRY_RELEASE_VERSION}"; 61 | 62 | echo; 63 | } 64 | 65 | main() { 66 | 67 | cd "${SRCROOT}"; 68 | 69 | read_keys; 70 | 71 | create_release; 72 | 73 | upload_dsyms; 74 | } 75 | 76 | 77 | main "${@}" 2>&1 | tee -a "${SCRIPT_DIR}/mle-sentry-upload.log"; 78 | 79 | -------------------------------------------------------------------------------- /src/Common/Filter/MediaItem/MediaItemFilterGroup.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemFilterGroup.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "MediaItemFilterGroup.h" 9 | 10 | #import 11 | 12 | #import "MediaItemFiltering.h" 13 | #import "MediaItemKindFilter.h" 14 | 15 | @implementation MediaItemFilterGroup { 16 | 17 | NSMutableArray*>* _filters; 18 | } 19 | 20 | - (instancetype)init { 21 | 22 | if (self = [super init]) { 23 | 24 | _filters = [NSMutableArray array]; 25 | 26 | return self; 27 | } 28 | else { 29 | return nil; 30 | } 31 | } 32 | 33 | - (instancetype)initWithFilters:(NSArray*>*)filters { 34 | 35 | if (self = [self init]) { 36 | 37 | _filters = [filters mutableCopy]; 38 | 39 | return self; 40 | } 41 | else { 42 | return nil; 43 | } 44 | } 45 | 46 | - (instancetype)initWithBaseFilters { 47 | 48 | NSMutableArray*>* baseFilters = [NSMutableArray array]; 49 | 50 | [baseFilters addObject:[[MediaItemKindFilter alloc] initWithBaseKinds]]; 51 | 52 | return [self initWithFilters:baseFilters]; 53 | } 54 | 55 | - (NSArray*>*)filters { 56 | 57 | return _filters; 58 | } 59 | 60 | - (void)setFilters:(NSArray*>*)filters { 61 | 62 | _filters = [filters mutableCopy]; 63 | } 64 | 65 | - (void)addFilter:(NSObject*)filter { 66 | 67 | NSAssert(![_filters containsObject:filter], @"MediaItemFilterGroup already contains specified filter"); 68 | 69 | [_filters addObject:filter]; 70 | } 71 | 72 | - (void)removeFilter:(NSObject*)filter { 73 | 74 | NSAssert([_filters containsObject:filter], @"MediaItemFilterGroup does not contain specified filter"); 75 | 76 | [_filters removeObject:filter]; 77 | } 78 | 79 | - (BOOL)filtersPassForItem:(ITLibMediaItem*)item { 80 | 81 | for (NSObject* filter in _filters) { 82 | if (![filter filterPassesForItem:item]) { 83 | return NO; 84 | } 85 | } 86 | 87 | return YES; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /src/Common/Serializer/PathMapper.m: -------------------------------------------------------------------------------- 1 | // 2 | // PathMapper.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import "PathMapper.h" 9 | 10 | #import 11 | 12 | @implementation PathMapper 13 | 14 | - (instancetype)init { 15 | 16 | if (self = [super init]) { 17 | 18 | _addLocalhostPrefix = NO; 19 | return self; 20 | } 21 | else { 22 | return nil; 23 | } 24 | } 25 | 26 | - (NSString*)processPath:(NSString*)path { 27 | if (path == nil) { 28 | os_log_fault(OS_LOG_DEFAULT, "[PathMapper processPath] was erroneously provided a null file path!"); 29 | return nil; 30 | } 31 | 32 | if (_searchString != nil && _replaceString != nil) { 33 | return [path stringByReplacingOccurrencesOfString:_searchString withString:_replaceString]; 34 | } 35 | else { 36 | return path; 37 | } 38 | } 39 | 40 | - (NSURL*)mapURLFromPath:(NSString*)path { 41 | if (path == nil) { 42 | os_log_fault(OS_LOG_DEFAULT, "[PathMapper mapURLFromPath] was erroneously provided a null file path!"); 43 | return nil; 44 | } 45 | 46 | NSString* mappedPath = [self processPath:path]; 47 | os_log_debug(OS_LOG_DEFAULT, "Mapped item path from: '%{public}@' to '%{public}@'", path, mappedPath); 48 | 49 | NSURL* mappedUrl = [NSURL fileURLWithPath:mappedPath relativeToURL:[NSURL fileURLWithPath:@"/"]]; 50 | 51 | return mappedUrl; 52 | } 53 | 54 | - (NSString*)mapPath:(NSURL*)pathURL { 55 | if (pathURL == nil) { 56 | os_log_fault(OS_LOG_DEFAULT, "[PathMapper mapPath] was erroneously provided a null path URL!"); 57 | return nil; 58 | } 59 | 60 | os_log_debug(OS_LOG_DEFAULT, "Mapping item path from URL: '%{public}@'", pathURL); 61 | 62 | NSURL* mappedURL = [self mapURLFromPath:pathURL.path]; 63 | NSString* mappedString = [mappedURL absoluteString]; 64 | 65 | if (_addLocalhostPrefix) { 66 | mappedString = [mappedString stringByReplacingOccurrencesOfString:@"file:///" withString:@"file://localhost/"]; 67 | os_log_debug(OS_LOG_DEFAULT, "Injected localhost prefix into path: %{public}@", pathURL); 68 | } 69 | else { 70 | os_log_debug(OS_LOG_DEFAULT, "Mapped path from '%{public}@' to '%{public}@'", pathURL, mappedString); 71 | } 72 | 73 | return mappedString; 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /src/Music Library Exporter/Supporting Files/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2578 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;\f1\fnil\fcharset0 LucidaGrande;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\})}{\leveltext\leveltemplateid1\'02\'00);}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 6 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 7 | \margl1440\margr1440\vieww30060\viewh10920\viewkind0 8 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0 9 | 10 | \f0\b\fs24 \cf0 Acknowledgments\ 11 | \ 12 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 13 | 14 | \fs20 \cf0 Charcoal Design ( OrderedDictionary ) 15 | \f1\b0 \ 16 | Copyright \'a9 2010 Charcoal Design\ 17 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 18 | \cf0 \ul \ulc0 https://github.com/nicklockwood/OrderedDictionary\ulnone \ 19 | Version 1.4, September 12th, 2016\ 20 | This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.\ 21 | Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:\ 22 | \pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx4768\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\partightenfactor0 23 | \ls1\ilvl0\cf0 {\listtext 1) }The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\ 24 | {\listtext 2) }Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\ 25 | {\listtext 3) }This notice may not be removed or altered from any source distribution.} -------------------------------------------------------------------------------- /src/Music Library Exporter/ConfigurationView/ConfigurationViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationViewController.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-29. 6 | // 7 | 8 | #import 9 | 10 | #import "Defines.h" 11 | #import "ExportManagerDelegate.h" 12 | 13 | @class ScheduleConfiguration; 14 | @class ExportConfiguration; 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | @interface ConfigurationViewController : NSViewController 19 | 20 | 21 | extern NSErrorDomain const __MLE_ErrorDomain_ConfigurationView; 22 | 23 | typedef NS_ENUM(NSUInteger, ConfigurationViewErrorCode) { 24 | ConfigurationViewErrorUknown = 0, 25 | ConfigurationViewErrorOutputDirectoryUnwritable, 26 | }; 27 | 28 | 29 | #pragma mark - Initializers 30 | 31 | - (instancetype)init; 32 | - (instancetype)initWithExportConfiguration:(ExportConfiguration*)exportConfiguration 33 | andScheduleConfiguration:(ScheduleConfiguration*)scheduleConfiguration; 34 | 35 | 36 | #pragma mark - Accessors 37 | 38 | - (id)firstResponderView; 39 | 40 | 41 | #pragma mark - Mutators 42 | 43 | - (void)updateFromConfiguration; 44 | 45 | - (IBAction)setMediaFolderLocation:(id)sender; 46 | - (IBAction)browseAndValidateOutputDirectory:(id)sender; 47 | - (IBAction)setOutputFileName:(id)sender; 48 | 49 | - (IBAction)setRemapRootDirectory:(id)sender; 50 | - (IBAction)setRemapOriginalText:(id)sender; 51 | - (IBAction)setRemapReplacementText:(id)sender; 52 | - (IBAction)setRemapLocalhostPrefix:(id)sender; 53 | 54 | - (IBAction)setFlattenPlaylistHierarchy:(id)sender; 55 | - (IBAction)setIncludeInternalPlaylists:(id)sender; 56 | - (IBAction)customizePlaylists:(id)sender; 57 | 58 | - (IBAction)setScheduleEnabled:(id)sender; 59 | - (IBAction)setScheduleInterval:(id)sender; 60 | - (IBAction)incrementScheduleInterval:(id)sender; 61 | - (IBAction)setScheduleSkipOnBattery:(id)sender; 62 | 63 | - (IBAction)exportLibrary:(id)sender; 64 | 65 | - (BOOL)validateOutputDirectory:(NSURL*)outputDirectoryURL error:(NSError**)error; 66 | - (void)browseForOutputDirectoryWithCallback:(nullable void(^)(NSURL* _Nullable outputUrl))callback; 67 | - (void)browseAndValidateOutputDirectoryWithCallback:(nullable void(^)(BOOL isValid))callback; 68 | 69 | - (void)showAlertForError:(NSError*)error callback:(nullable void(^)(NSModalResponse response))callback; 70 | 71 | @end 72 | 73 | NS_ASSUME_NONNULL_END 74 | -------------------------------------------------------------------------------- /src/Common/PlaylistTree/PlaylistTreeNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistTreeNode.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-08. 6 | // 7 | 8 | #import "PlaylistTreeNode.h" 9 | 10 | #import "PlaylistSerializer.h" 11 | #import "Utils.h" 12 | 13 | 14 | @implementation PlaylistTreeNode 15 | 16 | #pragma mark - Initializers 17 | 18 | - (instancetype)init { 19 | 20 | if (self = [super init]) { 21 | 22 | _children = [NSArray array]; 23 | 24 | _customSortProperty = nil; 25 | _customSortOrder = PlaylistSortOrderNull; 26 | 27 | _playlistPersistentHexID = nil; 28 | _playlistParentPersistentHexID = nil; 29 | _playlistName = nil; 30 | _playlistDistinguishedKind = ITLibDistinguishedPlaylistKindNone; 31 | _playlistKind = ITLibPlaylistKindRegular; 32 | _playlistMaster = NO; 33 | 34 | return self; 35 | } 36 | else { 37 | return nil; 38 | } 39 | } 40 | 41 | + (PlaylistTreeNode*)nodeWithPlaylist:(nullable ITLibPlaylist*)playlist { 42 | 43 | PlaylistTreeNode* node = [[PlaylistTreeNode alloc] init]; 44 | if (playlist != nil) { 45 | node->_playlistPersistentHexID = [Utils hexStringForPersistentId:playlist.persistentID]; 46 | node->_playlistParentPersistentHexID = [Utils hexStringForPersistentId:playlist.parentID]; 47 | node->_playlistName = playlist.name; 48 | node->_playlistDistinguishedKind = playlist.distinguishedKind; 49 | node->_playlistKind = playlist.kind; 50 | node->_playlistMaster = playlist.isMaster; 51 | } 52 | else { 53 | node->_playlistPersistentHexID = nil; 54 | } 55 | 56 | return node; 57 | } 58 | 59 | + (PlaylistTreeNode*)nodeWithPlaylist:(nullable ITLibPlaylist*)playlist andChildren:(NSArray*)childNodes { 60 | 61 | PlaylistTreeNode* node = [PlaylistTreeNode nodeWithPlaylist:playlist]; 62 | [node setChildren:childNodes]; 63 | 64 | return node; 65 | } 66 | 67 | 68 | #pragma mark - Accessors 69 | 70 | - (NSString*)kindDescription { 71 | 72 | if (_playlistDistinguishedKind != ITLibDistinguishedPlaylistKindNone || _playlistMaster) { 73 | return @"Internal"; 74 | } 75 | 76 | return [PlaylistSerializer describePlaylistKind:_playlistKind]; 77 | } 78 | 79 | - (NSString*)itemsDescription { 80 | 81 | switch (_playlistKind) { 82 | case ITLibPlaylistKindFolder: { 83 | return [NSString stringWithFormat:@"%lu playlists", _children.count]; 84 | } 85 | case ITLibPlaylistKindRegular: 86 | case ITLibPlaylistKindSmart: 87 | case ITLibPlaylistKindGenius: 88 | case ITLibPlaylistKindGeniusMix: { 89 | return [NSString string]; //[NSString stringWithFormat:@"%lu songs", _playlist.items.count]; 90 | } 91 | } 92 | } 93 | 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /src/Common/Serializer/LibrarySerializer.m: -------------------------------------------------------------------------------- 1 | // 2 | // LibrarySerializer.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import "LibrarySerializer.h" 9 | 10 | #import 11 | #import 12 | 13 | #import "OrderedDictionary.h" 14 | 15 | @implementation LibrarySerializer 16 | 17 | - (instancetype)init { 18 | 19 | if (self = [super init]) { 20 | 21 | _persistentID = nil; 22 | _musicLibraryDir = nil; 23 | 24 | return self; 25 | } 26 | else { 27 | return nil; 28 | } 29 | } 30 | 31 | - (OrderedDictionary*)serializeLibrary:(ITLibrary*)library withItems:(OrderedDictionary*)items andPlaylists:(NSArray*)playlists { 32 | 33 | os_log_debug(OS_LOG_DEFAULT, "Serializing library dict - '%{public}@'. (item count: %lu, top-level playlist count: %lu)", library.musicFolderLocation, items.count, playlists.count); 34 | 35 | MutableOrderedDictionary* libraryDict = [MutableOrderedDictionary dictionary]; 36 | 37 | [libraryDict setValue:[NSNumber numberWithUnsignedInteger:library.apiMajorVersion] forKey:@"Major Version"]; 38 | [libraryDict setValue:[NSNumber numberWithUnsignedInteger:library.apiMinorVersion] forKey:@"Minor Version"]; 39 | 40 | // TODO: timezone encoding? 41 | [libraryDict setValue:[NSDate date] forKey:@"Date"]; 42 | [libraryDict setValue:library.applicationVersion forKey:@"Application Version"]; 43 | [libraryDict setValue:[NSNumber numberWithUnsignedInteger:library.features] forKey:@"Features"]; 44 | [libraryDict setValue:@(library.showContentRating) forKey:@"Show Content Ratings"]; 45 | 46 | if (_persistentID != nil) { 47 | [libraryDict setValue:_persistentID forKey:@"Library Persistent ID"]; 48 | } 49 | if (_musicLibraryDir != nil && _musicLibraryDir.length > 0) { 50 | NSString* musicFolderUrlStr = [[NSURL fileURLWithPath:_musicLibraryDir] absoluteString]; 51 | if (musicFolderUrlStr != nil) { 52 | os_log_info(OS_LOG_DEFAULT, "Setting library dict 'Music Folder' to absolute path URL '%{public}@' derived from '%{public}@'", musicFolderUrlStr, _musicLibraryDir ); 53 | [libraryDict setValue:musicFolderUrlStr forKey:@"Music Folder"]; 54 | } else { 55 | os_log_fault(OS_LOG_DEFAULT, "Derived Music folder URL is NIL despite input music library path passing included checks (path: '%{public}@')", _musicLibraryDir); 56 | } 57 | } 58 | else { 59 | os_log_info(OS_LOG_DEFAULT, "Skipping library dict 'Music Folder', Music library directory is either NULL or empty"); 60 | } 61 | 62 | // set tracks/items 63 | [libraryDict setObject:items forKey:@"Tracks"]; 64 | 65 | // set playlists 66 | [libraryDict setObject:playlists forKey:@"Playlists"]; 67 | 68 | os_log_debug(OS_LOG_DEFAULT, "Finished serializing library"); 69 | 70 | return libraryDict; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistDistinguishedKindFilter.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistDistinguishedKindFilter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistDistinguishedKindFilter.h" 9 | 10 | @implementation PlaylistDistinguishedKindFilter { 11 | 12 | NSMutableSet* _includedKinds; 13 | } 14 | 15 | - (instancetype)init { 16 | 17 | if (self = [super init]) { 18 | 19 | _includedKinds = [NSMutableSet set]; 20 | 21 | return self; 22 | } 23 | else { 24 | return nil; 25 | } 26 | } 27 | 28 | - (instancetype)initWithKinds:(NSSet*)kinds { 29 | 30 | if (self = [self init]) { 31 | 32 | _includedKinds = [kinds mutableCopy]; 33 | 34 | return self; 35 | } 36 | else { 37 | return nil; 38 | } 39 | } 40 | 41 | - (instancetype)initWithBaseKinds { 42 | 43 | NSMutableSet* baseKinds = [NSMutableSet set]; 44 | [baseKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindNone]]; 45 | 46 | return [self initWithKinds:baseKinds]; 47 | } 48 | 49 | - (instancetype)initWithInternalKinds { 50 | 51 | NSMutableSet* internalKinds = [NSMutableSet set]; 52 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindNone]]; 53 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindMusic]]; 54 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindPurchases]]; 55 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKind90sMusic]]; 56 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindMyTopRated]]; 57 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindTop25MostPlayed]]; 58 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindRecentlyPlayed]]; 59 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindRecentlyAdded]]; 60 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindClassicalMusic]]; 61 | [internalKinds addObject:[NSNumber numberWithUnsignedInteger:ITLibDistinguishedPlaylistKindLovedSongs]]; 62 | 63 | return [self initWithKinds:internalKinds]; 64 | } 65 | 66 | - (void)addKind:(ITLibDistinguishedPlaylistKind)kind { 67 | 68 | [_includedKinds addObject:[NSNumber numberWithUnsignedInteger:kind]]; 69 | } 70 | 71 | - (void)removeKind:(ITLibDistinguishedPlaylistKind)kind { 72 | 73 | [_includedKinds removeObject:[NSNumber numberWithUnsignedInteger:kind]]; 74 | } 75 | 76 | - (BOOL)filterPassesForPlaylist:(ITLibPlaylist*)playlist { 77 | 78 | return [_includedKinds containsObject:[NSNumber numberWithUnsignedInteger:playlist.distinguishedKind]]; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /src/music-library-exporter/ArgParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // ArgParser.h 3 | // music-library-exporter 4 | // 5 | // Created by Kyle King on 2021-02-15. 6 | // 7 | 8 | #import 9 | 10 | #import "CLIDefines.h" 11 | #import "Defines.h" 12 | 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @class XPMArgumentSignature; 17 | @class ExportConfiguration; 18 | 19 | 20 | @interface ArgParser : NSObject 21 | 22 | extern NSErrorDomain const __MLE_ErrorDomain_ArgParser; 23 | 24 | typedef NS_ENUM(NSUInteger, ArgParserErrorCode) { 25 | ArgParserErrorUknown = 0, 26 | ArgParserErrorInvalidCommand, 27 | ArgParserErrorInvalidOption, 28 | ArgParserErrorMissingRequiredOption, 29 | ArgParserErrorMalformedPlaylistIdOption, 30 | ArgParserErrorMalformedSortingOptionFormat, 31 | ArgParserErrorUnknownSortProperty, 32 | ArgParserErrorUnknownSortOrder, 33 | ArgParserErrorAppPrefsPropertyListInvalid, 34 | }; 35 | 36 | 37 | #pragma mark - Properties 38 | 39 | @property (readonly) NSProcessInfo* processInfo; 40 | 41 | @property (readonly) CLICommandKind command; 42 | 43 | 44 | #pragma mark - Initializers 45 | 46 | - (instancetype)init; 47 | - (instancetype)initWithProcessInfo:(NSProcessInfo*)processInfo; 48 | 49 | 50 | #pragma mark - Accessors 51 | 52 | - (nullable XPMArgumentSignature*)signatureForCommand:(CLICommandKind)command; 53 | - (nullable XPMArgumentSignature*)signatureForOption:(CLIOptionKind)option; 54 | 55 | - (BOOL)isOptionSet:(CLIOptionKind)option; 56 | 57 | - (NSSet*)determineCommandTypes; 58 | 59 | - (BOOL)populateExportConfiguration:(ExportConfiguration*)configuration error:(NSError**)error; 60 | - (BOOL)populateExportConfigurationFromAppPreferences:(ExportConfiguration*)configuration error:(NSError**)error; 61 | 62 | - (void)dumpArguments; 63 | - (void)dumpOptions; 64 | 65 | - (BOOL)readPrefsEnabled; 66 | 67 | + (nullable NSSet*)playlistIdsForIdsOption:(NSString*)playlistIdsOption error:(NSError**)error; 68 | 69 | + (BOOL)parsePlaylistSortingOption:(NSString*)sortOption forPropertyDict:(NSMutableDictionary*)sortPropertyDict andOrderDict:(NSMutableDictionary*)sortOrderDict andReturnError:(NSError**)error; 70 | + (BOOL)parsePlaylistSortingSegment:(NSString*)sortOption forPropertyDict:(NSMutableDictionary*)sortPropertyDict andOrderDict:(NSMutableDictionary*)sortOrderDict andReturnError:(NSError**)error; 71 | + (nullable NSString*)parsePlaylistSortingSegmentValue:(NSString*)sortOptionValue forOrder:(PlaylistSortOrderType*)sortOrder andReturnError:(NSError**)error; 72 | 73 | + (nullable NSString*)sortPropertyForOptionName:(NSString*)sortPropertyOption; 74 | + (PlaylistSortOrderType)sortOrderForOptionName:(NSString*)sortOrderOption; 75 | 76 | 77 | #pragma mark - Mutators 78 | 79 | - (void)initMemberSignatures; 80 | 81 | - (void)parse; 82 | 83 | - (BOOL)validateCommandAndReturnError:(NSError**)error; 84 | - (BOOL)validateOptionsAndReturnError:(NSError**)error; 85 | 86 | 87 | @end 88 | 89 | NS_ASSUME_NONNULL_END 90 | -------------------------------------------------------------------------------- /src/Config/Common/Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Base.xcconfig 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-05. 6 | // 7 | 8 | #include "Config/Common/Version.xcconfig" 9 | #include "Config/Common/Signing.xcconfig" 10 | 11 | // Architectures 12 | SDKROOT = macosx 13 | 14 | // Deployment 15 | COPY_PHASE_STRIP = NO 16 | MACOSX_DEPLOYMENT_TARGET = 10.15 17 | 18 | // Packaging 19 | PRODUCT_NAME=$(TARGET_NAME) 20 | 21 | // Search Paths 22 | ALWAYS_SEARCH_USER_PATHS = NO 23 | 24 | // Signing 25 | CODE_SIGN_ENTITLEMENTS = 26 | CODE_SIGN_IDENTITY_DEBUG = 27 | CODE_SIGN_IDENTITY_RELEASE = 28 | PROVISIONING_PROFILE_SPECIFIER_DEBUG = 29 | PROVISIONING_PROFILE_SPECIFIER_RELEASE = 30 | 31 | // Apple Clang - Code Generation 32 | GCC_DYNAMIC_NO_PIC = NO 33 | GCC_NO_COMMON_BLOCKS = YES 34 | 35 | // Apple Clang - Language 36 | GCC_C_LANGUAGE_STANDARD = gnu11 37 | 38 | // Apple Clang - Language - C++ 39 | CLANG_CXX_LANGUAGE_STANDARD = gnu++14 40 | CLANG_CXX_LIBRARY = libc++ 41 | 42 | // Apple Clang - Language - Modules 43 | CLANG_ENABLE_MODULES = YES 44 | 45 | // Apple Clang - Language - Objective-C 46 | CLANG_ENABLE_OBJC_ARC = YES 47 | CLANG_ENABLE_OBJC_WEAK = YES 48 | 49 | // Apple Clang - Preprocessing 50 | ENABLE_STRICT_OBJC_MSGSEND = YES 51 | 52 | // Apple Clang - Warnings - All languages 53 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 54 | CLANG_WARN_BOOL_CONVERSION = YES 55 | CLANG_WARN_COMMA = YES 56 | CLANG_WARN_CONSTANT_CONVERSION = YES 57 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 58 | CLANG_WARN_EMPTY_BODY = YES 59 | CLANG_WARN_ENUM_CONVERSION = YES 60 | CLANG_WARN_INFINITE_RECURSION = YES 61 | CLANG_WARN_INT_CONVERSION = YES 62 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 63 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES 64 | CLANG_WARN_STRICT_PROTOTYPES = YES 65 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 66 | CLANG_WARN_UNREACHABLE_CODE = YES 67 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 68 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 69 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 70 | GCC_WARN_UNUSED_FUNCTION = YES 71 | GCC_WARN_UNUSED_VARIABLE = YES 72 | 73 | // Apple Clang - Warnings - C++ 74 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 75 | CLANG_WARN_SUSPICIOUS_MOVE = YES 76 | 77 | // Apple Clang - Warnings - Objective-C 78 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 79 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 80 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 81 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 82 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 83 | GCC_WARN_UNDECLARED_SELECTOR = YES 84 | 85 | // Apple Clang - Warnings - Objective-C and ARC 86 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 87 | 88 | // Static Analyzer - Generic Issues 89 | CLANG_ANALYZER_NONNULL = YES 90 | 91 | // Static Analyzer - Issues - Apple APIs 92 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 93 | 94 | // User-Defined 95 | OUTPUT_DIRECTORY_BOOKMARK_KEY = 'OutputDirectoryBookmark' 96 | MTL_FAST_MATH = YES 97 | GCC_PREPROCESSOR_DEFINITIONS_BASE = $(MLE_LOG_LEVEL) SENTRY_ENABLED=$(SENTRY_ENABLED) SENTRY_ENVIRONMENT='@"$(SENTRY_ENVIRONMENT)"' VERSION_BUILD='$(VERSION_BUILD)' CURRENT_PROJECT_VERSION='@"$(CURRENT_PROJECT_VERSION)"' 98 | 99 | -------------------------------------------------------------------------------- /src/Music Library Exporter/HelperAppManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // HelperAppManager.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import "HelperAppManager.h" 9 | 10 | #import 11 | 12 | #import "Logger.h" 13 | #import "Defines.h" 14 | 15 | 16 | @implementation HelperAppManager 17 | 18 | 19 | #pragma mark - Initializers 20 | 21 | - (instancetype)init { 22 | 23 | if (self = [super init]) { 24 | 25 | return self; 26 | } 27 | else { 28 | return nil; 29 | } 30 | } 31 | 32 | 33 | #pragma mark - Accessors 34 | 35 | - (BOOL)isHelperRegisteredWithSystem { 36 | 37 | // source: http://blog.mcohen.me/2012/01/12/login-items-in-the-sandbox/ 38 | // > As of WWDC 2017, Apple engineers have stated that [SMCopyAllJobDictionaries] is still the preferred API to use. 39 | // ref: https://github.com/alexzielenski/StartAtLoginController/issues/12#issuecomment-307525807 40 | 41 | #pragma clang diagnostic push 42 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 43 | CFArrayRef cfJobDictsArr = SMCopyAllJobDictionaries(kSMDomainUserLaunchd); 44 | #pragma pop 45 | NSArray* jobDictsArr = CFBridgingRelease(cfJobDictsArr); 46 | 47 | if (jobDictsArr && jobDictsArr.count > 0) { 48 | 49 | for (NSDictionary* jobDict in jobDictsArr) { 50 | 51 | if ([__MLE__HelperBundleIdentifier isEqualToString:[jobDict objectForKey:@"Label"]]) { 52 | return [[jobDict objectForKey:@"OnDemand"] boolValue]; 53 | } 54 | } 55 | } 56 | 57 | return NO; 58 | } 59 | 60 | - (NSString*)errorForHelperRegistration:(BOOL)registerFlag { 61 | 62 | if (registerFlag) { 63 | return @"Couldn't add Music Library Exporter Helper to launch at login item list."; 64 | } 65 | else { 66 | return @"Couldn't remove Music Library Exporter Helper from launch at login item list."; 67 | } 68 | } 69 | 70 | 71 | #pragma mark - Mutators 72 | 73 | - (BOOL)registerHelperWithSystem:(BOOL)flag { 74 | 75 | MLE_Log_Info(@"HelperAppManager [registerHelperWithSystem:%@]", (flag ? @"YES" : @"NO")); 76 | 77 | BOOL success = SMLoginItemSetEnabled ((__bridge CFStringRef)__MLE__HelperBundleIdentifier, flag); 78 | 79 | if (success) { 80 | MLE_Log_Info(@"HelperAppManager [registerHelperWithSystem] succesfully %@ helper", (flag ? @"registered" : @"unregistered")); 81 | } 82 | else { 83 | MLE_Log_Info(@"HelperAppManager [registerHelperWithSystem] failed to %@ helper", (flag ? @"register" : @"unregister")); 84 | } 85 | 86 | return success; 87 | } 88 | 89 | - (void)updateHelperRegistrationWithScheduleEnabled:(BOOL)scheduleEnabled { 90 | 91 | #if HELPER_REGISTRATION_ENABLED == 1 92 | MLE_Log_Info(@"HelperAppManager [updateHelperRegistrationWithScheduleEnabled:%@]", (scheduleEnabled ? @"YES" : @"NO")); 93 | 94 | BOOL shouldUpdate = (scheduleEnabled != [self isHelperRegisteredWithSystem]); 95 | if (shouldUpdate) { 96 | MLE_Log_Info(@"HelperAppManager [updateHelperRegistrationIfRequired] updating registration to: %@", (scheduleEnabled ? @"registered" : @"unregistered")); 97 | [self registerHelperWithSystem:scheduleEnabled]; 98 | } 99 | #endif 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/xcshareddata/xcschemes/Music Library Exporter Helper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/Common/Filter/Playlist/PlaylistFilterGroup.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistFilterGroup.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-10-31. 6 | // 7 | 8 | #import "PlaylistFilterGroup.h" 9 | 10 | #import "PlaylistKindFilter.h" 11 | #import "PlaylistDistinguishedKindFilter.h" 12 | #import "PlaylistMasterFilter.h" 13 | #import "PlaylistIDFilter.h" 14 | #import "PlaylistParentIDFilter.h" 15 | 16 | @implementation PlaylistFilterGroup { 17 | 18 | NSMutableArray*>* _filters; 19 | } 20 | 21 | - (instancetype)init { 22 | 23 | if (self = [super init]) { 24 | 25 | _filters = [NSMutableArray array]; 26 | 27 | return self; 28 | } 29 | else { 30 | return nil; 31 | } 32 | } 33 | 34 | - (instancetype)initWithFilters:(NSArray*>*)filters { 35 | 36 | if (self = [self init]) { 37 | 38 | _filters = [filters mutableCopy]; 39 | 40 | return self; 41 | } 42 | else { 43 | return nil; 44 | } 45 | } 46 | 47 | - (instancetype)initWithBaseFiltersAndIncludeInternal:(BOOL)includeInternal andFlattenPlaylists:(BOOL)flatten { 48 | 49 | if (self = [self init]) { 50 | 51 | _filters = [NSMutableArray array]; 52 | 53 | // include internal 54 | if (includeInternal) { 55 | [self addFilter:[[PlaylistDistinguishedKindFilter alloc] initWithInternalKinds]]; 56 | } 57 | // exclude internal 58 | else { 59 | [self addFilter:[[PlaylistDistinguishedKindFilter alloc] initWithBaseKinds]]; 60 | [self addFilter:[[PlaylistMasterFilter alloc] init]]; 61 | } 62 | 63 | PlaylistKindFilter* playlistKindFilter = [[PlaylistKindFilter alloc] initWithBaseKinds]; 64 | // exclude folders 65 | if (!flatten) { 66 | [playlistKindFilter addKind:ITLibPlaylistKindFolder]; 67 | } 68 | [self addFilter:playlistKindFilter]; 69 | 70 | return self; 71 | } 72 | else { 73 | return nil; 74 | } 75 | } 76 | 77 | - (NSArray*>*)filters { 78 | 79 | return _filters; 80 | } 81 | 82 | - (void)setFilters:(NSArray*>*)filters { 83 | 84 | _filters = [filters mutableCopy]; 85 | } 86 | 87 | - (void)addFilter:(NSObject*)filter { 88 | 89 | NSAssert(![_filters containsObject:filter], @"PlaylistFilterGroup already contains specified filter"); 90 | 91 | [_filters addObject:filter]; 92 | } 93 | 94 | - (void)removeFilter:(NSObject*)filter { 95 | 96 | NSAssert([_filters containsObject:filter], @"PlaylistFilterGroup does not contain specified filter"); 97 | 98 | [_filters removeObject:filter]; 99 | } 100 | 101 | - (nullable PlaylistParentIDFilter*)addFiltersForExcludedIDs:(NSSet*)excludedIDs andFlattenPlaylists:(BOOL)flatten { 102 | 103 | PlaylistParentIDFilter* parentIDFilter = nil; 104 | 105 | // manually excluded playlists 106 | PlaylistIDFilter* playlistIDFilter = [[PlaylistIDFilter alloc] initWithExcludedIDs:excludedIDs]; 107 | [self addFilter:playlistIDFilter]; 108 | 109 | // exclude parent folders that have been manually excluded 110 | if (!flatten) { 111 | parentIDFilter = [[PlaylistParentIDFilter alloc] initWithExcludedIDs:excludedIDs]; 112 | [self addFilter:parentIDFilter]; 113 | } 114 | 115 | return parentIDFilter; 116 | } 117 | 118 | - (BOOL)filtersPassForPlaylist:(ITLibPlaylist*)playlist { 119 | 120 | for (NSObject* filter in _filters) { 121 | if (![filter filterPassesForPlaylist:playlist]) { 122 | 123 | return NO; 124 | } 125 | } 126 | 127 | return YES; 128 | } 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /src/Common/PlaylistTree/PlaylistTreeGenerator.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistTreeGenerator.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-13. 6 | // 7 | 8 | #import "PlaylistTreeGenerator.h" 9 | 10 | #import 11 | #import 12 | 13 | #import "PlaylistFilterGroup.h" 14 | #import "PlaylistTreeNode.h" 15 | #import "Utils.h" 16 | 17 | @implementation PlaylistTreeGenerator 18 | 19 | - (instancetype)init { 20 | 21 | if (self = [super init]) { 22 | 23 | _filters = nil; 24 | _flattenFolders = NO; 25 | 26 | _customSortProperties = [NSDictionary dictionary]; 27 | _customSortOrders = [NSDictionary dictionary]; 28 | 29 | return self; 30 | } 31 | else { 32 | return nil; 33 | } 34 | } 35 | 36 | - (instancetype)initWithFilters:(PlaylistFilterGroup*)filters { 37 | 38 | if (self = [self init]) { 39 | 40 | _filters = filters; 41 | 42 | return self; 43 | } 44 | else { 45 | return nil; 46 | } 47 | } 48 | 49 | - (nullable PlaylistTreeNode*)generateTreeWithError:(NSError**)error { 50 | 51 | PlaylistTreeNode* root = [[PlaylistTreeNode alloc] init]; 52 | 53 | // init ITLibrary 54 | ITLibrary* library = [ITLibrary libraryWithAPIVersion:@"1.1" options:ITLibInitOptionNone error:error]; 55 | 56 | if (library != nil) { 57 | 58 | NSMutableArray* topLevelPlaylists = [NSMutableArray array]; 59 | 60 | for (ITLibPlaylist* playlist in library.allPlaylists) { 61 | 62 | if ([_filters filtersPassForPlaylist:playlist]) { 63 | 64 | // additional filter to only generate top level playlists when folders are retained 65 | if (_flattenFolders || playlist.parentID == nil) { 66 | 67 | [topLevelPlaylists addObject:[self createNodeForPlaylist:playlist fromSourcePlaylists:library.allPlaylists]]; 68 | } 69 | } 70 | } 71 | 72 | [root setChildren:topLevelPlaylists]; 73 | } 74 | 75 | return root; 76 | } 77 | 78 | - (PlaylistTreeNode*)createNodeForPlaylist:(ITLibPlaylist*)playlist fromSourcePlaylists:(NSArray*)sourcePlaylists{ 79 | 80 | PlaylistTreeNode* node = [PlaylistTreeNode nodeWithPlaylist:playlist]; 81 | 82 | NSString* playlistHexID = [Utils hexStringForPersistentId:playlist.persistentID]; 83 | 84 | // set custom sort property 85 | NSString* sortProperty = [_customSortProperties valueForKey:playlistHexID]; 86 | [node setCustomSortProperty:sortProperty]; 87 | 88 | // set custom sort order 89 | NSString* sortOrderTitle = [_customSortOrders valueForKey:playlistHexID]; 90 | PlaylistSortOrderType sortOrder = [Utils playlistSortOrderForTitle:sortOrderTitle]; 91 | [node setCustomSortOrder:sortOrder]; 92 | 93 | // generate children if folders are enabled 94 | if (!_flattenFolders) { 95 | [node setChildren:[self generateChildrenForPlaylist:playlist fromSourcePlaylists:sourcePlaylists]]; 96 | } 97 | 98 | return node; 99 | } 100 | 101 | - (NSArray*)generateChildrenForPlaylist:(ITLibPlaylist*)playlist fromSourcePlaylists:(NSArray*)sourcePlaylists{ 102 | 103 | NSMutableArray* children = [NSMutableArray array]; 104 | 105 | if (playlist.kind == ITLibPlaylistKindFolder) { 106 | 107 | for (ITLibPlaylist* sourcePlaylist in sourcePlaylists) { 108 | 109 | // sourcePlaylist is a child of the provided playlist 110 | if (sourcePlaylist.parentID != nil && [sourcePlaylist.parentID isEqualToNumber:playlist.persistentID]) { 111 | 112 | // generate child 113 | [children addObject:[self createNodeForPlaylist:sourcePlaylist fromSourcePlaylists:sourcePlaylists]]; 114 | } 115 | } 116 | } 117 | 118 | return children; 119 | } 120 | 121 | @end 122 | -------------------------------------------------------------------------------- /src/Common/Configuration/ExportConfiguration.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExportConfiguration.h 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-01-29. 6 | // 7 | 8 | #import 9 | 10 | #include "Defines.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface ExportConfiguration : NSObject 16 | 17 | 18 | #pragma mark - Initializers 19 | 20 | - (instancetype)init; 21 | 22 | 23 | #pragma mark - Accessors 24 | 25 | - (NSString*)musicLibraryPath; 26 | 27 | - (NSString*)generatedPersistentLibraryId; 28 | 29 | - (nullable NSURL*)outputDirectoryUrl; 30 | - (NSString*)outputDirectoryUrlAsPath; 31 | - (NSString*)outputDirectoryPath; 32 | - (BOOL)isOutputDirectoryValid; 33 | 34 | - (NSString*)outputFileName; 35 | 36 | - (nullable NSURL*)outputFileUrl; 37 | 38 | - (BOOL)remapRootDirectory; 39 | - (NSString*)remapRootDirectoryOriginalPath; 40 | - (NSString*)remapRootDirectoryMappedPath; 41 | - (BOOL)remapRootDirectoryLocalhostPrefix; 42 | 43 | - (BOOL)flattenPlaylistHierarchy; 44 | - (BOOL)includeInternalPlaylists; 45 | - (NSSet*)excludedPlaylistPersistentIds; 46 | - (BOOL)isPlaylistIdExcluded:(NSString*)playlistId; 47 | 48 | - (NSDictionary*)playlistCustomSortPropertyDict; 49 | - (NSDictionary*)playlistCustomSortOrderDict; 50 | 51 | + (NSString*)generatePersistentLibraryId; 52 | 53 | - (void)dumpProperties; 54 | 55 | 56 | #pragma mark - Mutators 57 | 58 | - (void)setGeneratedPersistentLibraryId:(NSString*)generatedPersistentLibraryId; 59 | 60 | - (void)setMusicLibraryPath:(NSString*)musicLibraryPath; 61 | 62 | - (void)setOutputDirectoryPath:(nullable NSString*)dirPath; 63 | - (void)setOutputDirectoryUrl:(nullable NSURL*)dirUrl; 64 | - (void)setOutputFileName:(NSString*)fileName; 65 | 66 | - (void)setRemapRootDirectory:(BOOL)flag; 67 | - (void)setRemapRootDirectoryOriginalPath:(NSString*)originalPath; 68 | - (void)setRemapRootDirectoryMappedPath:(NSString*)mappedPath; 69 | - (void)setRemapRootDirectoryLocalhostPrefix:(BOOL)flag; 70 | 71 | - (void)setFlattenPlaylistHierarchy:(BOOL)flag; 72 | - (void)setIncludeInternalPlaylists:(BOOL)flag; 73 | 74 | - (void)setExcludedPlaylistPersistentIds:(NSSet*)excludedIds; 75 | - (void)addExcludedPlaylistPersistentId:(NSString*)playlistId; 76 | - (void)removeExcludedPlaylistPersistentId:(NSString*)playlistId; 77 | - (void)setExcluded:(BOOL)excluded forPlaylistId:(NSString*)playlistId; 78 | 79 | - (void)setCustomSortPropertyDict:(NSDictionary*)dict; 80 | - (void)setCustomSortOrderDict:(NSDictionary*)dict; 81 | 82 | - (void)setDefaultSortingForPlaylist:(NSString*)playlistId; 83 | - (void)setCustomSortProperty:(nullable NSString*)sortProperty forPlaylist:(NSString*)playlistId; 84 | - (void)setCustomSortOrder:(PlaylistSortOrderType)sortOrder forPlaylist:(NSString*)playlistId; 85 | 86 | - (void)loadValuesFromDictionary:(NSDictionary*)dict; 87 | 88 | @end 89 | 90 | extern NSString* const ExportConfigurationKeyMusicLibraryPath; 91 | extern NSString* const ExportConfigurationKeyGeneratedPersistentLibraryId; 92 | extern NSString* const ExportConfigurationKeyOutputDirectoryPath; 93 | extern NSString* const ExportConfigurationKeyOutputFileName; 94 | extern NSString* const ExportConfigurationKeyRemapRootDirectory; 95 | extern NSString* const ExportConfigurationKeyRemapRootDirectoryOriginalPath; 96 | extern NSString* const ExportConfigurationKeyRemapRootDirectoryMappedPath; 97 | extern NSString* const ExportConfigurationKeyRemapRootDirectoryLocalhostPrefix; 98 | extern NSString* const ExportConfigurationKeyFlattenPlaylistHierarchy; 99 | extern NSString* const ExportConfigurationKeyIncludeInternalPlaylists; 100 | extern NSString* const ExportConfigurationKeyExcludedPlaylistPersistentIds; 101 | extern NSString* const ExportConfigurationKeyPlaylistCustomSortProperties; 102 | extern NSString* const ExportConfigurationKeyPlaylistCustomSortOrders; 103 | 104 | NS_ASSUME_NONNULL_END 105 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/HelperAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // HelperAppDelegate.m 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-01-26. 6 | // 7 | 8 | #import "HelperAppDelegate.h" 9 | 10 | #import "Logger.h" 11 | #import "UserDefaultsExportConfiguration.h" 12 | #import "ScheduleConfiguration.h" 13 | #import "DirectoryBookmarkHandler.h" 14 | #import "ExportScheduler.h" 15 | #if SENTRY_ENABLED == 1 16 | #import "SentryHandler.h" 17 | #endif 18 | 19 | @implementation HelperAppDelegate { 20 | 21 | NSUserDefaults* _groupDefaults; 22 | 23 | UserDefaultsExportConfiguration* _exportConfiguration; 24 | 25 | ScheduleConfiguration* _scheduleConfiguration; 26 | ExportScheduler* _exportScheduler; 27 | } 28 | 29 | 30 | #pragma mark - Initializers 31 | 32 | - (instancetype)init { 33 | 34 | if (self = [super init]) { 35 | 36 | // detect changes in NSUSerDefaults for app group 37 | _groupDefaults = [[NSUserDefaults alloc] initWithSuiteName:__MLE__AppGroupIdentifier]; 38 | [_groupDefaults addObserver:self forKeyPath:ScheduleConfigurationKeyScheduleEnabled options:NSKeyValueObservingOptionNew context:NULL]; 39 | [_groupDefaults addObserver:self forKeyPath:ScheduleConfigurationKeyScheduleInterval options:NSKeyValueObservingOptionNew context:NULL]; 40 | [_groupDefaults addObserver:self forKeyPath:ScheduleConfigurationKeyLastExportedAt options:NSKeyValueObservingOptionNew context:NULL]; 41 | [_groupDefaults addObserver:self forKeyPath:ExportConfigurationKeyOutputDirectoryPath options:NSKeyValueObservingOptionNew context:NULL]; 42 | 43 | _exportConfiguration = nil; 44 | 45 | _scheduleConfiguration = nil; 46 | _exportScheduler = nil; 47 | 48 | return self; 49 | } 50 | else { 51 | return nil; 52 | } 53 | } 54 | 55 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 56 | 57 | #if SENTRY_ENABLED == 1 58 | [[SentryHandler sharedSentryHandler] setupSentry]; 59 | #endif 60 | 61 | // init exportConfiguration 62 | _exportConfiguration = [[UserDefaultsExportConfiguration alloc] initWithOutputDirectoryBookmarkKey:OUTPUT_DIRECTORY_BOOKMARK_KEY]; 63 | [_exportConfiguration loadPropertiesFromUserDefaults]; 64 | 65 | // resolve output directory bookmark data 66 | DirectoryBookmarkHandler* bookmarkHandler = [[DirectoryBookmarkHandler alloc] initWithUserDefaultsKey:OUTPUT_DIRECTORY_BOOKMARK_KEY]; 67 | [_exportConfiguration setOutputDirectoryUrl:[bookmarkHandler urlFromDefaultsAndReturnError:nil]]; 68 | 69 | // init scheduleConfiguration 70 | _scheduleConfiguration = [[ScheduleConfiguration alloc] init]; 71 | [_scheduleConfiguration loadPropertiesFromUserDefaults]; 72 | 73 | // init scheduleDelegate 74 | _exportScheduler = [[ExportScheduler alloc] initWithExportConfiguration:_exportConfiguration andScheduleConfiguration:_scheduleConfiguration]; 75 | } 76 | 77 | - (void)applicationWillTerminate:(NSNotification *)aNotification { 78 | 79 | [_exportScheduler deactivateScheduler]; 80 | 81 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 82 | } 83 | 84 | - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)anObject change:(NSDictionary*)aChange context:(void*)aContext { 85 | 86 | MLE_Log_Info(@"HelperAppDelegate [observeValueForKeyPath:%@]", keyPath); 87 | 88 | if ([keyPath isEqualToString:ScheduleConfigurationKeyScheduleEnabled] || 89 | [keyPath isEqualToString:ScheduleConfigurationKeyScheduleInterval] || 90 | [keyPath isEqualToString:ScheduleConfigurationKeyLastExportedAt] || 91 | [keyPath isEqualToString:ExportConfigurationKeyOutputDirectoryPath]) { 92 | 93 | // fetch latest configuration values 94 | [_scheduleConfiguration loadPropertiesFromUserDefaults]; 95 | [_exportConfiguration loadPropertiesFromUserDefaults]; 96 | 97 | [_exportScheduler requestOutputDirectoryPermissionsIfRequired]; 98 | [_exportScheduler updateSchedule]; 99 | } 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/xcshareddata/xcschemes/Music Library Exporter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 80 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/Common/Configuration/DirectoryBookmarkHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryBookmarkHandler.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-15. 6 | // 7 | 8 | #import "DirectoryBookmarkHandler.h" 9 | 10 | #import "Defines.h" 11 | #import "Logger.h" 12 | 13 | @implementation DirectoryBookmarkHandler { 14 | 15 | NSString* _defaultsKey; 16 | 17 | NSUserDefaults* _userDefaults; 18 | } 19 | 20 | #pragma mark - Initializers 21 | 22 | - (instancetype)init { 23 | 24 | if (self = [super init]) { 25 | 26 | _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:__MLE__AppGroupIdentifier]; 27 | 28 | return self; 29 | } 30 | else { 31 | return nil; 32 | } 33 | } 34 | 35 | - (instancetype)initWithUserDefaultsKey:(NSString*)defaultsKey { 36 | 37 | if (self = [self init]) { 38 | 39 | _defaultsKey = defaultsKey; 40 | 41 | return self; 42 | } 43 | else { 44 | return nil; 45 | } 46 | } 47 | 48 | #pragma mark - Accessors 49 | 50 | - (nullable NSData*)bookmarkDataFromDefaults { 51 | 52 | if (_defaultsKey == nil || _defaultsKey.length == 0) { 53 | return nil; 54 | } 55 | 56 | return [_userDefaults dataForKey:_defaultsKey]; 57 | } 58 | 59 | - (nullable NSURL*)urlFromDefaultsAndReturnError:(NSError**)error { 60 | 61 | NSData* bookmarkData = [self bookmarkDataFromDefaults]; 62 | 63 | // no bookmark has been saved yet 64 | if (bookmarkData == nil) { 65 | MLE_Log_Info(@"DirectoryBookmarkHandler [urlFromDefaults] bookmark is nil"); 66 | return nil; 67 | } 68 | 69 | // resolve URL for bookmark data 70 | BOOL bookmarkDataIsStale; 71 | NSURL* bookmarkURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:error]; 72 | 73 | // error resolving bookmark data 74 | if (bookmarkURL == nil) { 75 | if (error) { 76 | MLE_Log_Info(@"DirectoryBookmarkHandler [urlFromDefaults] error resolving output dir bookmark: %@", [*error localizedDescription]); 77 | } 78 | return nil; 79 | } 80 | 81 | // bookmark data is stale, regenerate and save bookmark data 82 | if (bookmarkDataIsStale) { 83 | MLE_Log_Info(@"DirectoryBookmarkHandler [urlFromDefaults] bookmark is stale, saving new bookmark"); 84 | [self saveURLToDefaults:bookmarkURL]; 85 | } 86 | 87 | MLE_Log_Info(@"DirectoryBookmarkHandler [urlFromDefaults] bookmarked directory: %@", bookmarkURL.path); 88 | 89 | return bookmarkURL; 90 | } 91 | 92 | - (nullable NSURL*)urlFromDefaultsWithFilename:(NSString*)filename andReturnError:(NSError**)error { 93 | 94 | if (filename.length == 0) { 95 | return nil; 96 | } 97 | 98 | NSURL* directoryURL = [self urlFromDefaultsAndReturnError:error]; 99 | if (directoryURL == nil) { 100 | return nil; 101 | } 102 | return [directoryURL URLByAppendingPathComponent:filename]; 103 | } 104 | 105 | #pragma mark - Mutators 106 | 107 | - (void)saveBookmarkDataToDefaults:(nullable NSData*)bookmarkData { 108 | 109 | if (_defaultsKey == nil || _defaultsKey.length == 0) { 110 | return; 111 | } 112 | MLE_Log_Info(@"DirectoryBookmarkHandler [saveBookmarkDataToDefaults]"); 113 | 114 | // data is nil, remove the value from user defaults 115 | [_userDefaults setValue:bookmarkData forKey:_defaultsKey]; 116 | } 117 | 118 | - (BOOL)saveURLToDefaults:(nullable NSURL*)url { 119 | 120 | if (_defaultsKey == nil || _defaultsKey.length == 0) { 121 | return YES; 122 | } 123 | MLE_Log_Info(@"DirectoryBookmarkHandler [saveURLToDefaults: %@]", url); 124 | 125 | // URL is nil, remove the value from user defaults 126 | if (url == nil) { 127 | [_userDefaults removeObjectForKey:_defaultsKey]; 128 | return YES; 129 | } 130 | 131 | /* ---- scoped security access started ---- */ 132 | [url startAccessingSecurityScopedResource]; 133 | 134 | // create new bookmark 135 | NSError* bookmarkCreateError; 136 | NSData* bookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&bookmarkCreateError]; 137 | 138 | [url stopAccessingSecurityScopedResource]; 139 | /* ---- scoped security access stopped ---- */ 140 | 141 | // error generating bookmark 142 | if (bookmarkCreateError) { 143 | MLE_Log_Info(@"DirectoryBookmarkHandler [saveURLToDefaults] error generating bookmark data: %@", bookmarkCreateError.localizedDescription); 144 | return NO; 145 | } 146 | 147 | // save bookmark data to user defaults 148 | if (bookmarkData != nil) { 149 | [self saveBookmarkDataToDefaults:bookmarkData]; 150 | } 151 | // error generating bookmark 152 | else { 153 | MLE_Log_Info(@"DirectoryBookmarkHandler [saveURLToDefaults] error generating bookmark data: %@", bookmarkCreateError.localizedDescription); 154 | } 155 | 156 | return bookmarkData != nil; 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /src/Music Library Exporter/PreferencesWindow/PreferencesWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Common/Configuration/ScheduleConfiguration.m: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduleConfiguration.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-02. 6 | // 7 | 8 | #import "ScheduleConfiguration.h" 9 | 10 | #import 11 | 12 | #import "Logger.h" 13 | #import "Defines.h" 14 | 15 | @implementation ScheduleConfiguration { 16 | 17 | NSUserDefaults* _userDefaults; 18 | 19 | BOOL _scheduleEnabled; 20 | NSTimeInterval _scheduleInterval; 21 | 22 | NSDate* _lastExportedAt; 23 | NSDate* _nextExportAt; 24 | 25 | BOOL _skipOnBattery; 26 | } 27 | 28 | 29 | #pragma mark - Initializers 30 | 31 | - (instancetype)init { 32 | 33 | if (self = [super init]) { 34 | 35 | _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:__MLE__AppGroupIdentifier]; 36 | 37 | return self; 38 | } 39 | else { 40 | return nil; 41 | } 42 | } 43 | 44 | 45 | #pragma mark - Accessors 46 | 47 | - (NSDictionary*)defaultValues { 48 | 49 | return [NSDictionary dictionaryWithObjectsAndKeys: 50 | @NO, ScheduleConfigurationKeyScheduleEnabled, 51 | @3600, ScheduleConfigurationKeyScheduleInterval, 52 | // nil, ScheduleConfigurationKeyLastExportedAt, 53 | // nil, ScheduleConfigurationKeyNextExportAt, 54 | @NO, ScheduleConfigurationKeySkipOnBattery, 55 | nil 56 | ]; 57 | } 58 | 59 | - (BOOL)scheduleEnabled { 60 | 61 | return _scheduleEnabled; 62 | } 63 | 64 | - (NSTimeInterval)scheduleInterval { 65 | 66 | return _scheduleInterval; 67 | } 68 | 69 | - (nullable NSDate*)lastExportedAt { 70 | 71 | return _lastExportedAt; 72 | } 73 | 74 | - (nullable NSDate*)nextExportAt { 75 | 76 | return _nextExportAt; 77 | } 78 | 79 | - (BOOL)skipOnBattery { 80 | 81 | return _skipOnBattery; 82 | } 83 | 84 | - (void)dumpProperties { 85 | 86 | MLE_Log_Info(@"ScheduleConfiguration [dumpProperties]"); 87 | 88 | MLE_Log_Info(@" ScheduleEnabled: '%@'", (_scheduleEnabled ? @"YES" : @"NO")); 89 | MLE_Log_Info(@" ScheduleInterval: '%f'", _scheduleInterval); 90 | MLE_Log_Info(@" LastExportedAt: '%@'", _lastExportedAt.description); 91 | MLE_Log_Info(@" NextExportAt: '%@'", _nextExportAt.description); 92 | MLE_Log_Info(@" SkipOnBattery: '%@'", (_skipOnBattery ? @"YES" : @"NO")); 93 | } 94 | 95 | 96 | #pragma mark - Mutators 97 | 98 | - (void)loadPropertiesFromUserDefaults { 99 | 100 | // register default values for properties 101 | [_userDefaults registerDefaults:[self defaultValues]]; 102 | 103 | // read user defaults 104 | _scheduleEnabled = [_userDefaults boolForKey:ScheduleConfigurationKeyScheduleEnabled]; 105 | _scheduleInterval = [_userDefaults doubleForKey:ScheduleConfigurationKeyScheduleInterval]; 106 | 107 | _lastExportedAt = [_userDefaults valueForKey:ScheduleConfigurationKeyLastExportedAt]; 108 | _nextExportAt = [_userDefaults valueForKey:ScheduleConfigurationKeyNextExportAt]; 109 | 110 | _skipOnBattery = [_userDefaults boolForKey:ScheduleConfigurationKeySkipOnBattery]; 111 | } 112 | 113 | - (void)setScheduleEnabled:(BOOL)flag { 114 | 115 | MLE_Log_Info(@"ScheduleConfiguration [setScheduleEnabled:%@]", (flag ? @"YES" : @"NO")); 116 | 117 | _scheduleEnabled = flag; 118 | 119 | [_userDefaults setBool:_scheduleEnabled forKey:ScheduleConfigurationKeyScheduleEnabled]; 120 | } 121 | 122 | - (void)setScheduleInterval:(NSTimeInterval)interval { 123 | 124 | MLE_Log_Info(@"ScheduleConfiguration [setScheduleInterval:%ld]", (long)interval); 125 | 126 | if (_scheduleInterval != interval) { 127 | 128 | _scheduleInterval = interval; 129 | 130 | [_userDefaults setDouble:_scheduleInterval forKey:ScheduleConfigurationKeyScheduleInterval]; 131 | } 132 | } 133 | 134 | - (void)setLastExportedAt:(nullable NSDate*)timestamp { 135 | 136 | MLE_Log_Info(@"ScheduleConfiguration [setLastExportedAt:%@]", timestamp.description); 137 | 138 | if (_lastExportedAt != timestamp) { 139 | 140 | _lastExportedAt = timestamp; 141 | 142 | [_userDefaults setValue:_lastExportedAt forKey:ScheduleConfigurationKeyLastExportedAt]; 143 | } 144 | } 145 | 146 | - (void)setNextExportAt:(nullable NSDate*)timestamp { 147 | 148 | MLE_Log_Info(@"ScheduleConfiguration [setNextExportAt:%@]", timestamp.description); 149 | 150 | if (_nextExportAt != timestamp) { 151 | 152 | _nextExportAt = timestamp; 153 | 154 | [_userDefaults setValue:_nextExportAt forKey:ScheduleConfigurationKeyNextExportAt]; 155 | } 156 | } 157 | 158 | - (void)setSkipOnBattery:(BOOL)flag { 159 | 160 | MLE_Log_Info(@"ScheduleConfiguration [setSkipOnBattery:%@]", (flag ? @"YES" : @"NO")); 161 | 162 | _skipOnBattery = flag; 163 | 164 | [_userDefaults setBool:_skipOnBattery forKey:ScheduleConfigurationKeySkipOnBattery]; 165 | } 166 | 167 | @end 168 | 169 | NSString* const ScheduleConfigurationKeyScheduleEnabled = @"ScheduleEnabled"; 170 | NSString* const ScheduleConfigurationKeyScheduleInterval = @"ScheduleInterval"; 171 | NSString* const ScheduleConfigurationKeyLastExportedAt = @"LastExportedAt"; 172 | NSString* const ScheduleConfigurationKeyNextExportAt = @"NextExportAt"; 173 | NSString* const ScheduleConfigurationKeySkipOnBattery = @"SkipOnBattery"; 174 | -------------------------------------------------------------------------------- /src/Common/SentryHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // SentryHandler.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-22. 6 | // 7 | 8 | #import "SentryHandler.h" 9 | 10 | #include "Defines.h" 11 | #include "Logger.h" 12 | 13 | @import Sentry; 14 | 15 | 16 | static SentryHandler* _sharedSentryHandler; 17 | 18 | 19 | @interface SentryHandler () 20 | 21 | @property NSUserDefaults* groupDefaults; 22 | 23 | + (NSString*)crashReportingDefaultsKey; 24 | + (NSString*)promptedForPermissionsDefaultsKey; 25 | 26 | - (nullable NSString*)sentryDsn; 27 | - (nullable NSString*)sentryEnvironment; 28 | - (nullable NSString*)sentryReleaseName; 29 | 30 | - (void)setUserHasEnabledCrashReporting:(BOOL)flag; 31 | 32 | @end 33 | 34 | 35 | @implementation SentryHandler 36 | 37 | #pragma mark - Initializers 38 | 39 | - (instancetype)init { 40 | 41 | if (self = [super init]) { 42 | 43 | NSAssert((_sharedSentryHandler == nil), @"SentryHandler sharedSentryHandler has already been initialized"); 44 | 45 | _groupDefaults = [[NSUserDefaults alloc] initWithSuiteName:__MLE__AppGroupIdentifier]; 46 | [_groupDefaults registerDefaults:@{ [SentryHandler crashReportingDefaultsKey]:@NO }]; 47 | [_groupDefaults registerDefaults:@{ [SentryHandler promptedForPermissionsDefaultsKey]:@NO }]; 48 | [_groupDefaults addObserver:self forKeyPath:[SentryHandler crashReportingDefaultsKey] options:NSKeyValueObservingOptionNew context:NULL]; 49 | 50 | return self; 51 | } 52 | else { 53 | return nil; 54 | } 55 | } 56 | 57 | 58 | #pragma mark - Accessors 59 | 60 | + (SentryHandler*)sharedSentryHandler { 61 | 62 | if (_sharedSentryHandler == nil) { 63 | _sharedSentryHandler = [[SentryHandler alloc] init]; 64 | } 65 | 66 | return _sharedSentryHandler; 67 | } 68 | 69 | + (NSString*)crashReportingDefaultsKey { 70 | 71 | return @"CrashReporting"; 72 | } 73 | 74 | + (NSString*)promptedForPermissionsDefaultsKey { 75 | 76 | return @"PromptedForCrashReporting"; 77 | } 78 | 79 | - (BOOL)userHasEnabledCrashReporting { 80 | 81 | return [_groupDefaults boolForKey:[SentryHandler crashReportingDefaultsKey]]; 82 | } 83 | 84 | - (BOOL)userHasBeenPromptedForCrashReportingPermissions { 85 | 86 | return [_groupDefaults boolForKey:[SentryHandler promptedForPermissionsDefaultsKey]]; 87 | } 88 | 89 | - (nullable NSString*)sentryDsn { 90 | 91 | NSString* envDsn = SENTRY_DSN; 92 | 93 | if (envDsn && envDsn.length > 0) { 94 | return [NSString stringWithFormat:@"https://%@", envDsn]; 95 | } 96 | 97 | return nil; 98 | } 99 | 100 | - (nullable NSString*)sentryEnvironment { 101 | 102 | NSString* envEnvironment = SENTRY_ENVIRONMENT; 103 | 104 | if (envEnvironment && envEnvironment.length > 0) { 105 | return envEnvironment; 106 | } 107 | 108 | return nil; 109 | } 110 | 111 | - (nullable NSString*)sentryReleaseName { 112 | 113 | NSString* appId = __MLE__AppBundleIdentifier; 114 | NSString* versionString = CURRENT_PROJECT_VERSION; 115 | NSUInteger versionBuild = VERSION_BUILD; 116 | 117 | NSString* sentryReleaseName = [NSString stringWithFormat:@"%@@%@+%lu", appId, versionString, versionBuild]; 118 | 119 | return sentryReleaseName; 120 | } 121 | 122 | #pragma mark - Mutators 123 | 124 | - (void)setupSentry { 125 | 126 | NSString* sentryDsn = [self sentryDsn]; 127 | if (sentryDsn == nil) { 128 | MLE_Log_Error(@"SentryHandler [setupSentry] error - sentry dsn is unset"); 129 | return; 130 | } 131 | 132 | BOOL sentryEnabled = [self userHasEnabledCrashReporting]; 133 | NSString* sentryReleaseName = [self sentryReleaseName]; 134 | NSString* sentryEnvironment = [self sentryEnvironment]; 135 | MLE_Log_Info(@"SentryHandler [setupSentry] enabled:%@ release:%@ environment:%@", (sentryEnabled ? @"YES" : @"NO"), sentryReleaseName, sentryEnvironment); 136 | 137 | [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { 138 | options.dsn = sentryDsn; 139 | options.enabled = sentryEnabled; 140 | options.releaseName = sentryReleaseName; 141 | options.environment = sentryEnvironment; 142 | }]; 143 | } 144 | 145 | - (void)restartSentry { 146 | 147 | [SentrySDK close]; 148 | [self setupSentry]; 149 | } 150 | 151 | - (void)setUserHasEnabledCrashReporting:(BOOL)flag { 152 | 153 | [_groupDefaults setBool:flag forKey:[SentryHandler crashReportingDefaultsKey]]; 154 | } 155 | 156 | - (void)setUserHasBeenPromptedForCrashReportingPermissions:(BOOL)flag { 157 | 158 | [_groupDefaults setBool:flag forKey:[SentryHandler promptedForPermissionsDefaultsKey]]; 159 | } 160 | 161 | - (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject change:(NSDictionary *)aChange context:(void *)aContext { 162 | 163 | MLE_Log_Info(@"SentryHandler [observeValueForKeyPath:%@]", aKeyPath); 164 | 165 | if ([aKeyPath isEqualToString:[SentryHandler crashReportingDefaultsKey]]) { 166 | [_sharedSentryHandler restartSentry]; 167 | } 168 | } 169 | 170 | + (void)setCrashReportingEnabled:(BOOL)flag { 171 | 172 | if (_sharedSentryHandler != nil) { 173 | 174 | MLE_Log_Info(@"SentryHandler [setCrashReportingEnabled:%@]", (flag ? @"YES" : @"NO")); 175 | 176 | [_sharedSentryHandler setUserHasEnabledCrashReporting:flag]; 177 | [_sharedSentryHandler restartSentry]; 178 | } 179 | } 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /src/3rd/OrderedDictionary.h: -------------------------------------------------------------------------------- 1 | // 2 | // OrderedDictionary.h 3 | // 4 | // Version 1.4 5 | // 6 | // Created by Nick Lockwood on 21/09/2010. 7 | // Copyright 2010 Charcoal Design 8 | // 9 | // Distributed under the permissive zlib license 10 | // Get the latest version from here: 11 | // 12 | // https://github.com/nicklockwood/OrderedDictionary 13 | // 14 | // This software is provided 'as-is', without any express or implied 15 | // warranty. In no event will the authors be held liable for any damages 16 | // arising from the use of this software. 17 | // 18 | // Permission is granted to anyone to use this software for any purpose, 19 | // including commercial applications, and to alter it and redistribute it 20 | // freely, subject to the following restrictions: 21 | // 22 | // 1. The origin of this software must not be misrepresented; you must not 23 | // claim that you wrote the original software. If you use this software 24 | // in a product, an acknowledgment in the product documentation would be 25 | // appreciated but is not required. 26 | // 27 | // 2. Altered source versions must be plainly marked as such, and must not be 28 | // misrepresented as being the original software. 29 | // 30 | // 3. This notice may not be removed or altered from any source distribution. 31 | // 32 | 33 | #import 34 | 35 | NS_ASSUME_NONNULL_BEGIN 36 | 37 | /** 38 | * Ordered subclass of NSDictionary. 39 | * Supports all the same methods as NSDictionary, plus a few 40 | * new methods for operating on entities by index rather than key. 41 | */ 42 | @interface OrderedDictionary<__covariant KeyType, __covariant ObjectType> : NSDictionary 43 | 44 | /** 45 | * These methods can be used to load an XML plist file. The file must have a 46 | * dictionary node as its root object, and all dictionaries in the file will be 47 | * treated as ordered. Currently, only XML plist files are supported, not 48 | * binary or ascii. Xcode will automatically convert XML plists included in the 49 | * project to binary files in built apps, so you will need to disable that 50 | * functionality if you wish to load them with these functions. A good approach 51 | * is to rename such files with a .xml extension instead of .plist. See the 52 | * OrderedDictionary README file for more details. 53 | */ 54 | + (nullable instancetype)dictionaryWithContentsOfFile:(NSString *)path; 55 | + (nullable instancetype)dictionaryWithContentsOfURL:(NSURL *)url; 56 | - (nullable instancetype)initWithContentsOfFile:(NSString *)path; 57 | - (nullable instancetype)initWithContentsOfURL:(NSURL *)url; 58 | 59 | /** Returns the nth key in the dictionary. */ 60 | - (KeyType)keyAtIndex:(NSUInteger)index; 61 | /** Returns the nth object in the dictionary. */ 62 | - (ObjectType)objectAtIndex:(NSUInteger)index; 63 | - (ObjectType)objectAtIndexedSubscript:(NSUInteger)index; 64 | /** Returns the index of the specified key, or NSNotFound if key is not found. */ 65 | - (NSUInteger)indexOfKey:(KeyType)key; 66 | /** Returns an enumerator for backwards traversal of the dictionary keys. */ 67 | - (NSEnumerator *)reverseKeyEnumerator; 68 | /** Returns an enumerator for backwards traversal of the dictionary objects. */ 69 | - (NSEnumerator *)reverseObjectEnumerator; 70 | /** Enumerates keys ands objects with index using block. */ 71 | - (void)enumerateKeysAndObjectsWithIndexUsingBlock:(void (^)(KeyType key, ObjectType obj, NSUInteger idx, BOOL *stop))block; 72 | 73 | @end 74 | 75 | 76 | /** 77 | * Mutable subclass of OrderedDictionary. 78 | * Supports all the same methods as NSMutableDictionary, plus a few 79 | * new methods for operating on entities by index rather than key. 80 | * Note that although it has the same interface, MutableOrderedDictionary 81 | * is not a subclass of NSMutableDictionary, and cannot be used as one 82 | * without generating compiler warnings (unless you cast it). 83 | */ 84 | @interface MutableOrderedDictionary : OrderedDictionary 85 | 86 | + (instancetype)dictionaryWithCapacity:(NSUInteger)count; 87 | - (instancetype)initWithCapacity:(NSUInteger)count; 88 | 89 | - (void)addEntriesFromDictionary:(NSDictionary *)otherDictionary; 90 | - (void)removeAllObjects; 91 | - (void)removeObjectForKey:(KeyType)key; 92 | - (void)removeObjectsForKeys:(NSArray *)keyArray; 93 | - (void)setDictionary:(NSDictionary *)otherDictionary; 94 | - (void)setObject:(ObjectType)object forKey:(KeyType)key; 95 | - (void)setObject:(ObjectType)object forKeyedSubscript:(KeyType)key; 96 | 97 | /** Inserts an object at a specific index in the dictionary. */ 98 | - (void)insertObject:(ObjectType)object forKey:(KeyType)key atIndex:(NSUInteger)index; 99 | /** Replace an object at a specific index in the dictionary. */ 100 | - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)object; 101 | - (void)setObject:(ObjectType)object atIndexedSubscript:(NSUInteger)index; 102 | /** Swap the indexes of two key/value pairs in the dictionary. */ 103 | - (void)exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2; 104 | /** Removes the nth object in the dictionary. */ 105 | - (void)removeObjectAtIndex:(NSUInteger)index; 106 | 107 | @end 108 | 109 | NS_ASSUME_NONNULL_END 110 | 111 | -------------------------------------------------------------------------------- /src/Music Library Exporter.xcodeproj/xcshareddata/xcschemes/music-library-exporter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 61 | 62 | 65 | 66 | 69 | 70 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 89 | 90 | 93 | 94 | 95 | 96 | 102 | 104 | 110 | 111 | 112 | 113 | 115 | 116 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/DirectoryPermissionsWindow/DirectoryPermissionsWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryPermissionsWindowController.m 3 | // Music Library Exporter Helper 4 | // 5 | // Created by Kyle King on 2021-02-13. 6 | // 7 | 8 | #import "DirectoryPermissionsWindowController.h" 9 | 10 | #import "Logger.h" 11 | #import "ExportConfiguration.h" 12 | #import "ScheduleConfiguration.h" 13 | #import "DirectoryBookmarkHandler.h" 14 | 15 | 16 | @implementation DirectoryPermissionsWindowController { 17 | 18 | ExportConfiguration* _exportConfiguration; 19 | ScheduleConfiguration* _scheduleConfiguration; 20 | } 21 | 22 | 23 | #pragma mark - Initializers 24 | 25 | - (instancetype)init { 26 | 27 | if (self = [super initWithWindowNibName:@"DirectoryPermissionsWindow"]) { 28 | 29 | _exportConfiguration = nil; 30 | _scheduleConfiguration = nil; 31 | 32 | return self; 33 | } 34 | else { 35 | return nil; 36 | } 37 | } 38 | 39 | - (instancetype)initWithExportConfiguration:(ExportConfiguration*)exportConfiguration 40 | andScheduleConfiguration:(ScheduleConfiguration*)scheduleConfiguration { 41 | 42 | if (self = [self init]) { 43 | 44 | _exportConfiguration = exportConfiguration; 45 | _scheduleConfiguration = scheduleConfiguration; 46 | 47 | return self; 48 | } 49 | else { 50 | return nil; 51 | } 52 | } 53 | 54 | - (void)windowDidLoad { 55 | 56 | [super windowDidLoad]; 57 | 58 | // Set activation policy to regular to allow for modal to pop up 59 | [self.window setLevel:NSFloatingWindowLevel]; 60 | [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular]; 61 | } 62 | 63 | - (IBAction)chooseOutputDirectory:(id)sender { 64 | 65 | [self requestOutputDirectoryPermissions]; 66 | } 67 | 68 | - (void)showIncorrectDirectoryAlert { 69 | 70 | NSAlert *alert = [[NSAlert alloc] init]; 71 | [alert addButtonWithTitle:@"Ok"]; 72 | [alert setMessageText:@"Incorrect output directory selected"]; 73 | [alert setInformativeText:@"Please choose the same output directory that you selected in the Music Library Exporter main application."]; 74 | [alert setAlertStyle:NSAlertStyleCritical]; 75 | [alert runModal]; 76 | 77 | [self requestOutputDirectoryPermissions]; 78 | } 79 | 80 | - (void)showAutomaticExportsDisabledDirectoryAlert { 81 | 82 | NSAlert *alert = [[NSAlert alloc] init]; 83 | [alert addButtonWithTitle:@"Ok"]; 84 | [alert setMessageText:@"Automatic exports have been disabled. "]; 85 | [alert setInformativeText:@"Automatic exports can be re-enabled from the Music Library Exporter main application."]; 86 | [alert setAlertStyle:NSAlertStyleCritical]; 87 | [alert runModal]; 88 | 89 | [_scheduleConfiguration setNextExportAt:nil]; 90 | [_scheduleConfiguration setScheduleEnabled:NO]; 91 | [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyProhibited]; 92 | 93 | [self close]; 94 | } 95 | 96 | - (void)requestOutputDirectoryPermissions { 97 | 98 | MLE_Log_Info(@"DirectoryPermissionsWindowController [requestOutputDirectoryPermissions]"); 99 | 100 | NSString* outputDirectoryPath = _exportConfiguration.outputDirectoryPath; 101 | 102 | NSOpenPanel* openPanel = [NSOpenPanel openPanel]; 103 | [openPanel setCanChooseDirectories:YES]; 104 | [openPanel setCanChooseFiles:NO]; 105 | [openPanel setAllowsMultipleSelection:NO]; 106 | 107 | [openPanel setMessage:@"Please select the same output directory that you selected in the main application ."]; 108 | if (outputDirectoryPath.length > 0) { 109 | [openPanel setDirectoryURL:[NSURL fileURLWithPath:outputDirectoryPath]]; 110 | } 111 | 112 | [openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { 113 | 114 | if (result == NSModalResponseOK) { 115 | 116 | NSURL* outputDirectoryURL = [openPanel URL]; 117 | 118 | if (outputDirectoryURL) { 119 | if (outputDirectoryPath.length == 0 || [outputDirectoryURL.path isEqualToString:outputDirectoryPath]) { 120 | MLE_Log_Info(@"DirectoryPermissionsWindowController [requestOutputDirectoryPermissions] the correct output directory has been selected"); 121 | DirectoryBookmarkHandler* bookmarkHandler = [[DirectoryBookmarkHandler alloc] initWithUserDefaultsKey:OUTPUT_DIRECTORY_BOOKMARK_KEY]; 122 | [bookmarkHandler saveURLToDefaults:outputDirectoryURL]; 123 | [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyProhibited]; 124 | return; 125 | } 126 | else { 127 | MLE_Log_Info(@"DirectoryPermissionsWindowController [requestOutputDirectoryPermissions] the user has selected a directory that differs from the output directory set with the main app."); 128 | [openPanel orderOut:nil]; 129 | [self showIncorrectDirectoryAlert]; 130 | return; 131 | } 132 | } 133 | else { 134 | MLE_Log_Info(@"DirectoryPermissionsWindowController [requestOutputDirectoryPermissions] the user has cancelled granting permissions. Automated exports will be disabled."); 135 | [openPanel orderOut:nil]; 136 | [self showAutomaticExportsDisabledDirectoryAlert]; 137 | return; 138 | } 139 | } 140 | else { 141 | MLE_Log_Info(@"DirectoryPermissionsWindowController [requestOutputDirectoryPermissions] the user has cancelled granting permissions. Automated exports will be disabled."); 142 | [openPanel orderOut:nil]; 143 | [self showAutomaticExportsDisabledDirectoryAlert]; 144 | return; 145 | } 146 | }]; 147 | } 148 | 149 | @end 150 | -------------------------------------------------------------------------------- /src/Common/Sorter/MediaItemSorter.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemSorter.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import "MediaItemSorter.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | #import "Logger.h" 15 | #import "SorterDefines.h" 16 | 17 | @interface MediaItemSorter() 18 | 19 | - (instancetype)init; 20 | 21 | - (nullable id)valueOfItem:(ITLibMediaItem*)item forProperty:(NSString*)property; 22 | 23 | - (NSComparisonResult)compareItem:(ITLibMediaItem*)item1 withItem:(ITLibMediaItem*)item2; 24 | - (NSComparisonResult)compareProperty:(NSString*)property ofItem:(ITLibMediaItem*)item1 withItem:(ITLibMediaItem*)item2 order:(PlaylistSortOrderType)order; 25 | 26 | - (NSComparisonResult)alphabeticallyCompareString:(NSString*)str1 withString:(NSString*)str2; 27 | 28 | @end 29 | 30 | @implementation MediaItemSorter 31 | 32 | #pragma mark - Initializers 33 | 34 | - (instancetype)init { 35 | 36 | return [self initWithSortProperty:nil andSortOrder:PlaylistSortOrderNull]; 37 | } 38 | 39 | - (instancetype)initWithSortProperty:(nullable NSString*)sortProperty andSortOrder:(PlaylistSortOrderType)sortOrder { 40 | 41 | if (self = [super init]) { 42 | 43 | _sortProperty = sortProperty; 44 | _sortOrder = sortOrder; 45 | 46 | return self; 47 | } 48 | else { 49 | return nil; 50 | } 51 | } 52 | 53 | #pragma mark - Accessors 54 | 55 | - (NSArray*)sortItems:(NSArray*)items { 56 | 57 | // don't sort if sort property is null 58 | if (_sortProperty == nil) { 59 | return items; 60 | } 61 | 62 | // default to ascending sort order 63 | if (_sortOrder == PlaylistSortOrderNull) { 64 | _sortOrder = PlaylistSortOrderAscending; 65 | } 66 | 67 | 68 | return [items sortedArrayUsingComparator:^NSComparisonResult(id item1, id item2) { 69 | return [self compareItem:item1 withItem:item2]; 70 | }]; 71 | } 72 | 73 | - (nullable id)valueOfItem:(ITLibMediaItem*)item forProperty:(NSString*)property { 74 | 75 | id itemValue; 76 | 77 | if ([[SorterDefines propertySubstitutions] objectForKey:property] != nil) { 78 | 79 | // if substitutions exist for the property, return the first non-empty value 80 | for (NSString* substituteProperty in [SorterDefines substitutionsForProperty:property]) { 81 | 82 | itemValue = [item valueForProperty:substituteProperty]; 83 | 84 | // return first non-empty value 85 | if (itemValue != nil) { 86 | /* DEBUG 87 | if (substituteProperty != property) { 88 | MLE_Log_Info(@"MediaItemSorter [valueOfItem] using substitute %@ for property %@ ('%@ - %@') ('%@ -> %@')", substituteProperty, property, item.album.albumArtist, item.title, [item valueForProperty:property], [item valueForProperty:substituteProperty]); 89 | } 90 | */ 91 | // re-assign property for correct pre-processing of the substituted property 92 | property = substituteProperty; 93 | break; 94 | } 95 | } 96 | } 97 | 98 | // directly return value for given property 99 | else { 100 | itemValue = [item valueForProperty:property]; 101 | } 102 | 103 | return itemValue; 104 | } 105 | 106 | - (NSComparisonResult)compareItem:(ITLibMediaItem*)item1 withItem:(ITLibMediaItem*)item2 { 107 | 108 | NSComparisonResult result = [self compareProperty:_sortProperty ofItem:item1 withItem:item2 order:_sortOrder]; 109 | 110 | // values are identical, attempt to sort by fallback properties 111 | if (result == NSOrderedSame) { 112 | 113 | for (NSString* fallbackProperty in [SorterDefines fallbackPropertiesForProperty:_sortProperty]) { 114 | 115 | // skip redundant fallback properties (useful when fallbacks are the default list) 116 | if (fallbackProperty != _sortProperty) { 117 | // use ascending order for fallback properties 118 | result = [self compareProperty:fallbackProperty ofItem:item1 withItem:item2 order:PlaylistSortOrderAscending]; 119 | 120 | // use first result that is not equal 121 | if (result != NSOrderedSame) { 122 | // MLE_Log_Info(@"MediaItemSorter [compareItem] used fallback %@ for property %@ ('%@ - %@', '%@ - %@')", fallbackProperty, _sortProperty, item1.album.albumArtist, item1.title, item2.album.albumArtist, item2.title); 123 | break; 124 | } 125 | } 126 | } 127 | } 128 | 129 | return result; 130 | } 131 | 132 | - (NSComparisonResult)compareProperty:(NSString*)property ofItem:(ITLibMediaItem*)item1 withItem:(ITLibMediaItem*)item2 order:(PlaylistSortOrderType)order { 133 | 134 | id item1Value = [self valueOfItem:item1 forProperty:property]; 135 | id item2Value = [self valueOfItem:item2 forProperty:property]; 136 | 137 | // handle nil values 138 | if (item1Value == nil || item2Value == nil) { 139 | if ([item1Value isEqualTo:item2Value]) { 140 | return NSOrderedSame; 141 | } 142 | else if (item1Value) { 143 | return NSOrderedAscending; 144 | } 145 | else { 146 | return NSOrderedDescending; 147 | } 148 | } 149 | 150 | NSComparisonResult result; 151 | if (item1Value && [item1Value isKindOfClass:[NSString class]]) { 152 | result = [self alphabeticallyCompareString:item1Value withString:item2Value]; 153 | } 154 | else { 155 | result = [item1Value compare:item2Value]; 156 | } 157 | 158 | if (order == PlaylistSortOrderAscending) { 159 | return result; 160 | } 161 | else { 162 | return -result; 163 | } 164 | } 165 | 166 | - (NSComparisonResult)alphabeticallyCompareString:(NSString*)str1 withString:(NSString*)str2 { 167 | 168 | // sort so that strings that begin with letters come before non-letter strings (begin with digit, special char, etc) 169 | BOOL str1LetterPrefix = [[NSCharacterSet letterCharacterSet] characterIsMember:[str1 characterAtIndex:0]]; 170 | BOOL str2LetterPrefix = [[NSCharacterSet letterCharacterSet] characterIsMember:[str2 characterAtIndex:0]]; 171 | if (str1LetterPrefix != str2LetterPrefix) { 172 | return str1LetterPrefix ? NSOrderedAscending : NSOrderedDescending; 173 | } 174 | 175 | return [str1 compare:str2 options:(NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSNumericSearch)]; 176 | } 177 | 178 | @end 179 | -------------------------------------------------------------------------------- /src/Common/Serializer/PlaylistSerializer.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistSerializer.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-01. 6 | // 7 | 8 | #import "PlaylistSerializer.h" 9 | 10 | #import 11 | #import 12 | 13 | #import "Logger.h" 14 | #import "MediaEntityRepository.h" 15 | #import "MediaItemFilterGroup.h" 16 | #import "MediaItemSorter.h" 17 | #import "OrderedDictionary.h" 18 | #import "PlaylistFilterGroup.h" 19 | #import "Utils.h" 20 | 21 | @implementation PlaylistSerializer { 22 | 23 | MediaEntityRepository* _entityRepository; 24 | } 25 | 26 | - (instancetype)init { 27 | 28 | if (self = [super init]) { 29 | 30 | _delegate = nil; 31 | 32 | _flattenFolders = false; 33 | 34 | _playlistFilters = nil; 35 | _itemFilters = nil; 36 | 37 | _playlistCustomSortProperties = [NSDictionary dictionary]; 38 | _playlistCustomSortOrders = [NSDictionary dictionary]; 39 | 40 | _entityRepository = nil; 41 | 42 | return self; 43 | } 44 | else { 45 | return nil; 46 | } 47 | } 48 | 49 | - (instancetype) initWithEntityRepository:(MediaEntityRepository*)entityRepository { 50 | 51 | if (self = [self init]) { 52 | 53 | _entityRepository = entityRepository; 54 | 55 | return self; 56 | } 57 | else { 58 | return nil; 59 | } 60 | } 61 | 62 | - (NSArray*)serializePlaylists:(NSArray*)playlists { 63 | 64 | NSMutableArray* playlistsArray = [NSMutableArray array]; 65 | 66 | NSUInteger serializedPlaylists = 0; 67 | NSUInteger totalPlaylists = playlists.count; 68 | 69 | for (ITLibPlaylist* playlist in playlists) { 70 | 71 | // ignore excluded playlists 72 | if (_playlistFilters == nil || [_playlistFilters filtersPassForPlaylist:playlist]) { 73 | 74 | [playlistsArray addObject:[self serializePlaylist:playlist]]; 75 | } 76 | else if (_delegate != nil && [_delegate respondsToSelector:@selector(excludedPlaylist:)]) { 77 | [_delegate excludedPlaylist:playlist]; 78 | } 79 | 80 | serializedPlaylists++; 81 | 82 | if (_delegate != nil && [_delegate respondsToSelector:@selector(serializedPlaylists:ofTotal:)]) { 83 | [_delegate serializedPlaylists:serializedPlaylists ofTotal:totalPlaylists]; 84 | } 85 | } 86 | 87 | return playlistsArray; 88 | } 89 | 90 | - (OrderedDictionary*)serializePlaylist:(ITLibPlaylist*)playlist { 91 | 92 | os_log_info(OS_LOG_DEFAULT, "Serializing playlist: '%{public}@' (kind: %{public}@)", playlist.name, [PlaylistSerializer describePlaylistKind:playlist.kind]); 93 | 94 | MutableOrderedDictionary* playlistDict = [MutableOrderedDictionary dictionary]; 95 | 96 | [playlistDict setValue:playlist.name forKey:@"Name"]; 97 | /* unavailable 98 | [playlistDict setValue:playlistItem. forKey:@"Description"]; - unavailable 99 | */ 100 | if (playlist.master) { 101 | [playlistDict setValue:[NSNumber numberWithBool:YES] forKey:@"Master"]; 102 | [playlistDict setValue:[NSNumber numberWithBool:NO] forKey:@"Visible"]; 103 | } 104 | [playlistDict setValue:[_entityRepository getIDForEntity:playlist] forKey:@"Playlist ID"]; 105 | [playlistDict setValue:[Utils hexStringForPersistentId:playlist.persistentID] forKey:@"Playlist Persistent ID"]; 106 | 107 | if (playlist.parentID && !_flattenFolders) { 108 | [playlistDict setValue:[Utils hexStringForPersistentId:playlist.parentID] forKey:@"Parent Persistent ID"]; 109 | } 110 | if (playlist.distinguishedKind > ITLibDistinguishedPlaylistKindNone) { 111 | [playlistDict setValue:[NSNumber numberWithUnsignedInteger:playlist.distinguishedKind] forKey:@"Distinguished Kind"]; 112 | if (playlist.distinguishedKind == ITLibDistinguishedPlaylistKindMusic) { 113 | [playlistDict setValue:[NSNumber numberWithBool:YES] forKey:@"Music"]; 114 | } 115 | } 116 | if (!playlist.visible) { 117 | [playlistDict setValue:[NSNumber numberWithBool:NO] forKey:@"Visible"]; 118 | } 119 | [playlistDict setValue:[NSNumber numberWithBool:YES] forKey:@"All Items"]; 120 | if (playlist.kind == ITLibPlaylistKindFolder) { 121 | [playlistDict setValue:[NSNumber numberWithBool:YES] forKey:@"Folder"]; 122 | } 123 | 124 | MediaItemSorter* sorter = nil; 125 | 126 | if (_playlistCustomSortProperties != nil && _playlistCustomSortProperties != nil) { 127 | 128 | NSString* sortProperty = [_playlistCustomSortProperties valueForKey:[Utils hexStringForPersistentId:playlist.persistentID]]; 129 | 130 | NSString* sortOrderTitle = [_playlistCustomSortOrders valueForKey:[Utils hexStringForPersistentId:playlist.persistentID]]; 131 | PlaylistSortOrderType sortOrder = [Utils playlistSortOrderForTitle:sortOrderTitle]; 132 | 133 | sorter = [[MediaItemSorter alloc] initWithSortProperty:sortProperty andSortOrder:sortOrder]; 134 | } 135 | else { 136 | sorter = [[MediaItemSorter alloc] init]; 137 | } 138 | 139 | NSArray* sortedItems = [sorter sortItems:playlist.items]; 140 | os_log_info(OS_LOG_DEFAULT, "Starting serialization of %lu child items in playlist: '%{public}@' (kind: %{public}@)", sortedItems.count, playlist.name, [PlaylistSerializer describePlaylistKind:playlist.kind]); 141 | [playlistDict setObject:[self serializePlaylistItems:sortedItems] forKey:@"Playlist Items"]; 142 | 143 | return playlistDict; 144 | } 145 | 146 | - (NSArray*)serializePlaylistItems:(NSArray*)items { 147 | 148 | NSMutableArray* itemsArray = [NSMutableArray array]; 149 | 150 | for (ITLibMediaItem* item in items) { 151 | 152 | // ignore excluded media items 153 | if (_itemFilters == nil || [_itemFilters filtersPassForItem:item]) { 154 | 155 | MutableOrderedDictionary* itemDict = [MutableOrderedDictionary dictionary]; 156 | [itemDict setValue:[_entityRepository getIDForEntity:item] forKey:@"Track ID"]; 157 | 158 | [itemsArray addObject:itemDict]; 159 | } 160 | } 161 | 162 | return itemsArray; 163 | } 164 | 165 | + (nonnull NSString *)describePlaylistKind:(ITLibPlaylistKind)kind { 166 | switch (kind) { 167 | case ITLibPlaylistKindRegular: { 168 | return @"Playlist"; 169 | } 170 | case ITLibPlaylistKindSmart: { 171 | return @"Smart Playlist"; 172 | } 173 | case ITLibPlaylistKindGenius: { 174 | return @"Genius"; 175 | } 176 | case ITLibPlaylistKindFolder: { 177 | return @"Folder"; 178 | } 179 | case ITLibPlaylistKindGeniusMix: { 180 | return @"Genius Mix"; 181 | } 182 | } 183 | } 184 | 185 | @end 186 | -------------------------------------------------------------------------------- /src/music-library-exporter/CLIDefines.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLIDefines.m 3 | // music-library-exporter 4 | // 5 | // Created by Kyle King on 2021-02-15. 6 | // 7 | 8 | #import "CLIDefines.h" 9 | 10 | #import "Defines.h" 11 | 12 | @implementation CLIDefines 13 | 14 | + (NSArray*)optionsForCommand:(CLICommandKind)command { 15 | 16 | switch (command) { 17 | 18 | case CLICommandKindHelp: { 19 | return @[ 20 | @(CLIOptionKindHelp) 21 | ]; 22 | } 23 | 24 | case CLICommandKindVersion: { 25 | return @[ 26 | @(CLIOptionKindVersion) 27 | ]; 28 | } 29 | 30 | case CLICommandKindPrint: { 31 | return @[ 32 | @(CLIOptionKindHelp), 33 | @(CLIOptionKindReadPrefs), 34 | @(CLIOptionKindFlatten), 35 | @(CLIOptionKindExcludeInternal), 36 | @(CLIOptionKindExcludeIds), 37 | ]; 38 | } 39 | 40 | case CLICommandKindExport: { 41 | return @[ 42 | @(CLIOptionKindHelp), 43 | @(CLIOptionKindReadPrefs), 44 | @(CLIOptionKindFlatten), 45 | @(CLIOptionKindExcludeInternal), 46 | @(CLIOptionKindExcludeIds), 47 | @(CLIOptionKindMusicMediaDirectory), 48 | @(CLIOptionKindSort), 49 | @(CLIOptionKindRemapSearch), 50 | @(CLIOptionKindRemapReplace), 51 | @(CLIOptionKindRemapLocalhostPrefix), 52 | @(CLIOptionKindOutputPath), 53 | ]; 54 | } 55 | 56 | case CLICommandKindUnknown: { 57 | return @[ 58 | @(CLIOptionKindHelp) 59 | ]; 60 | } 61 | } 62 | } 63 | 64 | + (NSArray*)requiredOptionsForCommand:(CLICommandKind)command { 65 | 66 | switch (command) { 67 | 68 | case CLICommandKindHelp: { 69 | return @[ ]; 70 | } 71 | 72 | case CLICommandKindVersion: { 73 | return @[ ]; 74 | } 75 | 76 | case CLICommandKindPrint: { 77 | return @[ ]; 78 | } 79 | 80 | case CLICommandKindExport: { 81 | return @[ 82 | @(CLIOptionKindMusicMediaDirectory), 83 | @(CLIOptionKindOutputPath), 84 | ]; 85 | } 86 | 87 | case CLICommandKindUnknown: { 88 | return @[ ]; 89 | } 90 | } 91 | } 92 | 93 | + (nullable NSString*)nameForCommand:(CLICommandKind)command { 94 | 95 | switch (command) { 96 | case CLICommandKindHelp: { 97 | return @"help"; 98 | } 99 | case CLICommandKindVersion: { 100 | return @"version"; 101 | } 102 | case CLICommandKindPrint: { 103 | return @"print"; 104 | } 105 | case CLICommandKindExport: { 106 | return @"export"; 107 | } 108 | case CLICommandKindUnknown: { 109 | return nil; 110 | } 111 | } 112 | } 113 | 114 | + (nullable NSString*)nameForOption:(CLIOptionKind)option { 115 | 116 | switch (option) { 117 | 118 | case CLIOptionKindHelp: { 119 | return @"--help"; 120 | } 121 | case CLIOptionKindVersion: { 122 | return @"--version"; 123 | } 124 | 125 | case CLIOptionKindReadPrefs: { 126 | return @"--read_prefs"; 127 | } 128 | 129 | case CLIOptionKindFlatten: { 130 | return @"--flatten"; 131 | } 132 | case CLIOptionKindExcludeInternal: { 133 | return @"--exclude_internal"; 134 | } 135 | case CLIOptionKindExcludeIds: { 136 | return @"--exclude_ids"; 137 | } 138 | 139 | case CLIOptionKindMusicMediaDirectory: { 140 | return @"--music_media_dir"; 141 | } 142 | case CLIOptionKindSort: { 143 | return @"--sort"; 144 | } 145 | case CLIOptionKindRemapSearch: { 146 | return @"--remap_search"; 147 | } 148 | case CLIOptionKindRemapReplace: { 149 | return @"--remap_replace"; 150 | } 151 | case CLIOptionKindRemapLocalhostPrefix: { 152 | return @"--localhost_path_prefix"; 153 | } 154 | case CLIOptionKindOutputPath: { 155 | return @"--output_path"; 156 | } 157 | 158 | case CLIOptionKind_MAX: { 159 | return nil; 160 | } 161 | } 162 | } 163 | 164 | + (NSArray*)commandNames { 165 | 166 | NSMutableArray* names = [NSMutableArray array]; 167 | 168 | for (CLICommandKind command = CLICommandKindHelp; command < CLICommandKindUnknown; command++) { 169 | [names addObject:[CLIDefines nameForCommand:command]]; 170 | } 171 | 172 | return names; 173 | } 174 | 175 | + (nullable NSString*)signatureFormatForCommand:(CLICommandKind)command { 176 | 177 | switch (command) { 178 | 179 | case CLICommandKindHelp: { 180 | return @"[-h --help help]"; 181 | } 182 | case CLICommandKindVersion: { 183 | return @"[-v --version version]"; 184 | } 185 | case CLICommandKindPrint: { 186 | return @"[print]"; 187 | } 188 | case CLICommandKindExport: { 189 | return @"[export]"; 190 | } 191 | 192 | case CLICommandKindUnknown: { 193 | return nil; 194 | } 195 | } 196 | } 197 | 198 | + (nullable NSString*)signatureFormatForOption:(CLIOptionKind)option { 199 | 200 | switch (option) { 201 | 202 | case CLIOptionKindHelp: { 203 | return [CLIDefines signatureFormatForCommand:CLICommandKindHelp]; 204 | } 205 | 206 | case CLIOptionKindVersion: { 207 | return [CLIDefines signatureFormatForCommand:CLICommandKindVersion]; 208 | } 209 | 210 | case CLIOptionKindReadPrefs: { 211 | return @"[-p --read_prefs]"; 212 | } 213 | 214 | case CLIOptionKindFlatten: { 215 | return @"[-f --flatten]"; 216 | } 217 | case CLIOptionKindExcludeInternal: { 218 | return @"[-n --exclude_internal]"; 219 | } 220 | case CLIOptionKindExcludeIds: { 221 | return @"[-e --exclude_ids]={1,1}"; 222 | } 223 | 224 | case CLIOptionKindMusicMediaDirectory: { 225 | return @"[-m --music_media_dir]={1,1}"; 226 | } 227 | case CLIOptionKindSort: { 228 | return @"[--sort]={1,1}"; 229 | } 230 | case CLIOptionKindRemapSearch: { 231 | return @"[-s --remap_search]={1,1}"; 232 | } 233 | case CLIOptionKindRemapReplace: { 234 | return @"[-r --remap_replace]={1,1}"; 235 | } 236 | case CLIOptionKindRemapLocalhostPrefix: { 237 | return @"[--localhost_path_prefix]"; 238 | } 239 | case CLIOptionKindOutputPath: { 240 | return @"[-o --output_path]={1,1}"; 241 | } 242 | 243 | case CLIOptionKind_MAX: { 244 | return nil; 245 | } 246 | } 247 | } 248 | 249 | + (NSURL*)fileUrlForAppPreferences { 250 | 251 | NSArray* pathComponents = @[ 252 | NSFileManager.defaultManager.homeDirectoryForCurrentUser.path, 253 | @"Library", 254 | @"Group Containers", 255 | __MLE__AppGroupIdentifier, 256 | @"Library", 257 | @"Preferences", 258 | [__MLE__AppGroupIdentifier stringByAppendingString:@".plist"], 259 | ]; 260 | 261 | return [NSURL fileURLWithPathComponents:pathComponents]; 262 | } 263 | 264 | @end 265 | -------------------------------------------------------------------------------- /src/Common/Configuration/UserDefaultsExportConfiguration.m: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsExportConfiguration.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2021-02-01. 6 | // 7 | 8 | #import "UserDefaultsExportConfiguration.h" 9 | 10 | #import "Logger.h" 11 | #import "SorterDefines.h" 12 | 13 | 14 | @implementation UserDefaultsExportConfiguration { 15 | 16 | NSUserDefaults* _userDefaults; 17 | 18 | NSString* _outputDirectoryBookmarkKey; 19 | } 20 | 21 | 22 | #pragma mark - Initializers 23 | 24 | - (instancetype)init { 25 | 26 | if (self = [super init]) { 27 | 28 | _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:__MLE__AppGroupIdentifier]; 29 | 30 | _outputDirectoryBookmarkKey = nil; 31 | 32 | return self; 33 | } 34 | else { 35 | return nil; 36 | } 37 | } 38 | 39 | - (instancetype)initWithOutputDirectoryBookmarkKey:(NSString*)outputDirectoryBookmarkKey { 40 | 41 | if (self = [self init]) { 42 | 43 | _outputDirectoryBookmarkKey = outputDirectoryBookmarkKey; 44 | 45 | // observe changes of output directory bookmark to allow for automatic updating of OutputDirectoryPath 46 | [_userDefaults addObserver:self forKeyPath:_outputDirectoryBookmarkKey options:NSKeyValueObservingOptionNew context:NULL]; 47 | 48 | return self; 49 | } 50 | else { 51 | return nil; 52 | } 53 | } 54 | 55 | 56 | #pragma mark - Accessors 57 | 58 | - (NSDictionary*)defaultValues { 59 | 60 | return [NSDictionary dictionaryWithObjectsAndKeys: 61 | @"", ExportConfigurationKeyMusicLibraryPath, 62 | 63 | // nil, ExportConfigurationKeyGeneratedPersistentLibraryId, 64 | 65 | @"", ExportConfigurationKeyOutputDirectoryPath, 66 | @"", ExportConfigurationKeyOutputFileName, 67 | 68 | @NO, ExportConfigurationKeyRemapRootDirectory, 69 | @"", ExportConfigurationKeyRemapRootDirectoryOriginalPath, 70 | @"", ExportConfigurationKeyRemapRootDirectoryMappedPath, 71 | @NO, ExportConfigurationKeyRemapRootDirectoryLocalhostPrefix, 72 | 73 | @NO, ExportConfigurationKeyFlattenPlaylistHierarchy, 74 | @YES, ExportConfigurationKeyIncludeInternalPlaylists, 75 | @[], ExportConfigurationKeyExcludedPlaylistPersistentIds, 76 | 77 | @{}, ExportConfigurationKeyPlaylistCustomSortProperties, 78 | @{}, ExportConfigurationKeyPlaylistCustomSortOrders, 79 | 80 | nil 81 | ]; 82 | } 83 | 84 | #pragma mark - Mutators 85 | 86 | - (void)setMusicLibraryPath:(NSString*)musicLibraryPath { 87 | 88 | [super setMusicLibraryPath:musicLibraryPath]; 89 | 90 | [_userDefaults setValue:musicLibraryPath forKey:ExportConfigurationKeyMusicLibraryPath]; 91 | } 92 | 93 | - (void)setGeneratedPersistentLibraryId:(NSString*)generatedPersistentLibraryId { 94 | 95 | [super setGeneratedPersistentLibraryId:generatedPersistentLibraryId]; 96 | 97 | [_userDefaults setValue:generatedPersistentLibraryId forKey:ExportConfigurationKeyGeneratedPersistentLibraryId]; 98 | } 99 | 100 | - (void)setOutputDirectoryUrl:(nullable NSURL*)dirUrl { 101 | 102 | [super setOutputDirectoryUrl:dirUrl]; 103 | 104 | // The NSUserDefaults setValue call is skipped here as this is done by the security scoped bookmark handler 105 | } 106 | 107 | - (void)setOutputDirectoryPath:(nullable NSString*)dirPath { 108 | 109 | [super setOutputDirectoryPath:dirPath]; 110 | 111 | [_userDefaults setValue:dirPath forKey:ExportConfigurationKeyOutputDirectoryPath]; 112 | } 113 | 114 | - (void)setOutputFileName:(NSString*)fileName { 115 | 116 | [super setOutputFileName:fileName]; 117 | 118 | [_userDefaults setValue:fileName forKey:ExportConfigurationKeyOutputFileName]; 119 | } 120 | 121 | - (void)setRemapRootDirectory:(BOOL)flag { 122 | 123 | [super setRemapRootDirectory:flag]; 124 | 125 | [_userDefaults setBool:flag forKey:ExportConfigurationKeyRemapRootDirectory]; 126 | } 127 | 128 | - (void)setRemapRootDirectoryOriginalPath:(NSString*)originalPath { 129 | 130 | [super setRemapRootDirectoryOriginalPath:originalPath]; 131 | 132 | [_userDefaults setValue:originalPath forKey:ExportConfigurationKeyRemapRootDirectoryOriginalPath]; 133 | } 134 | 135 | - (void)setRemapRootDirectoryMappedPath:(NSString*)mappedPath { 136 | 137 | [super setRemapRootDirectoryMappedPath:mappedPath]; 138 | 139 | [_userDefaults setValue:mappedPath forKey:ExportConfigurationKeyRemapRootDirectoryMappedPath]; 140 | } 141 | 142 | - (void)setRemapRootDirectoryLocalhostPrefix:(BOOL)flag { 143 | 144 | [super setRemapRootDirectoryLocalhostPrefix:flag]; 145 | 146 | [_userDefaults setBool:flag forKey:ExportConfigurationKeyRemapRootDirectoryLocalhostPrefix]; 147 | } 148 | 149 | - (void)setFlattenPlaylistHierarchy:(BOOL)flag { 150 | 151 | [super setFlattenPlaylistHierarchy:flag]; 152 | 153 | [_userDefaults setBool:flag forKey:ExportConfigurationKeyFlattenPlaylistHierarchy]; 154 | } 155 | 156 | - (void)setIncludeInternalPlaylists:(BOOL)flag { 157 | 158 | [super setIncludeInternalPlaylists:flag]; 159 | 160 | [_userDefaults setBool:flag forKey:ExportConfigurationKeyIncludeInternalPlaylists]; 161 | } 162 | 163 | - (void)setExcludedPlaylistPersistentIds:(NSSet*)excludedIds { 164 | 165 | [super setExcludedPlaylistPersistentIds:excludedIds]; 166 | 167 | [_userDefaults setObject:[excludedIds allObjects] forKey:ExportConfigurationKeyExcludedPlaylistPersistentIds]; 168 | } 169 | 170 | - (void)addExcludedPlaylistPersistentId:(NSString*)playlistId { 171 | 172 | [super addExcludedPlaylistPersistentId:playlistId]; 173 | 174 | [_userDefaults setObject:[[super excludedPlaylistPersistentIds] allObjects] forKey:ExportConfigurationKeyExcludedPlaylistPersistentIds]; 175 | } 176 | 177 | - (void)removeExcludedPlaylistPersistentId:(NSString*)playlistId { 178 | 179 | [super removeExcludedPlaylistPersistentId:playlistId]; 180 | 181 | [_userDefaults setObject:[[super excludedPlaylistPersistentIds] allObjects] forKey:ExportConfigurationKeyExcludedPlaylistPersistentIds]; 182 | } 183 | 184 | - (void)setCustomSortPropertyDict:(NSDictionary*)dict { 185 | 186 | [super setCustomSortPropertyDict:dict]; 187 | 188 | [_userDefaults setObject:dict forKey:ExportConfigurationKeyPlaylistCustomSortProperties]; 189 | } 190 | 191 | - (void)setCustomSortOrderDict:(NSDictionary*)dict { 192 | 193 | [super setCustomSortOrderDict:dict]; 194 | 195 | [_userDefaults setObject:dict forKey:ExportConfigurationKeyPlaylistCustomSortOrders]; 196 | } 197 | 198 | - (void)loadPropertiesFromUserDefaults { 199 | 200 | MLE_Log_Info(@"UserDefaultsExportConfiguration [loadPropertiesFromUserDefaults]"); 201 | 202 | [_userDefaults registerDefaults:[self defaultValues]]; 203 | 204 | [super loadValuesFromDictionary:[_userDefaults dictionaryRepresentation]]; 205 | 206 | if ([self generatedPersistentLibraryId] == nil) { 207 | [self setGeneratedPersistentLibraryId:[ExportConfiguration generatePersistentLibraryId]]; 208 | } 209 | 210 | [self migrateOutdatedSortProperties]; 211 | 212 | // TODO: validate sort properties 213 | } 214 | 215 | - (void)migrateOutdatedSortProperties { 216 | 217 | MLE_Log_Info(@"UserDefaultsExportConfiguration [migrateOutdatedSortProperties] Migrating any stale sort columns"); 218 | 219 | NSUInteger updatedCount = 0; 220 | 221 | NSMutableDictionary* sortProperties = [[self playlistCustomSortPropertyDict] mutableCopy]; 222 | for (NSString* playlistID in [sortProperties allKeys]) { 223 | 224 | NSString* sortProperty = [sortProperties valueForKey:playlistID]; 225 | 226 | // currently stored sort property is listed in migrated property values 227 | if ([[SorterDefines migratedProperties] objectForKey:sortProperty] != nil) { 228 | 229 | NSString* newSortProperty = [[SorterDefines migratedProperties] objectForKey:sortProperty]; 230 | 231 | // ensure old property != new property (possible since migratedProperties dict uses ITLibMediaItem exports as values 232 | if ([sortProperty isNotEqualTo:newSortProperty]) { 233 | MLE_Log_Info(@"UserDefaultsExportConfiguration [migrateSortColumnsToSortProperties] Migrated playlist '%@' sort property from '%@' to '%@'", playlistID, sortProperty, newSortProperty); 234 | [sortProperties setValue:newSortProperty forKey:playlistID]; 235 | updatedCount++; 236 | } 237 | } 238 | } 239 | if (updatedCount > 0) { 240 | MLE_Log_Info(@"UserDefaultsExportConfiguration [migrateSortColumnsToSortProperties] Total deprecated sort properties migrated: %lu", (unsigned long)updatedCount); 241 | [self setCustomSortPropertyDict:sortProperties]; 242 | } 243 | } 244 | 245 | - (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject change:(NSDictionary *)aChange context:(void *)aContext { 246 | 247 | MLE_Log_Info(@"UserDefaultsExportConfiguration [observeValueForKeyPath:%@]", aKeyPath); 248 | 249 | if ([aKeyPath isEqualToString:_outputDirectoryBookmarkKey]) { 250 | 251 | NSData* bookmarkData = [_userDefaults dataForKey:aKeyPath]; 252 | if (bookmarkData == nil) { 253 | [self setOutputDirectoryUrl:nil]; 254 | return; 255 | } 256 | // update output directory url 257 | else { 258 | NSURL* bookmarkURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:nil error:nil]; 259 | [self setOutputDirectoryUrl:bookmarkURL]; 260 | } 261 | } 262 | } 263 | 264 | @end 265 | -------------------------------------------------------------------------------- /src/Common/Sorter/SorterDefines.m: -------------------------------------------------------------------------------- 1 | // 2 | // SorterDefines.m 3 | // Music Library Exporter 4 | // 5 | // Created by Kyle King on 2022-11-17. 6 | // 7 | 8 | #import "SorterDefines.h" 9 | 10 | #import 11 | 12 | static SorterDefines* _sharedDefines; 13 | 14 | @interface SorterDefines () 15 | 16 | @property NSArray* allProperties; 17 | @property NSSet* allPropertiesSet; 18 | 19 | @property NSDictionary* propertyNames; 20 | @property NSDictionary* propertySubstitutions; 21 | 22 | @property NSDictionary* fallbackSortProperties; 23 | @property NSArray* defaultFallbackSortProperties; 24 | 25 | @property NSDictionary* migratedProperties; 26 | 27 | - (void)populateAllProperties; 28 | 29 | - (void)populatePropertyNames; 30 | - (void)populatePropertySubstitutions; 31 | 32 | - (void)populateFallbackSortProperties; 33 | - (void)populateDefaultFallbackSortProperties; 34 | 35 | - (void)populateMigratedProperties; 36 | 37 | @end 38 | 39 | 40 | @implementation SorterDefines 41 | 42 | #pragma mark - Inintializers 43 | 44 | - (instancetype)init { 45 | 46 | if (self = [super init]) { 47 | 48 | NSAssert((_sharedDefines == nil), @"SorterDefines _sharedDefines has already been initialized"); 49 | 50 | [self populateAllProperties]; 51 | 52 | [self populatePropertyNames]; 53 | [self populatePropertySubstitutions]; 54 | 55 | [self populateFallbackSortProperties]; 56 | [self populateDefaultFallbackSortProperties]; 57 | 58 | [self populateMigratedProperties]; 59 | 60 | return self; 61 | } 62 | else { 63 | return nil; 64 | } 65 | } 66 | 67 | #pragma mark - Accessors 68 | 69 | + (NSArray*)allProperties { 70 | 71 | if (_sharedDefines == nil) { 72 | _sharedDefines = [[SorterDefines alloc] init]; 73 | } 74 | 75 | return _sharedDefines.allProperties; 76 | } 77 | 78 | + (NSSet*)allPropertiesSet { 79 | 80 | if (_sharedDefines == nil) { 81 | _sharedDefines = [[SorterDefines alloc] init]; 82 | } 83 | 84 | return _sharedDefines.allPropertiesSet; 85 | } 86 | 87 | + (NSDictionary*)propertyNames { 88 | 89 | if (_sharedDefines == nil) { 90 | _sharedDefines = [[SorterDefines alloc] init]; 91 | } 92 | 93 | return _sharedDefines.propertyNames; 94 | } 95 | 96 | + (NSDictionary*)propertySubstitutions { 97 | 98 | if (_sharedDefines == nil) { 99 | _sharedDefines = [[SorterDefines alloc] init]; 100 | } 101 | 102 | return _sharedDefines.propertySubstitutions; 103 | } 104 | 105 | + (NSDictionary*)fallbackSortProperties { 106 | 107 | if (_sharedDefines == nil) { 108 | _sharedDefines = [[SorterDefines alloc] init]; 109 | } 110 | 111 | return _sharedDefines.fallbackSortProperties; 112 | } 113 | 114 | + (NSArray*)defaultFallbackSortProperties { 115 | 116 | if (_sharedDefines == nil) { 117 | _sharedDefines = [[SorterDefines alloc] init]; 118 | } 119 | 120 | return _sharedDefines.defaultFallbackSortProperties; 121 | } 122 | 123 | + (NSDictionary*)migratedProperties { 124 | 125 | if (_sharedDefines == nil) { 126 | _sharedDefines = [[SorterDefines alloc] init]; 127 | } 128 | 129 | return _sharedDefines.migratedProperties; 130 | } 131 | 132 | + (nullable NSString*)nameForProperty:(NSString*)property { 133 | 134 | return [[SorterDefines propertyNames] valueForKey:property]; 135 | } 136 | 137 | + (NSArray*)substitutionsForProperty:(NSString*)property { 138 | 139 | if ([[SorterDefines propertySubstitutions] objectForKey:property] != nil) { 140 | return [[_sharedDefines propertySubstitutions] valueForKey:property]; 141 | } 142 | else { 143 | return [NSArray array]; 144 | } 145 | } 146 | 147 | + (NSArray*)fallbackPropertiesForProperty:(NSString*)property { 148 | 149 | if ([[SorterDefines fallbackSortProperties] objectForKey:property] != nil) { 150 | return [[_sharedDefines fallbackSortProperties] valueForKey:property]; 151 | } 152 | else { 153 | return [_sharedDefines defaultFallbackSortProperties]; 154 | } 155 | } 156 | 157 | 158 | #pragma mark - Mutators 159 | 160 | - (void)populateAllProperties { 161 | 162 | _allProperties = @[ 163 | ITLibMediaItemPropertyAlbumTitle, 164 | ITLibMediaItemPropertyAlbumArtist, 165 | ITLibMediaItemPropertyAlbumRating, 166 | ITLibMediaItemPropertyAlbumDiscNumber, 167 | ITLibMediaItemPropertyArtistName, 168 | ITLibMediaItemPropertyBitRate, 169 | ITLibMediaItemPropertyBeatsPerMinute, 170 | ITLibMediaItemPropertyCategory, 171 | ITLibMediaItemPropertyComments, 172 | ITLibMediaItemPropertyComposer, 173 | ITLibMediaItemPropertyAddedDate, 174 | ITLibMediaItemPropertyModifiedDate, 175 | ITLibMediaItemPropertyDescription, 176 | ITLibMediaItemPropertyGenre, 177 | ITLibMediaItemPropertyGrouping, 178 | ITLibMediaItemPropertyKind, 179 | ITLibMediaItemPropertyTitle, 180 | ITLibMediaItemPropertyPlayCount, 181 | ITLibMediaItemPropertyLastPlayDate, 182 | ITLibMediaItemPropertyMovementName, 183 | ITLibMediaItemPropertyMovementNumber, 184 | ITLibMediaItemPropertyRating, 185 | ITLibMediaItemPropertyReleaseDate, 186 | ITLibMediaItemPropertySampleRate, 187 | ITLibMediaItemPropertySize, 188 | ITLibMediaItemPropertyUserSkipCount, 189 | ITLibMediaItemPropertySkipDate, 190 | ITLibMediaItemPropertyTotalTime, 191 | ITLibMediaItemPropertyTrackNumber, 192 | ITLibMediaItemPropertyWork, 193 | ITLibMediaItemPropertyYear, 194 | ]; 195 | 196 | _allPropertiesSet = [NSSet setWithArray:_allProperties]; 197 | } 198 | 199 | - (void)populatePropertyNames { 200 | 201 | _propertyNames = @{ 202 | ITLibMediaItemPropertyAlbumTitle: @"Album", 203 | ITLibMediaItemPropertyAlbumArtist: @"Album Artist", 204 | ITLibMediaItemPropertyAlbumRating: @"Album Rating", 205 | ITLibMediaItemPropertyAlbumDiscNumber: @"Disc Number", 206 | ITLibMediaItemPropertyArtistName: @"Artist", 207 | ITLibMediaItemPropertyBitRate: @"Bit Rate", 208 | ITLibMediaItemPropertyBeatsPerMinute: @"Beats Per Minute", 209 | ITLibMediaItemPropertyCategory: @"Category", 210 | ITLibMediaItemPropertyComments: @"Comments", 211 | ITLibMediaItemPropertyComposer: @"Composer", 212 | ITLibMediaItemPropertyAddedDate: @"Date Added", 213 | ITLibMediaItemPropertyModifiedDate: @"Date Modified", 214 | ITLibMediaItemPropertyDescription: @"Description", 215 | ITLibMediaItemPropertyGenre: @"Genre", 216 | ITLibMediaItemPropertyGrouping: @"Grouping", 217 | ITLibMediaItemPropertyKind: @"Kind", 218 | ITLibMediaItemPropertyTitle: @"Title", 219 | ITLibMediaItemPropertyPlayCount: @"Plays", 220 | ITLibMediaItemPropertyLastPlayDate: @"Last Played", 221 | ITLibMediaItemPropertyMovementName: @"Movement Name", 222 | ITLibMediaItemPropertyMovementNumber: @"Movement Number", 223 | ITLibMediaItemPropertyRating: @"Rating", 224 | ITLibMediaItemPropertyReleaseDate: @"Release Date", 225 | ITLibMediaItemPropertySampleRate: @"Sample Rate", 226 | ITLibMediaItemPropertySize: @"Size", 227 | ITLibMediaItemPropertyUserSkipCount: @"Skips", 228 | ITLibMediaItemPropertySkipDate: @"Last Skipped", 229 | ITLibMediaItemPropertyTotalTime: @"Time", 230 | ITLibMediaItemPropertyTrackNumber: @"Track Number", 231 | ITLibMediaItemPropertyWork: @"Work", 232 | ITLibMediaItemPropertyYear: @"Year", 233 | }; 234 | } 235 | 236 | - (void)populatePropertySubstitutions { 237 | 238 | _propertySubstitutions = @{ 239 | ITLibMediaItemPropertyTitle: @[ 240 | ITLibMediaItemPropertySortTitle, 241 | ITLibMediaItemPropertyTitle, 242 | ], 243 | ITLibMediaItemPropertyAlbumTitle: @[ 244 | ITLibMediaItemPropertySortAlbumTitle, 245 | ITLibMediaItemPropertyAlbumTitle, 246 | ], 247 | ITLibMediaItemPropertyAlbumArtist: @[ 248 | ITLibMediaItemPropertySortAlbumArtist, 249 | ITLibMediaItemPropertyAlbumArtist, 250 | ITLibMediaItemPropertySortArtistName, 251 | ITLibMediaItemPropertyArtistName, 252 | ], 253 | ITLibMediaItemPropertyArtistName: @[ 254 | ITLibMediaItemPropertySortArtistName, 255 | ITLibMediaItemPropertyArtistName, 256 | ITLibMediaItemPropertySortAlbumArtist, 257 | ITLibMediaItemPropertyAlbumArtist, 258 | ], 259 | ITLibMediaItemPropertyComposer: @[ 260 | ITLibMediaItemPropertySortComposer, 261 | ITLibMediaItemPropertyComposer, 262 | ], 263 | }; 264 | } 265 | 266 | - (void)populateFallbackSortProperties { 267 | 268 | _fallbackSortProperties = @{ 269 | ITLibMediaItemPropertyAlbumTitle: @[ 270 | ITLibMediaItemPropertyAlbumArtist, 271 | ITLibMediaItemPropertyAlbumDiscNumber, 272 | ITLibMediaItemPropertyTrackNumber, 273 | ITLibMediaItemPropertyTitle, 274 | ITLibMediaItemPropertyYear, 275 | ITLibMediaItemPropertyAddedDate 276 | ], 277 | }; 278 | } 279 | 280 | - (void)populateDefaultFallbackSortProperties { 281 | 282 | _defaultFallbackSortProperties = @[ 283 | ITLibMediaItemPropertyAlbumArtist, 284 | ITLibMediaItemPropertyAlbumTitle, 285 | ITLibMediaItemPropertyAlbumDiscNumber, 286 | ITLibMediaItemPropertyTrackNumber, 287 | ITLibMediaItemPropertyTitle, 288 | ITLibMediaItemPropertyYear, 289 | ITLibMediaItemPropertyAddedDate, 290 | ]; 291 | } 292 | 293 | - (void)populateMigratedProperties { 294 | 295 | _migratedProperties = @{ 296 | @"Title": ITLibMediaItemPropertyTitle, 297 | @"Artist": ITLibMediaItemPropertyArtistName, 298 | @"Album Artist": ITLibMediaItemPropertyAlbumArtist, 299 | @"Date Added": ITLibMediaItemPropertyAddedDate, 300 | }; 301 | } 302 | 303 | 304 | 305 | 306 | @end 307 | -------------------------------------------------------------------------------- /src/Music Library Exporter Helper/DirectoryPermissionsWindow/DirectoryPermissionsWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | When automatic library exporting is enabled, Music Library Exporter uses it's background helper tool to generate your library without the main application being open.

Since this helper tool is a separate application, it requires your permission to write to the directory that you have selected.

You will only be asked to grant save permissions the first time that automatic exporting is enabled, or when the output directory option is updated. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | In order for automatic exports to work correctly, the location that you choose must be the same as the Output Directory that was selected from the Music Library Exporter main application. 52 | 53 | 54 | 55 | 56 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | --------------------------------------------------------------------------------