Initial commit
[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)
42 if profile.exists():
43 output("Loaded profile '%s'" % profile.name)
44
45def output(msg):
46 if not settings.silent():
47 print msg
48
49def error(msg):
50 print >>sys.stderr, msg
51 sys.exit(1)
52
53def abs_path(path):
54 """Returns the absolute path"""
55 if not os.path.isabs(path): ret = os.path.join(cwd, path)
56 else: ret = path
57 return os.path.abspath(ret)
58
59def manga_dir(path):
60 """Checks if path is a manga directory"""
61 for node in os.listdir(path):
62 if node.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp"):
63 return True
64 return False
65
66def natsorted(strings):
67 """Sorts a list of strings naturally"""
68# import re
69# return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
70 return sorted(strings, key=lambda s: [int(t) if t.isdigit() else t for t in s.rsplit(".")[0].split("-")])
71
72class Reader(object):
73 """GUI"""
74 def __init__(self):
75 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
76 self.window.connect("delete_event", lambda widget, event, data = None: False)
77 self.window.connect("destroy", lambda widget, data = None: self.quit())
78 self.window.connect("key_press_event", self.keypress)
79# self.window.set_border_width(10)
80 self.window.set_position(gtk.WIN_POS_CENTER) # save position in settings?
81# self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#222")) # seems to do nothing
82
83 self.set_title()
84 self.fullscreen = False
85 self.cursor = None
86
87 self.container = gtk.VBox()
88 self.menu_bar = self.build_menu()
89 self.page_bar = gtk.HBox()
90 self.key = gtk.Label()
91# self.key.set_padding(10, 10)
92 self.page_num = gtk.Label()
93 self.page = gtk.Image()
94 self.sep = gtk.HSeparator()
95 self.sbar = gtk.Statusbar(); self.sbar.show(); self.sbar.set_has_resize_grip(False)
96
97 self.combo = gtk.combo_box_new_text()
98 self.combo.set_wrap_width(2)
99 for i in range(50):
100 self.combo.append_text('item - %d' % i)
101 self.combo.set_active(0)
102
103 self.page_bar.pack_start(self.key, expand=False)
104 vsep = gtk.VSeparator(); vsep.show(); self.page_bar.pack_start(vsep, expand=False)
105# self.page_bar.pack_start(self.combo, expand=False); self.combo.show()
106 self.page_bar.pack_start(self.page_num, expand=False)
107# self.container.pack_start(self.menu_bar, expand=False)
108 self.container.pack_start(self.page_bar, expand=False)
109 self.container.pack_start(self.sep, expand=False)
110 self.container.pack_start(self.page)
111 self.container.pack_start(self.sbar, expand=False)
112 self.window.add(self.container)
113
114 self.key.show()
115 self.page_num.show()
116 self.sep.show()
117 self.page.show()
118 self.page_bar.show()
119 self.container.show()
120 self.window.show()
121
122 def build_menu(self):
123 menus = (("_File", (
124 ("_New profile", gtk.STOCK_NEW, lambda widget, data: None),
125 ("_Load profile", gtk.STOCK_OPEN, lambda widget, data: None),
126 ("_Delete profile", gtk.STOCK_DELETE, lambda widget, data: None),
127 (),
128 ("_Quit", gtk.STOCK_QUIT, lambda widget, data: self.quit()),
129 )),
130 ("_Edit", (
131 ("_Profile", gtk.STOCK_EDIT, lambda widget, data: None),
132 (),
133 ("_Settings", gtk.STOCK_PREFERENCES, lambda widget, data: None),
134 )),
135 )
136 menu_bar = gtk.MenuBar()
137 menu_bar.show()
138 for submenu in menus:
139 lbl, items = submenu
140 menu = gtk.Menu()
141 menu.show()
142 mi = gtk.MenuItem(lbl, True)
143 mi.show()
144 mi.set_submenu(menu)
145 menu_bar.add(mi)
146 for item in items:
147 if not item:
148 mi = gtk.SeparatorMenuItem()
149 mi.show()
150 menu.add(mi)
151 else:
152 lbl, icon, func = item
153 img = gtk.Image()
154 img.set_from_stock(icon, gtk.ICON_SIZE_MENU)
155 mi = gtk.ImageMenuItem(lbl, True)
156 mi.show()
157 mi.set_image(img)
158 mi.connect("activate", func, None)
159 menu.add(mi)
160 return menu_bar
161
162 def start(self):
163 gtk.main()
164
165 def quit(self):
166 gtk.main_quit()
167
168 def set_title(self, title = None):
169 self.window.set_title("Automanga" + (" - " + title if title else ""))
170
171 def set_manga(self, manga):
172 self.manga = manga
173 self.set_title(manga.title())
174 self.cursor = manga.mark
175 self.update_page()
176
177 def update_page(self):
178 self.page.set_from_file(self.cursor.path)
179 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()))
180 self.window.resize(*self.container.size_request())
181
182 self.sbar.pop(self.sbar.get_context_id("stat"))
183 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()))
184
185 def keypress(self, widget, event, data = None):
186 if event.keyval in [32]: self.read_page(1) # space
187 elif event.keyval in [65288]: self.read_page(-1) # backspace
188 elif event.keyval in [65362, 65363]: self.browse_page(1) # up, right
189 elif event.keyval in [65361, 65364]: self.browse_page(-1) # left, down
190 elif event.keyval in [70, 102]: self.toggle_fullscreen() # f, F
191 elif event.keyval in [81, 113, 65307]: self.quit() # q, Q, esc
192 elif event.keyval in [65360]: self.browse_start() # home
193 elif event.keyval in [65367]: self.browse_end() # end
194 else: self.key.set_text(str(event.keyval))
195
196 def browse_page(self, step):
197 self.cursor = self.cursor.previous() if step < 0 else self.cursor.next()
198 self.update_page()
199
200 def read_page(self, step):
201 if self.cursor == self.manga.mark:
202 if step < 0: self.manga.set_mark(self.cursor.previous())
203 else: self.cursor = self.cursor.next()
204 if step < 0: self.cursor = self.manga.mark
205 else: self.manga.set_mark(self.cursor)
206 self.update_page()
207
208 def browse_start(self):
209 self.cursor = self.manga.first_page()
210 self.update_page()
211
212 def browse_end(self):
213 self.cursor = self.manga.last_page()
214 self.update_page()
215
216 def toggle_fullscreen(self):
217 self.fullscreen = not self.fullscreen
218# if self.fullscreen: self.window.fullscreen()
219# else: self.window.unfullscreen()
220 if self.fullscreen:
221 self.window.set_decorated(False)
222 self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
223 self.menu_bar.hide()
224 else:
225 self.window.set_decorated(True)
226 self.window.set_position(gtk.WIN_POS_NONE)
227 self.menu_bar.show()
228
229class File(object):
230 """Loads and parses a file and returns a File object from which
231 you can access the parsed sections as attributes"""
232
233 def __init__(self, path, create = False): ## add autosync - save everytime an attribute is set - does not work well with list attributes
234 self.path = path
235 self.attributes = [] ## make this a dict which stores the attributes, instead of adding them as attributes to the File object
236 if os.path.exists(path):
237 file = open(path)
238 self.parse(file)
239 file.close()
240 elif create:
241 self.save()
242
243 def __setattr__(self, name, val):
244 if name not in ("path", "attributes") and name not in self.attributes:
245 self.attributes.append(name)
246 object.__setattr__(self, name, val)
247
248 def __getattr__(self, name):
249 try: return object.__getattribute__(self, name)
250 except: return None
251
252 def __delattr__(self, name):
253 self.attributes.remove(name)
254 object.__delattr__(self, name)
255
256 def parse(self, file):
257 def add_attr(type, name, val):
258 if type and name and val:
259 if type == "str": val = val[0]
260 elif type == "int": val = int(val[0])
261 elif type == "bool": val = (val[0].lower() == "true")
262 setattr(self, name, val)
263 type, attr, val = None, "", []
264 for line in file.xreadlines():
265 line = line.strip()
266 if line.startswith("["):
267 add_attr(type, attr, val)
268 (type, attr), val = line[1:-1].split(":"), []
269 elif line:
270 val.append(line)
271 add_attr(type, attr, val)
272
273 def exists(self):
274 return os.path.exists(self.path)
275
276 def save(self):
277 file = open(self.path, "w")
278 for attr in self.attributes:
279 val = getattr(self, attr)
280 file.write("[%s:%s]\n" % (str(type(val))[7:-2], attr))
281 if type(val) in (list, tuple):
282 for i in val:
283 file.write(i + "\n")
284 else:
285 file.write(str(val) + "\n")
286 file.write("\n")
287 file.close()
288
289class Settings(object):
290 def __init__(self):
291 self.file = File(FILE_SETTINGS, True)
292 if self.file.dirs is None:
293 self.file.dirs = []
294
295 def save(self):
296 self.file.save()
297
298 def add_dir(self, path):
299 if not os.path.exists(path):
300 output("Failed to add directory - '%s' does not exist!" % path)
301 return
302 if path not in self.file.dirs:
303 self.file.dirs.append(path)
304 self.save()
305 output("Added '%s'!" % path)
306
307 def delete_dir(self, dir):
308 if path not in self.file.dirs:
309 output("Failed to remove directory - '%s' not in list!" % path)
310 return
311 self.file.dirs.remove(dir)
312 self.save()
313 output("Removed '%s'!" % path)
314
315 def silent(self, state = None):
316 if state is None: return self.file.silent
317 else: self.file.silent = state
318
319 def last_profile(self, val = None):
320 if val is None: return self.file.last_profile
321 else: self.file.last_profile = val
322
323 def load_last_profile(self):
324 if not self.file.last_profile:
325 return False
326 load_profile(self.file.last_profile)
327 if not profile.exists():
328 del self.file.last_profile
329 self.save()
330 return False
331 return True
332
333 def list_manga(self):
334 ret = []
335 for dir in self.file.dirs:
336 if os.path.exists(dir):
337 ret += os.listdir(dir)
338 return ret
339
340class Profile(object):
341 def __init__(self, name):
342 self.name = name
343 self.file = File(os.path.join(DIR_PROFILES, name))
344 if not self.file.mangas:
345 self.file.mangas = []
346
347 def exists(self):
348 return self.file.exists()
349
350 def delete(self):
351 os.remove(self.file.path)
352
353 def rename(self, new_name):
354 if self.exists():
355 new_path = os.path.join(DIR_PROFILES, new_name)
356 if os.path.exists(new_path):
357 return False
358 os.rename(self.file.path, new_path)
359 self.name = new_name
360 return True
361
362 def save(self):
363 self.file.save()
364
365 def save_page(self, manga, page):
366 for n, m in enumerate(self.file.mangas):
367 manga_path = m.split("\t")[0]
368 if manga_path == manga.path:
369 self.file.mangas[n] = "%s\t%s" % (manga_path, page.name)
370 break
371 else:
372 self.file.mangas.append("%s\t%s" % (manga.path, page.name))
373 self.save()
374
375 def load_manga(self, path):
376 self.file.last_read = path
377 for manga in self.file.mangas:
378 manga_path, page_name = manga.split("\t")
379 if manga_path == path:
380 return Manga(self, path, page_name)
381 return Manga(self, path)
382
383class Manga(object):
384 def __init__(self, reader_profile, path, page = None):
385 self.reader = reader_profile
386 self.path = path
387 self.pages = self.load_pages()
388 self.mark = Page(self, page) if page else self.first_page()
389
390 def load_pages(self):
391 files = [f for f in os.listdir(self.path) if f.rsplit(".", 1)[-1] in ("jpg", "png", "gif", "bmp")]
392 return natsorted(files) # check if there's an order file / only load image files
393
394 def first_page(self):
395 return Page(self, self.pages[0])
396
397 def last_page(self):
398 return Page(self, self.pages[-1])
399
400 def num_pages(self):
401 return len(self.pages)
402
403 def index_of(self, page):
404 return self.pages.index(page.name)
405
406 def set_mark(self, page):
407 self.mark = page
408 self.reader.save_page(self, page)
409
410 def previous(self, page, step = 1):
411 return Page(self, self.pages[max(0, self.pages.index(page) - step)])
412
413 def next(self, page, step = 1):
414 return Page(self, self.pages[min(len(self.pages) - 1, self.pages.index(page) + step)])
415
416 def title(self):
417 return self.path.rsplit("/", 1)[-1]
418
419class Page(object):
420 def __init__(self, manga, name):
421 self.manga = manga
422 self.name = name
423 self.path = os.path.join(manga.path, name)
424
425 def previous(self, step = 1):
426 return self.manga.previous(self.name, step)
427
428 def next(self, step = 1):
429 return self.manga.next(self.name, step)
430
431if __name__ == "__main__":
432 init()
433
434 if opts.add:
435 path = abs_path(opts.add)
436 settings.add_dir(path)
437
438 if opts.delete:
439 path = abs_path(opts.delete)
440 settings.delete_dir(path)
441
442 if opts.silent:
443 settings.silent(True)
444 if opts.verbose:
445 settings.silent(False)
446
447 if opts.profile:
448 load_profile(opts.profile)
449 if not profile.exists():
450 profile.save()
451 output("Created profile '%s'" % profile.name)
452 else:
453 if not settings.load_last_profile():
454 user = home.split("/")[-1]
455 output("No profile exists - creating one for '%s'" % user)
456 load_profile(user)
457 profile.save()
458
459 reader = Reader()
460 manga_path = abs_path(args[0]) if args else cwd
461 if manga_dir(manga_path):
462 reader.set_manga(profile.load_manga(manga_path))
463 reader.start()
464
465 profile.save()
466 settings.save()