/* * Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * version 3 along with this work. If not, see <http://www.gnu.org/licenses/>. */ package com.tulskiy.musique.playlist; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Map.Entry; import java.util.logging.Logger; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.datatype.Pair; import com.tulskiy.musique.audio.AudioFileReader; import com.tulskiy.musique.gui.model.FieldValues; import com.tulskiy.musique.gui.playlist.PlaylistColumn; import com.tulskiy.musique.gui.playlist.SeparatorTrack; import com.tulskiy.musique.playlist.formatting.Parser; import com.tulskiy.musique.playlist.formatting.tokens.Expression; import com.tulskiy.musique.system.TrackIO; import com.tulskiy.musique.util.AudioMath; import com.tulskiy.musique.util.Util; /** * Author: Denis Tulskiy * Date: Dec 30, 2009 */ public class Playlist extends ArrayList<Track> { private static final String OLD_META_KEY_COMMENT = "comment"; private static final String OLD_META_KEY_GENRE = "genre"; private static final String OLD_META_KEY_YEAR = "year"; private static final String OLD_META_KEY_TOTAL_DISCS = "totalDiscs"; private static final String OLD_META_KEY_DISC_NUMBER = "discNumber"; private static final String OLD_META_KEY_TOTAL_TRACKS = "totalTracks"; private static final String OLD_META_KEY_TRACK_NUMBER = "trackNumber"; private static final String OLD_META_KEY_TITLE = "title"; private static final String OLD_META_KEY_ALBUM_ARTIST = "albumArtist"; private static final String OLD_META_KEY_ALBUM = "album"; private static final String OLD_META_KEY_ARTIST = "artist"; private static final String META_KEY_CODEC = "codec"; private static final String META_KEY_ENCODER = "encoder"; private static MessageFormat format = new MessageFormat("\"{0}\" \"{1}\" {2}"); private static final int VERSION = 3; private static final byte[] MAGIC = "BARABASHKA".getBytes(); private final Logger logger = Logger.getLogger(getClass().getName()); private ArrayList<PlaylistListener> listeners = new ArrayList<PlaylistListener>(); private String name; private boolean sortAscending = true; private String sortBy; private String groupBy; private Expression groupExpression; private boolean libraryView; private List<PlaylistColumn> columns; @Deprecated public Playlist(String fmt) { try { Object[] objects = format.parse(fmt); setName((String) objects[0]); setGroupBy((String) objects[1]); setLibraryView(Boolean.valueOf((String) objects[2])); } catch (Exception e) { e.printStackTrace(); } } public Playlist() { } public void cleanUp() { Iterator<Track> it = iterator(); while (it.hasNext()) { Track next = it.next(); if (next instanceof SeparatorTrack) it.remove(); } } public void save(File file) { try { //remove the garbage cleanUp(); logger.fine("Saving playlist: " + file.getName()); DataOutputStream dos = new DataOutputStream( new BufferedOutputStream(new FileOutputStream(file))); dos.write(MAGIC); dos.writeInt(VERSION); dos.writeInt(size()); List<Pair> meta = new LinkedList<Pair>(); TrackData trackData; for (Track track : this) { trackData = track.getTrackData(); trackData.removeEmptyTagFields(); dos.writeUTF(trackData.getLocation().toString()); dos.writeLong(trackData.getStartPosition()); dos.writeLong(trackData.getTotalSamples()); dos.writeInt(trackData.getSubsongIndex()); if (trackData.getSubsongIndex() > 0) { dos.writeBoolean(trackData.isCueEmbedded()); if (!trackData.isCueEmbedded()) dos.writeUTF(trackData.getCueLocation()); } dos.writeInt(trackData.getBps()); dos.writeInt(trackData.getChannels()); dos.writeInt(trackData.getSampleRate()); dos.writeInt(trackData.getBitrate()); dos.writeLong(trackData.getDateAdded()); dos.writeLong(trackData.getLastModified()); meta.clear(); if (!Util.isEmpty(trackData.getCodec())) meta.add(new Pair(META_KEY_CODEC, trackData.getCodec())); if (!Util.isEmpty(trackData.getEncoder())) { meta.add(new Pair(META_KEY_ENCODER, trackData.getEncoder())); } Iterator<Entry<FieldKey, FieldValues>> fields = trackData.getAllTagFieldValuesIterator(); if (fields != null) { while (fields.hasNext()) { Entry<FieldKey, FieldValues> field = fields.next(); for (int i = 0; i < field.getValue().size(); i++) { String value = field.getValue().get(i); meta.add(new Pair(field.getKey().toString(), value)); } } } dos.writeInt(meta.size()); for (Pair pair : meta) { dos.writeUTF(pair.getKey()); dos.writeUTF(pair.getValue()); } } dos.close(); regroup(); } catch (IOException e) { logger.warning("Failed to save playlist " + file.getName() + ": " + e.getMessage()); } } public void load(File file) { try { TrackDataCache cache = TrackDataCache.getInstance(); logger.fine("Loading musique playlist: " + file.getName()); DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream(file))); byte[] b = new byte[MAGIC.length]; dis.readFully(b); if (!Arrays.equals(b, MAGIC)) { logger.warning("Wrong magic word"); throw new RuntimeException(); } int version = dis.readInt(); if (version > VERSION) { logger.warning("Playlist has newer version, expected: " + VERSION + " got: " + version); throw new RuntimeException(); } int size = dis.readInt(); ensureCapacity(size); for (int i = 0; i < size; i++) { Track track = new Track(); TrackData trackData = track.getTrackData(); trackData.setLocation(dis.readUTF()); trackData.setStartPosition(dis.readLong()); trackData.setTotalSamples(dis.readLong()); trackData.setSubsongIndex(dis.readInt()); cache.cache(track); trackData = track.getTrackData(); if (trackData.getSubsongIndex() > 0) { trackData.setCueEmbedded(dis.readBoolean()); if (!trackData.isCueEmbedded()) trackData.setCueLocation(dis.readUTF()); } trackData.setBps(dis.readInt()); trackData.setChannels(dis.readInt()); trackData.setSampleRate(dis.readInt()); trackData.setBitrate(dis.readInt()); if (version == 1) { trackData.setDateAdded(System.currentTimeMillis()); } else { trackData.setDateAdded(dis.readLong()); trackData.setLastModified(dis.readLong()); } int metaSize = dis.readInt(); for (int j = 0; j < metaSize; j++) { String key = dis.readUTF(); String value = dis.readUTF(); if (version == VERSION) { if (key.equals(META_KEY_CODEC)) { trackData.setCodec(value); } else if (key.equals(META_KEY_ENCODER)) { trackData.setEncoder(value); } else { trackData.addTagFieldValues(FieldKey.valueOf(key), value); } } // read older playlist version else { if (key.equals(META_KEY_CODEC)) { trackData.setCodec(value); } else if (key.equals(OLD_META_KEY_ARTIST)) { trackData.addArtist(value); } else if (key.equals(OLD_META_KEY_ALBUM)) { trackData.addAlbum(value); } else if (key.equals(OLD_META_KEY_ALBUM_ARTIST)) { trackData.addAlbumArtist(value); } else if (key.equals(OLD_META_KEY_TITLE)) { trackData.addTitle(value); } else if (key.equals(OLD_META_KEY_TRACK_NUMBER)) { trackData.addTrack(value); } else if (key.equals(OLD_META_KEY_TOTAL_TRACKS)) { trackData.addTrackTotal(value); } else if (key.equals(OLD_META_KEY_DISC_NUMBER)) { trackData.addDisc(value); } else if (key.equals(OLD_META_KEY_TOTAL_DISCS)) { trackData.addDiscTotal(value); } else if (key.equals(OLD_META_KEY_YEAR)) { trackData.addYear(value); } else if (key.equals(OLD_META_KEY_GENRE)) { trackData.addGenre(value); } else if (key.equals(OLD_META_KEY_COMMENT)) { trackData.addComment(value); } } } add(track); } dis.close(); } catch (Exception e) { logger.warning("Failed to load playlist " + file.getName() + ": " + e.getMessage()); } } public boolean isLibraryView() { return libraryView; } public void setLibraryView(boolean libraryView) { this.libraryView = libraryView; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGroupBy() { return groupBy; } public List<String> loadM3U(String location) { Scanner fi; ArrayList<String> items = new ArrayList<String>(); logger.fine("Loading M3U from: " + location); try { File parent = null; if (location.toLowerCase().startsWith("http://")) { fi = new Scanner(new URL(location).openStream()); } else { File source = new File(location); fi = new Scanner(source); parent = source.getParentFile().getAbsoluteFile(); } while (fi.hasNextLine()) { String line = fi.nextLine().trim(); if (line.isEmpty() || line.startsWith("#")) continue; // skip utf8 BOM if (((int) line.charAt(0)) == 0xFEFF) { line = line.substring(1); } if (line.toLowerCase().startsWith("http://")) { items.add(line); } else { //it's a file, resolve it File file = new File(line); if (!file.isAbsolute()) file = new File(parent, line); items.add(file.getAbsolutePath()); } } fi.close(); } catch (IOException e) { e.printStackTrace(); } return items; } public void saveM3U(File file) { try { PrintWriter pw = new PrintWriter(file); pw.println("#EXTM3U"); Expression expression = Parser.parse("[%artist% - ]%title%"); for (Track track : this) { if (track.getTrackData().isStream()) { pw.println(track.getTrackData().getLocation()); } else if (track.getTrackData().isFile()) { int seconds = (int) AudioMath.samplesToMillis( track.getTrackData().getTotalSamples(), track.getTrackData().getSampleRate()) / 1000; String title = String.valueOf(expression.eval(track)); pw.printf("#EXTINF:%d,%s\n%s\n", seconds, title, track.getTrackData().getFile().getAbsolutePath()); } } pw.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } } public List<String> loadPLS(String location) { Scanner fi; ArrayList<String> items = new ArrayList<String>(); logger.fine("Loading PLS from: " + location); try { if (location.toLowerCase().startsWith("http://")) { fi = new Scanner(new URL(location).openStream()); } else { fi = new Scanner(new File(location)); } if (!fi.nextLine().equalsIgnoreCase("[playlist]")) { logger.warning("PLS has to start with [playlist]: " + location); return items; } fi.useDelimiter("[=\\p{javaWhitespace}+]"); while (fi.hasNext()) { String line = fi.next().trim(); if (line.toLowerCase().startsWith("file")) { items.add(fi.next()); } } fi.close(); } catch (Exception e) { e.printStackTrace(); } return items; } public void savePLS(File file) { try { PrintWriter pw = new PrintWriter(file); Expression expression = Parser.parse("[%artist% - ]%title%"); pw.println("[playlist]"); pw.println("NumberOfEntries=" + size()); for (int i = 0; i < size(); i++) { Track track = get(i); int index = i + 1; if (track.getTrackData().isFile()) { pw.printf("File%d=%s\n", index, track.getTrackData().getFile().getAbsolutePath()); pw.printf("Title%d=%s\n", index, expression.eval(track)); pw.printf("Length%d=%s\n", index, (int) AudioMath.samplesToMillis( track.getTrackData().getTotalSamples(), track.getTrackData().getSampleRate()) / 1000); } else if (track.getTrackData().isStream()) { pw.printf("File%d=%s\n", index, track.getTrackData().getLocation().normalize()); } pw.println(); } pw.println("Version=2"); pw.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } } public int insertItem(String address, int location, boolean recurse, Map<String, Object> progress) { ArrayList<Track> temp = new ArrayList<Track>(); LinkedList<Object> queue = new LinkedList<Object>(); if (location == -1) location = size(); String ext = Util.getFileExt(address); if (ext.equals("m3u") || ext.equals("m3u8")) { queue.addAll(loadM3U(address)); } else if (ext.equals("pls")) { queue.addAll(loadPLS(address)); } else if (ext.equals("mus")) { Playlist newPl = new Playlist(); newPl.load(new File(address)); addAll(location, newPl); } else { queue.push(address); } while (!queue.isEmpty()) { try { Object top = queue.pop(); String topStr = top.toString(); if (progress != null) { if (progress.get("processing.stop") != null) { break; } progress.put("processing.file", topStr); } if (top instanceof String && topStr.startsWith("http://")) { Track track = new Track(); URI uri = new URI(topStr); String title = uri.getPath(); if (Util.isEmpty(title)) title = uri.getHost(); track.getTrackData().addTitle(title); track.getTrackData().setLocation(uri.toString()); track.getTrackData().setTotalSamples(-1); temp.add(track); } else { File file = null; if (top instanceof String) file = new File(topStr); else if (top instanceof File) file = (File) top; if (recurse && file.isDirectory()) { queue.addAll(0, Arrays.asList(file.listFiles())); } else if (file.isFile()) { AudioFileReader reader = TrackIO.getAudioFileReader(file.getName()); if (reader != null) { if (!Util.getFileExt(file).equals("cue")) { String name = Util.removeExt(file.getAbsolutePath()) + ".cue"; if (new File(name).exists()) { continue; } } reader.read(file, temp); } } } } catch (Exception ignored) { } } Collections.sort(temp, new Comparator<Track>() { @Override public int compare(Track o1, Track o2) { return o1.getTrackData().getLocation().compareTo(o2.getTrackData().getLocation()); } }); TrackDataCache cache = TrackDataCache.getInstance(); for (Track track : temp) { cache.cache(track); } int sizeOld = size(); addAll(location, temp); firePlaylistChanged(); queue.clear(); temp.clear(); return size() - sizeOld; } public void sort(String expression, boolean toggle) { logger.fine("Sorting playlist with expression: " + expression); if (toggle && expression.equals(sortBy)) { sortAscending = !sortAscending; } else { sortAscending = true; sortBy = expression; } final Expression e = Parser.parse(expression); TrackComparator trackComparator = new TrackComparator(e); if (sortAscending) Collections.sort(this, trackComparator); else Collections.sort(this, Collections.reverseOrder(trackComparator)); } public void setGroupBy(String expression) { groupBy = expression; logger.fine("Grouping playlist with expression: " + expression); groupExpression = Util.isEmpty(expression) ? null : Parser.parse(expression); firePlaylistChanged(); } public void firePlaylistChanged() { regroup(); for (PlaylistListener listener : listeners) { listener.playlistUpdated(this); } } public void regroup() { cleanUp(); if (groupExpression == null) return; int start = 0; int size = 0; final String unknown = "?"; String groupName = null; for (int i = 0; i < size(); i++) { Track track = get(i); Object o = groupExpression.eval(track); String value = null; if (o != null) value = o.toString(); if (Util.isEmpty(value)) value = unknown; if (groupName == null) { groupName = value; start = i; size = 1; continue; } //noinspection ConstantConditions boolean sameGroup = value.equalsIgnoreCase(groupName); if (sameGroup) size++; if (!sameGroup) { if (size > 0) { addGroup(groupName, start, size); } else { i--; } groupName = null; } } if (groupName != null) addGroup(groupName, start, size); } private void addGroup(String groupName, int start, int size) { SeparatorTrack group = new SeparatorTrack(groupName, size); add(start, group); } @Override public Track get(int index) { return index >= 0 && index < size() ? super.get(index) : null; } @Override public String toString() { if (groupBy == null) groupBy = ""; return format.format(new Object[]{name, groupBy, libraryView}); } @Override public boolean equals(Object o) { return o instanceof Playlist && this == o; } public void removeDeadItems() { for (Iterator it = this.iterator(); it.hasNext();) { Track track = (Track) it.next(); if (track.getTrackData().getLocation() == null) continue; if (track.getTrackData().isFile() && !track.getTrackData().getFile().exists()) { it.remove(); } } firePlaylistChanged(); } public void removeDuplicates() { ArrayList<Track> dup = new ArrayList<Track>(); for (int i = 0; i < size() - 1; i++) { Track t1 = get(i); URI l1 = t1.getTrackData().getLocation(); if (l1 == null) continue; for (int j = i + 1; j < size(); j++) { Track t2 = get(j); if (l1.equals(t2.getTrackData().getLocation()) && t1.getTrackData().getSubsongIndex() == t2.getTrackData().getSubsongIndex()) { dup.add(t2); } } } removeAll(dup); firePlaylistChanged(); } public void addChangeListener(PlaylistListener listener) { listeners.add(listener); } public void removeChangeListener(PlaylistListener listener) { listeners.remove(listener); } public void setColumns(List<PlaylistColumn> columns) { this.columns = columns; } public List<PlaylistColumn> getColumns() { return columns; } }