1 /* EasyTAG - Tag editor for audio files
2 * Copyright (C) 2014-2015 David King <amigadave@amigadave.com>
3 * Copyright (C) 2000-2003 Jerome Couderc <easytag@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20 #include "config.h"
21
22 #include "misc.h"
23
24 #include <glib/gi18n.h>
25 #include <glib/gstdio.h>
26 #include <stdlib.h>
27 #include <sys/stat.h>
28
29 #include "easytag.h"
30 #include "id3_tag.h"
31 #include "browser.h"
32 #include "setting.h"
33 #include "preferences_dialog.h"
34
35 #ifdef G_OS_WIN32
36 #include <windows.h>
37 #endif /* G_OS_WIN32 */
38
39
40 /*
41 * Add the 'string' passed in parameter to the list store
42 * If this string already exists in the list store, it doesn't add it.
43 * Returns TRUE if string was added.
44 */
Add_String_To_Combo_List(GtkListStore * liststore,const gchar * str)45 gboolean Add_String_To_Combo_List (GtkListStore *liststore, const gchar *str)
46 {
47 GtkTreeIter iter;
48 gchar *text;
49 const gint HISTORY_MAX_LENGTH = 15;
50 //gboolean found = FALSE;
51 gchar *string = g_strdup(str);
52
53 if (et_str_empty (string))
54 {
55 g_free (string);
56 return FALSE;
57 }
58
59 #if 0
60 // We add the string to the beginning of the list store
61 // So we will start to parse from the second line below
62 gtk_list_store_prepend(liststore, &iter);
63 gtk_list_store_set(liststore, &iter, MISC_COMBO_TEXT, string, -1);
64
65 // Search in the list store if string already exists and remove other same strings in the list
66 found = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(liststore), &iter);
67 //gtk_tree_model_get(GTK_TREE_MODEL(liststore), &iter, MISC_COMBO_TEXT, &text, -1);
68 while (found && gtk_tree_model_iter_next(GTK_TREE_MODEL(liststore), &iter))
69 {
70 gtk_tree_model_get(GTK_TREE_MODEL(liststore), &iter, MISC_COMBO_TEXT, &text, -1);
71 //g_print(">0>%s\n>1>%s\n",string,text);
72 if (g_utf8_collate(text, string) == 0)
73 {
74 g_free(text);
75 // FIX ME : it seems that after it selects the next item for the
76 // combo (changes 'string')????
77 // So should select the first item?
78 gtk_list_store_remove(liststore, &iter);
79 // Must be rewinded?
80 found = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(liststore), &iter);
81 //gtk_tree_model_get(GTK_TREE_MODEL(liststore), &iter, MISC_COMBO_TEXT, &text, -1);
82 continue;
83 }
84 g_free(text);
85 }
86
87 // Limit list size to HISTORY_MAX_LENGTH
88 while (gtk_tree_model_iter_n_children(GTK_TREE_MODEL(liststore),NULL) > HISTORY_MAX_LENGTH)
89 {
90 if ( gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(liststore),
91 &iter,NULL,HISTORY_MAX_LENGTH) )
92 {
93 gtk_list_store_remove(liststore, &iter);
94 }
95 }
96
97 g_free(string);
98 // Place again to the beginning of the list, to select the right value?
99 //gtk_tree_model_get_iter_first(GTK_TREE_MODEL(liststore), &iter);
100
101 return TRUE;
102
103 #else
104
105 // Search in the list store if string already exists.
106 // FIXME : insert string at the beginning of the list (if already exists),
107 // and remove other same strings in the list
108 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(liststore), &iter))
109 {
110 do
111 {
112 gtk_tree_model_get(GTK_TREE_MODEL(liststore), &iter, MISC_COMBO_TEXT, &text, -1);
113 if (g_utf8_collate(text, string) == 0)
114 {
115 g_free (string);
116 g_free(text);
117 return FALSE;
118 }
119
120 g_free(text);
121 } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(liststore), &iter));
122 }
123
124 /* We add the string to the beginning of the list store. */
125 gtk_list_store_insert_with_values (liststore, &iter, -1, MISC_COMBO_TEXT,
126 string, -1);
127
128 // Limit list size to HISTORY_MAX_LENGTH
129 while (gtk_tree_model_iter_n_children(GTK_TREE_MODEL(liststore),NULL) > HISTORY_MAX_LENGTH)
130 {
131 if ( gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(liststore),
132 &iter,NULL,HISTORY_MAX_LENGTH) )
133 {
134 gtk_list_store_remove(liststore, &iter);
135 }
136 }
137
138 g_free(string);
139 return TRUE;
140 #endif
141 }
142
143 static void
et_on_child_exited(GPid pid,gint status,gpointer user_data)144 et_on_child_exited (GPid pid, gint status, gpointer user_data)
145 {
146 g_spawn_close_pid (pid);
147 }
148
149 /*
150 * Run a program with a list of parameters
151 * - args_list : list of filename (with path)
152 */
153 gboolean
et_run_program(const gchar * program_name,GList * args_list,GError ** error)154 et_run_program (const gchar *program_name,
155 GList *args_list,
156 GError **error)
157 {
158 gchar *program_tmp;
159 const gchar *program_args;
160 gchar **program_args_argv = NULL;
161 guint n_program_args = 0;
162 gsize i;
163 GPid pid;
164 gchar **argv;
165 GList *l;
166 gchar *program_path;
167 gboolean res = FALSE;
168
169 g_return_val_if_fail (program_name != NULL && args_list != NULL, FALSE);
170 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
171
172 /* Check if a name for the program has been supplied */
173 if (!*program_name)
174 {
175 GtkWidget *msgdialog;
176
177 msgdialog = gtk_message_dialog_new(GTK_WINDOW(MainWindow),
178 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
179 GTK_MESSAGE_ERROR,
180 GTK_BUTTONS_OK,
181 "%s",
182 _("You must type a program name"));
183 gtk_window_set_title(GTK_WINDOW(msgdialog),_("Program Name Error"));
184
185 gtk_dialog_run(GTK_DIALOG(msgdialog));
186 gtk_widget_destroy(msgdialog);
187 return res;
188 }
189
190 /* If user arguments are included, try to skip them. FIXME: This works
191 * poorly when there are spaces in the absolute path to the binary. */
192 program_tmp = g_strdup (program_name);
193
194 /* Skip the binary name and a delimiter. */
195 #ifdef G_OS_WIN32
196 /* FIXME: Should also consider .com, .bat, .sys. See
197 * g_find_program_in_path(). */
198 if ((program_args = strstr (program_tmp, ".exe")))
199 {
200 /* Skip ".exe". */
201 program_args += 4;
202 }
203 #else /* !G_OS_WIN32 */
204 /* Remove arguments if found. */
205 program_args = strchr (program_tmp, ' ');
206 #endif /* !G_OS_WIN32 */
207
208 if (program_args && *program_args)
209 {
210 size_t len;
211
212 len = program_args - program_tmp;
213 program_path = g_strndup (program_name, len);
214
215 /* FIXME: Splitting arguments based on a delimiting space is bogus
216 * if the arguments have been quoted. */
217 program_args_argv = g_strsplit (program_args, " ", 0);
218 n_program_args = g_strv_length (program_args_argv);
219 }
220 else
221 {
222 n_program_args = 1;
223 program_path = g_strdup (program_name);
224 }
225
226 g_free (program_tmp);
227
228 /* +1 for NULL, program_name is already included in n_program_args. */
229 argv = g_new0 (gchar *, n_program_args + g_list_length (args_list) + 1);
230
231 argv[0] = program_path;
232
233 if (program_args_argv)
234 {
235 /* Skip program_args_argv[0], which is " ". */
236 for (i = 1; program_args_argv[i] != NULL; i++)
237 {
238 argv[i] = program_args_argv[i];
239 }
240 }
241 else
242 {
243 i = 1;
244 }
245
246 /* Load arguments from 'args_list'. */
247 for (l = args_list; l != NULL; l = g_list_next (l), i++)
248 {
249 argv[i] = (gchar *)l->data;
250 }
251
252 argv[i] = NULL;
253
254 /* Execution ... */
255 if (g_spawn_async (NULL, argv, NULL,
256 G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
257 NULL, NULL, &pid, error))
258 {
259 g_child_watch_add (pid, et_on_child_exited, NULL);
260
261 res = TRUE;
262 }
263
264 g_strfreev (program_args_argv);
265 g_free (program_path);
266 g_free (argv);
267
268 return res;
269 }
270
271 gboolean
et_run_audio_player(GList * files,GError ** error)272 et_run_audio_player (GList *files,
273 GError **error)
274 {
275 GFileInfo *info;
276 const gchar *content_type;
277 GAppInfo *app_info;
278 GdkAppLaunchContext *context;
279
280 g_return_val_if_fail (files != NULL, FALSE);
281 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
282
283 info = g_file_query_info (files->data,
284 G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
285 G_FILE_QUERY_INFO_NONE, NULL, error);
286
287 if (info == NULL)
288 {
289 return FALSE;
290 }
291
292 content_type = g_file_info_get_content_type (info);
293 app_info = g_app_info_get_default_for_type (content_type, FALSE);
294 g_object_unref (info);
295
296 context = gdk_display_get_app_launch_context (gdk_display_get_default ());
297
298 if (!g_app_info_launch (app_info, files, G_APP_LAUNCH_CONTEXT (context),
299 error))
300 {
301 g_object_unref (context);
302 g_object_unref (app_info);
303
304 return FALSE;
305 }
306
307 g_object_unref (context);
308 g_object_unref (app_info);
309
310 return TRUE;
311 }
312
313 /*
314 * Convert a series of seconds into a readable duration
315 * Remember to free the string that is returned
316 */
Convert_Duration(gulong duration)317 gchar *Convert_Duration (gulong duration)
318 {
319 guint hour=0;
320 guint minute=0;
321 guint second=0;
322 gchar *data = NULL;
323
324 if (duration == 0)
325 {
326 return g_strdup_printf ("%u:%.2u", minute, second);
327 }
328
329 hour = duration/3600;
330 minute = (duration%3600)/60;
331 second = (duration%3600)%60;
332
333 if (hour)
334 {
335 data = g_strdup_printf ("%u:%.2u:%.2u", hour, minute, second);
336 }
337 else
338 {
339 data = g_strdup_printf ("%u:%.2u", minute, second);
340 }
341
342 return data;
343 }
344
345 gchar *
et_disc_number_to_string(const guint disc_number)346 et_disc_number_to_string (const guint disc_number)
347 {
348 if (g_settings_get_boolean (MainSettings, "tag-disc-padded"))
349 {
350 return g_strdup_printf ("%.*u",
351 (gint)g_settings_get_uint (MainSettings,
352 "tag-disc-length"),
353 disc_number);
354 }
355
356 return g_strdup_printf ("%u", disc_number);
357 }
358
359 gchar *
et_track_number_to_string(const guint track_number)360 et_track_number_to_string (const guint track_number)
361 {
362 if (g_settings_get_boolean (MainSettings, "tag-number-padded"))
363 {
364 return g_strdup_printf ("%.*u",
365 (gint)g_settings_get_uint (MainSettings,
366 "tag-number-length"),
367 track_number);
368 }
369 else
370 {
371 return g_strdup_printf ("%u", track_number);
372 }
373 }
374
375 /*
376 * et_rename_file:
377 * @old_filepath: path of file to be renamed
378 * @new_filepath: path of renamed file
379 * @error: a #GError to provide information on errors, or %NULL to ignore
380 *
381 * Rename @old_filepath to @new_filepath.
382 *
383 * Returns: %TRUE if the rename was successful, %FALSE otherwise
384 */
385 gboolean
et_rename_file(const char * old_filepath,const char * new_filepath,GError ** error)386 et_rename_file (const char *old_filepath,
387 const char *new_filepath,
388 GError **error)
389 {
390 GFile *file_old;
391 GFile *file_new;
392 GFile *file_new_parent;
393
394 g_return_val_if_fail (old_filepath != NULL && new_filepath != NULL, FALSE);
395 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
396
397 file_old = g_file_new_for_path (old_filepath);
398 file_new = g_file_new_for_path (new_filepath);
399 file_new_parent = g_file_get_parent (file_new);
400
401 if (!g_file_make_directory_with_parents (file_new_parent, NULL, error))
402 {
403 /* Ignore an error if the directory already exists. */
404 if (!g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_EXISTS))
405 {
406 g_object_unref (file_new_parent);
407 goto err;
408 }
409
410 g_clear_error (error);
411 }
412
413 g_assert (error == NULL || *error == NULL);
414 g_object_unref (file_new_parent);
415
416 /* Move the file. */
417 if (!g_file_move (file_old, file_new, G_FILE_COPY_NONE, NULL, NULL, NULL,
418 error))
419 {
420 if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_EXISTS))
421 {
422 /* Possibly a case change on a case-insensitive filesystem. */
423 /* TODO: casefold the paths of both files, and check to see whether
424 * they only differ by case? */
425 gchar *tmp_filename;
426 mode_t old_mode;
427 gint fd;
428 GFile *tmp_file;
429 GError *tmp_error = NULL;
430
431 tmp_filename = g_strconcat (old_filepath, ".XXXXXX", NULL);
432
433 old_mode = umask (077);
434 fd = g_mkstemp (tmp_filename);
435 umask (old_mode);
436
437 if (fd >= 0)
438 {
439 /* TODO: Handle error. */
440 g_close (fd, NULL);
441 }
442
443 tmp_file = g_file_new_for_path (tmp_filename);
444 g_free (tmp_filename);
445
446 if (!g_file_move (file_old, tmp_file, G_FILE_COPY_OVERWRITE, NULL,
447 NULL, NULL, &tmp_error))
448 {
449 g_file_delete (tmp_file, NULL, NULL);
450
451 g_object_unref (tmp_file);
452 g_clear_error (error);
453 g_propagate_error (error, tmp_error);
454 goto err;
455 }
456 else
457 {
458 /* Move to temporary file succeeded, now move to the real new
459 * location. */
460 if (!g_file_move (tmp_file, file_new, G_FILE_COPY_NONE, NULL,
461 NULL, NULL, &tmp_error))
462 {
463 g_file_move (tmp_file, file_old, G_FILE_COPY_NONE, NULL,
464 NULL, NULL, NULL);
465 g_object_unref (tmp_file);
466 g_clear_error (error);
467 g_propagate_error (error, tmp_error);
468 goto err;
469 }
470 else
471 {
472 /* Move succeeded, so clear the original error about the
473 * new file already existing. */
474 g_object_unref (tmp_file);
475 g_clear_error (error);
476 goto out;
477 }
478 }
479 }
480 else
481 {
482 /* Error moving file. */
483 goto err;
484 }
485 }
486
487 out:
488 g_object_unref (file_old);
489 g_object_unref (file_new);
490 g_assert (error == NULL || *error == NULL);
491 return TRUE;
492
493 err:
494 g_object_unref (file_old);
495 g_object_unref (file_new);
496 g_assert (error == NULL || *error != NULL);
497 return FALSE;
498 }
499
500 /*
501 * et_filename_prepare:
502 * @filename_utf8: UTF8-encoded basename
503 * @replace_illegal: whether to replace illegal characters in the file name
504 *
505 * Used to replace (in place) the illegal characters in the filename.
506 */
507 void
et_filename_prepare(gchar * filename_utf8,gboolean replace_illegal)508 et_filename_prepare (gchar *filename_utf8,
509 gboolean replace_illegal)
510 {
511 gchar *character;
512
513 g_return_if_fail (filename_utf8 != NULL);
514
515 // Convert automatically the directory separator ('/' on LINUX and '\' on WIN32) to '-'.
516 while ((character = strchr (filename_utf8, G_DIR_SEPARATOR)) != NULL)
517 {
518 *character = '-';
519 }
520
521 #ifdef G_OS_WIN32
522 /* Convert character '/' on WIN32 to '-'. May be converted to '\' after. */
523 while ((character = strchr (filename_utf8, '/')) != NULL)
524 {
525 *character = '-';
526 }
527 #endif /* G_OS_WIN32 */
528
529 /* Convert other illegal characters on FAT32/16 filesystems and ISO9660 and
530 * Joliet (CD-ROM filesystems). */
531 if (replace_illegal)
532 {
533 size_t last;
534
535 while ((character = strchr (filename_utf8, ':')) != NULL)
536 {
537 *character = '-';
538 }
539 while ((character = strchr (filename_utf8, '*')) != NULL)
540 {
541 *character = '+';
542 }
543 while ((character = strchr (filename_utf8, '?')) != NULL)
544 {
545 *character = '_';
546 }
547 while ((character = strchr (filename_utf8, '\"')) != NULL)
548 {
549 *character = '\'';
550 }
551 while ((character = strchr (filename_utf8, '<')) != NULL)
552 {
553 *character = '(';
554 }
555 while ((character = strchr (filename_utf8, '>')) != NULL)
556 {
557 *character = ')';
558 }
559 while ((character = strchr (filename_utf8, '|')) != NULL)
560 {
561 *character = '-';
562 }
563
564 /* FAT has additional restrictions on the last character of a filename.
565 * https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx#naming_conventions */
566 last = strlen (filename_utf8) - 1;
567
568 if (filename_utf8[last] == ' ' || filename_utf8[last] == '.')
569 {
570 filename_utf8[last] = '_';
571 }
572 }
573 }
574
575 /* Key for Undo */
576 guint
et_undo_key_new(void)577 et_undo_key_new (void)
578 {
579 static guint ETUndoKey = 0;
580 return ++ETUndoKey;
581 }
582
583 /*
584 * et_normalized_strcmp0:
585 * @str1: UTF-8 string, or %NULL
586 * @str2: UTF-8 string to compare against, or %NULL
587 *
588 * Compare two UTF-8 strings, normalizing them before doing so, and return the
589 * difference.
590 *
591 * Returns: an integer less than, equal to, or greater than zero, if str1 is <,
592 * == or > than str2
593 */
594 gint
et_normalized_strcmp0(const gchar * str1,const gchar * str2)595 et_normalized_strcmp0 (const gchar *str1,
596 const gchar *str2)
597 {
598 gint result;
599 gchar *normalized1;
600 gchar *normalized2;
601
602 /* Check for NULL, as it cannot be passed to g_utf8_normalize(). */
603 if (!str1)
604 {
605 return -(str1 != str2);
606 }
607
608 if (!str2)
609 {
610 return str1 != str2;
611 }
612
613 normalized1 = g_utf8_normalize (str1, -1, G_NORMALIZE_DEFAULT);
614 normalized2 = g_utf8_normalize (str2, -1, G_NORMALIZE_DEFAULT);
615
616 result = g_strcmp0 (normalized1, normalized2);
617
618 g_free (normalized1);
619 g_free (normalized2);
620
621 return result;
622 }
623
624 /*
625 * et_normalized_strcasecmp0:
626 * @str1: UTF-8 string, or %NULL
627 * @str2: UTF-8 string to compare against, or %NULL
628 *
629 * Compare two UTF-8 strings, normalizing them before doing so, in a
630 * case-insensitive manner.
631 *
632 * Returns: an integer less than, equal to, or greater than zero, if str1 is
633 * less than, equal to or greater than str2
634 */
635 gint
et_normalized_strcasecmp0(const gchar * str1,const gchar * str2)636 et_normalized_strcasecmp0 (const gchar *str1,
637 const gchar *str2)
638 {
639 gint result;
640 gchar *casefolded1;
641 gchar *casefolded2;
642
643 /* Check for NULL, as it cannot be passed to g_utf8_casefold(). */
644 if (!str1)
645 {
646 return -(str1 != str2);
647 }
648
649 if (!str2)
650 {
651 return str1 != str2;
652 }
653
654 /* The strings are automatically normalized during casefolding. */
655 casefolded1 = g_utf8_casefold (str1, -1);
656 casefolded2 = g_utf8_casefold (str2, -1);
657
658 result = g_utf8_collate (casefolded1, casefolded2);
659
660 g_free (casefolded1);
661 g_free (casefolded2);
662
663 return result;
664 }
665
666 /*
667 * et_str_empty:
668 * @str: string to test for emptiness
669 *
670 * Test if @str is empty, in other words either %NULL or the empty string.
671 *
672 * Returns: %TRUE is @str is either %NULL or "", %FALSE otherwise
673 */
674 gboolean
et_str_empty(const gchar * str)675 et_str_empty (const gchar *str)
676 {
677 return !str || !str[0];
678 }
679