My iTunes / Traktor tagging Workflow

I was going to post this as a reply to another topic but figured would post as separate topic

I DJ 2-3 Times a week in restaurants, pubs, bars, concerts and festivals (like Bass Coast and Shambhala)

I use Traktor, iTunes, Spotify, Kid3, dbPowerAmp, SoulSeek and my own custom scripts, appleScripts, ObjC Code.

Along with many various help code bits I have been in constant development of the following

- KTelHelperUtilities Framework mainly many categories / extensions to help
- like NSArray

-(BOOL)notEmpty 
    - so rather than 
if (aArray.count > o) then
    - use 
if (aArray.notEmpty) then
- NSString 
-(Range)fullRange
    - so for RegEx stuff rather than 
NSMakeRange(0, aString.length)
    - use 
aString.fullRange
- NSString 
-(BOOL)matchesAllWordsInString:(NSString*)aString;
- NSString 
-(BOOL)matchesAnyWordsInString:(NSString*)aString;
     - above takes aString, splits into words, creates aPattern based on
            matchingType, creates a regEx from aPattern, evaluates countOfMatches
     - many of the above have further categories on each IE 
regEx containsMatchesInString;
 - also use above to filter an arrayOfStrings, using a [b]NSPredicate[/b] and [b]NSRegularExpression[/b].
  • I have a further category on NSObject which extends use of
dictionaryWithValuesForKeys:
      - its 
-(NSArray*)objectNonNullValuesForKeys:(NSArray*)keys;
  • have custom DJSpecific extensions
    • using many of the above functions say want to find all tracks in iTunes who’s
      title, sortTitle, artists, albumArtist, sortArtist, sortAlbumArtist, album contain aArtistName
    • once I create aRegEx from aArtistName I create a
predicateWithBlock
      - i get trackKeys for titles, sortTitle,artists, albumArtist, sortArtist, sortAlbumArtist, album
  - get aArray of values using 
aTrack objectNonNullValuesForKeys:trackKeys
  - then use 
aRegEx matchesAnyStringsInArray:aArray

to return a boolean for the predicate

- SoulSeek Search App
- automate searching SoulSeek/iTunes/Spotify
- automate searching from currently playing track in iTunes/Spotify

- Spotify Suggestions App
- take Spotify or iTunes playlist and import it, find matching Spotify Tracks
- use Spotify api to get suggested tracks for each track
- set of suggestions based only on that track
- set of suggestions based on that track plus surrounding tracks (-2/theTrack/+2)
- allow user to preview(listen) and rate the suggested tracks
- filter and combine tracks based on user scoring, number of suggestions, saved favorites
and create 5 different playlists based on above
- allow to create Spotify Playlists from these new playlists
- allow to search current iTunes library to find tracks that match these suggestions
- allow to export any tracks not matched to a file to be used for external fetching of tracks
- allow to create iTunes playlists from matched tracks
- will create dummy missing tracks in playlist for easy replacement later

- Traktor Current Track Suggestions App
- program gets the newly loaded track
- searches the Traktor history playlists and suggests best tracks to have played before
within a -5/+5 distance in each history playlist. Scores by distance and count.
- (in future will use Spotify api to see what Spotify suggests based on single track and
the history suggestions, then find matching tracks in iTunes library)
- allows the selection of suggested track to be loaded into Traktor

- iTunes Playlist Helper
- import folders as playlists
- checks for dupes
- allow tagging updates
-etc

My music tagging work flow:

1) Obtain new music
-use SoulSeek and have some scripts to automate this)

2) Use kid3 to preTag my music

  • kid3 has some great scripting, regEx, automation that is easily customizable to my needs
  • use some QML java scripts to further automate customizing)
  • some customizing use regEx to calculate the source file type, and to set custom Traktor Tags

3) Use dBPowerAmp to convert Files

  • I convert to m4a 512kb 24Bit / 96kHz files
  • only program that can create higher res m4a files
  • automated destination location based on source paths
  • some automated tag scripting

