1#!/usr/local/bin/python3.8
2
3from gi.repository import Gio, GLib
4import dbus, dbus.service, dbus.glib
5from dbus.mainloop.glib import DBusGMainLoop
6import random
7import os, locale
8from xml.etree import ElementTree
9from setproctitle import setproctitle
10
11SLIDESHOW_DBUS_NAME = "org.Cinnamon.Slideshow"
12SLIDESHOW_DBUS_PATH = "/org/Cinnamon/Slideshow"
13
14BACKGROUND_COLLECTION_TYPE_DIRECTORY = "directory"
15BACKGROUND_COLLECTION_TYPE_XML = "xml"
16
17class CinnamonSlideshow(dbus.service.Object):
18    def __init__(self):
19        bus_name = dbus.service.BusName(SLIDESHOW_DBUS_NAME, bus=dbus.SessionBus())
20        dbus.service.Object.__init__(self, bus_name, SLIDESHOW_DBUS_PATH)
21
22        self.slideshow_settings = Gio.Settings(schema="org.cinnamon.desktop.background.slideshow")
23        self.background_settings = Gio.Settings(schema="org.cinnamon.desktop.background")
24
25        if self.slideshow_settings.get_boolean("slideshow-paused"):
26            self.slideshow_settings.set_boolean("slideshow-paused", False)
27
28        self.image_playlist = []
29        self.used_image_playlist = []
30        self.images_ready = False
31        self.update_in_progress = False
32        self.current_image = self.background_settings.get_string("picture-uri")
33
34        self.update_id = 0
35        self.loop_counter = self.slideshow_settings.get_int("delay")
36
37        self.folder_monitor = None
38        self.folder_monitor_id = 0
39
40    @dbus.service.method(SLIDESHOW_DBUS_NAME, in_signature='', out_signature='')
41    def begin(self):
42        self.setup_slideshow()
43
44    @dbus.service.method(SLIDESHOW_DBUS_NAME, in_signature='', out_signature='')
45    def end(self):
46        if self.update_id > 0:
47            GLib.source_remove(self.update_id)
48            self.update_id = 0
49
50        ml.quit()
51
52    @dbus.service.method(SLIDESHOW_DBUS_NAME, in_signature='', out_signature='')
53    def getNextImage(self):
54        if self.update_id > 0:
55            GLib.source_remove(self.update_id)
56            self.update_id = 0
57
58        self.loop_counter = self.slideshow_settings.get_int("delay")
59        self.start_mainloop()
60
61    def setup_slideshow(self):
62        self.load_settings()
63        self.connect_signals()
64        self.gather_images()
65        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
66            self.connect_folder_monitor()
67        self.start_mainloop()
68
69    def format_source(self, type, path):
70        # returns 'type://path'
71        return ("%s://%s" % (type, path))
72
73    def load_settings(self):
74        self.random_order = self.slideshow_settings.get_boolean("random-order")
75        self.collection = self.slideshow_settings.get_string("image-source")
76        self.collection_path = ""
77        self.collection_type = None
78        if self.collection != "" and "://" in self.collection:
79            (self.collection_type, self.collection_path) = self.collection.split("://")
80            self.collection_path = os.path.expanduser(self.collection_path)
81
82    def connect_signals(self):
83        self.slideshow_settings.connect("changed::image-source", self.on_slideshow_source_changed)
84        self.slideshow_settings.connect("changed::random-order", self.on_random_order_changed)
85        self.background_settings.connect("changed::picture-uri", self.on_picture_uri_changed)
86
87    def connect_folder_monitor(self):
88        folder_path = Gio.file_new_for_path(self.collection_path)
89        self.folder_monitor = folder_path.monitor_directory(0, None)
90        self.folder_monitor_id = self.folder_monitor.connect("changed", self.on_monitored_folder_changed)
91
92    def disconnect_folder_monitor(self):
93        if self.folder_monitor_id > 0:
94            self.folder_monitor.disconnect(self.folder_monitor_id)
95            self.folder_monitor_id = 0
96
97    def gather_images(self):
98        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
99            folder_at_path = Gio.file_new_for_path(self.collection_path)
100
101            if folder_at_path.query_exists(None):
102                folder_at_path.enumerate_children_async("standard::name,standard::type,standard::content-type",
103                                                        Gio.FileQueryInfoFlags.NONE,
104                                                        GLib.PRIORITY_LOW,
105                                                        None,
106                                                        self.gather_images_cb,
107                                                        None)
108
109        elif self.collection_type == BACKGROUND_COLLECTION_TYPE_XML:
110            pictures = self.parse_xml_backgrounds_list(self.collection_path)
111            for picture in pictures:
112                filename = picture["filename"]
113                self.add_image_to_playlist(filename)
114
115    def gather_images_cb(self, obj, res, user_data):
116        all_files = []
117        enumerator = obj.enumerate_children_finish(res)
118        def on_next_file_complete(obj, res, user_data=all_files):
119            files = obj.next_files_finish(res)
120            file_list = all_files
121            if len(files) != 0:
122                file_list = file_list.extend(files)
123                enumerator.next_files_async(100, GLib.PRIORITY_LOW, None, on_next_file_complete, None)
124            else:
125                enumerator.close(None)
126                self.ensure_file_is_image(file_list)
127
128        enumerator.next_files_async(100, GLib.PRIORITY_LOW, None, on_next_file_complete, all_files)
129
130    def ensure_file_is_image(self, file_list):
131        for item in file_list:
132            file_type = item.get_file_type();
133            if file_type is not Gio.FileType.DIRECTORY:
134                file_contents = item.get_content_type();
135                if file_contents.startswith("image"):
136                    self.add_image_to_playlist(self.collection_path + "/" + item.get_name())
137
138    def add_image_to_playlist(self, file_path):
139        image = Gio.file_new_for_path(file_path)
140        image_uri = image.get_uri();
141        self.image_playlist.append(image_uri)
142        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
143            self.image_playlist.sort()
144        self.images_ready = True
145
146    def on_slideshow_source_changed(self, settings, key):
147        if self.update_id > 0:
148            GLib.source_remove(self.update_id)
149            self.update_id = 0
150        self.disconnect_folder_monitor()
151        self.image_playlist = []
152        self.used_image_playlist = []
153        self.images_ready = False
154        self.collection = self.slideshow_settings.get_string("image-source")
155        self.collection_path = ""
156        self.collection_type = None
157        if self.collection != "" and "://" in self.collection:
158            (self.collection_type, self.collection_path) = self.collection.split("://")
159            self.collection_path = os.path.expanduser(self.collection_path)
160        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
161            self.connect_folder_monitor()
162        self.gather_images()
163        self.loop_counter = self.slideshow_settings.get_int("delay")
164        self.start_mainloop()
165
166    def on_monitored_folder_changed(self, monitor, file1, file2, event_type):
167        try:
168            if event_type == Gio.FileMonitorEvent.DELETED:
169                file_uri = file1.get_uri();
170                if self.image_playlist.count(file_uri) > 0:
171                    index_to_remove = self.image_playlist.index(file_uri)
172                    del self.image_playlist[index_to_remove]
173                elif self.used_image_playlist.count(file_uri) > 0:
174                    index_to_remove = self.used_image_playlist.index(file_uri)
175                    del self.used_image_playlist[index_to_remove]
176
177            if event_type == Gio.FileMonitorEvent.CREATED:
178                file_path = file1.get_path()
179                file_info = file1.query_info("standard::type,standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
180                file_type = file_info.get_file_type()
181                if file_type is not Gio.FileType.DIRECTORY:
182                    file_contents = file_info.get_content_type();
183                    if file_contents.startswith("image"):
184                        self.add_image_to_playlist(file_path)
185        except:
186            pass
187
188    def on_random_order_changed(self, settings, key):
189        self.random_order = self.slideshow_settings.get_boolean("random-order")
190
191    def on_picture_uri_changed(self, settings, key):
192        if self.update_in_progress:
193            return
194        else:
195            if self.background_settings.get_string("picture-uri") != self.current_image:
196                self.slideshow_settings.set_boolean("slideshow-enabled", False)
197
198    def start_mainloop(self):
199        if self.update_id > 0:
200            GLib.source_remove(self.update_id)
201            self.update_id = 0
202
203        if not self.images_ready:
204            self.update_id = GLib.timeout_add_seconds(1, self.start_mainloop)
205        else:
206            if self.loop_counter >= self.slideshow_settings.get_int("delay") and not self.slideshow_settings.get_boolean("slideshow-paused"):
207                self.loop_counter = 1
208                self.update_background()
209                self.update_id = GLib.timeout_add_seconds(60, self.start_mainloop)
210            else:
211                self.loop_counter = self.loop_counter + 1
212                self.update_id = GLib.timeout_add_seconds(60, self.start_mainloop)
213
214    def update_background(self):
215        if self.update_in_progress:
216            return
217
218        self.update_in_progress = True
219
220        if len(self.image_playlist) == 0:
221            self.move_used_images_to_original_playlist()
222
223        next_image = self.get_next_image_from_list()
224        if next_image is not None:
225            self.background_settings.set_string("picture-uri", next_image)
226            self.current_image = next_image
227
228        self.update_in_progress = False
229
230    def get_next_image_from_list(self):
231        if self.random_order:
232            index = random.randint(0, len(self.image_playlist) - 1)
233            image = self.image_playlist[index]
234        else:
235            index = 0
236            image = self.image_playlist[index]
237
238        self.move_image_to_used_playlist(index, image)
239
240        return image
241
242    def move_image_to_used_playlist(self, index, image):
243        self.image_playlist.pop(index)
244        self.used_image_playlist.append(image)
245
246    def move_used_images_to_original_playlist(self):
247        self.image_playlist = self.used_image_playlist
248        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
249            self.image_playlist.sort()
250        self.used_image_playlist = []
251
252
253########### TAKEN FROM CS_BACKGROUND
254    def splitLocaleCode(self, localeCode):
255        loc = localeCode.partition("_")
256        loc = (loc[0], loc[2])
257        return loc
258
259    def getLocalWallpaperName(self, names, loc):
260        result = ""
261        mainLocFound = False
262        for wp in names:
263            wpLoc = wp[0]
264            wpName = wp[1]
265            if wpLoc == ("", ""):
266                if not mainLocFound:
267                    result = wpName
268            elif wpLoc[0] == loc[0]:
269                if wpLoc[1] == loc[1]:
270                    return wpName
271                elif wpLoc[1] == "":
272                    result = wpName
273                    mainLocFound = True
274        return result
275
276    def parse_xml_backgrounds_list(self, filename):
277        try:
278            locAttrName = "{http://www.w3.org/XML/1998/namespace}lang"
279            loc = self.splitLocaleCode(locale.getdefaultlocale()[0])
280            res = []
281            subLocaleFound = False
282            f = open(filename)
283            rootNode = ElementTree.fromstring(f.read())
284            f.close()
285            if rootNode.tag == "wallpapers":
286                for wallpaperNode in rootNode:
287                    if wallpaperNode.tag == "wallpaper" and wallpaperNode.get("deleted") != "true":
288                        wallpaperData = {"metadataFile": filename}
289                        names = []
290                        for prop in wallpaperNode:
291                            if type(prop.tag) == str:
292                                if prop.tag != "name":
293                                    wallpaperData[prop.tag] = prop.text
294                                else:
295                                    propAttr = prop.attrib
296                                    wpName = prop.text
297                                    locName = self.splitLocaleCode(propAttr.get(locAttrName)) if locAttrName in propAttr else ("", "")
298                                    names.append((locName, wpName))
299                        wallpaperData["name"] = self.getLocalWallpaperName(names, loc)
300
301                        if "filename" in wallpaperData and wallpaperData["filename"] != "" and os.path.exists(wallpaperData["filename"]) and os.access(wallpaperData["filename"], os.R_OK):
302                            if wallpaperData["name"] == "":
303                                wallpaperData["name"] = os.path.basename(wallpaperData["filename"])
304                            res.append(wallpaperData)
305            return res
306        except Exception as detail:
307            print(detail)
308            return []
309###############
310
311if __name__ == "__main__":
312    setproctitle("cinnamon-slideshow")
313    DBusGMainLoop(set_as_default=True)
314
315    sessionBus = dbus.SessionBus ()
316    request = sessionBus.request_name(SLIDESHOW_DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
317    if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
318        slideshow = CinnamonSlideshow()
319    else:
320        print("cinnamon-slideshow already running.")
321        quit()
322
323    ml = GLib.MainLoop.new(None, True)
324    ml.run()
325