Manipulate UserDefined tag (TXXX frame) with Taglib-Sharp - powershell

Situation & Task
I have a large music collection and I want to clean their ID3V2 tags with PowerShell and taglip-sharp. Some tags like comment or encoding should be deleted while others like artist or title should not.
Usually you manipulate ID3 tags this way (Simplified version)
# Add taglib dll
[Void][System.Reflection.Assembly]::LoadFrom("$PSScriptRoot\taglib-sharp.dll")
# Load example mp3 into memory as [taglib.file]
$media = [TagLib.File]::Create("C:\path\to\musicFile.mp3")
# Change comment tag
$media.tag.tags[0].Comment = "Hello World"
# Save tags back to mp3 file
$Media.Save()
Problem
Many music files store custom information like URL or Shop Name in a frame called TXXX. Unfortunately, this frame is not accessible with the method shown above. Or I haven't found a way yet.
Instead you use
# Read UserTextInformationFrame
$media.GetTag([TagLib.TagTypes]::Id3v2).GetFrames("TXXX")
This User defined text information frame can hold multiple values. And some are useful since music players like Foobar store PERFORMER, DATE or replay_track_gain tags in TXXX.
Example output for the line above could be:
Description : replaygain_track_gain
Text : {-5.00 dB}
FieldList : {-5.00 dB}
TextEncoding : Latin1
FrameId : {84, 88, 88, 88}
Size : 32
Flags : None
GroupId : -1
EncryptionId : -1
Description : URL
Text : {www.amazon.com}
FieldList : {www.amazon.com}
TextEncoding : UTF16
FrameId : {84, 88, 88, 88}
Size : 43
Flags : None
GroupId : -1
EncryptionId : -1
After this, I was able to filter out all unnecessary TXXX values
# Create a whitelist of TXXX frames
$goodTXXX = 'performer','replaygain_track_gain','date'
# Read UserTextInformationFrame AND filter it
$newTXXX = $Media.GetTag([TagLib.TagTypes]::Id3v2).GetFrames("TXXX") |
where { $goodTXXX -contains $_.Description }
Question: How to write multiple values to TXXX frame
So my question is, how do I save my filtered results back to mp3 file?
My failed attempts were:
$media.GetTag([TagLib.TagTypes]::Id3v2).RemoveFrames("TXXX")
$media.GetTag([TagLib.TagTypes]::Id3v2).SetTextFrame("TXXX",$newTXXX)
# Removes old values, but does not show anything in Foobar
#$media.GetTag([TagLib.TagTypes]::Id3v2).GetFrames("TXXX").SetText("Hello World")
# Shows garbage in Foobar. And it's not usable for multiple values
Taglib-Sharp documentation for SetTextFrame
Bonus question: Is taglib-sharp able to strip out Id3v1 and ID3v2.4 tags while saving new tags as ID3v2.3 tags? (Related SO answer, but doesn't distinguish between v2.3 and v2.4)

I found a way with try & error. It's not elegant since you have to remove all TXXX values and add them back if you just want to change a single one
# Add taglib dll
[Void][System.Reflection.Assembly]::LoadFrom("$PSScriptRoot\taglib-sharp.dll")
# Load example mp3 into memory as [taglib.file]
$media = [TagLib.File]::Create("C:\path\to\musicFile.mp3")
# Get or create the ID3v2 tag.
[TagLib.Id3v2.Tag]$id3v2tag = $media.GetTag([TagLib.TagTypes]::Id3v2, 1)
# Create new 'TXXX frame' object
$TXXXFrame = [TagLib.Id3v2.UserTextInformationFrame]("WWW")
# Delete complete TXXX frame first, or else all values are just appended
$id3v2tag.RemoveFrames("TXXX")
# Set the value/text in the newly created TXXX frame, default Text encoding is UTF8
# Use curly brackets instead of single quotation marks
$TXXXFrame.Text = {www.myurl.com}
# Add TXXX frame to tag
$id3v2tag.AddFrame($TXXXFrame)
# Write all changed tags back to file
$media.Save()

Related

Replacement of text in xml file by AHK - get error when trying to open as xml file

I am using an AHK script to replace some text in an .xml file (Result.xml in this case). The script then saves a file as Result_copy.xml. It changes exactly what I need, but when I try to open the new xml file, it won't open, giving me the error:
This page contains the following errors:
error on line 4 at column 16: Encoding error
Below is a rendering of the page up to the first error.
I only replaced text at line 38 using:
#Include TF.ahk
path = %1%
text = %2%
TF_ReplaceLine(path, 38, 38, text)
%1% and %2% are given by another program and are working as should
I also see that the orginal Result.xml is 123 kb and Result_copy.xml is 62 kb, even though I only add text. When I take Result.xml and manually add the text and save it, it's 123 kb and still opens. so now both files contain exactly the same Characters, but one won't open as xml. I think that something happens during saving/copying, which I don't understand.
Could someone help me out on this one? I don't have a lot of experience in AHK scripting and do not have a programming background.
Thank you in advance!
Michel
TF.ahk contains this:
/*
Name : TF: Textfile & String Library for AutoHotkey
Version : 3.8
Documentation : https://github.com/hi5/TF
AutoHotkey.com: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=576
AutoHotkey.com: http://www.autohotkey.com/forum/topic46195.html (Also for examples)
License : see license.txt (GPL 2.0)
Credits & History: See documentation at GH above.
TF_ReplaceLine(Text, StartLine = 1, Endline = 0, ReplaceText = "")
{
TF_GetData(OW, Text, FileName)
TF_MatchList:=_MakeMatchList(Text, StartLine, EndLine, 0, A_ThisFunc) ; create MatchList
Loop, Parse, Text, `n, `r
{
If A_Index in %TF_MatchList%
Output .= ReplaceText "`n"
Else
Output .= A_LoopField "`n"
}
Return TF_ReturnOutPut(OW, OutPut, FileName)
}

Stata: Unable to return Global Macro of Filename

I am using filelist to generate a dataframe of each folder and the files contained in it. I would like to save each folder name and file name with an observation number x to be able to pull their names out later.
ssc install filelist
filelist
//Save each file's name and corresponding folder:
forvalues x = 1 / `=_N' { //for every row in the filelist dataframe shown
local file = filename[`x']
global folder_`x' dirname[`x'] //save the folder name as folder_i, i = 1, 2, ... _N
global file_`x' filename[`x'] //save the file name as file_i, i = 1, 2, ... _N
}
global filecount `=_N'
This runs smoothly, and if I was to run di $file_2, for instance, it would produce the given filename. The issue I have is that then when I try to use this and access these Globals later on, they appear to have saved the "filename[`x']" rather than the actual filename. For instance, if I run:
import excel "InterestRates.xlsx", sheet("US") firstrow
di $file_2
Then I get the error filename not found. I have tried changing up my `' and "" and {} in many different ways, and I still cannot seem to get this to reference the actual filename. Any help would be greatly appreciated!
There are two ways to assign local or global macros: with an equal sign, and without. If it is assigned without an equal sign, the content will be stored as it is. With an equal sign, the content will be evaluated first.
clear
input str8 filename
"file.dta"
end
global file_1 filename[1]
global file_2 = filename[1]
di "$file_1"
di "$file_2"
Result:
. di "$file_1"
filename[1]
. di "$file_2"
file.dta
Here is a slight alteration to Wouter's code which still gives me the error:
cd [MY_PATH_HERE]
ssc install filelist
filelist
//Save each file's name and corresponding folder:
forvalues x = 1 / `=_N' { //for every row in the filelist dataframe shown
local file = filename[`x']
global folder_`x' dirname[`x'] //save the folder name as folder_i, i = 1, 2, ... _N
global file_`x' filename[`x'] //save the file name as file_i, i = 1, 2, ... _N
}
export excel test.xlsx, firstrow(var)
import excel different.xlsx, firstrow clear
di $file_1
This then still produces the filename not found error as before. It works fine when telling Stata to import the same Excel (test.xlsx) you just saved, but if you import a different Excel, it causes issues. I don't understand why this is the case though if you are saving it as a global macro.

dicom header personal information conversion to a .txt file

I have a series of DICOM Images which I want to anonymize, I found few Matlab codes and some programs which do the job, but none of them export a .txt file of removed personal information. I was wondering if there is a function which can also save removed personal information of a DICOM images in .txt format for features uses. Also, I am trying to create a table which shows the corresponding new images ID to their real name.(subjects real name = personal-information-removed image ID)
Any thoughts?
Thanks for considering my request!
I'm guessing you only want to output to your text file the fields that are changed by anonymization (either modified, removed, or added). First, you may want to modify some dicomanon options to reduce the number of changes, in particular passing the arguments 'WritePrivate', true to ensure private extensions are kept.
First, you can perform the anonymization, saving structures of pre- and post-anonymization metadata using dicominfo:
preAnonData = dicominfo('input_file.dcm');
dicomanon('input_file.dcm', 'output_file.dcm', 'WritePrivate', true);
postAnonData = dicominfo('output_file.dcm');
Then you can use fieldnames and setdiff to find fields that are removed or added by anonymization, and add them to the post-anonymization or pre-anonymization data, respectively, with a nan value as a place holder:
preFields = fieldnames(preAnonData);
postFields = fieldnames(postAnonData);
removedFields = setdiff(preFields, postFields);
for iField = 1:numel(removedFields)
postAnonData.(removedFields{iField}) = nan;
end
addedFields = setdiff(postFields, preFields);
for iField = 1:numel(addedFields)
preAnonData.(addedFields{iField}) = nan;
end
It will also be helpful to use orderfields so that both data structures have the same ordering for their field names:
postAnonData = orderfields(postAnonData, preAnonData);
Finally, now that each structure has the same fields in the same order we can use struct2cell to convert their field data to a cell array and use cellfun and isequal to find any fields that have been modified by the anonymization:
allFields = fieldnames(preAnonData);
preAnonCell = struct2cell(preAnonData);
postAnonCell = struct2cell(postAnonData);
index = ~cellfun(#isequal, preAnonCell, postAnonCell);
modFields = allFields(index);
Now you can create a table of the changes like so:
T = table(modFields, preAnonCell(index), postAnonCell(index), ...
'VariableNames', {'Field', 'PreAnon', 'PostAnon'});
And you could use writetable to easily output the table data to a text file:
writetable(T, 'anonymized_data.txt');
Note, however, that if any of the fields in the table contain vectors or structures of data, the formatting of your output file may look a little funky (i.e. lots of columns, most of them empty, except for those few fields).
One way to do this is to store the tags before and after anonymisation and use these to write your text file. In Matlab, dicominfo() will read the tags into a structure:
% Get tags before anonymization
tags_before = dicominfo(file_in);
% Anoymize
dicomanon(file_in, file_out); % Need to set tags values where required
% Get tags after anonymization
tags_after = dicominfo(file_out);
% Do something with the two structures
disp(['Patient ID:', tags_before.PatientID ' -> ' tags_after.PatientID]);
disp(['Date of Birth:', tags_before.PatientBirthDate ' -> ' tags_after.PatientBirthDate]);
disp(['Family Name:', tags_before.PatientName.FamilyName ' -> ' tags_after.PatientName.FamilyName]);
You can then write out the before/after fields into a text file. You'd need to modify dicomanon() to choose your own values for the removed fields, since by default they are set to empty.

Matlab Read Text File List Exclude first 34 characters

I am trying to read values from a text file. I want the value after ': '.
Here is a sample of the text file. All lines are formated the same.
There are 34 places before the start of the data.
File Name : IMG_1184.JPG
File Size : 2.1 MB
File Modification Date/Time : 2012:07:14 11:53:18-05:00
File Permissions : rw-rw-rw-
File Type : JPEG
MIME Type : image/jpeg
Exif Byte Order : Big-endian (Motorola, MM)
I tried to use this code:
fileID = fopen('Exif.txt');
Exif1 = textscan(fileID, '%s %s','delimiter', ':');
This worked on most of the data but some data also used ':' so that didn't work.
I tried to use this code:
fileID = fopen('Exif.txt');
Exif1 = textscan(fileID, '%s %s','delimiter', ': ');
This returned a mess. Not sure why. Everything was fragmented.
Can anyone explain how to just get the 35th value to the end of every string and put it into an array?
There is the function strtrim(string) in Matlab which will strip the leading and trailing spaces for you. Try reading the data in a line at the time into the textscan function after using strtrim?
Read the whole line into a variable then get the 35th and subsequent characters like this:
whole_line(35:end)

Silent exporting of globals using %GOF in Caché

I would like to know if it's possible to use "^%GOF" without user interaction. I'm using Caché 2008. ^%GO isn't an option as it's to slow. I'm using input from a temporary file for automatically answer the questions, but it can fail (rarely happens).
I couldn't find the routine of this utility in %SYS. Where is it located?
Thanks,
Answer: Using "%SYS.GlobalQuery:NameSpaceList" to get list of globals (system globals excluding).
Set Rset = ##class(%ResultSet).%New("%SYS.GlobalQuery:NameSpaceList")
d Rset.Execute(namespace, "*", 0)
s globals=""
while (Rset.Next()){
s globalName=Rset.Data("Name")_".gbl"
if (globals=""){
s globals = globalName
}else{
s globals = globals_","_globalName
}
d ##class(%Library.Global).Export(namespace, globals, "/tmp/export.gof", 7)
The only drawback is that if you have a namespace with concatination of globals exceeding the maximum allowed for a global entry, the program crashes. You should then split the globals list.
I would recommend that you look at the %Library.Global() class with output format 7.
classmethod Export(Nsp As %String = $zu(5), ByRef GlobalList As %String, FileName As %String, OutputFormat As %Integer = 5, RecordFormat As %String = "V", qspec As %String = "d", Translation As %String = "") as %Status
Exports a list of globals GlobalList from a namespace Nsp to FileName using OutputFormat and RecordFormat.
OutputFormat can take the values below:
1 - DTM format
3 - VAXDSM format
4 - DSM11 format
5 - ISM/Cache format
6 - MSM format
7 - Cache Block format (%GOF)
RecordFormat can take the values below:
V - Variable Length Records
S - Stream Data
You can find it in the class documentation here: http://docs.intersystems.com/cache20082/csp/documatic/%25CSP.Documatic.cls
I've never used it, it looks like it would do the trick however.
export your global to file
d $system.OBJ.Export("myGlobal.GBL","c:\global.xml")
import global from your file
d $system.OBJ.Load("c:\global.xml")
Export items as an XML file
The extension of the items determine what
type they are, they can be one of:
CLS - classes
CSP - Cache Server Pages
CSR - Cache Rule files
MAC - Macro routines
INT - None macro routines
BAS - Basic routines
INC - Include files
GBL - Globals
PRJ - Studio Projects
OBJ - Object code
PKG - Package definition
If you wish to export multiple classes then separate then with commas or
pass the items("item")="" as an array or use wild cards.
If filename is empty then it will export to the current device.
link to docbook
edit: adding "-d" as qspec value will suppress the terminal output of the export. If you want to use this programmtically, it might get in the way.
And just for completeness' sake:
SAMPLES>s IO="c:\temp\test.gof"
SAMPLES>s IOT="RMS"
SAMPLES>s IOPAR="WNS"
SAMPLES>s globals("Sample.PersonD")=""
SAMPLES>d entry^%GOF(.globals)
SAMPLES>
-> results in c:\temp\test.gof having the export. You can define up to 65435 globals in you array (named globals in this example)
But I would recommend you go with DAiMor's answer as this is the more 'modern' way.
To avoid maximum string error, you should use subscripts instead of comma delimited string:
Set Rset = ##class(%ResultSet).%New("%SYS.GlobalQuery:NameSpaceList")
d Rset.Execute(namespace, "*", 0)
while (Rset.Next()) {
s globals(Rset.Data("Name"))="" // No need for _".gbl" in recent Cache
}
d ##class(%Library.Global).Export(namespace, .globals, "/tmp/export.gof", 7) // Note dot before globals