Merge lp:~jamesh/mediascanner2/taglib-extractor into lp:mediascanner2

Proposed by James Henstridge
Status: Merged
Approved by: Michi Henning
Approved revision: 332
Merged at revision: 322
Proposed branch: lp:~jamesh/mediascanner2/taglib-extractor
Merge into: lp:mediascanner2
Diff against target: 1670 lines (+1034/-409)
15 files modified
CMakeLists.txt (+1/-0)
HACKING (+22/-0)
debian/control (+1/-0)
debian/control.in (+1/-0)
src/extractor/CMakeLists.txt (+5/-1)
src/extractor/ExtractorBackend.cc (+15/-340)
src/extractor/GStreamerExtractor.cc (+178/-0)
src/extractor/GStreamerExtractor.hh (+48/-0)
src/extractor/ImageExtractor.cc (+241/-0)
src/extractor/ImageExtractor.hh (+47/-0)
src/extractor/TaglibExtractor.cc (+295/-0)
src/extractor/TaglibExtractor.hh (+42/-0)
test/CMakeLists.txt (+1/-1)
test/test_extractorbackend.cc (+137/-1)
test/test_metadataextractor.cc (+0/-66)
To merge this branch: bzr merge lp:~jamesh/mediascanner2/taglib-extractor
Reviewer Review Type Date Requested Status
Michi Henning (community) Approve
PS Jenkins bot (community) continuous-integration Needs Fixing
Review via email: mp+285834@code.launchpad.net

Commit message

Use taglib to extract metadata from Vorbis, Opus, Flac, MP3 and MP4 audio files. Other formats will fall back to the existing GStreamer code path.

Description of the change

Use taglib to extract metadata from Vorbis, Opus, Flac, MP3 and MP4 files.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

This is a reasonably significant change. It might warrant a bump in the version number, and it probably should be mentioned in the changelog?

Splitting the extractor into separate helper classes has made this much cleaner and comprehensible, thanks!

I'm wondering whether we should still make a corresponding change to the thumbnailer taglib branch to avoid the files being opened read/write?

I'm seeing a core dump when runnig the test_qml_nodb test. This is probably worth looking at.

Program terminated with signal SIGABRT, Aborted.
#0 0x00007f7018eff227 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
55 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
[Current thread is 1 (Thread 0x7f701b1d6800 (LWP 6972))]
(gdb) bt
#0 0x00007f7018eff227 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
#1 0x00007f7018f00e8a in __GI_abort () at abort.c:89
#2 0x00000000004285cd in (anonymous namespace)::ExtractorDaemon::handleExtractMetadata (invocation=0x7f7008006880,
    filename=0x11317a0 "/home/michi/src/mediascanner2/taglib-extractor/test/media/testfile.ogg",
    etag=0x1129000 "1455579826:124200", content_type=0x11318e0 "audio/ogg", mtime=1455579826, type=1,
    user_data=0x7ffd784f8f60) at /home/michi/src/mediascanner2/taglib-extractor/src/extractor/main.cc:147
#3 0x00007f7016ee1e40 in ffi_call_unix64 () from /usr/lib/x86_64-linux-gnu/libffi.so.6
#4 0x00007f7016ee18ab in ffi_call () from /usr/lib/x86_64-linux-gnu/libffi.so.6
#5 0x00007f7019e217c9 in g_cclosure_marshal_generic () from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#6 0x00007f7019e20fa5 in g_closure_invoke () from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#7 0x00007f7019e32ff1 in ?? () from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#8 0x00007f7019e3ad71 in g_signal_emitv () from /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0
#9 0x0000000000436eec in _ms_extractor_skeleton_handle_method_call (connection=0x11001c0,
    sender=0x7f70080011a0 ":1.0", object_path=0x7f70080053d0 "/com/canonical/MediaScanner2/Extractor",
    interface_name=0x7f7008002f80 "com.canonical.MediaScanner2.Extractor",
    method_name=0x7f70080051f0 "ExtractMetadata", parameters=0x113a720, invocation=0x7f7008006880, user_data=0x1111a10)
    at /home/michi/src/mediascanner2/taglib-extractor/build/src/extractor/dbus-generated.c:933

review: Needs Fixing
Revision history for this message
Michi Henning (michihenning) wrote :

OK, after speaking with James, the core dump is red herring; it is intentional. It would be good to remove the core at the end of the test so, when the test fails, I don't jump to conclusions.

Turns out that the failure was actually caused by me not having qt5-default installed, and not having set QT_SELECT. Again, some message to that effect when the test fails would be useful. Otherwise, someone not familiar with the code might spend ages trying to figure out the failure.

Besides those two niggles, this looks really nice!

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

Sweet, thank you!

review: Approve
Revision history for this message
Michi Henning (michihenning) wrote :

