1# Copyright (c) 2016 Marinus Schraal <mschraal@src.gnome.org>
2#
3# GNOME Music is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# GNOME Music is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16#
17# The GNOME Music authors hereby grant permission for non-GPL compatible
18# GStreamer plugins to be used and distributed together with GStreamer
19# and GNOME Music.  This permission is above and beyond the permissions
20# granted by the GPL license by which GNOME Music is covered.  If you
21# modify this code, you may extend this exception to your version of the
22# code, but you are not obligated to do so.  If you do not wish to do so,
23# delete this exception statement from your version.
24
25from enum import Enum, IntEnum
26import re
27import unicodedata
28
29from gettext import gettext as _
30from gi.repository import Gio, GLib
31from gi._gi import pygobject_new_full
32
33from gnomemusic.musiclogger import MusicLogger
34
35
36class ArtSize(Enum):
37    """Enum for icon sizes"""
38    XSMALL = (42, 42)
39    SMALL = (74, 74)
40    MEDIUM = (192, 192)
41    LARGE = (256, 256)
42
43    def __init__(self, width, height):
44        """Intialize width and height"""
45        self.width = width
46        self.height = height
47
48
49class SongStateIcon(Enum):
50    """Enum for icons used in song playing and validation"""
51    ERROR = "dialog-error-symbolic"
52    PLAYING = "media-playback-start-symbolic"
53
54
55class View(IntEnum):
56    """Enum for views"""
57    EMPTY = 0
58    ALBUM = 1
59    ARTIST = 2
60    SONG = 3
61    PLAYLIST = 4
62    SEARCH = 5
63
64
65def get_album_title(item):
66    """Returns the album title associated with the media item
67
68    In case of an audio file the get_album call returns the
69    album title and in case of a container we are looking for
70    the title.
71
72    :param Grl.Media item: A Grilo Media object
73    :return: The album title
74    :rtype: str
75    """
76    if item.is_container():
77        album = get_media_title(item)
78    else:
79        album = (item.get_album()
80                 or _("Unknown album"))
81
82    return album
83
84
85def get_artist_name(item):
86    """Returns the preferred artist for a media item.
87
88    The artist name for a particular media item can be either
89    the main artist of the full album (album artist), the
90    artist of the song (artist) or possibly it is not known at
91    all. The first is preferred in most cases, because it is
92    the most accurate in an album setting.
93
94    :param Grl.Media item: A Grilo Media object
95    :return: The artist name
96    :rtype: str
97    """
98
99    return (item.get_album_artist()
100            or item.get_artist()
101            or _("Unknown Artist"))
102
103
104def get_media_title(item):
105    """Returns the title of the media item.
106
107    :param Grl.Media item: A Grilo Media object
108    :return: The title
109    :rtype: str
110    """
111
112    title = item.get_title()
113
114    if not title:
115        url = item.get_url()
116        # FIXME: This and the later occurance are user facing strings,
117        # but they ideally should never be seen. A media should always
118        # contain a URL or we can not play it, in that case it should
119        # be removed.
120        if url is None:
121            return "NO URL"
122        file_ = Gio.File.new_for_uri(url)
123        try:
124            # FIXME: query_info is not async.
125            fileinfo = file_.query_info(
126                "standard::display-name", Gio.FileQueryInfoFlags.NONE, None)
127        except GLib.Error as error:
128            MusicLogger().warning(
129                "Error: {}, {}".format(error.domain, error.message))
130            return "NO URL"
131        title = fileinfo.get_display_name()
132        title = title.replace("_", " ")
133
134    return title
135
136
137def get_media_year(item):
138    """Returns the year when the media was published.
139
140    :param Grl.Media item: A Grilo Media object
141    :return: The publication year or '----' if not defined
142    :rtype: str
143    """
144    date = item.get_publication_date()
145
146    if not date:
147        return "----"
148
149    return str(date.get_year())
150
151
152def seconds_to_string(duration):
153    """Convert a time in seconds to a mm:ss string
154
155    :param int duration: Time in seconds
156    :return: Time in mm:ss format
157    :rtype: str
158    """
159    seconds = duration
160    minutes = seconds // 60
161    seconds %= 60
162
163    return '{:d}∶{:02d}'.format(minutes, seconds)
164
165
166def normalize_caseless(text):
167    """Get a normalized casefolded version of a string.
168
169    :param str text: string to normalize
170    :returns: normalized casefolded string
171    :rtype: str
172    """
173    return unicodedata.normalize("NFKD", text.casefold())
174
175
176def natural_sort_names(name_a, name_b):
177    """Natural order comparison of two strings.
178
179    A natural order is an alphabetical order which takes into account
180    digit numbers. For example, it returns ["Album 3", "Album 10"]
181    instead of ["Album 10", "Album 3"] for an alphabetical order.
182    The names are also normalized to properly take into account names
183    which contain accents.
184
185    :param str name_a: first string to compare
186    :param str name_b: second string to compare
187    :returns: False if name_a should be before name_b. True otherwise.
188    :rtype: boolean
189    """
190    def _extract_numbers(text):
191        return [int(tmp) if tmp.isdigit() else tmp
192                for tmp in re.split(r"(\d+)", normalize_caseless(text))]
193
194    return _extract_numbers(name_b) < _extract_numbers(name_a)
195
196
197def wrap_list_store_sort_func(func):
198    """PyGI wrapper for SortListModel set_sort_func.
199    """
200    def wrap(a, b, *user_data):
201        a = pygobject_new_full(a, False)
202        b = pygobject_new_full(b, False)
203        return func(a, b, *user_data)
204
205    return wrap
206