1# Copyright (c) 2017-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
2# This program is free software: you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation, either version 3 of the License, or
5# (at your option) any later version.
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU General Public License for more details.
10# You should have received a copy of the GNU General Public License
11# along with this program. If not, see <http://www.gnu.org/licenses/>.
12
13from gi.repository import Gdk, GdkPixbuf, Gio, GLib
14
15from hashlib import md5
16from time import time
17from urllib.parse import urlparse
18
19from eolie.define import EOLIE_CACHE_PATH, ArtSize
20from eolie.utils import get_round_surface
21from eolie.logger import Logger
22
23
24class Art:
25    """
26        Base art manager
27    """
28
29    __CACHE_DELTA = 43200
30
31    def __init__(self):
32        """
33            Init base art
34        """
35        self.__use_cache = True
36        self.__create_cache()
37
38    def disable_cache(self):
39        """
40            Disable cache
41        """
42        self.__use_cache = False
43
44    def save_artwork(self, uri, surface, suffix):
45        """
46            Save artwork for uri with suffix
47            @param uri as str
48            @param surface as cairo.surface
49            @param suffix as str
50        """
51        try:
52            parsed = urlparse(uri)
53            if parsed.scheme in ["http", "https"]:
54                filepath = self.get_path(uri, suffix)
55                pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
56                                                     surface.get_width(),
57                                                     surface.get_height())
58                pixbuf.savev(filepath, "png", [None], [None])
59        except Exception as e:
60            Logger.error("Art::save_artwork(): %s", e)
61
62    def get_artwork(self, uri, suffix, scale_factor, width, heigth):
63        """
64            @param uri as str
65            @param suffix as str
66            @param scale factor as int
67            @param width as int
68            @param height as int
69            @return cairo.surface
70        """
71        if not uri:
72            return None
73        filepath = self.get_path(uri, suffix)
74        try:
75            if GLib.file_test(filepath, GLib.FileTest.IS_REGULAR):
76                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filepath,
77                                                                 width,
78                                                                 heigth,
79                                                                 True)
80                surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf,
81                                                               scale_factor,
82                                                               None)
83                return surface
84        except:
85            pass
86        return None
87
88    def get_favicon(self, uri, scale_factor):
89        """
90            @param uri as str
91            @param suffix as str
92            @param scale factor as int
93            @return cairo.surface
94        """
95        try:
96            if not uri:
97                return None
98            filepath = self.get_favicon_path(uri)
99            if filepath is not None and\
100                    GLib.file_test(filepath, GLib.FileTest.IS_REGULAR):
101                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
102                    filepath,
103                    ArtSize.FAVICON * scale_factor,
104                    ArtSize.FAVICON * scale_factor,
105                    True)
106                surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf,
107                                                               scale_factor,
108                                                               None)
109                surface = get_round_surface(surface,
110                                            scale_factor,
111                                            ArtSize.FAVICON / 4)
112                return surface
113        except Exception as e:
114            Logger.debug("Art::get_favicon(): %s", e)
115        return None
116
117    def get_icon_theme_artwork(self, uri, ephemeral):
118        """
119            Get artwork from icon theme
120            @param uri as str
121            @param ephemeral as bool
122            @return artwork as str/None
123        """
124        if ephemeral:
125            return "user-not-tracked-symbolic"
126        elif uri == "populars:":
127            return "emote-love-symbolic"
128        elif uri == "about:":
129            return "web-browser-symbolic"
130        else:
131            return None
132
133    def get_favicon_path(self, uri):
134        """
135            Return favicon cache path for uri
136            @param uri as str/None
137            @return str/None
138        """
139        if uri is None:
140            return None
141        favicon_path = self.get_path(uri, "favicon")
142        if favicon_path is not None and\
143                GLib.file_test(favicon_path, GLib.FileTest.IS_REGULAR):
144            return favicon_path
145        return None
146
147    def get_path(self, uri, suffix):
148        """
149            Return cache image path
150            @param uri as str
151            @param suffix as str
152            @return str/None
153        """
154        parsed = urlparse(uri)
155        if uri is None or not parsed.netloc:
156            return None
157        path = "%{}{}%".format(parsed.netloc, parsed.path)
158        encoded = md5(path.encode("utf-8")).hexdigest()
159        filepath = "%s/art/%s_%s.png" % (EOLIE_CACHE_PATH, encoded, suffix)
160        return filepath
161
162    def uncache(self, uri, suffix):
163        """
164            Remove from cache
165            @param uri as str
166            @param suffix as str
167        """
168        try:
169            f = Gio.File.new_for_path(self.get_path(uri, suffix))
170            f.delete()
171        except Exception as e:
172            Logger.debug("Art::uncache(): %s", e)
173
174    def exists(self, uri, suffix):
175        """
176            Check if file exists and is cached
177            @param uri as str (raise exception if None)
178            @param suffix as str
179            @return exists as bool
180        """
181        filepath = self.get_path(uri, suffix)
182        if filepath is None:
183            return True  # Because we know Lollypop will do nothing on True
184        f = Gio.File.new_for_path(filepath)
185        exists = f.query_exists()
186        if exists and self.__use_cache:
187            info = f.query_info('time::modified',
188                                Gio.FileQueryInfoFlags.NONE,
189                                None)
190            mtime = int(info.get_attribute_as_string('time::modified'))
191            return time() - mtime < self.__CACHE_DELTA
192        else:
193            return False
194
195    def vacuum(self):
196        """
197            Remove artwork older than 1 month
198        """
199        current_time = time()
200        try:
201            d = Gio.File.new_for_path("%s/art" % EOLIE_CACHE_PATH)
202            children = d.enumerate_children("standard::name",
203                                            Gio.FileQueryInfoFlags.NONE,
204                                            None)
205            for child in children:
206                f = children.get_child(child)
207                if child.get_file_type() == Gio.FileType.REGULAR:
208                    info = f.query_info("time::modified",
209                                        Gio.FileQueryInfoFlags.NONE,
210                                        None)
211                    mtime = info.get_attribute_uint64("time::modified")
212                    if current_time - mtime > 31536000:
213                        f.delete()
214        except Exception as e:
215            Logger.error("Art::vacuum(): %s", e)
216
217#######################
218# PRIVATE             #
219#######################
220    def __create_cache(self):
221        """
222            Create cache dir
223        """
224        if not GLib.file_test("%s/art" % EOLIE_CACHE_PATH,
225                              GLib.FileTest.IS_DIR):
226            try:
227                GLib.mkdir_with_parents(EOLIE_CACHE_PATH, 0o0750)
228                GLib.mkdir_with_parents("%s/art" % EOLIE_CACHE_PATH, 0o0750)
229                GLib.mkdir_with_parents("%s/css" % EOLIE_CACHE_PATH, 0o0750)
230            except Exception as e:
231                Logger.error("Art::__create_cache(): %s", e)
232