Merge lp:~jamesh/mediascanner2/taglib-extractor into lp:mediascanner2
- taglib-extractor
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michi Henning (community) | Approve | ||
PS Jenkins bot (community) | continuous-integration | Needs Fixing | |
Review via email:
|
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
55 ../sysdeps/
[Current thread is 1 (Thread 0x7f701b1d6800 (LWP 6972))]
(gdb) bt
#0 0x00007f7018eff227 in __GI_raise (sig=sig@entry=6) at ../sysdeps/
#1 0x00007f7018f00e8a in __GI_abort () at abort.c:89
#2 0x00000000004285cd in (anonymous namespace)
filename=
etag=0x1129000 "1455579826:
user_
#3 0x00007f7016ee1e40 in ffi_call_unix64 () from /usr/lib/
#4 0x00007f7016ee18ab in ffi_call () from /usr/lib/
#5 0x00007f7019e217c9 in g_cclosure_
#6 0x00007f7019e20fa5 in g_closure_invoke () from /usr/lib/
#7 0x00007f7019e32ff1 in ?? () from /usr/lib/
#8 0x00007f7019e3ad71 in g_signal_emitv () from /usr/lib/
#9 0x0000000000436eec in _ms_extractor_
sender=
interface_
method_
at /home/michi/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:326
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:327
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:328
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:328
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michi Henning (michihenning) wrote : | # |
Sweet, thank you!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michi Henning (michihenning) wrote : | # |
Cool, thanks!
Preview Diff
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 | } |
FAILED: Continuous integration, rev:325 /code.launchpad .net/~jamesh/ mediascanner2/ taglib- extractor/ +merge/ 285834/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http:// jenkins. qa.ubuntu. com/job/ mediascanner2- ci/206/ jenkins. qa.ubuntu. com/job/ mediascanner2- vivid-amd64- ci/43 jenkins. qa.ubuntu. com/job/ mediascanner2- vivid-armhf- ci/43 jenkins. qa.ubuntu. com/job/ mediascanner2- vivid-armhf- ci/43/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ mediascanner2- vivid-i386- ci/43 jenkins. qa.ubuntu. com/job/ mediascanner2- wily-amd64- ci/18 jenkins. qa.ubuntu. com/job/ mediascanner2- wily-armhf- ci/18 jenkins. qa.ubuntu. com/job/ mediascanner2- wily-armhf- ci/18/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ mediascanner2- wily-i386- ci/18
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/mediascanne r2-ci/206/ rebuild
http://