4) Use custom App to import into iTunes

  • can import multiple folders
  • placing them in folders and playlists naming them based on the Finders Folder Name / Parent Folder Name

5) Once in iTunes continue further custom tagging:

  • set episodeID to the tracks persistentID (persistentID is not stored in tag)
  • use Kid3cli script to set Traktor’s catalog number tag to the episodeID

[center]^^^^^^^^^ idea is to have each file tagged with it’s persistentID in the epsodeID…
this is the only constant in iTunes to use to find the unique file and want to make this availble to other programs

[/center]

6) Tag With my custom iTunes helper program

  • gets selected files/playlists into “Source Files”
  • then finds all matching files based on the artists in the source files.
  • Places these in “Matched Files” and groups them by artist.
  • For each group calculates best matched genres, similar artists, collab artists etc.
  • Then the genre tagging allows many further functions from there.

7) Update the tracks genres

8) Anying Tagging Updates to Tracks Do the Following

  • updates a custom taggingHistoryDict.
  • with keys of persistentID, currentLocation, oldLocations, changedTags, previousTags etc
  • looks like:
{
  "currentLocation" : "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Stevie Nicks/Bella Donna (Deluxe Edition) HD/Edge of Seventeen.m4a",
  "persistentID" : "60AE4F2C160D5672",
  "changes" : [
    {
      "dateCreated" : "2022-07-08, 06:02:45"
    },
    {
      "newValues" : {
        "genre" : "Rock - Classic - Pop - Female - 80s - FM Radio"
      },
      "oldValues" : {
        "genre" : "Rock"
      },
      "dateChanged" : "2022-07-08, 06:02:45"
    }
  ],
  "locations" : [
    "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Stevie Nicks/Bella Donna (Deluxe Edition) HD/Edge of Seventeen.m4a"
  ]
}
  • the above gets stored into the lyrics tag as JSONtext.

8) Update the files finder comments

  • with a smaller version of above. Usually just the persistentID, and oldLocation
  • i have been moving away from this…

9) If Location Changes Do Following:

  • Any time any of my scripts / programs change any tags.
  • It compares the old location of the track. To it’s new location.
  • If it has changed. It sends the persistentID, oldPath, newPath info to
  • my KTelTrackRelocator class. Which handles a plist file of a array of dictionaries
  • Each dict looks like below:
  1. I have been manually using this to help relocate tracks in my Traktor program and will in the future use this to help automate the process for me with Changing XML file in Traktor or performing User Interface scripting automation along with the Finder.

  2. Also my tagging scripts and classes if the location changes. It will also automate the process of adding the changed track to a iTunes playlist called “2022 Master Relocate List” which I can use to tell which tracks tracks location has changed.

if anyone is interested in any bits or pieces let me know

1 Like

Really appreciating you sharing your workflow.
Over time, I’m looking forward to trying this out. I really do need to get into the Xcode/Objective-C side of things with some of the work you do.
Storing persistentID… I’m really liking this. I’d love to be able to automate this without 3rd party tools, but that’s for later.
Thank you.

It takes a bit to figure out the workings of Xcode, once you get it going the whole
interface builder is really a great tool.
The general Class and moving from AsOBJc isn’t so bad. I can help with anything.

OK here’s AppleScript code (Based on a template that can easily be reused)
That will set the tvEpisodeID tag of each track to it’s persistentID.
This tag can be read by other programs and used. (see below)


--
--	Created by: Kerry Uchida
--	Created on: 2021-09-04
--
--	Copyright (c) 2021 MyCompanyName
--	All Rights Reserved
--
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

--property tagKey : "pEpD"
property tagDesc : "episode ID"
property processDesc : "Add PersistentID to " & tagDesc
property confirmMessage : "Confirm " & processDesc
property finishMessage : "Finished " & processDesc

property tracksToProcess : {}
property currentIndex : 0
property totalCount : 0

-- show a progress report every progressFactor tracks
-- set to 0 to not show any progress
property progressFactor : 20
property showProgress : (progressFactor ≠ 0)

