package net.filebot.ui.filter; import static java.util.Collections.*; import static net.filebot.Logging.*; import static net.filebot.MediaTypes.*; import static net.filebot.util.FileUtilities.*; import java.awt.Color; import java.io.File; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.CancellationException; import java.util.regex.Pattern; import java.util.stream.IntStream; import javax.swing.BorderFactory; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import net.filebot.mediainfo.MediaInfo; import net.filebot.mediainfo.MediaInfo.StreamKind; import net.filebot.util.ui.LoadingOverlayPane; import net.miginfocom.swing.MigLayout; class MediaInfoTool extends Tool<TableModel> { private JTable table = new JTable(new MediaInfoTableModel()); public MediaInfoTool() { super("MediaInfo"); table.setAutoCreateRowSorter(true); table.setAutoCreateColumnsFromModel(true); table.setFillsViewportHeight(true); table.setCellSelectionEnabled(true); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setBackground(Color.white); table.setGridColor(new Color(0xEEEEEE)); table.setRowHeight(25); JScrollPane scrollPane = new JScrollPane(table); scrollPane.setBorder(BorderFactory.createEmptyBorder()); setLayout(new MigLayout("insets 0, fill")); add(new LoadingOverlayPane(scrollPane, this), "grow"); } @Override protected TableModel createModelInBackground(List<File> root) { if (root.isEmpty()) { return new MediaInfoTableModel(); } List<File> files = listFiles(root, filter(VIDEO_FILES, AUDIO_FILES), HUMAN_NAME_ORDER); Map<MediaInfoKey, String[]> data = new TreeMap<MediaInfoKey, String[]>(); try (MediaInfo mi = new MediaInfo()) { IntStream.range(0, files.size()).forEach(f -> { try { mi.open(files.get(f)); mi.snapshot().forEach((kind, streams) -> { IntStream.range(0, streams.size()).forEach(i -> { streams.get(i).forEach((name, value) -> { String[] values = data.computeIfAbsent(new MediaInfoKey(kind, i, name), k -> new String[files.size()]); values[f] = value; }); }); }); } catch (IllegalArgumentException e) { debug.finest(e::toString); } catch (Exception e) { debug.warning(e::toString); } if (Thread.interrupted()) { throw new CancellationException(); } }); } return new MediaInfoTableModel(data.isEmpty() ? emptyList() : files, data); } @Override protected void setModel(TableModel model) { table.setModel(model); table.setAutoResizeMode(table.getRowCount() > 0 ? JTable.AUTO_RESIZE_OFF : JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); TableColumnModel columnModel = table.getColumnModel(); IntStream.range(0, columnModel.getColumnCount()).forEach(i -> columnModel.getColumn(i).setMinWidth(150)); } private static class MediaInfoKey implements Comparable<MediaInfoKey> { public final StreamKind kind; public final int stream; public final String name; private static final Pattern strip = Pattern.compile("[^a-z]", Pattern.CASE_INSENSITIVE); public MediaInfoKey(StreamKind kind, int stream, String name) { this.kind = kind; this.stream = stream; this.name = strip.matcher(name).replaceAll(""); } @Override public boolean equals(Object obj) { if (obj instanceof MediaInfoKey) { MediaInfoKey other = (MediaInfoKey) obj; return kind == other.kind && stream == other.stream && name.equals(other.name); } return false; } @Override public int hashCode() { return kind.ordinal() + (stream << 8) + name.hashCode(); } @Override public int compareTo(MediaInfoKey other) { if (kind != other.kind) return kind.compareTo(other.kind); if (stream != other.stream) return Integer.compare(stream, other.stream); else return name.compareTo(other.name); } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (kind != StreamKind.General) { sb.append(kind.name()); if (stream > 0) { sb.append('[').append(stream).append(']'); } sb.append('.'); } return sb.append(name).toString(); } } private static class MediaInfoTableModel extends AbstractTableModel { private final MediaInfoKey[] keys; private final String[][] values; private final String[] files; private final Class<?>[] columnClass; public MediaInfoTableModel() { this(emptyList(), emptyMap()); } public MediaInfoTableModel(List<File> files, Map<MediaInfoKey, String[]> values) { this.keys = values.keySet().toArray(new MediaInfoKey[0]); this.values = values.values().toArray(new String[0][]); this.files = files.stream().map(File::getName).toArray(String[]::new); this.columnClass = new Class<?>[getColumnCount()]; } public int getHeaderColumnCount() { return 1; } @Override public int getColumnCount() { return keys.length + getHeaderColumnCount(); } @Override public String getColumnName(int column) { switch (column) { case 0: return "File"; default: return keys[column - getHeaderColumnCount()].toString(); } } private boolean isNumber(String s) { try { Double.parseDouble(s); return true; } catch (Exception e) { return false; } } @Override public Class<?> getColumnClass(int column) { int c = column - getHeaderColumnCount(); if (c < 0) { return String.class; } if (columnClass[c] != null) { return columnClass[c]; } if (IntStream.range(0, files.length).mapToObj(i -> values[c][i]).filter(Objects::nonNull).allMatch(this::isNumber)) { columnClass[c] = Number.class; return columnClass[c]; } columnClass[c] = String.class; return columnClass[c]; } @Override public int getRowCount() { return files.length; } @Override public String getValueAt(int row, int column) { switch (column) { case 0: return files[row]; default: return values[column - getHeaderColumnCount()][row]; } } } }