001package net.filebot.mediainfo;
002
003import static java.nio.charset.StandardCharsets.*;
004import static java.util.stream.Collectors.*;
005import static net.filebot.Logging.*;
006import static net.filebot.util.FileUtilities.*;
007import static net.filebot.util.RegularExpressions.*;
008
009import java.io.File;
010import java.io.IOException;
011import java.io.RandomAccessFile;
012import java.lang.ref.Cleaner;
013import java.time.Duration;
014import java.time.Instant;
015import java.time.ZonedDateTime;
016import java.time.format.DateTimeFormatter;
017import java.util.ArrayList;
018import java.util.EnumMap;
019import java.util.LinkedHashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.function.Function;
024import java.util.stream.IntStream;
025import java.util.stream.Stream;
026
027import com.sun.jna.Platform;
028import com.sun.jna.Pointer;
029import com.sun.jna.WString;
030
031import net.filebot.media.ImageMetadata;
032import net.filebot.media.MediaCharacteristics;
033
034public class MediaInfo implements MediaCharacteristics {
035
036        private Pointer handle;
037        private Cleaner.Cleanable cleanable;
038
039        public MediaInfo() {
040                try {
041                        handle = MediaInfoLibrary.INSTANCE.New();
042                        cleanable = cleaner.register(this, new Finalizer(handle));
043                } catch (LinkageError e) {
044                        throw new MediaInfoException(e);
045                }
046        }
047
048        public synchronized MediaInfo open(File file) throws IOException, IllegalArgumentException {
049                if (!file.isFile() || file.length() <= 0) {
050                        throw new IllegalArgumentException("Invalid media file: " + file);
051                }
052
053                if (preferOpenViaBuffer(file)) {
054                        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
055                                if (openViaBuffer(raf)) {
056                                        return this;
057                                }
058                                throw new IOException("Failed to initialize media info buffer: " + file);
059                        }
060                }
061
062                // open via file path
063                if (0 != MediaInfoLibrary.INSTANCE.Open(handle, new WString(file.getPath()))) {
064                        return this;
065                }
066                throw new IOException("Failed to open media file: " + file);
067        }
068
069        private boolean preferOpenViaBuffer(File file) {
070                // on Windows file paths that are longer than 260 characters cannot be opened
071                if (Platform.isWindows() && file.getPath().length() > 250)
072                        return true;
073
074                // on Mac files that contain accents cannot be opened via JNA WString file paths due to encoding differences so we use the buffer interface instead for these files
075                if (Platform.isMac() && !US_ASCII.newEncoder().canEncode(file.getPath()))
076                        return true;
077
078                return false;
079        }
080
081        private boolean openViaBuffer(RandomAccessFile f) throws IOException {
082                long size = f.length();
083                byte[] buffer = new byte[LARGE_BUFFER_SIZE]; // use large buffer to reduce JNA calls
084                int read = -1;
085
086                if (0 == MediaInfoLibrary.INSTANCE.Open_Buffer_Init(handle, size, 0)) {
087                        return false;
088                }
089
090                while ((read = f.read(buffer)) >= 0) {
091                        if ((MediaInfoLibrary.INSTANCE.Open_Buffer_Continue(handle, buffer, read) & 0x08) == 0x08) {
092                                break;
093                        }
094
095                        long seek = MediaInfoLibrary.INSTANCE.Open_Buffer_Continue_GoTo_Get(handle);
096                        if (seek >= 0) {
097                                f.seek(seek);
098                                MediaInfoLibrary.INSTANCE.Open_Buffer_Init(handle, size, seek);
099                        }
100                }
101
102                MediaInfoLibrary.INSTANCE.Open_Buffer_Finalize(handle);
103                return true;
104        }
105
106        public synchronized String inform() {
107                return MediaInfoLibrary.INSTANCE.Inform(handle).toString();
108        }
109
110        public String option(String option) {
111                return option(option, "");
112        }
113
114        public synchronized String option(String option, String value) {
115                return MediaInfoLibrary.INSTANCE.Option(handle, new WString(option), new WString(value)).toString();
116        }
117
118        public String get(StreamKind streamKind, int streamNumber, String parameter) {
119                return get(streamKind, streamNumber, parameter, InfoKind.Text, InfoKind.Name);
120        }
121
122        public String get(StreamKind streamKind, int streamNumber, String parameter, InfoKind infoKind) {
123                return get(streamKind, streamNumber, parameter, infoKind, InfoKind.Name);
124        }
125
126        public synchronized String get(StreamKind streamKind, int streamNumber, String parameter, InfoKind infoKind, InfoKind searchKind) {
127                return MediaInfoLibrary.INSTANCE.Get(handle, streamKind.ordinal(), streamNumber, new WString(parameter), infoKind.ordinal(), searchKind.ordinal()).toString();
128        }
129
130        public String get(StreamKind streamKind, int streamNumber, int parameterIndex) {
131                return get(streamKind, streamNumber, parameterIndex, InfoKind.Text);
132        }
133
134        public synchronized String get(StreamKind streamKind, int streamNumber, int parameterIndex, InfoKind infoKind) {
135                return MediaInfoLibrary.INSTANCE.GetI(handle, streamKind.ordinal(), streamNumber, parameterIndex, infoKind.ordinal()).toString();
136        }
137
138        public synchronized int streamCount(StreamKind streamKind) {
139                return MediaInfoLibrary.INSTANCE.Count_Get(handle, streamKind.ordinal(), -1);
140        }
141
142        public synchronized int parameterCount(StreamKind streamKind, int streamNumber) {
143                return MediaInfoLibrary.INSTANCE.Count_Get(handle, streamKind.ordinal(), streamNumber);
144        }
145
146        @Override
147        public String getVideoCodec() {
148                return getString(StreamKind.General, "Video_Codec_List");
149        }
150
151        @Override
152        public String getAudioCodec() {
153                return getString(StreamKind.General, "Audio_Codec_List");
154        }
155
156        @Override
157        public String getAudioLanguage() {
158                return getString(StreamKind.General, "Audio_Language_List");
159        }
160
161        @Override
162        public String getSubtitleCodec() {
163                return getString(StreamKind.General, "Text_Codec_List");
164        }
165
166        @Override
167        public String getSubtitleLanguage() {
168                return getString(StreamKind.General, "Text_Language_List");
169        }
170
171        @Override
172        public Duration getDuration() {
173                return get(StreamKind.General, "Duration", s -> Duration.ofMillis(Math.round(Double.parseDouble(s))));
174        }
175
176        @Override
177        public Integer getWidth() {
178                return getInteger(StreamKind.Video, "Width");
179        }
180
181        @Override
182        public Integer getHeight() {
183                return getInteger(StreamKind.Video, "Height");
184        }
185
186        public Double getBitRate() {
187                return getDouble(StreamKind.General, "OverallBitRate");
188        }
189
190        @Override
191        public Double getFrameRate() {
192                return getDouble(StreamKind.Video, "FrameRate");
193        }
194
195        @Override
196        public String getTitle() {
197                return getString(StreamKind.General, "Title");
198        }
199
200        @Override
201        public Instant getCreationTime() {
202                // Check for fake MediaInfo properties (e.g. fake encoding date)
203                // e.g.
204                // Encoded_Date: UTC 2010-02-22 21:41:29
205                // Encoded_Application: no_variable_data
206                if (stream(StreamKind.General, "Encoded_Application").anyMatch("no_variable_data"::equals)) {
207                        return null;
208                }
209
210                // e.g. UTC 2008-01-08 19:54:39
211                return get(StreamKind.General, "Encoded_Date", d -> ZonedDateTime.parse(d, DateTimeFormatter.ofPattern("zzz uuuu-MM-dd HH:mm:ss")).toInstant());
212        }
213
214        public String getString(StreamKind kind, String property) {
215                return get(kind, property, Object::toString);
216        }
217
218        public Integer getInteger(StreamKind kind, String property) {
219                return get(kind, property, Integer::parseInt);
220        }
221
222        public Double getDouble(StreamKind kind, String property) {
223                return get(kind, property, Double::parseDouble);
224        }
225
226        public <T> T get(StreamKind kind, String property, Function<String, T> mapper) {
227                return stream(kind, property).findFirst().map(mapper).orElse(null);
228        }
229
230        public Stream<String> stream(StreamKind streamKind, String... keys) {
231                return IntStream.range(0, streamCount(streamKind)).mapToObj(i -> {
232                        return Stream.of(keys).map(key -> {
233                                return get(streamKind, i, key);
234                        }).filter(s -> !s.isEmpty()).findFirst().orElse(null);
235                }).filter(Objects::nonNull);
236        }
237
238        public Map<StreamKind, List<Map<String, String>>> snapshot() {
239                Map<StreamKind, List<Map<String, String>>> mediaInfo = new EnumMap<StreamKind, List<Map<String, String>>>(StreamKind.class);
240
241                for (StreamKind streamKind : StreamKind.values()) {
242                        int streamCount = streamCount(streamKind);
243
244                        if (streamCount > 0) {
245                                List<Map<String, String>> streamInfoList = new ArrayList<Map<String, String>>(streamCount);
246
247                                for (int i = 0; i < streamCount; i++) {
248                                        streamInfoList.add(snapshot(streamKind, i));
249                                }
250
251                                mediaInfo.put(streamKind, streamInfoList);
252                        }
253                }
254
255                return mediaInfo;
256        }
257
258        public Map<String, String> snapshot(StreamKind streamKind, int streamNumber) {
259                Map<String, String> streamInfo = new LinkedHashMap<String, String>();
260
261                for (int i = 0, count = parameterCount(streamKind, streamNumber); i < count; i++) {
262                        String value = get(streamKind, streamNumber, i, InfoKind.Text);
263
264                        if (value.length() > 0) {
265                                streamInfo.put(get(streamKind, streamNumber, i, InfoKind.Name), value);
266                        }
267                }
268
269                // MediaInfo does not support EXIF image metadata natively so we use the metadata-extractor library and implicitly merge that information in
270                if (streamKind == StreamKind.Image && streamNumber == 0) {
271                        String path = get(StreamKind.General, 0, "CompleteName");
272                        try {
273                                Map<String, String> values = new ImageMetadata(new File(path)).snapshot(t -> {
274                                        return Stream.of(t.getDirectoryName(), t.getTagName()).flatMap(NON_WORD::splitAsStream).distinct().collect(joining("_"));
275                                });
276                                streamInfo.putAll(values);
277                        } catch (Throwable e) {
278                                debug.warning(message(e, path));
279                        }
280                }
281
282                return streamInfo;
283        }
284
285        @Override
286        public synchronized void close() {
287                cleanable.clean();
288        }
289
290        public enum StreamKind {
291                General, Video, Audio, Text, Chapters, Image, Menu;
292        }
293
294        public enum InfoKind {
295                /**
296                 * Unique name of parameter.
297                 */
298                Name,
299
300                /**
301                 * Value of parameter.
302                 */
303                Text,
304
305                /**
306                 * Unique name of measure unit of parameter.
307                 */
308                Measure,
309
310                Options,
311
312                /**
313                 * Translated name of parameter.
314                 */
315                Name_Text,
316
317                /**
318                 * Translated name of measure unit.
319                 */
320                Measure_Text,
321
322                /**
323                 * More information about the parameter.
324                 */
325                Info,
326
327                /**
328                 * How this parameter is supported, could be N (No), B (Beta), R (Read only), W (Read/Write).
329                 */
330                HowTo,
331
332                /**
333                 * Domain of this piece of information.
334                 */
335                Domain;
336        }
337
338        public static String version() {
339                return staticOption("Info_Version");
340        }
341
342        public static String parameters() {
343                return staticOption("Info_Parameters");
344        }
345
346        public static String codecs() {
347                return staticOption("Info_Codecs");
348        }
349
350        public static String capacities() {
351                return staticOption("Info_Capacities");
352        }
353
354        public static String staticOption(String option) {
355                return staticOption(option, "");
356        }
357
358        public static String staticOption(String option, String value) {
359                try {
360                        return MediaInfoLibrary.INSTANCE.Option(null, new WString(option), new WString(value)).toString();
361                } catch (LinkageError e) {
362                        throw new MediaInfoException(e);
363                }
364        }
365
366        public static Map<StreamKind, List<Map<String, String>>> snapshot(File file) throws IOException {
367                try (MediaInfo mi = new MediaInfo().open(file)) {
368                        return mi.snapshot();
369                }
370        }
371
372        /**
373         * Use {@link Cleaner} instead of Object.finalize()
374         */
375        private static final Cleaner cleaner = Cleaner.create();
376
377        private static class Finalizer implements Runnable {
378
379                private Pointer handle;
380
381                public Finalizer(Pointer handle) {
382                        this.handle = handle;
383                }
384
385                @Override
386                public void run() {
387                        MediaInfoLibrary.INSTANCE.Close(handle);
388                        MediaInfoLibrary.INSTANCE.Delete(handle);
389                }
390        }
391
392}