1
0
mirror of https://github.com/gryf/pygtktalog.git synced 2025-12-17 19:40:21 +01:00

Added observable property for discs current directory,

Clean up dialogs a bit
Some additional variables in init (mainly for about() stuff)
Rearrange video module for usage PIL, which is slightly faster than
 ImageMagick utils
This commit is contained in:
2010-11-17 22:25:53 +01:00
parent 54b6a377bf
commit 3c6c9a552a
8 changed files with 297 additions and 261 deletions

View File

@@ -7,6 +7,11 @@
""" """
__version__ = "1.9.0" __version__ = "1.9.0"
__appname__ = "pyGTKtalog"
__copyright__ = u"\u00A9 Roman 'gryf' Dobosz"
__summary__ = "%s is simple tool for managing file collections." % __appname__
__web__ = "http://bitbucket.org/gryf"
__logo_img__ = "views/pixmaps/Giant Worms.png"
import os import os
import locale import locale

View File

@@ -18,19 +18,24 @@ class DiscsController(Controller):
""" """
Controller for discs TreeView Controller for discs TreeView
""" """
def __init__(self, model, view): def __init__(self, model, view):
""" """
Initialize DiscsController Initialize DiscsController
""" """
LOG.debug(self.__init__.__doc__.strip()) LOG.debug(self.__init__.__doc__.strip())
Controller.__init__(self, model, view) Controller.__init__(self, model, view)
self.discs_model = self.model.discs.discs
def register_view(self, view): def register_view(self, view):
""" """
Do DiscTree registration Do DiscTree registration
""" """
LOG.debug(self.register_view.__doc__.strip()) LOG.debug(self.register_view.__doc__.strip())
view['discs'].set_model(self.model.discs) view['discs'].set_model(self.discs_model)
# register observers
self.model.discs.register_observer(self)
# connect signals to popup menu - framework somehow omits automatic # connect signals to popup menu - framework somehow omits automatic
# signal connection for subviews which are not included to widgets # signal connection for subviews which are not included to widgets
@@ -51,7 +56,7 @@ class DiscsController(Controller):
# make cell text editabe # make cell text editabe
cell.set_property('editable', True) cell.set_property('editable', True)
cell.connect('edited', self.on_editing_done, self.model.discs) cell.connect('edited', self.on_editing_done, self.discs_model)
# TODO: find a way how to disable default return keypress on editable # TODO: find a way how to disable default return keypress on editable
# fields # fields
@@ -93,8 +98,8 @@ class DiscsController(Controller):
LOG.debug(self.on_discs_cursor_changed.__doc__.strip()) LOG.debug(self.on_discs_cursor_changed.__doc__.strip())
selection = treeview.get_selection() selection = treeview.get_selection()
path = selection.get_selected_rows()[1][0] path = selection.get_selected_rows()[1][0]
self.model.update_files(self.model.discs.get_value(\ self.model.files.refresh(self.discs_model.get_value(\
self.model.discs.get_iter(path), 0)) self.discs_model.get_iter(path), 0))
def on_discs_key_release_event(self, treeview, event): def on_discs_key_release_event(self, treeview, event):
""" """
@@ -172,6 +177,13 @@ class DiscsController(Controller):
LOG.debug(self.on_statistics_activate.__doc__.strip()) LOG.debug(self.on_statistics_activate.__doc__.strip())
raise NotImplementedError raise NotImplementedError
# observable properties
def property_currentdir_value_change(self, model, old, new):
"""
Change of a current dir signalized by other controllers/models
"""
LOG.debug(self.property_currentdir_value_change.__doc__.strip())
# private methods # private methods
def _popup_menu(self, selection, event, button): def _popup_menu(self, selection, event, button):
""" """

View File

