2014-04-19 02:30:29 -04:00
|
|
|
|
package net.filebot.format;
|
2014-04-13 15:37:11 -04:00
|
|
|
|
|
2016-10-10 09:29:55 -04:00
|
|
|
|
import static java.util.Arrays.*;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
import static java.util.regex.Pattern.*;
|
2016-10-30 19:58:27 -04:00
|
|
|
|
import static java.util.stream.Collectors.*;
|
2016-09-20 01:56:10 -04:00
|
|
|
|
import static net.filebot.MediaTypes.*;
|
2016-10-30 19:19:11 -04:00
|
|
|
|
import static net.filebot.WebServices.*;
|
2016-03-28 06:13:55 -04:00
|
|
|
|
import static net.filebot.format.ExpressionFormatFunctions.*;
|
2016-09-20 01:56:10 -04:00
|
|
|
|
import static net.filebot.media.MediaDetection.*;
|
2019-02-23 05:20:31 -05:00
|
|
|
|
import static net.filebot.similarity.Normalization.*;
|
2016-09-26 06:53:20 -04:00
|
|
|
|
import static net.filebot.util.RegularExpressions.*;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
import java.io.File;
|
2017-02-25 15:28:06 -05:00
|
|
|
|
import java.io.IOException;
|
2014-04-19 03:31:24 -04:00
|
|
|
|
import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.attribute.BasicFileAttributeView;
|
|
|
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
2017-06-20 21:18:04 -04:00
|
|
|
|
import java.text.Normalizer;
|
2018-12-02 15:19:54 -05:00
|
|
|
|
import java.text.ParseException;
|
|
|
|
|
import java.text.SimpleDateFormat;
|
2018-04-24 03:15:59 -04:00
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.time.LocalDateTime;
|
2017-03-27 07:39:30 -04:00
|
|
|
|
import java.time.LocalTime;
|
2018-04-24 03:15:59 -04:00
|
|
|
|
import java.time.ZoneOffset;
|
2017-02-26 15:48:39 -05:00
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.time.temporal.Temporal;
|
2017-03-27 07:39:30 -04:00
|
|
|
|
import java.time.temporal.TemporalAmount;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
import java.util.ArrayList;
|
2016-10-10 09:29:55 -04:00
|
|
|
|
import java.util.Collection;
|
2018-12-02 15:19:54 -05:00
|
|
|
|
import java.util.Date;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
import java.util.List;
|
2015-03-30 23:58:33 -04:00
|
|
|
|
import java.util.Locale;
|
2016-03-09 00:58:52 -05:00
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Map.Entry;
|
|
|
|
|
import java.util.Objects;
|
2016-11-21 20:59:42 -05:00
|
|
|
|
import java.util.TreeMap;
|
2016-10-11 16:56:56 -04:00
|
|
|
|
import java.util.function.Function;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
import java.util.regex.Matcher;
|
2016-03-09 00:58:52 -05:00
|
|
|
|
import java.util.regex.Pattern;
|
2016-04-06 03:16:30 -04:00
|
|
|
|
import java.util.stream.IntStream;
|
2016-10-10 09:29:55 -04:00
|
|
|
|
import java.util.stream.Stream;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
2016-12-22 07:32:44 -05:00
|
|
|
|
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
|
2019-02-23 05:20:31 -05:00
|
|
|
|
import org.codehaus.groovy.runtime.StringGroovyMethods;
|
2016-03-09 00:58:52 -05:00
|
|
|
|
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
import com.ibm.icu.text.Transliterator;
|
|
|
|
|
|
2016-03-26 03:44:03 -04:00
|
|
|
|
import groovy.lang.Closure;
|
|
|
|
|
import net.filebot.util.FileUtilities;
|
2016-10-30 19:19:11 -04:00
|
|
|
|
import net.filebot.web.Episode;
|
|
|
|
|
import net.filebot.web.EpisodeInfo;
|
2018-05-28 04:07:16 -04:00
|
|
|
|
import net.filebot.web.Movie;
|
2016-10-30 19:58:27 -04:00
|
|
|
|
import net.filebot.web.Person;
|
|
|
|
|
import net.filebot.web.SeriesInfo;
|
2016-03-26 03:44:03 -04:00
|
|
|
|
|
2014-04-13 15:37:11 -04:00
|
|
|
|
public class ExpressionFormatMethods {
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Convert all characters to lower case/
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Firelfy" ➔ "firefly"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2014-04-13 15:37:11 -04:00
|
|
|
|
public static String lower(String self) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return self.toLowerCase();
|
2014-04-13 15:37:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
/**
|
|
|
|
|
* Convert all characters to upper case.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Firelfy" ➔ "FIREFLY"
|
|
|
|
|
*/
|
2014-04-13 15:37:11 -04:00
|
|
|
|
public static String upper(String self) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return self.toUpperCase();
|
2014-04-13 15:37:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Pad to length using the given character.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "1" ➔ "01"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2014-04-13 15:37:11 -04:00
|
|
|
|
public static String pad(String self, int length) {
|
|
|
|
|
return pad(self, length, "0");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String pad(Number self, int length) {
|
2014-04-15 08:23:58 -04:00
|
|
|
|
return pad(self.toString(), length, "0");
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
public static String pad(CharSequence self, int length, CharSequence padding) {
|
|
|
|
|
return StringGroovyMethods.padLeft(self, length, padding);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Round decimal number to precision.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "3.14" ➔ "3.1"
|
|
|
|
|
*/
|
2017-04-09 04:01:45 -04:00
|
|
|
|
public static double round(Number self, int precision) {
|
|
|
|
|
return DefaultGroovyMethods.round(self.doubleValue(), precision);
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Match pattern and return or unwind if pattern cannot be found.
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2014-11-18 12:14:18 -05:00
|
|
|
|
public static String match(String self, String pattern) throws Exception {
|
2014-04-15 08:23:58 -04:00
|
|
|
|
return match(self, pattern, -1);
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-18 12:14:18 -05:00
|
|
|
|
public static String match(String self, String pattern, int matchGroup) throws Exception {
|
2014-04-27 23:55:42 -04:00
|
|
|
|
Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
if (matcher.find()) {
|
2016-04-06 03:16:30 -04:00
|
|
|
|
return firstCapturingGroup(matcher, matchGroup);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
} else {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
throw new Exception("Pattern not found: " + self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Match all occurrences of the given pattern or unwind if pattern cannot be found.
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2014-11-18 12:14:18 -05:00
|
|
|
|
public static List<String> matchAll(String self, String pattern) throws Exception {
|
2016-04-02 23:30:29 -04:00
|
|
|
|
return matchAll(self, pattern, -1);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2014-11-18 12:14:18 -05:00
|
|
|
|
public static List<String> matchAll(String self, String pattern, int matchGroup) throws Exception {
|
2014-04-15 08:23:58 -04:00
|
|
|
|
List<String> matches = new ArrayList<String>();
|
2014-04-27 23:55:42 -04:00
|
|
|
|
Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
while (matcher.find()) {
|
2016-04-06 03:16:30 -04:00
|
|
|
|
matches.add(firstCapturingGroup(matcher, matchGroup));
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2016-04-06 03:16:30 -04:00
|
|
|
|
if (matches.isEmpty()) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
throw new Exception("Pattern not found: " + self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
2016-04-06 03:16:30 -04:00
|
|
|
|
return matches;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
private static String firstCapturingGroup(Matcher self, int matchGroup) throws Exception {
|
2016-04-06 03:16:30 -04:00
|
|
|
|
int g = matchGroup < 0 ? self.groupCount() > 0 ? 1 : 0 : matchGroup;
|
|
|
|
|
|
|
|
|
|
// return the entire match
|
|
|
|
|
if (g == 0) {
|
|
|
|
|
return self.group();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// otherwise find first non-empty capturing group
|
|
|
|
|
return IntStream.rangeClosed(g, self.groupCount()).mapToObj(self::group).filter(Objects::nonNull).map(String::trim).filter(s -> s.length() > 0).findFirst().orElseThrow(() -> {
|
|
|
|
|
return new Exception(String.format("Capturing group %d not found", g));
|
|
|
|
|
});
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-06 23:09:03 -05:00
|
|
|
|
public static String replaceAll(String self, String pattern) {
|
|
|
|
|
return compile(pattern).matcher(self).replaceAll("");
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
public static String removeAll(String self, String pattern) {
|
2014-04-27 23:55:42 -04:00
|
|
|
|
return compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self).replaceAll("").trim();
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
/**
|
|
|
|
|
* Strip characters that aren't allowed on Windows from the given filename.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Sissi: The Young Empress" ➔ "Sissi The Young Empress"
|
|
|
|
|
*/
|
2016-01-08 08:28:46 -05:00
|
|
|
|
public static String removeIllegalCharacters(String self) {
|
2019-02-23 05:20:31 -05:00
|
|
|
|
return FileUtilities.validateFileName(normalizeQuotationMarks(self));
|
2016-01-08 08:28:46 -05:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Replace all spaces.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "Doctor Who" ➔ "Doctor_Who"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String space(String self, String replacement) {
|
2019-02-23 05:20:31 -05:00
|
|
|
|
return normalizeSpace(self, replacement);
|
2014-04-13 15:37:11 -04:00
|
|
|
|
}
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
2016-03-30 12:43:45 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Replace all colons.
|
2016-03-30 12:43:45 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "Sissi: The Young Empress" ➔ "Sissi - The Young Empress"
|
2016-03-30 12:43:45 -04:00
|
|
|
|
*/
|
2016-09-26 06:53:20 -04:00
|
|
|
|
public static String colon(String self, String colon) {
|
|
|
|
|
return COLON.matcher(self).replaceAll(colon);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String colon(String self, String ratio, String colon) {
|
|
|
|
|
return COLON.matcher(RATIO.matcher(self).replaceAll(ratio)).replaceAll(colon);
|
2016-03-30 12:43:45 -04:00
|
|
|
|
}
|
|
|
|
|
|
2016-05-04 11:27:49 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Replace all slashes.
|
2016-05-04 11:27:49 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "V_MPEG4/ISO/AVC" ➔ "V_MPEG4.ISO.AVC"
|
2016-05-04 11:27:49 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String slash(String self, String replacement) {
|
2016-09-26 06:53:20 -04:00
|
|
|
|
return SLASH.matcher(self).replaceAll(replacement);
|
2016-05-04 11:27:49 -04:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Convert all initial characters to upper case.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "The Day a new Demon was born" ➔ "The Day A New Demon Was Born"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String upperInitial(String self) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return replaceHeadTail(self, String::toUpperCase, String::toString);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Convert all trailing characters to lower case.
|
2016-10-11 16:56:56 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "Gundam SEED" ➔ "Gundam Seed"
|
2016-10-11 16:56:56 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String lowerTrail(String self) {
|
|
|
|
|
return replaceHeadTail(self, String::toString, String::toLowerCase);
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
private static String replaceHeadTail(String self, Function<String, String> head, Function<String, String> tail) {
|
2016-12-14 10:58:54 -05:00
|
|
|
|
Matcher matcher = compile("\\b(['`´]|\\p{Alnum})(\\p{Alnum}*)\\b", UNICODE_CHARACTER_CLASS).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
StringBuffer buffer = new StringBuffer();
|
|
|
|
|
while (matcher.find()) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
matcher.appendReplacement(buffer, head.apply(matcher.group(1)) + tail.apply(matcher.group(2)));
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
2016-12-14 10:58:54 -05:00
|
|
|
|
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return matcher.appendTail(buffer).toString();
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
/**
|
|
|
|
|
* Convert to sort name.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "The Walking Dead" ➔ "Walking Dead"
|
|
|
|
|
*/
|
2014-04-15 08:23:58 -04:00
|
|
|
|
public static String sortName(String self) {
|
2015-11-04 04:35:40 -05:00
|
|
|
|
return sortName(self, "$2");
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String sortName(String self, String replacement) {
|
2016-04-01 13:48:01 -04:00
|
|
|
|
return compile("^(The|A|An)\\s(.+)", CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self).replaceFirst(replacement).trim();
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2016-01-26 06:29:16 -05:00
|
|
|
|
public static String sortInitial(String self) {
|
|
|
|
|
// use primary initial, ignore The XY, A XY, etc
|
2016-10-11 16:56:56 -04:00
|
|
|
|
char c = ascii(sortName(self)).charAt(0);
|
2016-01-26 06:29:16 -05:00
|
|
|
|
|
|
|
|
|
if (Character.isDigit(c)) {
|
|
|
|
|
return "0-9";
|
2016-10-11 16:56:56 -04:00
|
|
|
|
} else if (Character.isLetter(c)) {
|
|
|
|
|
return String.valueOf(c).toUpperCase();
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
2016-01-26 06:29:16 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Reduce first name to initials.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "James Cameron" ➔ "J. Cameron"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2019-02-23 05:20:31 -05:00
|
|
|
|
public static String initialName(String self) {
|
|
|
|
|
String[] words = SPACE.split(self);
|
|
|
|
|
for (int i = 0; i < words.length - 1; i++) {
|
|
|
|
|
words[i] = words[i].charAt(0) + ".";
|
|
|
|
|
}
|
|
|
|
|
return String.join(" ", words);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2015-06-08 13:15:44 -04:00
|
|
|
|
public static String truncate(String self, int limit) {
|
|
|
|
|
if (limit >= self.length())
|
|
|
|
|
return self;
|
|
|
|
|
|
|
|
|
|
return self.substring(0, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String truncate(String self, int hardLimit, String nonWordPattern) {
|
|
|
|
|
if (hardLimit >= self.length())
|
|
|
|
|
return self;
|
|
|
|
|
|
|
|
|
|
int softLimit = 0;
|
|
|
|
|
Matcher matcher = compile(nonWordPattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
|
|
|
|
|
while (matcher.find()) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
if (matcher.start() > hardLimit) {
|
2015-06-08 13:15:44 -04:00
|
|
|
|
break;
|
2016-10-11 16:56:56 -04:00
|
|
|
|
}
|
2015-06-08 13:15:44 -04:00
|
|
|
|
softLimit = matcher.start();
|
|
|
|
|
}
|
|
|
|
|
return truncate(self, softLimit);
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Match substring before the given pattern or return the original value.
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String before(String self, String pattern) {
|
2014-04-27 23:55:42 -04:00
|
|
|
|
Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
// pattern was found, return leading substring, else return original value
|
|
|
|
|
return matcher.find() ? self.substring(0, matcher.start()).trim() : self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Match substring before the given pattern or return the original value.
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String after(String self, String pattern) {
|
2014-04-27 23:55:42 -04:00
|
|
|
|
Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
// pattern was found, return trailing substring, else return original value
|
|
|
|
|
return matcher.find() ? self.substring(matcher.end(), self.length()).trim() : self;
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-27 23:55:42 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Find match in case-insensitive mode.
|
2014-04-27 23:55:42 -04:00
|
|
|
|
*/
|
2016-11-06 02:12:22 -05:00
|
|
|
|
public static boolean findMatch(String self, String pattern) {
|
|
|
|
|
if (pattern == null || pattern.isEmpty())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self).find();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Find match in between word boundaries in case-insensitive mode.
|
2016-11-06 02:12:22 -05:00
|
|
|
|
*/
|
|
|
|
|
public static boolean findWordMatch(String self, String pattern) {
|
|
|
|
|
if (pattern == null || pattern.isEmpty())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return findMatch(self, "\\b(" + pattern + ")\\b");
|
2014-04-27 23:55:42 -04:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Replace trailing parenthesis.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "The IT Crowd (UK)" ➔ "The IT Crowd"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String replaceTrailingBrackets(String self) {
|
|
|
|
|
return replaceTrailingBrackets(self, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String replaceTrailingBrackets(String self, String replacement) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return compile("\\s*[(]([^)]*)[)]$", UNICODE_CHARACTER_CLASS).matcher(self).replaceAll(replacement);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Replace trailing part number.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "Today Is the Day (1)" ➔ "Today Is the Day, Part 1"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String replacePart(String self) {
|
|
|
|
|
return replacePart(self, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String replacePart(String self, String replacement) {
|
|
|
|
|
// handle '(n)', '(Part n)' and ': Part n' like syntax
|
2018-02-05 01:55:33 -05:00
|
|
|
|
String[] patterns = new String[] { "\\s*[(](\\w{1,3})[)]$", "\\W+Part (\\w+)\\W*$" };
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
for (String pattern : patterns) {
|
2016-04-01 13:48:01 -04:00
|
|
|
|
Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
if (matcher.find()) {
|
|
|
|
|
return matcher.replaceAll(replacement).trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no pattern matches, nothing to replace
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-21 20:59:42 -05:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Convert to acronym.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Deep Space 9" ➔ "DS9"
|
|
|
|
|
*/
|
|
|
|
|
public static String acronym(String self) {
|
|
|
|
|
return compile("\\s|\\B\\p{Alnum}+", UNICODE_CHARACTER_CLASS).matcher(space(self, " ")).replaceAll("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace numbers 1..12 with Roman numerals.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Star Wars: Episode 4" ➔ "Star Wars: Episode IV"
|
2016-11-21 20:59:42 -05:00
|
|
|
|
*/
|
|
|
|
|
public static String roman(String self) {
|
|
|
|
|
TreeMap<Integer, String> numerals = new TreeMap<Integer, String>();
|
|
|
|
|
numerals.put(10, "X");
|
|
|
|
|
numerals.put(9, "IX");
|
|
|
|
|
numerals.put(5, "V");
|
|
|
|
|
numerals.put(4, "IV");
|
2016-11-22 05:49:31 -05:00
|
|
|
|
numerals.put(1, "I");
|
2016-11-21 20:59:42 -05:00
|
|
|
|
|
|
|
|
|
StringBuffer s = new StringBuffer();
|
|
|
|
|
Matcher m = compile("\\b\\d+\\b").matcher(self);
|
|
|
|
|
while (m.find()) {
|
|
|
|
|
int n = Integer.parseInt(m.group());
|
|
|
|
|
m.appendReplacement(s, n >= 1 && n <= 12 ? roman(n, numerals) : m.group());
|
|
|
|
|
}
|
|
|
|
|
return m.appendTail(s).toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String roman(Integer n, TreeMap<Integer, String> numerals) {
|
|
|
|
|
int l = numerals.floorKey(n);
|
|
|
|
|
if (n == l) {
|
|
|
|
|
return numerals.get(n);
|
|
|
|
|
}
|
|
|
|
|
return numerals.get(l) + roman(n - l, numerals);
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Apply any ICU script transliteration.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "中国" ➔ "zhōng guó"
|
|
|
|
|
*
|
2014-04-15 08:23:58 -04:00
|
|
|
|
* @see http://userguide.icu-project.org/transforms/general
|
|
|
|
|
*/
|
|
|
|
|
public static String transliterate(String self, String transformIdentifier) {
|
|
|
|
|
return Transliterator.getInstance(transformIdentifier).transform(self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Convert Unicode characters to ASCII.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* e.g. "カタカナ" ➔ "katakana"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
|
|
|
|
public static String ascii(String self) {
|
|
|
|
|
return ascii(self, " ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String ascii(String self, String fallback) {
|
2016-10-11 16:56:56 -04:00
|
|
|
|
return Transliterator.getInstance("Any-Latin;Latin-ASCII;[:Diacritic:]remove").transform(asciiQuotes(self)).replaceAll("\\P{ASCII}+", fallback).trim();
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2015-06-04 13:17:30 -04:00
|
|
|
|
public static String asciiQuotes(String self) {
|
2019-02-23 05:20:31 -05:00
|
|
|
|
return normalizeQuotationMarks(self);
|
2015-06-04 13:17:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-20 21:18:04 -04:00
|
|
|
|
public static boolean isLatin(String self) {
|
2017-06-20 21:19:14 -04:00
|
|
|
|
return Normalizer.normalize(self, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}", "").matches("\\p{InBasicLatin}+");
|
2017-06-20 21:18:04 -04:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 08:23:58 -04:00
|
|
|
|
/**
|
2019-02-26 02:02:04 -05:00
|
|
|
|
* Apply replacement mappings.
|
2015-07-25 18:47:19 -04:00
|
|
|
|
*
|
2016-03-09 00:58:52 -05:00
|
|
|
|
* e.g. replace(ä:'ae', ö:'oe', ü:'ue')
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2019-02-26 02:02:04 -05:00
|
|
|
|
public static String replace(String self, Map<?, ?> replacer) {
|
2014-04-15 08:23:58 -04:00
|
|
|
|
// the first two parameters are required, the rest of the parameter sequence is optional
|
2019-02-26 02:02:04 -05:00
|
|
|
|
for (Entry<?, ?> it : replacer.entrySet()) {
|
2016-03-09 00:58:52 -05:00
|
|
|
|
if (it.getKey() instanceof Pattern) {
|
|
|
|
|
self = ((Pattern) it.getKey()).matcher(self).replaceAll(it.getValue().toString());
|
|
|
|
|
} else {
|
|
|
|
|
self = self.replace(it.getKey().toString(), it.getValue().toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return self;
|
|
|
|
|
}
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
2019-02-26 02:02:04 -05:00
|
|
|
|
/**
|
|
|
|
|
* Find matching pattern and return mapped value.
|
|
|
|
|
*
|
|
|
|
|
* e.g. az.match('[a-f]': '/volume1', '[g-x]': '/volume2') ?: '/volume3'
|
|
|
|
|
*/
|
|
|
|
|
public static Object match(String self, Map<?, ?> matcher) {
|
|
|
|
|
// the first two parameters are required, the rest of the parameter sequence is optional
|
|
|
|
|
for (Entry<?, ?> it : matcher.entrySet()) {
|
|
|
|
|
Pattern p = it.getKey() instanceof Pattern ? (Pattern) it.getKey() : Pattern.compile(it.getKey().toString(), CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE);
|
|
|
|
|
if (p.matcher(self).find()) {
|
|
|
|
|
return it.getValue();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-10 09:29:55 -04:00
|
|
|
|
public static String joining(Collection<?> self, String delimiter) throws Exception {
|
2016-10-08 11:13:07 -04:00
|
|
|
|
String[] list = self.stream().filter(Objects::nonNull).map(Objects::toString).filter(s -> !s.isEmpty()).toArray(String[]::new);
|
2016-10-10 09:29:55 -04:00
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
return String.join(delimiter, list);
|
2016-10-08 11:13:07 -04:00
|
|
|
|
}
|
2016-10-10 09:29:55 -04:00
|
|
|
|
|
|
|
|
|
throw new Exception("Collection did not yield any values: " + self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String joiningDistinct(Collection<?> self, String delimiter, Closure<?>... mapper) throws Exception {
|
|
|
|
|
Stream<?> stream = self.stream().filter(Objects::nonNull);
|
|
|
|
|
|
|
|
|
|
// apply custom mappers if any
|
|
|
|
|
if (mapper.length > 0) {
|
|
|
|
|
stream = stream.flatMap(v -> stream(mapper).map(m -> m.call(v)).filter(Objects::nonNull));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sort unique
|
2016-10-10 09:31:39 -04:00
|
|
|
|
String[] list = stream.map(Objects::toString).filter(s -> !s.isEmpty()).distinct().sorted().toArray(String[]::new);
|
2016-10-10 09:29:55 -04:00
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
return String.join(delimiter, list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Exception("Collection did not yield any values: " + self);
|
2016-10-08 11:13:07 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
public static List<?> bounds(Iterable<?> self) {
|
|
|
|
|
return Stream.of(DefaultGroovyMethods.min(self), DefaultGroovyMethods.max(self)).filter(Objects::nonNull).distinct().collect(toList());
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-09 00:58:52 -05:00
|
|
|
|
/**
|
|
|
|
|
* Unwind if an object does not satisfy the given predicate
|
|
|
|
|
*
|
|
|
|
|
* e.g. (0..9)*.check{it < 10}.sum()
|
|
|
|
|
*/
|
|
|
|
|
public static Object check(Object self, Closure<?> c) throws Exception {
|
|
|
|
|
if (DefaultTypeTransformation.castToBoolean(c.call(self))) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
2016-10-10 09:29:55 -04:00
|
|
|
|
|
|
|
|
|
throw new Exception("Object failed check: " + self);
|
2014-04-15 08:23:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Add values to the filename.
|
|
|
|
|
*
|
|
|
|
|
* e.g. "Avatar (2009).mp4" ➔ "Avatar (2009) [720p].mp4"
|
2014-04-15 08:23:58 -04:00
|
|
|
|
*/
|
2016-09-20 01:56:10 -04:00
|
|
|
|
public static File derive(File self, Object tag, Object... tagN) {
|
|
|
|
|
// e.g. plex.derive{" by $director"}{" [$vc, $ac]"}
|
|
|
|
|
String name = FileUtilities.getName(self);
|
|
|
|
|
String extension = self.getName().substring(name.length());
|
|
|
|
|
|
|
|
|
|
// e.g. Avatar (2009).eng.srt => Avatar (2009) 1080p.eng.srt
|
|
|
|
|
if (SUBTITLE_FILES.accept(self)) {
|
|
|
|
|
Matcher nameMatcher = releaseInfo.getSubtitleLanguageTagPattern().matcher(name);
|
|
|
|
|
if (nameMatcher.find()) {
|
|
|
|
|
extension = name.substring(nameMatcher.start() - 1) + extension;
|
|
|
|
|
name = name.substring(0, nameMatcher.start() - 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-24 14:34:58 -04:00
|
|
|
|
return new File(self.getParentFile(), concat(null, name, slash(concat(null, tag, null, tagN), ""), extension));
|
2016-09-20 01:56:10 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
/**
|
|
|
|
|
* File utilities
|
|
|
|
|
*/
|
2014-04-15 08:23:58 -04:00
|
|
|
|
|
|
|
|
|
public static long getDiskSpace(File self) {
|
|
|
|
|
List<File> list = FileUtilities.listPath(self);
|
|
|
|
|
for (int i = list.size() - 1; i >= 0; i--) {
|
|
|
|
|
if (list.get(i).exists()) {
|
|
|
|
|
long usableSpace = list.get(i).getUsableSpace();
|
|
|
|
|
if (usableSpace > 0) {
|
|
|
|
|
return usableSpace;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-25 15:28:06 -05:00
|
|
|
|
public static long getCreationDate(File self) throws IOException {
|
|
|
|
|
BasicFileAttributes attr = Files.getFileAttributeView(self.toPath(), BasicFileAttributeView.class).readAttributes();
|
|
|
|
|
long creationDate = attr.creationTime().toMillis();
|
|
|
|
|
if (creationDate > 0) {
|
|
|
|
|
return creationDate;
|
2014-04-19 03:31:24 -04:00
|
|
|
|
}
|
2017-02-25 15:28:06 -05:00
|
|
|
|
return attr.lastModifiedTime().toMillis();
|
2014-04-19 03:31:24 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
public static File getRoot(File self) {
|
|
|
|
|
return FileUtilities.listPath(self).get(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File getTail(File self) {
|
|
|
|
|
return FileUtilities.getRelativePathTail(self, FileUtilities.listPath(self).size() - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static List<File> listPath(File self) {
|
|
|
|
|
return FileUtilities.listPath(self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static List<File> listPath(File self, int tailSize) {
|
|
|
|
|
return FileUtilities.listPath(FileUtilities.getRelativePathTail(self, tailSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File getRelativePathTail(File self, int tailSize) {
|
|
|
|
|
return FileUtilities.getRelativePathTail(self, tailSize);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-24 03:15:59 -04:00
|
|
|
|
public static LocalDateTime toDate(Long self) {
|
|
|
|
|
return LocalDateTime.ofInstant(Instant.ofEpochMilli(self), ZoneOffset.systemDefault());
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-26 11:13:43 -04:00
|
|
|
|
public static File toFile(String self) {
|
2014-05-12 04:25:42 -04:00
|
|
|
|
if (self == null || self.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2014-04-26 11:13:43 -04:00
|
|
|
|
return new File(self);
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-12 04:25:42 -04:00
|
|
|
|
public static File toFile(String self, String parent) {
|
|
|
|
|
if (self == null || self.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
File file = new File(self);
|
|
|
|
|
if (file.isAbsolute()) {
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
return new File(parent, self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-30 23:58:33 -04:00
|
|
|
|
public static Locale toLocale(String self) {
|
|
|
|
|
return Locale.forLanguageTag(self);
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 05:20:31 -05:00
|
|
|
|
/**
|
|
|
|
|
* Date utilities
|
|
|
|
|
*/
|
2017-02-26 15:48:39 -05:00
|
|
|
|
public static String format(Temporal self, String pattern) {
|
2018-12-02 15:23:37 -05:00
|
|
|
|
return DateTimeFormatter.ofPattern(pattern).format(self);
|
2017-03-27 07:39:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String format(TemporalAmount self, String pattern) {
|
2018-12-02 15:23:37 -05:00
|
|
|
|
return DateTimeFormatter.ofPattern(pattern).format(LocalTime.MIDNIGHT.plus(self));
|
2017-02-26 15:48:39 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-02 15:19:54 -05:00
|
|
|
|
public static String format(Date self, String format) {
|
|
|
|
|
return new SimpleDateFormat(format).format(self);
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-02 15:23:37 -05:00
|
|
|
|
public static Date parseDate(String self, String format) throws ParseException {
|
2018-12-02 15:19:54 -05:00
|
|
|
|
return new SimpleDateFormat(format).parse(self);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-30 19:19:11 -04:00
|
|
|
|
/**
|
2019-02-23 05:20:31 -05:00
|
|
|
|
* Episode utilities
|
2016-10-30 19:19:11 -04:00
|
|
|
|
*/
|
|
|
|
|
public static EpisodeInfo getInfo(Episode self) throws Exception {
|
2016-11-13 06:12:30 -05:00
|
|
|
|
if (TheTVDB.getIdentifier().equals(self.getSeriesInfo().getDatabase())) {
|
2017-04-11 05:48:02 -04:00
|
|
|
|
return TheTVDB.getEpisodeInfo(self.getId(), Locale.ENGLISH);
|
2016-11-13 06:12:30 -05:00
|
|
|
|
}
|
2016-10-30 19:19:11 -04:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-30 19:58:27 -04:00
|
|
|
|
public static List<String> getActors(SeriesInfo self) throws Exception {
|
2016-11-13 06:12:30 -05:00
|
|
|
|
if (TheTVDB.getIdentifier().equals(self.getDatabase())) {
|
2017-04-11 05:48:02 -04:00
|
|
|
|
return TheTVDB.getActors(self.getId(), Locale.ENGLISH).stream().map(Person::getName).collect(toList());
|
2016-11-13 06:12:30 -05:00
|
|
|
|
}
|
2016-10-30 19:58:27 -04:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-28 04:07:16 -04:00
|
|
|
|
public static Map<String, List<String>> getAlternativeTitles(Movie self) throws Exception {
|
|
|
|
|
if (self.getTmdbId() > 0) {
|
|
|
|
|
return TheMovieDB.getAlternativeTitles(self.getTmdbId());
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 23:04:23 -04:00
|
|
|
|
public static List<Movie> getCollection(Movie self) throws Exception {
|
|
|
|
|
if (self.getTmdbId() > 0) {
|
|
|
|
|
return TheMovieDB.getCollection(self.getTmdbId(), Locale.US);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-24 02:58:41 -04:00
|
|
|
|
/**
|
|
|
|
|
* DSL utilities
|
|
|
|
|
*/
|
|
|
|
|
public static File plus(File self, String path) {
|
|
|
|
|
return new File(self.getPath().concat(path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File div(File self, String path) {
|
|
|
|
|
return new File(self, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File div(String self, String path) {
|
|
|
|
|
return new File(self, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File div(File self, File path) {
|
|
|
|
|
return new File(self, path.getPath());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static File div(String self, File path) {
|
|
|
|
|
return new File(self, path.getPath());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static String negative(String self) {
|
|
|
|
|
return '-' + self;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-24 03:44:09 -04:00
|
|
|
|
public static String plus(String self, Closure closure) {
|
2019-05-24 14:34:58 -04:00
|
|
|
|
Object value = call(null, closure);
|
2019-04-24 03:44:09 -04:00
|
|
|
|
return value == null ? self : self + value;
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 08:26:04 -05:00
|
|
|
|
private ExpressionFormatMethods() {
|
|
|
|
|
throw new UnsupportedOperationException();
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-13 15:37:11 -04:00
|
|
|
|
}
|