Fixed bug with anonymous profile.
[automanga.git] / automanga.py
CommitLineData
861c3221
TW
1#!/usr/bin/python
2#encoding: utf8
3#
4# automanga
5# by Kaka <kaka@dolda2000.com>
6
7import os, sys, optparse
8from user import home
9try:
10 import gtk
11 import pygtk; pygtk.require("2.0")
12except AssertionError, e:
13 error("You need to install the package 'python-gtk2'")
14
15DIR_BASE = os.path.join(home, ".automanga")
16DIR_PROFILES = os.path.join(DIR_BASE, "profiles")
17FILE_SETTINGS = os.path.join(DIR_BASE, "settings")
18
19def init():
20 global settings, profile, opts, args, cwd
21
22 if not os.path.exists(DIR_PROFILES):
23 os.makedirs(DIR_PROFILES)
24 settings = Settings()
25
26 usage = "Usage: %prog [options]"
27 parser = optparse.OptionParser(usage) # one delete option to use in combination with profile and directory?
28 parser.add_option("-p", "--profile", help="load or create a profile", metavar="profile")
29 parser.add_option("-r", "--remove-profile", help="remove profile", metavar="profile")
30 parser.add_option("-a", "--add", help="add a directory", metavar="dir")
31 parser.add_option("-d", "--delete", help="delete a directory", metavar="dir")
32 parser.add_option("-s", "--silent", help="no output", default=False, action="store_true")
33 parser.add_option("-v", "--verbose", help="show output", default=False, action="store_true")
34 opts, args = parser.parse_args()
35
36 cwd = os.getcwd()
37
38def load_profile(name):
39 global profile
40 profile = Profile(name)
41 settings.last_profile(profile.name)
861c3221
TW
42
43def output(msg):
44 if not settings.silent():
45 print msg
46
47def error(msg):
48 print >>sys.stderr, msg
49 sys.exit(1)
50
51def abs_path(path):
52 """Returns the absolute path"""
53 if not os.path.isabs(path): ret = os.path.join(cwd, path)
54 else: ret = path
55 return os.path.abspath(ret)
56
57def manga_dir(path):
58 """Checks if path is a manga directory"""
59 for node in os.listdir(path):
60 if node.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp"):
61 return True
62 return False
63
64def natsorted(strings):
65 """Sorts a list of strings naturally"""
66# import re
67# return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
68 return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in s.rsplit(".")[0].split("-")])
69
70class Reader(object):
71 """GUI"""
72 def __init__(self):
73 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
74 self.window.connect("delete_event", lambda widget, event, data = None: False)
75 self.window.connect("destroy", lambda widget, data = None: self.quit())
76 self.window.connect("key_press_event", self.keypress)
77# self.window.set_border_width(10)
78 self.window.set_position(gtk.WIN_POS_CENTER) # save position in settings?
79# self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#222")) # seems to do nothing
80
81 self.set_title()
82 self.fullscreen = False
83 self.cursor = None
84
85 self.container = gtk.VBox()
86 self.menu_bar = self.build_menu()
87 self.page_bar = gtk.HBox()
88 self.key = gtk.Label()
89# self.key.set_padding(10, 10)
90 self.page_num = gtk.Label()
91 self.page = gtk.Image()
92 self.sep = gtk.HSeparator()
93 self.sbar = gtk.Statusbar(); self.sbar.show(); self.sbar.set_has_resize_grip(False)
94
95 self.combo = gtk.combo_box_new_text()
96 self.combo.set_wrap_width(2)
97 for i in range(50):
98 self.combo.append_text('item - %d' % i)
99 self.combo.set_active(0)
100
101 self.page_bar.pack_start(self.key, expand=False)
102 vsep = gtk.VSeparator(); vsep.show(); self.page_bar.pack_start(vsep, expand=False)
103# self.page_bar.pack_start(self.combo, expand=False); self.combo.show()
104 self.page_bar.pack_start(self.page_num, expand=False)
105# self.container.pack_start(self.menu_bar, expand=False)
106 self.container.pack_start(self.page_bar, expand=False)
107 self.container.pack_start(self.sep, expand=False)
108 self.container.pack_start(self.page)
109 self.container.pack_start(self.sbar, expand=False)
110 self.window.add(self.container)
111
112 self.key.show()
113 self.page_num.show()
114 self.sep.show()
115 self.page.show()
116 self.page_bar.show()
117 self.container.show()
118 self.window.show()
119
120 def build_menu(self):
121 menus = (("_File", (
122 ("_New profile", gtk.STOCK_NEW, lambda widget, data: None),
123 ("_Load profile", gtk.STOCK_OPEN, lambda widget, data: None),
124 ("_Delete profile", gtk.STOCK_DELETE, lambda widget, data: None),
125 (),
126 ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()),
127 )),
128 ("_Edit", (
129 ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None),
130 (),
131 ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None),
132 )),
133 )
134 menu_bar = gtk.MenuBar()
135 menu_bar.show()
136 for submenu in menus:
137 lbl, items = submenu
138 menu = gtk.Menu()
139 menu.show()
140 mi = gtk.MenuItem(lbl, True)
141 mi.show()
142 mi.set_submenu(menu)
143 menu_bar.add(mi)
144 for item in items:
145 if not item:
146 mi = gtk.SeparatorMenuItem()
147 mi.show()
148 menu.add(mi)
149 else:
150 lbl, icon, func = item
151 img = gtk.Image()
152 img.set_from_stock(icon, gtk.ICON_SIZE_MENU)
153 mi = gtk.ImageMenuItem(lbl, True)
154 mi.show()
155 mi.set_image(img)
156 mi.connect("activate", func, None)
157 menu.add(mi)
158 return menu_bar
159
160 def start(self):
161 gtk.main()
162
163 def quit(self):
164 gtk.main_quit()
165
166 def set_title(self, title = None):
167 self.window.set_title("Automanga" + (" - " + title if title else ""))
168
169 def set_manga(self, manga):
170 self.manga = manga
171 self.set_title(manga.title())
172 self.cursor = manga.mark
173 self.update_page()
174
175 def update_page(self):
176 self.page.set_from_file(self.cursor.path)
177 self.page_num.set_label("Mark's at %s (%s/%s)\t\tYou're at %s (%s/%s)" % (self.manga.mark.name, self.manga.index_of(self.manga.mark) + 1, self.manga.num_pages(), self.cursor.name, self.manga.index_of(self.cursor) + 1, self.manga.num_pages()))
178 self.window.resize(*self.container.size_request())
179
180 self.sbar.pop(self.sbar.get_context_id("stat"))
181 self.sbar.push(self.sbar.get_context_id("stat"), "Mark's at %s (%s/%s)\t\tYou're at %s (%s/%s)" % (self.manga.mark.name, self.manga.index_of(self.manga.mark) + 1, self.manga.num_pages(), self.cursor.name, self.manga.index_of(self.cursor) + 1, self.manga.num_pages()))
182
183 def keypress(self, widget, event, data = None):
184 if event.keyval in [32]: self.read_page(1) # space
185 elif event.keyval in [65288]: self.read_page(-1) # backspace
186 elif event.keyval in [65362, 65363]: self.browse_page(1) # up, right
187 elif event.keyval in [65361, 65364]: self.browse_page(-1) # left, down
188 elif event.keyval in [70, 102]: self.toggle_fullscreen() # f, F
189 elif event.keyval in [81, 113, 65307]: self.quit() # q, Q, esc
190 elif event.keyval in [65360]: self.browse_start() # home
191 elif event.keyval in [65367]: self.browse_end() # end
192 else: self.key.set_text(str(event.keyval))
193
194 def browse_page(self, step):
195 self.cursor = self.cursor.previous() if step < 0 else self.cursor.next()
196 self.update_page()
197
198 def read_page(self, step):
199 if self.cursor == self.manga.mark:
200 if step < 0: self.manga.set_mark(self.cursor.previous())
201 else: self.cursor = self.cursor.next()
202 if step < 0: self.cursor = self.manga.mark
203 else: self.manga.set_mark(self.cursor)
204 self.update_page()
205
206 def browse_start(self):
207 self.cursor = self.manga.first_page()
208 self.update_page()
209
210 def browse_end(self):
211 self.cursor = self.manga.last_page()
212 self.update_page()
213
214 def toggle_fullscreen(self):
215 self.fullscreen = not self.fullscreen
216# if self.fullscreen: self.window.fullscreen()
217# else: self.window.unfullscreen()
218 if self.fullscreen:
219 self.window.set_decorated(False)
220 self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
221 self.menu_bar.hide()
222 else:
223 self.window.set_decorated(True)
224 self.window.set_position(gtk.WIN_POS_NONE)
225 self.menu_bar.show()
226
227class File(object):
d9b99a70
TW
228 """A class for accessing the parsed content of a file as
229 attributes on an object."""
861c3221
TW
230
231 def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes
232 self.path = path
233 self.attributes = [] ## make this a dict which stores the attributes, instead of adding them as attributes to the File object
234 if os.path.exists(path):
235 file = open(path)
236 self.parse(file)
237 file.close()
238 elif create:
239 self.save()
240
241 def __setattr__(self, name, val):
242 if name not in ("path", "attributes") and name not in self.attributes:
243 self.attributes.append(name)
244 object.__setattr__(self, name, val)
245
246 def __getattr__(self, name):
247 try: return object.__getattribute__(self, name)
248 except: return None
249
250 def __delattr__(self, name):
251 self.attributes.remove(name)
252 object.__delattr__(self, name)
253
254 def parse(self, file):
255 def add_attr(type, name, val):
256 if type and name and val:
257 if type == "str": val = val[0]
258 elif type == "int": val = int(val[0])
259 elif type == "bool": val = (val[0].lower() == "true")
260 setattr(self, name, val)
261 type, attr, val = None, "", []
262 for line in file.xreadlines():
263 line = line.strip()
264 if line.startswith("["):
265 add_attr(type, attr, val)
266 (type, attr), val = line[1:-1].split(":"), []
267 elif line:
268 val.append(line)
269 add_attr(type, attr, val)
270
271 def exists(self):
272 return os.path.exists(self.path)
273
d9b99a70
TW
274 def delete(self):
275 self.attributes = []
276 os.remove(self.path)
277
861c3221 278 def save(self):
d9b99a70
TW
279 dir = self.path.rsplit("/", 1)[0]
280 if not os.path.exists(dir):
281 os.makedirs(dir)
861c3221
TW
282 file = open(self.path, "w")
283 for attr in self.attributes:
284 val = getattr(self, attr)
285 file.write("[%s:%s]\n" % (str(type(val))[7:-2], attr))
286 if type(val) in (list, tuple):
287 for i in val:
288 file.write(i + "\n")
289 else:
290 file.write(str(val) + "\n")
291 file.write("\n")
292 file.close()
293
1773398f
TW
294 def save_all(self):
295 self.save()
296
d9b99a70
TW
297class Directory(object):
298 """A class for accessing files, or directories, in a directory as
299 attributes on an object."""
300
301 def __init__(self, path):
302 self._path = path
303 self._nodes = [f for f in os.listdir(path) if "." not in f] if os.path.exists(path) else []
304 self._opened_nodes = {}
305
1773398f
TW
306 def __iter__(self):
307 return self._opened_nodes.itervalues()
308
7939bf81
TW
309 def __contains__(self, name):
310 return name in self._nodes
311
1773398f
TW
312 def __getitem__(self, name):
313 if name in self._nodes:
314 return getattr(self, name)
315 raise KeyError(name)
316
d9b99a70
TW
317 def __getattr__(self, name):
318 if name in self._nodes:
319 if name not in self._opened_nodes:
320 if os.path.isdir(self.path(name)): self._opened_nodes[name] = Directory(self.path(name))
321 else: self._opened_nodes[name] = File(self.path(name))
322 return self._opened_nodes[name]
323 return object.__getattribute__(self, name)
324
325 def __delattr__(self, name):
326 if name in self._nodes:
327 self._nodes.remove(name)
328 if name in self._opened_nodes:
329 self._opened_nodes.pop(name).delete()
330 else:
331 object.__delattr__(self, name)
332
333 def delete(self):
334 for n in self._nodes:
335 getattr(self, n).delete()
336 self._opened_nodes = {}
337 self._nodes = []
338 os.rmdir(self.path())
339
340 def save(self):
1773398f
TW
341 if not self.exists():
342 os.makedirs(self.path())
343
344 def save_all(self):
345 self.save()
346 for name, node in self._opened_nodes.items():
347 node.save_all()
348
349 def exists(self):
350 return os.path.exists(self.path())
351
352 def move(self, new_path):
353 if not self.exists() or os.path.exists(new_path):
354 return False
355 os.rename(self.path(), new_path)
356 self._path = new_path
357 return True
d9b99a70
TW
358
359 def path(self, *name):
360 return os.path.join(self._path, *name)
361
362 def add_dir(self, name):
363 if not os.path.exists(self.path(name)):
364 self._nodes.append(name)
365 self._opened_nodes[name] = Directory(self.path(name))
1773398f 366 return getattr(self, name)
d9b99a70
TW
367
368 def add_file(self, name):
369 if not os.path.exists(self.path(name)):
370 self._nodes.append(name)
371 self._opened_nodes[name] = File(self.path(name))
1773398f 372 return getattr(self, name)
d9b99a70 373
861c3221
TW
374class Settings(object):
375 def __init__(self):
376 self.file = File(FILE_SETTINGS, True)
377 if self.file.dirs is None:
378 self.file.dirs = []
379
380 def save(self):
381 self.file.save()
382
383 def add_dir(self, path):
384 if not os.path.exists(path):
385 output("Failed to add directory - '%s' does not exist!" % path)
386 return
387 if path not in self.file.dirs:
388 self.file.dirs.append(path)
389 self.save()
390 output("Added '%s'!" % path)
391
392 def delete_dir(self, dir):
393 if path not in self.file.dirs:
394 output("Failed to remove directory - '%s' not in list!" % path)
395 return
396 self.file.dirs.remove(dir)
397 self.save()
398 output("Removed '%s'!" % path)
399
400 def silent(self, state = None):
401 if state is None: return self.file.silent
402 else: self.file.silent = state
403
404 def last_profile(self, val = None):
405 if val is None: return self.file.last_profile
406 else: self.file.last_profile = val
407
408 def load_last_profile(self):
409 if not self.file.last_profile:
410 return False
411 load_profile(self.file.last_profile)
412 if not profile.exists():
413 del self.file.last_profile
414 self.save()
415 return False
416 return True
417
418 def list_manga(self):
419 ret = []
420 for dir in self.file.dirs:
421 if os.path.exists(dir):
422 ret += os.listdir(dir)
423 return ret
424
425class Profile(object):
426 def __init__(self, name):
427 self.name = name
1773398f
TW
428 self.dir = Directory(os.path.join(DIR_PROFILES, name))
429 self.mangas = self.dir.add_dir("manga")
430 self.settings = self.dir.add_file("settings")
861c3221
TW
431
432 def exists(self):
1773398f 433 return self.dir.exists()
861c3221
TW
434
435 def delete(self):
1773398f 436 self.dir.delete()
861c3221
TW
437
438 def rename(self, new_name):
1773398f
TW
439 if not self.dir.move(os.path.join(DIR_PROFILES, new_name)):
440 return False
861c3221
TW
441 self.name = new_name
442 return True
443
444 def save(self):
1773398f 445 self.dir.save_all()
861c3221 446
1773398f
TW
447 def save_page(self, manga):
448 if manga.title() not in self.mangas:
449 self.mangas.add_file(manga.title())
450 file = self.mangas[manga.title()]
7939bf81 451 file.page = manga.mark.name
1773398f 452 file.save()
861c3221
TW
453
454 def load_manga(self, path):
1773398f
TW
455 self.settings.last_read = path
456 manga = path.rsplit("/", 1)[-1]
457 if manga in self.mangas:
458 return Manga(self, path, self.mangas[manga].page)
861c3221
TW
459 return Manga(self, path)
460
461class Manga(object):
462 def __init__(self, reader_profile, path, page = None):
463 self.reader = reader_profile
464 self.path = path
465 self.pages = self.load_pages()
466 self.mark = Page(self, page) if page else self.first_page()
467
468 def load_pages(self):
469 files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
470 return natsorted(files) # check if there's an order file / only load image files
471
472 def first_page(self):
473 return Page(self, self.pages[0])
474
475 def last_page(self):
476 return Page(self, self.pages[-1])
477
478 def num_pages(self):
479 return len(self.pages)
480
481 def index_of(self, page):
482 return self.pages.index(page.name)
483
484 def set_mark(self, page):
485 self.mark = page
1773398f 486 self.reader.save_page(self)
861c3221
TW
487
488 def previous(self, page, step = 1):
489 return Page(self, self.pages[max(0, self.pages.index(page) - step)])
490
491 def next(self, page, step = 1):
492 return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
493
494 def title(self):
495 return self.path.rsplit("/", 1)[-1]
496
497class Page(object):
498 def __init__(self, manga, name):
499 self.manga = manga
500 self.name = name
501 self.path = os.path.join(manga.path, name)
502
503 def previous(self, step = 1):
504 return self.manga.previous(self.name, step)
505
506 def next(self, step = 1):
507 return self.manga.next(self.name, step)
508
509if __name__ == "__main__":
510 init()
511
512 if opts.add:
513 path = abs_path(opts.add)
514 settings.add_dir(path)
515
516 if opts.delete:
517 path = abs_path(opts.delete)
518 settings.delete_dir(path)
519
520 if opts.silent:
521 settings.silent(True)
522 if opts.verbose:
523 settings.silent(False)
524
525 if opts.profile:
526 load_profile(opts.profile)
d9b99a70
TW
527 if profile.exists():
528 output("Loaded profile '%s'" % profile.name)
529 else:
861c3221
TW
530 profile.save()
531 output("Created profile '%s'" % profile.name)
532 else:
d9b99a70
TW
533 if settings.load_last_profile():
534 output("No profile specified - loading '%s'" % profile.name)
535 else:
861c3221
TW
536 user = home.split("/")[-1]
537 output("No profile exists - creating one for '%s'" % user)
538 load_profile(user)
539 profile.save()
540
541 reader = Reader()
542 manga_path = abs_path(args[0]) if args else cwd
543 if manga_dir(manga_path):
544 reader.set_manga(profile.load_manga(manga_path))
545 reader.start()
546
547 profile.save()
548 settings.save()