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