@@ -10,17 +10,17 @@ public class Xmp
1010 /// </summary>
1111 private static readonly string s_xmlTemplate = """
1212 <?xml version="1.0" encoding="utf-8"?>
13- <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.6.0">
13+ <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.6.0">
1414 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
1515 <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ablFR="https://ns.ableton.com/xmp/fs-resources/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
16- <dc:format>application/vnd.ableton.folder</dc:format>
17- <ablFR:resource>folder</ablFR:resource>
18- <ablFR:items>
19- <rdf:Bag></rdf:Bag>
20- </ablFR:items>
16+ <dc:format>application/vnd.ableton.folder</dc:format>
17+ <ablFR:resource>folder</ablFR:resource>
18+ <ablFR:items>
19+ <rdf:Bag></rdf:Bag>
20+ </ablFR:items>
2121 </rdf:Description>
2222 </rdf:RDF>
23- </x:xmpmeta>
23+ </x:xmpmeta>
2424 """ ;
2525
2626 /// <summary>
@@ -33,13 +33,24 @@ public class Xmp
3333 /// </summary>
3434 public XElement Xml { get ; private set ; }
3535
36+ /// <summary>
37+ /// A pointer to the main content of the document.
38+ /// </summary>
39+ private XElement _itemsBag ;
40+
3641 /// <summary>
3742 /// Creates a new XMP document.
3843 /// </summary>
3944 /// <param name="xml">The XML to populate the document with.</param>
4045 private Xmp ( XElement xml )
4146 {
4247 Xml = xml ;
48+
49+ _itemsBag = Xml . Element ( Rdf . RdfWrapper ) ?
50+ . Element ( Rdf . Description ) ?
51+ . Element ( Ableton . Items ) ?
52+ . Element ( Rdf . Bag )
53+ ?? throw new InvalidOperationException ( "Malformed XMP document" ) ;
4354 }
4455
4556 /// <summary>
@@ -79,6 +90,55 @@ public void Save(string path)
7990 IsDirty = false ;
8091 }
8192
93+ /// <summary>
94+ /// Tag a file.
95+ /// </summary>
96+ /// <param name="file">The file to tag.</param>
97+ /// <param name="tags">The tags to apply.</param>
98+ public void AddTags ( string file , List < string > tags )
99+ {
100+ var tagsAdded = new List < string > ( ) ;
101+
102+ var existingItem = GetFileItem ( file ) ;
103+
104+ if ( existingItem != null )
105+ {
106+ // Add keyword to existing items
107+ var keywordBag = existingItem . Element ( Ableton . Keywords ) . Element ( Rdf . Bag ) ;
108+
109+ foreach ( string tag in tags )
110+ {
111+ if ( ! keywordBag . Elements ( Rdf . Li ) . Any ( e => e . Value == tag ) )
112+ {
113+ keywordBag . Add ( new XElement ( Rdf . Li , tag ) ) ;
114+ tagsAdded . Add ( tag ) ;
115+ IsDirty = true ;
116+ }
117+ }
118+ }
119+ else
120+ {
121+ // Create new item
122+ var newItem = new XElement ( Rdf . Li , new XAttribute ( Rdf . ParseType , "Resource" ) ,
123+ new XElement ( Ableton . FilePath , file ) ,
124+ new XElement ( Ableton . Keywords ,
125+ new XElement ( Rdf . Bag ,
126+ tags . Select ( tag => new XElement ( Rdf . Li , tag ) )
127+ )
128+ )
129+ ) ;
130+
131+ _itemsBag . Add ( newItem ) ;
132+ tagsAdded = tags ;
133+ IsDirty = true ;
134+ }
135+
136+ if ( tagsAdded . Count > 0 )
137+ {
138+ Console . WriteLine ( $ "Added tags to { file } : { string . Join ( ", " , tagsAdded ) } ") ;
139+ }
140+ }
141+
82142
83143 /// <summary>
84144 /// Tag a list of files.
@@ -87,49 +147,50 @@ public void Save(string path)
87147 /// <param name="tags">The tags to apply.</param>
88148 public void AddTags ( List < string > files , List < string > tags )
89149 {
90- var itemsBag = Xml . Descendants ( Ableton . Items ) . First ( ) . Element ( Rdf . Bag ) ;
91-
92150 foreach ( string file in files )
93151 {
94- var tagsAdded = new List < string > ( ) ;
152+ AddTags ( file , tags ) ;
153+ }
154+ }
95155
96- var existingItem = itemsBag . Elements ( Rdf . Li ) . FirstOrDefault ( e => e . Element ( Ableton . FilePath ) . Value == file ) ;
156+ /// <summary>
157+ /// Remove a specific set of tags from a file.
158+ /// </summary>
159+ /// <param name="file">The file to untag.</param>
160+ /// <param name="tags">The tags to remove.</param>
161+ public void RemoveTags ( string file , List < string > tags )
162+ {
163+ var item = GetFileItem ( file ) ;
97164
98- if ( existingItem != null )
99- {
100- // Add keyword to existing items
101- var keywordBag = existingItem . Element ( Ableton . Keywords ) . Element ( Rdf . Bag ) ;
165+ if ( item != null )
166+ {
167+ var keywordBag = item . Element ( Ableton . Keywords ) . Element ( Rdf . Bag ) ;
168+
169+ var tagsRemoved = new List < string > ( ) ;
170+ var elementsToRemove = new List < XElement > ( ) ;
102171
103- foreach ( string tag in tags )
172+ foreach ( XElement keyword in keywordBag . Elements ( Rdf . Li ) )
173+ {
174+ if ( tags . Contains ( keyword . Value ) )
104175 {
105- if ( ! keywordBag . Elements ( Rdf . Li ) . Any ( e => e . Value == tag ) )
106- {
107- keywordBag . Add ( new XElement ( Rdf . Li , tag ) ) ;
108- tagsAdded . Add ( tag ) ;
109- IsDirty = true ;
110- }
176+ tagsRemoved . Add ( keyword . Value ) ;
177+ elementsToRemove . Add ( keyword ) ;
111178 }
112179 }
113- else
180+
181+ if ( elementsToRemove . Count > 0 )
114182 {
115- // Create new item
116- var newItem = new XElement ( Rdf . Li , new XAttribute ( Rdf . ParseType , "Resource" ) ,
117- new XElement ( Ableton . FilePath , file ) ,
118- new XElement ( Ableton . Keywords ,
119- new XElement ( Rdf . Bag ,
120- tags . Select ( tag => new XElement ( Rdf . Li , tag ) )
121- )
122- )
123- ) ;
183+ if ( elementsToRemove . Count == keywordBag . Elements ( Rdf . Li ) . Count ( ) )
184+ {
185+ item . Remove ( ) ;
186+ }
187+ else
188+ {
189+ elementsToRemove . Remove ( ) ;
190+ }
124191
125- itemsBag . Add ( newItem ) ;
126- tagsAdded = tags ;
127192 IsDirty = true ;
128- }
129-
130- if ( tagsAdded . Count > 0 )
131- {
132- Console . WriteLine ( $ "Added tags to { file } : { string . Join ( ", " , tagsAdded ) } ") ;
193+ Console . WriteLine ( $ "Removed tags from { file } : { string . Join ( ", " , tagsRemoved ) } ") ;
133194 }
134195 }
135196 }
@@ -141,43 +202,25 @@ public void AddTags(List<string> files, List<string> tags)
141202 /// <param name="tags">The tags to remove.</param>
142203 public void RemoveTags ( List < string > files , List < string > tags )
143204 {
144- var itemsBag = Xml . Descendants ( Ableton . Items ) . First ( ) . Element ( Rdf . Bag ) ;
145-
146205 foreach ( string file in files )
147206 {
148- var item = itemsBag . Elements ( Rdf . Li ) . FirstOrDefault ( e => e . Element ( Ableton . FilePath ) . Value == file ) ;
149-
150- if ( item != null )
151- {
152- var keywordBag = item . Element ( Ableton . Keywords ) . Element ( Rdf . Bag ) ;
153-
154- var tagsRemoved = new List < string > ( ) ;
155- var elementsToRemove = new List < XElement > ( ) ;
156-
157- foreach ( XElement keyword in keywordBag . Elements ( Rdf . Li ) )
158- {
159- if ( tags . Contains ( keyword . Value ) )
160- {
161- tagsRemoved . Add ( keyword . Value ) ;
162- elementsToRemove . Add ( keyword ) ;
163- }
164- }
207+ RemoveTags ( file , tags ) ;
208+ }
209+ }
165210
166- if ( elementsToRemove . Count > 0 )
167- {
168- if ( elementsToRemove . Count == keywordBag . Elements ( Rdf . Li ) . Count ( ) )
169- {
170- item . Remove ( ) ;
171- }
172- else
173- {
174- elementsToRemove . Remove ( ) ;
175- }
211+ /// <summary>
212+ /// Remove all tags from a file.
213+ /// </summary>
214+ /// <param name="file">The file to untag.</param>
215+ public void RemoveTags ( string file )
216+ {
217+ var item = GetFileItem ( file ) ;
176218
177- IsDirty = true ;
178- Console . WriteLine ( $ "Removed tags from { file } : { string . Join ( ", " , tagsRemoved ) } ") ;
179- }
180- }
219+ if ( item != null )
220+ {
221+ item . Remove ( ) ;
222+ IsDirty = true ;
223+ Console . WriteLine ( $ "Removed all tags from { file } ") ;
181224 }
182225 }
183226
@@ -187,18 +230,29 @@ public void RemoveTags(List<string> files, List<string> tags)
187230 /// <param name="files">The files to untag.</param>
188231 public void RemoveTags ( List < string > files )
189232 {
190- var itemsBag = Xml . Descendants ( Ableton . Items ) . First ( ) . Element ( Rdf . Bag ) ;
191-
192233 foreach ( string file in files )
193234 {
194- var item = itemsBag . Elements ( Rdf . Li ) . FirstOrDefault ( e => e . Element ( Ableton . FilePath ) . Value == file ) ;
235+ RemoveTags ( file ) ;
236+ }
237+ }
195238
196- if ( item != null )
239+ /// <summary>
240+ /// Gets the bag item for the given filename.
241+ /// </summary>
242+ /// <param name="file"></param>
243+ /// <returns></returns>
244+ private XElement ? GetFileItem ( string file )
245+ {
246+ foreach ( var item in _itemsBag . Elements ( Rdf . Li ) )
247+ {
248+ var filePath = item . Element ( Ableton . FilePath ) ;
249+
250+ if ( filePath != null && filePath . Value == file )
197251 {
198- item . Remove ( ) ;
199- IsDirty = true ;
200- Console . WriteLine ( $ "Removed all tags from { file } ") ;
252+ return item ;
201253 }
202254 }
255+
256+ return null ;
203257 }
204258}
0 commit comments