@@ -26,13 +26,13 @@ class FilesController(Controller):
""" """
Controller.__init__(self, model, view) Controller.__init__(self, model, view)
self.DND_TARGETS = [('files_tags', 0, 69)] self.DND_TARGETS = [('files_tags', 0, 69)]
self.files_model = self.model.files
def register_view(self, view): def register_view(self, view):
""" """
Register view, and setup columns for files treeview Register view, and setup columns for files treeview
""" """
view['files'].set_model(self.model.files) view['files'].set_model(self.files_model.files)
sigs = {"add_tag": ("activate", self.on_add_tag1_activate), sigs = {"add_tag": ("activate", self.on_add_tag1_activate),
"delete_tag": ("activate", self.on_delete_tag_activate), "delete_tag": ("activate", self.on_delete_tag_activate),
@@ -46,7 +46,6 @@ class FilesController(Controller):
for signal in sigs: for signal in sigs:
view.menu[signal].connect(sigs[signal][0], sigs[signal][1]) view.menu[signal].connect(sigs[signal][0], sigs[signal][1])
view['files'].get_selection().set_mode(gtk.SELECTION_MULTIPLE) view['files'].get_selection().set_mode(gtk.SELECTION_MULTIPLE)
col = gtk.TreeViewColumn(_('Disc'), gtk.CellRendererText(), text=1) col = gtk.TreeViewColumn(_('Disc'), gtk.CellRendererText(), text=1)
@@ -108,10 +107,11 @@ class FilesController(Controller):
""" """
Handle right click on files treeview - show popup menu. Handle right click on files treeview - show popup menu.
""" """
LOG.debug(self.on_files_button_press_event.__doc__.strip()) LOG.debug("Mouse button pressed")
pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
if event.button == 3: # Right mouse button. Show context menu. if event.button == 3: # Right mouse button. Show context menu.
LOG.debug("It's a right button")
if pathinfo: if pathinfo:
path = pathinfo[0] path = pathinfo[0]
@@ -151,6 +151,7 @@ class FilesController(Controller):
# self.view['edit'].set_sensitive(True) # self.view['edit'].set_sensitive(True)
#self.__popup_menu(event, 'files_popup') #self.__popup_menu(event, 'files_popup')
#return True #return True
LOG.debug("It's other button")
def on_files_cursor_changed(self, treeview): def on_files_cursor_changed(self, treeview):
"""Show details of selected file/directory""" """Show details of selected file/directory"""
@@ -171,75 +172,96 @@ class FilesController(Controller):
self._popup_menu(selection, event, 0) self._popup_menu(selection, event, 0)
if gtk.gdk.keyval_name(event.keyval) == 'BackSpace': if gtk.gdk.keyval_name(event.keyval) == 'BackSpace':
d_path, d_column = self.view['discs'].get_cursor() row, gtk_column = self.view['files'].get_cursor()
if d_path and d_column: if row and gtk_column:
# easy way fileob = self.files_model.get_value(row)
model = self.view['discs'].get_model() if fileob.parent.parent.id != 1:
child_iter = model.get_iter(d_path) self.files_model.refresh(fileob.parent.parent)
parent_iter = model.iter_parent(child_iter) # TODO: synchronize with disks
if parent_iter: self.model.discs.currentdir = fileob.parent.parent
self.view['discs'].set_cursor(model.get_path(parent_iter))
else: self.view['files'].grab_focus()
# hard way
f_model = treeview.get_model()
first_iter = f_model.get_iter_first()
first_child_value = f_model.get_value(first_iter, 0)
# get two steps up
val = self.model.get_parent_id(first_child_value)
parent_value = self.model.get_parent_id(val)
iter = self.model.discs_tree.get_iter_first()
while iter:
current_value = self.model.discs_tree.get_value(iter,
0)
if current_value == parent_value:
path = self.model.discs_tree.get_path(iter)
self.view['discs'].set_cursor(path)
iter = None
else:
iter = self.model.discs_tree.iter_next(iter)
#if gtk.gdk.keyval_name(event.keyval) == 'Delete':
# for file_id in self.__get_tv_selection_ids(treeview):
# self.main.delete(file_id)
#ids = self.__get_tv_selection_ids(self.view['files'])
def on_files_row_activated(self, files_obj, row, column): def on_files_row_activated(self, files_obj, row, column):
"""On directory doubleclick in files listview dive into desired """
branch.""" On directory doubleclick in files listview dive into desired branch.
f_iter = self.model.files_list.get_iter(row) """
current_id = self.model.files_list.get_value(f_iter, 0)
if self.model.files_list.get_value(f_iter, 6) == 1: fileob = self.files_model.get_value(row=row)
if not fileob.children:
# ONLY directories. files are omitted. # ONLY directories. files are omitted.
self.__set_files_hiden_columns_visible(False)
self.model.get_root_entries(current_id)
d_path, d_column = self.view['discs'].get_cursor()
if d_path:
if not self.view['discs'].row_expanded(d_path):
self.view['discs'].expand_row(d_path, False)
discs_model = self.model.discs_tree
iterator = discs_model.get_iter(d_path)
new_iter = discs_model.iter_children(iterator)
if new_iter:
while new_iter:
current_value = discs_model.get_value(new_iter, 0)
if current_value == current_id:
path = discs_model.get_path(new_iter)
self.view['discs'].set_cursor(path)
new_iter = discs_model.iter_next(new_iter)
return return
def on_add_tag1_activate(self, menu_item): pass self.files_model.refresh(fileob)
def on_delete_tag_activate(self, menuitem): pass self.model.discs.currentdir = fileob
def on_add_thumb1_activate(self, menuitem): pass self.view['files'].grab_focus()
def on_remove_thumb1_activate(self, menuitem): pass
def on_add_image1_activate(self, menuitem): pass # TODO: synchronize with disks
def on_remove_image1_activate(self, menuitem): pass return
def on_edit2_activate(self, menuitem): pass
def on_delete3_activate(self, menuitem): pass def on_add_tag1_activate(self, menu_item):
def on_rename2_activate(self, menuitem): pass """
TODO
"""
LOG.debug(self.on_add_tag1_activate.__doc__.strip())
raise NotImplementedError
def on_delete_tag_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_delete_tag_activate.__doc__.strip())
raise NotImplementedError
def on_add_thumb1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_add_thumb1_activate.__doc__.strip())
raise NotImplementedError
def on_remove_thumb1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_remove_thumb1_activate.__doc__.strip())
raise NotImplementedError
def on_add_image1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_add_image1_activate.__doc__.strip())
raise NotImplementedError
def on_remove_image1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_remove_image1_activate.__doc__.strip())
raise NotImplementedError
def on_edit2_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_edit2_activate.__doc__.strip())
raise NotImplementedError
def on_delete3_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_delete3_activate.__doc__.strip())
raise NotImplementedError
def on_rename2_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_rename2_activate.__doc__.strip())
raise NotImplementedError
# private methods # private methods
def _popup_menu(self, selection, event, button): def _popup_menu(self, selection, event, button):
@@ -258,4 +280,3 @@ class FilesController(Controller):
self.view.menu['files_popup'].popup(None, None, None, self.view.menu['files_popup'].popup(None, None, None,
button, event.time) button, event.time)

