1 /*
2  * * Copyright (C) 2009-2011 Ali <aliov@xfce.org>
3  * * Copyright (C) 2012-2017 Simon Steinbeiß <ochosi@xfce.org>
4  * * Copyright (C) 2012-2020 Sean Davis <bluesabre@xfce.org>
5  *
6  * Licensed under the GNU General Public License Version 2
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21  */
22 
23 #ifdef HAVE_CONFIG_H
24 #include <config.h>
25 #endif
26 
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 
31 #include <gst/gst.h>
32 #include <glib.h>
33 
34 #include <fcntl.h>
35 #include <unistd.h>
36 #include <sys/stat.h>
37 #include <sys/ioctl.h>
38 
39 #if defined(__linux__)
40 #include <linux/cdrom.h>
41 #endif
42 
43 #ifdef HAVE_TAGLIBC
44 #include <taglib/tag_c.h>
45 #endif
46 
47 #include <libxfce4util/libxfce4util.h>
48 
49 #include "src/misc/parole.h"
50 
51 #include "src/parole-utils.h"
52 
53 /* List from xine-lib's demux_sputext.c */
54 static const char subtitle_ext[][4] = {
55     "asc",
56     "txt",
57     "sub",
58     "srt",
59     "smi",
60     "ssa",
61     "ass"
62 };
63 
64 /*
65  * compare_by_name_using_number
66  *
67  * * Copyright (c) 2005-2007 Benedikt Meurer <benny@xfce.org>
68  *
69  */
70 static gint
compare_by_name_using_number(const gchar * ap,const gchar * bp)71 compare_by_name_using_number(const gchar *ap, const gchar *bp) {
72     guint anum;
73     guint bnum;
74 
75     /* determine the numbers in ap and bp */
76     anum = strtoul(ap, NULL, 10);
77     bnum = strtoul(bp, NULL, 10);
78 
79     /* compare the numbers */
80     if (anum < bnum)
81         return -1;
82     else if (anum > bnum)
83         return 1;
84 
85     /* the numbers are equal, and so the higher first digit should
86     * be sorted first, i.e. 'file10' before 'file010', since we
87     * also sort 'file10' before 'file011'.
88     */
89     return (*bp - *ap);
90 }
91 
92 /*
93  * thunar_file_compare_by_name
94  *
95  * * Copyright (c) 2005-2007 Benedikt Meurer <benny@xfce.org>
96  *
97  */
98 gint
thunar_file_compare_by_name(ParoleFile * file_a,ParoleFile * file_b,gboolean case_sensitive)99 thunar_file_compare_by_name(ParoleFile *file_a,
100                             ParoleFile *file_b,
101                             gboolean    case_sensitive) {
102     const gchar *ap;
103     const gchar *bp;
104     guint        ac;
105     guint        bc;
106 
107 
108     /* we compare only the display names (UTF-8!) */
109     ap = parole_file_get_display_name(file_a);
110     bp = parole_file_get_display_name(file_b);
111 
112     /* check if we should ignore case */
113     if (G_LIKELY(case_sensitive)) {
114         /* try simple (fast) ASCII comparison first */
115         for (;; ++ap, ++bp) {
116             /* check if the characters differ or we have a non-ASCII char */
117             ac = *((const guchar *) ap);
118             bc = *((const guchar *) bp);
119             if (ac != bc || ac == 0 || ac > 127)
120             break;
121         }
122 
123         /* fallback to Unicode comparison */
124         if (G_UNLIKELY(ac > 127 || bc > 127)) {
125             for (;; ap = g_utf8_next_char(ap), bp = g_utf8_next_char(bp)) {
126                 /* check if characters differ or end of string */
127                 ac = g_utf8_get_char(ap);
128                 bc = g_utf8_get_char(bp);
129                 if (ac != bc || ac == 0)
130                     break;
131             }
132         }
133     } else {
134         /* try simple (fast) ASCII comparison first (case-insensitive!) */
135         for (;; ++ap, ++bp) {
136             /* check if the characters differ or we have a non-ASCII char */
137             ac = *((const guchar *) ap);
138             bc = *((const guchar *) bp);
139             if (g_ascii_tolower (ac) != g_ascii_tolower (bc) || ac == 0 || ac > 127)
140             break;
141         }
142 
143         /* fallback to Unicode comparison (case-insensitive!) */
144         if (G_UNLIKELY(ac > 127 || bc > 127)) {
145             for (;; ap = g_utf8_next_char(ap), bp = g_utf8_next_char(bp)) {
146                 /* check if characters differ or end of string */
147                 ac = g_utf8_get_char(ap);
148                 bc = g_utf8_get_char(bp);
149                 if (g_unichar_tolower (ac) != g_unichar_tolower (bc) || ac == 0)
150                     break;
151             }
152         }
153     }
154 
155     /* if both strings are equal, we're done */
156     if (G_UNLIKELY(ac == bc || (!case_sensitive && g_unichar_tolower (ac) == g_unichar_tolower (bc))))
157         return 0;
158 
159     /* check if one of the characters that differ is a digit */
160     if (G_UNLIKELY(g_ascii_isdigit(ac) || g_ascii_isdigit(bc))) {
161         /* if both strings differ in a digit, we use a smarter comparison
162          * to get sorting 'file1', 'file5', 'file10' done the right way.
163          */
164         if (g_ascii_isdigit (ac) && g_ascii_isdigit (bc))
165             return compare_by_name_using_number (ap, bp);
166 
167         /* a second case is '20 file' and '2file', where comparison by number
168          * makes sense, if the previous char for both strings is a digit.
169          */
170         if (ap > parole_file_get_display_name (file_a) && bp > parole_file_get_display_name (file_b)
171             && g_ascii_isdigit(*(ap - 1)) && g_ascii_isdigit(*(bp - 1))) {
172             return compare_by_name_using_number (ap - 1, bp - 1);
173         }
174     }
175 
176     /* otherwise, if they differ in a unicode char, use the
177      * appropriate collate function for the current locale (only
178      * if charset is UTF-8, else the required transformations
179      * would be too expensive)
180      */
181 #ifdef HAVE_STRCOLL
182     if ((ac > 127 || bc > 127) && g_get_charset(NULL)) {
183         /* case-sensitive is easy, case-insensitive is expensive,
184              * but we use a simple optimization to make it fast.
185              */
186         if (G_LIKELY(case_sensitive)) {
187             return strcoll (ap, bp);
188         } else {
189             /* we use a trick here, so we don't need to allocate
190                  * and transform the two strings completely first (8
191                  * byte for each buffer, so all compilers should align
192                  * them properly)
193                  */
194             gchar abuf[8];
195             gchar bbuf[8];
196 
197             /* transform the unicode chars to strings and
198                  * make sure the strings are null-terminated.
199              */
200             abuf[g_unichar_to_utf8(ac, abuf)] = '\0';
201             bbuf[g_unichar_to_utf8(bc, bbuf)] = '\0';
202 
203             /* compare the unicode chars (as strings) */
204             return strcoll (abuf, bbuf);
205         }
206     }
207 #endif
208 
209     /* else, they differ in an ASCII character */
210     if (G_UNLIKELY (!case_sensitive))
211         return (g_unichar_tolower (ac) > g_unichar_tolower (bc)) ? 1 : -1;
212     else
213         return (ac > bc) ? 1 : -1;
214 }
215 
216 gchar *
parole_get_name_without_extension(const gchar * name)217 parole_get_name_without_extension(const gchar *name) {
218     guint len, suffix;
219     gchar *ret;
220 
221     len = strlen(name);
222 
223     for (suffix = len -1; suffix > 0;  suffix--) {
224         if ( name[suffix] == '.' )
225             break;
226     }
227 
228     ret = g_strndup (name, sizeof (char) * (suffix));
229     return ret;
230 }
231 
232 static gchar *
parole_get_subtitle_in_dir(const gchar * dir_path,const gchar * file)233 parole_get_subtitle_in_dir(const gchar *dir_path, const gchar *file) {
234     gchar *sub_path = NULL;
235     gchar *file_no_ext;
236     guint i;
237 
238     file_no_ext = parole_get_name_without_extension(file);
239 
240     for (i = 0; i < G_N_ELEMENTS(subtitle_ext); i++) {
241         sub_path = g_strdup_printf("%s%c%s.%s", dir_path, G_DIR_SEPARATOR, file_no_ext, subtitle_ext[i]);
242 
243         if ( g_file_test (sub_path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR ) )
244             break;
245 
246         g_free(sub_path);
247         sub_path = NULL;
248     }
249 
250     g_free(file_no_ext);
251 
252     return sub_path;
253 }
254 
parole_get_subtitle_path(const gchar * uri)255 gchar *parole_get_subtitle_path(const gchar *uri) {
256     GFile *file, *parent;
257     GFileInfo *info;
258     GError *error = NULL;
259     gchar *path;
260     gchar *file_name;
261     gchar *ret = NULL;
262 
263     file = g_file_new_for_commandline_arg(uri);
264     parent = g_file_get_parent(file);
265 
266     TRACE("uri : %s", uri);
267 
268     if ( !parent ) {
269         g_object_unref(file);
270         return NULL;
271     }
272 
273     info = g_file_query_info(file,
274                   "standard::*,",
275                   0,
276                   NULL,
277                   &error);
278 
279     if ( error ) {
280         g_warning("%s: \n", error->message);
281         g_error_free(error);
282         return NULL;
283     }
284 
285     file_name = g_strdup(g_file_info_get_display_name(info));
286 
287     path = g_file_get_path(parent);
288 
289     ret = parole_get_subtitle_in_dir(path, file_name);
290 
291     g_object_unref(file);
292     g_object_unref(parent);
293     g_object_unref(info);
294 
295     g_free(file_name);
296     g_free(path);
297 
298     return ret;
299 }
300 
301 gboolean
parole_is_uri_disc(const gchar * uri)302 parole_is_uri_disc(const gchar *uri) {
303     if (   g_str_has_prefix (uri, "dvd:/")  || g_str_has_prefix (uri, "vcd:/")
304         || g_str_has_prefix(uri, "svcd:/") || g_str_has_prefix(uri, "cdda:/"))
305         return TRUE;
306     else
307         return FALSE;
308 }
309 
parole_icon_load(const gchar * icon_name,gint size)310 GdkPixbuf *parole_icon_load(const gchar *icon_name, gint size) {
311     GdkPixbuf *pix = NULL;
312     GError *error = NULL;
313 
314     pix = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
315                                     icon_name,
316                                     size,
317                                     GTK_ICON_LOOKUP_USE_BUILTIN,
318                                     &error);
319 
320     if ( error ) {
321         g_warning("Unable to load icon : %s : %s", icon_name, error->message);
322         g_error_free(error);
323     }
324 
325     return pix;
326 }
327 
parole_get_media_files(GtkFileFilter * filter,const gchar * path,gboolean recursive,GSList ** list)328 void parole_get_media_files(GtkFileFilter *filter, const gchar *path, gboolean recursive, GSList **list) {
329     GtkFileFilter *playlist_filter;
330     GSList *list_internal = NULL;
331     GSList *playlist = NULL;
332     GDir *dir;
333     const gchar *name;
334     ParoleFile *file;
335 
336     playlist_filter = parole_get_supported_playlist_filter();
337     g_object_ref_sink(playlist_filter);
338 
339     gtk_main_iteration_do(FALSE);
340 
341 
342     if (g_file_test(path, G_FILE_TEST_IS_REGULAR)) {
343         file = parole_file_new(path);
344         if ( parole_file_filter (playlist_filter, file) &&
345              parole_pl_parser_guess_format_from_extension(path) != PAROLE_PL_FORMAT_UNKNOWN ) {
346             playlist = parole_pl_parser_parse_from_file_by_extension(path);
347             g_object_unref(file);
348             if (playlist) {
349                 *list = g_slist_concat(*list, playlist);
350             }
351         } else if (parole_file_filter(filter, file)) {
352             *list = g_slist_append(*list, file);
353         } else {
354             g_object_unref(file);
355         }
356     } else if (g_file_test(path, G_FILE_TEST_IS_DIR)) {
357         dir = g_dir_open(path, 0, NULL);
358 
359         if ( G_UNLIKELY (dir == NULL) )
360             return;
361 
362         while ((name = g_dir_read_name(dir))) {
363             gchar *path_internal = g_build_filename(path, name, NULL);
364             if (g_file_test(path_internal, G_FILE_TEST_IS_DIR) && recursive) {
365                 parole_get_media_files(filter, path_internal, TRUE, list);
366             } else if ( g_file_test(path_internal, G_FILE_TEST_IS_REGULAR) ) {
367                 file = parole_file_new(path_internal);
368                 if (parole_file_filter (playlist_filter, file) &&
369                      parole_pl_parser_guess_format_from_extension(path) != PAROLE_PL_FORMAT_UNKNOWN) {
370                     playlist = parole_pl_parser_parse_from_file_by_extension(path_internal);
371                     g_object_unref(file);
372                     if (playlist) {
373                         *list = g_slist_concat(*list, playlist);
374                     }
375                 } else if ( parole_file_filter(filter, file) ) {
376                     list_internal = g_slist_append(list_internal, file);
377                 } else {
378                     g_object_unref(file);
379                 }
380             }
381             g_free(path_internal);
382         }
383         list_internal = g_slist_sort(list_internal, (GCompareFunc) (void (*)(void)) thunar_file_compare_by_name);
384         g_dir_close(dir);
385         *list = g_slist_concat(*list, list_internal);
386     }
387     g_object_unref(playlist_filter);
388 }
389 
390 /***
391  * FIXME, parole_device_has_cdda and parole_guess_uri_from_mount
392  *        have common code with parole-disc.c
393  ***/
394 
395 
396 gboolean
parole_device_has_cdda(const gchar * device)397 parole_device_has_cdda(const gchar *device) {
398     gboolean ret_val = FALSE;
399 
400 #if defined(__linux__)
401     gint fd;
402     gint drive;
403 
404     TRACE("device : %s", device);
405 
406     if ((fd = open(device, O_RDONLY)) < 0) {
407         g_debug("Failed to open device : %s", device);
408         return FALSE;
409     }
410 
411     if ((drive = ioctl(fd, CDROM_DRIVE_STATUS, NULL))) {
412         if ( drive == CDS_DRIVE_NOT_READY ) {
413             g_debug("Drive :%s is not yet ready\n", device);
414         } else if ( drive == CDS_DISC_OK ) {
415             if ((drive = ioctl(fd, CDROM_DISC_STATUS, NULL)) > 0) {
416                 if ( drive == CDS_AUDIO ) {
417                     ret_val = TRUE;
418                 }
419             }
420         }
421     }
422 
423     close(fd);
424 #endif /* if defined(__linux__) */
425     return ret_val;
426 }
427 
428 gchar *
parole_guess_uri_from_mount(GMount * mount)429 parole_guess_uri_from_mount(GMount *mount) {
430     GFile *file;
431     gchar *uri = NULL;
432 
433     g_return_val_if_fail(G_IS_MOUNT(mount), NULL);
434 
435     file = g_mount_get_root(mount);
436 
437     if ( g_file_has_uri_scheme(file, "cdda") ) {
438         uri = g_strdup("cdda://");
439     } else {
440         gchar **content_type;
441         int i;
442 
443         content_type = g_content_type_guess_for_tree(file);
444 
445         for (i = 0; content_type && content_type[i]; i++) {
446             TRACE("Checking disc content type : %s", content_type[i]);
447 
448             if ( !g_strcmp0(content_type[i], "x-content/video-dvd") ) {
449                 uri = g_strdup("dvd:/");
450                 break;
451             } else if ( !g_strcmp0(content_type[i], "x-content/video-vcd") ) {
452                 uri = g_strdup("vcd:/");
453                 break;
454             } else if ( !g_strcmp0(content_type[i], "x-content/video-svcd") ) {
455                 uri = g_strdup("svcd:/");
456                 break;
457             } else if ( !g_strcmp0(content_type[i], "x-content/audio-cdda") ) {
458                 uri = g_strdup("cdda://");
459                 break;
460             }
461         }
462 
463         if ( content_type )
464             g_strfreev(content_type);
465     }
466 
467     g_object_unref(file);
468 
469     TRACE("Got uri=%s for mount=%s", uri, g_mount_get_name(mount));
470 
471     return uri;
472 }
473 
474 gchar *
parole_get_uri_from_unix_device(const gchar * device)475 parole_get_uri_from_unix_device(const gchar *device) {
476     GVolumeMonitor *monitor;
477     GList *list;
478     guint len;
479     guint i;
480     gchar *uri = NULL;
481 
482     if ( device == NULL )
483         return NULL;
484 
485     /*Check for cdda */
486     if (parole_device_has_cdda(device)) {
487         return g_strdup ("cdda://");
488     }
489 
490     monitor = g_volume_monitor_get();
491 
492     list = g_volume_monitor_get_volumes(monitor);
493 
494     len = g_list_length(list);
495 
496     for (i = 0; i < len; i++) {
497         GVolume *volume;
498         GDrive *drive;
499 
500         volume = g_list_nth_data(list, i);
501 
502         drive = g_volume_get_drive(volume);
503 
504         if (g_drive_can_eject(drive) && g_drive_has_media(drive)) {
505             gchar *unix_device;
506             unix_device = g_volume_get_identifier(volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
507 
508             if (!g_strcmp0(unix_device, device)) {
509                 GMount *mount;
510                 mount = g_volume_get_mount(volume);
511 
512                 if ( mount ) {
513                     uri = parole_guess_uri_from_mount(mount);
514                     g_object_unref(mount);
515                     g_object_unref(drive);
516                     g_free(unix_device);
517                     break;
518                 }
519             }
520             g_free(unix_device);
521         }
522 
523         g_object_unref(drive);
524     }
525 
526     g_list_foreach(list, (GFunc) (void (*)(void)) g_object_unref, NULL);
527     g_list_free(list);
528 
529     g_object_unref(monitor);
530 
531     TRACE("Got uri=%s for device=%s", uri, device);
532 
533     return uri;
534 }
535 
536 /**
537  * parole_format_media_length:
538  *
539  * @total_seconds: length of the media file in seconds
540  *
541  * Returns : formatted string for the media length
542  **/
parole_format_media_length(gint total_seconds)543 gchar *parole_format_media_length(gint total_seconds) {
544     gchar *timestring;
545 
546     gint  hours;
547     gint  minutes;
548     gint  seconds;
549 
550     minutes =  total_seconds / 60;
551     seconds = total_seconds % 60;
552     hours = minutes / 60;
553     minutes = minutes % 60;
554 
555     if ( hours == 0 ) {
556         timestring = g_strdup_printf("%02i:%02i", minutes, seconds);
557     } else {
558         timestring = g_strdup_printf("%i:%02i:%02i", hours, minutes, seconds);
559     }
560 
561     return timestring;
562 }
563 
564 
565 /**
566  * parole_taglibc_get_media_length:
567  *
568  * @ParoleFile: a ParoleFile
569  *
570  * Returns: the length of the media only if the file is a local
571  *          media file.
572  **/
parole_taglibc_get_media_length(ParoleFile * file)573 gchar *parole_taglibc_get_media_length(ParoleFile *file) {
574     #ifdef HAVE_TAGLIBC
575 
576     TagLib_File *tag_file;
577 
578     if (g_str_has_prefix(parole_file_get_uri(file), "file:/")) {
579         tag_file = taglib_file_new(parole_file_get_file_name(file));
580 
581         if ( tag_file ) {
582             gint length = 0;
583             const TagLib_AudioProperties *prop = taglib_file_audioproperties(tag_file);
584 
585             if (prop)
586                 length = taglib_audioproperties_length(prop);
587 
588             taglib_file_free(tag_file);
589 
590             if (length != 0)
591                 return parole_format_media_length (length);
592         }
593     }
594     #endif /* HAVE_TAGLIBC */
595 
596     return NULL;
597 }
598 
g_simple_toggle_action_new(const gchar * action_name,const GVariantType * parameter_type)599 GSimpleAction* g_simple_toggle_action_new(const gchar *action_name, const GVariantType *parameter_type) {
600     GSimpleAction *simple;
601 
602     simple = g_simple_action_new_stateful(action_name, parameter_type,
603                                            g_variant_new_boolean(FALSE));
604 
605     return simple;
606 }
607 
g_simple_toggle_action_get_active(GSimpleAction * simple)608 gboolean g_simple_toggle_action_get_active(GSimpleAction *simple) {
609     GVariant *state;
610 
611     g_object_get(simple,
612                  "state", &state,
613                  NULL);
614 
615     return g_variant_get_boolean(state);
616 }
617 
g_simple_toggle_action_set_active(GSimpleAction * simple,gboolean active)618 void g_simple_toggle_action_set_active(GSimpleAction *simple, gboolean active) {
619     if (g_simple_toggle_action_get_active(simple) != active) {
620         g_simple_action_set_state(simple, g_variant_new_boolean(active));
621     }
622 }
623