Use of new sayformat function in websource scripts

The latest development build 3.31k (2025-11-07)

  • NEW: added function SayFormat to Web Sources Framework.

This new feature now allows the use of (probably) all of the funtions documented in Scripting Functions – Mp3tag Documentation even in websource scripts. Here are a few examples for deduplication and sorting a string containing a list of comma separated entries:

set "Teststring" "CD,CD,CD,CD,CD,CD,CD,Blu-Ray,Blu-Ray,Blu-Ray,Vinyl"

# Deduplication

OutputTo "Deduplicated"
sayformat "$dedup(%Teststring%,',',1)"

# Sorting

OutputTo "Sorted"
sayformat "$sort(%Teststring%,',')"

# Sort and Dedup

OutputTo "Sort and Dedup"
sayformat "$sort($dedup(%Teststring%,',',1),',')"

These examples refer to How to prevent duplicate tags in Websources Script? and Scripting-Funktion zum Löschen doppelter Einträge and show how easy and simple the problem now can be solved.

The next example deals with calculating the total number of tracks of an album by using the length of a string that contains a vertical bar for every track in that album:

# Calculation of the total number of Tracks

set "Tracks" "|||||"

# Number of Tracks is number of vertical bars in output buffer "Tracks"

outputto "Totaltracks"
SayFormat "$len(%Tracks%)"

This exampla is taken from Counting tracks.

These solution of the above tasks with the new sayformat function are almost trivial (and creative programmers have become nearly obsolete :wink:). Please let me know if you have more examples for using that new function.
Edit: Sorry I forgot to show the result of the test examples:

Thank you for the examples!

I plan to use it for capitalizing lowercase strings provided by an API, i.e.,

OutputTo "LANGUAGE"
json_select "language"
SayFormat "$caps(%_current%)" 1

Also trivial, but not so easy to do correctly before. I know that @rboss used an amazing workaround, but this only worked for a fixed-length identifier and not for strings of arbitrary length.

I’ve tried using it to combine multiple fields like DESCRIPTION, SUBTITLE etc. into a longer description. The new command also allows inserting special characters like newlines using $char(). I’ve went from:

	OutputTo "Temp_PodcastDesc"
	IfOutput "Subtitle"
	    SayOutput "Subtitle"
	    Set "SPACE" "1"
	EndIf
	IfOutput "TVShowDesc"
	    IfOutput "SPACE"
		SayNewline
		SayNewline
	    EndIf
	    Set "SPACE" "1"
	    SayOutput "TVShowDesc"
	EndIf
	IfOutput "TVSeasonTitle"
	    IfOutput "TVSeasonDesc"
		IfOutput "SPACE"
		    SayNewline
		    SayNewline
		EndIf
		Set "SPACE" "1"
		SayOutput "TVSeasonTitle"
		SayNewline
		SayOutput "TVSeasonDesc"
	    EndIf
	EndIf
	Set "SPACE"

to:

	SayFormat "%subtitle%$if($and(%subtitle%,%tvshowdesc%%tvseasontitle%),$char(13)$char(10)$char(13)$char(10),)%tvshowdesc%$if($and(%tvshowdesc%,%tvseasondesc%),$char(13)$char(10)$char(13)$char(10),)$if($and(%tvseasontitle%,%tvseasondesc%),%tvseasontitle%$char(13)$char(10)%tvseasondesc%,)"

… okay, a bit nightmare-ish to look at actually. I’m not sure if less lines helps speed up initial syntax error-check for web sources or slowed by more characters/indents but worth a try. :smiling_face_with_tear:

It does also let you safely say an %output% without fear of it being empty then becoming a literal string of “%output%”. It’ll just say nothing because scripting functions prefer to see it as an output with no value rather than as a string that happens to look like an output. This saves having to declare outputs like variables that are expected to either have/not have values which hopefully means less Set “output” and Say ““.

$meta() and $meta_sep() don’t work sadly, even with values delimited with \\. Mind you, working solutions are Replace, json_select_array or slotting in a %delimiter% so not a real issue.

…Speaking of Replace, the new command can be used for a light optimisation:

Replace “+” “, ”
Replace “/” “, ”
Replace “; ” “, ”
SayRest

