diff --git a/README b/README index 2e8b63b..ab6768b 100644 --- a/README +++ b/README @@ -4,15 +4,15 @@ pyGTKtalog 1.0 pyGTKtalog is Linux/FreeBSD program for indexing CD/DVD or directories on filesystem. It is similar to gtktalog or gwhere . There is no coincidence in name of -application, because it's ment to be replacement (in some way) for gtktalog, +application, because it's meant to be replacement (in some way) for gtktalog, which seems to be dead project for years. FEATURES ======== - scan for files in selected media -- get/generate thumbnails from exif and other images -- most important exif tags +- get/generate thumbnails from EXIF and other images +- most important EXIF tags - add/edit description and notes - fetch comments for images made in gThumb - add/remove unlimited images to any file or directory @@ -30,19 +30,20 @@ pyGTKtalog is written in python with following dependencies: Optional modules: -- PIL for image manipulation +- PIL for image + manipulation -Additional pyGTKtalog uses pygtkmvc by Roberto -Cavada and EXIF module by Gene Cash (slightly updatetd to EXIF 2.2 by me) which -are included in sources. +Additional pyGTKtalog uses pygtkmvc by +Roberto Cavada and EXIF module by Gene Cash (slightly updatetd to EXIF 2.2 by +me) which are included in sources. -pyGTKtalog extensivly uses external programs in unix spirit, however there is -small possibility of using it Windows (probably with limitations) and quite big -possiblity to run it on other sofisticated unix-like systems (i.e. +pyGTKtalog extensively uses external programs in unix spirit, however there is +small possibility of using it Windows (probably with limitations) and quite +big possibility to run it on other sophisticated unix-like systems (i.e. BeOS/ZETA/Haiku, QNX or MacOSX). -INSTALATION -=========== +INSTALLATION +============ You don't have to install it if you don't want to. You can just change current directory to pyGTKtalog and simply run: @@ -80,11 +81,12 @@ For version 2.0: - command line support: query, adding media to collection etc - internationalization - export to XLS -- user definied group of tags (represented by color in cloud tag) -- hiding specified files - configurable, like dot prefixed, cfg and manualy - selected +- user defined group of tags (represented by color in cloud tag) +- hiding specified files - configurable, like dot prefixed, config files and + manually selected - tests - warning about existing image in media directory + Removed: - filetypes handling (movies, images, archives, documents etc). Now it have common, unified external "plugin" system - simple text output from command @@ -126,4 +128,3 @@ BUGS ==== All bugs please report to Roman 'gryf' Dobosz - diff --git a/src/ctrls/c_main.py b/src/ctrls/c_main.py index bc51fed..5eb66fa 100644 --- a/src/ctrls/c_main.py +++ b/src/ctrls/c_main.py @@ -27,7 +27,7 @@ __version__ = "1.0.1" LICENCE = '' import os.path from os import popen -from utils import deviceHelper +from utils import device_helper from gtkmvc import Controller from c_config import ConfigController @@ -526,9 +526,9 @@ class MainController(Controller): def on_add_cd_activate(self, widget, label=None, current_id=None): """Add directory structure from cd/dvd disc""" - mount = deviceHelper.volmount(self.model.config.confd['cd']) + mount = device_helper.volmount(self.model.config.confd['cd']) if mount == 'ok': - guessed_label = deviceHelper.volname(self.model.config.confd['cd']) + guessed_label = device_helper.volname(self.model.config.confd['cd']) if not label: label = Dialogs.InputDiskLabel(guessed_label).run() if label: @@ -541,7 +541,7 @@ class MainController(Controller): self.model.unsaved_project = True self.__set_title(filepath=self.model.filename, modified=True) else: - deviceHelper.volumount(self.model.config.confd['cd']) + device_helper.volumount(self.model.config.confd['cd']) return True else: Dialogs.Wrn("Error mounting device - pyGTKtalog", @@ -1373,7 +1373,7 @@ class MainController(Controller): # umount/eject cd ejectapp = self.model.config.confd['ejectapp'] if self.model.config.confd['eject'] and ejectapp: - msg = deviceHelper.eject_cd(ejectapp, + msg = device_helper.eject_cd(ejectapp, self.model.config.confd['cd']) if msg != 'ok': Dialogs.Wrn("error ejecting device - pyGTKtalog", @@ -1381,7 +1381,7 @@ class MainController(Controller): self.model.config.confd['cd'], "Last eject message:\n%s" % msg) else: - msg = deviceHelper.volumount(self.model.config.confd['cd']) + msg = device_helper.volumount(self.model.config.confd['cd']) if msg != 'ok': Dialogs.Wrn("error unmounting device - pyGTKtalog", "Cannot unmount device pointed to %s" % diff --git a/src/models/m_main.py b/src/models/m_main.py index dce1ca5..e328d6d 100644 --- a/src/models/m_main.py +++ b/src/models/m_main.py @@ -34,7 +34,11 @@ import gobject from gtkmvc.model_mt import ModelMT -from pysqlite2 import dbapi2 as sqlite +try: + import sqlite3 as sqlite +except ImportError: + from pysqlite2 import dbapi2 as sqlite + from datetime import datetime import threading as _threading @@ -50,6 +54,20 @@ from utils.gthumb import GthumbCommentParser from utils.no_thumb import no_thumb as no_thumb_img +def mangle_date(date): + """Return date object depending on the record type.""" + + if date: + try: + dateobj = datetime.fromtimestamp(date) + except TypeError: + dateobj = datetime.strptime(date, "%Y-%m-%d %H:%M:%S.%f") + else: + dateobj = datetime.fromtimestamp(0) + + return dateobj + + class MainModel(ModelMT): """Create, load, save, manipulate db file which is container for data""" @@ -169,7 +187,6 @@ class MainModel(ModelMT): else: os.mkdir(path) - if os.path.exists(imgpath): if not os.path.isdir(imgpath): print "Warning:", @@ -360,8 +377,6 @@ class MainModel(ModelMT): res = self.db_cursor.fetchone() if res and res[0]: # there is such an image. going back. - if __debug__: - print res[0] return # check if file have have thumbnail. if not, make it with first @@ -556,8 +571,6 @@ class MainModel(ModelMT): try: os.unlink(self.db_tmp_path) except: - if __debug__: - print "Exception in removing temporary db file!" pass #if self.internal_dirname != None: @@ -573,6 +586,7 @@ class MainModel(ModelMT): self.filename = None self.__create_temporary_db_file() self.__connect_to_db() + self._set_image_path() self.__create_database() self.__clear_trees() self.clear_search_tree() @@ -592,6 +606,14 @@ class MainModel(ModelMT): return if filename: + if not '.sqlite' in filename: + filename += '.sqlite' + else: + filename = filename[:filename.rindex('.sqlite')] + '.sqlite' + + if self.config.confd['compress']: + filename += '.bz2' + self.filename = filename val, err = self.__compress_and_save() if not val: @@ -683,6 +705,7 @@ class MainModel(ModelMT): #tar.close() self.__connect_to_db() + self._set_image_path() self.__fetch_db_into_treestore() self.config.add_recent(filename) self.get_tags() @@ -758,8 +781,9 @@ class MainModel(ModelMT): self.search_list.set_value(myiter, 3, self.__get_file_path(row[0])) self.search_list.set_value(myiter, 4, row[2]) - self.search_list.set_value(myiter, 5, - datetime.fromtimestamp(row[3])) + + self.search_list.set_value(myiter, 5, mangle_date(row[3])) + self.search_list.set_value(myiter, 6, 1) self.search_list.set_value(myiter, 7, gtk.STOCK_DIRECTORY) @@ -794,8 +818,7 @@ class MainModel(ModelMT): self.search_list.set_value(myiter, 3, self.__get_file_path(row[0])) self.search_list.set_value(myiter, 4, row[2]) - self.search_list.set_value(myiter, 5, - datetime.fromtimestamp(row[3])) + self.search_list.set_value(myiter, 5, mangle_date(row[3])) self.search_list.set_value(myiter, 6, row[4]) if row[4] == self.FIL: self.search_list.set_value(myiter, 7, gtk.STOCK_FILE) @@ -825,9 +848,7 @@ class MainModel(ModelMT): #self.__fetch_db_into_treestore() self.unsaved_project = True - else: - if __debug__: - print "m_main.py: rename(): no label defined" + return def refresh_discs_tree(self): @@ -879,8 +900,7 @@ class MainModel(ModelMT): self.files_list.set_value(myiter, 2, row[1]) self.files_list.set_value(myiter, 3, self.__get_file_path(row[0])) self.files_list.set_value(myiter, 4, row[2]) - self.files_list.set_value(myiter, 5, - datetime.fromtimestamp(row[3])) + self.files_list.set_value(myiter, 5, mangle_date(row[3])) self.files_list.set_value(myiter, 6, 1) self.files_list.set_value(myiter, 7, gtk.STOCK_DIRECTORY) @@ -923,8 +943,7 @@ class MainModel(ModelMT): self.files_list.set_value(myiter, 2, row[1]) self.files_list.set_value(myiter, 3, self.__get_file_path(row[0])) self.files_list.set_value(myiter, 4, row[2]) - self.files_list.set_value(myiter, 5, - datetime.fromtimestamp(row[3])) + self.files_list.set_value(myiter, 5, mangle_date(row[3])) self.files_list.set_value(myiter, 6, row[4]) if row[4] == self.FIL: self.files_list.set_value(myiter, 7, gtk.STOCK_FILE) @@ -954,11 +973,10 @@ class MainModel(ModelMT): res = self.db_cursor.fetchone() if res: retval['fileinfo'] = {'id': file_id, - 'date': datetime.fromtimestamp(res[1]), - 'size': res[2], 'type': res[3]} - - retval['fileinfo']['disc'] = self.__get_file_root(file_id) - + 'size': res[2], + 'type': res[3], + 'date': mangle_date(res[1]), + 'disc': self.__get_file_root(file_id)} retval['filename'] = res[0] if res[4]: @@ -969,9 +987,13 @@ class MainModel(ModelMT): if res[6]: thumbfile = os.path.join(self.image_path, res[6] + "_t") + thumb2 = os.path.join(self.image_path, res[6]) if os.path.exists(thumbfile): pix = gtk.gdk.pixbuf_new_from_file(thumbfile) retval['thumbnail'] = thumbfile + elif os.path.exists(thumb2): + pix = gtk.gdk.pixbuf_new_from_file(thumb2) + retval['thumbnail'] = thumb2 sql = """SELECT id, filename FROM images WHERE file_id = ?""" @@ -981,8 +1003,13 @@ class MainModel(ModelMT): self.images_store = gtk.ListStore(gobject.TYPE_INT, gtk.gdk.Pixbuf) for im_id, filename in res: thumbfile = os.path.join(self.image_path, filename + "_t") + file_, ext_ = os.path.splitext(filename) + thumb2 = os.path.join(self.image_path, + "".join([file_, "_t", ext_])) if os.path.exists(thumbfile): pix = gtk.gdk.pixbuf_new_from_file(thumbfile) + elif os.path.exists(thumb2): + pix = gtk.gdk.pixbuf_new_from_file(thumb2) else: pix = gtk.gdk.pixbuf_new_from_inline(len(no_thumb_img), no_thumb_img, False) @@ -1083,13 +1110,10 @@ class MainModel(ModelMT): sql = """DELETE FROM tags_files WHERE file_id = ?""" db_cursor.executemany(sql, generator()) - if __debug__: - print "m_main.py: delete(): deleting:", fids - - if len(fids) == 1: - arg = "(%d)" % fids[0] - else: - arg = str(tuple(fids)) + #if len(fids) == 1: + # arg = "(%d)" % fids[0] + #else: + # arg = str(tuple(fids)) # remove thumbnails #sql = """SELECT filename FROM thumbnails WHERE file_id IN %s""" % arg @@ -1258,12 +1282,15 @@ class MainModel(ModelMT): path = os.path.join(self.image_path, res[0]) if os.path.exists(path): return path + path = os.path.join('/home/gryf/.pygtktalog/imgs2/', res[0]) + if os.path.exists(path): + return path return None def update_desc_and_note(self, file_id, desc='', note=''): """update note and description""" sql = """UPDATE files SET description=?, note=? WHERE id=?""" - self.db_cursor.execute(sql, (desc, note, file_id)) + self.db_cursor.execute(sql, (unicode(desc), unicode(note), file_id)) self.db_connection.commit() return @@ -1378,6 +1405,34 @@ class MainModel(ModelMT): self.db_cursor = self.db_connection.cursor() return + def _set_image_path(self): + """hack, hack, hack!""" + if not self.filename: + return + + sql = ("select name from sqlite_master where name='config' and " + "type='table'") + if not self.db_cursor.execute(sql).fetchone(): + return + + sql = "SELECT value FROM config WHERE key = ?" + res = self.db_cursor.execute(sql, ("image_path", )).fetchone() + if not res: + return + + if res[0] == ":same_as_db:": + dir_, file_ = (os.path.dirname(self.filename), + os.path.basename(self.filename)) + file_base, dummy = os.path.splitext(file_) + self.image_path = os.path.abspath(os.path.join(dir_, file_base + + "_images")) + else: + self.image_path = res[0] + if "~" in self.image_path: + self.images_dir = os.path.expanduser(self.image_path) + if "$" in self.image_path: + self.images_dir = os.path.expandvars(self.image_path) + def __close_db_connection(self): """close db conection""" @@ -1407,8 +1462,6 @@ class MainModel(ModelMT): output_file = bz2.BZ2File(self.filename, "w") else: output_file = open(self.filename, "w") - if __debug__: - print "m_main.py: __compress_and_save(): tar open successed" except IOError, (errno, strerror): return False, strerror @@ -1597,8 +1650,6 @@ class MainModel(ModelMT): for root, dirs, files in os.walk(self.path): count += len(files) except: - if __debug__: - print 'm_main.py: os.walk in %s' % self.path pass if count > 0: @@ -1630,14 +1681,14 @@ class MainModel(ModelMT): files(parent_id, filename, filepath, date, size, type, source) VALUES(?,?,?,?,?,?,?)""" - db_cursor.execute(sql, (parent_id, name, path, date, size, - filetype, self.source)) + db_cursor.execute(sql, (parent_id, name, path.decode("utf-8"), + date, size, filetype, self.source)) else: self.discs_tree.set_value(myit, 2, gtk.STOCK_DIRECTORY) sql = """INSERT INTO files(parent_id, filename, filepath, date, size, type) VALUES(?,?,?,?,?,?)""" - db_cursor.execute(sql, (parent_id, name, path, + db_cursor.execute(sql, (parent_id, name, path.decode("utf-8"), date, size, filetype)) sql = """SELECT seq FROM sqlite_sequence WHERE name='files'""" @@ -1645,14 +1696,13 @@ class MainModel(ModelMT): currentid = db_cursor.fetchone()[0] self.discs_tree.set_value(myit, 0, currentid) + self.discs_tree.set_value(myit, 1, name) self.discs_tree.set_value(myit, 3, parent_id) try: root, dirs, files = os.walk(path).next() except: - if __debug__: - print "m_main.py: cannot access ", path #return -1 return 0 @@ -1721,8 +1771,12 @@ class MainModel(ModelMT): sql = """INSERT INTO files(parent_id, filename, filepath, date, size, type) VALUES(?,?,?,?,?,?)""" - db_cursor.execute(sql, (currentid, j, current_file, - st_mtime, st_size, self.FIL)) + try: + db_cursor.execute(sql, (currentid, unicode(j), + unicode(current_file), + st_mtime, st_size, self.FIL)) + except: + raise if self.count % 32 == 0: update = True @@ -1812,7 +1866,10 @@ class MainModel(ModelMT): sql = """UPDATE files SET description=? WHERE id=?""" - db_cursor.execute(sql, (desc, fileid)) + db_cursor.execute(sql, + (unicode(desc.decode("utf8", + "ignore")), + fileid)) ### end of scan if update: @@ -1827,18 +1884,11 @@ class MainModel(ModelMT): return _size if __recurse(1, self.label, self.path, 0, 0, self.DIR) == -1: - if __debug__: - print "m_main.py: __scan() __recurse()", - print "interrupted self.abort = True" self.discs_tree.remove(self.fresh_disk_iter) db_cursor.close() db_connection.rollback() else: - if __debug__: - print "m_main.py: __scan() __recurse() goes without interrupt" if self.currentid: - if __debug__: - print "m_main.py: __scan() removing old branch" self.statusmsg = "Removing old branch..." self.delete(self.currentid, db_cursor, db_connection) @@ -1847,8 +1897,6 @@ class MainModel(ModelMT): db_cursor.close() db_connection.commit() db_connection.close() - if __debug__: - print "m_main.py: __scan() time: ", (datetime.now() - timestamp) self.busy = False @@ -1903,13 +1951,8 @@ class MainModel(ModelMT): gtk.STOCK_DIRECTORY) return - if __debug__: - start_date = datetime.now() # launch scanning. get_children() - if __debug__: - print "m_main.py: __fetch_db_into_treestore()", - print "tree generation time: ", (datetime.now() - start_date) db_connection.close() return @@ -1944,13 +1987,8 @@ class MainModel(ModelMT): gtk.STOCK_DIRECTORY) return - if __debug__: - start_date = datetime.now() # launch scanning. get_children() - if __debug__: - print "m_main.py: __append_added_volume() tree generation time: ", - print datetime.now() - start_date db_connection.close() return diff --git a/src/utils/deviceHelper.py b/src/utils/deviceHelper.py deleted file mode 100644 index 8ee2a52..0000000 --- a/src/utils/deviceHelper.py +++ /dev/null @@ -1,112 +0,0 @@ -# This Python file uses the following encoding: utf-8 -# -# Author: Roman 'gryf' Dobosz gryf@elysium.pl -# -# Copyright (C) 2007 by Roman 'gryf' Dobosz -# -# This file is part of pyGTKtalog. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -# ------------------------------------------------------------------------- -""" -device (cd, dvd) helper -""" - -import os - - -def volname(mntp): - """read volume name from cd/dvd""" - dev = mountpoint_to_dev(mntp) - if dev != None: - try: - a = open(dev,"rb") - a.seek(32808) - b = a.read(32).strip() - a.close() - except: - return None - return b - return None - - -def volmount(mntp): - """mount device, return 'ok' or error message""" - _in,_out,_err = os.popen3("mount %s" % mntp) - inf = _err.readlines() - if len(inf) > 0: - for i in inf: - i.strip() - return i - else: - return 'ok' - - -def volumount(mntp): - """mount device, return 'ok' or error message""" - _in,_out,_err = os.popen3("umount %s" % mntp) - inf = _err.readlines() - if len(inf) > 0: - for error in inf: - error.strip() - - if error.strip()[:7] == 'umount:': - return error.strip() - return 'ok' - - -def check_mount(dev): - """Refresh the entries from fstab or mount.""" - mounts = os.popen('mount') - for line in mounts.readlines(): - parts = line.split() - device, txt1, mount_point, txt2, filesystem, options = parts - if device == dev: - return True - return False - - -def mountpoint_to_dev(mntp): - """guess mountpoint from fstab""" - fstab = open("/etc/fstab") - for line in fstab.readlines(): - a = line.split() - try: - if a[1] == mntp and a[0][0] != '#': - fstab.close() - return a[0] - except: - pass - fstab.close() - return None - - -def eject_cd(eject_app, cd): - """mount device, return 'ok' or error message""" - if len(eject_app) > 0: - _in,_out,_err = os.popen3("%s %s" % (eject_app, cd)) - inf = _err.readlines() - error = '' - - for error in inf: - error.strip() - - if error !='': - return error - - return 'ok' - return "Eject program not specified" - diff --git a/src/utils/img.py b/src/utils/img.py index 81f31cc..27a556a 100644 --- a/src/utils/img.py +++ b/src/utils/img.py @@ -26,7 +26,8 @@ from shutil import copy from os import path from hashlib import sha512 -import Image +from PIL import Image + class Img(object): diff --git a/src/utils/thumbnail.py b/src/utils/thumbnail.py index e832d4b..cbfcc8e 100644 --- a/src/utils/thumbnail.py +++ b/src/utils/thumbnail.py @@ -29,7 +29,7 @@ from os import path import sys from utils import EXIF -import Image +from PIL import Image class Thumbnail(object): """Class for generate/extract thumbnail from image file""" @@ -56,7 +56,7 @@ class Thumbnail(object): # rotated 90 CW 8: Image.ROTATE_90} # Rotated 90 CCW flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT} - + image_file = open(self.filename, 'rb') try: exif = EXIF.process_file(image_file) @@ -71,7 +71,7 @@ class Thumbnail(object): if __debug__: print "file", self.filename, "with hash", self.sha512, "exists" return self.sha512, exif - + if 'JPEGThumbnail' in exif: if __debug__: print self.filename, "exif thumb" @@ -79,15 +79,15 @@ class Thumbnail(object): thumb_file = open(self.thumbnail_path, 'wb') thumb_file.write(exif_thumbnail) thumb_file.close() - + if 'Image Orientation' in exif: orient = exif['Image Orientation'].values[0] if orient > 1 and orient in orientations: temp_image_path = mkstemp()[1] - + thumb_image = Image.open(self.thumbnail_path) tmp_thumb_img = thumb_image.transpose(orientations[orient]) - + if orient in flips: tmp_thumb_img = tmp_thumb_img.transpose(flips[orient]) diff --git a/src/views/v_image.py b/src/views/v_image.py index 1764194..7fe09f8 100644 --- a/src/views/v_image.py +++ b/src/views/v_image.py @@ -34,9 +34,8 @@ class ImageView(object): image = gtk.Image() image.set_from_file(image_filename) - pixbuf = image.get_pixbuf() - pic_width = pixbuf.get_width() + 23 - pic_height = pixbuf.get_height() + 23 + pic_width = image.size_request()[0] + 23 + pic_height = image.size_request()[1] + 23 screen_width = gtk.gdk.screen_width() screen_height = gtk.gdk.screen_height()