diff --git a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java index 47fca41ba..cd1a489d8 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java @@ -17,8 +17,8 @@ public class ConverterTest extends AndroidTestCase { public void testGetDurationStringShort() throws Exception { String expected = "13:05"; - int input = 47110000; - assertEquals(expected, Converter.getDurationStringShort(input)); + assertEquals(expected, Converter.getDurationStringShort(47110000, true)); + assertEquals(expected, Converter.getDurationStringShort(785000, false)); } public void testDurationStringLongToMs() throws Exception { @@ -29,7 +29,7 @@ public class ConverterTest extends AndroidTestCase { public void testDurationStringShortToMs() throws Exception { String input = "8:30"; - long expected = 30600000; - assertEquals(expected, Converter.durationStringShortToMs(input)); + assertEquals(30600000, Converter.durationStringShortToMs(input, true)); + assertEquals(510000, Converter.durationStringShortToMs(input, false)); } } diff --git a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java index 7e535e12c..4bef14cd9 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java @@ -30,12 +30,12 @@ public class TimelineTest extends InstrumentationTestCase { context = getInstrumentation().getTargetContext(); } - private Playable newTestPlayable(List chapters, String shownotes) { + private Playable newTestPlayable(List chapters, String shownotes, int duration) { FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), FeedItem.PLAYED, null); item.setChapters(chapters); item.setContentEncoded(shownotes); FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3"); - media.setDuration(Integer.MAX_VALUE); + media.setDuration(duration); item.setMedia(media); return media; } @@ -44,7 +44,17 @@ public class TimelineTest extends InstrumentationTestCase { final String timeStr = "10:11:12"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11 + 12 * 1000; - Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

"); + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", Integer.MAX_VALUE); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeHHMMSSMoreThen24HoursNoChapters() throws Exception { + final String timeStr = "25:00:00"; + final long time = 25 * 60 * 60 * 1000; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", Integer.MAX_VALUE); Timeline t = new Timeline(context, p); String res = t.processShownotes(true); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); @@ -54,17 +64,67 @@ public class TimelineTest extends InstrumentationTestCase { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

"); + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", Integer.MAX_VALUE); Timeline t = new Timeline(context, p); String res = t.processShownotes(true); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } + public void testProcessShownotesAddTimecodeMMSSNoChapters() throws Exception { + final String timeStr = "10:11"; + final long time = 10 * 60 * 1000 + 11 * 1000; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", 11 * 60 * 1000); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeHMMSSNoChapters() throws Exception { + final String timeStr = "2:11:12"; + final long time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", Integer.MAX_VALUE); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeMSSNoChapters() throws Exception { + final String timeStr = "1:12"; + final long time = 60 * 1000 + 12 * 1000; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStr + " here.

", 2 * 60 * 1000); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + public void testProcessShownotesAddTimecodeMultipleFormatsNoChapters() throws Exception { + final String[] timeStrings = new String[]{ "10:12", "1:10:12" }; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!

", 2 * 60 * 60 * 1000); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000 }, timeStrings); + } + + public void testProcessShownotesAddTimecodeMultipleShortFormatNoChapters() throws Exception { + + // One of these timecodes fits as HH:MM and one does not so both should be parsed as MM:SS. + final String[] timeStrings = new String[]{ "10:12", "2:12" }; + + Playable p = newTestPlayable(null, "

Some test text with a timecode " + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!

", 3 * 60 * 60 * 1000); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000 }, timeStrings); + } + public void testProcessShownotesAddTimecodeParentheses() throws Exception { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "

Some test text with a timecode (" + timeStr + ") here.

"); + Playable p = newTestPlayable(null, "

Some test text with a timecode (" + timeStr + ") here.

", Integer.MAX_VALUE); Timeline t = new Timeline(context, p); String res = t.processShownotes(true); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); @@ -74,7 +134,7 @@ public class TimelineTest extends InstrumentationTestCase { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "

Some test text with a timecode [" + timeStr + "] here.

"); + Playable p = newTestPlayable(null, "

Some test text with a timecode [" + timeStr + "] here.

", Integer.MAX_VALUE); Timeline t = new Timeline(context, p); String res = t.processShownotes(true); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); @@ -84,12 +144,27 @@ public class TimelineTest extends InstrumentationTestCase { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "

Some test text with a timecode <" + timeStr + "> here.

"); + Playable p = newTestPlayable(null, "

Some test text with a timecode <" + timeStr + "> here.

", Integer.MAX_VALUE); Timeline t = new Timeline(context, p); String res = t.processShownotes(true); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } + public void testProcessShownotesAndInvalidTimecode() throws Exception { + final String[] timeStrs = new String[] {"2:1", "0:0", "000", "00", "00:000"}; + + StringBuilder shownotes = new StringBuilder("

Some test text with timecodes "); + for (String timeStr : timeStrs) { + shownotes.append(timeStr).append(" "); + } + shownotes.append("here.

"); + + Playable p = newTestPlayable(null, shownotes.toString(), Integer.MAX_VALUE); + Timeline t = new Timeline(context, p); + String res = t.processShownotes(true); + checkLinkCorrect(res, new long[0], new String[0]); + } + private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) { assertNotNull(res); Document d = Jsoup.parse(res); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java index 6966667bf..6ecca941a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -74,13 +74,14 @@ public final class Converter { return String.format(Locale.getDefault(), "%02d:%02d:%02d", h, m, s); } - /** Converts milliseconds to a string containing hours and minutes */ - public static String getDurationStringShort(int duration) { - int h = duration / HOURS_MIL; - int rest = duration - h * HOURS_MIL; - int m = rest / MINUTES_MIL; - - return String.format(Locale.getDefault(), "%02d:%02d", h, m); + /** Converts milliseconds to a string containing hours and minutes or minutes and seconds*/ + public static String getDurationStringShort(int duration, boolean durationIsInHours) { + int firstPartBase = durationIsInHours ? HOURS_MIL : MINUTES_MIL; + int firstPart = duration / firstPartBase; + int leftoverFromFirstPart = duration - firstPart * firstPartBase; + int secondPart = leftoverFromFirstPart / (durationIsInHours ? MINUTES_MIL : SECONDS_MIL); + + return String.format(Locale.getDefault(), "%02d:%02d", firstPart, secondPart); } /** Converts long duration string (HH:MM:SS) to milliseconds. */ @@ -94,14 +95,20 @@ public final class Converter { Integer.parseInt(parts[2]) * 1000; } - /** Converts short duration string (HH:MM) to milliseconds. */ - public static int durationStringShortToMs(String input) { + /** + * Converts short duration string (XX:YY) to milliseconds. If durationIsInHours is true then the + * format is HH:MM, otherwise it's MM:SS. + * */ + public static int durationStringShortToMs(String input, boolean durationIsInHours) { String[] parts = input.split(":"); if (parts.length != 2) { return 0; } - return Integer.parseInt(parts[0]) * 3600 * 1000 + - Integer.parseInt(parts[1]) * 1000 * 60; + + int modifier = durationIsInHours ? 60 : 1; + + return Integer.parseInt(parts[0]) * 60 * 1000 * modifier+ + Integer.parseInt(parts[1]) * 1000 * modifier; } /** Converts milliseconds to a localized string containing hours and minutes */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java index 34cfe6d05..56550bb06 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -7,6 +7,7 @@ import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.TypedValue; import org.jsoup.Jsoup; @@ -14,6 +15,7 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import java.util.ArrayList; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -68,7 +70,7 @@ public class Timeline { private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))"); private static final String TIMECODE_LINK = "%s"; - private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b"); + private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b((\\d+):)?(\\d+):(\\d{2})\\b"); private static final Pattern LINE_BREAK_REGEX = Pattern.compile("
"); @@ -127,35 +129,12 @@ public class Timeline { // apply timecode links if (addTimecodes) { - Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); - Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); - for (Element element : elementsWithTimeCodes) { - Matcher matcherLong = TIMECODE_REGEX.matcher(element.html()); - StringBuffer buffer = new StringBuffer(); - while (matcherLong.find()) { - String h = matcherLong.group(1); - String group = matcherLong.group(0); - int time = (h != null) ? Converter.durationStringLongToMs(group) : - Converter.durationStringShortToMs(group); - - String rep; - if (playable == null || playable.getDuration() > time) { - rep = String.format(Locale.getDefault(), TIMECODE_LINK, time, group); - } else { - rep = group; - } - matcherLong.appendReplacement(buffer, rep); - } - matcherLong.appendTail(buffer); - - element.html(buffer.toString()); - } + addTimecodes(document, playable); } return document.toString(); } - /** * Returns true if the given link is a timecode link. */ @@ -186,4 +165,69 @@ public class Timeline { public void setShownotesProvider(@NonNull ShownotesProvider shownotesProvider) { this.shownotesProvider = shownotesProvider; } + + private void addTimecodes(Document document, final Playable playable) { + Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + + if (elementsWithTimeCodes.size() == 0) { + // No elements with timecodes + return; + } + + int playableDuration = playable == null ? Integer.MAX_VALUE : playable.getDuration(); + boolean useHourFormat = true; + + if (playableDuration != Integer.MAX_VALUE) { + + // We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do + // so we will parse all the short timecodes and see if they fit in the duration. If one + // does not we will use MM:SS, otherwise all will be parsed as HH:MM. + for (Element element : elementsWithTimeCodes) { + Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); + while (matcherForElement.find()) { + + // We only want short timecodes right now. + if (matcherForElement.group(1) == null) { + int time = Converter.durationStringShortToMs(matcherForElement.group(0), true); + + // If the parsed timecode is greater then the duration then we know we need to + // use the minute format so we are done. + if (time > playableDuration) { + useHourFormat = false; + break; + } + } + } + + if (!useHourFormat) { + break; + } + } + } + + for (Element element : elementsWithTimeCodes) { + + Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); + StringBuffer buffer = new StringBuffer(); + + while (matcherForElement.find()) { + String group = matcherForElement.group(0); + + int time = matcherForElement.group(1) != null + ? Converter.durationStringLongToMs(group) + : Converter.durationStringShortToMs(group, useHourFormat); + + String replacementText = group; + if (time < playableDuration) { + replacementText = String.format(Locale.getDefault(), TIMECODE_LINK, time, group); + } + + matcherForElement.appendReplacement(buffer, replacementText); + } + + matcherForElement.appendTail(buffer); + element.html(buffer.toString()); + } + } }