to:

SayFormat “$replace(%_current%,+,',' ,/,',' ,;,',' )”

Bit shorter, takes advantage of multiple parameters and preserves content in Line and position.

One really tasty find was that $list() doesn’t reference your selected files but instead the script’s output buffers. With it, you can make a rudimentary debug file:

OutputTo "DEBUG"
SayFormat "$replace($list( output'['\",\"']'= \",\"$char(10)),|,'$'verticalbar'('')')"

Note there's 'escape' and \escape.

The big benefit is this will show the current values of outputs at that position of the script in runtime, rather than a summary from the end of runtime that the debug command shows.

… or you could just OutputTo “TEST”. :melting_face: Either way, $list() works. Just don’t use it in a loop or your computer will hate you.

Actually, I had come up with an even better workaround; but SayFormat will be a game changer going forward.
I can find use for most of the examples shown above in my scripting.

But now we have new tools to be creative with........... :wrapped_gift: :heart_eyes:

Until now it was impossible to do arithmetic calculations in websource scripts, but with sayformat we now have such functions at hand. So if I want to know the length of an album, i first have to calculate the length of its songs, that are given as "minutes:seconds", in seconds and after this I can add the timings.
I came up with two ways to convert "minutes:seconds" into seconds in a script, a working and one that isn't.

	set "Length" "3:48"
	regexpreplace "^.*" "%Length%"
	outputto "Min"
	sayuntil ":"
	outputto "Sec"
	movechar 1
	sayrest

	# Working
	outputto "seconds_1"
	sayformat "$add($mul(%Min%,60),%Sec%)"

	# Not working
	set "Formula" "$add($mul(%Min%,60),%Sec%)"
	outputto "seconds_2"
	sayformat "%Formula%"

The result from debug output looks like this:

 output["Length"]= "3:48"
 output["Min"]= "3"
 output["Sec"]= "48"
 output["seconds_1"]= "228"
 output["Formula"]= "$add($mul(3,60),48)"
 output["seconds_2"]= "$add($mul(3,60),48)"

The placeholders %output% buffer variables are only working with simple strings and not if they itself contain a scripting function. I had hoped and expected that this would work with function calls.
Is this a bug or a feature?

Can confirm:

set "String" "hello world"

outputto "Test1" 
sayformat "$caps(hello world)"

outputto "Test2" 
sayformat "$caps(%string%)"

set "Function3" "$caps(hello world)"
outputto "Test3" 
sayformat "%function3%"

set "Function4" "$caps(%string%)"
outputto "Test4" 
sayformat "%function4%"

set "Function5" "$caps"
outputto "Test5" 
sayformat "%function5%(hello world)"

set "Function6" "$caps"
outputto "Test6" 
sayformat "%function6%(%string%)"

set "Function7" "$caps("
outputto "Test7" 
sayformat "%function7%hello world)"

set "Function8" "$caps("
outputto "Test8" 
sayformat "%function8%%string%)"
 output["String"]= "hello world"
 output["Test1"]= "Hello World"
 output["Test2"]= "Hello World"
 output["Function3"]= "$caps(hello world)"
 output["Test3"]= "$caps(hello world)"
 output["Function4"]= "$caps(hello world)"
 output["Test4"]= "$caps(hello world)"
 output["Function5"]= "$caps"
 output["Test5"]= "$caps(hello world)"
 output["Function6"]= "$caps"
 output["Test6"]= "$caps(hello world)"
 output["Function7"]= "$caps("
 output["Test7"]= "$caps(hello world)"
 output["Function8"]= "$caps("
 output["Test8"]= "$caps(hello world)"

Must be SayFormat converts %placeholders% as literal strings then knowingly skips processing them as actual code. I even made a quick Action to test this:

