]> code.delx.au - bg-scripts/blob - randombg.py
Automated merge with ssh://hg@kagami.tsukasa.net.au/bg_scripts/
[bg-scripts] / randombg.py
1 #!/usr/bin/env python
2
3 VERSION = "2.0"
4
5
6 import asyncore, asynchat, socket
7 import os, os.path, random, sys, time
8 from optparse import OptionParser
9 import logging
10 try:
11 logging.basicConfig(format="%(levelname)s: %(message)s")
12 except TypeError:
13 # Python 2.3's logging.basicConfig does not support parameters
14 logging.basicConfig()
15
16 try:
17 import cPickle as pickle
18 except ImportError:
19 import pickle
20
21 try:
22 # Required libraries
23 import asyncsched
24 import wallchanger
25 except ImportError, e:
26 logging.critical("Missing libraries! Exiting...", exc_info=1)
27 sys.exit(1)
28
29
30
31
32 def filter_images(filenames):
33 extensions = ('.jpg', '.jpe', '.jpeg', '.png', '.gif', '.bmp')
34 for filename in filenames:
35 _, ext = os.path.splitext(filename)
36 if ext.lower() in extensions:
37 yield filename
38
39 class BaseFileList(object):
40 """Base file list implementation"""
41 def __init__(self):
42 self.paths = []
43 self.favourites = []
44
45 def add_path(self, path):
46 self.paths.append(path)
47
48 def store_cache(self, filename):
49 try:
50 logging.debug("Attempting to store cache")
51 fd = open(filename, 'wb')
52 pickle.dump(self, fd, 2)
53 logging.debug("Cache successfully stored")
54 except Exception, e:
55 warning("Storing cache: %s" % e)
56
57 def load_cache(self, filename):
58 try:
59 logging.debug("Attempting to load cache from: %s" % filename)
60 self.paths.sort()
61
62 fd = open(filename, 'rb')
63 tmp = pickle.load(fd)
64
65 if tmp.__class__ != self.__class__:
66 raise ValueError("Using different file list type")
67
68 self.paths.sort()
69 if self.paths != getattr(tmp, "paths"):
70 raise ValueError("Path list changed")
71
72 for attr, value in tmp.__dict__.items():
73 setattr(self, attr, value)
74
75 return True
76
77 except Exception, e:
78 logging.warning("Loading cache: %s" % e)
79 return False
80
81 def add_to_favourites(self):
82 '''Adds the current image to the list of favourites'''
83 self.favourites.append(self.get_current_image())
84
85 def scan_paths(self):
86 raise NotImplementedError()
87
88 def get_next_image(self):
89 raise NotImplementedError()
90
91 def get_prev_image(self):
92 raise NotImplementedError()
93
94 def get_current_image(self):
95 raise NotImplementedError()
96
97 def is_empty(self):
98 return True
99
100
101 class RandomFileList(BaseFileList):
102 def __init__(self):
103 super(RandomFileList, self).__init__()
104 self.list = []
105 self.last_image = None
106
107 def scan_paths(self):
108 for path in self.paths:
109 for dirpath, dirsnames, filenames in os.walk(path):
110 for filename in filter_images(filenames):
111 self.list.append(os.path.join(dirpath, filename))
112
113 def add_path(self, path):
114 self.paths.append(path)
115 logging.debug('Added path "%s" to the list' % path)
116
117 def get_next_image(self):
118 n = random.randint(0, len(self.list)-1)
119 self.last_image = self.list[n]
120 logging.debug("Picked file '%s' from list" % self.last_image)
121 return self.last_image
122
123 def get_current_image(self):
124 if self.last_image:
125 return self.last_image
126 else:
127 return self.get_next_image()
128
129 def is_empty(self):
130 return len(self.list) == 0
131
132
133 class AllRandomFileList(BaseFileList):
134 def __init__(self):
135 super(AllRandomFileList, self).__init__()
136 self.list = None
137 self.imagePointer = 0
138
139 # Scan the input directory, and then randomize the file list
140 def scan_paths(self):
141 logging.debug("Scanning paths")
142
143 self.list = []
144 for path in self.paths:
145 logging.debug('Scanning "%s"' % path)
146 for dirpath, dirsnames, filenames in os.walk(path):
147 for filename in filter_images(filenames):
148 logging.debug('Adding file "%s"' % filename)
149 self.list.append(os.path.join(dirpath, filename))
150
151 random.shuffle(self.list)
152
153 def add_path(self, path):
154 self.paths.append(path)
155 logging.debug('Added path "%s" to the list' % path)
156
157 def store_cache(self, filename):
158 try:
159 fd = open(filename, 'wb')
160 pickle.dump(self, fd, 2)
161 logging.debug("Cache successfully stored")
162 except Exception, e:
163 logging.warning("Storing cache", exc_info=1)
164
165 def load_cache(self, filename, rescanPaths = False):
166 logging.debug('Attempting to load cache from "%s"' % filename)
167 self.paths.sort()
168 try:
169 fd = open(filename, 'rb')
170 tmp = pickle.load(fd)
171 if self.paths == tmp.paths:
172 logging.debug("Path lists match, copying properties")
173 # Overwrite this object with the other
174 for attr in ('list', 'imagePointer', 'favourites'):
175 setattr(self, attr, getattr(tmp, attr))
176 else:
177 logging.debug("Ignoring cache, path lists do not match")
178 except Exception, e:
179 logging.warning("Loading cache", exc_info=1)
180 else:
181 return True
182
183 def get_current_image(self):
184 return self.list[self.imagePointer]
185
186 def __inc_in_range(self, n, amount = 1, rangeMax = None, rangeMin = 0):
187 if rangeMax == None: rangeMax = len(self.list)
188 assert rangeMax > 0
189 return (n + amount) % rangeMax
190
191 def get_next_image(self):
192 self.imagePointer = self.__inc_in_range(self.imagePointer)
193 imageName = self.list[self.imagePointer]
194 logging.debug("Picked file '%s' (pointer=%d) from list" % (imageName, self.imagePointer))
195 return imageName
196
197 def get_prev_image(self):
198 self.imagePointer = self.__inc_in_range(self.imagePointer, amount=-1)
199 imageName = self.list[self.imagePointer]
200 logging.debug("Picked file '%s' (pointer=%d) from list" % (imageName, self.imagePointer))
201 return imageName
202
203 def is_empty(self):
204 return len(self.list) == 0
205
206 class FolderRandomFileList(BaseFileList):
207 """A file list that will pick a file randomly within a directory. Each
208 directory has the same chance of being chosen."""
209 def __init__(self):
210 super(FolderRandomFileList, self).__init__()
211 self.directories = {}
212 self.last_image = None
213
214 def scan_paths(self):
215 pass
216
217 def add_path(self, path):
218 logging.debug('Added path "%s" to the list' % path)
219 for dirpath, dirs, filenames in os.walk(path):
220 logging.debug('Scanning "%s" for images' % dirpath)
221 if self.directories.has_key(dirpath):
222 continue
223 filenames = list(filter_images(filenames))
224 if len(filenames):
225 self.directories[dirpath] = filenames
226 logging.debug('Adding "%s" to "%s"' % (filenames, dirpath))
227 else:
228 logging.debug("No images found in '%s'" % dirpath)
229
230 def get_next_image(self):
231 directory = random.choice(self.directories.keys())
232 logging.debug('directory: "%s"' % directory)
233 filename = random.choice(self.directories[directory])
234 logging.debug('filename: "%s"' % filename)
235 return os.path.join(directory, filename)
236
237 def get_current_image(self):
238 if self.last_image:
239 return self.last_image
240 else:
241 return self.get_next_image()
242
243 def is_empty(self):
244 return len(self.directories.values()) == 0
245
246
247 class Cycler(object):
248 def init(self, options, paths):
249 self.cycle_time = options.cycle_time
250 self.history_filename = options.history_filename
251
252 logging.debug("Initialising wallchanger")
253 wallchanger.init(options.background_colour, options.permanent, options.convert)
254
255 logging.debug("Initialising file list")
256 if options.all_random:
257 self.filelist = AllRandomFileList()
258 elif options.folder_random:
259 self.filelist = FolderRandomFileList()
260 else:
261 self.filelist = RandomFileList()
262
263 for path in paths:
264 self.filelist.add_path(path)
265
266 if self.filelist.load_cache(self.history_filename):
267 logging.debug("Loaded cache successfully")
268 else:
269 logging.debug("Could not load cache")
270 self.filelist.scan_paths()
271
272 if self.filelist.is_empty():
273 logging.error("No images were found. Exiting...")
274 sys.exit(1)
275
276 self.task = None
277 self.cmd_reload()
278
279 def finish(self):
280 self.filelist.store_cache(self.history_filename)
281
282 def find_files(self, options, paths):
283 return filelist
284
285 def cmd_reset(self):
286 def next():
287 image = self.filelist.get_next_image()
288 wallchanger.set_image(image)
289 self.task = None
290 self.cmd_reset()
291
292 if self.task is not None:
293 self.task.cancel()
294 self.task = asyncsched.schedule(self.cycle_time, next)
295 logging.debug("Reset timer for %s seconds" % self.cycle_time)
296
297 def cmd_reload(self):
298 image = self.filelist.get_current_image()
299 wallchanger.set_image(image)
300 self.cmd_reset()
301
302 def cmd_next(self):
303 image = self.filelist.get_next_image()
304 wallchanger.set_image(image)
305 self.cmd_reset()
306
307 def cmd_prev(self):
308 image = self.filelist.get_prev_image()
309 wallchanger.set_image(image)
310 self.cmd_reset()
311
312 def cmd_rescan(self):
313 self.filelist.scan_paths()
314
315 def cmd_pause(self):
316 if self.task is not None:
317 self.task.cancel()
318 self.task = None
319
320 def cmd_exit(self):
321 asyncsched.exit()
322
323 def cmd_favourite(self):
324 self.filelist.add_to_favourites()
325
326 class Server(asynchat.async_chat):
327 def __init__(self, cycler, conn, addr):
328 asynchat.async_chat.__init__(self, conn=conn)
329 self.cycler = cycler
330 self.ibuffer = []
331 self.set_terminator("\n")
332
333 def collect_incoming_data(self, data):
334 self.ibuffer.append(data)
335
336 def found_terminator(self):
337 line = "".join(self.ibuffer).lower()
338 self.ibuffer = []
339 prefix, cmd = line.split(None, 1)
340 if prefix != "cmd":
341 logging.debug('Bad line received "%s"' % line)
342 return
343 if hasattr(self.cycler, "cmd_" + cmd):
344 logging.debug('Executing command "%s"' % cmd)
345 getattr(self.cycler, "cmd_" + cmd)()
346 else:
347 logging.debug('Unknown command received "%s"' % cmd)
348
349
350
351 class Listener(asyncore.dispatcher):
352 def __init__(self, socket_filename, cycler):
353 asyncore.dispatcher.__init__(self)
354 self.cycler = cycler
355 self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
356 self.bind(socket_filename)
357 self.listen(2) # Backlog = 2
358
359 def handle_accept(self):
360 conn, addr = self.accept()
361 Server(self.cycler, conn, addr)
362
363 def writable(self):
364 return False
365
366
367 def do_server(options, paths):
368 try:
369 cycler = Cycler()
370 listener = Listener(options.socket_filename, cycler)
371 # Initialisation of Cycler delayed so we grab the socket quickly
372 cycler.init(options, paths)
373 try:
374 asyncsched.loop()
375 except KeyboardInterrupt:
376 print
377 cycler.finish()
378 finally:
379 # Make sure that the socket is cleaned up
380 try:
381 os.unlink(options.socket_filename)
382 except:
383 pass
384
385 def do_client(options, args):
386 if len(args) == 0:
387 args = ["next"]
388 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
389 sock.connect(options.socket_filename)
390 sock = sock.makefile()
391 for i, cmd in enumerate(args):
392 sock.write("cmd %s\n" % cmd)
393 if i < len(args) - 1:
394 time.sleep(options.cycle_time)
395 sock.close()
396
397 def do_oneshot(options, paths):
398 cycler = Cycler()
399 cycler.init(options, paths)
400
401 def build_parser():
402 parser = OptionParser(version="%prog " + VERSION,
403 description = "Cycles through random background images.",
404 usage =
405 "\n(server) %prog [options] dir [dir2 ...]"
406 "\n(client) %prog [options] [next|prev|rescan|reload|pause] [...]"
407 "\nThe first instance to be run will be the server.\n"
408 )
409 parser.add_option("-p", "--permanent",
410 action="store_true", dest="permanent", default=False,
411 help="Make the background permanent. Note: This will cause all machines logged in with this account to simultaneously change background [Default: %default]")
412 parser.add_option("-v", '-d', "--verbose", "--debug",
413 action="count", dest="verbose", default=0,
414 help="Make the louder (good for debugging, or those who are curious)")
415 parser.add_option("-b", "--background-colour",
416 action="store", type="string", dest="background_colour", default="black",
417 help="Change the default background colour that is displayed if the image is not in the correct aspect ratio [Default: %default]")
418 parser.add_option("--all-random",
419 action="store_true", dest="all_random", default=False,
420 help="Make sure that all images have been displayed before repeating an image")
421 parser.add_option("-1", "--oneshot",
422 action="store_true", dest="oneshot", default=False,
423 help="Set one random image and terminate immediately.")
424 parser.add_option("--folder-random",
425 action="store_true", dest="folder_random", default=False,
426 help="Give each folder an equal chance of having an image selected from it")
427 parser.add_option("--convert",
428 action="store_true", dest="convert", default=False,
429 help="Do conversions using ImageMagick or PIL, don't rely on the window manager")
430 parser.add_option("--cycle-time",
431 action="store", type="int", default=1800, dest="cycle_time",
432 help="Cause the image to cycle every X seconds")
433 parser.add_option("--socket",
434 action="store", type="string", dest="socket_filename", default=os.path.expanduser('~/.randombg_socket'),
435 help="Location of the command/control socket.")
436 parser.add_option("--history-file",
437 action="store", type="string", dest="history_filename", default=os.path.expanduser('~/.randombg_historyfile'),
438 help="Stores the location of the last image to be loaded.")
439 return parser
440
441 def main():
442 parser = build_parser()
443 options, args = parser.parse_args(sys.argv[1:])
444
445 if options.verbose == 1:
446 logging.getLogger().setLevel(logging.INFO)
447 elif options.verbose >= 2:
448 logging.getLogger().setLevel(logging.DEBUG)
449
450 if options.oneshot:
451 do_oneshot(options, args)
452
453 if os.path.exists(options.socket_filename):
454 do_client(options, args)
455 else:
456 do_server(options, args)
457
458
459 if __name__ == "__main__":
460 main()
461