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