Help in debuggin an AppleScript to get the total duration of selected media files

Hi,
I want to get the duration of all selected audio files in Finder. So I written an applescript like so

-- Zwraca długość podanego pliku w sekundach.
on pobierzDlugosc(sciezka)
	set sekundy to 0
	try
		set informacjeOPliku to do shell script "/usr/bin/afinfo -x " & quoted form of POSIX path of sciezka
		tell application "System Events"
			set plist to make new property list item with data informacjeOPliku
			set audio to property list item "audio_file" of property list item 1 of plist
			set sciezki to property list item "tracks" of property list item 1 of audio
			set sciezka to property list item "track" of property list item 1 of sciezki
			set sekundy to property list item "duration" of property list item 1 of sciezka
			return sekundy as number
		end tell
	on error w
		tell application "VoiceOver" to output w
		error number -128
	end try
end pobierzDlugosc
-- Pobiera zaznaczenie pliku
tell application "Finder"
	set zaznaczenie to selection as list
end tell
if length of zaznaczenie is 0 then tell application "VoiceOver" to output "Brak zaznaczenia"
set calkowitaDlugosc to 0
repeat with plik in zaznaczenie
	set sciezkaDoPliku to (plik as alias)
	set dlugoscPliku to pobierzDlugosc(sciezkaDoPliku)
	set calkowitaDlugosc to calkowitaDlugosc + dlugoscPliku
end repeat
set godziny to calkowitaDlugosc div (60 * 60)
set minuty to (calkowitaDlugosc mod (60 * 60)) div 60
set sekundy to calkowitaDlugosc mod 60

But it gives me a system events error. Example output of afinfo when run straight from the terminal

<?xml version="1.0" encoding="UTF8"?>
<audio_info xmlns="http://apple.com/core_audio/audio_info">
	<audio_file audio_file_id="0">
		<file_name>/Users/nuno/Downloads/w tymbarku jest gaz.mp3</file_name>
		<file_type>'MPG3'</file_type>
		<tracks>
			<track track_id="1">
				<num_channels>2</num_channels>
				<channel_layout>NA</channel_layout>
				<sample_rate units="Hz">44100</sample_rate>
				<format_type>.mp3</format_type>
				<format_flags>0x00000000</format_flags>
				<bit_depth units="bits">0</bit_depth>
				<bytes_per_packet>0</bytes_per_packet>
				<frames_per_packet>1152</frames_per_packet>
				<bytes_per_frame>0</bytes_per_frame>
				<audio_bytes units="bytes">480801</audio_bytes>
				<audio_packets>1068</audio_packets>
				<restricts_random_access>false</restricts_random_access>
				<duration units="sec">27.898776</duration>
				<bit_rate units="bps">137870</bit_rate>
				<max_packet_size_bound units="bytes">1052</max_packet_size_bound>
				<max_packet_size units="bytes">417</max_packet_size>
				<data_offset units="bytes">555</data_offset>
				<optimized>true</optimized>
				<packet_table_info>
					<total_frames>1230336</total_frames>
					<valid_frames>1230336</valid_frames>
					<priming_frames>0</priming_frames>
					<remainder_frames>0</remainder_frames>
				</packet_table_info>

Can someone help?

1 Like

The main issue with your pobierzDlugosc handler is that it is mixing Property List with XML text. System Events has both Property List and XML suites, and while similar, they are not quite the same.

To use the XML from the shell script, your handler can be modified, for example:

   tell application "System Events"
      set XMLdata to make new XML data with properties {text:informacjeOPliku}
      set baseElement to first XML element of XMLdata
      set audio to XML element "audio_file" of baseElement
      set sciezki to XML element "tracks" of audio
      set sciezka to XML element "track" of sciezki
      set sekundy to XML element "duration" of sciezka
      return (value of sekundy) as number
   end tell

You can also use a more general-purpose handler to get the value or names of an XML element, for example:

# Get the contents of an XML element from simple XML data for a tag hierarchy list.
# The optional names boolean argument can be used to get list tag names instead of values.
to getElementContent from XMLdata for hierarchy as list given names:names as boolean : false
   tell application "System Events" to try
      if class of XMLdata is not XML data then error "not XML data"
      set element to first XML element of XMLdata -- start at the base element
      repeat with tagItem in hierarchy -- traverse tag hierarchy
         set element to (get XML element tagItem of element)
      end repeat
      tell (get value of element) to if it is not missing value then return it -- element content
      return item ((names as integer) + 1) of {value, name} of XML elements of element -- contained elements
   on error errorMessage number errorNumber
      error "getElementContent error:  " & errorMessage & " (" & errorNumber & ")"
   end try
end getElementContent