Cool, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2015-12-16 07:07:41 +0000
3+++ CMakeLists.txt 2016-02-24 05:55:29 +0000
4@@ -30,6 +30,7 @@
5 pkg_check_modules(GLIB glib-2.0 REQUIRED)
6 pkg_check_modules(PIXBUF gdk-pixbuf-2.0 REQUIRED)
7 pkg_check_modules(EXIF libexif REQUIRED)
8+pkg_check_modules(TAGLIB taglib REQUIRED)
9 pkg_check_modules(DBUSCPP dbus-cpp REQUIRED)
10 pkg_check_modules(APPARMOR libapparmor REQUIRED)
11 pkg_check_modules(UDISKS udisks2 REQUIRED)
12
13=== added file 'HACKING'
14--- HACKING 1970-01-01 00:00:00 +0000
15+++ HACKING 2016-02-24 05:55:29 +0000
16@@ -0,0 +1,22 @@
17+Building the code
18+-----------------
19+
20+The list of packages required to build mediascanner can be found in
21+the Build-Depends stanza of the debian/control.in file. In addition
22+to those packages, you should install "qt5-default" to ensure that the
23+Qt 5.x versions of build tools are used in preference to the Qt 4.x
24+versions.
25+
26+The software can then be built with the following commands:
27+
28+ $ mkdir build
29+ $ cd build
30+ $ cmake ..
31+ $ make
32+
33+The tests can then be run using:
34+
35+ $ make test
36+
37+Note that "make test" will not trigger a rebuild, so it is necessary
38+to rerun "make" first if any code has been changed.
39
40=== modified file 'debian/control'
41--- debian/control 2015-11-02 02:36:21 +0000
42+++ debian/control 2016-02-24 05:55:29 +0000
43@@ -23,6 +23,7 @@
44 libgtest-dev,
45 libproperties-cpp-dev,
46 libsqlite3-dev (>= 3.8.5),
47+ libtag1-dev,
48 libudisks2-dev,
49 lsb-release,
50 qtbase5-dev,
51
52=== modified file 'debian/control.in'
53--- debian/control.in 2015-11-02 02:36:21 +0000
54+++ debian/control.in 2016-02-24 05:55:29 +0000
55@@ -18,6 +18,7 @@
56 libgtest-dev,
57 libproperties-cpp-dev,
58 libsqlite3-dev (>= 3.8.5),
59+ libtag1-dev,
60 libudisks2-dev,
61 lsb-release,
62 qtbase5-dev,
63
64=== modified file 'src/extractor/CMakeLists.txt'
65--- src/extractor/CMakeLists.txt 2015-12-15 04:27:08 +0000
66+++ src/extractor/CMakeLists.txt 2016-02-24 05:55:29 +0000
67@@ -1,5 +1,5 @@
68
69-add_definitions(${MEDIASCANNER_DEPS_CFLAGS} ${GST_CFLAGS} ${EXIF_CFLAGS} ${PIXBUF_CFLAGS})
70+add_definitions(${MEDIASCANNER_DEPS_CFLAGS} ${GST_CFLAGS} ${EXIF_CFLAGS} ${PIXBUF_CFLAGS} ${TAGLIB_CFLAGS})
71 include_directories(.. ${CMAKE_CURRENT_BINARY_DIR})
72
73 # Build stubs/skeletons for D-Bus interface
74@@ -32,12 +32,16 @@
75 # The backend code for the extractor daemon, as a library for use by tests
76 add_library(extractor-backend STATIC
77 ExtractorBackend.cc
78+ GStreamerExtractor.cc
79+ ImageExtractor.cc
80+ TaglibExtractor.cc
81 )
82 target_link_libraries(extractor-backend
83 extractor-client
84 ${GST_LDFLAGS}
85 ${EXIF_LDFLAGS}
86 ${PIXBUF_LDFLAGS}
87+ ${TAGLIB_LDFLAGS}
88 )
89
90 add_executable(mediascanner-extractor
91
92=== modified file 'src/extractor/ExtractorBackend.cc'
93--- src/extractor/ExtractorBackend.cc 2015-11-02 09:38:48 +0000
94+++ src/extractor/ExtractorBackend.cc 2016-02-24 05:55:29 +0000
95@@ -20,363 +20,35 @@
96
97 #include "ExtractorBackend.hh"
98 #include "DetectedFile.hh"
99+#include "GStreamerExtractor.hh"
100+#include "ImageExtractor.hh"
101+#include "TaglibExtractor.hh"
102 #include "../mediascanner/MediaFile.hh"
103 #include "../mediascanner/MediaFileBuilder.hh"
104-#include "../mediascanner/internal/utils.hh"
105-
106-#include <exif-loader.h>
107-#include <glib-object.h>
108-#include <gio/gio.h>
109-#include <gst/gst.h>
110-#include <gst/pbutils/pbutils.h>
111-#include <gdk-pixbuf/gdk-pixbuf.h>
112
113 #include <cstdio>
114-#include <ctime>
115-#include <memory>
116 #include <string>
117-#include <stdexcept>
118-#include <vector>
119-
120-#include <unistd.h>
121
122 using namespace std;
123
124-namespace {
125-const char exif_date_template[] = "%Y:%m:%d %H:%M:%S";
126-const char iso8601_date_format[] = "%Y-%m-%dT%H:%M:%S";
127-const char iso8601_date_with_zone_format[] = "%Y-%m-%dT%H:%M:%S%z";
128-}
129-
130 namespace mediascanner {
131
132 struct ExtractorBackendPrivate {
133- std::unique_ptr<GstDiscoverer, decltype(&g_object_unref)> discoverer;
134- ExtractorBackendPrivate() : discoverer(nullptr, g_object_unref) {};
135+ ExtractorBackendPrivate(int seconds) : gstreamer(seconds) {}
136
137- void extract_gst(const DetectedFile &d, MediaFileBuilder &mfb);
138- bool extract_exif(const DetectedFile &d, MediaFileBuilder &mfb);
139- void extract_pixbuf(const DetectedFile &d, MediaFileBuilder &mfb);
140+ GStreamerExtractor gstreamer;
141+ ImageExtractor image;
142+ TaglibExtractor taglib;
143 };
144
145-ExtractorBackend::ExtractorBackend(int seconds) {
146- p = new ExtractorBackendPrivate();
147- GError *error = nullptr;
148-
149- p->discoverer.reset(gst_discoverer_new(GST_SECOND * seconds, &error));
150- if (not p->discoverer) {
151- string errortxt(error->message);
152- g_error_free(error);
153- delete p;
154-
155- string msg = "Failed to create discoverer: ";
156- msg += errortxt;
157- throw runtime_error(msg);
158- }
159- if (error) {
160- // Sometimes this is filled in even though no error happens.
161- g_error_free(error);
162- }
163+ExtractorBackend::ExtractorBackend(int seconds)
164+ : p(new ExtractorBackendPrivate(seconds)) {
165 }
166
167 ExtractorBackend::~ExtractorBackend() {
168 delete p;
169 }
170
171-static void
172-extract_tag_info (const GstTagList * list, const gchar * tag, gpointer user_data) {
173- MediaFileBuilder *mfb = (MediaFileBuilder *) user_data;
174- int i, num;
175- string tagname(tag);
176-
177- if(tagname == GST_TAG_IMAGE || tagname == GST_TAG_PREVIEW_IMAGE) {
178- mfb->setHasThumbnail(true);
179- return;
180- }
181- num = gst_tag_list_get_tag_size (list, tag);
182- for (i = 0; i < num; ++i) {
183- const GValue *val;
184-
185- val = gst_tag_list_get_value_index (list, tag, i);
186- if (G_VALUE_HOLDS_STRING(val)) {
187- if (tagname == GST_TAG_ARTIST)
188- mfb->setAuthor(g_value_get_string(val));
189- else if (tagname == GST_TAG_TITLE)
190- mfb->setTitle(g_value_get_string(val));
191- else if (tagname == GST_TAG_ALBUM)
192- mfb->setAlbum(g_value_get_string(val));
193- else if (tagname == GST_TAG_ALBUM_ARTIST)
194- mfb->setAlbumArtist(g_value_get_string(val));
195- else if (tagname == GST_TAG_GENRE)
196- mfb->setGenre(g_value_get_string(val));
197- } else if (G_VALUE_HOLDS(val, GST_TYPE_DATE_TIME)) {
198- if (tagname == GST_TAG_DATE_TIME) {
199- GstDateTime *dt = static_cast<GstDateTime*>(g_value_get_boxed(val));
200- if (!dt) {
201- continue;
202- }
203- char *dt_string = gst_date_time_to_iso8601_string(dt);
204- mfb->setDate(dt_string);
205- g_free(dt_string);
206- }
207- } else if (G_VALUE_HOLDS(val, G_TYPE_DATE)) {
208- if (tagname == GST_TAG_DATE) {
209- GDate *dt = static_cast<GDate*>(g_value_get_boxed(val));
210- if (!dt) {
211- continue;
212- }
213- char buf[100];
214- if (g_date_strftime(buf, sizeof(buf), "%Y-%m-%d", dt) != 0) {
215- mfb->setDate(buf);
216- }
217- }
218- } else if (G_VALUE_HOLDS_UINT(val)) {
219- if (tagname == GST_TAG_TRACK_NUMBER) {
220- mfb->setTrackNumber(g_value_get_uint(val));
221- } else if (tagname == GST_TAG_ALBUM_VOLUME_NUMBER) {
222- mfb->setDiscNumber(g_value_get_uint(val));
223- }
224- }
225-
226- }
227-}
228-
229-void ExtractorBackendPrivate::extract_gst(const DetectedFile &d, MediaFileBuilder &mfb) {
230- string uri = getUri(d.filename);
231-
232- GError *error = nullptr;
233- unique_ptr<GstDiscovererInfo, void(*)(void *)> info(
234- gst_discoverer_discover_uri(discoverer.get(), uri.c_str(), &error),
235- g_object_unref);
236- if (info.get() == nullptr) {
237- string errortxt(error->message);
238- g_error_free(error);
239-
240- string msg = "Discovery of file ";
241- msg += d.filename;
242- msg += " failed: ";
243- msg += errortxt;
244- throw runtime_error(msg);
245- }
246- if (error) {
247- // Sometimes this gets filled in even if no error actually occurs.
248- g_error_free(error);
249- error = nullptr;
250- }
251-
252- if (gst_discoverer_info_get_result(info.get()) != GST_DISCOVERER_OK) {
253- throw runtime_error("Unable to discover file " + d.filename);
254- }
255-
256- const GstTagList *tags = gst_discoverer_info_get_tags(info.get());
257- if (tags != nullptr) {
258- gst_tag_list_foreach(tags, extract_tag_info, &mfb);
259- }
260- mfb.setDuration(static_cast<int>(
261- gst_discoverer_info_get_duration(info.get())/GST_SECOND));
262-
263- /* Check for video specific information */
264- unique_ptr<GList, void(*)(GList*)> streams(
265- gst_discoverer_info_get_stream_list(info.get()),
266- gst_discoverer_stream_info_list_free);
267- for (const GList *l = streams.get(); l != nullptr; l = l->next) {
268- auto stream_info = static_cast<GstDiscovererStreamInfo*>(l->data);
269-
270- if (GST_IS_DISCOVERER_VIDEO_INFO(stream_info)) {
271- mfb.setWidth(gst_discoverer_video_info_get_width(
272- GST_DISCOVERER_VIDEO_INFO(stream_info)));
273- mfb.setHeight(gst_discoverer_video_info_get_height(
274- GST_DISCOVERER_VIDEO_INFO(stream_info)));
275- break;
276- }
277- }
278-}
279-
280-static void parse_exif_date(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
281- static const std::vector<ExifTag> date_tags{
282- EXIF_TAG_DATE_TIME,
283- EXIF_TAG_DATE_TIME_ORIGINAL,
284- EXIF_TAG_DATE_TIME_DIGITIZED
285- };
286- struct tm timeinfo;
287- bool have_date = false;
288-
289- for (ExifTag tag : date_tags) {
290- ExifEntry *ent = exif_data_get_entry(data, tag);
291- if (ent == nullptr) {
292- continue;
293- }
294- if (strptime((const char*)ent->data, exif_date_template, &timeinfo) != nullptr) {
295- have_date = true;
296- break;
297- }
298- }
299- if (!have_date) {
300- return;
301- }
302-
303- char buf[100];
304- ExifEntry *ent = exif_data_get_entry(data, EXIF_TAG_TIME_ZONE_OFFSET);
305- if (ent) {
306- timeinfo.tm_gmtoff = (int)exif_get_sshort(ent->data, order) * 3600;
307-
308- if (strftime(buf, sizeof(buf), iso8601_date_with_zone_format, &timeinfo) != 0) {
309- mfb.setDate(buf);
310- }
311- } else {
312- /* No time zone info */
313- if (strftime(buf, sizeof(buf), iso8601_date_format, &timeinfo) != 0) {
314- mfb.setDate(buf);
315- }
316- }
317-}
318-
319-static int get_exif_int(ExifEntry *ent, ExifByteOrder order) {
320- switch (ent->format) {
321- case EXIF_FORMAT_BYTE:
322- return (unsigned char)ent->data[0];
323- case EXIF_FORMAT_SHORT:
324- return exif_get_short(ent->data, order);
325- case EXIF_FORMAT_LONG:
326- return exif_get_long(ent->data, order);
327- case EXIF_FORMAT_SBYTE:
328- return (signed char)ent->data[0];
329- case EXIF_FORMAT_SSHORT:
330- return exif_get_sshort(ent->data, order);
331- case EXIF_FORMAT_SLONG:
332- return exif_get_slong(ent->data, order);
333- default:
334- break;
335- }
336- return 0;
337-}
338-
339-static void parse_exif_dimensions(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
340- ExifEntry *w_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_X_DIMENSION);
341- ExifEntry *h_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_Y_DIMENSION);
342- ExifEntry *o_ent = exif_data_get_entry(data, EXIF_TAG_ORIENTATION);
343-
344- if (!w_ent || !h_ent) {
345- return;
346- }
347- int width = get_exif_int(w_ent, order);
348- int height = get_exif_int(h_ent, order);
349-
350- // Optionally swap height and width depending on orientation
351- if (o_ent) {
352- int tmp;
353-
354- // exif_data_fix() has ensured this is a short.
355- switch (exif_get_short(o_ent->data, order)) {
356- case 5: // Mirror horizontal and rotate 270 CW
357- case 6: // Rotate 90 CW
358- case 7: // Mirror horizontal and rotate 90 CW
359- case 8: // Rotate 270 CW
360- tmp = width;
361- width = height;
362- height = tmp;
363- break;
364- default:
365- break;
366- }
367- }
368- mfb.setWidth(width);
369- mfb.setHeight(height);
370-}
371-
372-static bool rational_to_degrees(ExifEntry *ent, ExifByteOrder order, double *out) {
373- if (ent->format != EXIF_FORMAT_RATIONAL) {
374- return false;
375- }
376-
377- ExifRational r = exif_get_rational(ent->data, order);
378- *out = ((double) r.numerator) / r.denominator;
379-
380- // Minutes
381- if (ent->components >= 2) {
382- r = exif_get_rational(ent->data + exif_format_get_size(EXIF_FORMAT_RATIONAL), order);
383- *out += ((double) r.numerator) / r.denominator / 60;
384- }
385- // Seconds
386- if (ent->components >= 3) {
387- r = exif_get_rational(ent->data + 2 * exif_format_get_size(EXIF_FORMAT_RATIONAL), order);
388- *out += ((double) r.numerator) / r.denominator / 3600;
389- }
390- return true;
391-}
392-
393-static void parse_exif_location(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
394- ExifContent *ifd = data->ifd[EXIF_IFD_GPS];
395- ExifEntry *lat_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE);
396- ExifEntry *latref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE_REF);
397- ExifEntry *long_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE);
398- ExifEntry *longref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE_REF);
399-
400- if (!lat_ent || !long_ent) {
401- return;
402- }
403-
404- double latitude, longitude;
405- if (!rational_to_degrees(lat_ent, order, &latitude)) {
406- return;
407- }
408- if (!rational_to_degrees(long_ent, order, &longitude)) {
409- return;
410- }
411- if (latref_ent && latref_ent->data[0] == 'S') {
412- latitude = -latitude;
413- }
414- if (longref_ent && longref_ent->data[0] == 'W') {
415- longitude = -longitude;
416- }
417- mfb.setLatitude(latitude);
418- mfb.setLongitude(longitude);
419-}
420-
421-bool ExtractorBackendPrivate::extract_exif(const DetectedFile &d, MediaFileBuilder &mfb) {
422- std::unique_ptr<ExifLoader, void(*)(ExifLoader*)> loader(
423- exif_loader_new(), exif_loader_unref);
424- exif_loader_write_file(loader.get(), d.filename.c_str());
425-
426- std::unique_ptr<ExifData, void(*)(ExifData*)> data(
427- exif_loader_get_data(loader.get()), exif_data_unref);
428- loader.reset();
429-
430- if (!data) {
431- return false;
432- }
433- exif_data_fix(data.get());
434- ExifByteOrder order = exif_data_get_byte_order(data.get());
435-
436- parse_exif_date(data.get(), order, mfb);
437- parse_exif_dimensions(data.get(), order, mfb);
438- parse_exif_location(data.get(), order, mfb);
439- return true;
440-}
441-
442-void ExtractorBackendPrivate::extract_pixbuf(const DetectedFile &d, MediaFileBuilder &mfb) {
443- gint width, height;
444-
445- if(!gdk_pixbuf_get_file_info(d.filename.c_str(), &width, &height)) {
446- string msg("Could not determine resolution of ");
447- msg += d.filename;
448- msg += ".";
449- throw runtime_error(msg);
450- }
451- mfb.setWidth(width);
452- mfb.setHeight(height);
453-
454- if (d.mtime != 0) {
455- auto t = static_cast<time_t>(d.mtime);
456- char buf[1024];
457- struct tm ptm;
458- localtime_r(&t, &ptm);
459- if (strftime(buf, sizeof(buf), iso8601_date_format, &ptm) != 0) {
460- mfb.setDate(buf);
461- }
462- }
463-}
464-
465 MediaFile ExtractorBackend::extract(const DetectedFile &d) {
466 printf("Extracting metadata from %s.\n", d.filename.c_str());
467 MediaFileBuilder mfb(d.filename);
468@@ -387,12 +59,15 @@
469
470 switch (d.type) {
471 case ImageMedia:
472- if(!p->extract_exif(d, mfb)) {
473- p->extract_pixbuf(d, mfb);
474+ p->image.extract(d, mfb);
475+ break;
476+ case AudioMedia:
477+ if (!p->taglib.extract(d, mfb)) {
478+ p->gstreamer.extract(d, mfb);
479 }
480 break;
481 default:
482- p->extract_gst(d, mfb);
483+ p->gstreamer.extract(d, mfb);
484 break;
485 }
486
487
488=== added file 'src/extractor/GStreamerExtractor.cc'
489--- src/extractor/GStreamerExtractor.cc 1970-01-01 00:00:00 +0000
490+++ src/extractor/GStreamerExtractor.cc 2016-02-24 05:55:29 +0000
491@@ -0,0 +1,178 @@
492+/*
493+ * Copyright (C) 2013-2014 Canonical, Ltd.
494+ *
495+ * Authors:
496+ * Jussi Pakkanen <jussi.pakkanen@canonical.com>
497+ * James Henstridge <james.henstridge@canonical.com>
498+ *
499+ * This library is free software; you can redistribute it and/or modify it under
500+ * the terms of version 3 of the GNU General Public License as published
501+ * by the Free Software Foundation.
502+ *
503+ * This library is distributed in the hope that it will be useful, but WITHOUT
504+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
505+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
506+ * details.
507+ *
508+ * You should have received a copy of the GNU General Public License
509+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
510+ */
511+
512+#include "GStreamerExtractor.hh"
513+#include "DetectedFile.hh"
514+#include "../mediascanner/MediaFile.hh"
515+#include "../mediascanner/MediaFileBuilder.hh"
516+#include "../mediascanner/internal/utils.hh"
517+
518+#include <glib-object.h>
519+#include <gio/gio.h>
520+#include <gst/gst.h>
521+#include <gst/pbutils/pbutils.h>
522+
523+#include <memory>
524+#include <string>
525+#include <stdexcept>
526+
527+using namespace std;
528+using mediascanner::MediaFileBuilder;
529+
530+namespace {
531+
532+void extract_tag_info (const GstTagList *list, const gchar *tag, void *user_data) {
533+ MediaFileBuilder *mfb = (MediaFileBuilder *) user_data;
534+ int i, num;
535+ string tagname(tag);
536+
537+ if(tagname == GST_TAG_IMAGE || tagname == GST_TAG_PREVIEW_IMAGE) {
538+ mfb->setHasThumbnail(true);
539+ return;
540+ }
541+ num = gst_tag_list_get_tag_size (list, tag);
542+ for (i = 0; i < num; ++i) {
543+ const GValue *val;
544+
545+ val = gst_tag_list_get_value_index (list, tag, i);
546+ if (G_VALUE_HOLDS_STRING(val)) {
547+ const char *value = g_value_get_string(val);
548+ if (!value) {
549+ continue;
550+ }
551+ if (tagname == GST_TAG_ARTIST)
552+ mfb->setAuthor(value);
553+ else if (tagname == GST_TAG_TITLE)
554+ mfb->setTitle(value);
555+ else if (tagname == GST_TAG_ALBUM)
556+ mfb->setAlbum(value);
557+ else if (tagname == GST_TAG_ALBUM_ARTIST)
558+ mfb->setAlbumArtist(value);
559+ else if (tagname == GST_TAG_GENRE)
560+ mfb->setGenre(value);
561+ } else if (G_VALUE_HOLDS(val, GST_TYPE_DATE_TIME)) {
562+ if (tagname == GST_TAG_DATE_TIME) {
563+ GstDateTime *dt = static_cast<GstDateTime*>(g_value_get_boxed(val));
564+ if (!dt) {
565+ continue;
566+ }
567+ char *dt_string = gst_date_time_to_iso8601_string(dt);
568+ mfb->setDate(dt_string);
569+ g_free(dt_string);
570+ }
571+ } else if (G_VALUE_HOLDS(val, G_TYPE_DATE)) {
572+ if (tagname == GST_TAG_DATE) {
573+ GDate *dt = static_cast<GDate*>(g_value_get_boxed(val));
574+ if (!dt) {
575+ continue;
576+ }
577+ char buf[100];
578+ if (g_date_strftime(buf, sizeof(buf), "%Y-%m-%d", dt) != 0) {
579+ mfb->setDate(buf);
580+ }
581+ }
582+ } else if (G_VALUE_HOLDS_UINT(val)) {
583+ if (tagname == GST_TAG_TRACK_NUMBER) {
584+ mfb->setTrackNumber(g_value_get_uint(val));
585+ } else if (tagname == GST_TAG_ALBUM_VOLUME_NUMBER) {
586+ mfb->setDiscNumber(g_value_get_uint(val));
587+ }
588+ }
589+
590+ }
591+}
592+
593+}
594+
595+namespace mediascanner {
596+
597+GStreamerExtractor::GStreamerExtractor(int seconds)
598+ : discoverer(nullptr, g_object_unref) {
599+ GError *error = nullptr;
600+
601+ discoverer.reset(gst_discoverer_new(GST_SECOND * seconds, &error));
602+ if (not discoverer) {
603+ string errortxt(error->message);
604+ g_error_free(error);
605+
606+ string msg = "Failed to create discoverer: ";
607+ msg += errortxt;
608+ throw runtime_error(msg);
609+ }
610+ if (error) {
611+ // Sometimes this is filled in even though no error happens.
612+ g_error_free(error);
613+ }
614+}
615+
616+GStreamerExtractor::~GStreamerExtractor() = default;
617+
618+void GStreamerExtractor::extract(const DetectedFile &d, MediaFileBuilder &mfb) {
619+ string uri = getUri(d.filename);
620+
621+ GError *error = nullptr;
622+ unique_ptr<GstDiscovererInfo, void(*)(void *)> info(
623+ gst_discoverer_discover_uri(discoverer.get(), uri.c_str(), &error),
624+ g_object_unref);
625+ if (info.get() == nullptr) {
626+ string errortxt(error->message);
627+ g_error_free(error);
628+
629+ string msg = "Discovery of file ";
630+ msg += d.filename;
631+ msg += " failed: ";
632+ msg += errortxt;
633+ throw runtime_error(msg);
634+ }
635+ if (error) {
636+ // Sometimes this gets filled in even if no error actually occurs.
637+ g_error_free(error);
638+ error = nullptr;
639+ }
640+
641+ if (gst_discoverer_info_get_result(info.get()) != GST_DISCOVERER_OK) {
642+ throw runtime_error("Unable to discover file " + d.filename);
643+ }
644+
645+ const GstTagList *tags = gst_discoverer_info_get_tags(info.get());
646+ if (tags != nullptr) {
647+ gst_tag_list_foreach(tags, extract_tag_info, &mfb);
648+ }
649+ mfb.setDuration(static_cast<int>(
650+ gst_discoverer_info_get_duration(info.get())/GST_SECOND));
651+
652+ /* Check for video specific information */
653+ unique_ptr<GList, void(*)(GList*)> streams(
654+ gst_discoverer_info_get_stream_list(info.get()),
655+ gst_discoverer_stream_info_list_free);
656+ for (const GList *l = streams.get(); l != nullptr; l = l->next) {
657+ auto stream_info = static_cast<GstDiscovererStreamInfo*>(l->data);
658+
659+ if (GST_IS_DISCOVERER_VIDEO_INFO(stream_info)) {
660+ mfb.setWidth(gst_discoverer_video_info_get_width(
661+ GST_DISCOVERER_VIDEO_INFO(stream_info)));
662+ mfb.setHeight(gst_discoverer_video_info_get_height(
663+ GST_DISCOVERER_VIDEO_INFO(stream_info)));
664+ break;
665+ }
666+ }
667+}
668+
669+}
670
671=== added file 'src/extractor/GStreamerExtractor.hh'
672--- src/extractor/GStreamerExtractor.hh 1970-01-01 00:00:00 +0000
673+++ src/extractor/GStreamerExtractor.hh 2016-02-24 05:55:29 +0000
674@@ -0,0 +1,48 @@
675+/*
676+ * Copyright (C) 2013-2014 Canonical, Ltd.
677+ *
678+ * Authors:
679+ * Jussi Pakkanen <jussi.pakkanen@canonical.com>
680+ * James Henstridge <james.henstridge@canonical.com>
681+ *
682+ * This library is free software; you can redistribute it and/or modify it under
683+ * the terms of version 3 of the GNU General Public License as published
684+ * by the Free Software Foundation.
685+ *
686+ * This library is distributed in the hope that it will be useful, but WITHOUT
687+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
688+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
689+ * details.
690+ *
691+ * You should have received a copy of the GNU General Public License
692+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
693+ */
694+
695+#ifndef EXTRACTOR_GSTREAMEREXTRACTOR_H
696+#define EXTRACTOR_GSTREAMEREXTRACTOR_H
697+
698+#include <memory>
699+
700+typedef struct _GstDiscoverer GstDiscoverer;
701+
702+namespace mediascanner {
703+
704+class MediaFileBuilder;
705+struct DetectedFile;
706+
707+class GStreamerExtractor final {
708+public:
709+ explicit GStreamerExtractor(int seconds);
710+ ~GStreamerExtractor();
711+ GStreamerExtractor(const GStreamerExtractor&) = delete;
712+ GStreamerExtractor& operator=(GStreamerExtractor &o) = delete;
713+
714+ void extract(const DetectedFile &d, MediaFileBuilder &builder);
715+
716+private:
717+ std::unique_ptr<GstDiscoverer, void(*)(void*)> discoverer;
718+};
719+
720+}
721+
722+#endif
723
724=== added file 'src/extractor/ImageExtractor.cc'
725--- src/extractor/ImageExtractor.cc 1970-01-01 00:00:00 +0000
726+++ src/extractor/ImageExtractor.cc 2016-02-24 05:55:29 +0000
727@@ -0,0 +1,241 @@
728+/*
729+ * Copyright (C) 2013-2014 Canonical, Ltd.
730+ *
731+ * Authors:
732+ * Jussi Pakkanen <jussi.pakkanen@canonical.com>
733+ * James Henstridge <james.henstridge@canonical.com>
734+ *
735+ * This library is free software; you can redistribute it and/or modify it under
736+ * the terms of version 3 of the GNU General Public License as published
737+ * by the Free Software Foundation.
738+ *
739+ * This library is distributed in the hope that it will be useful, but WITHOUT
740+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
741+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
742+ * details.
743+ *
744+ * You should have received a copy of the GNU General Public License
745+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
746+ */
747+
748+#include "ImageExtractor.hh"
749+#include "DetectedFile.hh"
750+#include "../mediascanner/MediaFile.hh"
751+#include "../mediascanner/MediaFileBuilder.hh"
752+
753+#include <exif-loader.h>
754+#include <gdk-pixbuf/gdk-pixbuf.h>
755+
756+#include <ctime>
757+#include <memory>
758+#include <string>
759+#include <stdexcept>
760+#include <vector>
761+
762+#include <unistd.h>
763+
764+using namespace std;
765+using mediascanner::MediaFileBuilder;
766+
767+namespace {
768+const char exif_date_template[] = "%Y:%m:%d %H:%M:%S";
769+const char iso8601_date_format[] = "%Y-%m-%dT%H:%M:%S";
770+const char iso8601_date_with_zone_format[] = "%Y-%m-%dT%H:%M:%S%z";
771+
772+void parse_exif_date(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
773+ static const std::vector<ExifTag> date_tags{
774+ EXIF_TAG_DATE_TIME,
775+ EXIF_TAG_DATE_TIME_ORIGINAL,
776+ EXIF_TAG_DATE_TIME_DIGITIZED
777+ };
778+ struct tm timeinfo;
779+ bool have_date = false;
780+
781+ for (ExifTag tag : date_tags) {
782+ ExifEntry *ent = exif_data_get_entry(data, tag);
783+ if (ent == nullptr) {
784+ continue;
785+ }
786+ if (strptime((const char*)ent->data, exif_date_template, &timeinfo) != nullptr) {
787+ have_date = true;
788+ break;
789+ }
790+ }
791+ if (!have_date) {
792+ return;
793+ }
794+
795+ char buf[100];
796+ ExifEntry *ent = exif_data_get_entry(data, EXIF_TAG_TIME_ZONE_OFFSET);
797+ if (ent) {
798+ timeinfo.tm_gmtoff = (int)exif_get_sshort(ent->data, order) * 3600;
799+
800+ if (strftime(buf, sizeof(buf), iso8601_date_with_zone_format, &timeinfo) != 0) {
801+ mfb.setDate(buf);
802+ }
803+ } else {
804+ /* No time zone info */
805+ if (strftime(buf, sizeof(buf), iso8601_date_format, &timeinfo) != 0) {
806+ mfb.setDate(buf);
807+ }
808+ }
809+}
810+
811+int get_exif_int(ExifEntry *ent, ExifByteOrder order) {
812+ switch (ent->format) {
813+ case EXIF_FORMAT_BYTE:
814+ return (unsigned char)ent->data[0];
815+ case EXIF_FORMAT_SHORT:
816+ return exif_get_short(ent->data, order);
817+ case EXIF_FORMAT_LONG:
818+ return exif_get_long(ent->data, order);
819+ case EXIF_FORMAT_SBYTE:
820+ return (signed char)ent->data[0];
821+ case EXIF_FORMAT_SSHORT:
822+ return exif_get_sshort(ent->data, order);
823+ case EXIF_FORMAT_SLONG:
824+ return exif_get_slong(ent->data, order);
825+ default:
826+ break;
827+ }
828+ return 0;
829+}
830+
831+void parse_exif_dimensions(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
832+ ExifEntry *w_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_X_DIMENSION);
833+ ExifEntry *h_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_Y_DIMENSION);
834+ ExifEntry *o_ent = exif_data_get_entry(data, EXIF_TAG_ORIENTATION);
835+
836+ if (!w_ent || !h_ent) {
837+ return;
838+ }
839+ int width = get_exif_int(w_ent, order);
840+ int height = get_exif_int(h_ent, order);
841+
842+ // Optionally swap height and width depending on orientation
843+ if (o_ent) {
844+ int tmp;
845+
846+ // exif_data_fix() has ensured this is a short.
847+ switch (exif_get_short(o_ent->data, order)) {
848+ case 5: // Mirror horizontal and rotate 270 CW
849+ case 6: // Rotate 90 CW
850+ case 7: // Mirror horizontal and rotate 90 CW
851+ case 8: // Rotate 270 CW
852+ tmp = width;
853+ width = height;
854+ height = tmp;
855+ break;
856+ default:
857+ break;
858+ }
859+ }
860+ mfb.setWidth(width);
861+ mfb.setHeight(height);
862+}
863+
864+bool rational_to_degrees(ExifEntry *ent, ExifByteOrder order, double *out) {
865+ if (ent->format != EXIF_FORMAT_RATIONAL) {
866+ return false;
867+ }
868+
869+ ExifRational r = exif_get_rational(ent->data, order);
870+ *out = ((double) r.numerator) / r.denominator;
871+
872+ // Minutes
873+ if (ent->components >= 2) {
874+ r = exif_get_rational(ent->data + exif_format_get_size(EXIF_FORMAT_RATIONAL), order);
875+ *out += ((double) r.numerator) / r.denominator / 60;
876+ }
877+ // Seconds
878+ if (ent->components >= 3) {
879+ r = exif_get_rational(ent->data + 2 * exif_format_get_size(EXIF_FORMAT_RATIONAL), order);
880+ *out += ((double) r.numerator) / r.denominator / 3600;
881+ }
882+ return true;
883+}
884+
885+void parse_exif_location(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) {
886+ ExifContent *ifd = data->ifd[EXIF_IFD_GPS];
887+ ExifEntry *lat_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE);
888+ ExifEntry *latref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE_REF);
889+ ExifEntry *long_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE);
890+ ExifEntry *longref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE_REF);
891+
892+ if (!lat_ent || !long_ent) {
893+ return;
894+ }
895+
896+ double latitude, longitude;
897+ if (!rational_to_degrees(lat_ent, order, &latitude)) {
898+ return;
899+ }
900+ if (!rational_to_degrees(long_ent, order, &longitude)) {
901+ return;
902+ }
903+ if (latref_ent && latref_ent->data[0] == 'S') {
904+ latitude = -latitude;
905+ }
906+ if (longref_ent && longref_ent->data[0] == 'W') {
907+ longitude = -longitude;
908+ }
909+ mfb.setLatitude(latitude);
910+ mfb.setLongitude(longitude);
911+}
912+
913+}
914+
915+namespace mediascanner {
916+
917+bool ImageExtractor::extract_exif(const DetectedFile &d, MediaFileBuilder &mfb) {
918+ std::unique_ptr<ExifLoader, void(*)(ExifLoader*)> loader(
919+ exif_loader_new(), exif_loader_unref);
920+ exif_loader_write_file(loader.get(), d.filename.c_str());
921+
922+ std::unique_ptr<ExifData, void(*)(ExifData*)> data(
923+ exif_loader_get_data(loader.get()), exif_data_unref);
924+ loader.reset();
925+
926+ if (!data) {
927+ return false;
928+ }
929+ exif_data_fix(data.get());
930+ ExifByteOrder order = exif_data_get_byte_order(data.get());
931+
932+ parse_exif_date(data.get(), order, mfb);
933+ parse_exif_dimensions(data.get(), order, mfb);
934+ parse_exif_location(data.get(), order, mfb);
935+ return true;
936+}
937+
938+void ImageExtractor::extract_pixbuf(const DetectedFile &d, MediaFileBuilder &builder) {
939+ gint width, height;
940+
941+ if(!gdk_pixbuf_get_file_info(d.filename.c_str(), &width, &height)) {
942+ string msg("Could not determine resolution of ");
943+ msg += d.filename;
944+ msg += ".";
945+ throw runtime_error(msg);
946+ }
947+ builder.setWidth(width);
948+ builder.setHeight(height);
949+
950+ if (d.mtime != 0) {
951+ auto t = static_cast<time_t>(d.mtime);
952+ char buf[1024];
953+ struct tm ptm;
954+ localtime_r(&t, &ptm);
955+ if (strftime(buf, sizeof(buf), iso8601_date_format, &ptm) != 0) {
956+ builder.setDate(buf);
957+ }
958+ }
959+}
960+
961+void ImageExtractor::extract(const DetectedFile &d, MediaFileBuilder &builder) {
962+ if (!extract_exif(d, builder)) {
963+ extract_pixbuf(d, builder);
964+ }
965+ return;
966+}
967+
968+}
969
970=== added file 'src/extractor/ImageExtractor.hh'
971--- src/extractor/ImageExtractor.hh 1970-01-01 00:00:00 +0000
972+++ src/extractor/ImageExtractor.hh 2016-02-24 05:55:29 +0000
973@@ -0,0 +1,47 @@
974+/*
975+ * Copyright (C) 2013-2014 Canonical, Ltd.
976+ *
977+ * Authors:
978+ * Jussi Pakkanen <jussi.pakkanen@canonical.com>
979+ * James Henstridge <james.henstridge@canonical.com>
980+ *
981+ * This library is free software; you can redistribute it and/or modify it under
982+ * the terms of version 3 of the GNU General Public License as published
983+ * by the Free Software Foundation.
984+ *
985+ * This library is distributed in the hope that it will be useful, but WITHOUT
986+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
987+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
988+ * details.
989+ *
990+ * You should have received a copy of the GNU General Public License
991+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
992+ */
993+
994+#ifndef EXTRACTOR_IMAGEEXTRACTOR_H
995+#define EXTRACTOR_IMAGEEXTRACTOR_H
996+
997+#include <string>
998+
999+namespace mediascanner {
1000+
1001+class MediaFileBuilder;
1002+struct DetectedFile;
1003+
1004+class ImageExtractor final {
1005+public:
1006+ ImageExtractor() = default;
1007+ ~ImageExtractor() = default;
1008+ ImageExtractor(const ImageExtractor&) = delete;
1009+ ImageExtractor& operator=(ImageExtractor &o) = delete;
1010+
1011+ void extract(const DetectedFile &d, MediaFileBuilder &builder);
1012+
1013+private:
1014+ bool extract_exif(const DetectedFile &d, MediaFileBuilder &builder);
1015+ void extract_pixbuf(const DetectedFile &d, MediaFileBuilder &builder);
1016+};
1017+
1018+}
1019+
1020+#endif
1021
1022=== added file 'src/extractor/TaglibExtractor.cc'
1023--- src/extractor/TaglibExtractor.cc 1970-01-01 00:00:00 +0000
1024+++ src/extractor/TaglibExtractor.cc 2016-02-24 05:55:29 +0000
1025@@ -0,0 +1,295 @@
1026+/*
1027+ * Copyright (C) 2016 Canonical, Ltd.
1028+ *
1029+ * Authors:
1030+ * James Henstridge <james.henstridge@canonical.com>
1031+ *
1032+ * This library is free software; you can redistribute it and/or modify it under
1033+ * the terms of version 3 of the GNU General Public License as published
1034+ * by the Free Software Foundation.
1035+ *
1036+ * This library is distributed in the hope that it will be useful, but WITHOUT
1037+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1038+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1039+ * details.
1040+ *
1041+ * You should have received a copy of the GNU General Public License
1042+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1043+ */
1044+
1045+#include "TaglibExtractor.hh"
1046+#include "DetectedFile.hh"
1047+#include "../mediascanner/MediaFile.hh"
1048+#include "../mediascanner/MediaFileBuilder.hh"
1049+
1050+#include <glib.h>
1051+#include <gio/gio.h>
1052+#include <gst/gstdatetime.h>
1053+
1054+#include <taglib/flacfile.h>
1055+#include <taglib/id3v1tag.h>
1056+#include <taglib/id3v2framefactory.h>
1057+#include <taglib/id3v2tag.h>
1058+#include <taglib/mp4file.h>
1059+#include <taglib/mpegfile.h>
1060+#include <taglib/oggflacfile.h>
1061+#include <taglib/opusfile.h>
1062+#include <taglib/tfilestream.h>
1063+#include <taglib/unknownframe.h>
1064+#include <taglib/vorbisfile.h>
1065+
1066+#include <cassert>
1067+#include <cstdio>
1068+#include <memory>
1069+#include <string>
1070+#include <stdexcept>
1071+
1072+using namespace std;
1073+using mediascanner::MediaFileBuilder;
1074+
1075+namespace {
1076+
1077+string get_content_type(const string &filename) {
1078+ unique_ptr<GFile, decltype(&g_object_unref)> file(
1079+ g_file_new_for_path(filename.c_str()), g_object_unref);
1080+ assert(file);
1081+
1082+ GError *error = nullptr;
1083+ unique_ptr<GFileInfo, decltype(&g_object_unref)> info(
1084+ g_file_query_info(
1085+ file.get(),
1086+ G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
1087+ G_FILE_QUERY_INFO_NONE, nullptr, &error),
1088+ g_object_unref);
1089+ if (!info) {
1090+ fprintf(stderr, "Failed to detect content type of '%s': %s\n",
1091+ filename.c_str(), error->message);
1092+ g_error_free(error);
1093+ return string();
1094+ }
1095+ return g_file_info_get_content_type(info.get());
1096+}
1097+
1098+typedef unique_ptr<GstDateTime, decltype(&gst_date_time_unref)> DatePtr;
1099+
1100+DatePtr parse_iso_date(const string &date_string) {
1101+ return DatePtr(
1102+ gst_date_time_new_from_iso8601_string(date_string.c_str()),
1103+ gst_date_time_unref);
1104+}
1105+
1106+string format_iso_date(const DatePtr &dt) {
1107+ if (!dt) {
1108+ return string();
1109+ }
1110+ char *iso = gst_date_time_to_iso8601_string(dt.get());
1111+ if (!iso) {
1112+ return string();
1113+ }
1114+ string result(iso);
1115+ g_free(iso);
1116+ return result;
1117+}
1118+
1119+string check_date(const string &date_string) {
1120+ // Parse and reserialise the date using GstDateTime to check its
1121+ // consistency.
1122+ return format_iso_date(parse_iso_date(date_string));
1123+}
1124+
1125+void parse_common(const TagLib::File &file, MediaFileBuilder &builder) {
1126+ TagLib::Tag *tag = file.tag();
1127+
1128+ const TagLib::AudioProperties *audio_props = file.audioProperties();
1129+ if (audio_props) {
1130+ builder.setDuration(audio_props->length());
1131+ }
1132+
1133+ TagLib::String s = tag->title();
1134+ if (!s.isEmpty()) {
1135+ builder.setTitle(s.to8Bit(true));
1136+ }
1137+ s = tag->artist();
1138+ if (!s.isEmpty()) {
1139+ builder.setAuthor(s.to8Bit(true));
1140+ }
1141+ s = tag->album();
1142+ if (!s.isEmpty()) {
1143+ builder.setAlbum(s.to8Bit(true));
1144+ }
1145+ s = tag->genre();
1146+ if (!s.isEmpty()) {
1147+ builder.setGenre(s.to8Bit(true));
1148+ }
1149+ unsigned int year = tag->year();
1150+ if (year > 0 && year <= 9999) {
1151+ DatePtr dt(gst_date_time_new_y(year), gst_date_time_unref);
1152+ builder.setDate(format_iso_date(dt));
1153+ }
1154+
1155+ unsigned int track = tag->track();
1156+ if (track != 0) {
1157+ builder.setTrackNumber(track);
1158+ }
1159+}
1160+
1161+void parse_xiph_comment(const TagLib::Ogg::XiphComment *tag, MediaFileBuilder &builder) {
1162+ const auto& fields = tag->fieldListMap();
1163+
1164+ if (!fields["DATE"].isEmpty()) {
1165+ builder.setDate(check_date(fields["DATE"].front().to8Bit(true)));
1166+ }
1167+
1168+ if (!fields["ALBUMARTIST"].isEmpty()) {
1169+ builder.setAlbumArtist(fields["ALBUMARTIST"].front().to8Bit(true));
1170+ }
1171+
1172+ if (!fields["DISCNUMBER"].isEmpty()) {
1173+ builder.setDiscNumber(fields["DISCNUMBER"].front().toInt());
1174+ }
1175+
1176+ if (!fields["COVERART"].isEmpty() || !fields["METADATA_BLOCK_PICTURE"].isEmpty()) {
1177+ builder.setHasThumbnail(true);
1178+ }
1179+}
1180+
1181+void parse_id3v2(const TagLib::ID3v2::Tag *tag, MediaFileBuilder &builder) {
1182+ const auto& frames = tag->frameListMap();
1183+
1184+ if (!frames["TDRC"].isEmpty()) {
1185+ DatePtr dt = parse_iso_date(frames["TDRC"].front()->toString().to8Bit(true));
1186+ // Taglib automatically renames the old TYER frame to TDRC,
1187+ // but doesn't migrate over the day and month from TDAT.
1188+ if (!gst_date_time_has_month(dt.get()) && !frames["TDAT"].isEmpty()) {
1189+ const TagLib::ID3v2::Frame *frame = frames["TDAT"].front();
1190+ // TagLib converts TDAT to an "UnknownFrame" type frame,
1191+ // since it believes it should be ignored. Create a text
1192+ // frame so we can get at the data.
1193+ TagLib::String data;
1194+ auto *unknown = dynamic_cast<const TagLib::ID3v2::UnknownFrame*>(frame);
1195+ if (unknown) {
1196+ // Text information frames consist of one byte for the
1197+ // encoding, followed the string data.
1198+ auto encoding = static_cast<TagLib::String::Type>(unknown->data()[0]);
1199+ data = TagLib::String(unknown->data().mid(1), encoding);
1200+ } else {
1201+ data = frame->toString();
1202+ }
1203+ int ddmm = data.toInt();
1204+ int day = ddmm / 100;
1205+ int month = ddmm % 100;
1206+ dt.reset(gst_date_time_new_ymd(
1207+ gst_date_time_get_year(dt.get()),
1208+ g_date_valid_month(static_cast<GDateMonth>(month)) ? month : -1,
1209+ g_date_valid_day(day) ? day : -1));
1210+ }
1211+ builder.setDate(format_iso_date(dt));
1212+ }
1213+
1214+ if (!frames["TPE2"].isEmpty()) {
1215+ builder.setAlbumArtist(frames["TPE2"].front()->toString().to8Bit(true));
1216+ }
1217+
1218+ if (!frames["TPOS"].isEmpty()) {
1219+ builder.setDiscNumber(frames["TPOS"].front()->toString().toInt());
1220+ }
1221+
1222+ if (!frames["APIC"].isEmpty()) {
1223+ builder.setHasThumbnail(true);
1224+ }
1225+}
1226+
1227+void parse_mp4(const TagLib::MP4::Tag *tag, MediaFileBuilder &builder) {
1228+ const auto& items = const_cast<TagLib::MP4::Tag*>(tag)->itemListMap();
1229+
1230+ if (items.contains("\251day")) {
1231+ builder.setDate(check_date(items["\251day"].toStringList().front().to8Bit(true)));
1232+ }
1233+
1234+ if (items.contains("aART")) {
1235+ builder.setAlbumArtist(items["aART"].toStringList().front().to8Bit(true));
1236+ }
1237+
1238+ if (items.contains("disk")) {
1239+ builder.setDiscNumber(items["disk"].toIntPair().first);
1240+ }
1241+
1242+ if (items.contains("covr")) {
1243+ builder.setHasThumbnail(true);
1244+ }
1245+}
1246+
1247+}
1248+
1249+namespace mediascanner {
1250+
1251+bool TaglibExtractor::extract(const DetectedFile &d, MediaFileBuilder &builder) {
1252+ string content_type = get_content_type(d.filename);
1253+ if (content_type.empty()) {
1254+ return false;
1255+ }
1256+
1257+ unique_ptr<TagLib::FileStream> fs(new TagLib::FileStream(d.filename.c_str(), true));
1258+ if (!fs->isOpen()) {
1259+ return false;
1260+ }
1261+
1262+ if (content_type == "audio/x-vorbis+ogg") {
1263+ TagLib::Ogg::Vorbis::File file(fs.get());
1264+ if (!file.isValid()) {
1265+ return false;
1266+ }
1267+ parse_common(file, builder);
1268+ parse_xiph_comment(file.tag(), builder);
1269+ } else if (content_type == "audio/x-opus+ogg") {
1270+ TagLib::Ogg::Opus::File file(fs.get());
1271+ if (!file.isValid()) {
1272+ return false;
1273+ }
1274+ parse_common(file, builder);
1275+ parse_xiph_comment(file.tag(), builder);
1276+ } else if (content_type == "audio/x-flac+ogg") {
1277+ TagLib::Ogg::FLAC::File file(fs.get());
1278+ if (!file.isValid()) {
1279+ return false;
1280+ }
1281+ parse_common(file, builder);
1282+ parse_xiph_comment(file.tag(), builder);
1283+ } else if (content_type == "audio/flac") {
1284+ TagLib::FLAC::File file(fs.get(), TagLib::ID3v2::FrameFactory::instance());
1285+ if (!file.isValid()) {
1286+ return false;
1287+ }
1288+ parse_common(file, builder);
1289+ if (file.hasID3v2Tag()) {
1290+ parse_id3v2(file.ID3v2Tag(), builder);
1291+ }
1292+ if (file.hasXiphComment()) {
1293+ parse_xiph_comment(file.xiphComment(), builder);
1294+ }
1295+ if (!file.pictureList().isEmpty()) {
1296+ builder.setHasThumbnail(true);
1297+ }
1298+ } else if (content_type == "audio/mpeg") {
1299+ TagLib::MPEG::File file(fs.get(), TagLib::ID3v2::FrameFactory::instance());
1300+ if (!file.isValid()) {
1301+ return false;
1302+ }
1303+ parse_common(file, builder);
1304+ if (file.hasID3v2Tag()) {
1305+ parse_id3v2(file.ID3v2Tag(), builder);
1306+ }
1307+ } else if (content_type == "audio/mp4") {
1308+ TagLib::MP4::File file(fs.get());
1309+ if (!file.isValid()) {
1310+ return false;
1311+ }
1312+ parse_common(file, builder);
1313+ parse_mp4(file.tag(), builder);
1314+ } else {
1315+ return false;
1316+ }
1317+ return true;
1318+}
1319+
1320+}
1321
1322=== added file 'src/extractor/TaglibExtractor.hh'
1323--- src/extractor/TaglibExtractor.hh 1970-01-01 00:00:00 +0000
1324+++ src/extractor/TaglibExtractor.hh 2016-02-24 05:55:29 +0000
1325@@ -0,0 +1,42 @@
1326+/*
1327+ * Copyright (C) 2016 Canonical, Ltd.
1328+ *
1329+ * Authors:
1330+ * James Henstridge <james.henstridge@canonical.com>
1331+ *
1332+ * This library is free software; you can redistribute it and/or modify it under
1333+ * the terms of version 3 of the GNU General Public License as published
1334+ * by the Free Software Foundation.
1335+ *
1336+ * This library is distributed in the hope that it will be useful, but WITHOUT
1337+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1338+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1339+ * details.
1340+ *
1341+ * You should have received a copy of the GNU General Public License
1342+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1343+ */
1344+
1345+#ifndef EXTRACTOR_TAGLIBEXTRACTOR_H
1346+#define EXTRACTOR_TAGLIBEXTRACTOR_H
1347+
1348+#include <string>
1349+
1350+namespace mediascanner {
1351+
1352+class MediaFileBuilder;
1353+struct DetectedFile;
1354+
1355+class TaglibExtractor final {
1356+public:
1357+ TaglibExtractor() = default;
1358+ ~TaglibExtractor() = default;
1359+ TaglibExtractor(const TaglibExtractor&) = delete;
1360+ TaglibExtractor& operator=(TaglibExtractor &o) = delete;
1361+
1362+ bool extract(const DetectedFile &d, MediaFileBuilder &builder);
1363+};
1364+
1365+}
1366+
1367+#endif
1368
1369=== modified file 'test/CMakeLists.txt'
1370--- test/CMakeLists.txt 2015-12-11 06:34:01 +0000
1371+++ test/CMakeLists.txt 2016-02-24 05:55:29 +0000
1372@@ -47,7 +47,7 @@
1373 add_test(test_extractorbackend test_extractorbackend)
1374
1375 add_executable(test_metadataextractor test_metadataextractor.cc)
1376-target_link_libraries(test_metadataextractor extractor-client gtest ${GST_LDFLAGS})
1377+target_link_libraries(test_metadataextractor extractor-client gtest)
1378 add_test(test_metadataextractor test_metadataextractor)
1379 # The gvfs modules interfere with the private D-Bus test fixtures
1380 set_tests_properties(test_metadataextractor PROPERTIES
1381
1382=== modified file 'test/test_extractorbackend.cc'
1383--- test/test_extractorbackend.cc 2015-10-22 08:40:58 +0000
1384+++ test/test_extractorbackend.cc 2016-02-24 05:55:29 +0000
1385@@ -19,8 +19,11 @@
1386 */
1387
1388 #include <mediascanner/MediaFile.hh>
1389+#include <mediascanner/MediaFileBuilder.hh>
1390 #include <extractor/DetectedFile.hh>
1391 #include <extractor/ExtractorBackend.hh>
1392+#include <extractor/GStreamerExtractor.hh>
1393+#include <extractor/TaglibExtractor.hh>
1394
1395 #include "test_config.h"
1396
1397@@ -33,6 +36,57 @@
1398 using namespace std;
1399 using namespace mediascanner;
1400
1401+namespace {
1402+
1403+bool supports_decoder(const std::string& format)
1404+{
1405+ typedef std::unique_ptr<GstCaps, decltype(&gst_caps_unref)> CapsPtr;
1406+ static std::vector<CapsPtr> formats;
1407+
1408+ if (formats.empty())
1409+ {
1410+ std::unique_ptr<GList, decltype(&gst_plugin_feature_list_free)> decoders(
1411+ gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DECODER, GST_RANK_NONE),
1412+ gst_plugin_feature_list_free);
1413+ for (const GList* l = decoders.get(); l != nullptr; l = l->next)
1414+ {
1415+ const auto factory = static_cast<GstElementFactory*>(l->data);
1416+
1417+ const GList* templates = gst_element_factory_get_static_pad_templates(factory);
1418+ for (const GList* l = templates; l != nullptr; l = l->next)
1419+ {
1420+ const auto t = static_cast<GstStaticPadTemplate*>(l->data);
1421+ if (t->direction != GST_PAD_SINK)
1422+ {
1423+ continue;
1424+ }
1425+ CapsPtr caps(gst_static_caps_get(&t->static_caps),
1426+ gst_caps_unref);
1427+ if (gst_caps_is_any(caps.get())) {
1428+ continue;
1429+ }
1430+ formats.emplace_back(std::move(caps));
1431+ }
1432+ }
1433+ }
1434+
1435+ char *end = nullptr;
1436+ GstStructure *structure = gst_structure_from_string(format.c_str(), &end);
1437+ assert(structure != nullptr);
1438+ assert(end == format.c_str() + format.size());
1439+ // GstCaps adopts the GstStructure
1440+ CapsPtr caps(gst_caps_new_full(structure, nullptr), gst_caps_unref);
1441+
1442+ for (const auto &other : formats) {
1443+ if (gst_caps_is_always_compatible(caps.get(), other.get())) {
1444+ return true;
1445+ }
1446+ }
1447+ return false;
1448+}
1449+
1450+}
1451+
1452 class ExtractorBackendTest : public ::testing::Test {
1453 protected:
1454 ExtractorBackendTest() {
1455@@ -53,7 +107,7 @@
1456 ExtractorBackend extractor;
1457 }
1458
1459-TEST_F(ExtractorBackendTest, extract_audio) {
1460+TEST_F(ExtractorBackendTest, extract_vorbis) {
1461 ExtractorBackend e;
1462 string testfile = SOURCE_DIR "/media/testfile.ogg";
1463 DetectedFile df(testfile, "etag", "audio/ogg", 42, AudioMedia);
1464@@ -68,6 +122,38 @@
1465 EXPECT_EQ(file.getDuration(), 5);
1466 }
1467
1468+TEST_F(ExtractorBackendTest, extract_mp3) {
1469+ ExtractorBackend e;
1470+ string testfile = SOURCE_DIR "/media/testfile.mp3";
1471+ DetectedFile df(testfile, "etag", "audio/mpeg", 42, AudioMedia);
1472+ MediaFile file = e.extract(df);
1473+
1474+ EXPECT_EQ(file.getType(), AudioMedia);
1475+ EXPECT_EQ(file.getTitle(), "track1");
1476+ EXPECT_EQ(file.getAuthor(), "artist1");
1477+ EXPECT_EQ(file.getAlbum(), "album1");
1478+ EXPECT_EQ(file.getGenre(), "Hip-Hop");
1479+ EXPECT_EQ(file.getDate(), "2013-06-03");
1480+ EXPECT_EQ(file.getTrackNumber(), 1);
1481+ EXPECT_EQ(file.getDuration(), 1);
1482+}
1483+
1484+TEST_F(ExtractorBackendTest, extract_m4a) {
1485+ ExtractorBackend e;
1486+ string testfile = SOURCE_DIR "/media/testfile.m4a";
1487+ DetectedFile df(testfile, "etag", "audio/mpeg4", 42, AudioMedia);
1488+ MediaFile file = e.extract(df);
1489+
1490+ EXPECT_EQ(file.getType(), AudioMedia);
1491+ EXPECT_EQ(file.getTitle(), "Title");
1492+ EXPECT_EQ(file.getAuthor(), "Artist");
1493+ EXPECT_EQ(file.getAlbum(), "Album");
1494+ EXPECT_EQ(file.getGenre(), "Rock");
1495+ EXPECT_EQ(file.getDate(), "2015-10-07");
1496+ EXPECT_EQ(file.getTrackNumber(), 4);
1497+ EXPECT_EQ(file.getDuration(), 1);
1498+}
1499+
1500 TEST_F(ExtractorBackendTest, extract_video) {
1501 ExtractorBackend e;
1502
1503@@ -117,6 +203,56 @@
1504 EXPECT_DOUBLE_EQ(153.1727346, file.getLongitude());
1505 }
1506
1507+void compare_taglib_gst(const DetectedFile d) {
1508+ GStreamerExtractor gst(5);
1509+ MediaFileBuilder builder_gst(d.filename);
1510+ gst.extract(d, builder_gst);
1511+
1512+ TaglibExtractor taglib;
1513+ MediaFileBuilder builder_taglib(d.filename);
1514+ ASSERT_TRUE(taglib.extract(d, builder_taglib));
1515+
1516+ MediaFile media_gst(builder_gst);
1517+ MediaFile media_taglib(builder_taglib);
1518+ EXPECT_EQ(media_gst, media_taglib);
1519+
1520+ // And check individual keys to improve error handling:
1521+ EXPECT_EQ(media_gst.getTitle(), media_taglib.getTitle());
1522+ EXPECT_EQ(media_gst.getAuthor(), media_taglib.getAuthor());
1523+ EXPECT_EQ(media_gst.getAlbum(), media_taglib.getAlbum());
1524+ EXPECT_EQ(media_gst.getAlbumArtist(), media_taglib.getAlbumArtist());
1525+ EXPECT_EQ(media_gst.getDate(), media_taglib.getDate());
1526+ EXPECT_EQ(media_gst.getGenre(), media_taglib.getGenre());
1527+ EXPECT_EQ(media_gst.getDiscNumber(), media_taglib.getDiscNumber());
1528+ EXPECT_EQ(media_gst.getTrackNumber(), media_taglib.getTrackNumber());
1529+ EXPECT_EQ(media_gst.getDuration(), media_taglib.getDuration());
1530+ EXPECT_EQ(media_gst.getHasThumbnail(), media_taglib.getHasThumbnail());
1531+}
1532+
1533+TEST_F(ExtractorBackendTest, check_taglib_gst_vorbis) {
1534+ DetectedFile d(SOURCE_DIR "/media/testfile.ogg", "etag", "audio/ogg", 42, AudioMedia);
1535+ compare_taglib_gst(d);
1536+}
1537+
1538+TEST_F(ExtractorBackendTest, check_taglib_gst_mp3) {
1539+ if (!supports_decoder("audio/mpeg, mpegversion=(int)1, layer=(int)3")) {
1540+ printf("MP3 codec not supported\n");
1541+ return;
1542+ }
1543+ DetectedFile d(SOURCE_DIR "/media/testfile.mp3", "etag", "audio/mpeg", 42, AudioMedia);
1544+ compare_taglib_gst(d);
1545+}
1546+
1547+TEST_F(ExtractorBackendTest, check_taglib_gst_m4a) {
1548+ if (!supports_decoder("audio/mpeg, mpegversion=(int)4, stream-format=(string)raw")) {
1549+ printf("M4A codec not supported\n");
1550+ return;
1551+ }
1552+ DetectedFile d(SOURCE_DIR "/media/testfile.m4a", "etag", "audio/mp4", 42, AudioMedia);
1553+ compare_taglib_gst(d);
1554+}
1555+
1556+
1557 int main(int argc, char **argv) {
1558 gst_init(&argc, &argv);
1559 ::testing::InitGoogleTest(&argc, argv);
1560
1561=== modified file 'test/test_metadataextractor.cc'
1562--- test/test_metadataextractor.cc 2015-11-08 01:50:15 +0000
1563+++ test/test_metadataextractor.cc 2016-02-24 05:55:29 +0000
1564@@ -32,64 +32,12 @@
1565 #include <sys/stat.h>
1566 #include <sys/types.h>
1567
1568-#include <gst/gst.h>
1569 #include <gio/gio.h>
1570 #include <gtest/gtest.h>
1571
1572 using namespace std;
1573 using namespace mediascanner;
1574
1575-namespace {
1576-
1577-bool supports_decoder(const std::string& format)
1578-{
1579- typedef std::unique_ptr<GstCaps, decltype(&gst_caps_unref)> CapsPtr;
1580- static std::vector<CapsPtr> formats;
1581-
1582- if (formats.empty())
1583- {
1584- std::unique_ptr<GList, decltype(&gst_plugin_feature_list_free)> decoders(
1585- gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DECODER, GST_RANK_NONE),
1586- gst_plugin_feature_list_free);
1587- for (const GList* l = decoders.get(); l != nullptr; l = l->next)
1588- {
1589- const auto factory = static_cast<GstElementFactory*>(l->data);
1590-
1591- const GList* templates = gst_element_factory_get_static_pad_templates(factory);
1592- for (const GList* l = templates; l != nullptr; l = l->next)
1593- {
1594- const auto t = static_cast<GstStaticPadTemplate*>(l->data);
1595- if (t->direction != GST_PAD_SINK)
1596- {
1597- continue;
1598- }
1599- CapsPtr caps(gst_static_caps_get(&t->static_caps),
1600- gst_caps_unref);
1601- if (gst_caps_is_any(caps.get())) {
1602- continue;
1603- }
1604- formats.emplace_back(std::move(caps));
1605- }
1606- }
1607- }
1608-
1609- char *end = nullptr;
1610- GstStructure *structure = gst_structure_from_string(format.c_str(), &end);
1611- assert(structure != nullptr);
1612- assert(end == format.c_str() + format.size());
1613- // GstCaps adopts the GstStructure
1614- CapsPtr caps(gst_caps_new_full(structure, nullptr), gst_caps_unref);
1615-
1616- for (const auto &other : formats) {
1617- if (gst_caps_is_always_compatible(caps.get(), other.get())) {
1618- return true;
1619- }
1620- }
1621- return false;
1622-}
1623-
1624-}
1625-
1626 class MetadataExtractorTest : public ::testing::Test {
1627 protected:
1628 virtual void SetUp() override {
1629@@ -187,10 +135,6 @@
1630 }
1631
1632 TEST_F(MetadataExtractorTest, extract_mp3) {
1633- if (!supports_decoder("audio/mpeg, mpegversion=(int)1, layer=(int)3")) {
1634- printf("MP3 codec not supported\n");
1635- return;
1636- }
1637 MetadataExtractor e(session_bus());
1638 string testfile = SOURCE_DIR "/media/testfile.mp3";
1639 MediaFile file = e.extract(e.detect(testfile));
1640@@ -209,11 +153,6 @@
1641 }
1642
1643 TEST_F(MetadataExtractorTest, extract_m4a) {
1644- if (!supports_decoder("audio/mpeg, mpegversion=(int)4, stream-format=(string)raw")) {
1645- printf("M4A codec not supported\n");
1646- return;
1647- }
1648-
1649 MetadataExtractor e(session_bus());
1650 string testfile = SOURCE_DIR "/media/testfile.m4a";
1651 MediaFile file = e.extract(e.detect(testfile));
1652@@ -291,10 +230,6 @@
1653 }
1654
1655 TEST_F(MetadataExtractorTest, extract_mp3_bad_date) {
1656- if (!supports_decoder("audio/mpeg, mpegversion=(int)1, layer=(int)3")) {
1657- printf("MP3 codec not supported\n");
1658- return;
1659- }
1660 MetadataExtractor e(session_bus());
1661 string testfile = SOURCE_DIR "/media/baddate.mp3";
1662 MediaFile file = e.extract(e.detect(testfile));
1663@@ -371,7 +306,6 @@
1664 }
1665
1666 int main(int argc, char **argv) {
1667- gst_init(&argc, &argv);
1668 ::testing::InitGoogleTest(&argc, argv);
1669 return RUN_ALL_TESTS();
1670 }

Subscribers

People subscribed via source and target branches