Music.app tags: use property and variable in array. Dynamic update

Hey all,

I need help converting a string to a track property that will help my current code.
I have a display dialog showing what I am attempting to do, these are all strings.
Potentially there is a better way to achieve the same result, maybe scripting o’s property which I need to learn when to invoke.

In the past, I’ve been relying on many lines of code to filter out text if the item begins with, contains, or ends with before I use ASOC in a subroutine. I’m attempting to do the filtering process using strings with repeat loops here which will help de-duplicate many lines of code.

I’m only focusing on 3 track tags here, but many more will be added. The idea is to remove domain names through ASOC after I have filtered results by text.


use framework "Foundation"
use scripting additions

tell application "Music"
	
	set trackID to (get persistent ID of every file track of library playlist 1)
	set trackName to (get name of every file track of library playlist 1)
	set trackArtist to (get artist of every file track of library playlist 1)
	set trackComment to (get comment of every file track of library playlist 1)
	
	repeat with a from 1 to count of the trackID
		set listBeginsWith to {" "}
		set listContains to {"  ", "www.", "http"}
		set listEndsWith to {".com", ".net", ".org", ".co", ".su", ".xyz", ".club", ".me", ".ro", ".biz", ".info", ".pro", ".us", " ", "-"}
		
		--set trackTag to {{name:item a of trackName}, {artist:item a of trackArtist}, {comment:item a of trackComment}}
		set trackTag to {{"name", item a of trackName}, {"artist", item a of trackArtist}, {"comment", item a of trackComment}}
		
		repeat with thisTag in trackTag
			repeat with thisBeginsWith in listBeginsWith
				if item 2 of thisTag begins with thisBeginsWith then
					display dialog "PROPERTY: " & item 1 of thisTag & return & "TAG: " & item 2 of thisTag & return & "BEGINS WITH: " & thisBeginsWith
					set item 1 of thisTag of (some track of library playlist 1 whose persistent ID is item a of trackID) to my cleanUpText(item 2 of thisTag)
				end if
			end repeat
			
			repeat with thisContains in listContains
				if item 2 of thisTag contains thisContains then
					display dialog "PROPERTY: " & item 1 of thisTag & return & "TAG: " & item 2 of thisTag & return & "CONTAINS: " & thisContains
					set item 1 of thisTag of (some track of library playlist 1 whose persistent ID is item a of trackID) to my cleanUpText(item 2 of thisTag)
				end if
			end repeat
			
			repeat with thisEndsWith in listEndsWith
				if item 2 of thisTag ends with thisEndsWith then
					display dialog "PROPERTY: " & item 1 of thisTag & return & "TAG: " & item 2 of thisTag & return & "ENDS WITH: " & thisEndsWith
					set item 1 of thisTag of (some track of library playlist 1 whose persistent ID is item a of trackID) to my cleanUpText(item 2 of thisTag)
				end if
			end repeat
		end repeat
		
	end repeat
end tell