[#0]
T=5
1=$caps
F=FUNCTION

[#1]
T=5
1=hello world
F=STRING

[#2]
T=5
1=%function%(%string%)
F=TEST

Still results in $caps(hello world) so works the same in standard scripting functions...

[#0]
T=5
1=$caps(
F=FUNCTION

[#1]
T=5
1=hello world
F=STRING

[#2]
T=5
1=%function%%string%)
F=TEST

Results in hello world). Looks like the format string framework tried out $caps( and ruled it out as a broken bit of code, therefore an empty string. One more time...

[#0]
T=5
1=hello world
F=STRING

[#1]
T=5
1=$caps(%string%)
F=TEST

Results in Hello World, though really just the same as your working method because $caps wasn't produced from a %placeholder%. I did the same process in Columns and got the same results as web sources, expect when I managed to get $caps(%string%).

So far it means, sure you can have empty %placeholders% render as expected but we’re back to using If statements in any framework to piece together batches of custom code. A shame but maybe wise as a precaution.

Not sure if there’s any song titles with coding commands in it, closest I can think of is anything by Aphex Twin :sweat_smile:

It's unfortunately not intended to use as a macro-like function feature (although I see how this can be useful, and could potentially even trigger infinite recursion :grimacing_face: ).

SayFormat uses scripting functions in the same way as other parts of Mp3tag and therefore does not support evaluating placeholders or functions within placeholder contents.

Thank you for this clarification.

After now knowing this, I tried the following script to to calculate the length of an album from the length of its songs. The output buffer "LENGTH" contains this info for the songs in the format "minutes:seconds" and I want to use a while loop to convert the length into seconds and sum it up in buffer "Total". The arithmetic now works but sadly to say, the while loop stops after the first iteration.

	# Preparation of Input Buffer for While Loop
	set "LENGTH" "3:49|3:43|4:10|4:03|4:01|4:38|3:48|4:01|5:48|5:16|4:47|"
	regexpreplace "^.*" " %Length%"
	regexpreplace "\|$" ""

	set "Total"
	do
		movechar 1
		set "Min"
		outputto "Min"
		sayuntil ":"
		set "Sec"
		outputto "Sec"
		movechar 1
		sayuntil "|"
		set "Sum"
		outputto "Sum"
		# Sum = Total + (Min*60 + Sec)
		sayformat "$add(%Total%,$add($mul(%Min%,60),%Sec%))"
		# Total = Sum
		set "Total" "%Sum%"
	while "|" 9999
	set "Albumlength"
	outputto "Albumlength"
	sayformat "$div(%Total%,60):$mod(%Total%,60)"

From the debug output we can find the reason for this crash: sayformat moves the input buffer pointer behind the end of the buffer.

Script-Line    : 41
Command        : outputto
Parameter 1    : >Sum<

Output         : ><

Line and position:
 3:49|3:43|4:10|4:03|4:01|4:38|3:48|4:01|5:48|5:16|4:47
     ^
------------------------------------------------------------
Script-Line    : 43
Command        : sayformat
Parameter 1    : >$add(%Total%,$add($mul(%Min%,60),%Sec%))<

Output         : >229<

Line and position:
 3:49|3:43|4:10|4:03|4:01|4:38|3:48|4:01|5:48|5:16|4:47
                                                       ^

This astonished me, because sayformat does not use the input buffer. It would be very nice if this side effect could be avoided.

It uses the input buffer and makes it available to %_current%. I've implemented it like this to allow for things like

OutputTo "LANGUAGE"
json_select "language"
SayFormat "$caps(%_current%)" 1

without first needing to output the current input to a temporary output like

OutputTo "TEMP_LANGUAGE"
json_select "language"
SayRest
OutputTo "LANGUAGE"
SayFormat "$caps(%TEMP_LANGUAGE%)"

Maybe this can be improved?

What I actually meant with

is, that in my example case sayformat did not use the input buffer because it did not refer to the buffer %_current%. Perhaps it is possible to use the input buffer only if it actually gets refered by sayformat instance in question. Maybe an additional parmeter can be used to exclude the use of %_current% and thereby preserve the input buffer?

Basically, SayFormat would work like SayRest (plus format features) if %_current% is referenced in the format string. And it would work like Say (plus format features) if it doesn't use the current input.

Here is an internal development version to try this:

https://download.mp3tag.de/support/998C7FB6-CA71-44C7-B92D-DF73372C4A3E-3-31-11-1/mp3tagv331k1-x64-setup.exe

Yes that works perfectly now!
Thank you very much.