// Sample beaTlet for beaTunes 5.x // More info at https://www.beatunes.com/en/beatlet-getting-started.html import javax.swing.* import java.awt.event.ActionEvent import java.awt.FileDialog import java.awt.Cursor import java.text.SimpleDateFormat import java.io.InputStreamReader import java.io.FileInputStream import java.nio.file.Paths import com.tagtraum.core.OperatingSystem import com.tagtraum.core.app.* import com.tagtraum.beatunes.MessageDialog import com.tagtraum.beatunes.action.* import com.tagtraum.audiokern.AudioSong import com.tagtraum.audiokern.AudioSongLocation import javax.xml.stream.* import javax.xml.stream.events.* import javax.xml.namespace.* import java.util.Collections import org.slf4j.* class ImportTraktorDatesAndCounts extends BaseAction { static log = LoggerFactory.getLogger("ImportTraktorDatesAndCounts.groovy") def String getId() { "Groovy.ImportTraktorDatesAndCounts" } def void init() { putValue(Action.NAME, "Import Traktor Dates, Counts & Comment 2") } def ActionLocation[] getActionLocations() { [new AbsoluteActionLocation(BeaTunesUIRegion.TOOL_MENU, AbsoluteActionLocation.LAST)] } def void actionPerformed(ActionEvent actionEvent) { int res = new MessageDialog( getApplication().getMainWindow(), "Do you really want to import Traktor dates, counts, and comment 2?

" + "This will overwrite all existing dates, counts, and the custom1 field.", JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION ).showDialog() if (res != JOptionPane.YES_OPTION) return // show dialog that lets user select the collection.nml file final File collection = getCollectionNML() if (collection == null) return // this can take a little while. set wait cursor (blue wheel) getApplication().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)) // iterate over ,, tags, but not in EDT def t = new Thread({ XMLInputFactory factory = null XMLEventReader reader = null try { factory = XMLInputFactory.newInstance() reader = factory.createXMLEventReader(new InputStreamReader(new FileInputStream(collection), "UTF-8")) String location = null while(reader.hasNext()) { XMLEvent event = reader.next() if (event.isStartElement()) { StartElement element = event.asStartElement() if (element.getName().getLocalPart().equals("LOCATION")) { location = getLocation(element) } if (location != null && element.getName().getLocalPart().equals("INFO")) { importInfo(element, location) location = null } } } // let user know we're done in EDT SwingUtilities.invokeLater({ // reset to regular cursor getApplication().getMainWindow().setCursor(null) new MessageDialog( getApplication().getMainWindow(), "Done.", JOptionPane.INFORMATION_MESSAGE, JOptionPane.DEFAULT_OPTION ).showDialog() } as Runnable) } catch (Exception e) { // report errod in EDT SwingUtilities.invokeLater({ // reset to regular cursor getApplication().getMainWindow().setCursor(null) new MessageDialog( getApplication().getMainWindow(), "An error occurred: " + e.toString(), JOptionPane.ERROR_MESSAGE, JOptionPane.DEFAULT_OPTION ).showDialog() } as Runnable) } finally { if (reader != null) reader.close() } } as Runnable) t.start() } def boolean importInfo(StartElement element, String location) { AudioSong song = null try { Attribute playcount = element.getAttributeByName(new QName("PLAYCOUNT")) Attribute lastPlayed = element.getAttributeByName(new QName("LAST_PLAYED")) // probably for historic reasons, Traktor calls comment 2 "rating" // its .nml file. // we map its contents to the field custom1. Attribute rating = element.getAttributeByName(new QName("RATING")) if (playcount != null || lastPlayed != null || rating != null) { song = getSong(location) if (song != null) { if (lastPlayed != null) { log.debug("Setting playdate for " + song + ": " + lastPlayed.getValue()) song.setPlayDateUTC(new SimpleDateFormat("yyyy/M/d").parse(lastPlayed.getValue())) } if (playcount != null) { log.debug("Setting playcount for " + song + ": " + playcount.getValue()) song.setPlayCount(Integer.valueOf(playcount.getValue())) } if (rating != null) { log.debug("Setting rating/comment2/custom1 for " + song + ": " + rating.getValue()) song.setCustom1(rating.getValue()) } } else { log.info("Failed to find song for location " + location) } } } catch(Exception e) { log.error("Failed to import info for " + song, e) } } def String getLocation(StartElement element) { String dir = element.getAttributeByName(new QName("DIR")).getValue() String file = element.getAttributeByName(new QName("FILE")).getValue() String volume = element.getAttributeByName(new QName("VOLUME")).getValue() //System.out.println(volume + dir + file) if (OperatingSystem.isMac()) { return "/Volumes/" + volume + (dir.replace("/:", "/")) + file } else { return volume + (dir.replace("/:", "/")) + file } } def AudioSong getSong(String location) { AudioSongLocation audioSongLocation = new AudioSongLocation(Paths.get(location)) List songs = getApplication().getMediaLibrary().getSongsWithProperties(Collections.singletonMap("location", audioSongLocation.getLocation())) if (songs.isEmpty() && audioSongLocation.isFile() && audioSongLocation.getLocation().contains("/localhost/")) { songs = getApplication().getMediaLibrary().getSongsWithProperties(Collections.singletonMap("location", audioSongLocation.getLocation().replace("localhost", ""))) } return songs.isEmpty() ? null : songs.get(0) } def File getCollectionNML() { final FileDialog dialog = new FileDialog(getApplication().getMainWindow(), "Please select the collection.nml file") dialog.setFilenameFilter({d, f-> f ==~ /.*.nml/ } as FilenameFilter) dialog.setDirectory(System.getProperty("user.home") + "/Documents/Native Instruments/") dialog.setMode(FileDialog.LOAD) dialog.setVisible(true) return dialog.getFile() == null ? null : dialog.getFiles()[0] } }