1 /*
2 * Copyright (C) 2011, ARQ Media <sam.thursfield@codethink.co.uk>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 *
19 * Author: Sam Thursfield <sam.thursfield@codethink.co.uk>
20 */
21
22 #include "config-miners.h"
23
24 #include <stdlib.h>
25 #include <string.h>
26
27 #include <glib.h>
28 #include <gio/gio.h>
29 #include <gst/gst.h>
30 #include <gst/tag/tag.h>
31
32 #if defined(HAVE_LIBCUE2)
33 #include <libcue.h>
34 #elif defined(HAVE_LIBCUE)
35 #include <libcue/libcue.h>
36 #endif
37
38 #include <libtracker-miners-common/tracker-file-utils.h>
39
40 #include "tracker-cue-sheet.h"
41
42 TrackerToc *
tracker_toc_new(void)43 tracker_toc_new (void)
44 {
45 TrackerToc *toc;
46
47 toc = g_slice_new (TrackerToc);
48 toc->tag_list = gst_tag_list_new_empty ();
49 toc->entry_list = NULL;
50
51 return toc;
52 }
53
54 void
tracker_toc_free(TrackerToc * toc)55 tracker_toc_free (TrackerToc *toc)
56 {
57 TrackerTocEntry *entry;
58 GList *n;
59
60 if (!toc) {
61 return;
62 }
63
64 for (n = toc->entry_list; n != NULL; n = n->next) {
65 entry = n->data;
66 gst_tag_list_free (entry->tag_list);
67 g_slice_free (TrackerTocEntry, entry);
68 }
69
70 gst_tag_list_free (toc->tag_list);
71 g_list_free (toc->entry_list);
72
73 g_slice_free (TrackerToc, toc);
74 }
75
76 void
tracker_toc_add_entry(TrackerToc * toc,GstTagList * tags,gdouble start,gdouble duration)77 tracker_toc_add_entry (TrackerToc *toc,
78 GstTagList *tags,
79 gdouble start,
80 gdouble duration)
81 {
82 TrackerTocEntry *toc_entry;
83
84 toc_entry = g_slice_new (TrackerTocEntry);
85 toc_entry->tag_list = gst_tag_list_ref (tags);
86 toc_entry->start = start;
87 toc_entry->duration = duration;
88
89 toc->entry_list = g_list_append (toc->entry_list, toc_entry);
90 }
91
92 #if defined(HAVE_LIBCUE)
93
94 static void
add_cdtext_string_tag(Cdtext * cd_text,enum Pti index,GstTagList * tag_list,const gchar * tag)95 add_cdtext_string_tag (Cdtext *cd_text,
96 enum Pti index,
97 GstTagList *tag_list,
98 const gchar *tag)
99 {
100 const gchar *text;
101
102 text = cdtext_get (index, cd_text);
103
104 if (text != NULL) {
105 gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, tag, text, NULL);
106 }
107 }
108
109 static void
add_cdtext_comment_date_tag(Rem * cd_comments,enum RemType index,GstTagList * tag_list,const gchar * tag)110 add_cdtext_comment_date_tag (Rem *cd_comments,
111 #if defined(HAVE_LIBCUE2)
112 enum RemType index,
113 #elif defined(HAVE_LIBCUE)
114 enum Cmt index,
115 #endif
116 GstTagList *tag_list,
117 const gchar *tag)
118 {
119 const gchar *text;
120 gint year;
121 GDate *date;
122
123 text = rem_get (index, cd_comments);
124
125 if (text != NULL) {
126 year = atoi (text);
127
128 if (year >= 1860) {
129 date = g_date_new_dmy (1, 1, year);
130 gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, tag, date, NULL);
131 g_date_free (date);
132 }
133 }
134 }
135
136 static void
add_cdtext_comment_double_tag(Rem * cd_comments,enum RemType index,GstTagList * tag_list,const gchar * tag)137 add_cdtext_comment_double_tag (Rem *cd_comments,
138 #if defined(HAVE_LIBCUE2)
139 enum RemType index,
140 #elif defined(HAVE_LIBCUE)
141 enum Cmt index,
142 #endif
143 GstTagList *tag_list,
144 const gchar *tag)
145 {
146 const gchar *text;
147 gdouble value;
148
149 text = rem_get (index, cd_comments);
150
151 if (text != NULL) {
152 value = strtod (text, NULL);
153
154 /* Shortcut: it just so happens that 0.0 is meaningless for the replay
155 * gain properties so we can get away with testing for errors this way.
156 */
157 if (value != 0.0)
158 gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, tag, value, NULL);
159 }
160 }
161
162 static void
set_album_tags_from_cdtext(GstTagList * tag_list,Cdtext * cd_text,Rem * cd_comments)163 set_album_tags_from_cdtext (GstTagList *tag_list,
164 Cdtext *cd_text,
165 Rem *cd_comments)
166 {
167 if (cd_text != NULL) {
168 add_cdtext_string_tag (cd_text, PTI_TITLE, tag_list, GST_TAG_ALBUM);
169 add_cdtext_string_tag (cd_text, PTI_PERFORMER, tag_list, GST_TAG_ALBUM_ARTIST);
170 }
171
172 if (cd_comments != NULL) {
173 add_cdtext_comment_date_tag (cd_comments, REM_DATE, tag_list, GST_TAG_DATE);
174
175 add_cdtext_comment_double_tag (cd_comments, REM_REPLAYGAIN_ALBUM_GAIN, tag_list, GST_TAG_ALBUM_GAIN);
176 add_cdtext_comment_double_tag (cd_comments, REM_REPLAYGAIN_ALBUM_PEAK, tag_list, GST_TAG_ALBUM_PEAK);
177 }
178 }
179
180 static void
set_track_tags_from_cdtext(GstTagList * tag_list,Cdtext * cd_text,Rem * cd_comments)181 set_track_tags_from_cdtext (GstTagList *tag_list,
182 Cdtext *cd_text,
183 Rem *cd_comments)
184 {
185 if (cd_text != NULL) {
186 add_cdtext_string_tag (cd_text, PTI_TITLE, tag_list, GST_TAG_TITLE);
187 add_cdtext_string_tag (cd_text, PTI_PERFORMER, tag_list, GST_TAG_PERFORMER);
188 add_cdtext_string_tag (cd_text, PTI_COMPOSER, tag_list, GST_TAG_COMPOSER);
189 }
190
191 if (cd_comments != NULL) {
192 add_cdtext_comment_double_tag (cd_comments, REM_REPLAYGAIN_TRACK_GAIN, tag_list, GST_TAG_TRACK_GAIN);
193 add_cdtext_comment_double_tag (cd_comments, REM_REPLAYGAIN_TRACK_PEAK, tag_list, GST_TAG_TRACK_PEAK);
194 }
195 }
196
197 /* Some simple heuristics to fill in missing tag information. */
198 static void
process_toc_tags(TrackerToc * toc)199 process_toc_tags (TrackerToc *toc)
200 {
201 gint track_count;
202
203 if (gst_tag_list_get_tag_size (toc->tag_list, GST_TAG_TRACK_COUNT) == 0) {
204 track_count = g_list_length (toc->entry_list);
205 gst_tag_list_add (toc->tag_list,
206 GST_TAG_MERGE_REPLACE,
207 GST_TAG_TRACK_COUNT,
208 track_count,
209 NULL);
210 }
211 }
212
213 /* This function runs in two modes: for external CUE sheets, it will check
214 * the FILE field for each track and build a TrackerToc for all the tracks
215 * contained in @file_name. If @file_name does not appear in the CUE sheet,
216 * %NULL will be returned. For embedded CUE sheets, @file_name will be NULL
217 * the whole TOC will be returned regardless of any FILE information.
218 */
219 static TrackerToc *
parse_cue_sheet_for_file(const gchar * cue_sheet,const gchar * file_name)220 parse_cue_sheet_for_file (const gchar *cue_sheet,
221 const gchar *file_name)
222 {
223 TrackerToc *toc;
224 TrackerTocEntry *toc_entry;
225 Cd *cd;
226 Track *track;
227 gint i;
228
229 toc = NULL;
230
231 cd = cue_parse_string (cue_sheet);
232
233 if (cd == NULL) {
234 g_debug ("Unable to parse CUE sheet for %s.",
235 file_name ? file_name : "(embedded in FLAC)");
236 return NULL;
237 }
238
239 for (i = 1; i <= cd_get_ntrack (cd); i++) {
240 track = cd_get_track (cd, i);
241
242 /* CUE sheets generally have the correct basename but wrong
243 * extension in the FILE field, so this is what we test for.
244 */
245 if (file_name != NULL) {
246 if (!tracker_filename_casecmp_without_extension (file_name,
247 track_get_filename (track))) {
248 continue;
249 }
250 }
251
252 if (track_get_mode (track) != MODE_AUDIO)
253 continue;
254
255 if (toc == NULL) {
256 toc = tracker_toc_new ();
257
258 set_album_tags_from_cdtext (toc->tag_list,
259 cd_get_cdtext (cd),
260 cd_get_rem (cd));
261 }
262
263 toc_entry = g_slice_new (TrackerTocEntry);
264 toc_entry->tag_list = gst_tag_list_new_empty ();
265 toc_entry->start = track_get_start (track) / 75.0;
266 toc_entry->duration = track_get_length (track) / 75.0;
267
268 set_track_tags_from_cdtext (toc_entry->tag_list,
269 track_get_cdtext (track),
270 track_get_rem (track));
271
272 gst_tag_list_add (toc_entry->tag_list,
273 GST_TAG_MERGE_REPLACE,
274 GST_TAG_TRACK_NUMBER,
275 i,
276 NULL);
277
278
279 toc->entry_list = g_list_prepend (toc->entry_list, toc_entry);
280 }
281
282 cd_delete (cd);
283
284 if (toc != NULL)
285 toc->entry_list = g_list_reverse (toc->entry_list);
286
287 return toc;
288 }
289
290 TrackerToc *
tracker_cue_sheet_parse(const gchar * cue_sheet)291 tracker_cue_sheet_parse (const gchar *cue_sheet)
292 {
293 TrackerToc *result;
294
295 result = parse_cue_sheet_for_file (cue_sheet, NULL);
296
297 if (result)
298 process_toc_tags (result);
299
300 return result;
301 }
302
303 static GList *
find_local_cue_sheets(GFile * audio_file)304 find_local_cue_sheets (GFile *audio_file)
305 {
306 GFile *container;
307 GFile *cue_sheet;
308 GFileEnumerator *e;
309 GFileInfo *file_info;
310 gchar *container_path;
311 const gchar *file_name;
312 const gchar *file_content_type;
313 gchar *file_path;
314 GList *result = NULL;
315 GError *error = NULL;
316
317 container = g_file_get_parent (audio_file);
318 container_path = g_file_get_path (container);
319
320 e = g_file_enumerate_children (container,
321 "standard::*",
322 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
323 NULL,
324 &error);
325
326 if (error != NULL) {
327 g_debug ("Unable to enumerate directory: %s", error->message);
328 g_object_unref (container);
329 g_error_free (error);
330 return NULL;
331 }
332
333 while ((file_info = g_file_enumerator_next_file (e, NULL, NULL))) {
334 file_name = g_file_info_get_attribute_byte_string (file_info,
335 G_FILE_ATTRIBUTE_STANDARD_NAME);
336
337 file_content_type = g_file_info_get_content_type (file_info);
338
339 if (file_name == NULL || file_content_type == NULL) {
340 g_debug ("Unable to get info for file %s/%s",
341 container_path,
342 g_file_info_get_display_name (file_info));
343 } else if (strcmp (file_content_type, "application/x-cue") == 0) {
344 file_path = g_build_filename (container_path, file_name, NULL);
345 cue_sheet = g_file_new_for_path (file_path);
346 result = g_list_prepend (result, cue_sheet);
347 g_free (file_path);
348 }
349
350 g_object_unref (file_info);
351 }
352
353 g_object_unref (e);
354 g_object_unref (container);
355 g_free (container_path);
356
357 return result;
358 }
359
360 TrackerToc *
tracker_cue_sheet_parse_uri(const gchar * uri)361 tracker_cue_sheet_parse_uri (const gchar *uri)
362 {
363 GFile *audio_file;
364 gchar *audio_file_name;
365 GList *cue_sheet_list;
366 TrackerToc *toc;
367 GError *error = NULL;
368 GList *n;
369
370 audio_file = g_file_new_for_uri (uri);
371 audio_file_name = g_file_get_basename (audio_file);
372
373 cue_sheet_list = find_local_cue_sheets (audio_file);
374
375 toc = NULL;
376
377 for (n = cue_sheet_list; n != NULL; n = n->next) {
378 GFile *cue_sheet_file;
379 gchar *buffer;
380
381 cue_sheet_file = n->data;
382
383 if (!g_file_load_contents (cue_sheet_file, NULL, &buffer, NULL, NULL, &error)) {
384 g_debug ("Unable to read cue sheet: %s", error->message);
385 g_error_free (error);
386 continue;
387 }
388
389 toc = parse_cue_sheet_for_file (buffer, audio_file_name);
390
391 g_free (buffer);
392
393 if (toc != NULL) {
394 char *path = g_file_get_path (cue_sheet_file);
395 g_debug ("Using external CUE sheet: %s", path);
396 g_free (path);
397 break;
398 }
399 }
400
401 g_list_foreach (cue_sheet_list, (GFunc) g_object_unref, NULL);
402 g_list_free (cue_sheet_list);
403
404 g_object_unref (audio_file);
405 g_free (audio_file_name);
406
407 if (toc)
408 process_toc_tags (toc);
409
410 return toc;
411 }
412
413 #else /* ! HAVE_LIBCUE */
414
415 TrackerToc *
tracker_cue_sheet_parse(const gchar * cue_sheet)416 tracker_cue_sheet_parse (const gchar *cue_sheet)
417 {
418 return NULL;
419 }
420
421 TrackerToc *
tracker_cue_sheet_parse_uri(const gchar * uri)422 tracker_cue_sheet_parse_uri (const gchar *uri)
423 {
424 return NULL;
425 }
426
427 #endif /* ! HAVE_LIBCUE */
428