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