001package net.filebot.postprocess;
002
003import static java.nio.charset.StandardCharsets.*;
004import static java.util.Collections.*;
005import static java.util.stream.Collectors.*;
006import static net.filebot.Logging.*;
007import static net.filebot.MediaTypes.*;
008import static net.filebot.util.FileUtilities.*;
009
010import java.io.File;
011import java.net.URL;
012import java.nio.ByteBuffer;
013import java.util.ArrayList;
014import java.util.Iterator;
015import java.util.LinkedHashMap;
016import java.util.List;
017import java.util.Locale;
018import java.util.Map;
019import java.util.Objects;
020import java.util.Optional;
021import java.util.function.Function;
022import java.util.function.Predicate;
023import java.util.stream.Stream;
024
025import org.codehaus.groovy.runtime.DefaultGroovyMethods;
026import org.codehaus.groovy.runtime.InvokerHelper;
027import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
028
029import groovy.json.JsonOutput;
030import groovy.json.JsonSlurper;
031import groovy.lang.Closure;
032import groovy.lang.GroovyObjectSupport;
033import groovy.lang.Script;
034import groovy.xml.XmlSlurper;
035
036import net.filebot.Execute;
037import net.filebot.Language;
038import net.filebot.UserFiles;
039import net.filebot.UserInteraction;
040import net.filebot.WebServices;
041import net.filebot.subtitle.SubtitleFormat;
042import net.filebot.subtitle.SubtitleUtilities;
043import net.filebot.util.Builder;
044import net.filebot.util.ByteBufferInputStream;
045import net.filebot.vfs.MemoryFile;
046import net.filebot.web.SubtitleDescriptor;
047import net.filebot.web.SubtitleLookupService;
048import net.filebot.web.SubtitleProvider;
049import net.filebot.web.WebRequest;
050
051public abstract class ScriptBaseClass extends Script {
052
053        public void alert(Object... messages) throws Exception {
054                String message = message(messages).toString();
055
056                println("ALERT " + message);
057                log.warning(message);
058        }
059
060        public void system(String executable, Object... args) throws Exception {
061                system(emptyMap(), executable, args);
062        }
063
064        public void system(Map env, String executable, Object... args) throws Exception {
065                List<String> arguments = Stream.of(args).filter(Objects::nonNull).map(Objects::toString).collect(toList());
066
067                Map<String, String> environment = new LinkedHashMap<String, String>(env.size());
068                env.forEach((k, v) -> {
069                        if (k != null && v != null) {
070                                environment.put(k.toString(), v.toString());
071                        }
072                });
073
074                println("EXECUTE " + executable + " " + arguments);
075                Execute.system(executable, arguments, null, environment);
076        }
077
078        public void trash(Object... args) throws Exception {
079                for (File f : asFileList(args)) {
080                        println("TRASH " + f);
081                        UserFiles.trash(f);
082                }
083        }
084
085        public void reveal(Object... args) throws Exception {
086                for (File f : asFileList(args)) {
087                        println("REVEAL " + f);
088                        UserInteraction.reveal(f);
089                }
090        }
091
092        public Response curl(Object url) throws Exception {
093                return request(url, emptyMap(), null);
094        }
095
096        public Response curl(Map header, Object url) throws Exception {
097                return request(url, header, null);
098        }
099
100        public Response curl(Object url, Map json) throws Exception {
101                return request(url, emptyMap(), Post.of(WebRequest.HTTP_POST, json, emptyMap()));
102        }
103
104        public Response submit(Object url, Map form) throws Exception {
105                return request(url, emptyMap(), Post.of(WebRequest.HTTP_POST, form, singletonMap("Content-Type", "application/x-www-form-urlencoded")));
106        }
107
108        public Response curl(Map header, Object url, Object data) throws Exception {
109                return request(url, header, Post.of(WebRequest.HTTP_POST, data, header));
110        }
111
112        public Response http(String method, Object url, Object data) throws Exception {
113                return request(url, emptyMap(), Post.of(method, data, emptyMap()));
114        }
115
116        public Response http(Map header, String method, Object url, Object data) throws Exception {
117                return request(url, header, Post.of(method, data, header));
118        }
119
120        private Response request(Object url, Map header, Post data) throws Exception {
121                URL resource = WebRequest.newURL(url.toString());
122
123                Map<String, String> responseHeader = new LinkedHashMap<String, String>();
124
125                // send HTTP GET request
126                if (data == null) {
127                        println("GET " + resource);
128
129                        ByteBuffer response = WebRequest.fetch(resource, 0, null, header, responseHeader::put);
130                        return new Response(response, responseHeader);
131                }
132
133                // send HTTP POST request
134                println(data.getRequestMethod() + " " + resource + " " + data.toString());
135
136                ByteBuffer response = WebRequest.post(data.getRequestMethod(), resource, data.toByteArray(), data.getContentType(), header, responseHeader::put);
137                return new Response(response, responseHeader);
138        }
139
140        public static class Response extends GroovyObjectSupport implements Iterable<Object> {
141
142                private final ByteBuffer content;
143                private final Map<String, String> header;
144
145                private Object object;
146
147                public Response(ByteBuffer content, Map<String, String> header) {
148                        this.content = content;
149                        this.header = header;
150                }
151
152                public ByteBuffer content() {
153                        return content.duplicate();
154                }
155
156                public Map<String, String> header() {
157                        return header;
158                }
159
160                public String header(String name) {
161                        // make response headers case-insensitive
162                        return header().getOrDefault(name.toLowerCase(Locale.ROOT), "");
163                }
164
165                public synchronized Object object() {
166                        if (object == null) {
167                                object = ContentType.forContentType(header("content-type")).parse(content());
168                        }
169                        return object;
170                }
171
172                @Override
173                public Object invokeMethod(String name, Object args) {
174                        return InvokerHelper.invokeMethod(object(), name, args);
175                }
176
177                @Override
178                public Object getProperty(String property) {
179                        return InvokerHelper.getProperty(object(), property);
180                }
181
182                @Override
183                public Iterator<Object> iterator() {
184                        return DefaultGroovyMethods.iterator(object());
185                }
186
187                public boolean asBoolean() {
188                        return DefaultTypeTransformation.castToBoolean(object());
189                }
190
191                @Override
192                public String toString() {
193                        return UTF_8.decode(content()).toString();
194                }
195        }
196
197        private static class Post {
198
199                private final String requestMethod;
200                private final Object content;
201                private final String contentType;
202
203                private Post(String requestMethod, Object content, String contentType) {
204                        this.requestMethod = requestMethod;
205                        this.content = content;
206                        this.contentType = contentType;
207                }
208
209                public String getRequestMethod() {
210                        return requestMethod;
211                }
212
213                public String getContentType() {
214                        return contentType;
215                }
216
217                public byte[] toByteArray() {
218                        // application/octet-stream
219                        if (content instanceof byte[]) {
220                                return (byte[]) content;
221                        }
222
223                        // application/json or application/x-www-form-urlencoded or text/plain
224                        return content.toString().getBytes(UTF_8);
225                }
226
227                @Override
228                public String toString() {
229                        // application/octet-stream
230                        if (content instanceof byte[]) {
231                                byte[] bytes = (byte[]) content;
232                                return "[" + bytes.length + " bytes]";
233                        }
234
235                        // application/json or application/x-www-form-urlencoded or text/plain
236                        return content.toString();
237                }
238
239                public static Post of(String method, Object data, Map header) {
240                        if (data instanceof Map && "application/x-www-form-urlencoded".equals(header.get("Content-Type"))) {
241                                return new Post(method, WebRequest.encodeParameters((Map) data), "application/x-www-form-urlencoded");
242                        }
243                        if (data instanceof CharSequence) {
244                                return new Post(method, data.toString(), "text/plain");
245                        }
246                        if (data instanceof byte[]) {
247                                return new Post(method, (byte[]) data, "application/octet-stream");
248                        }
249                        if (data == null) {
250                                return new Post(method, new byte[0], "application/octet-stream");
251                        }
252                        // post as application/json by default
253                        return new Post(method, JsonOutput.toJson(data), "application/json");
254                }
255
256        }
257
258        public static enum ContentType {
259
260                JSON, XML, TEXT, BYTES;
261
262                public Object parse(ByteBuffer bytes) {
263                        try {
264                                switch (this) {
265                                        case JSON:
266                                                return new JsonSlurper().parse(new ByteBufferInputStream(bytes));
267                                        case XML:
268                                                return new XmlSlurper().parse(new ByteBufferInputStream(bytes));
269                                        case TEXT:
270                                                return UTF_8.decode(bytes).toString();
271                                        default:
272                                                return bytes;
273                                }
274                        } catch (Exception e) {
275                                throw new IllegalStateException("Invalid " + this, e);
276                        }
277                }
278
279                public static ContentType forContentType(String contentType) {
280                        if (contentType.contains("json")) {
281                                return JSON;
282                        }
283                        if (contentType.contains("xml")) {
284                                return XML;
285                        }
286                        if (contentType.contains("text")) {
287                                return TEXT;
288                        }
289                        return BYTES;
290                }
291
292        }
293
294        public List<File> getSubtitles(File file, String... languages) throws Exception {
295                return getSubtitles(emptyMap(), file, languages);
296        }
297
298        public List<File> getSubtitles(Map options, File file, String... languages) throws Exception {
299                List<File> subtitleFiles = new ArrayList<File>(languages.length);
300
301                // strict by default
302                boolean strict = optional(options, "strict", ScriptBaseClass::flag, true);
303
304                // accept any subtitle descriptor by default
305                Predicate<SubtitleDescriptor> filter = optional(options, "filter", ScriptBaseClass::selector, ANY_SUBTITLE);
306
307                // SubRip is the only supported transcode target so the subtitle format is not configurable
308                SubtitleFormat format = SubtitleFormat.SubRip;
309
310                // ignore non-video files
311                if (VIDEO_FILES.accept(file) && file.length() > ONE_MEGABYTE) {
312                        // ignore video files that already have subtitles
313                        for (String language : languages) {
314                                File destination = new File(file.getParentFile(), SubtitleUtilities.formatSubtitle(getName(file), language, format.getFilter().extension()));
315                                if (destination.exists()) {
316                                        continue;
317                                }
318
319                                println("FIND " + destination.getName());
320                                SubtitleDescriptor descriptor = findSubtitles(file, language, strict, filter);
321                                if (descriptor == null) {
322                                        continue;
323                                }
324
325                                println("FETCH " + descriptor);
326                                MemoryFile subtitles = SubtitleUtilities.fetchSubtitle(descriptor);
327                                ByteBuffer bytes = SubtitleUtilities.exportSubtitles(subtitles, format, UTF_8);
328                                subtitleFiles.add(writeFile(bytes, destination));
329                        }
330                }
331
332                return subtitleFiles;
333        }
334
335        public SubtitleDescriptor findSubtitles(File file, String language, boolean strict, Predicate<SubtitleDescriptor> selector) throws Exception {
336                // resolve language code or language name
337                Locale locale = Language.forName(language).getLocale();
338
339                // lookup subtitles by hash
340                for (SubtitleLookupService service : WebServices.getSubtitleLookupServices(locale)) {
341                        SubtitleDescriptor descriptor = SubtitleUtilities.lookupSubtitlesByHash(service, singleton(file), locale, selector != ANY_SUBTITLE, strict).values().stream().flatMap(List::stream).filter(selector).findFirst().orElse(null);
342                        if (descriptor != null) {
343                                return descriptor;
344                        }
345                }
346
347                // only lookup subtitles by hash in strict mode
348                if (strict) {
349                        return null;
350                }
351
352                // lookup subtitles by name
353                for (SubtitleProvider service : WebServices.getSubtitleProviders(locale)) {
354                        SubtitleDescriptor descriptor = SubtitleUtilities.findSubtitlesByName(service, singleton(file), locale, null, selector != ANY_SUBTITLE, strict).values().stream().flatMap(List::stream).filter(selector).findFirst().orElse(null);
355                        if (descriptor != null) {
356                                return descriptor;
357                        }
358                }
359
360                return null;
361        }
362
363        private static final Predicate<SubtitleDescriptor> ANY_SUBTITLE = object -> true;
364
365        private static <T> Predicate<T> selector(Object value) {
366                if (value instanceof Closure) {
367                        Closure closure = (Closure) value;
368                        return object -> DefaultTypeTransformation.castToBoolean(closure.call(object));
369                }
370                return null;
371        }
372
373        private static boolean flag(Object value) {
374                if (value instanceof Boolean) {
375                        return (Boolean) value;
376                }
377                if (value instanceof String) {
378                        return Boolean.parseBoolean((String) value);
379                }
380                return DefaultTypeTransformation.castToBoolean(value);
381        }
382
383        private static <T> T optional(Map options, String name, Function<Object, T> value, T defaultValue) {
384                return Optional.ofNullable(options.get(name)).map(value).orElse(defaultValue);
385        };
386
387        public File XML(File file, Closure delegate) throws Exception {
388                return write(file, Builder.XML, delegate);
389        }
390
391        public String XML(Closure delegate) {
392                return Builder.XML.toString(delegate);
393        }
394
395        public File INI(File file, Closure delegate) throws Exception {
396                return write(file, Builder.INI, delegate);
397        }
398
399        public String INI(Closure delegate) throws Exception {
400                return Builder.INI.toString(delegate);
401        }
402
403        public File JSON(File file, Closure delegate) throws Exception {
404                return write(file, Builder.JSON, delegate);
405        }
406
407        public String JSON(Closure delegate) {
408                return Builder.JSON.toString(delegate);
409        }
410
411        private File write(File file, Builder builder, Closure delegate) throws Exception {
412                if (file.length() > ONE_MEGABYTE) {
413                        alert(format("%s file exists already: %s (%s)", builder, file, formatSize(file.length())));
414                        return null;
415                }
416
417                byte[] bytes = builder.toString(delegate).getBytes(UTF_8);
418                println(format("%s %s (%s)", builder, file, formatSize(bytes.length)));
419
420                return writeFile(bytes, file);
421        }
422
423}