View File

@@ -14,12 +14,8 @@ from pygtktalog.controllers.files import FilesController
#from pygtktalog.controllers.details import DetailsController #from pygtktalog.controllers.details import DetailsController
#from pygtktalog.controllers.tags import TagcloudController #from pygtktalog.controllers.tags import TagcloudController
#from pygtktalog.dialogs import yesno, okcancel, info, warn, error #from pygtktalog.dialogs import yesno, okcancel, info, warn, error
from pygtktalog.dialogs import open_catalog, save_catalog, error, yesno from pygtktalog.dialogs import open_catalog, save_catalog, error, yesno, about
from pygtktalog.dialogs import About # TODO: how about make it like a functions above?
from pygtktalog.logger import get_logger from pygtktalog.logger import get_logger
from pygtktalog import __version__
# although it seems to be unused, it is necessary, because it contains
# definitions for additional connectable widgets for observers.
LOG = get_logger("main controller") LOG = get_logger("main controller")
@@ -56,16 +52,13 @@ class MainController(Controller):
view.set_widgets_app_sensitivity(False) view.set_widgets_app_sensitivity(False)
view['main'].show() view['main'].show()
def register_adapters(self): # status bar
""" LOG.debug("register statusbar")
progress bar/status bar adapters goes here self.context_id = self.view['mainStatus'].get_context_id('status')
""" self.statusbar_id = \
LOG.debug(self.register_adapters.__doc__.strip()) self.view['mainStatus'].push(self.context_id,
#title_ad = Adapter(self.model, "cat_fname") self.model.status_bar_message)
#title_ad.connect_widget(self.view["main"],
# setter=lambda w,v: \
# w.set_title(self._get_title()))
pass
# signals # signals
def on_main_destroy_event(self, widget, event): def on_main_destroy_event(self, widget, event):
@@ -95,8 +88,6 @@ class MainController(Controller):
""" """
LOG.debug(self.on_new_activate.__doc__.strip()) LOG.debug(self.on_new_activate.__doc__.strip())
self.model.new() self.model.new()
self.view.discs['discs'].set_model(self.model.discs)
self.view.files['files'].set_model(self.model.files)
self._set_title() self._set_title()
self.view.set_widgets_app_sensitivity(True) self.view.set_widgets_app_sensitivity(True)
@@ -150,8 +141,7 @@ class MainController(Controller):
def on_about1_activate(self, widget): def on_about1_activate(self, widget):
"""Show about dialog""" """Show about dialog"""
About("pyGTKtalog", "%s" % __version__, "About", about()
["Roman 'gryf' Dobosz"], '')
def on_save_activate(self, widget): def on_save_activate(self, widget):
""" """

View File

@@ -9,6 +9,8 @@ import os
import gtk import gtk
import pygtktalog
class Dialog(object): class Dialog(object):
""" """
@@ -58,23 +60,6 @@ class Dialog(object):
self.dialog.format_secondary_text(self.secondary_msg) self.dialog.format_secondary_text(self.secondary_msg)
self.dialog.set_title(self.title) self.dialog.set_title(self.title)
class About(object):
"""
Show About dialog
"""
def __init__(self, name=None, ver="", title="", authors=[], licence=""):
self.dialog = gtk.AboutDialog()
self.dialog.set_title(title)
self.dialog.set_version(ver)
self.dialog.set_license(licence)
self.dialog.set_name(name)
self.dialog.set_authors(authors)
self.dialog.connect('response',
lambda dialog, response: self.dialog.destroy())
self.dialog.show()
# TODO: finish this, re-use Dialog class instead of copy/paste of old classes!
# def about(name, version, )
class ChooseFile(object): class ChooseFile(object):
""" """
@@ -97,7 +82,7 @@ class ChooseFile(object):
self.path = path self.path = path
self.title = title self.title = title
self.action = self.CHOOSER_TYPES[chooser_type] self.action = self.CHOOSER_TYPES[chooser_type]
self.buttons=[] self.buttons = []
for button in buttons: for button in buttons:
self.buttons.append(self.BUTTON_PAIRS[button][0]) self.buttons.append(self.BUTTON_PAIRS[button][0])
self.buttons.append(self.BUTTON_PAIRS[button][1]) self.buttons.append(self.BUTTON_PAIRS[button][1])
@@ -120,7 +105,7 @@ class ChooseFile(object):
if self.URI: if self.URI:
self.dialog.set_current_folder_uri(self.URI) self.dialog.set_current_folder_uri(self.URI)
elif self.path and os.path.exists(self.path): elif self.path and os.path.exists(self.path):
self.path = "file://"+os.path.abspath(self.path) self.path = "file://" + os.path.abspath(self.path)
self.dialog.set_current_folder_uri(self.path) self.dialog.set_current_folder_uri(self.path)
for filtr in self._get_filters(): for filtr in self._get_filters():
@@ -167,6 +152,7 @@ def yesno(message, secondarymsg="", title="", default=False):
dialog.ok_default = default dialog.ok_default = default
return dialog.run() return dialog.run()
def okcancel(message, secondarymsg="", title="", default=False): def okcancel(message, secondarymsg="", title="", default=False):
"""Question with ok-cancel buttons. Returns False on 'cancel', True on """Question with ok-cancel buttons. Returns False on 'cancel', True on
'ok'""" 'ok'"""
@@ -175,6 +161,7 @@ def okcancel(message, secondarymsg="", title="", default=False):
dialog.ok_default = default dialog.ok_default = default
return dialog.run() return dialog.run()
def info(message, secondarymsg="", title="", button=gtk.BUTTONS_OK): def info(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Info dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with """Info dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE. gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
@@ -184,6 +171,7 @@ def info(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
dialog.run() dialog.run()
return True return True
def warn(message, secondarymsg="", title="", button=gtk.BUTTONS_OK): def warn(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Warning dialog. Button defaults to gtk.BUTTONS_OK, but can be changed """Warning dialog. Button defaults to gtk.BUTTONS_OK, but can be changed
with gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE. with gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
@@ -193,6 +181,7 @@ def warn(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
dialog.run() dialog.run()
return True return True
def error(message, secondarymsg="", title="", button=gtk.BUTTONS_OK): def error(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Error dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with """Error dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE. gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
@@ -202,6 +191,7 @@ def error(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
dialog.run() dialog.run()
return True return True
def open_catalog(title=_("Open catalog"), path=None): def open_catalog(title=_("Open catalog"), path=None):
""" """
Request filename from user to open. Request filename from user to open.
@@ -211,6 +201,7 @@ def open_catalog(title=_("Open catalog"), path=None):
requester.filters = ['catalogs', 'all'] requester.filters = ['catalogs', 'all']
return requester.run() return requester.run()
def save_catalog(title=_("Open catalog"), path=None): def save_catalog(title=_("Open catalog"), path=None):
""" """
Request filename from user for save. Request filename from user for save.
@@ -220,3 +211,19 @@ def save_catalog(title=_("Open catalog"), path=None):
requester.filters = ['catalogs', 'all'] requester.filters = ['catalogs', 'all']
requester.confirmation = True requester.confirmation = True
return requester.run() return requester.run()
def about():
"""
Show About dialog
"""
dialog = gtk.AboutDialog()
dialog.set_version(pygtktalog.__version__)
dialog.set_program_name(pygtktalog.__appname__)
dialog.set_copyright(pygtktalog.__copyright__)
dialog.set_comments(pygtktalog.__summary__)
dialog.set_website(pygtktalog.__web__)
dialog.set_logo(gtk.gdk.pixbuf_new_from_file(\
os.path.join(os.path.dirname(__file__), pygtktalog.__logo_img__)))
dialog.run()
dialog.destroy()

View File

@@ -19,6 +19,9 @@ from pygtktalog.dbobjects import File, Exif, Group, Gthumb
from pygtktalog.dbobjects import Image, Tag, Thumbnail from pygtktalog.dbobjects import Image, Tag, Thumbnail
from pygtktalog.dbcommon import connect, Meta, Session from pygtktalog.dbcommon import connect, Meta, Session
from pygtktalog.logger import get_logger from pygtktalog.logger import get_logger
from pygtktalog.models.details import DetailsModel
from pygtktalog.models.discs import DiscsModel
from pygtktalog.models.files import FilesModel
LOG = get_logger("main model") LOG = get_logger("main model")
@@ -55,38 +58,12 @@ class MainModel(ModelMT):
self.db_unsaved = None self.db_unsaved = None
self.discs = None self.discs = DiscsModel()
self.files = None self.files = FilesModel()
self._init_discs()
self._init_files()
if self.cat_fname: if self.cat_fname:
self.open(self.cat_fname) self.open(self.cat_fname)
def _init_discs(self):
"""
Create TreeStore model for the discs
"""
self.discs = gtk.TreeStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
str)
def _init_files(self):
"""
Create ListStore model for the diles
"""
self.files = gtk.ListStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_UINT64,
gobject.TYPE_STRING,
gobject.TYPE_INT,
str)
def open(self, filename): def open(self, filename):
""" """
Open catalog file and read db Open catalog file and read db
@@ -103,7 +80,7 @@ class MainModel(ModelMT):
self.cat_fname = filename self.cat_fname = filename
if self._open_or_decompress(): if self._open_or_decompress():
return self._populate_discs_from_db() return self.discs.refresh(self._session)
else: else:
return False return False
@@ -143,8 +120,8 @@ class MainModel(ModelMT):
self.cleanup() self.cleanup()
self._create_temp_db_file() self._create_temp_db_file()
self._create_schema() self._create_schema()
self._init_discs() self.discs.clear()
self._init_files() self.files.clear()
self.db_unsaved = False self.db_unsaved = False
def cleanup(self): def cleanup(self):
@@ -247,6 +224,7 @@ class MainModel(ModelMT):
connect(os.path.abspath(self.tmp_filename)) connect(os.path.abspath(self.tmp_filename))
self._session = Session() self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
return True return True
def _create_temp_db_file(self): def _create_temp_db_file(self):
@@ -263,6 +241,7 @@ class MainModel(ModelMT):
""" """
""" """
self._session = Session() self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
connect(os.path.abspath(self.tmp_filename)) connect(os.path.abspath(self.tmp_filename))
@@ -277,60 +256,6 @@ class MainModel(ModelMT):
self._session.add(root) self._session.add(root)
self._session.commit() self._session.commit()
def _populate_discs_from_db(self):
"""
Read objects from database, fill TreeStore model with discs
information
"""
dirs = self._session.query(File).filter(File.type == 1)
dirs = dirs.order_by(File.filename).all()
def get_children(parent_id=1, iterator=None):
"""
Get all children of the selected parent.
Arguments:
@parent_id - integer with id of the parent (from db)
@iterator - gtk.TreeIter, which points to a path inside model
"""
for fileob in dirs:
if fileob.parent_id == parent_id:
myiter = self.discs.insert_before(iterator, None)
self.discs.set_value(myiter, 0, fileob)
self.discs.set_value(myiter, 1, fileob.filename)
if iterator is None:
self.discs.set_value(myiter, 2, gtk.STOCK_CDROM)
else:
self.discs.set_value(myiter, 2, gtk.STOCK_DIRECTORY)
get_children(fileob.id, myiter)
return
get_children()
return True
def update_files(self, fileob):
"""
Update files ListStore
Arguments:
fileob - File object
"""
LOG.info("found %d files for File object: %s" % (len(fileob.children),
str(fileob)))
self.files.clear()
for child in fileob.children:
myiter = self.files.insert_before(None, None)
self.files.set_value(myiter, 0, child.id)
self.files.set_value(myiter, 1, child.parent_id \
if child.parent_id!=1 else None)
self.files.set_value(myiter, 2, child.filename)
self.files.set_value(myiter, 3, child.filepath)
self.files.set_value(myiter, 4, child.size)
self.files.set_value(myiter, 5, child.date)
self.files.set_value(myiter, 6, 1)
self.files.set_value(myiter, 7, gtk.STOCK_DIRECTORY \
if child.type==1 else gtk.STOCK_FILE)
def _compress_and_save(self): def _compress_and_save(self):
""" """
Create (and optionaly compress) tar archive from working directory and Create (and optionaly compress) tar archive from working directory and

View File

@@ -33,17 +33,17 @@ class Video(object):
self.out_width = out_width self.out_width = out_width
self.tags = {} self.tags = {}
output = self.__get_movie_info() output = self._get_movie_info()
attrs = {'ID_VIDEO_WIDTH': ['width', int], attrs = {'ID_VIDEO_WIDTH': ['width', int],
'ID_VIDEO_HEIGHT': ['height', int], 'ID_VIDEO_HEIGHT': ['height', int],
# length is in seconds # length is in seconds
'ID_LENGTH': ['length', lambda x: int(x.split(".")[0])], 'ID_LENGTH': ['length', lambda x: int(x.split(".")[0])],
'ID_DEMUXER': ['container', str], 'ID_DEMUXER': ['container', self._return_lower],
'ID_VIDEO_FORMAT': ['video_format', str], 'ID_VIDEO_FORMAT': ['video_format', self._return_lower],
'ID_VIDEO_CODEC': ['video_codec', str], 'ID_VIDEO_CODEC': ['video_codec', self._return_lower],
'ID_AUDIO_CODEC': ['audio_codec', str], 'ID_AUDIO_CODEC': ['audio_codec', self._return_lower],
'ID_AUDIO_FORMAT': ['audio_format', str], 'ID_AUDIO_FORMAT': ['audio_format', self._return_lower],
'ID_AUDIO_NCH': ['audio_no_channels', int],} 'ID_AUDIO_NCH': ['audio_no_channels', int],}
# TODO: what about audio/subtitle language/existence? # TODO: what about audio/subtitle language/existence?
@@ -97,14 +97,15 @@ class Video(object):
tempdir = mkdtemp() tempdir = mkdtemp()
file_desc, image_fn = mkstemp() file_desc, image_fn = mkstemp()
os.close(file_desc) os.close(file_desc)
self.__make_captures(tempdir, no_pictures) self._make_captures(tempdir, no_pictures)
self.__make_montage(tempdir, image_fn, no_pictures, self.out_width) #self._make_montage(tempdir, image_fn, no_pictures)
self._make_montage3(tempdir, image_fn, no_pictures)
shutil.rmtree(tempdir) shutil.rmtree(tempdir)
return image_fn return image_fn
def __get_movie_info(self): def _get_movie_info(self):
""" """
Gather movie file information with midentify shell command. Gather movie file information with midentify shell command.
Returns: dict of command output. Each dict element represents pairs: Returns: dict of command output. Each dict element represents pairs:
@@ -134,7 +135,7 @@ class Video(object):
return_dict[key[0]] = line.replace("%s=" % key[0], "") return_dict[key[0]] = line.replace("%s=" % key[0], "")
return return_dict return return_dict
def __make_captures(self, directory, no_pictures): def _make_captures(self, directory, no_pictures):
""" """
Make screens with mplayer into given directory Make screens with mplayer into given directory
Arguments: Arguments:
@@ -154,13 +155,125 @@ class Video(object):
shutil.move(os.path.join(directory, "00000001.jpg"), shutil.move(os.path.join(directory, "00000001.jpg"),
os.path.join(directory, "picture_%s.jpg" % time)) os.path.join(directory, "picture_%s.jpg" % time))
def __make_montage2(self, directory, image_fn, no_pictures): def _make_montage2(self, directory, image_fn, no_pictures):
""" """
Generate one big image from screnshots and optionally resize it. Generate one big image from screnshots and optionally resize it. Use
external tools from ImageMagic package to arrange and compose final
image. First, images are prescaled, before they will be montaged.
Arguments: Arguments:
@directory - source directory containing images @directory - source directory containing images
@image_fn - destination final image @image_fn - destination final image
@no_pictures - number of pictures @no_pictures - number of pictures
timeit result:
python /usr/lib/python2.6/timeit.py -n 1 -r 1 'from pygtktalog.video import Video; v = Video("/home/gryf/t/a.avi"); v.capture()'
1 loops, best of 1: 25 sec per loop
"""
row_length = 4
if no_pictures < 8:
row_length = 2
if not (self.tags['width'] * row_length) > self.out_width:
for i in (8, 6, 5):
if (no_pictures % i) == 0 and \
(i * self.tags['width']) <= self.out_width:
row_length = i
break
coef = float(self.out_width - row_length * 4) / (self.tags['width'] * row_length)
scaled_width = int(self.tags['width'] * coef)
# scale images
for fname in os.listdir(directory):
cmd = "convert -scale %d %s %s_s.jpg"
os.popen(cmd % (scaled_width, os.path.join(directory, fname),
os.path.join(directory, fname))).readlines()
shutil.move(os.path.join(directory, fname + "_s.jpg"),
os.path.join(directory, fname))
tile = "%dx%d" % (row_length, no_pictures / row_length)
_curdir = os.path.abspath(os.path.curdir)
os.chdir(directory)
# composite pictures
# readlines trick will make to wait for process end
cmd = "montage -tile %s -geometry +2+2 picture_*.jpg montage.jpg"
os.popen(cmd % tile).readlines()
shutil.move(os.path.join(directory, 'montage.jpg'), image_fn)
os.chdir(_curdir)
def _make_montage3(self, directory, image_fn, no_pictures):
"""
Generate one big image from screnshots and optionally resize it. Uses
PIL package to create output image.
Arguments:
@directory - source directory containing images
@image_fn - destination final image
@no_pictures - number of pictures
timeit result:
python /usr/lib/python2.6/timeit.py -n 1 -r 1 'from pygtktalog.video import Video; v = Video("/home/gryf/t/a.avi"); v.capture()'
1 loops, best of 1: 18.8 sec per loop
"""
scale = False
row_length = 4
if no_pictures < 8:
row_length = 2
if not (self.tags['width'] * row_length) > self.out_width:
for i in [8, 6, 5]:
if (no_pictures % i) == 0 and \
(i * self.tags['width']) <= self.out_width:
row_length = i
break
coef = float(self.out_width - row_length - 1) / (self.tags['width'] * row_length)
if coef < 1:
dim = int(self.tags['width'] * coef), int(self.tags['height'] * coef)
else:
dim = int(self.tags['width']), int(self.tags['height'])
ifn_list = os.listdir(directory)
ifn_list.sort()
img_list = [Image.open(os.path.join(directory, fn)).resize(dim) \
for fn in ifn_list]
rows = no_pictures / row_length
cols = row_length
isize = (cols * dim[0] + cols + 1,
rows * dim[1] + rows + 1)
inew = Image.new('RGB', isize, (80, 80, 80))
for irow in range(no_pictures * row_length):
for icol in range(row_length):
left = 1 + icol*(dim[0] + 1)
right = left + dim[0]
upper = 1 + irow * (dim[1] + 1)
lower = upper + dim[1]
bbox = (left, upper, right, lower)
try:
img = img_list.pop(0)
except:
break
inew.paste(img, bbox)
inew.save(image_fn, 'JPEG')
def _make_montage(self, directory, image_fn, no_pictures):
"""
Generate one big image from screnshots and optionally resize it. Use
external tools from ImageMagic package to arrange and compose final
image.
Arguments:
@directory - source directory containing images
@image_fn - destination final image
@no_pictures - number of pictures
timeit result:
python /usr/lib/python2.6/timeit.py -n 1 -r 1 'from pygtktalog.video import Video; v = Video("/home/gryf/t/a.avi"); v.capture()'
1 loops, best of 1: 32.5 sec per loop
""" """
scale = False scale = False
row_length = 4 row_length = 4
@@ -181,52 +294,6 @@ class Video(object):
_curdir = os.path.abspath(os.path.curdir) _curdir = os.path.abspath(os.path.curdir)
os.chdir(directory) os.chdir(directory)
# composite pictures
# readlines trick will make to wait for process end
#cmd = "montage -tile %s -geometry +2+2 picture_*.jpg montage.jpg"
imgs = [Image.open(fn).resize((photow,photoh)) for fn in fnames]
os.popen(cmd % tile).readlines()
# scale it to minimum 'modern' width: 1024
if scale:
cmd = "convert -scale %s montage.jpg montage_scaled.jpg"
os.popen(cmd % out_width).readlines()
shutil.move(os.path.join(directory, 'montage_scaled.jpg'),
image_fn)
else:
shutil.move(os.path.join(directory, 'montage.jpg'),
image_fn)
os.chdir(_curdir)
def __make_montage(self, directory, image_fn, no_pictures):
"""
Generate one big image from screnshots and optionally resize it.
Arguments:
@directory - source directory containing images
@image_fn - destination final image
@no_pictures - number of pictures
"""
scale = False
row_length = 4
if no_pictures < 8:
row_length = 2
if (self.tags['width'] * row_length) > self.out_width:
scale = True
else:
for i in [8, 6, 5]:
if (no_pictures % i) == 0 and \
(i * self.tags['width']) <= self.ut_width:
row_length = i
break
tile = "%dx%d" % (row_length, no_pictures / row_length)
_curdir = os.path.abspath(os.path.curdir)
os.chdir(directory)
# composite pictures # composite pictures
# readlines trick will make to wait for process end # readlines trick will make to wait for process end
cmd = "montage -tile %s -geometry +2+2 picture_*.jpg montage.jpg" cmd = "montage -tile %s -geometry +2+2 picture_*.jpg montage.jpg"
@@ -244,6 +311,15 @@ class Video(object):
os.chdir(_curdir) os.chdir(_curdir)
def _return_lower(self, chain):
"""
Return lowercase version of provided string argument
Arguments:
@chain string to be lowered
Returns:
@string with lowered string
"""
return str(chain).lower()
def __str__(self): def __str__(self):
str_out = '' str_out = ''

View File

@@ -25,7 +25,7 @@ class TestVideo(unittest.TestCase):
self.assertEqual(avi.tags['width'], 128) self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2) self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96) self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'XVID') self.assertEqual(avi.tags['video_format'], 'xvid')
self.assertEqual(avi.tags['length'], 4) self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3') self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffodivx') self.assertEqual(avi.tags['video_codec'], 'ffodivx')
@@ -41,7 +41,7 @@ class TestVideo(unittest.TestCase):
self.assertEqual(avi.tags['width'], 128) self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2) self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96) self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'H264') self.assertEqual(avi.tags['video_format'], 'h264')
self.assertEqual(avi.tags['length'], 4) self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3') self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffh264') self.assertEqual(avi.tags['video_codec'], 'ffh264')
@@ -54,14 +54,14 @@ class TestVideo(unittest.TestCase):
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0") self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192') self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 128) self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2) self.assertTrue(avi.tags['audio_no_channels'] in (1, 2))
self.assertEqual(avi.tags['height'], 96) self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'mp4v') self.assertEqual(avi.tags['video_format'], 'mp4v')
self.assertEqual(avi.tags['length'], 4) self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'a52') self.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3'))
self.assertEqual(avi.tags['video_codec'], 'ffodivx') self.assertEqual(avi.tags['video_codec'], 'ffodivx')
self.assertEqual(avi.tags['duration'], '00:00:04') self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'mkv') self.assertTrue(avi.tags['container'] in ('mkv', 'lavfpref'))
def test_mpg(self): def test_mpg(self):
"""test mock mpg file, should return dict with expected values""" """test mock mpg file, should return dict with expected values"""
@@ -84,14 +84,14 @@ class TestVideo(unittest.TestCase):
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0") self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192') self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 160) self.assertEqual(avi.tags['width'], 160)
self.assertEqual(avi.tags['audio_no_channels'], 2) self.assertTrue(avi.tags['audio_no_channels'] in (1, 2))
self.assertEqual(avi.tags['height'], 120) self.assertEqual(avi.tags['height'], 120)
self.assertEqual(avi.tags['video_format'], 'H264') self.assertEqual(avi.tags['video_format'], 'h264')
self.assertEqual(avi.tags['length'], 4) self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'a52') self.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3'))
self.assertEqual(avi.tags['video_codec'], 'ffh264') self.assertEqual(avi.tags['video_codec'], 'ffh264')
self.assertEqual(avi.tags['duration'], '00:00:04') self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'ogg') self.assertTrue(avi.tags['container'] in ('ogg', 'lavfpref'))
def test_capture(self): def test_capture(self):
"""test capture with some small movie and play a little with tags""" """test capture with some small movie and play a little with tags"""
@@ -100,7 +100,7 @@ class TestVideo(unittest.TestCase):
self.assertTrue(filename != None) self.assertTrue(filename != None)
self.assertTrue(os.path.exists(filename)) self.assertTrue(os.path.exists(filename))
file_size = os.stat(filename)[6] file_size = os.stat(filename)[6]
self.assertEqual(file_size, 9075) self.assertAlmostEqual(file_size/10000.0, 0.9, 0)
os.unlink(filename) os.unlink(filename)
for length in (480, 380, 4): for length in (480, 380, 4):