Merge pull request #3032 from shortspider/3031-TimecodeRegex

Change Timecode Regex
This commit is contained in:
H. Lehmann 2019-03-03 23:26:32 +01:00 committed by GitHub
commit 1593a06077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 47 deletions

View File

@ -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));
}
}

View File

@ -30,12 +30,12 @@ public class TimelineTest extends InstrumentationTestCase {
context = getInstrumentation().getTargetContext();
}
private Playable newTestPlayable(List<Chapter> chapters, String shownotes) {
private Playable newTestPlayable(List<Chapter> 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, "<p> Some test text with a timecode " + timeStr + " here.</p>");
Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStr + " here.</p>");
Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStr + " here.</p>", 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, "<p> Some test text with a timecode " + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 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, "<p> Some test text with a timecode " + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 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, "<p> Some test text with a timecode (" + timeStr + ") here.</p>");
Playable p = newTestPlayable(null, "<p> Some test text with a timecode (" + timeStr + ") here.</p>", 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, "<p> Some test text with a timecode [" + timeStr + "] here.</p>");
Playable p = newTestPlayable(null, "<p> Some test text with a timecode [" + timeStr + "] here.</p>", 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, "<p> Some test text with a timecode <" + timeStr + "> here.</p>");
Playable p = newTestPlayable(null, "<p> Some test text with a timecode <" + timeStr + "> here.</p>", 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("<p> Some test text with timecodes ");
for (String timeStr : timeStrs) {
shownotes.append(timeStr).append(" ");
}
shownotes.append("here.</p>");
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);

View File

@ -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 */

View File

@ -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 = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>";
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("<br */?>");
@ -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());
}
}
}