1# Copyright 2020 The GNOME Music developers
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
25import gi
26gi.require_versions({"MediaArt": "2.0", "Soup": "2.4"})
27from gi.repository import Gio, GLib, GObject, MediaArt, Soup, GdkPixbuf
28
29from gnomemusic.musiclogger import MusicLogger
30from gnomemusic.coreartist import CoreArtist
31from gnomemusic.corealbum import CoreAlbum
32from gnomemusic.coresong import CoreSong
33
34
35class StoreArt(GObject.Object):
36    """Stores Art in the MediaArt cache.
37    """
38
39    __gsignals__ = {
40        "finished": (GObject.SignalFlags.RUN_FIRST, None, ())
41    }
42
43    def __init__(self):
44        """Initialize StoreArtistArt
45
46        :param coreobject: The CoreArtist or CoreAlbum to store art for
47        :param string uri: The art uri
48        """
49        super().__init__()
50
51        self._coreobject = None
52
53        self._file = None
54        self._log = MusicLogger()
55        self._soup_session = Soup.Session.new()
56
57    def start(self, coreobject, uri):
58        self._coreobject = coreobject
59
60        if (uri is None
61                or uri == ""):
62            self._coreobject.props.thumbnail = "generic"
63            self.emit("finished")
64            return
65
66        if isinstance(self._coreobject, CoreArtist):
67            success, self._file = MediaArt.get_file(
68                self._coreobject.props.artist, None, "artist")
69        elif isinstance(self._coreobject, CoreAlbum):
70            success, self._file = MediaArt.get_file(
71                self._coreobject.props.artist, self._coreobject.props.title,
72                "album")
73        elif isinstance(self._coreobject, CoreSong):
74            success, self._file = MediaArt.get_file(
75                self._coreobject.props.artist, self._coreobject.props.album,
76                "album")
77        else:
78            success = False
79
80        if not success:
81            self._coreobject.props.thumbnail = "generic"
82            self.emit("finished")
83            return
84
85        cache_dir = GLib.build_filenamev(
86            [GLib.get_user_cache_dir(), "media-art"])
87        cache_dir_file = Gio.File.new_for_path(cache_dir)
88        cache_dir_file.query_info_async(
89            Gio.FILE_ATTRIBUTE_ACCESS_CAN_READ, Gio.FileQueryInfoFlags.NONE,
90            GLib.PRIORITY_LOW, None, self._cache_dir_info_read, uri)
91
92    def _cache_dir_info_read(self, cache_dir_file, res, uri):
93        try:
94            cache_dir_file.query_info_finish(res)
95        except GLib.Error:
96            # directory does not exist yet
97            try:
98                cache_dir_file.make_directory(None)
99            except GLib.Error as error:
100                self._log.warning(
101                    "Error: {}, {}".format(error.domain, error.message))
102                self._coreobject.props.thumbnail = "generic"
103                self.emit("finished")
104                return
105
106        msg = Soup.Message.new("GET", uri)
107        self._soup_session.queue_message(msg, self._read_callback, None)
108
109    def _read_callback(self, src, result, data):
110        if result.props.status_code != 200:
111            self._log.debug(
112                "Failed to get remote art: {}".format(
113                    result.props.reason_phrase))
114            self.emit("finished")
115            return
116
117        istream = Gio.MemoryInputStream.new_from_bytes(
118            result.props.response_body_data)
119        GdkPixbuf.Pixbuf.new_from_stream_async(
120            istream, None, self._pixbuf_from_stream_finished)
121
122    def _pixbuf_from_stream_finished(
123            self, stream: Gio.MemoryInputStream,
124            result: Gio.AsyncResult) -> None:
125        try:
126            pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
127        except GLib.Error as error:
128            self._log.warning(f"Error: {error.domain}, {error.message}")
129            self.emit("finished")
130        else:
131            self._file.create_async(
132                Gio.FileCreateFlags.NONE, GLib.PRIORITY_LOW, None,
133                self._output_stream_created, pixbuf)
134        finally:
135            stream.close_async(GLib.PRIORITY_LOW, None, self._stream_closed)
136
137    def _output_stream_created(
138            self, stream: Gio.FileOutputStream, result: Gio.AsyncResult,
139            pixbuf: GdkPixbuf.Pixbuf) -> None:
140        try:
141            output_stream = stream.create_finish(result)
142        except GLib.Error as error:
143            # File already exists.
144            self._log.info(f"Error: {error.domain}, {error.message}")
145        else:
146            pixbuf.save_to_streamv_async(
147                output_stream, "jpeg", None, None, None,
148                self._output_stream_saved, output_stream)
149
150    def _output_stream_saved(
151            self, pixbuf: GdkPixbuf.Pixbuf, result: Gio.AsyncResult,
152            output_stream: Gio.FileOutputStream) -> None:
153        try:
154            pixbuf.save_to_stream_finish(result)
155        except GLib.Error as error:
156            self._log.warning(f"Error: {error.domain}, {error.message}")
157        else:
158            self.emit("finished")
159        finally:
160            output_stream.close_async(
161                GLib.PRIORITY_LOW, None, self._stream_closed)
162
163    def _stream_closed(
164            self, stream: Gio.OutputStream, result: Gio.AsyncResult) -> None:
165        try:
166            stream.close_finish(result)
167        except GLib.Error as error:
168            self._log.warning(f"Error: {error.domain}, {error.message}")
169