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