├── .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 |
--------------------------------------------------------------------------------