try
	set aAlert to (display dialog confirmMessage buttons {"OK", "Cancel"} default button "OK")
	set alertButton to (button returned of aAlert)
	if alertButton is "OK" then
		my selectTracksToProcess()
	else
		return
	end if
on error
	return
end try


(*
	Can Use as Template
	Get the selected tracks and add to tracksToProcess
	Loop thru the tracksToProcess
	Process Track THIS IS WHERE CAN CUSTOMIZE YOUR OWN CODE
	Display completed Alert
*)

on selectTracksToProcess()
	try
		tell application "iTunes"
			set sel to selection
			set totalCount to 0
			-- if tracks are selected
			if sel is not {} then
				-- Repeat with a reference to each of the selected tracks; 
				repeat with aTrack in sel
					copy aTrack to the end of my tracksToProcess
				end repeat
				-- if no tracks are selected	
			else
				-- repeat with a reference for each track in the currently selected playlist.
				set thePlaylist to view of the front browser window
				repeat with i from 1 to (index of last track of thePlaylist)
					set aTrack to track i of thePlaylist
					copy aTrack to the end of my tracksToProcess
				end repeat
			end if
			
			set my totalCount to (count of my tracksToProcess)
		end tell
		
		if (my totalCount > 0) then
			my processTracks()
			set aCompleteMessage to finishMessage & " for " & totalCount & " Tracks."
			set aAlertDone to (display dialog aCompleteMessage buttons {"OK"} default button "OK")
			set alertDoneButton to (button returned of aAlertDone)
		end if
	end try
end selectTracksToProcess

(*
	Process All the tracks in my tracksToProcess
*)
on processTracks()
	repeat with currentIndex from 1 to totalCount
		set aTrack to item currentIndex of (my tracksToProcess)
		(my processTrack:aTrack)
		set updateProgress to (showProgress and (currentIndex mod progressFactor) = 0) as boolean
		if updateProgress then
			set progressMessage to (currentIndex as text) & " of " & (totalCount as text) & "completed"
			display dialog progressMessage buttons {"√"} giving up after 1 with icon 1
		end if
	end repeat
end processTracks

(*
	-- do something with aTrack.
			-- This is where can customize any processing
*)
on processTrack:aTrack
	tell application "iTunes"
		set aPersistentID to persistent ID of aTrack
		set aEpisodeID to episode ID of aTrack
		if (aPersistentID is not aEpisodeID) then
			set episode ID of aTrack to aPersistentID
			try
				if aTrack's class is file track then refresh aTrack
			end try
		end if
	end tell
end processTrack:


I am using multiple tools but many of the tools are either scriptable or shell scripts.
So I can pull all the code together into on place and execute them from there.

I highly recommend getting Kid3 as it’s really helped me bridge the gap between iTunes and Traktor and allows me to update Tags without having to rely on iTunes being open and the limited ability for iTunes not being able to write certain MetaData Tags that I need in Traktor.

https://kid3.kde.org/#git

1 Like

Here’s an expanded script of the above that also updates the File Info Comments field with a JSON like this:


{
"persistentID" : "557AB975DB89C2B9",
"trackLocation" : "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Compilations/Acid Jazz 25th Anniversary CD3 How'd We Get Us Here/Oh Shit.m4a"
}

I’ve only included changes to the template scripts:
change:


property confirmMessage : "Confirm " & processDesc & " And Update Finder Comment"

add:


property updateFinderComment : true

change:


try
	set aDefault to "BOTH"
	if updateFinderComment is false then set aDefault to "EpID ONLY"
	set aAlert to (display dialog confirmMessage buttons {"BOTH", "EpID ONLY", "Cancel"} default button aDefault)
	set alertButton to (button returned of aAlert)
	if alertButton is "Cancel" then
		return
	else if alertButton is "EpID ONLY" then
		set updateFinderComment to false
	end if
	my selectTracksToProcess()
on error
	return
end try

New Process Track Function


