diff --git a/src/main/java/listfix/io/BufferedProgressReader.java b/src/main/java/listfix/io/BufferedProgressReader.java new file mode 100644 index 00000000..ec3bc862 --- /dev/null +++ b/src/main/java/listfix/io/BufferedProgressReader.java @@ -0,0 +1,42 @@ +package listfix.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; + +public class BufferedProgressReader extends BufferedReader +{ + + // Used to keep track how much of the file we've read. + private long charsRead = 0; + + public BufferedProgressReader(Reader in, int sz) + { + super(in, sz); + } + + public BufferedProgressReader(Reader in) + { + super(in); + } + + // Custom readLine implementation that appends to the internal cache so we know how much of the file we've read. + @Override + public String readLine() throws IOException + { + String line = super.readLine(); + if (line != null) + { + // Not the End-Of-File yet, count bytes + charsRead += line.length() + 1; + } + return line; + } + + /** + * @return Number of characters read + */ + public long getCharactersRead() { + return this.charsRead; + } +} diff --git a/src/main/java/listfix/io/playlists/PlaylistReader.java b/src/main/java/listfix/io/playlists/PlaylistReader.java index 4a999b94..3611ef35 100644 --- a/src/main/java/listfix/io/playlists/PlaylistReader.java +++ b/src/main/java/listfix/io/playlists/PlaylistReader.java @@ -1,5 +1,6 @@ package listfix.io.playlists; +import listfix.io.BufferedProgressReader; import listfix.io.Constants; import listfix.io.IPlaylistOptions; import listfix.model.playlists.FilePlaylistEntry; @@ -8,13 +9,16 @@ import listfix.util.ArrayFunctions; import listfix.util.OperatingSystem; import listfix.view.GUIScreen; +import org.apache.commons.io.ByteOrderMark; +import org.apache.commons.io.input.BOMInputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.File; +import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.List; import java.util.StringTokenizer; @@ -25,7 +29,7 @@ public abstract class PlaylistReader implements IPlaylistReader protected final Path playlistPath; protected Charset encoding; - private final Logger _logger = LogManager.getLogger(GUIScreen.class); + private final Logger logger = LogManager.getLogger(GUIScreen.class); public PlaylistReader(IPlaylistOptions playListOptions, Path playlistPath) { this.playListOptions = playListOptions; @@ -43,6 +47,22 @@ public Charset getEncoding() return this.encoding; } + public BufferedProgressReader openBufferedReader(Path textFile) throws IOException + { + final BOMInputStream bomInputStream = new BOMInputStream(new FileInputStream(textFile.toFile()), + ByteOrderMark.UTF_8, + ByteOrderMark.UTF_16BE, + ByteOrderMark.UTF_16LE, + ByteOrderMark.UTF_32BE, + ByteOrderMark.UTF_32LE + ); + final String charsetName = bomInputStream.getBOMCharsetName(); + this.encoding = charsetName == null ? StandardCharsets.UTF_8 : Charset.forName(charsetName); + + this.logger.info(String.format("Detected playlist file encoding for \"%s\": %s", textFile.getFileName().toString(), this.encoding.name())); + return new BufferedProgressReader(new InputStreamReader(bomInputStream, this.encoding)); + } + protected void processEntry(List results, String L2, String cid, String tid) { StringTokenizer pathTokenizer = null; @@ -156,7 +176,7 @@ else if (L2.startsWith("\\\\") && pathTokenizer.countTokens() >= 1 } catch (URISyntaxException e) { - this._logger.warn("While adding URI entry to playlist", e); + this.logger.warn("While adding URI entry to playlist", e); throw new RuntimeException(e); } } diff --git a/src/main/java/listfix/io/playlists/m3u/M3UReader.java b/src/main/java/listfix/io/playlists/m3u/M3UReader.java index 46a8456c..ed3ec3cd 100644 --- a/src/main/java/listfix/io/playlists/m3u/M3UReader.java +++ b/src/main/java/listfix/io/playlists/m3u/M3UReader.java @@ -20,50 +20,32 @@ package listfix.io.playlists.m3u; +import listfix.io.BufferedProgressReader; import listfix.io.IPlaylistOptions; -import listfix.io.UnicodeInputStream; import listfix.io.playlists.PlaylistReader; import listfix.model.enums.PlaylistType; import listfix.model.playlists.PlaylistEntry; import listfix.view.support.IProgressObserver; import listfix.view.support.ProgressAdapter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import java.io.*; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; /** * Reads in a M3U/M3U8 file and returns a List containing PlaylistEntries that represent the files & URIs in the playlist. - * - * @author jcaron */ public class M3UReader extends PlaylistReader { - private final BufferedReader buffer; private final List results = new ArrayList<>(); - private final long fileLength; private static final PlaylistType type = PlaylistType.M3U; - private static final Logger _logger = LogManager.getLogger(M3UReader.class); - - private StringBuilder _cache; - public M3UReader(IPlaylistOptions playListOptions, Path m3uPath) throws FileNotFoundException + public M3UReader(IPlaylistOptions playListOptions, Path m3uPath) throws IOException { super(playListOptions, m3uPath); - - File m3uFile = m3uPath.toFile(); - - // encoding = UnicodeUtils.getEncoding(m3uFile); - UnicodeInputStream unicodeInputStream = new UnicodeInputStream(new FileInputStream(m3uFile), StandardCharsets.UTF_8); - this.encoding = Charset.forName(unicodeInputStream.getEncoding()); - _logger.info(String.format("Detected M3U encoding for \"%s\": %s", m3uPath.getFileName().toString(), this.encoding.name())); - buffer = new BufferedReader(new InputStreamReader(unicodeInputStream, this.encoding)); - fileLength = m3uFile.length(); } @Override @@ -81,114 +63,107 @@ public PlaylistType getPlaylistType() @Override public List readPlaylist(IProgressObserver observer) throws IOException { - // See http://gonze.com/playlists/playlist-format-survey.html#M3U for the format of an M3U file. - // Line1 holds the metadata about the file that we just hang on to, line2 represents the file reference. - - //Initialize the progress adapter if we're given an observer. - ProgressAdapter progress = ProgressAdapter.wrap(observer); + long fileLength = Files.size(this.playlistPath); - _cache = new StringBuilder(); - String line1 = readLine(); - String line2; - if (line1 != null) + try (BufferedProgressReader buffer = openBufferedReader(this.playlistPath)) { - // Ignore the standard M3U header and random mediamonkey crap. - while (line1.contains("#EXTM3U") || line1.startsWith("#EXTINFUTF8") || line1.isEmpty()) - { - line1 = readLine(); - if (line1 == null) - { - // needed to handle empty playlists - return results; - } - } + // See http://gonze.com/playlists/playlist-format-survey.html#M3U for the format of an M3U file. + // Line1 holds the metadata about the file that we just hang on to, line2 represents the file reference. - // If after skipping that line the line doesn't start w/ a #, then we already have the file reference. Stuff that into line2. - if (!line1.startsWith("#")) - { - line2 = line1; - line1 = ""; - } - else - { - // Otherwise, read in the next line which should be our file reference. - line2 = readLine(); - while (line2.startsWith("#")) - { - // throw away non-standard metadata added by mediamonkey... - line2 = readLine(); - } - } - - // Declare this variable outside the loop so we don't do it over and over. - int cacheSize; + //Initialize the progress adapter if we're given an observer. + ProgressAdapter progress = ProgressAdapter.wrap(observer); + progress.setTotal(fileLength); - while (line1 != null) + String line1 = buffer.readLine(); + String line2; + if (line1 != null) { - // If we have an observer and the user cancelled, bail out. - if (observer != null) + // Ignore the standard M3U header and random mediamonkey crap. + while (line1.contains("#EXTM3U") || line1.startsWith("#EXTINFUTF8") || line1.isEmpty()) { - if (observer.getCancelled()) + line1 = buffer.readLine(); + if (line1 == null) { - return null; + // needed to handle empty playlists + return results; } } - // Process the two strings we have into a playlist entry - processEntry(line2, line1); - - // We just processed an entry, update the progress bar w/ the % of the file we've read if we have an observer. - cacheSize = _cache.toString().getBytes().length; - if (cacheSize < fileLength) + // If after skipping that line the line doesn't start w/ a #, then we already have the file reference. Stuff that into line2. + if (!line1.startsWith("#")) { - progress.setCompleted(cacheSize); + line2 = line1; + line1 = ""; } - - // Start processing the next entry. - line1 = readLine(); - if (line1 != null) + else { - // WMP produces M3Us with spaces between entries... have to read in an extra line to avoid this if line1 is empty. - // Let's also handle an arbitrary number of spaces between the entries while we're at it. - while (line1.isEmpty()) + // Otherwise, read in the next line which should be our file reference. + line2 = buffer.readLine(); + while (line2.startsWith("#")) { - line1 = readLine(); + // throw away non-standard metadata added by mediamonkey... + line2 = buffer.readLine(); + } + } - // And of course WMP ends the file w/ several blank lines, so if we find a null here return what we have... - if (line1 == null) + while (line1 != null) + { + // If we have an observer and the user cancelled, bail out. + if (observer != null) + { + if (observer.getCancelled()) { - // Fill the progress bar - progress.setCompleted((int) fileLength); - - return results; + return null; } } - if (!line1.startsWith("#")) - { - line2 = line1; - line1 = ""; - } - else + // Process the two strings we have into a playlist entry + processEntry(line2, line1); + + // We just processed an entry, update the progress bar w/ the % of the file we've read if we have an observer. + long bytesRead = Math.min(fileLength, buffer.getCharactersRead()); + progress.setCompleted((int) bytesRead); + + // Start processing the next entry. + line1 = buffer.readLine(); + if (line1 != null) { - line2 = readLine(); - while (line2.startsWith("#")) + // WMP produces M3Us with spaces between entries... have to read in an extra line to avoid this if line1 is empty. + // Let's also handle an arbitrary number of spaces between the entries while we're at it. + while (line1.isEmpty()) + { + line1 = buffer.readLine(); + + // And of course WMP ends the file w/ several blank lines, so if we find a null here return what we have... + if (line1 == null) + { + // Fill the progress bar + progress.setCompleted(fileLength); + + return results; + } + } + + if (!line1.startsWith("#")) { - // throw away non-standard metadata added by mediamonkey... - line2 = readLine(); + line2 = line1; + line1 = ""; + } + else + { + line2 = buffer.readLine(); + while (line2.startsWith("#")) + { + // throw away non-standard metadata added by mediamonkey... + line2 = buffer.readLine(); + } } } } } + progress.setCompleted((int) fileLength); + return results; } - - // Close the reader - buffer.close(); - - // Fill the progress bar - progress.setCompleted((int) fileLength); - - return results; } @Override @@ -198,16 +173,6 @@ public List readPlaylist() throws IOException return results; } - // Custom readLine implementation that appends to the internal cache so we know how much of the file we've read. - private String readLine() throws IOException - { - String line = buffer.readLine(); - if (_cache != null) - { - _cache.append(line); - } - return line; - } private void processEntry(String L2, String extInf) { diff --git a/src/main/java/listfix/view/support/ProgressAdapter.java b/src/main/java/listfix/view/support/ProgressAdapter.java index 9883ebdb..12c32899 100644 --- a/src/main/java/listfix/view/support/ProgressAdapter.java +++ b/src/main/java/listfix/view/support/ProgressAdapter.java @@ -48,18 +48,18 @@ public void reportProgress(int progress, T state) private IProgressObserver _observer; - public int getCompleted() + public long getCompleted() { return _completed; } - public void setCompleted(int completed) + public void setCompleted(long completed) { _completed = completed; refreshPercentComplete(); } - private int _completed; + private long _completed; public void stepCompleted() { @@ -67,18 +67,18 @@ public void stepCompleted() refreshPercentComplete(); } - public void stepCompleted(int done) + public void stepCompleted(long done) { _completed += done; refreshPercentComplete(); } - public int getTotal() + public long getTotal() { return _total; } - public void setTotal(int total) + public void setTotal(long total) { boolean report = isValid() && _percentComplete != 0 && total != 0; _total = total; @@ -88,18 +88,18 @@ public void setTotal(int total) reportProgress(_percentComplete); } - private int _total; + private long _total; private void refreshPercentComplete() { if (isValid()) { - int pct = _completed * 100 / _total; + double pct = Math.round(((double)_completed * 100.0) / (double)_total); if (pct != _percentComplete) { - _percentComplete = pct; - reportProgress(pct); + _percentComplete = (int) pct; + reportProgress(_percentComplete); } } }