Where it is called by using a list of element tag names, for example:

   tell application "System Events"
      set XMLdata to make new XML data with properties {text:informacjeOPliku}
      my (getElementContent from XMLdata for {"audio_file", "tracks", "track", "duration"})
   end tell

An easier way would be to use Spotlight to get the duration directly, instead of creating an XML file and trying to get System Events to parse it. For example:

tell application "Finder" to repeat with anItem in (get selection as alias list)
   tell me to set duration to (do shell script "/usr/bin/mdls -name kMDItemDurationSeconds -raw " & quoted form of POSIX path of anItem)
   if duration is not "(null)" then set duration to my formatSeconds(duration)
   log "The duration of '" & name of anItem & "' is " & duration -- or whatever
end repeat

to formatSeconds(theSeconds as integer) -- hh:mm:ss (wraps at 24 hours)
   if theSeconds < 0 then set theSeconds to 0
   tell "000000" & (10000 * (theSeconds mod days div hours) + 100 * (theSeconds mod hours div minutes) + (theSeconds mod minutes)) to return (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
end formatSeconds
3 Likes

FWIW, a somewhat different approach that uses the afinfo utility:

use framework "Foundation"
use scripting additions

tell application "Finder" to set theFiles to selection as alias list
if theFiles is {} then display dialog "One or more files must be selected in a Finder window" buttons {"OK"} cancel button 1 default button 1

set text item delimiters to {"/"}
set theSummary to ""
set totalDuration to 0
repeat with aFile in theFiles
	set aFile to POSIX path of aFile
	set fileName to text item -1 of aFile
	try
		set theInfo to do shell script "/usr/bin/afinfo -x " & quoted form of aFile
	on error
		display dialog quote & fileName & quote & " could not be processed" buttons {"OK"} cancel button 1 default button 1
	end try
	set durationSeconds to getSeconds(theInfo)
	set durationMinutes to getMinutes(durationSeconds)
	set theSummary to theSummary & (text item -1 of aFile) & " - " & durationMinutes & linefeed
	set totalDuration to totalDuration + durationSeconds
end repeat

set totalDuration to getMinutes(totalDuration)
set theSummary to theSummary & "Total - " & totalDuration
set text item delimiters to {""}
display dialog "The names of selected files and their durations are:" & linefeed & linefeed & theSummary buttons {"OK"} default button 1

on getSeconds(theString)
	set theString to current application's NSMutableString's stringWithString:theString
	set thePattern to "(?is).*?<duration.*?(\\d+)\\..*" -- needs improvement
	set matchesFound to (theString's replaceOccurrencesOfString:thePattern withString:"$1" options:1024 range:{0, theString's |length|()}) -- set to 0 if match not found
	return theString as text as integer
end getSeconds

on getMinutes(theSeconds) -- thanks red_menace
	if theSeconds < 0 then set theSeconds to 0
	tell "000000" & (10000 * (theSeconds mod days div hours) + 100 * (theSeconds mod hours div minutes) + (theSeconds mod minutes)) to return (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
end getMinutes

Hi,
I continued with this approach. Now I have a weird error, its in Polish but the main issue is that access is supposedly denied.

-- Zwraca długość podanego pliku w sekundach.
on getLength(path)
	try
		set fileInfo to do shell script "/usr/bin/afinfo -x " & quoted form of POSIX path of path
		tell application "System Events"
			set xml to make new XML data with properties {text:fileInfo}
			set baseElement to first XML element of xml
			set audio to XML element "audio_file" of baseElement
			set tracks to XML element "tracks" of audio
			set track to XML element "track" of tracks
			set durationElement to XML element "duration" of track
			set seconds to value of durationElement
			return (seconds as number)
		end tell
	on error w
		tell application "VoiceOver" to output w
		error number -128
	end try
end getLength

-- Pobiera zaznaczenie pliku
tell application "Finder"
	set sel to selection as list
end tell
if length of sel is 0 then
	tell application "VoiceOver" to output "Brak zaznaczenia"
	error -128
end if

set totalLength to 0
repeat with f in sel
	set filePath to f as alias
	set fileLength to getLength(filePath)
	set totalLength to totalLength + fileLength
end repeat

set hours to totalLength div (60 * 60)
set minutes to (totalLength mod (60 * 60)) div 60
set seconds to totalLength mod 60

tell application "VoiceOver"
end tell

And here’s the error
Syntax error. Cannot set a value of seconds to«class valL» of durationElement. access denied

AppleScript is complaining because the term seconds is a class name (note the different formatting color in the editor) - just choose a different name for your variable, such as in your original post.

I am blind, VoiceOver does not note such changes. Thank you!

I tried to find where the class was documented, but about the only place I found was for the with timeout statement, and even that wasn’t very clear. Without color cues, I can see where that might be an issue. The Xcode UI is even worse, unfortunately.