on processTrack:aTrack
	tell application "iTunes"
		set trackLocation to location of aTrack
		
		if (aTrack's class is file track) and (trackLocation is not missing value) then
			refresh aTrack
			set aPersistentID to persistent ID of aTrack
			set aEpisodeID to episode ID of aTrack
			if (aPersistentID is not aEpisodeID) then set episode ID of aTrack to aPersistentID
			
			if (updateFinderComment) then
				set trackPath to (POSIX path of trackLocation)
				set newFinderComment to "{" & linefeed & "\"persistentID\" : \"" & aPersistentID & "\"," & linefeed & "\"trackLocation\" : \"" & trackPath & "\"" & linefeed & "}"
				set trackAlias to trackLocation as alias
				
				tell application "Finder"
					set oldComment to comment of trackAlias
					if (newFinderComment is not oldComment) then
						try
							set comment of trackAlias to (newFinderComment)
						on error errMs number errNum
							display dialog errMs & return & errNum
						end try
					end if
				end tell
			end if
		end if
	end tell
end processTrack:

1 Like

Sorry if it’s just me, but we can’t access episode ID using “Music” anymore. This changed a while back. Let me know if I’m incorrect though.

I’ve looked at Kid3 before. Looks like at some point I’m going to need to use this.

The only thing I’ll find hard to let go of, is; over the years I have optimised AppleScript with Objective C to process the entire library of 15,000 tracks in under 6 minutes or less (mostly using lists and repeat loops that I find difficult explaining to others. The loops I use optimise everything). I’ll come back to this. Thank you.

I looked in to this a bit further… From my understanding, since 2019 Music.app (Catalina?) hasn’t been able to write to the tags that TV.app uses. We can see these tags when calling properties of the file track in Music.app, we just can’t write to them as we get a file permission error.

To quote Doug Adams

a script that targets composer of a TV track will fail. Because TV doesn't use the Composer tag. Or the Album Artist tag. Or the artist property. Instead of artist, TV has a director property. And a Music track doesn't understand show, season number or episode ID; a TV track won't grok movement or work properties.

But what better place to store the persistent ID than the Work tag? I won’t be using classical music in this workflow, and most of the time this tag field has URLs or spam.
I think I’m just going to settle for this for now…

tell application "Music"
	set allTracks to (get every file track of library playlist 1)
	repeat with aTrack in allTracks
		if aTrack's work is not aTrack's persistent ID then set aTrack's work to aTrack's persistent ID as text
	end repeat
end tell

I do happen to find this faster in every case I am iterating over lists… pull it in to memory first, do the calculations there, and only read from disk in one clean sweep and write to disk if necessary…

tell application "Music"
	set trackID to (get persistent ID of every file track of library playlist 1)
	set trackWork to (get work of every file track of library playlist 1)
	repeat with a from 1 to count of trackID
		if item a of trackID is not item a of trackWork then set work of (some track whose persistent ID is item a of trackID) to item a of trackID
	end repeat
end tell

Hmmm thats to bad about being able to set them.
You might wanna try a few possible “hacks” by doing:

tell application "Music"
    aTrack's setValue:aPersistentID forKey:"episode ID"
end tell

another option for the key is the 4Character code “tven”
other possible tags
text “show” or “tvsh”
text “grouping”
integer “episodeNumber” (script bridge) “episode number”
text “work” or
integer “movementNumber” (script bridge) “movement number”
text “movement”

I’ve yet to play with “description” or “long description”

If you want to integrate with Traktor you’ll really want to use Kid3
I copy the episodeID (movement in your case) to the CATALOG NUMBER via Kid3
I can send you my Kid3 QML scripts that do it and also I have AppleScript that can run the Kid3-CLI to do it as well. All you need for the CLI is the track location.

kid3-cli -c "set CATALOGNUMBER '26C32C011A818CE3' 2" /path/toFile

Kid3-cli -c "set CATALOGNUMBER '%@' 2 '%@'" , persitentID, filePath


kid3-cli -c "set CATALOGNUMBER '26C32C011A818CE3' 2" "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Barry White/Sings For Someone You Love HD/It's Ecstasy When You Lay Down Next To Me.m4a"

To Have it copy a tag from tven to catalognumber

kid3-cli -c "import tags '%{tven}' '%{catalognumber}(.+)' 2" /path/toFile

kid3-cli -c "import tags '%{tven}' '%{catalognumber}(.+)' 2" "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Barry White/Sings For Someone You Love HD/It's Ecstasy When You Lay Down Next To Me.m4a"

You can also use it to return JSON of all of the tags it can read:

kid3-cli -c "{\"method\": \"get\", \"params\": [\"all\",2]}" "/Volumes/Tekno/Users/kerry/Music/iTunes/iTunes Media/Music/Musical Youth/The Youth of Today/Pass The Dutchie (Special Dub Mix).m4a"

Which Returns:

   "result": {
        "taggedFile": {
            "fileName": "Pass The Dutchie (Special Dub Mix).m4a",
            "fileNameChanged": false,
            "format": "MP4 AAC 16 bit 513 kbps 30464 Hz 2 Channels 4:40",
            "tag2": {
                "format": "MP4",
                "frames": [
                    {
                        "changed": false,
                        "name": "Title",
                        "value": "Pass The Dutchie (Special Dub Mix)"
                    },
                    {
                        "changed": false,
                        "name": "Artist",
                        "value": "Musical Youth"
                    },
                    {
                        "changed": false,
                        "name": "Album",
                        "value": "The Youth of Today"
                    },
                    {
                        "changed": false,
                        "name": "Comment",
                        "value": "44 TRAKTOR"
                    },
                    {
                        "changed": false,
                        "name": "Date",
                        "value": "1982"
                    },
                    {
                        "changed": false,
                        "name": "Genre",
                        "value": "Reggae"
                    },
                    {
                        "changed": false,
                        "name": "Track Number",
                        "value": "6/6"
                    },
                    {
                        "changed": false,
                        "name": "Composer",
                        "value": "WAV 16bit/44.1khz"
                    },
                    {
                        "changed": false,
                        "name": "Catalog Number",
                        "value": "B64A7E4AF8E0888C"
                    },
                    {
                        "changed": false,
                        "name": "Label",
                        "value": "WAV 16bit/44.1khz"
                    },
                    {
                        "changed": false,
                        "name": "BPM",
                        "value": "75"
                    },
                    {
                        "changed": false,
                        "name": "Encoder Settings",
                        "value": "fdkaac 0.6.3, libfdk-aac 4.0.1, CBR 512kbps"
                    },
                    {
                        "changed": false,
                        "name": "MIXER",
                        "value": "KID3 MIXER TAG"
                    },
                    {
                        "changed": false,
                        "name": "Producer",
                        "value": "KID3 PRODUCER TAG"
                    },
                    {
                        "changed": false,
                        "name": "iTunSMPB",
                        "value": " 00000000 00000800 000002E8 00000000019A2918 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000"
                    },
                    {
                        "changed": false,
                        "name": "initialkey",
                        "value": "1d"
                    },
                    {
                        "changed": false,
                        "name": "publisher",
                        "value": "WAV 16bit/44.1khz"
                    },
                    {
                        "changed": false,
                        "name": "rating wmp",
                        "value": "153"
                    },
                    {
                        "changed": false,
                        "name": "replaygain_track_gain",
                        "value": "-2.3 dB"
                    },
                    {
                        "changed": false,
                        "name": "replaygain_track_peak",
                        "value": "0.852295"
                    },
                    {
                        "changed": false,
                        "name": "TV Episode",
                        "value": "B64A7E4AF8E0888C"
                    },
                    {
                        "changed": false,
                        "name": "TV Show Name",
                        "value": "CATNO"
                    }
                ]
            }
        }
    }
}

Also have a look at my deep hacking to get Traktor Private NITR Frame data as JSON from m4a file.

Includes data from Traktor Private Tags
Like Cue Points etc