{"id":66870,"date":"2022-08-31T09:00:45","date_gmt":"2022-08-31T09:00:45","guid":{"rendered":"https:\/\/www.cryptocabaret.com\/?p=66870"},"modified":"2022-08-31T09:00:45","modified_gmt":"2022-08-31T09:00:45","slug":"make-your-own-music-tagging-framework-with-groovy","status":"publish","type":"post","link":"https:\/\/www.cryptocabaret.com\/?p=66870","title":{"rendered":"Make your own music tagging framework with Groovy"},"content":{"rendered":"<p><span class=\"field field--name-title field--type-string field--label-hidden\">Make your own music tagging framework with Groovy<\/span><br \/>\n<span class=\"field field--name-uid field--type-entity-reference field--label-hidden\"><a title=\"View user profile.\" href=\"https:\/\/opensource.com\/users\/clhermansen\" class=\"username\">Chris Hermansen<\/a><\/span><br \/>\n<span class=\"field field--name-created field--type-created field--label-hidden\">Wed, 08\/31\/2022 &#8211; 03:00<\/span><\/p>\n<div data-drupal-selector=\"rate-node-70177\" class=\"rate-widget-thumbs-up\" title=\"Register or Login to like.\">\n<div class=\"rate-thumbs-up-btn-up vote-pending\">1 reader likes this<\/div>\n<div class=\"rate-score\">1 reader likes this<\/div>\n<\/div>\n<div class=\"clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item\">\n<p>In this series, I&#8217;m developing several scripts to help in cleaning up my music collection. In the last article I wrote and tested a Groovy script to clean up the motley assembly of tag fields. In this article, I&#8217;ll separate the framework I&#8217;ve been using into a separate class and then write a test program to exercise it.<\/p>\n<h2>Install Java and Groovy<\/h2>\n<p>Groovy is based on Java and requires a Java installation. Both a recent and decent version of Java and Groovy might be in your Linux distribution&#8217;s repositories. Groovy can also be installed following the instructions on the <a href=\"https:\/\/groovy.apache.org\/download.html\" target=\"_blank\" rel=\"noopener\">Groovy homepage<\/a>. A nice alternative for Linux users is <a href=\"https:\/\/opensource.com\/article\/22\/3\/manage-java-versions-sdkman\" target=\"_blank\" rel=\"noopener\">SDKMan<\/a>, which can be used to get multiple versions of Java, Groovy and many other related tools. For this article, I&#8217;m using SDK&#8217;s releases of:<\/p>\n<ul>\n<li>Java: version 11.0.12-open of OpenJDK 11;<\/li>\n<li>Groovy: version 3.0.8.<\/li>\n<\/ul>\n<h2>Back to the problem<\/h2>\n<p>If you haven&#8217;t read parts 1-5 of this series, do that now so you understand the intended structure of my music directory, the framework created in that article and how we pick up FLAC, MP3 and OGG files.<\/p>\n<ul>\n<li><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-script-java-music\" target=\"_blank\" rel=\"noopener\">How I analyze my music directory with Groovy<\/a><\/li>\n<li><a href=\"https:\/\/opensource.com\/article\/22\/8\/analyze-music-files-jaudiotagger\" target=\"_blank\" rel=\"noopener\">My favorite open source library for analyzing music files<\/a><\/li>\n<li><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-scripting-analyzing-music-directory-part-3\" target=\"_blank\" rel=\"noopener\">How I use Groovy to analyze album art in my music directory<\/a><\/li>\n<li><a href=\"https:\/\/opensource.com\/article\/22\/8\/remove-files-music-directory-groovy\" target=\"_blank\" rel=\"noopener\">Clean up unwanted files in your music directory using Groovy<\/a><\/li>\n<li><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-script-music-tags\" target=\"_blank\" rel=\"noopener\">Clean up music tags with a Groovy script<\/a><\/li>\n<\/ul>\n<h2>The framework class<\/h2>\n<p>As I have mentioned a number of times, because of the music directory structure, we have a standard framework to read the artist subdirectories, the album sub-subdirectories, the music, and other files contained within. Rather than copying that code into each script, you should create a Groovy class that encapsulates the general framework behavior and delegates the application-specific behavior to scripts that call it.<\/p>\n<\/p>\n<div class=\"embedded-resource-list callout-float-right\">\n<div class=\"field field--name-title field--type-string field--label-hidden field__item\">More on Java<\/div>\n<div class=\"field field--name-links field--type-link field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/topics\/enterprise-java\/?intcmp=7013a000002Cxq6AAC\">What is enterprise Java programming?<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/products\/openjdk\/overview?intcmp=7013a000002Cxq6AAC\">Red Hat build of OpenJDK<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/java-cheat-sheet?intcmp=7013a000002Cxq6AAC\">Java cheat sheet<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/www.redhat.com\/en\/services\/training\/do092-developing-cloud-native-applications-microservices-architectures?intcmp=7013a000002Cxq6AAC\">Free online course: Developing cloud-native applications with microservices<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/java?intcmp=7013a000002Cxq6AAC\">Fresh Java articles<\/a><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Here&#8217;s the framework, moved into a Groovy class:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"java geshifilter-java\">\u00a0<span class=\"nu0\">1<\/span> \u00a0 \u00a0<span class=\"kw1\">public<\/span> <span class=\"kw1\">class<\/span> TagAnalyzerFramework <span class=\"br0\">{<\/span><br>\n\u00a0 \u00a0<br>\n\u00a0<span class=\"nu0\">2<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ called before any data is processed<\/span><br>\n\u00a0<span class=\"nu0\">3<\/span> \u00a0 \u00a0 \u00a0 \u00a0Closure atBeginning<br>\n\u00a0 \u00a0<br>\n\u00a0<span class=\"nu0\">4<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ called for each file to be processed<\/span><br>\n\u00a0<span class=\"nu0\">5<\/span> \u00a0 \u00a0 \u00a0 \u00a0Closure onEachLine<br>\n\u00a0 \u00a0<br>\n\u00a0<span class=\"nu0\">6<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ called after all data is processed<\/span><br>\n\u00a0<span class=\"nu0\">7<\/span> \u00a0 \u00a0 \u00a0 \u00a0Closure atEnd<br>\n\u00a0 \u00a0<br>\n\u00a0<span class=\"nu0\">8<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ the full path name to the music library<\/span><br>\n\u00a0<span class=\"nu0\">9<\/span> \u00a0 \u00a0 \u00a0 \u00a0<a href=\"http:\/\/www.google.com\/search?hl=en&amp;q=allinurl%3Adocs.oracle.com+javase+docs+api+string\"><span class=\"kw3\">String<\/span><\/a> musicLibraryDirName<br>\n\u00a0 \u00a0<br><span class=\"nu0\">10<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"kw1\">public<\/span> <span class=\"kw4\">void<\/span> processMusicLibrary<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span> <span class=\"br0\">{<\/span><br><span class=\"nu0\">11<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Before we start processing...<\/span><br><span class=\"nu0\">12<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0atBeginning<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span><br><span class=\"nu0\">13<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Iterate over each dir in music library <\/span><br><span class=\"nu0\">14<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ These are assumed to be artist directories<\/span><br>\n\u00a0 <br><span class=\"nu0\">15<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"kw1\">new<\/span> <a href=\"http:\/\/www.google.com\/search?hl=en&amp;q=allinurl%3Adocs.oracle.com+javase+docs+api+file\"><span class=\"kw3\">File<\/span><\/a><span class=\"br0\">(<\/span>musicLibraryDirName<span class=\"br0\">)<\/span>.<span class=\"me1\">eachDir<\/span> <span class=\"br0\">{<\/span> artistDir <span class=\"sy0\">-&gt;<\/span><br>\n\u00a0 \u00a0<br><span class=\"nu0\">16<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Iterate over each dir in artist dir<\/span><br><span class=\"nu0\">17<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ These are assumed to be album directories<\/span><br><span class=\"nu0\">18<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0artistDir.<span class=\"me1\">eachDir<\/span> <span class=\"br0\">{<\/span> albumDir <span class=\"sy0\">-&gt;<\/span><br><span class=\"nu0\">19<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Iterate over each file in the album directory<\/span><br><span class=\"nu0\">20<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ These are assumed to be content or related<\/span><br><span class=\"nu0\">21<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ (cover.jpg, PDFs with liner notes etc)<\/span><br><span class=\"nu0\">22<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0albumDir.<span class=\"me1\">eachFile<\/span> <span class=\"br0\">{<\/span> contentFile <span class=\"sy0\">-&gt;<\/span><br>\n\u00a0 \u00a0<br><span class=\"nu0\">23<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Then on each line...<\/span><br><span class=\"nu0\">24<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0onEachLine<span class=\"br0\">(<\/span>artistDir, albumDir, contentFile<span class=\"br0\">)<\/span><br><span class=\"nu0\">25<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"br0\">}<\/span><br><span class=\"nu0\">26<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"br0\">}<\/span><br><span class=\"nu0\">27<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"br0\">}<\/span><br><span class=\"nu0\">28<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ And before we finish...<\/span><br><span class=\"nu0\">29<\/span> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0atEnd<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span><br><span class=\"nu0\">30<\/span> \u00a0 \u00a0 \u00a0 \u00a0<span class=\"br0\">}<\/span><br><span class=\"nu0\">31<\/span> \u00a0 \u00a0<span class=\"br0\">}<\/span><\/div><\/div><\/pre>\n<p>Line 1 introduces the public class name.<\/p>\n<p>Lines 2-7 declare the three closures that the application script uses to define the specifics of the processing needed. This is called <em>delegation of behavior<\/em>.<\/p>\n<p>Lines 8-9 declare the string holding the music directory file name.<\/p>\n<p>Lines 10-30 declare the method that actually handles the processing.<\/p>\n<p>Line 12 calls the <code>Closure<\/code> that is run before any data is processed.<\/p>\n<p>Lines 15-27 loop over the artist\/album\/content file structure.<\/p>\n<p>Line 24 calls the <code>Closure<\/code> that processes each content file.<\/p>\n<p>Line 29 calls the <code>Closure<\/code> that is run after all data is processed.<\/p>\n<p>I want to compile this class before I use it, as follows:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"bash geshifilter-bash\"><span class=\"co4\">$ <\/span>groovyc TagAnalyzerFramework.groovy$<\/code><\/span><\/pre>\n<p>That&#8217;s it for the framework.<\/p>\n<h2>Using the framework in a script<\/h2>\n<p>Here&#8217;s a simple script that prints out a bar-separated value listing of all the files in the music directory:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"java geshifilter-java\">\u00a0<span class=\"nu0\">1<\/span> \u00a0<span class=\"kw4\">int<\/span> fileCount<br>\n\u00a0<br>\n\u00a0<span class=\"nu0\">2<\/span> \u00a0def myTagAnalyzer <span class=\"sy0\">=<\/span> <span class=\"kw1\">new<\/span> TagAnalyzerFramework<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span><br>\n\u00a0<br>\n\u00a0<span class=\"nu0\">3<\/span> \u00a0myTagAnalyzer.<span class=\"me1\">atBeginning<\/span> <span class=\"sy0\">=<\/span> <span class=\"br0\">{<\/span><br>\n\u00a0<span class=\"nu0\">4<\/span> \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Print the CSV file header and initialize the file counter<\/span><br>\n\u00a0<span class=\"nu0\">5<\/span> \u00a0 \u00a0 \u00a0println <span class=\"st0\">\"artistDir|albumDir|contentFile\"<\/span><br>\n\u00a0<span class=\"nu0\">6<\/span> \u00a0 \u00a0 \u00a0fileCount <span class=\"sy0\">=<\/span> <span class=\"nu0\">0<\/span><br>\n\u00a0<span class=\"nu0\">7<\/span> \u00a0<span class=\"br0\">}<\/span><br>\n\u00a0<br>\n\u00a0<span class=\"nu0\">8<\/span> \u00a0myTagAnalyzer.<span class=\"me1\">onEachLine<\/span> <span class=\"sy0\">=<\/span> <span class=\"br0\">{<\/span> artistDir, albumDir, contentFile <span class=\"sy0\">-&gt;<\/span><br>\n\u00a0<span class=\"nu0\">9<\/span> \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Print the line for this file<\/span><br><span class=\"nu0\">10<\/span> \u00a0 \u00a0 \u00a0println <span class=\"st0\">\"$artistDir.name|$albumDir.name|$contentFile.name\"<\/span><br><span class=\"nu0\">11<\/span> \u00a0 \u00a0 \u00a0fileCount<span class=\"sy0\">++<\/span><br><span class=\"nu0\">12<\/span> \u00a0<span class=\"br0\">}<\/span><br>\n\u00a0<br><span class=\"nu0\">13<\/span> \u00a0myTagAnalyzer.<span class=\"me1\">atEnd<\/span> <span class=\"sy0\">=<\/span> <span class=\"br0\">{<\/span><br><span class=\"nu0\">14<\/span> \u00a0 \u00a0 \u00a0<span class=\"co1\">\/\/ Print the file counter value<\/span><br><span class=\"nu0\">15<\/span> \u00a0 \u00a0 \u00a0<a href=\"http:\/\/www.google.com\/search?hl=en&amp;q=allinurl%3Adocs.oracle.com+javase+docs+api+system\"><span class=\"kw3\">System<\/span><\/a>.<span class=\"me1\">err<\/span>.<span class=\"me1\">println<\/span> <span class=\"st0\">\"fileCount $fileCount\"<\/span><br><span class=\"nu0\">16<\/span> \u00a0<span class=\"br0\">}<\/span><br>\n\u00a0<br><span class=\"nu0\">17<\/span> \u00a0myTagAnalyzer.<span class=\"me1\">musicLibraryDirName<\/span> <span class=\"sy0\">=<\/span> <span class=\"st0\">'\/home\/clh\/Test\/Music'<\/span><br>\n\u00a0<br><span class=\"nu0\">18<\/span> \u00a0myTagAnalyzer.<span class=\"me1\">processMusicLibrary<\/span><span class=\"br0\">(<\/span><span class=\"br0\">)<\/span><\/div><\/div><\/pre>\n<p>Line 1 defines a local variable, <code>fileCount<\/code>, used to count the number of content files. Note that this variable doesn&#8217;t need to be final.<\/p>\n<p>Line 2 calls the constructor for the <code>TagAnalyzerFramework<\/code> class.<\/p>\n<p>Line 3 does what looks like a mistake in Java. It <em>appears<\/em> to refer to a field in a foreign class. However, in Groovy this is actually calling a setter on that field, so it&#8217;s acceptable, as long as the implementing class &#8220;remembers&#8221; that it has a contract to supply a setter for this property.<\/p>\n<p>Lines 3-7 create a <code>Closure<\/code> that prints the bar-separated value header and initialize the <code>fileCount<\/code> variable.<\/p>\n<p>Lines 8-12 similarly define the <code>Closure<\/code> that handles the logic for processing each line. In this case,it is simply printing the artist, album and content file names. If I refer back to line 24 of <code>TagAnalyzerFramework<\/code>, I see that it calls this <code>Closure<\/code> with three arguments corresponding to the parameters shown here.<\/p>\n<p>Lines 13-16 define the Closure that wraps up the processing once all the data is read. In this case, it prints a count of files to standard error.<\/p>\n<p>Line 17 sets the music library directory name.<\/p>\n<p>And line 18 calls the method to process the music library.<\/p>\n<p>Run the script:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"bash geshifilter-bash\">$ groovy MyTagAnalyzer.groovy<br>\nartistDir<span class=\"sy0\">|<\/span>albumDir<span class=\"sy0\">|<\/span>contentFile<br>\nBombino<span class=\"sy0\">|<\/span>Azel<span class=\"sy0\">|<\/span>07_Igmayagh_Dum_1.6.16.mp3<br>\nBombino<span class=\"sy0\">|<\/span>Azel<span class=\"sy0\">|<\/span>08_Ashuhada_1.6.16.mp3<br>\nBombino<span class=\"sy0\">|<\/span>Azel<span class=\"sy0\">|<\/span>04_Tamiditine_Tarhanam_1.6.16.mp3<br>\nBombino<span class=\"sy0\">|<\/span>Azel<span class=\"sy0\">|<\/span><span class=\"nu0\">10<\/span>_Naqqim_Dagh_Timshar_1.6.16.mp3<br><span class=\"br0\">[<\/span>...<span class=\"br0\">]<\/span><br>\nSt Germain<span class=\"sy0\">|<\/span>Tourist<span class=\"sy0\">|<\/span>04_-_St Germain_-_Land Of....flac<br>\nfileCount <span class=\"nu0\">55<\/span><br>\n$<\/div><\/div><\/pre>\n<p>Of course the <code>.class<\/code> files created by compiling the framework class must be on the classpath for this to work. Naturally, I could <a href=\"https:\/\/opensource.com\/article\/21\/8\/fastjar\" target=\"_blank\" rel=\"noopener\">use jar<\/a> to package up those class files.<\/p>\n<p>Those who are made queasy by what looks like setting fields in a foreign class could define local instances of closures and pass those as parameters, either to the constructor or <code>processMusicLibrary()<\/code>, and achieve the same effect.<\/p>\n<p>I could go back to the code samples provided in the earlier articles to retrofit this framework class. I&#8217;ll leave that exercise to the reader.<\/p>\n<h2>Delegation of behavior<\/h2>\n<p>To me, the coolest thing happening here is the delegation of behavior, which requires various shenanigans in other languages. For many years, Java required anonymous classes and quite a bit of extra code. Lambdas have gone a long way to fixing this, but they still cannot refer to non-final variables outside their scope.<\/p>\n<p>That&#8217;s it for this series on using Groovy to manage the tags in my music library. There will be more Groovy articles in the future.<\/p>\n<\/div>\n<div class=\"clearfix text-formatted field field--name-field-article-subhead field--type-text-long field--label-hidden field__item\">\n<p>I&#8217;ll separate the framework I&#8217;ve been using into a separate class and then write a test program to exercise it.<\/p>\n<\/div>\n<div class=\"field field--name-field-lead-image field--type-entity-reference field--label-hidden field__item\">\n<article class=\"media media--type-image media--view-mode-caption\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/08\/LIFE_musicinfinity.png\" width=\"520\" height=\"292\" alt=\"music infinity\" title=\"Open source software helps artists create music\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>Opensource.com<\/p>\n<\/div>\n<\/article>\n<\/div>\n<div class=\"field field--name-field-tags field--type-entity-reference field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/java\" hreflang=\"en\">Java<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/audio-and-music\" hreflang=\"en\">Audio and music<\/a><\/div>\n<\/p><\/div>\n<div class=\"field field--name-field-listicle-title field--type-string field--label-hidden field__item\">What to read next<\/div>\n<div class=\"field field--name-field-listicles field--type-entity-reference field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-script-java-music\" hreflang=\"en\">How I analyze my music directory with Groovy<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/article\/22\/8\/analyze-music-files-jaudiotagger\" hreflang=\"en\">My favorite open source library for analyzing music files<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-album-music-directory\" hreflang=\"en\">How I use Groovy to analyze album art in my music directory<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/article\/22\/8\/remove-files-music-directory-groovy\" hreflang=\"en\">Clean up unwanted files in your music directory using Groovy<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/article\/22\/8\/groovy-script-music-tags\" hreflang=\"en\">Clean up music tags with a Groovy script<\/a><\/div>\n<\/p><\/div>\n<div class=\"field field--name-field-default-license field--type-list-string field--label-hidden field__item\"><a rel=\"license\" href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/4.0\/\"><br \/>\n        <img decoding=\"async\" alt=\"Creative Commons License\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/08\/cc-by-sa--46.png\" title=\"This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.\"><\/a>This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.<\/div>\n<section class=\"field field--name-field-comments field--type-comment field--label-hidden comment-wrapper\">\n<div class=\"comments__count\">\n<div class=\"login\"><a href=\"https:\/\/opensource.com\/user\/register?absolute=1\">Register<\/a> or <a href=\"https:\/\/opensource.com\/user\/login?current=\/rss.xml&amp;absolute=1\">Login<\/a> to post a comment.<\/div>\n<\/p><\/div>\n<\/section>\n<p class=\"wpematico_credit\"><small>Powered by <a href=\"http:\/\/www.wpematico.com\" target=\"_blank\" rel=\"noopener\">WPeMatico<\/a><\/small><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Make your own music tagging framework with Groovy Chris Hermansen Wed, 08\/31\/2022 &#8211; 03:00 1 reader likes this 1 reader likes this In this series, I&#8217;m developing several scripts to help in cleaning up my music collection. In the last article I wrote and tested a Groovy script to clean up the motley assembly of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":66871,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[307],"tags":[],"class_list":["post-66870","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-open-source"],"_links":{"self":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/66870","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=66870"}],"version-history":[{"count":0,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/66870\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/media\/66871"}],"wp:attachment":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=66870"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=66870"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=66870"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}