{"id":17,"date":"2012-01-23T22:21:27","date_gmt":"2012-01-24T05:21:27","guid":{"rendered":"http:\/\/blogs.callutheran.edu\/jpdillon\/?p=17"},"modified":"2014-07-17T07:54:34","modified_gmt":"2014-07-17T14:54:34","slug":"lotusscript-interface-for-itunes","status":"publish","type":"post","link":"https:\/\/blogs.callutheran.edu\/jpdillon\/2012\/01\/23\/lotusscript-interface-for-itunes\/","title":{"rendered":"LotusScript Interface for iTunes"},"content":{"rendered":"<p>A while ago (a long while ago now!) I played around with creating a database for storing my iTunes tracks. The reason behind it was simple: I wanted to be able to synchronize playlists and last-played data between copies on different computers.<\/p>\n<p>More importantly, I wanted to learn more about working with external COM interfaces.<\/p>\n<p>To that end, I built a Notes application and wrote the attached script library.<\/p>\n<p>One should never apologize for their code, especially if it works, but I will point out that I never polished it up, so you may see some clunky bits. You&#8217;re welcome to play with it, adapt it to your own needs, or ignore it!<\/p>\n<p>Anyway, attached is a LotusScript file that walks through your collection of tracks and creates or updates matching Notes documents. It requires the iTunes SDK version 7.7 or later (available from Apple),<\/p>\n<p>Code is offered, of course, without warranty, guarantee, or support of any kind.<\/p>\n<p>NOTE: For some reason I&#8217;m not allowed to upload text or lss or zip files, so the code is included in plain text below.<\/p>\n<pre><!--more--><span style=\"color: #000080\">Option Public\r\nOption Declare\r\n\r\nUse \"Custom Error Handling\"\r\nUse \"Standard Functions v4\"\r\nUse \"Cycle Value Handling v3\"\r\nSub Initialize\r\n\r\n ' REQUIRES iTunes version 7.7 or later because of Persistent IDs\r\n\r\n On Error Goto errHandler\r\n\r\n Print \"Started BuildEntriesFromiTunes()\"\r\n\r\n Print \"Launching iTunes (if not already running) ... this may take a moment ...\"\r\n\r\n Call BuildEntriesFromiTunes()\u00a0\u00a0 \u00a0\r\n\r\n Print \"BuildEntriesFromiTunes() done\"\r\n\r\n Print \"\"\r\n\r\n Goto Done\r\n\r\nerrHandler:\r\n Call ErrorMessage( Err, Erl, Error$, Lsi_info( 2 ), True )\r\n Resume Done\r\nDone:\r\n\r\nEnd Sub\r\nPrivate Function GetLineFromTrack( _\r\niTunes As Variant, _\r\nitTrack As Variant, _\r\nvarArray As Variant _\r\n) As String\r\n\r\n ' build a pipe-separated string containing data of interest for a track\r\n\r\n ' if the track is invalid, exit with nothing in the string\r\n\r\n On Error Goto errHandler\r\n\r\n Dim i As Integer\r\n Dim intLen As Integer\r\n Dim strOut As String\r\n\r\n Const PIPE = \"|\"\r\n\r\n ' if we don't have a valid input track, bail\r\n If itTrack Is Nothing Then\r\n Print \"Invalid track discovered\"\r\n Exit Function\r\n End If\r\n\r\n Forall v In varArray\r\n strOut = strOut &amp; GetTrackValue( iTunes, itTrack, Cstr( v ) ) &amp; PIPE\r\n End Forall\r\n\r\n ' kill trailing PIPE symbol\r\n If Not Len( strOut ) = 0 Then\r\n strOut = Left$( strOut, Len( strOut ) - 1 )\r\n End If\r\n\r\n GetLineFromTrack = strOut\r\n\r\n Goto Done\r\n\r\nerrHandler:\r\n Call ErrorMessage( Err, Erl, Error$, Lsi_info( 2 ), False )\r\n Resume Done\r\nDone:\r\n\r\nEnd Function\r\nPrivate Function GetTrackValue( iTunes As Variant, _\r\nitTrack As Variant, strProperty As String ) As String\r\n\r\n ' given an iTunes Track input and the name of a track property, return\r\n ' the value for that property\r\n\r\n ' if property has no value, return empty string to save space\r\n\r\n On Error Goto errHandler\r\n\r\n Dim strTag As String\r\n Dim strValue As String\r\n Dim strCRLF As String\r\n\r\n Const LT = \"&lt;\"\r\n Const LTSLASH = \"&lt;\/\"\r\n Const GT = \"&gt;\"\r\n Const COLON = \":\"\r\n\r\n If itTrack Is Nothing Then\r\n GetTrackValue = \"\"\r\n Exit Function\r\n End If\r\n\r\n strCRLF = Chr$( 13 ) &amp; Chr$( 10 )\r\n\r\n strTag = Lcase$( strProperty ) ' default\r\n\r\n ' case is root of field name before preprocessing\r\n ' ex:\u00a0 \"skip count\" becomes field named \"skip_count_tx\"\r\n ' value is Track property\r\n\r\n Select Case Lcase$( strProperty )\r\n\r\n Case \"name\": strValue = itTrack.Name\r\n\r\n Case \"composer\": strValue = itTrack.Composer\r\n Case \"album\": strValue = itTrack.Album\r\n Case \"grouping\": strValue = itTrack.Grouping\r\n Case \"genre\": strValue = itTrack.Genre\r\n Case \"size\": strValue = itTrack.Size\r\n Case \"time\": strValue = itTrack.Time\r\n Case \"disc number\": strValue = itTrack.DiscNumber\r\n Case \"disc count\": strValue = itTrack.DiscCount\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\r\n Case \"track number\": strValue = itTrack.TrackNumber\r\n Case \"track count\": strValue = itTrack.TrackCount\r\n Case \"year\": strValue = itTrack.Year\r\n Case \"date modified\": strValue = itTrack.ModificationDate\r\n Case \"date added\": strValue = itTrack.DateAdded\r\n Case \"bit rate\": strValue = itTrack.BitRate\r\n Case \"sample rate\": strValue = itTrack.SampleRate\r\n Case \"volume adjustment\": strValue = itTrack.VolumeAdjustment\r\n Case \"kind\": strValue = itTrack.KindAsString\r\n Case \"equalizer\": strValue = itTrack.EQ\r\n Case \"comments\": strValue = itTrack.Comment\r\n Case \"play count\": strValue = itTrack.PlayedCount\r\n Case \"last played\": strValue = itTrack.PlayedDate\r\n Case \"skip count\": strValue = itTrack.SkippedCount\r\n Case \"last skipped\": strValue = itTrack.SkippedDate\r\n Case \"my rating\": strValue = itTrack.Rating\r\n\r\n Case \"location\":\r\n strValue = itTrack.Location\r\n If InStr( strValue, COLON ) &gt; 0 Then\r\n strValue = StrRight( strValue, COLON ) ' get rid of drive letter because of different storage paths\r\n End If\r\n\r\n Case \"trackid\": strValue = itTrack.TrackID\r\n Case \"trackdatabaseid\": strValue = itTrack.TrackDatabaseID\r\n Case \"internal_index\": strValue = itTrack.Index\r\n Case \"playorder_index\": strValue = itTrack.PlayOrderIndex\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\r\n Case \"artist\": strValue = itTrack.Artist\r\n\r\n Case \"persistent id low\": strValue = Hex$( iTunes.ITObjectPersistentIDHigh( itTrack ) )\r\n Case \"persistent id high\": strValue = Hex$( iTunes.ITObjectPersistentIDLow( itTrack ) )\r\n\r\n Case \"playlists\": strValue = \"\" ' get this later\r\n\r\n Case Else:\r\n strValue = \"Unrecognized Tag '\" &amp; strProperty &amp; \"'\"\r\n End Select\r\n\r\nGetTrackValue = Trim$( strValue ) \r\n\r\n Goto Done\r\n\r\nerrHandler:\r\n Print \"Failed for property: \" &amp; strProperty\r\n Call LogErrorEx( \"Failed for property: \" &amp; strProperty, \"FAIL\", Nothing )\r\n Print \"Track name: \" &amp; itTrack.Name\r\n Call LogErrorEx( \"Track name: \" &amp; itTrack.Name, \"FAIL\", Nothing )\r\n\r\n Print \"Track location: \" &amp; itTrack.Location\r\n Call LogErrorEx( \"Track location: \" &amp; itTrack.Location, \"FAIL\", Nothing )\r\n\r\n Call ErrorMessage( Err, Erl, Error$, Lsi_info( 2 ), False )\r\n Resume Done\r\nDone:\r\n\r\nEnd Function\r\nPrivate Function BuildEntriesFromiTunes() As Boolean\r\n\r\n On Error Goto errHandler\r\n\r\n Const DEBUG = False\r\n\r\n Dim strMsgList List As String\r\n\r\n Dim strLine As String\r\n Dim strRaw As String\r\n Dim strRawInDoc As string\r\n Dim strLocation As String\r\n Dim strPlayLists As String\r\n\r\n Dim i As Long\r\n Dim j As Long\r\n Dim intTracks As Long\r\n Dim intPL As Integer\r\n\r\n Dim varArray As Variant ' array for holding individual track values\r\n Dim varRoots As Variant ' root names for iTunes property searches\r\n Dim varFields As Variant ' array for holding field names\r\n\r\n Dim lngCount As Long\r\n Dim lngCreated As Long\u00a0\u00a0 \u00a0\r\n Dim lngUpdated As Long\r\n Dim strKey As String\r\n Dim boolCreated As Boolean\r\n Dim boolOneFound As boolean\r\n\r\n Dim session As New NotesSession\r\n Dim doc As notesdocument\r\n Dim db As NotesDatabase\r\n Dim view As NotesView\r\n Dim dc As NotesDocumentCollection\r\n\r\n Dim iTunes As Variant\r\n Dim itTracks As Variant\r\n Dim itTrack As Variant\r\n Dim itList As Variant\r\n Dim itSubList As Variant\r\n\r\n Dim itSources As Variant\r\n Dim itSource As Variant\r\n Dim itPlayLists As Variant\r\n\r\n Const FIELD_ROOTS = \"Name,Artist,Composer,Album,Grouping,\" _\r\n &amp; \"Genre,Size,Time,Disc Number,Disc Count,Track Number,\" _\r\n &amp; \"Track Count,Year,Date Modified,Date Added,Bit Rate,\" _\r\n &amp; \"Sample Rate,Volume Adjustment,Kind,Equalizer,Comments,\" _\r\n &amp; \"Play Count,Last Played,Skip Count,Last Skipped,\" _\r\n &amp; \"My Rating,Location,Persistent ID Low,Persistent ID High,PlayLists\"\r\n'\u00a0\u00a0 \u00a0&amp; \"My Rating,Location,TrackID,TrackDatabaseID,Internal_Index,PlayOrder_Index\" \r\n\r\n ' not used for array, hence wrapping commas\r\n Const SKIP_LISTS = \",Music,My Bulk Sort,Library,My Collections,\"\r\n\r\n Const COMMA = \",\"\r\n Const PIPE = \"|\"\r\n Const COLON = \":\"\r\n Const MAX_TRACKS = 30\r\n\r\n Set db = session.currentdatabase\r\n Set view = db.GetView( \"Location\" )\r\n\r\n varRoots = Split( FIELD_ROOTS, COMMA )\r\n varFields = Split( FIELD_ROOTS, COMMA )\r\n\r\n ' define field names\r\n For i = 0 To Ubound( varFields )\r\n varFields( i ) = _\r\n ReplaceSubstring( varFields( i ), \" \", \"_\" ) &amp; \"_TX\"\r\n Next\r\n\r\n Set iTunes = CreateObject( \"iTunes.Application\" )\r\n\r\n Set itSources = iTunes.Sources\r\n Set itSource = itSources.ItemByName( \"Library\" )\r\n Set itList = iTunes.LibraryPlayList()\u00a0\u00a0 \u00a0\r\n\r\n intTracks = itList.Tracks.Count\r\n\r\n lngCount = intTracks\r\n\r\n For j = 1 To intTracks\r\n\r\n ' bail out quickly if we're debugging the code\r\n If DEBUG Then\r\n If j &gt; MAX_TRACKS Then\r\n Print \"[\" &amp; i &amp; \"] \" _\r\n &amp; itList.Name &amp; \" has more than \" &amp; MAX_TRACKS _\r\n &amp; \" tracks ... skipping the rest for now\"\r\n Exit For\r\n End If\r\n End If\r\n\r\n Set itTrack = itList.Tracks.Item( j )\r\n\r\n Stop\r\n\r\n If itTrack Is Nothing Then\r\n\r\n Print \"Track is 'Nothing' in BuildEntriesFromiTunes() function of ImportMusicCOM agent\"\r\n strMsgList( lngCount ) = \"Invalid track encountered - Track is 'Nothing'\"\r\n\r\n Else\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\r\n ' get pipe-separated string of info about the track\r\n ' if track is nothing, strLine is empty string\r\n strLine = GetLineFromTrack( iTunes, itTrack, varRoots )\r\n\r\n Stop\r\n\r\n boolCreated = False\r\n\r\n strRaw = strLine\r\n strRawInDoc = \"Bogus Value to Start ||||||||||||||||||||||\"\r\n\r\n varArray = Split( strLine, PIPE )\r\n\r\n strLocation = Lcase$( Trim$( itTrack.Location ) )\r\n\r\n If InStr( strLocation, COLON ) &gt; 0 Then\r\n strLocation = StrRight( strLocation, COLON ) ' ignore drive letters\r\n End If\r\n\r\n ' if no location, delete from iTunes\r\n If strLocation = \"\" Then\r\n Print \"DELETING missing track: \" &amp; itTrack.Name &amp; \" by \" &amp; itTrack.Artist\r\n strMsgList( lngCount ) = \"DELETING missing track: \" &amp; itTrack.Name &amp; \" by \" &amp; itTrack.Artist\r\n\r\n Call itTrack.Delete\r\n\r\n Redim varArray( 0 ) ' force next part to get skipped\r\n\r\n Stop\r\n\r\n Set itPlayLists = itTrack.Playlists()\r\n strPlayLists = \"\"\r\n If itPlayLists.Count &gt; 0 Then\r\n For intPL = 1 To itPlayLists.Count\r\n Set itSubList = itPlayLists( intPL )\r\n strKey = COMMA &amp; itSubList.Name &amp; COMMA\r\n\r\n ' look the other way!\r\n If Instr( SKIP_LISTS, strKey ) = 0 Then\r\n strPlayLists = strPlayLists &amp; itSubList.Name &amp; COMMA\r\n End If\r\n\r\n Next\r\n If strPlayLists &lt;&gt; \"\" Then\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\r\n strPlayLists\u00a0 = Left$( strPlayLists, Len( strPlayLists ) - 1 )\r\n strRaw = strRaw &amp; PIPE &amp; strPlayLists\r\n End If\r\n End If\r\n End If\r\n\r\n boolOneFound = False ' reset\r\n If Ubound( varArray ) = Ubound( varFields ) Then\r\n\r\n Set dc = view.GetAllDocumentsByKey( strLocation )\r\n\r\n If dc.Count = 1 Then\r\n\r\n boolOneFound = true\r\n Set doc = dc.GetFirstDocument()\r\n strRawInDoc = doc.RawData_TX( 0 )\r\n\r\n ' get rid of pers IDs and if found\r\n strRawInDoc = StrLeftBack( StrLeftBack( strRawInDoc, PIPE ), PIPE )\r\n strRawInDoc = strRawInDoc &amp; PIPE &amp; doc.Plalists_TX( 0 )\r\n strRaw = StrLeftBack( StrLeftBack( strRaw, PIPE ), PIPE )\r\n strRaw = strRaw &amp; PIPE &amp; strPlayLists\r\n\r\n Elseif dc.Count &gt; 1 Then\r\n\r\n Print \"ERROR: Multiple docs found for: \"_\r\n &amp; strLocation\r\n\r\n Call dc.RemoveAll( True ) ' get rid of all, create new\r\n\r\n Set doc = New NotesDocument( db )\r\n Call doc.ReplaceItemValue( \"Form\", \"iTrack\" )\r\n Call SetCycleValues( doc, \"Created after erasing dupes\" )\r\n\r\n Else\r\n Print \"[\" &amp; lngCount &amp; \"] CREATING \" &amp; strRaw\r\n boolCreated = True\r\n lngCreated = lngCreated + 1\r\n Set doc = New NotesDocument( db )\r\n Call doc.ReplaceItemValue( \"Form\", \"iTrack\" )\r\n Call SetCycleValues( doc, \"Created\" )\r\n\r\n End If\r\n\r\n ' don't update unless something changed\r\n If strRawInDoc = strRaw Then\r\n Print \"[\" &amp; lngCount &amp; \"] SKIPPING \" &amp; strRaw\r\n Else\r\n lngUpdated = lngUpdated + 1\r\n If Not boolCreated Then\r\n Print \"[\" &amp; lngCount &amp; \"] UPDATING \" &amp; strRaw\r\n End If\r\n\r\n Call doc.ReplaceItemValue( \"RawDataPrev_TX\", doc.RawData_TX( 0 ) )\r\n Call doc.ReplaceItemValue( \"RawData_TX\", strRaw )\r\n\r\n For i = 0 To Ubound( varArray )\r\n Call doc.ReplaceItemValue( varFields( i ), _\r\n varArray( i ) )\r\n Next\r\n Call doc.ReplaceItemValue( \"PlayLists_TX\", strPlayLists )\r\n Call SetLastCycleValue( doc, \"Last Updated\" )\r\n\r\n Call doc.save( True, False )\r\n End If\r\n End If\r\n End If\r\n\r\n Stop\r\n\r\nNext_Track:\r\n\r\n lngCount = lngCount - 1\r\n\r\n Next\r\n\r\n strRaw = \"\"\r\n Forall s In strMsgList\r\n strRaw = strRaw &amp; s &amp; Chr$( 10 )\r\n End Forall\r\n\r\n If strRaw &lt;&gt; \"\" Then\r\n Print strRaw\r\n Messagebox strRaw, 64, \"iTunes\"\r\n End If\r\n\r\n Print \"Done: Created \" &amp; lngCreated &amp; \", updated \" &amp; lngUpdated _\r\n &amp; \", out of \" &amp; lngCount &amp; \" total tracks\"\r\n\r\n Goto Done\r\n\r\nerrHandler:\r\n\r\n If Instr( Error$, \"The track has been deleted.\" ) &gt; 0 Then\r\n Print \"THE TRACK HAS BEEN DELETED.\"\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\r\n Resume Next_Track\r\n End If\r\n\r\n Call ErrorMessage( Err, Erl, Error$, Lsi_info( 2 ), False )\r\n Resume Done\r\nDone:\r\n\r\nEnd Function<\/span><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>A while ago (a long while ago now!) I played around with creating a database for storing my iTunes tracks. The reason behind it was simple: I wanted to be able to synchronize playlists and last-played data between copies on different computers. More importantly, I wanted to learn more about working with external COM interfaces. [&hellip;]<\/p>\n","protected":false},"author":10785,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16],"tags":[],"class_list":["post-17","post","type-post","status-publish","format-standard","hentry","category-ibm-notes-lotus-notes"],"_links":{"self":[{"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/posts\/17","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/users\/10785"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/comments?post=17"}],"version-history":[{"count":6,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/posts\/17\/revisions"}],"predecessor-version":[{"id":20,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/posts\/17\/revisions\/20"}],"wp:attachment":[{"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/media?parent=17"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/categories?post=17"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.callutheran.edu\/jpdillon\/wp-json\/wp\/v2\/tags?post=17"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}