on cleanUpText(someText)
	set theNSString to current application's NSString's stringWithString:someText
	
	set theOptions to (current application's NSRegularExpressionSearch as integer) + (current application's NSRegularExpressionAnchorsMatchLines as integer) + (current application's NSRegularExpressionCaseInsensitive as integer)
	
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"(http.*)?(www\\.)?[\\w]+\\.(com|net|org|co|su|xyz|club|me|ro|biz|info|pro|us)" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"^[\\d]+\\. +" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"-( +)?$" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:" +" withString:" " options:theOptions range:{location:0, |length|:theNSString's |length|()}
	
	set theWhiteSet to current application's NSCharacterSet's whitespaceAndNewlineCharacterSet()
	set theNSString to theNSString's stringByTrimmingCharactersInSet:theWhiteSet
	
	return theNSString as text
end cleanUpText

Properties of tracks in Music.app is not array of keys and values, but a record. Also, AppleScript has lists and not arrays.

You can’t do repeat loop with keys in the record. Because the key-value pairs are not indexed in the record. For example, following attempt should throw error:


set theCount to (count {aKey:"aValue", cKey:"cValue"})

repeat with i from 1 to theCount
	item i of {aKey:"aValue", cKey:"cValue"}
end repeat

The proper way to change values in the record doesn’t use repeat loops, but key names:


tell {aKey:"aValue", cKey:"cValue"} -- source record
	set aKey of it to "cleanedKeyValue"
	set cKey of it to "cleanedKeyValue2"
	return it -- changed record
end tell

Could you give some examples of what your wanting to replace?
Have you checked and tested your RegEx Patterns?

I see that you already have RegEx Patterns.
I’ve got a bunch of Extensions on NSString and NSRegularExpression and NSDictionary
that I’ve trimmed down to a Framework that you can import and use.

Many of them help simply the creation of items.
For Example:

NSString
-(NSRegularExpression*)stringAsRegExPattern;
-(NSRange)fullRange;
-(NSString*)cleanAllWhiteSpace;

-(NSRegularExpression*)stringAsRegExPattern {
    NSString* pattern = self;
    NSRegularExpressionOptions regexOptions = NSRegularExpressionCaseInsensitive | NSRegularExpressionUseUnicodeWordBoundaries;
    NSError *error = NULL;
    NSRegularExpression *regex = [NSRegularExpression
                                  regularExpressionWithPattern:pattern
                                  options:regexOptions
                                  error:&error];
    if (error) {
        NSLog(@"%s  --------------- ERROR CREATING THE REGEX", __PRETTY_FUNCTION__);
        return nil;
    }
    return regex;
}

-(NSRange)fullRange {
    return NSMakeRange(0, self.length);
}

-(NSString*)cleanAllWhiteSpace {
    NSString* dirtyString = [NSString stringWithString:self];
    NSRegularExpressionOptions regexOptions = NSRegularExpressionCaseInsensitive;
    NSString* pattern = @"[\\s]+";
    NSError *error = NULL;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:regexOptions error:&error];
    if (error) {
        NSLog(@"Couldn't create regex with given string and options");
    }
    
    NSString* cleanSpaceString = [regex stringByReplacingMatchesInString:dirtyString options:0 range:dirtyString.fullRange withTemplate:@" "];
    
    NSString* cleanEndSpace = [cleanSpaceString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    return cleanEndSpace;
}
NSRegularExpression
-(BOOL)containsMatchesForString:(NSString*)aString;
-(NSUInteger)countOfMatchesInString:(NSString*)aString;
-(NSString*)replaceMatchesInString:(NSString*)aString
                      withTemplate:(NSString*)replaceString;

-(BOOL)containsMatchesForString:(NSString*)aString {
    BOOL hasMatches = ([self countOfMatchesInString:aString] > 0);
     return hasMatches;
}

-(NSUInteger)countOfMatchesInString:(NSString*)aString {
    NSUInteger aCount = 0;
    if (aString.ok) {
        aCount = [self numberOfMatchesInString:aString
                                       options:0 range:aString.fullRange];
    }
    return aCount;
}

-(NSString*)replaceMatchesInString:(NSString*)aString
                      withTemplate:(NSString*)replaceString {
    NSString* newString;
    NSString* source = aString;
    if (source.ok) {
        newString = [self
                     stringByReplacingMatchesInString:source
                     options:0
                     range:source.fullRange
                     withTemplate:replaceString];
        newString = [newString cleanAllWhiteSpace];
    }
    return newString;
}
NDDictionary
+(NSDictionary*)dictionaryWithDictionary:(NSDictionary*)aDict
                            withOnlyKeys:(NSArray*)keepKeys;
+(NSDictionary*)dictionaryWithDictionary:(NSDictionary*)aDict
                            withOnlyKeys:(NSArray*)keepKeys
                    replaceMatchesInString:(NSString*)aFlagString
                              withString:(NSString*)aReplaceString;


+(NSDictionary*)dictionaryWithDictionary:(NSDictionary*)aDict
                            withOnlyKeys:(NSArray*)keepKeys {
    NSMutableDictionary* trimDict = [NSMutableDictionary dictionaryWithDictionary:aDict];
    NSArray* allKeys = trimDict.allKeys;
    for (NSString* aKey in allKeys) {
        if (![keepKeys containsObject:aKey]) {
            [trimDict removeObjectForKey:aKey];
        }
    }
    return trimDict;
}

+(NSDictionary*)dictionaryWithDictionary:(NSDictionary*)aDict
                            withOnlyKeys:(NSArray*)keepKeys
                  replaceMatchesInString:(NSString*)aFlagString
                              withString:(NSString*)aReplaceString {
    NSMutableDictionary* aCleanedDict = [NSMutableDictionary new];
    NSDictionary* aSourceDict = [self dictionaryWithDictionary:aDict
                                                         withOnlyKeys];
    NSRegularExpression* aRegEx = [aFlagString stringAsRegExPatternForMatchWordType:RxMatch_Any];
    NSArray* allKeys = aSourceDict.allKeys;
    for (NSString* aKey in allKeys) {
        NSString* aValue = [aSourceDict valueForKey:aKey];
        if ([aRegEx containsMatchesForString:aValue]) {
       NSString* aCleanString = [aRegEx replaceMatchesInString:aValue
                          withTemplate:aReplaceString];
         [aCleanedDict setValue:aCleanString
                        forKey:aKey];
        }
    }
    return aCleanedDict;
}

I think you could use the NSDictionary Extension (may need to change it a bit
but you get the idea)

set keepKeys to currentApplication's NSArray's arrayWithArray:{"name", "artist", "comment"}
set aFlagString to currentApplication's NSString's stringWithString:".com .net .org .co"

which will create aPattern “\b(\.com|\.new|\.org|\.co)\b”
you could alway change it so you supply a pattern to the function
set aPattern to urrent application’s NSString’s stringWithString:“(http.*)?(www\.)?[\w]+\.(com|net|org|co|su|xyz|club|me|ro|biz|info|pro|us)”


repeat with aTrack in allLibraryTracks 
    set aCleanedDict to currentApplication's NSDictionary's dictionaryWithDictionary:(aTrack's properties)
                            withOnlyKeys:keepKeys
                    replaceMatchesInString:aFlagString
                              withString:""

    set cleanedKeys to aCleanedDict's allKeys()
        repeat with aKey in cleanedKeys
            aTracks's setValue:(aCleanedDict's valueForKey:aKey) forKey:aKey
        end repeat
end repeat

basically you go thru each track
get a dictionary from the properties of the tracks that contains on the keys you want
go thru that dictionary and check the values, clean if needed
if they are cleaned add that info to aCleanedDict
get back a cleaned dictionary
update the track with values from that dictionary

actually I just updated and added another function on NSDictionary

+(NSDictionary*)dictionaryWithDictionary:(NSDictionary*)aDict
                            withOnlyKeys:(NSArray*)keepKeys
                  replaceMatchesInPattern:(NSString*)aPattern
                              withString:(NSString*)aReplaceString;

you can DL the framework here:
https://drive.google.com/drive/folders/1gA_zrWl2sRq8zbwL36Wh3djDnOHQo83A?usp=sharing

Thanks all for the great suggestions. I’m still yet to experiment with the above suggestions but for now, here are some explanations for my code so far…

The reason for so many repeat loops is that I have found as per Nigel’s suggestion some time ago it is more efficient to qualify text first before passing it to a subroutine that invokes ASOC clean-up.

I started using repeat loops because I was searching for a better way of writing repetitive code.

Has anyone ever used the ‘eval’ command in Linux (bash/zsh)? What I wouldn’t give to concatenate a line of applescript code with simple text in my OP where (item 1 of thisTag) could be converted from string {“title”, “artist”, “comment”} to property of record as it is reflected in the ‘display dialogue’. So there has to be a better way.

Because I am dealing with so many records, and have never had a problem with scanning/updating every file track of Library Playlist 1 (and I would prefer to do this rather than selecting tracks), I found that text filtering needed something similar to this on every tag I was checking before invoking my cleanUpText(someText)
This is just 1 tag I’m checking before invoking ASOC and it is so much faster by qualifying the text first… although looking forward to checking out @technomorph dictionary framework.


					if item a of trackComment begins with " " or ¬
						item a of trackComment contains "  " or ¬
						item a of trackComment contains "www." or ¬
						item a of trackComment contains "http" or ¬
						item a of trackComment ends with ".com" or ¬
						item a of trackComment ends with ".net" or ¬
						item a of trackComment ends with ".org" or ¬
						item a of trackComment ends with ".co" or ¬
						item a of trackComment ends with ".su" or ¬
						item a of trackComment ends with ".xyz" or ¬
						item a of trackComment ends with ".club" or ¬
						item a of trackComment ends with ".me" or ¬
						item a of trackComment ends with ".ro" or ¬
						item a of trackComment ends with ".su" or ¬
						item a of trackComment ends with ".biz" or ¬
						item a of trackComment ends with ".info" or ¬
						item a of trackComment ends with ".pro" or ¬
						item a of trackComment ends with ".us" or ¬
						item a of trackComment ends with " " or ¬
						item a of trackComment ends with "-" then
						set comment of (some track of library playlist 1 whose persistent ID is item a of trackID) to my cleanUpText(item a of trackComment)
					end if


on cleanUpText(someText)
	set theNSString to current application's NSString's stringWithString:someText
	set theOptions to (current application's NSRegularExpressionSearch as integer) + (current application's NSRegularExpressionAnchorsMatchLines as integer) + (current application's NSRegularExpressionCaseInsensitive as integer)
	
	-- remove a domain that optionally may start with http or www.  
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"(http.*)?(www\\.)?[\\w]+\\.(com|net|org|co|su|xyz|club|me|ro|biz|info|pro|us)" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	
	-- remove specific digits pattern e.g. "01. ", "02. ", "03. " when track numbers appear at the start of any tag 
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"^[\\d]+\\. +" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	
	-- remove trailing hyphens and spaces after domain is removed
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:"-( +)?$" withString:"" options:theOptions range:{location:0, |length|:theNSString's |length|()}
	
	-- replace double spaces with a single space
	set theNSString to theNSString's stringByReplacingOccurrencesOfString:" +" withString:" " options:theOptions range:{location:0, |length|:theNSString's |length|()}
	
	-- trim beginning and trailing whitespace
	set theWhiteSet to current application's NSCharacterSet's whitespaceAndNewlineCharacterSet()
	set theNSString to theNSString's stringByTrimmingCharactersInSet:theWhiteSet
	
	return theNSString as text
end cleanUpText


This is where RegEx is your friend .
Once you build the right pattern you can check for
All of your tests and replace them as needed.

Apple events are pretty taxing that’s why I
Suggest getting all the properties in one request.

Then checking for pattern matches and cleaning.
Only setting new changed values
On the track. (Again reducing calls)