1
0
mirror of https://github.com/gryf/pygtktalog.git synced 2026-03-27 14:33:34 +01:00

1 Commits

Author SHA1 Message Date
0e6ed92c53 Added junk stuff, that belongs to 1.x branch. Mostly it is attempt to add i18n
support, rest are files with some temporary ideas.
2009-05-04 15:48:40 +00:00
83 changed files with 1607 additions and 5367 deletions

View File

@@ -1,4 +0,0 @@
include setup.py
include pavement.py
include paver-minilib.zip
include pygtktalog/locale/*/*/*.mo

204
README
View File

@@ -1,98 +1,66 @@
pyGTKtalog pyGTKtalog 1.0
========== ==============
pyGTKtalog is Linux/FreeBSD program for indexing CD/DVD or directories on pyGTKtalog is Linux/FreeBSD program for indexing CD/DVD or directories on
filesystem. It is similar to `gtktalog <http://www.nongnu.org/gtktalog/>`_ or filesystem. It is similar to gtktalog <http://www.nongnu.org/gtktalog/> or
`gwhere <http://www.gwhere.org/home.php3>`_. There is no coincidence in name of gwhere <http://www.gwhere.org/home.php3>. There is no coincidence in name of
application, because it's menat to be replacement (in some way) for gtktalog, application, because it's ment to be replacement (in some way) for gtktalog,
which seems to be dead project for years. which seems to be dead project for years.
Current version is 1.9.
FEATURES FEATURES
-------- ========
* scan for files in selected media - scan for files in selected media
* get/generate thumbnails from exif and other images - get/generate thumbnails from exif and other images
* most important exif tags - most important exif tags
* add/edit description and notes - add/edit description and notes
* fetch comments for images made in `gThumb <http://gthumb.sourceforge.net>`_ - fetch comments for images made in gThumb <http://gthumb.sourceforge.net>
* add/remove unlimited images to any file or directory - add/remove unlimited images to any file or directory
* `tagging files <http://en.wikipedia.org/wiki/Tag_%28metadata%29>`_ - tagging files <http://en.wikipedia.org/wiki/Tag_%28metadata%29>
* and more :) - and more :)
REQUIREMENTS REQUIREMENTS
------------ ============
pyGTKtalog requires python and following libraries: pyGTKtalog is written in python with following dependencies:
* `python 2.6 <http://www.python.org/>`_ - python 2.5 or higher
* `pygtk 2.16 <http://www.pygtk.org>`_ - pygtk 2.12 or higher <http://www.pygtk.org>
* `pygtkmvc 1.99 <http://sourceforge.net/apps/trac/pygtkmvc/wiki>`_
* `sqlalchemy 0.6 <http://www.sqlalchemy.org>`_
It may work on other (lower) version of libraries, and it should work with Optional modules:
higher versions of libraries.
.. note:: - PIL <http://www.pythonware.com/products/pil/index.htm> for image manipulation
Although pygtkmvc is `listed on pypi Additional pyGTKtalog uses pygtkmvc <http://pygtkmvc.sourceforge.net> by Roberto
<http://pypi.python.org/pypi/python-gtkmvc/>`_ it may happen that you Cavada and EXIF module by Gene Cash (slightly updatetd to EXIF 2.2 by me) which
have to download it directly from are included in sources.
`sourceforge <http://sourceforge.net/apps/trac/pygtkmvc/wiki>`_ page and
install manually. I don't know about pygtk (I've installed it by my
system package manager), but all the others python libraries (sqlalchemy,
paver, nose, coverage) should be installable via `pip
<http://pypi.python.org/pypi/pip>`_
Optional modules
^^^^^^^^^^^^^^^^
* `PIL <http://www.pythonware.com/products/pil/index.htm>`_ for image manipulation
Additional pyGTKtalog uses EXIF module by Gene Cash (slightly updatetd to EXIF
2.2 by me) which is included in sources.
pyGTKtalog extensivly uses external programs in unix spirit, however there is pyGTKtalog extensivly uses external programs in unix spirit, however there is
small possibility of using it Windows (probably with limitations) and quite big small possibility of using it Windows (probably with limitations) and quite big
possiblity to run it on other sofisticated unix-like systems (i.e. possiblity to run it on other sofisticated unix-like systems (i.e.
BeOS/ZETA/Haiku, QNX or MacOSX). BeOS/ZETA/Haiku, QNX or MacOSX).
Programs that are used:
* mencoder (provided by mplayer package)
* montage, convert from ImageMagick
For development process following programs are used:
* `gettext <http://www.gnu.org/software/gettext/gettext.html>`_
* `intltool <http://www.gnome.org/>`_
* `nose <http://code.google.com/p/python-nose/>`_
* `coverage <http://nedbatchelder.com/code/coverage/>`_
* `paver <http://code.google.com/p/paver/>`__
INSTALATION INSTALATION
----------- ===========
You don't have to install it if you don't want to. You can just change current You don't have to install it if you don't want to. You can just change current
directory to pyGTKtalog and simply run:: directory to pyGTKtalog and simply run:
$ paver run ./pyGTKtalog
That's it. Alternatively, if you like to put it in more system wide place, all That's it. Alternatively, if you like to put it in more system wide place, all
you have to do is: you have to do is:
#. put pyGTKtalog directory into your destination of choice (/usr/local/share, - put pyGTKtalog directory into your destination of choice (/usr/local/share,
/opt or ~/ is typical bet) /opt or ~/ is typical bet)
- copy pyGTKtalog shell script to /usr/bin, /usr/local/bin or in
#. copy pyGTKtalog shell script to /usr/bin, /usr/local/bin or in
other place, where PATH variable is pointing or you feel like. other place, where PATH variable is pointing or you feel like.
- then modify pyGTKtalog line 6 to match right pygtktalog.py directory
#. then modify pyGTKtalog line 6 to match right pygtktalog.py directory
Then, just run pyGTKtalog script. Then, just run pyGTKtalog script.
TODO TODO
---- ====
PyGTKtalog is still under heavy development, however there is small chance to PyGTKtalog is still under heavy development, however there is small chance to
change structure of catalogs (and if it'll change, there will be transparent change structure of catalogs (and if it'll change, there will be transparent
@@ -101,48 +69,43 @@ function to update DB schema).
For version 1.0 there are no features to be done, just bug fixes. For version 1.0 there are no features to be done, just bug fixes.
There are still minor aims for versions 1.x to be done: There are still minor aims for versions 1.x to be done:
- consolidate popup-menus with edit menu
* consolidate popup-menus with edit menu - add popup menu for directly removing tag from tag cloud
* add popup menu for directly removing tag from tag cloud - implement advanced search
* implement advanced search
For version 2.0: For version 2.0:
- Export/Import
* Export/Import - Icon grid in files view
* Icon grid in files view - command line support: query, adding media to collection etc
* command line support: query, adding media to collection etc - internationalization
* internationalization - export to XLS
* export to XLS - user definied group of tags (represented by color in cloud tag)
* user definied group of tags (represented by color in cloud tag) - hiding specified files - configurable, like dot prefixed, cfg and manualy
* hiding specified files - configurable, like dot prefixed, cfg and manualy
selected selected
* tests - tests
* warning about existing image in media directory - warning about existing image in media directory
Removed: Removed:
- filetypes handling (movies, images, archives, documents etc). Now it have
* filetypes handling (movies, images, archives, documents etc). Now it have
common, unified external "plugin" system - simple text output from command common, unified external "plugin" system - simple text output from command
line programs. line programs.
* anime/movie - anime/movie
* title - title
* alt title - alt title
* type (anime movie, movie, anime oav, anime tv series, tv series, etc) - type (anime movie, movie, anime oav, anime tv series, tv series, etc)
* cover/images - cover/images
* genre - genre
* lang - lang
* sub lang - sub lang
* release date (from - to) - release date (from - to)
* anidb link/imdb link - anidb link/imdb link
Maybe in future versions. Now text file descriptions/notes and tags have to
Maybe in future versions. Now text file descriptions/notes and tags have to be enough for good and fast information search.
be enough for good and fast information search.
NOTES NOTES
----- =====
Catalog file is plain sqlite database (optionally compressed with bzip2). All Catalog file is plain sqlite database (optionally compressed with bzip2). All
images are stored in ``~/.pygtktalog/images`` directory. Names for images are images are stored in ~/.pygtktalog/images directory. Names for images are
generated sha512 hash from image file itself. There is small possibility for two generated sha512 hash from image file itself. There is small possibility for two
identical hash for different image files. However, no images are overwritten. identical hash for different image files. However, no images are overwritten.
Thumbnail filename for each image is simply concatenation of image filename in Thumbnail filename for each image is simply concatenation of image filename in
@@ -150,33 +113,52 @@ images directory and '_t' string.
There is also converter from old database to new for internal use only. In There is also converter from old database to new for internal use only. In
public release there will be no other formats so it will be useless, and public release there will be no other formats so it will be useless, and
deleted. There are some issues with converting. All thumbnails will be lost. deleted. There are some issues with converting. All thumbnails will be lost. All
All images without big image will be lost. There are serious changes with images without big image will be lost. There are serious changes with
application design, and I decided, that is better to keep media unpacked on application design, and I decided, that is better to keep media unpacked on
disk, instead of pack it every time with save and unpack with open methods. New disk, instead of pack it every time with save and unpack with open methods. New
design prevent from deleting any file from media directory (placed in design prevent from deleting any file from media directory (placed in
``~/.pygtktalog/images``). Functionality for exporting images and corresponding ~/.pygtktalog/images). Functionality for exporting images and corresponding db
db file is planned. file is planned.
UPDATE
------
There can be added images for virtually any item in catalog. Therefore there is
some hazard with image filenames.
After long consideration and experiments I've decided, that images for every
item will have file name as follows:
DEVELOPMENT sha512("filename" + "file size" + "file modification date").hexdigest()
-----------
Several tools has been used to develop pyGTKtalog. for thumbnails:
sha512("filename" + "file size" + "file modification date").hexdigest() + "_t"
Paver Why that way? There is plenty ways to achive goal to keep thumbnails/data with
^^^^^ applications, however I wanted to keep all things in one place, just to prevent
mixing this up with existing, system specific (Gnome, KDE, maybe MacOS, or any
other which is capable to run this application) own solution. Another reason
lays on catalogs update mechanizm. Imagine, that you have large collection of
movie clips and want to frequently add and/or delete somethong from that.
Changing file names of virtually all files is rather rare case. However moving
them between directories will be much more frequent scenario. And now, if you
want to update things in catalog, program will just check if there is such
generated image from movie filename, size and dates, and then it will just
assign that image to file in catalog. No need to wase time again for generating
movie shots all over again.
I've choose `Paver <http://www.blueskyonmars.com/projects/paver/>`_ as make Of course there are some limits for such approach. There is relatively small
equivalent. Inside main project directory there is pavement.py script, which possibility to generate two filenames that are the same in two cases:
provides several tasks, that can be helpfull in a work with sources. Paver is
also used to generate standard setup.py. 1. There are two different files (movies or images) with the same name, same
size and same timestamp in different directories. This could happen in case of
images that have fixed size (like BMP) and then due to image/thumbnail creating
policy only the first one will be placed in images directory.
2. Another possibility........ fuck.
Nose
^^^^
BUGS BUGS
---- ====
All bugs please report to Roman 'gryf' Dobosz <gryf73@gmail.com>. All bugs please report to Roman 'gryf' Dobosz <roman.dobosz@gmail.com>

5
cleanup.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
# remove ~, pyc, pyo files from current directory
find . -name \*~ -exec rm '{}' ';'
find . -name \*pyc -exec rm '{}' ';'
find . -name \*pyo -exec rm '{}' ';'

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env python
"""
Project: pyGTKtalog
Description: convert db created with v.1.x into v.2.x
Type: tool
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-14
"""
import sys
import os
import bz2
import shutil
from tempfile import mkstemp
from sqlite3 import dbapi2 as sqlite
from datetime import datetime
# import db objects just to create schema
from pygtktalog.dbobjects import File, Exif, Group, Gthumb
from pygtktalog.dbobjects import Image, Tag, Thumbnail
from pygtktalog.dbcommon import connect
def create_schema(cur):
pass
def create_temporary_db_file():
"""create temporary db file"""
fd, fname = mkstemp()
os.close(fd)
return fname
def connect_to_db(filename):
"""initialize db connection and store it in class attributes"""
db_connection = sqlite.connect(filename, \
detect_types=sqlite.PARSE_DECLTYPES | sqlite.PARSE_COLNAMES)
db_cursor = db_connection.cursor()
return db_connection, db_cursor
def opendb(filename=None):
"""try to open db file"""
db_tmp_path = create_temporary_db_file()
try:
test_file = open(filename).read(15)
except IOError:
os.unlink(db_tmp_path)
return False
if test_file == "SQLite format 3":
db_tmp = open(db_tmp_path, "wb")
db_tmp.write(open(filename).read())
db_tmp.close()
elif test_file[0:10] == "BZh91AY&SY":
open_file = bz2.BZ2File(filename)
try:
curdb = open(db_tmp_path, "w")
curdb.write(open_file.read())
curdb.close()
open_file.close()
except IOError:
# file is not bz2
os.unlink(db_tmp_path)
return False
else:
os.unlink(db_tmp_path)
return False
return connect_to_db(db_tmp_path), db_tmp_path
if __name__ == "__main__":
if len(sys.argv) != 3:
print "usage: %s src_base dst_base" % sys.argv[0]
exit()
result = opendb(sys.argv[1])
if not result:
print "unable to open src db file"
exit()
(src_con, src_c), src_tmpf = result
shutil.copy(src_tmpf, sys.argv[2])
# close src db.
src_c.close()
src_con.close()
os.unlink(src_tmpf)
# create or update shema
connect(sys.argv[2])
dst_con = sqlite.connect(sys.argv[2])
dst_c = dst_con.cursor()
sql = "select id, date from files"
for id, date in dst_c.execute(sql).fetchall():
sql = "update files set date=? where id=?"
if date and int(date) > 0:
dst_c.execute(sql, (datetime.fromtimestamp(int(date)), id))
else:
dst_c.execute(sql, (None, id))
sql = "select id, date from gthumb"
for id, date in dst_c.execute(sql).fetchall():
sql = "update gthumb set date=? where id=?"
try:
if int(date) > 0:
dst_c.execute(sql, (datetime.fromtimestamp(int(date)), id))
else:
dst_c.execute(sql, (None, id))
except:
print id, date
dst_con.commit()
dst_c.close()
dst_con.close()

23
extract_gettext.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
# Create new messages.pot file
blacklist="EXIF.py no_thumb.py"
rm locale/pygtktalog.pot
xgettext -L Python --keyword=_ -o locale/pygtktalog.pot pygtktalog.py
svn ls | grep '.py$'|grep -v 'pygtktalog.py' |while read file; do
xgettext -j -L Python --keyword=_ -o locale/pygtktalog.pot $file
done
for dir in src/ctrls src/models src/views src/lib; do
svn ls -R $dir| grep '.py$' |grep -v 'EXIF.py'|grep -v 'no_thumb.py'| while read file; do
xgettext -j -L Python --keyword=_ -o locale/pygtktalog.pot $dir/$file
done
done
cd locale
msginit --input=pygtktalog.pot --locale=pl_PL.UTF-8
# now its time to make .mo files:
mkdir -p pl_PL/LC_MESSAGES
#msgfmt --output-file=pl_PL/LC_MESSAGES/pygtktalog.mo pl.po

View File

@@ -1,375 +1,303 @@
# # Polish translations for PACKAGE package
# pygtktalog Language File # Polskie tłumaczenia dla pakietu PACKAGE.
# Copyright (C) 2009 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# <gryf73@gmail.com>, 2009.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: pygtktalog\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2009-08-26 22:10:33.892815\n" "Report-Msgid-Bugs-To: \n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\n" "POT-Creation-Date: 2009-01-11 13:51+0100\n"
"PO-Revision-Date: 2009-01-11 13:51+0100\n"
"Last-Translator: <gryf73@gmail.com>\n"
"Language-Team: Polish\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: slightly modified generate_pot.py\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: pygtktalog/views/glade/main.glade.h:1 #: pygtktalog.py:38
msgid "Add CD"
msgstr "Dodaj CD"
#: pygtktalog/views/glade/main.glade.h:2
msgid "Add CD/DVD to catalog"
msgstr "Dodaj CD/DVD do katalogu"
#: pygtktalog/views/glade/main.glade.h:3
msgid "Add Dir"
msgstr "Dodaj katalog"
#: pygtktalog/views/glade/main.glade.h:4
msgid "Add _CD/DVD"
msgstr "Dodaj _Cd/DVD"
#: pygtktalog/views/glade/main.glade.h:5
msgid "Add _Directory"
msgstr "Dodaj kata_log"
#: pygtktalog/views/glade/main.glade.h:6
#: pygtktalog/views/glade/files.glade.h:1
msgid "Add _Images"
msgstr "Dodaj _obrazy"
#: pygtktalog/views/glade/main.glade.h:7
#: pygtktalog/views/glade/files.glade.h:2
msgid "Add _Thumbnail"
msgstr "Dodaj miniaturÄ™"
#: pygtktalog/views/glade/main.glade.h:8
#: pygtktalog/views/glade/files.glade.h:3
msgid "" msgid ""
"Add images to file. If file have no thumbnail,\n" "WARNING: You'll need Python Imaging Library (PIL), if you want to make "
"thumbnail from first image will be generated." "thumbnails!"
msgstr ""
"Dodanie obrazów do pliku. Jeśli plik nie posiada miniatury,\n"
"zostanie wygenerowana miniatura z pierwszego obrazu."
#: pygtktalog/views/glade/main.glade.h:10
#: pygtktalog/views/glade/main.glade.h:6
msgid "Cancel"
msgstr "Analuj"
#: pygtktalog/views/glade/main.glade.h:11
#: pygtktalog/views/glade/main.glade.h:7
msgid "Catalog _statistics"
msgstr "_Statystyka katalogu"
#: pygtktalog/views/glade/main.glade.h:12
#: pygtktalog/views/glade/discs.glade.h:1
msgid "Collapse all nodes"
msgstr "Zwiń wszystkie gałęzie"
#: pygtktalog/views/glade/main.glade.h:13
#: pygtktalog/views/glade/main.glade.h:8
msgid "Create new catalog"
msgstr "UtwĂłrz nowy katalog"
#: pygtktalog/views/glade/main.glade.h:14
#: pygtktalog/views/glade/main.glade.h:9
msgid "Delete all images"
msgstr "Usuń wszystkie obrazy"
#: pygtktalog/views/glade/main.glade.h:15
#: pygtktalog/views/glade/main.glade.h:10
msgid "Delete all thumbnals"
msgstr "Usuń wszystkie miniatury"
#: pygtktalog/views/glade/main.glade.h:16
#: pygtktalog/views/glade/main.glade.h:11
msgid "Delete tag"
msgstr "Usuń tag"
#: pygtktalog/views/glade/main.glade.h:17
#: pygtktalog/views/glade/main.glade.h:12
msgid "Deletes all images from files in current colection"
msgstr "Usuwa wszystkie obrazy z plikĂłw w bieĹĽÄ…cej kolekcji"
#: pygtktalog/views/glade/main.glade.h:18
#: pygtktalog/views/glade/main.glade.h:13
msgid "Discs"
msgstr "Dyski"
#: pygtktalog/views/glade/main.glade.h:19
#: pygtktalog/views/glade/details.glade.h:1
msgid "Double click to open image"
msgstr "Dwukrotne kliknięcie otwiera obraz"
#: pygtktalog/views/glade/main.glade.h:20
#: pygtktalog/views/glade/details.glade.h:2
msgid "EXIF"
msgstr "EXIF"
#: pygtktalog/views/glade/main.glade.h:21
#: pygtktalog/views/glade/discs.glade.h:2
msgid "Expand all nodes"
msgstr "Rozwiń wszystkie gałęzie"
#: pygtktalog/views/glade/main.glade.h:22
#: pygtktalog/views/glade/main.glade.h:14
msgid "Export"
msgstr "Eksport"
#: pygtktalog/views/glade/main.glade.h:23
#: pygtktalog/views/glade/details.glade.h:3
msgid "File info"
msgstr "Informacje o pliku"
#: pygtktalog/views/glade/main.glade.h:24
#: pygtktalog/views/glade/main.glade.h:15
msgid "Find file"
msgstr "ZnajdĹş plik"
#: pygtktalog/views/glade/main.glade.h:25
#: pygtktalog/views/glade/details.glade.h:4
msgid "Images"
msgstr "Obrazy"
#: pygtktalog/views/glade/main.glade.h:26
#: pygtktalog/views/glade/main.glade.h:16
msgid "Import"
msgstr "Import"
#: pygtktalog/views/glade/main.glade.h:27
#: pygtktalog/views/glade/main.glade.h:17
msgid "Open catalog file"
msgstr "OtwĂłrz plik katalogu"
#: pygtktalog/views/glade/main.glade.h:28
#: pygtktalog/views/glade/main.glade.h:18
msgid "Quit pyGTKtalog"
msgstr "Zakończ pyGTKtalog"
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:5
msgid "Re_move Thumbnail"
msgstr "Usuń miniaturę"
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/main.glade.h:19
msgid "Recent files"
msgstr "Ostatnio uĹĽywane pliki"
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/files.glade.h:6
msgid "Rem_ove All Images"
msgstr "Usuń wszystkie obrazy"
#: pygtktalog/views/glade/main.glade.h:32
#: pygtktalog/views/glade/files.glade.h:7
msgid "Remo_ve tag"
msgstr "Usuń tag"
#: pygtktalog/views/glade/main.glade.h:33
#: pygtktalog/views/glade/main.glade.h:20
msgid "Save all images..."
msgstr "Zapisz wszytkie obrazy..."
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/main.glade.h:21
msgid "Save catalog"
msgstr "Zapisz katalog"
#: pygtktalog/views/glade/main.glade.h:35
#: pygtktalog/views/glade/main.glade.h:22
msgid "Set as _thumbnail"
msgstr "Ustaw jako miniaturÄ™"
#: pygtktalog/views/glade/main.glade.h:36
#: pygtktalog/views/glade/main.glade.h:23
msgid "Status bar"
msgstr "Pasek stanu"
#: pygtktalog/views/glade/main.glade.h:37
#: pygtktalog/views/glade/main.glade.h:24
msgid "Tags"
msgstr "Tagi"
#: pygtktalog/views/glade/main.glade.h:38
#: pygtktalog/views/glade/main.glade.h:25
msgid "Toolbar"
msgstr "Pasek narzędzi"
#: pygtktalog/views/glade/main.glade.h:39
#: pygtktalog/views/glade/main.glade.h:26
msgid "_Add images"
msgstr "Dodaj obrazy"
#: pygtktalog/views/glade/main.glade.h:40
#: pygtktalog/views/glade/files.glade.h:8
msgid "_Add tag"
msgstr "_Dodaj tag"
#: pygtktalog/views/glade/main.glade.h:41
#: pygtktalog/views/glade/main.glade.h:27
msgid "_Catalog"
msgstr "_Katalog"
#: pygtktalog/views/glade/main.glade.h:42
#: pygtktalog/views/glade/discs.glade.h:3
msgid "_Collapse all"
msgstr "_Zwiń wszystko"
#: pygtktalog/views/glade/main.glade.h:43
#: pygtktalog/views/glade/discs.glade.h:4
#: pygtktalog/views/glade/files.glade.h:9
msgid "_Delete"
msgstr "_Usuń"
#: pygtktalog/views/glade/main.glade.h:44
#: pygtktalog/views/glade/main.glade.h:28
msgid "_Delete images"
msgstr "Usuń obrazy"
#: pygtktalog/views/glade/main.glade.h:45
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:10
#: pygtktalog/views/glade/test.glade.h:1
msgid "_Edit"
msgstr "_Edycja"
#: pygtktalog/views/glade/main.glade.h:46
#: pygtktalog/views/glade/discs.glade.h:5
msgid "_Expand all"
msgstr "_Rozwiń wszystko"
#: pygtktalog/views/glade/main.glade.h:47 pygtktalog/views/main_menu.py:19
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/test.glade.h:2
msgid "_File"
msgstr "_Plik"
#: pygtktalog/views/glade/main.glade.h:48
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/test.glade.h:3
msgid "_Help"
msgstr "_Pomoc"
#: pygtktalog/views/glade/main.glade.h:49
#: pygtktalog/views/glade/main.glade.h:32
msgid "_Remove Thumbnail"
msgstr "Usuń miniaturę"
#: pygtktalog/views/glade/main.glade.h:50
#: pygtktalog/views/glade/discs.glade.h:6
#: pygtktalog/views/glade/files.glade.h:11
msgid "_Rename"
msgstr "_Zmień nazwę"
#: pygtktalog/views/glade/main.glade.h:51
#: pygtktalog/views/glade/main.glade.h:33
msgid "_Save images to..."
msgstr "Zapisz obrazy do..."
#: pygtktalog/views/glade/main.glade.h:52
#: pygtktalog/views/glade/discs.glade.h:7
msgid "_Statistics"
msgstr "_Statystyka"
#: pygtktalog/views/glade/main.glade.h:53
#: pygtktalog/views/glade/discs.glade.h:8
msgid "_Update"
msgstr "_Uaktualnij"
#: pygtktalog/views/glade/main.glade.h:54
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/test.glade.h:4
msgid "_View"
msgstr "_Widok"
#: pygtktalog/views/glade/main.glade.h:55
msgid "gtk-clear"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:56 #: src/ctrls/c_main.py:284
#: pygtktalog/views/glade/main.glade.h:35 msgid "Delete thumbnails"
msgid "pyGTKtalog" msgstr "Usunięcie miniaturek"
msgstr "pyGTKtalog"
#: pygtktalog/views/glade/main.glade.h:57 #: src/ctrls/c_main.py:284
#: pygtktalog/views/glade/main.glade.h:36 msgid "Delete thumbnails?"
msgid "pyGTKtalog - Image" msgstr "Usunąć mniniaturki?"
msgstr "pyGTKtalog - Obraz"
#: pygtktalog/controllers/main.py:47 #: src/ctrls/c_main.py:285
msgid "Do you really want to quit?" msgid "Thumbnails for selected items will be permanently removed from catalog."
msgstr "Czy na pewno chcesz zakończyć?" msgstr "Miniaturki zaznaczonych obiektów zostaną trwale usunięte z katalogu."
#: pygtktalog/controllers/main.py:48 #: src/ctrls/c_main.py:308 src/ctrls/c_main.py:731 src/ctrls/c_main.py:736
msgid "Current database is not saved, any changes will be lost." msgid "Delete images"
msgstr "Bieżący katalog nie jest zapisany, wszelkie zmiany zostaną utracone." msgstr "Usnięcie obrazów"
#: pygtktalog/controllers/main.py:48 pygtktalog/controllers/main.py:49 #: src/ctrls/c_main.py:308
msgid "Delete all images?"
msgstr "Usunąć wszystkie obrazy?"
#: src/ctrls/c_main.py:309
msgid "All images for selected items will be permanently removed from catalog."
msgstr "Wszystkie obrazy zaznaczonych obiektów zostaną trwale usunięte z katalogu."
#: src/ctrls/c_main.py:342
msgid "Image view"
msgstr "PrzeglÄ…darka obrazĂłw"
#: src/ctrls/c_main.py:342
msgid "No Image"
msgstr "Brak obrazu"
#: src/ctrls/c_main.py:343
msgid "Image file does not exist."
msgstr "Plik obrazu nie istnieje."
#: src/ctrls/c_main.py:491
msgid "Quit application" msgid "Quit application"
msgstr "Zakończ aplikację" msgstr "Zakończ aplikację"
#: pygtktalog/models/main.py:16 pygtktalog/models/main.py:31 #: src/ctrls/c_main.py:492
msgid "Idle" msgid "Do you really want to quit?"
msgstr "Bezczynny" msgstr "Czy naprawdę chcesz zakończyć?"
#: pygtktalog/controllers/files.py:33 #: src/ctrls/c_main.py:493 src/ctrls/c_main.py:506
#, fuzzy msgid "Current database is not saved, any changes will be lost."
msgid "Disc" msgstr "Bieżąca baza nie została zachowana, wszystkie zmiany zostaną utracone."
msgstr "Dyski"
#: pygtktalog/controllers/files.py:39 #: src/ctrls/c_main.py:504 src/ctrls/c_main.py:638
#, fuzzy msgid "Unsaved data"
msgid "Filename" msgstr "Niezapisane dane"
msgstr "_Zmień nazwę"
#: pygtktalog/controllers/files.py:50 #: src/ctrls/c_main.py:505
msgid "Path" msgid "Do you want to abandon changes?"
msgstr "Czy chcesz porzucić zmiany?"
#: src/ctrls/c_main.py:541
msgid "Error mounting device"
msgstr "Błąd w trakcie podłączania urzadzenia"
#: src/ctrls/c_main.py:542
#, python-format
msgid "Cannot mount device pointed to %s"
msgstr "Nie można podłączyć urządzenia %s"
#: src/ctrls/c_main.py:544
#, python-format
msgid ""
"Last mount message:\n"
"%s"
msgstr "Ostatni komunikat polecenia mount:\n%s"
#: src/ctrls/c_main.py:616
msgid "Error writing file"
msgstr "Błąd w zapisie pliku"
#: src/ctrls/c_main.py:617
#, python-format
msgid "Cannot write file %s."
msgstr "Nie można zapisać pliku %s."
#: src/ctrls/c_main.py:639
msgid "There is not saved database"
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:56 #: src/ctrls/c_main.py:640
msgid "Size" msgid "Pressing <b>Ok</b> will abandon catalog."
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:61 #: src/ctrls/c_main.py:664
msgid "Date" msgid "Error opening file"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:5 #: src/ctrls/c_main.py:665
msgid "gtk-about" #, python-format
msgid "Cannot open file %s."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:6 #: src/ctrls/c_main.py:731
msgid "gtk-copy" msgid "No images selected"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:7 #: src/ctrls/c_main.py:732
msgid "gtk-cut" msgid "You have to select at least one image to delete."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:8 #: src/ctrls/c_main.py:736
msgid "gtk-delete" msgid "Delete selected images?"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:9 #: src/ctrls/c_main.py:737
msgid "gtk-new" msgid "Selected images will be permanently removed from catalog."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:10 #: src/ctrls/c_main.py:767
msgid "gtk-open" msgid "Choose directory to save images"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:11 #: src/ctrls/c_main.py:792 src/ctrls/c_main.py:798
msgid "gtk-paste" msgid "Save images"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:12 #: src/ctrls/c_main.py:793
msgid "gtk-quit" #, python-format
msgid "%d images was succsefully saved."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:13 #: src/ctrls/c_main.py:794
msgid "gtk-save" #, python-format
msgid ""
"Images are placed in directory:\n"
"%s."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:14 #: src/ctrls/c_main.py:796
msgid "gtk-save-as" msgid "Images probably don't have real images - only thumbnails."
msgstr ""
#: src/ctrls/c_main.py:798
msgid "No images was saved."
msgstr ""
#: src/ctrls/c_main.py:807 src/ctrls/c_main.py:811
msgid "Set thumbnail"
msgstr ""
#: src/ctrls/c_main.py:807
msgid "No image selected"
msgstr ""
#: src/ctrls/c_main.py:808 src/ctrls/c_main.py:812
msgid "You have to select one image to set as thumbnail."
msgstr ""
#: src/ctrls/c_main.py:811
msgid "To many images selected"
msgstr ""
#: src/ctrls/c_main.py:870
msgid "Choose export file"
msgstr ""
#: src/ctrls/c_main.py:1016 src/ctrls/c_main.py:1025
msgid "Remove tags"
msgstr ""
#: src/ctrls/c_main.py:1016 src/ctrls/c_main.py:1167
msgid "No files selected"
msgstr ""
#: src/ctrls/c_main.py:1017
msgid "You have to select some files first."
msgstr ""
#: src/ctrls/c_main.py:1025
msgid "No tags selected"
msgstr ""
#: src/ctrls/c_main.py:1026
msgid "You have to select any tag to remove from files."
msgstr ""
#: src/ctrls/c_main.py:1052
msgid "Don't copy images. Generate only thumbnails."
msgstr ""
#: src/ctrls/c_main.py:1116
msgid "Delete disc"
msgstr ""
#: src/ctrls/c_main.py:1116
msgid "No disc selected"
msgstr ""
#: src/ctrls/c_main.py:1117
msgid "You have to select disc first before you can delete it"
msgstr ""
#: src/ctrls/c_main.py:1123
#, python-format
msgid "Delete %s"
msgstr ""
#: src/ctrls/c_main.py:1123
#, python-format
msgid "Delete %s?"
msgstr ""
#: src/ctrls/c_main.py:1124
msgid "Object will be permanently removed."
msgstr ""
#: src/ctrls/c_main.py:1167 src/ctrls/c_main.py:1172
msgid "Delete files"
msgstr ""
#: src/ctrls/c_main.py:1168
msgid "You have to select at least one file to delete."
msgstr ""
#: src/ctrls/c_main.py:1172
msgid "Delete files?"
msgstr ""
#: src/ctrls/c_main.py:1173
msgid ""
"Selected files and directories will be permanently\n"
" removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:1215
msgid "Delete thumbnail"
msgstr ""
#: src/ctrls/c_main.py:1215
msgid "Delete thumbnail?"
msgstr ""
#: src/ctrls/c_main.py:1216
msgid "Current thumbnail will be permanently removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:1364
msgid "Error ejecting device"
msgstr ""
#: src/ctrls/c_main.py:1366
#, python-format
msgid "Cannot eject device pointed to %s"
msgstr ""
#: src/ctrls/c_main.py:1368
#, python-format
msgid ""
"Last eject message:\n"
"%s"
msgstr ""
#: src/ctrls/c_main.py:1372
msgid "Error unmounting device"
msgstr ""
#: src/ctrls/c_main.py:1374
#, python-format
msgid "Cannot unmount device pointed to %s"
msgstr ""
#: src/ctrls/c_main.py:1376
#, python-format
msgid ""
"Last umount message:\n"
"%s"
msgstr ""
#: src/lib/device_helper.py:85
msgid "Eject program not specified"
msgstr "" msgstr ""

View File

@@ -1,370 +1,301 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
# pygtktalog Language File #, fuzzy
#
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: pygtktalog\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2009-08-26 22:10:33.892815\n" "Report-Msgid-Bugs-To: \n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\n" "POT-Creation-Date: 2009-01-11 13:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: 8bit\n"
#: pygtktalog/views/glade/main.glade.h:1 #: pygtktalog.py:38
msgid "Add CD"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:2
msgid "Add CD/DVD to catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:3
msgid "Add Dir"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:4
msgid "Add _CD/DVD"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:5
msgid "Add _Directory"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:6
#: pygtktalog/views/glade/files.glade.h:1
msgid "Add _Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:7
#: pygtktalog/views/glade/files.glade.h:2
msgid "Add _Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:8
#: pygtktalog/views/glade/files.glade.h:3
msgid "" msgid ""
"Add images to file. If file have no thumbnail,\n" "WARNING: You'll need Python Imaging Library (PIL), if you want to make "
"thumbnail from first image will be generated." "thumbnails!"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:10 #: src/ctrls/c_main.py:284
#: pygtktalog/views/glade/main.glade.h:6 msgid "Delete thumbnails"
msgid "Cancel"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:11 #: src/ctrls/c_main.py:284
#: pygtktalog/views/glade/main.glade.h:7 msgid "Delete thumbnails?"
msgid "Catalog _statistics"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:12 #: src/ctrls/c_main.py:285
#: pygtktalog/views/glade/discs.glade.h:1 msgid "Thumbnails for selected items will be permanently removed from catalog."
msgid "Collapse all nodes"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:13 #: src/ctrls/c_main.py:308 src/ctrls/c_main.py:731 src/ctrls/c_main.py:736
#: pygtktalog/views/glade/main.glade.h:8 msgid "Delete images"
msgid "Create new catalog"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:14 #: src/ctrls/c_main.py:308
#: pygtktalog/views/glade/main.glade.h:9 msgid "Delete all images?"
msgid "Delete all images"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:15 #: src/ctrls/c_main.py:309
#: pygtktalog/views/glade/main.glade.h:10 msgid "All images for selected items will be permanently removed from catalog."
msgid "Delete all thumbnals"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:16 #: src/ctrls/c_main.py:342
#: pygtktalog/views/glade/main.glade.h:11 msgid "Image view"
msgid "Delete tag"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:17 #: src/ctrls/c_main.py:342
#: pygtktalog/views/glade/main.glade.h:12 msgid "No Image"
msgid "Deletes all images from files in current colection"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:18 #: src/ctrls/c_main.py:343
#: pygtktalog/views/glade/main.glade.h:13 msgid "Image file does not exist."
msgid "Discs"
msgstr "" msgstr ""
#: pygtktalog/views/glade/main.glade.h:19 #: src/ctrls/c_main.py:491
#: pygtktalog/views/glade/details.glade.h:1
msgid "Double click to open image"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:20
#: pygtktalog/views/glade/details.glade.h:2
msgid "EXIF"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:21
#: pygtktalog/views/glade/discs.glade.h:2
msgid "Expand all nodes"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:22
#: pygtktalog/views/glade/main.glade.h:14
msgid "Export"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:23
#: pygtktalog/views/glade/details.glade.h:3
msgid "File info"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:24
#: pygtktalog/views/glade/main.glade.h:15
msgid "Find file"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:25
#: pygtktalog/views/glade/details.glade.h:4
msgid "Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:26
#: pygtktalog/views/glade/main.glade.h:16
msgid "Import"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:27
#: pygtktalog/views/glade/main.glade.h:17
msgid "Open catalog file"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:28
#: pygtktalog/views/glade/main.glade.h:18
msgid "Quit pyGTKtalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:5
msgid "Re_move Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/main.glade.h:19
msgid "Recent files"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/files.glade.h:6
msgid "Rem_ove All Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:32
#: pygtktalog/views/glade/files.glade.h:7
msgid "Remo_ve tag"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:33
#: pygtktalog/views/glade/main.glade.h:20
msgid "Save all images..."
msgstr ""
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/main.glade.h:21
msgid "Save catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:35
#: pygtktalog/views/glade/main.glade.h:22
msgid "Set as _thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:36
#: pygtktalog/views/glade/main.glade.h:23
msgid "Status bar"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:37
#: pygtktalog/views/glade/main.glade.h:24
msgid "Tags"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:38
#: pygtktalog/views/glade/main.glade.h:25
msgid "Toolbar"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:39
#: pygtktalog/views/glade/main.glade.h:26
msgid "_Add images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:40
#: pygtktalog/views/glade/files.glade.h:8
msgid "_Add tag"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:41
#: pygtktalog/views/glade/main.glade.h:27
msgid "_Catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:42
#: pygtktalog/views/glade/discs.glade.h:3
msgid "_Collapse all"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:43
#: pygtktalog/views/glade/discs.glade.h:4
#: pygtktalog/views/glade/files.glade.h:9
msgid "_Delete"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:44
#: pygtktalog/views/glade/main.glade.h:28
msgid "_Delete images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:45
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:10
#: pygtktalog/views/glade/test.glade.h:1
msgid "_Edit"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:46
#: pygtktalog/views/glade/discs.glade.h:5
msgid "_Expand all"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:47 pygtktalog/views/main_menu.py:19
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/test.glade.h:2
msgid "_File"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:48
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/test.glade.h:3
msgid "_Help"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:49
#: pygtktalog/views/glade/main.glade.h:32
msgid "_Remove Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:50
#: pygtktalog/views/glade/discs.glade.h:6
#: pygtktalog/views/glade/files.glade.h:11
msgid "_Rename"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:51
#: pygtktalog/views/glade/main.glade.h:33
msgid "_Save images to..."
msgstr ""
#: pygtktalog/views/glade/main.glade.h:52
#: pygtktalog/views/glade/discs.glade.h:7
msgid "_Statistics"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:53
#: pygtktalog/views/glade/discs.glade.h:8
msgid "_Update"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:54
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/test.glade.h:4
msgid "_View"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:55
msgid "gtk-clear"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:56
#: pygtktalog/views/glade/main.glade.h:35
msgid "pyGTKtalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:57
#: pygtktalog/views/glade/main.glade.h:36
msgid "pyGTKtalog - Image"
msgstr ""
#: pygtktalog/controllers/main.py:47
msgid "Do you really want to quit?"
msgstr ""
#: pygtktalog/controllers/main.py:48
msgid "Current database is not saved, any changes will be lost."
msgstr ""
#: pygtktalog/controllers/main.py:48 pygtktalog/controllers/main.py:49
msgid "Quit application" msgid "Quit application"
msgstr "" msgstr ""
#: pygtktalog/models/main.py:16 pygtktalog/models/main.py:31 #: src/ctrls/c_main.py:492
msgid "Idle" msgid "Do you really want to quit?"
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:33 #: src/ctrls/c_main.py:493 src/ctrls/c_main.py:506
msgid "Disc" msgid "Current database is not saved, any changes will be lost."
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:39 #: src/ctrls/c_main.py:504 src/ctrls/c_main.py:638
msgid "Filename" msgid "Unsaved data"
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:50 #: src/ctrls/c_main.py:505
msgid "Path" msgid "Do you want to abandon changes?"
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:56 #: src/ctrls/c_main.py:541
msgid "Size" msgid "Error mounting device"
msgstr "" msgstr ""
#: pygtktalog/controllers/files.py:61 #: src/ctrls/c_main.py:542
msgid "Date" #, python-format
msgid "Cannot mount device pointed to %s"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:5 #: src/ctrls/c_main.py:544
msgid "gtk-about" #, python-format
msgid ""
"Last mount message:\n"
"%s"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:6 #: src/ctrls/c_main.py:616
msgid "gtk-copy" msgid "Error writing file"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:7 #: src/ctrls/c_main.py:617
msgid "gtk-cut" #, python-format
msgid "Cannot write file %s."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:8 #: src/ctrls/c_main.py:639
msgid "gtk-delete" msgid "There is not saved database"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:9 #: src/ctrls/c_main.py:640
msgid "gtk-new" msgid "Pressing <b>Ok</b> will abandon catalog."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:10 #: src/ctrls/c_main.py:664
msgid "gtk-open" msgid "Error opening file"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:11 #: src/ctrls/c_main.py:665
msgid "gtk-paste" #, python-format
msgid "Cannot open file %s."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:12 #: src/ctrls/c_main.py:731
msgid "gtk-quit" msgid "No images selected"
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:13 #: src/ctrls/c_main.py:732
msgid "gtk-save" msgid "You have to select at least one image to delete."
msgstr "" msgstr ""
#: pygtktalog/views/glade/test.glade.h:14 #: src/ctrls/c_main.py:736
msgid "gtk-save-as" msgid "Delete selected images?"
msgstr ""
#: src/ctrls/c_main.py:737
msgid "Selected images will be permanently removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:767
msgid "Choose directory to save images"
msgstr ""
#: src/ctrls/c_main.py:792 src/ctrls/c_main.py:798
msgid "Save images"
msgstr ""
#: src/ctrls/c_main.py:793
#, python-format
msgid "%d images was succsefully saved."
msgstr ""
#: src/ctrls/c_main.py:794
#, python-format
msgid ""
"Images are placed in directory:\n"
"%s."
msgstr ""
#: src/ctrls/c_main.py:796
msgid "Images probably don't have real images - only thumbnails."
msgstr ""
#: src/ctrls/c_main.py:798
msgid "No images was saved."
msgstr ""
#: src/ctrls/c_main.py:807 src/ctrls/c_main.py:811
msgid "Set thumbnail"
msgstr ""
#: src/ctrls/c_main.py:807
msgid "No image selected"
msgstr ""
#: src/ctrls/c_main.py:808 src/ctrls/c_main.py:812
msgid "You have to select one image to set as thumbnail."
msgstr ""
#: src/ctrls/c_main.py:811
msgid "To many images selected"
msgstr ""
#: src/ctrls/c_main.py:870
msgid "Choose export file"
msgstr ""
#: src/ctrls/c_main.py:1016 src/ctrls/c_main.py:1025
msgid "Remove tags"
msgstr ""
#: src/ctrls/c_main.py:1016 src/ctrls/c_main.py:1167
msgid "No files selected"
msgstr ""
#: src/ctrls/c_main.py:1017
msgid "You have to select some files first."
msgstr ""
#: src/ctrls/c_main.py:1025
msgid "No tags selected"
msgstr ""
#: src/ctrls/c_main.py:1026
msgid "You have to select any tag to remove from files."
msgstr ""
#: src/ctrls/c_main.py:1052
msgid "Don't copy images. Generate only thumbnails."
msgstr ""
#: src/ctrls/c_main.py:1116
msgid "Delete disc"
msgstr ""
#: src/ctrls/c_main.py:1116
msgid "No disc selected"
msgstr ""
#: src/ctrls/c_main.py:1117
msgid "You have to select disc first before you can delete it"
msgstr ""
#: src/ctrls/c_main.py:1123
#, python-format
msgid "Delete %s"
msgstr ""
#: src/ctrls/c_main.py:1123
#, python-format
msgid "Delete %s?"
msgstr ""
#: src/ctrls/c_main.py:1124
msgid "Object will be permanently removed."
msgstr ""
#: src/ctrls/c_main.py:1167 src/ctrls/c_main.py:1172
msgid "Delete files"
msgstr ""
#: src/ctrls/c_main.py:1168
msgid "You have to select at least one file to delete."
msgstr ""
#: src/ctrls/c_main.py:1172
msgid "Delete files?"
msgstr ""
#: src/ctrls/c_main.py:1173
msgid ""
"Selected files and directories will be permanently\n"
" removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:1215
msgid "Delete thumbnail"
msgstr ""
#: src/ctrls/c_main.py:1215
msgid "Delete thumbnail?"
msgstr ""
#: src/ctrls/c_main.py:1216
msgid "Current thumbnail will be permanently removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:1364
msgid "Error ejecting device"
msgstr ""
#: src/ctrls/c_main.py:1366
#, python-format
msgid "Cannot eject device pointed to %s"
msgstr ""
#: src/ctrls/c_main.py:1368
#, python-format
msgid ""
"Last eject message:\n"
"%s"
msgstr ""
#: src/ctrls/c_main.py:1372
msgid "Error unmounting device"
msgstr ""
#: src/ctrls/c_main.py:1374
#, python-format
msgid "Cannot unmount device pointed to %s"
msgstr ""
#: src/ctrls/c_main.py:1376
#, python-format
msgid ""
"Last umount message:\n"
"%s"
msgstr ""
#: src/lib/device_helper.py:85
msgid "Eject program not specified"
msgstr "" msgstr ""

View File

@@ -1,228 +0,0 @@
"""
Project: pyGTKtalog
Description: Makefile and setup.py replacement. Used python packages -
paver, nosetests. External commands - xgettext, intltool-extract, hg,
grep.
Type: management
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-07
"""
import os
import sys
import shutil
from datetime import datetime
from paver.easy import sh, dry, call_task, options, Bunch
from paver.tasks import task, needs, help, cmdopts
from paver.setuputils import setup
from paver.misctasks import generate_setup, minilib
import paver.doctools
try:
from pylint import lint
HAVE_LINT = True
except ImportError:
HAVE_LINT = False
PO_HEADER = """#
# pygtktalog Language File
#
msgid ""
msgstr ""
"Project-Id-Version: pygtktalog\\n"
"POT-Creation-Date: %(time)s\\n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: utf-8\\n"
"""
REV = os.popen("hg sum 2>/dev/null|grep ^Revis|cut -d ' ' -f 2").readlines()
if REV:
REV = "r" + REV[0].strip()
else:
REV = '0'
LOCALES = {'pl': 'pl_PL.utf8', 'en': 'en_EN'}
POTFILE = 'locale/pygtktalog.pot'
# distutil/setuptool setup method.
setup(
name='pyGTKtalog',
version='1.99.%s' % REV,
long_description='pyGTKtalog is application similar to WhereIsIT, '
'for collecting information on files from CD/DVD '
'or directories.',
description='pyGTKtalog is a file indexing tool written in pyGTK',
author='Roman Dobosz',
author_email='gryf73@gmail.com',
url='http://google.com',
platforms=['Linux', 'BSD'],
license='GNU General Public License (GPL)',
classifiers=['Development Status :: 2 - Pre-Alpha',
'Environment :: X11 Applications :: GTK',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License '
'(GPL)',
'Natural Language :: English',
'Natural Language :: Polish',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Topic :: Desktop Environment',
'Topic :: Utilities'],
include_package_data=True,
exclude_package_data={'': ['*.patch']},
packages=["pygtktalog"],
scripts=['bin/gtktalog.py'],
test_suite='nose.collector'
)
options(sphinx=Bunch(builddir="build", sourcedir="source"))
@task
@needs(['locale_gen', 'minilib', 'generate_setup'])
def sdist():
"""sdist with message catalogs"""
call_task("setuptools.command.sdist")
@task
@needs(['locale_gen'])
def build():
"""build with message catalogs"""
call_task("setuptools.command.build")
@task
def clean():
"""remove 'pyo', 'pyc', 'h' and '~' files"""
# clean *.pyc, *.pyo and jEdit backup files *~
for root, dummy, files in os.walk("."):
for fname in files:
if fname.endswith(".pyc") or fname.endswith(".pyo") or \
fname.endswith("~") or fname.endswith(".h") or \
fname == '.coverage':
fdel = os.path.join(root, fname)
os.unlink(fdel)
print "deleted", fdel
@task
@needs(["clean"])
def distclean():
"""make clean, and remove any dist/build/egg stuff from project"""
for dirname in [os.path.join('pygtktalog', 'locale'), 'build', 'dist',
'pyGTKtalog.egg-info']:
if os.path.exists(dirname):
shutil.rmtree(dirname, ignore_errors=True)
print "removed directory", dirname
for filename in ['paver-minilib.zip', 'setup.py', 'test/.coverage']:
if os.path.exists(filename):
os.unlink(filename)
print "deleted", filename
@task
def run():
"""run application"""
sh("PYTHONPATH=%s:$PYTHONPATH bin/gtktalog.py" % _setup_env())
#import gtktalog
#gtktalog.run()
@task
def pot():
"""generate 'pot' file out of python/glade files"""
if not os.path.exists('locale'):
os.mkdir('locale')
if not os.path.exists(POTFILE):
fname = open(POTFILE, "w")
fname.write(PO_HEADER % {'time': datetime.now()})
cmd = "xgettext --omit-header -k_ -kN_ -j -o %s %s"
cmd_glade = "intltool-extract --type=gettext/glade %s"
for walk_tuple in os.walk("pygtktalog"):
root = walk_tuple[0]
for fname in walk_tuple[2]:
if fname.endswith(".py"):
sh(cmd % (POTFILE, os.path.join(root, fname)))
elif fname.endswith(".glade"):
sh(cmd_glade % os.path.join(root, fname))
sh(cmd % (POTFILE, os.path.join(root, fname + ".h")))
@task
@needs(['pot'])
def locale_merge():
"""create or merge if exists 'po' translation files"""
potfile = os.path.join('locale', 'pygtktalog.pot')
for lang in LOCALES:
msg_catalog = os.path.join('locale', "%s.po" % lang)
if os.path.exists(msg_catalog):
sh('msgmerge -U %s %s' % (msg_catalog, potfile))
else:
shutil.copy(potfile, msg_catalog)
@task
@needs(['locale_merge'])
def locale_gen():
"""generate message catalog file for available locale files"""
full_path = os.path.join('pygtktalog', 'locale')
if not os.path.exists(full_path):
os.mkdir(full_path)
for lang in LOCALES:
lang_path = full_path
for dirname in [lang, 'LC_MESSAGES']:
lang_path = os.path.join(lang_path, dirname)
if not os.path.exists(lang_path):
os.mkdir(lang_path)
catalog_file = os.path.join(lang_path, 'pygtktalog.mo')
msg_catalog = os.path.join('locale', "%s.po" % lang)
sh('msgfmt %s -o %s' % (msg_catalog, catalog_file))
if HAVE_LINT:
@task
def pylint():
'''Check the module you're building with pylint.'''
pylintopts = ['pygtktalog']
dry('pylint %s' % (" ".join(pylintopts)), lint.Run, pylintopts)
@task
@cmdopts([('coverage', 'c', 'display coverage information')])
def test(options):
"""run unit tests"""
cmd = "PYTHONPATH=%s:$PYTHONPATH nosetests -w test" % _setup_env()
if hasattr(options.test, 'coverage'):
cmd += " --with-coverage --cover-package pygtktalog"
os.system(cmd)
@task
@needs(['locale_gen'])
def runpl():
"""run application with pl_PL localization. This is just for my
convenience"""
os.environ['LC_ALL'] = 'pl_PL.utf8'
run()
def _setup_env():
"""Helper function to set up paths"""
# current directory
this_path = os.path.dirname(os.path.abspath(__file__))
if this_path not in sys.path:
sys.path.insert(0, this_path)
return this_path

69
prefs_prefs.py Normal file
View File

@@ -0,0 +1,69 @@
import pygtk
pygtk.require('2.0')
import gtk
class PreferencesMgr(gtk.Dialog):
def __init__(self):
gtk.Dialog.__init__(self, 'Preferences', None,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_OK, gtk.RESPONSE_OK,
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
self.current_frame = None
self.create_gui()
def create_gui(self):
model = gtk.ListStore(str, gtk.gdk.Pixbuf)
pixbuf = gtk.gdk.pixbuf_new_from_file('/usr/share/icons/Buuf/128x128/status/stock_weather-night-clear.png')
model.append(['General', pixbuf])
pixbuf = gtk.gdk.pixbuf_new_from_file('/usr/share/icons/Buuf/128x128/stock/generic/stock_alarm.png')
model.append(['Security', pixbuf])
self.icon_view = gtk.IconView(model)
self.icon_view.set_text_column(0)
self.icon_view.set_pixbuf_column(1)
self.icon_view.set_orientation(gtk.ORIENTATION_VERTICAL)
self.icon_view.set_selection_mode(gtk.SELECTION_SINGLE)
self.icon_view.connect('selection-changed', self.on_select, model)
self.icon_view.set_columns(1)
self.icon_view.set_item_width(-1)
self.icon_view.set_size_request(72, -1)
self.content_box = gtk.HBox(False)
self.content_box.pack_start(self.icon_view, fill=True, expand=False)
self.icon_view.select_path((0,)) # select a category, will create frame
self.show_all()
self.vbox.pack_start(self.content_box)
self.resize(640, 480)
self.show_all()
def on_select(self, icon_view, model=None):
selected = icon_view.get_selected_items()
if len(selected) == 0: return
i = selected[0][0]
category = model[i][0]
if self.current_frame is not None:
self.content_box.remove(self.current_frame)
self.current_frame.destroy()
self.current_frame = None
if category == 'General':
self.current_frame = self.create_general_frame()
elif category == 'Security':
self.current_frame = self.create_security_frame()
self.content_box.pack_end(self.current_frame, fill=True, expand=True)
self.show_all()
def create_general_frame(self):
frame = gtk.Frame('General')
return frame
def create_security_frame(self):
frame = gtk.Frame('Security')
return frame
if __name__ == '__main__':
p = PreferencesMgr()
p.run()
p.destroy()

32
prepare_dist_package.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/sh
# remove ~, pyc, pyo files from current directory
mkdir t 2>/dev/null
if [ $? != 0 ]; then
echo "cannot create directory 't': File exist."
echo "Rename it, move or rename, bcoz it's on the way."
exit
fi
cd t
alias ls=ls
PREV=`ls -1 ..|grep bz2|tail -n 1|cut -f '2' -d '_'|cut -f 1 -d '.'`
REV=`svn export svn://10.0.0.10/repos/Python/pyGTKtalog pyGTKtalog |tail -n 1|cut -f 3 -d " "|cut -f 1 -d '.'`
cd pyGTKtalog
find . -name \*~ -exec rm '{}' ';'
find . -name \*pyc -exec rm '{}' ';'
find . -name \*pyo -exec rm '{}' ';'
find . -type d -name .svn -exec rm -fr '{}' ';'
rm -fr db img
rm -fr prepare_dist_package.sh
svn log -r ${PREV}:HEAD -v svn://10.0.0.10/repos/Python/pyGTKtalog > CHANGELOG
cd ..
tar jcf ../pygtktalog_${REV}.tar.bz2 pyGTKtalog
cd ..
rm -fr t

11
pyGTKtalog Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
# Simple shell wraper to launch pyGTKtalog
# change path to pygtktalog.py file full path, then you can move this shell
# script to desired place (like /usr/bin, /usr/local/bin, ~/bin and so on)
path_to_gtktalog_directory="."
# python interpreter
python_intrpreter="/usr/bin/python"
exec $python_intrpreter -OO ${path_to_gtktalog_directory}/pygtktalog.py "$@"

75
pygtktalog.py Normal file
View File

@@ -0,0 +1,75 @@
"""
Project: pyGTKtalog
Description: Application main launch file.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2007-05-01
"""
import sys
import os
import locale
import gettext
import gtk
import pygtk
pygtk.require("2.0")
import gtkmvc
gtkmvc.require("1.2.2")
from src.lib.globs import TOPDIR
from src.lib.globs import APPL_SHORT_NAME
sys.path = [os.path.join(TOPDIR, "src")] + sys.path
from models.m_config import ConfigModel
from models.m_main import MainModel
from ctrls.c_main import MainController
from views.v_main import MainView
def check_requirements():
"""Checks versions and other requirements"""
conf = ConfigModel()
conf.load()
if conf.confd['thumbs'] and conf.confd['retrive']:
try:
import Image
except ImportError:
print _("WARNING: You'll need Python Imaging Library (PIL), if "
"you want to make thumbnails!")
raise
return
def run():
"""Create model, controller and view and launch it."""
# Directory from where pygtkatalog was invoced. We need it for calculate
# path for argument (catalog file)
execution_dir = os.path.abspath(os.path.curdir)
# Directory, where this files lies. We need it to setup private source
# paths
libraries_dir = os.path.dirname(__file__)
if libraries_dir:
os.chdir(libraries_dir)
# Setup i18n
locale.setlocale(locale.LC_ALL, '')
gettext.install(APPL_SHORT_NAME, 'locale', unicode=True)
check_requirements()
model = MainModel()
if len(sys.argv) > 1:
model.open(os.path.join(execution_dir, sys.argv[1]))
controler = MainController(model)
view = MainView(controler)
try:
gtk.main()
except KeyboardInterrupt:
model.config.save()
model.cleanup()
gtk.main_quit
if __name__ == "__main__":
run()

View File

@@ -1,65 +0,0 @@
"""
Project: pyGTKtalog
Description: Initialization for main module - i18n and so.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-05
"""
__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 sys
import locale
import gettext
import __builtin__
import gtk.glade
from logger import get_logger
__all__ = ['controllers',
'models',
'views',
'EXIF',
'dbcommon',
'dbobjects',
'dialogs',
'logger',
'misc']
GETTEXT_DOMAIN = 'pygtktalog'
# There should be message catalogs in "locale" directory placed by setup.py
# script. If there is no such directory, let's assume that message catalogs are
# placed in system wide location such as /usr/share/locale by Linux
# distribution package maintainer.
LOCALE_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'locale')
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
# unknown locale string, fallback to C
locale.setlocale(locale.LC_ALL, 'C')
for module in gtk.glade, gettext:
if os.path.exists(LOCALE_PATH):
module.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
else:
module.bindtextdomain(GETTEXT_DOMAIN)
module.textdomain(GETTEXT_DOMAIN)
# register the gettext function for the whole interpreter as "_"
__builtin__._ = gettext.gettext
# wrap errors into usefull message
def log_exception(exc_type, exc_val, traceback):
get_logger(__name__).error(exc_val)
sys.excepthook = log_exception

View File

@@ -1,18 +0,0 @@
"""
Project: pyGTKtalog
Description: Controller for Details NoteBook
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
from gtkmvc import Controller
class DetailsController(Controller):
"""
Controller for details NoteBook.
"""
def register_view(self, view):
"""Default view registration stuff"""
pass

View File

@@ -1,214 +0,0 @@
"""
Project: pyGTKtalog
Description: Controller for Discs TreeView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
import gtk
from gtkmvc import Controller
from pygtktalog.logger import get_logger
LOG = get_logger("discs ctrl")
class DiscsController(Controller):
"""
Controller for discs TreeView
"""
def __init__(self, model, view):
"""
Initialize DiscsController
"""
LOG.debug(self.__init__.__doc__.strip())
Controller.__init__(self, model, view)
self.discs_model = self.model.discs.discs
def register_view(self, view):
"""
Do DiscTree registration
"""
LOG.debug(self.register_view.__doc__.strip())
view['discs'].set_model(self.discs_model)
# register observers
self.model.discs.register_observer(self)
# connect signals to popup menu - framework somehow omits automatic
# signal connection for subviews which are not included to widgets
# tree
sigs = {'expand_all': ('activate', self.on_expand_all_activate),
'collapse_all': ('activate', self.on_collapse_all_activate),
'update': ('activate', self.on_update_activate),
'rename': ('activate', self.on_rename_activate),
'delete': ('activate', self.on_delete_activate),
'statistics': ('activate', self.on_statistics_activate)}
for signal in sigs:
view.menu[signal].connect(sigs[signal][0], sigs[signal][1])
col = gtk.TreeViewColumn('discs_column')
cellpb = gtk.CellRendererPixbuf()
cell = gtk.CellRendererText()
# make cell text editabe
cell.set_property('editable', True)
cell.connect('edited', self.on_editing_done, self.discs_model)
# TODO: find a way how to disable default return keypress on editable
# fields
col.pack_start(cellpb, False)
col.pack_start(cell, True)
col.set_attributes(cellpb, stock_id=2)
col.set_attributes(cell, text=1)
view['discs'].append_column(col)
view['discs'].show()
# treeview signals
def on_discs_button_press_event(self, treeview, event):
"""
Handle right click on discs treeview - show popup menu.
"""
LOG.debug(self.on_discs_button_press_event.__doc__.strip())
pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
if event.button == 3:
if pathinfo:
path = pathinfo[0]
# Make sure, that there is selected row
sel = treeview.get_selection()
sel.unselect_all()
sel.select_path(path)
self._popup_menu(sel, event, event.button)
else:
self._popup_menu(None, event, event.button)
return True
def on_discs_cursor_changed(self, treeview):
"""
Show files on right treeview, after clicking the left disc treeview.
"""
LOG.debug(self.on_discs_cursor_changed.__doc__.strip())
selection = treeview.get_selection()
path = selection.get_selected_rows()[1][0]
self.model.files.refresh(self.discs_model.get_value(\
self.discs_model.get_iter(path), 0))
def on_discs_key_release_event(self, treeview, event):
"""
Watch for specific keys
"""
LOG.debug(self.on_discs_key_release_event.__doc__.strip())
if gtk.gdk.keyval_name(event.keyval) == 'Menu':
LOG.debug('Menu key pressed')
self._popup_menu(treeview.get_selection(), event, 0)
return True
return False
def on_discs_row_activated(self, treeview, path, treecolumn):
"""
If possible, expand or collapse branch of discs tree
"""
LOG.debug(self.on_discs_row_activated.__doc__.strip())
if treeview.row_expanded(path):
treeview.collapse_row(path)
else:
treeview.expand_row(path, False)
def on_editing_done(self, cell, path, new_text, model):
"""
Store changed filename text
"""
LOG.debug(self.on_editing_done.__doc__.strip())
model[path][0].filename = new_text
model[path][1] = new_text
return
# popup menu signals
def on_expand_all_activate(self, menu_item):
"""
Expand all
"""
LOG.debug(self.on_expand_all_activate.__doc__.strip())
self.view['discs'].expand_all()
def on_collapse_all_activate(self, menu_item):
"""
Collapse all
"""
LOG.debug(self.on_collapse_all_activate.__doc__.strip())
self.view['discs'].collapse_all()
def on_update_activate(self, menu_item):
"""
Trigger update specified tree entry
"""
LOG.debug(self.on_update_activate.__doc__.strip())
raise NotImplementedError
def on_rename_activate(self, menu_item):
"""
Rename disk or directory
"""
LOG.debug(self.on_rename_activate.__doc__.strip())
treeview = self.view['discs']
selection = treeview.get_selection()
path = selection.get_selected_rows()[1][0]
treeview.set_cursor(path, treeview.get_column(0), start_editing=True)
def on_delete_activate(self, menu_item):
"""
Delete disk or directory from catalog
"""
LOG.debug(self.on_delete_activate.__doc__.strip())
raise NotImplementedError
def on_statistics_activate(self, menu_item):
"""
Show statistics for selected item
"""
LOG.debug(self.on_statistics_activate.__doc__.strip())
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())
self._set_cursor_to_obj_position(new)
# private methods
def _popup_menu(self, selection, event, button):
"""
Popup menu for discs treeview. Gather information from discs model,
and trigger menu popup.
"""
LOG.debug(self._popup_menu.__doc__.strip())
if selection is None:
self.view.menu.set_menu_items_sensitivity(False)
else:
model, list_of_paths = selection.get_selected_rows()
for path in list_of_paths:
self.view.menu.set_menu_items_sensitivity(True)
self.view.menu.set_update_sensitivity(model.get_value(\
model.get_iter(path), 0).parent_id == 1)
self.view.menu['discs_popup'].popup(None, None, None,
button, event.time)
def _set_cursor_to_obj_position(self, obj):
"""
Set cursor/focus to specified object postion in Discs treeview.
"""
path = self.model.discs.find_path(obj)
self.view['discs'].expand_to_path(path)
self.view['discs'].set_cursor(path)

View File

@@ -1,282 +0,0 @@
"""
Project: pyGTKtalog
Description: Controller for Files TreeView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
import gtk
from gtkmvc import Controller
from pygtktalog.pygtkutils import get_tv_item_under_cursor
from pygtktalog.logger import get_logger
LOG = get_logger("files ctrl")
class FilesController(Controller):
"""
Controller for files TreeView list.
"""
def __init__(self, model, view):
"""
FilesController initialization
"""
Controller.__init__(self, model, view)
self.DND_TARGETS = [('files_tags', 0, 69)]
self.files_model = self.model.files
def register_view(self, view):
"""
Register view, and setup columns for files treeview
"""
view['files'].set_model(self.files_model.files)
sigs = {"add_tag": ("activate", self.on_add_tag1_activate),
"delete_tag": ("activate", self.on_delete_tag_activate),
"add_thumb": ("activate", self.on_add_thumb1_activate),
"remove_thumb": ("activate", self.on_remove_thumb1_activate),
"add_image": ("activate", self.on_add_image1_activate),
"remove_image": ("activate", self.on_remove_image1_activate),
"edit": ("activate", self.on_edit2_activate),
"delete": ("activate", self.on_delete3_activate),
"rename": ("activate", self.on_rename2_activate)}
for signal in sigs:
view.menu[signal].connect(sigs[signal][0], sigs[signal][1])
view['files'].get_selection().set_mode(gtk.SELECTION_MULTIPLE)
col = gtk.TreeViewColumn(_('Disc'), gtk.CellRendererText(), text=1)
col.set_sort_column_id(1)
col.set_resizable(True)
col.set_visible(False)
view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Filename'))
cellpb = gtk.CellRendererPixbuf()
cell = gtk.CellRendererText()
col.pack_start(cellpb, False)
col.pack_start(cell, True)
col.set_attributes(cellpb, stock_id=7)
col.set_attributes(cell, text=2)
col.set_sort_column_id(2)
col.set_resizable(True)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Path'), gtk.CellRendererText(), text=3)
col.set_sort_column_id(3)
col.set_resizable(True)
col.set_visible(False)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(), text=4)
col.set_sort_column_id(4)
col.set_resizable(True)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Date'), gtk.CellRendererText(), text=5)
col.set_sort_column_id(5)
col.set_resizable(True)
self.view['files'].append_column(col)
self.view['files'].set_search_column(2)
# setup d'n'd support
self.view['files'].drag_source_set(gtk.gdk.BUTTON1_MASK,
self.DND_TARGETS,
gtk.gdk.ACTION_COPY)
# signals
def on_files_drag_data_get(self, treeview, context, selection,
targetType, eventTime):
"""responce to "data get" DnD signal"""
# get selection, and send it to the client
if targetType == self.DND_TARGETS[0][2]:
# get selection
treesrl = treeview.get_selection()
model, list_of_paths = treesrl.get_selected_rows()
ids = []
for path in list_of_paths:
fid = model.get_value(model.get_iter(path), 0)
ids.append(fid)
string = str(tuple(ids)).replace(",)", ")")
selection.set(selection.target, 8, string)
def on_files_button_press_event(self, treeview, event):
"""
Handle right click on files treeview - show popup menu.
"""
LOG.debug("Mouse button pressed")
pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
if event.button == 3: # Right mouse button. Show context menu.
LOG.debug("It's a right button")
if pathinfo:
path = pathinfo[0]
# Make sure, that there is selected row
sel = treeview.get_selection()
sel.unselect_all()
sel.select_path(path)
self._popup_menu(sel, event, event.button)
else:
self._popup_menu(None, event, event.button)
return True
#try:
# selection = tree.get_selection()
# model, list_of_paths = selection.get_selected_rows()
#except TypeError:
# list_of_paths = []
#if len(list_of_paths) == 0:
# # try to select item under cursor
# try:
# path, column, x, y = tree.get_path_at_pos(int(event.x),
# int(event.y))
# except TypeError:
# # failed, do not show any popup and return
# tree.get_selection().unselect_all()
# return False
# selection.select_path(path[0])
#if len(list_of_paths) > 1:
# self.view['add_image'].set_sensitive(False)
# self.view['rename'].set_sensitive(False)
# self.view['edit'].set_sensitive(False)
#else:
# self.view['add_image'].set_sensitive(True)
# self.view['rename'].set_sensitive(True)
# self.view['edit'].set_sensitive(True)
#self.__popup_menu(event, 'files_popup')
#return True
LOG.debug("It's other button")
def on_files_cursor_changed(self, treeview):
"""Show details of selected file/directory"""
file_id = get_tv_item_under_cursor(treeview)
LOG.debug("found item: %s" % file_id)
return
def on_files_key_release_event(self, treeview, event):
"""do something with pressed keys"""
if gtk.gdk.keyval_name(event.keyval) == 'Menu':
try:
selection = treeview.get_selection()
model, list_of_paths = selection.get_selected_rows()
if not list_of_paths:
return
except TypeError:
return
self._popup_menu(selection, event, 0)
if gtk.gdk.keyval_name(event.keyval) == 'BackSpace':
row, gtk_column = self.view['files'].get_cursor()
if row and gtk_column:
fileob = self.files_model.get_value(row)
if fileob.parent.parent.id != 1:
#self.files_model.refresh(fileob.parent.parent)
# TODO: synchronize with disks
self.model.discs.currentdir = fileob.parent.parent
self.view['files'].grab_focus()
def on_files_row_activated(self, files_obj, row, column):
"""
On directory doubleclick in files listview dive into desired branch.
"""
fileob = self.files_model.get_value(row=row)
if not fileob.children:
# ONLY directories. files are omitted.
return
self.files_model.refresh(fileob)
self.model.discs.currentdir = fileob
self.view['files'].grab_focus()
# TODO: synchronize with disks
return
def on_add_tag1_activate(self, menu_item):
"""
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
def _popup_menu(self, selection, event, button):
"""
Popup menu for files treeview. Gather information from discs model,
and trigger menu popup.
"""
LOG.debug(self._popup_menu.__doc__.strip())
if selection is None:
self.view.menu.set_menu_items_sensitivity(False)
else:
model, list_of_paths = selection.get_selected_rows()
for path in list_of_paths:
self.view.menu.set_menu_items_sensitivity(True)
self.view.menu['files_popup'].popup(None, None, None,
button, event.time)

View File

@@ -1,201 +0,0 @@
"""
Project: pyGTKtalog
Description: Controller for main window
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import gtk
from gtkmvc import Controller
from pygtktalog.controllers.discs import DiscsController
from pygtktalog.controllers.files import FilesController
#from pygtktalog.controllers.details import DetailsController
#from pygtktalog.controllers.tags import TagcloudController
#from pygtktalog.dialogs import yesno, okcancel, info, warn, error
from pygtktalog.dialogs import open_catalog, save_catalog, error, yesno, about
from pygtktalog.logger import get_logger
LOG = get_logger("main controller")
class MainController(Controller):
"""
Controller for main application window
"""
TITLE = "pyGTKtalog"
UNTITLED = _("untitled")
def __init__(self, model, view):
"""
Initialize MainController, add controllers for trees and details.
"""
LOG.debug(self.__init__.__doc__.strip())
Controller.__init__(self, model, view)
# add controllers for files/tags/details components
self.discs = DiscsController(model, view.discs)
self.files = FilesController(model, view.files)
#self.details = DetailsController(model, view.details)
#self.tags = TagcloudController(model, view.tags)
def register_view(self, view):
"""
Registration view for MainController class
"""
LOG.debug(self.register_view.__doc__.strip())
LOG.debug("replace hardcoded defaults with configured!")
view['main'].set_default_size(800, 600)
view['hpaned1'].set_position(200)
if not self.model.tmp_filename:
view.set_widgets_app_sensitivity(False)
view['main'].show()
# status bar
LOG.debug("register statusbar")
self.context_id = self.view['mainStatus'].get_context_id('status')
self.statusbar_id = \
self.view['mainStatus'].push(self.context_id,
self.model.status_bar_message)
# signals
def on_main_destroy_event(self, widget, event):
"""
Window destroyed. Cleanup before quit.
"""
LOG.debug(self.on_main_destroy_event.__doc__.strip())
self.on_quit_activate(widget)
return True
def on_quit_activate(self, widget):
"""
Quit and save window parameters to config file
"""
LOG.debug(self.on_quit_activate.__doc__.strip())
#if yesno(_("Do you really want to quit?"),
# _("Current database is not saved, any changes will be "
# "lost."), _("Quit application") + " - pyGTKtalog", 0):
self.model.cleanup()
LOG.debug("quit application")
gtk.main_quit()
return False
def on_new_activate(self, widget):
"""
Create new catalog file
"""
LOG.debug(self.on_new_activate.__doc__.strip())
self.model.new()
self._set_title()
self.view.set_widgets_app_sensitivity(True)
def on_open_activate(self, widget):
"""
Open catalog file
"""
LOG.debug(self.on_open_activate.__doc__.strip())
if self.model.db_unsaved and 'confirm' in self.model.config and \
self.model.config['confirm']:
if not yesno(_("Current database is not saved"),
_("Do you really want to open new file and abandon "
"current one?"),
_("Unsaved data")):
LOG.debug("Cancel opening catalog file - unsaved data remain")
return
initial_path = None
#if self.model.config.recent and self.model.config.recent[0]:
# initial_path = os.path.dirname(self.model.config.recent[0])
#if not path:
path = open_catalog(path=initial_path)
if not path:
return
# cleanup files and details
try:
self.model.files_list.clear()
except:
pass
#self.__hide_details()
#self.view['tag_path_box'].hide()
#buf = self.view['tag_cloud_textview'].get_buffer()
#buf.set_text('')
#self.view['tag_cloud_textview'].set_buffer(buf)
if not self.model.open(path):
error(_("Cannot open file."),
_("File %s cannot be open") % path,
_("Error opening file"))
else:
#self.__generate_recent_menu()
#self.__activate_ui(path)
#self.__tag_cloud()
self.view.set_widgets_app_sensitivity()
self._set_title()
return
def on_about1_activate(self, widget):
"""Show about dialog"""
about()
def on_save_activate(self, widget):
"""
Save current catalog
"""
LOG.debug(self.on_save_activate.__doc__.strip())
if not self.model.cat_fname:
self.on_save_as_activate(widget)
else:
self.model.save()
self._set_title()
def on_save_as_activate(self, widget):
"""
Save current catalog under differnet file
"""
LOG.debug(self.on_save_as_activate.__doc__.strip())
initial_path = None
#if self.model.config.recent[0]:
# initial_path = os.path.dirname(self.model.config.recent[0])
path = save_catalog(path=initial_path)
if path:
ret, err = self.model.save(path)
if ret:
#self.model.config.add_recent(path)
self._set_title()
pass
else:
error(_("Cannot write file %s.") % path,
"%s" % err,
_("Error writing file"))
# helpers
def _set_title(self):
"""
Get title of the main window, to reflect state of the catalog file
Returns:
String with apropriate title for main form
"""
LOG.debug("change the title")
if not self.model.tmp_filename:
LOG.debug("application has been initialized, title should"
" be empty")
fname = ""
elif not self.model.cat_fname:
fname = self.UNTITLED
else:
fname = self.model.cat_fname
modified = self.model.db_unsaved and "*" or ""
self.view['main'].set_title("%s%s - %s" % (fname,
modified, self.TITLE))

View File

@@ -1,23 +0,0 @@
"""
Project: pyGTKtalog
Description: Controller for Tagcloud TextView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
from gtkmvc import Controller
class TagcloudController(Controller):
"""
Controller for Tagcloud TextView
"""
#def __init__(self, model, view):
# """Initialize main controller"""
# Controller.__init__(self, model, view)
# return
def register_view(self, view):
"""Default view registration stuff"""
pass

View File

@@ -1,45 +0,0 @@
"""
Project: pyGTKtalog
Description: Common database operations.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-07
"""
from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from pygtktalog.logger import get_logger
# setup SQLAlchemy logging facility
# TODO: Logger("sqlalchemy")
# or maybe it will be better to separate sqlalchemy stuff from application
#get_logger("sqlalchemy", 'INFO')
# Prepare SQLAlchemy objects
Meta = MetaData()
Base = declarative_base(metadata=Meta)
Session = sessionmaker()
LOG = get_logger("dbcommon")
def connect(filename=None):
"""
create engine and bind to Meta object.
Arguments:
@filename - string with absolute or relative path to sqlite database
file. If None, db in-memory will be created
"""
if not filename:
filename = ':memory:'
LOG.info("db filename: %s" % filename)
connect_string = "sqlite:///%s" % filename
engine = create_engine(connect_string)
Meta.bind = engine
Meta.create_all(checkfirst=True)

View File

@@ -1,299 +0,0 @@
"""
Project: pyGTKtalog
Description: Definition of DB objects classes. Using SQLAlchemy.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-07
"""
import os
from cStringIO import StringIO
from hashlib import sha256
from sqlalchemy import Column, Table, Integer, Text, Binary, \
DateTime, ForeignKey, Sequence
from sqlalchemy.orm import relation, backref
from pygtktalog.dbcommon import Base
from pygtktalog.thumbnail import ThumbCreator
IMG_PATH = "/home/gryf/.pygtktalog/imgs/" # FIXME: should be configurable
tags_files = Table("tags_files", Base.metadata,
Column("file_id", Integer, ForeignKey("files.id")),
Column("tag_id", Integer, ForeignKey("tags.id")))
TYPE = {'root': 0, 'dir': 1, 'file': 2, 'link': 3}
class File(Base):
__tablename__ = "files"
id = Column(Integer, Sequence("file_id_seq"), primary_key=True)
parent_id = Column(Integer, ForeignKey("files.id"))
filename = Column(Text)
filepath = Column(Text)
date = Column(DateTime)
size = Column(Integer)
type = Column(Integer)
source = Column(Integer)
note = Column(Text)
description = Column(Text)
checksum = Column(Text)
thumbnail = Column(Binary)
children = relation('File',
backref=backref('parent', remote_side="File.id"),
order_by=[type, filename])
tags = relation("Tag", secondary=tags_files, order_by="Tag.tag")
#thumbnail = relation("Thumbnail", backref="file")
images = relation("Image", backref="file")
def __init__(self, filename=None, path=None, date=None, size=None,
ftype=None, src=None):
"""Create file object with empty defaults"""
self.filename = filename
self.filepath = path
self.date = date
self.size = size
self.type = ftype
self.source = src
def __repr__(self):
return "<File('%s', %s)>" % (str(self.filename), str(self.id))
def generate_checksum(self):
"""
Generate checksum of first 10MB of the file
"""
if self.type != TYPE['file']:
return
buf = open(os.path.join(self.filepath, self.filename)).read(10485760)
self.checksum = sha256(buf).hexdigest()
def get_all_children(self):
"""
Return list of all node direct and indirect children
"""
def _recursive(node):
children = []
if node.children:
for child in node.children:
children += _recursive(child)
if node != self:
children.append(node)
return children
if self.children:
return _recursive(self)
else:
return []
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, Sequence("group_id_seq"), primary_key=True)
name = Column(Text)
color = Column(Text)
def __init__(self, name=None, color=None):
self.name = name
self.color = color
def __repr__(self):
return "<Group('%s', %s)>" % (str(self.name), str(self.id))
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, Sequence("tags_id_seq"), primary_key=True)
group_id = Column(Integer, ForeignKey("groups.id"))
tag = Column(Text)
group = relation('Group', backref=backref('tags', remote_side="Group.id"))
files = relation("File", secondary=tags_files)
def __init__(self, tag=None, group=None):
self.tag = tag
self.group = group
def __repr__(self):
return "<Tag('%s', %s)>" % (str(self.tag), str(self.id))
#class Thumbnail(Base):
# __tablename__ = "thumbnails"
# id = Column(Integer, Sequence("thumbnail_id_seq"), primary_key=True)
# file_id = Column(Integer, ForeignKey("files.id"))
# filename = Column(Text)
#
# def __init__(self, filename=None, file_obj=None):
# self.filename = filename
# self.file = file_obj
# if self.filename:
# self.save(self.filename)
#
# def save(self, fname):
# """
# Create file related thumbnail, add it to the file object.
# """
# new_name = sha1(str(uuid1())).hexdigest()
# new_name = [new_name[start:start+10] for start in range(0,
# len(new_name),
# 10)]
# try:
# os.makedirs(os.path.join(IMG_PATH, *new_name[:-1]))
# except OSError as exc:
# if exc.errno != errno.EEXIST:
# raise
#
# ext = os.path.splitext(self.filename)[1]
# if ext:
# new_name.append("".join([new_name.pop(), ext]))
#
# thumb = thumbnail.Thumbnail(self.filename)
# thumb_tmp_name = thumb.save()
# name, ext = os.path.splitext(new_name.pop())
# new_name.append("".join([name, "_t", '.jpg']))
# self.filename = os.path.sep.join(new_name)
# shutil.move(thumb_tmp_name, os.path.join(IMG_PATH, *new_name))
#
# def get_copy(self):
# """
# Create the very same object as self with exception of id field
# """
# thumb = Thumbnail()
# thumb.filename = self.filename
# return thumb
#
# def __repr__(self):
# return "<Thumbnail('%s', %s)>" % (str(self.filename), str(self.id))
#
class Image(Base):
__tablename__ = "images"
id = Column(Integer, Sequence("images_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
image = Column(Binary)
thumb = Column(Binary)
checksum = Column(Text)
def __init__(self, filename=None, file_obj=None):
self.file = file_obj
if filename:
self.save(filename)
def save(self, fname):
"""
Save and create coressponding thumbnail (note: it differs from file
related thumbnail!)
"""
file_buffer = StringIO()
with open(fname) as f:
file_buffer.write(f.read())
self.image = file_buffer.getvalue()
self.checksum = sha256(file_buffer.getvalue()).hexdigest()
file_buffer.seek(0)
thumb = ThumbCreator(fname).generate()
if thumb:
self.thumb = thumb.getvalue()
thumb.close()
file_buffer.close()
def get_copy(self):
"""
Create the very same object as self with exception of id field
"""
img = Image()
img.image = self.image
img.thumb = self.thumb
img.checksum = self.checksum
return img
@property
def fthumb(self):
"""
Return file-like object with thumbnail
"""
if self.thumb:
buf = StringIO()
buf.write(self.thumb)
buf.seek(0)
return buf
else:
return None
@property
def fimage(self):
"""
Return file-like object with image
"""
if self.image:
buf = StringIO()
buf.write(self.image)
buf.seek(0)
return buf
else:
return None
def __repr__(self):
return "<Image(%s)>" % str(self.id)
class Exif(Base):
__tablename__ = "exif"
id = Column(Integer, Sequence("exif_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
camera = Column(Text)
date = Column(Text)
aperture = Column(Text)
exposure_program = Column(Text)
exposure_bias = Column(Text)
iso = Column(Text)
focal_length = Column(Text)
subject_distance = Column(Text)
metering_mode = Column(Text)
flash = Column(Text)
light_source = Column(Text)
resolution = Column(Text)
orientation = Column(Text)
def __init__(self):
self.camera = None
self.date = None
self.aperture = None
self.exposure_program = None
self.exposure_bias = None
self.iso = None
self.focal_length = None
self.subject_distance = None
self.metering_mode = None
self.flash = None
self.light_source = None
self.resolution = None
self.orientation = None
def __repr__(self):
return "<Exif('%s', %s)>" % (str(self.date), str(self.id))
class Gthumb(Base):
__tablename__ = "gthumb"
id = Column(Integer, Sequence("gthumb_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
note = Column(Text)
place = Column(Text)
date = Column(DateTime)
def __init__(self, note=None, place=None, date=None):
self.note = note
self.place = place
self.date = date
def __repr__(self):
return "<Gthumb('%s', '%s', %s)>" % (str(self.date), str(self.place),
str(self.id))

View File

@@ -1,231 +0,0 @@
"""
Project: pyGTKtalog
Description: Simple dialogs for interact with user
Type: helper
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-12
"""
import os
import gtk
import pygtktalog
class Dialog(object):
"""
Show simple dialog for questions
Returns: Bool - True, if "OK" button pressed, False otherwise.
"""
def __init__(self, dialog_type, message, secondary_msg="", title=""):
"""
Initialize some defaults
"""
self.dialog = None
self.buttons = gtk.BUTTONS_OK
self.ok_default = False
self.message = message
self.secondary_msg = secondary_msg
self.type = dialog_type
self.title = title
def run(self):
"""Show the dialog"""
if self.dialog is None:
self._create_dialog()
# Change default/focus from cancel/no to ok/yes. Suitable only for
# Question dialog.
# Ofcourse, if something changes in the future, this could break
# things.
if self.ok_default:
# this is tricky: Ok/Yes buttons appears as first on the list, but
# they are visibile in oposite order. This could be a bug.
button = self.dialog.get_action_area().get_children()[0]
button.grab_default()
retval = self.dialog.run()
self.dialog.destroy()
if retval == gtk.RESPONSE_OK or retval == gtk.RESPONSE_YES:
return True
return False
def _create_dialog(self):
"""Create MessageDialog widgt"""
if self.type == gtk.MESSAGE_QUESTION and \
self.buttons not in (gtk.BUTTONS_YES_NO, gtk.BUTTONS_OK_CANCEL):
self.buttons = gtk.BUTTONS_YES_NO
self.dialog = gtk.MessageDialog(buttons=self.buttons, type=self.type,
message_format=self.message)
self.dialog.format_secondary_text(self.secondary_msg)
self.dialog.set_title(self.title)
class ChooseFile(object):
"""
Common file chooser
"""
URI = None
BUTTON_PAIRS = {'cancel': (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
'ok': (gtk.STOCK_OK, gtk.RESPONSE_APPLY),
'save': (gtk.STOCK_SAVE, gtk.RESPONSE_APPLY),
'open': (gtk.STOCK_OPEN, gtk.RESPONSE_APPLY)}
CHOOSER_TYPES = {'open': gtk.FILE_CHOOSER_ACTION_OPEN,
'save': gtk.FILE_CHOOSER_ACTION_SAVE}
FILTERS = {'catalogs': {'name': "Catalog files",
'patterns': ("*.sqlite", "*.sqlite.bz2")},
'all': {'name': "All files", 'patterns': ("*.*",)}}
def __init__(self, title="", buttons=('cancel', 'ok'), path=None,
chooser_type="open"):
super(ChooseFile, self).__init__()
self.path = path
self.title = title
self.action = self.CHOOSER_TYPES[chooser_type]
self.buttons = []
for button in buttons:
self.buttons.append(self.BUTTON_PAIRS[button][0])
self.buttons.append(self.BUTTON_PAIRS[button][1])
self.buttons = tuple(self.buttons)
self.confirmation = False
self.dialog = None
self.filters = []
def _mk_dialog(self):
"""
Create FileChooserDialog object
"""
self.dialog = gtk.FileChooserDialog(self.title, None, self.action,
self.buttons)
self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
self.dialog.set_default_response(gtk.RESPONSE_OK)
self.dialog.set_do_overwrite_confirmation(self.confirmation)
self.dialog.set_title(self.title)
if self.URI:
self.dialog.set_current_folder_uri(self.URI)
elif self.path and os.path.exists(self.path):
self.path = "file://" + os.path.abspath(self.path)
self.dialog.set_current_folder_uri(self.path)
for filtr in self._get_filters():
self.dialog.add_filter(filtr)
def _get_filters(self):
"""
"""
filters = []
for filter_def in self.filters:
filtr = gtk.FileFilter()
filtr.set_name(self.FILTERS[filter_def]['name'])
for pat in self.FILTERS[filter_def]['patterns']:
filtr.add_pattern(pat)
filters.append(filtr)
return filters
def run(self):
"""
Show dialog, get response.
Returns:
Returns: String - with filename, None otherwise.
"""
if self.dialog is None:
self._mk_dialog()
response = self.dialog.run()
filename = None
if response == gtk.RESPONSE_APPLY:
filename = self.dialog.get_filename()
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
def yesno(message, secondarymsg="", title="", default=False):
"""Question with yes-no buttons. Returns False on 'no', True on 'yes'"""
dialog = Dialog(gtk.MESSAGE_QUESTION, message, secondarymsg, title)
dialog.buttons = gtk.BUTTONS_YES_NO
dialog.ok_default = default
return dialog.run()
def okcancel(message, secondarymsg="", title="", default=False):
"""Question with ok-cancel buttons. Returns False on 'cancel', True on
'ok'"""
dialog = Dialog(gtk.MESSAGE_QUESTION, message, secondarymsg, title)
dialog.buttons = gtk.BUTTONS_OK_CANCEL
dialog.ok_default = default
return dialog.run()
def info(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Info dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_INFO, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def warn(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Warning dialog. Button defaults to gtk.BUTTONS_OK, but can be changed
with gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_WARNING, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def error(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Error dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_ERROR, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def open_catalog(title=_("Open catalog"), path=None):
"""
Request filename from user to open.
Returns: string - full path and filename or None
"""
requester = ChooseFile(title)
requester.filters = ['catalogs', 'all']
return requester.run()
def save_catalog(title=_("Open catalog"), path=None):
"""
Request filename from user for save.
Returns: string - full path and filename or None
"""
requester = ChooseFile(title, chooser_type="save")
requester.filters = ['catalogs', 'all']
requester.confirmation = True
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

@@ -1,81 +0,0 @@
"""
Project: pyGTKtalog
Description: Logging functionality
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-09-02
"""
import os
import sys
import logging
LEVEL = {'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARN': logging.WARN,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL}
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
COLORS = {'WARNING': YELLOW,
'INFO': GREEN,
'DEBUG': BLUE,
'CRITICAL': WHITE,
'ERROR': RED}
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) \
+ levelname + RESET_SEQ
record.levelname = levelname_color
return logging.Formatter.format(self, record)
#def get_logger(module_name, level='INFO', to_file=False):
#def get_logger(module_name, level='DEBUG', to_file=True):
def get_logger(module_name, level='DEBUG', to_file=False):
"""
Prepare and return log object. Standard formatting is used for all logs.
Arguments:
@module_name - String name for Logger object.
@level - Log level (as string), one of DEBUG, INFO, WARN, ERROR and
CRITICAL.
@to_file - If True, additionally stores full log in file inside
.pygtktalog config directory and to stderr, otherwise log
is only redirected to stderr.
Returns: object of logging.Logger class
"""
path = os.path.join(os.path.expanduser("~"), ".pygtktalog", "app.log")
#path = "/dev/null"
log = logging.getLogger(module_name)
log.setLevel(LEVEL[level])
console_handler = logging.StreamHandler(sys.stderr)
console_formatter = ColoredFormatter("%(filename)s:%(lineno)s - "
"%(levelname)s - %(message)s")
console_handler.setFormatter(console_formatter)
log.addHandler(console_handler)
if to_file:
file_handler = logging.FileHandler(path)
file_formatter = logging.Formatter("%(asctime)s %(levelname)6s "
"%(filename)s: %(lineno)s - "
"%(message)s")
file_handler.setFormatter(file_formatter)
file_handler.setLevel(LEVEL[level])
log.addHandler(file_handler)
return log

View File

@@ -1,31 +0,0 @@
"""
Project: pyGTKtalog
Description: Model(s) for details part of the application
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-09
"""
import gtk
import gobject
from gtkmvc import Model
class DetailsModel(Model):
"""
Main model for application.
It is responsible for communicate with database objects and I/O
operations.
"""
exif = gtk.ListStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_UINT64,
gobject.TYPE_STRING,
gobject.TYPE_INT,
str)
__observables__ = ['exif']

View File

@@ -1,107 +0,0 @@
"""
Project: pyGTKtalog
Description: Model for discs representation
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import gtk
import gobject
from gtkmvc import Model
from pygtktalog.dbobjects import File
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
LOG = get_logger("discs model")
class DiscsModel(Model):
"""
Model for discs representation.
"""
currentdir = None
__observables__ = ("currentdir",)
def __init__(self):
"""
Initialization. Make some nice defaults.
"""
Model.__init__(self)
self.discs = gtk.TreeStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
str)
self.files_model = None
def clear(self):
"""
Make TreeStore empty
"""
self.discs.clear()
def refresh(self, session=Session()):
"""
Read objects from database, fill TreeStore model with discs
information
Arguments:
@session current sqlalchemy.orm.session.Session object
"""
LOG.debug("session obj: %s" % str(session))
dirs = 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 find_path(self, obj):
"""
Return path of specified File object (which should be the first one)
"""
path = None
gtkiter = self.discs.get_iter_first()
def get_children(iterator):
"""
Iterate through entire TreeModel, and return path for specified in
outter scope File object
"""
if self.discs.get_value(iterator, 0) == obj:
return self.discs.get_path(iterator)
if self.discs.iter_has_child(iterator):
path = get_children(self.discs.iter_children(iterator))
if path:
return path
iterator = self.discs.iter_next(iterator)
if iterator is None:
return None
return get_children(iterator)
path = get_children(gtkiter)
LOG.debug("found path for object '%s': %s" % (str(obj), str(path)))
return path

View File

@@ -1,79 +0,0 @@
"""
Project: pyGTKtalog
Description: Model for files representation
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-12
"""
import gtk
import gobject
from gtkmvc import Model
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
LOG = get_logger("files model")
class FilesModel(Model):
"""
Model for files representation
"""
def __init__(self):
"""
Initialization. Make some nice defaults.
"""
Model.__init__(self)
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)
self.discs_model = None
def clear(self):
"""
Cleanup ListStore model
"""
self.files.clear()
def refresh(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)
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 get_value(self, row=None, fiter=None, column=0):
"""
TODO:
"""
if row:
fiter = self.files.get_iter(row)
if not fiter:
LOG.error("ERROR: there is no way to determine gtk_iter object!"
" Please specify valid row or gtk_iter!")
return None
return self.files.get_value(fiter, column)

View File

@@ -1,289 +0,0 @@
"""
Project: pyGTKtalog
Description: Model for main application
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import os
import bz2
from string import printable
from tempfile import mkstemp
import gtk
import gobject
from gtkmvc import ModelMT
from sqlalchemy import create_engine
from pygtktalog.dbobjects import File, Exif, Group, Gthumb
from pygtktalog.dbobjects import Image, Tag, Thumbnail
from pygtktalog.dbcommon import connect, Meta, Session
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")
class MainModel(ModelMT):
"""
Main model for application.
It is responsible for communicate with database objects and I/O
operations.
"""
status_bar_message = _("Idle")
current_disc = None
__observables__ = ("status_bar_message", "current_disc")
def __init__(self, filename=None):
"""
Initialization. Make some nice defaults.
Arguments:
filename - String that indicates optionally compressed with bzip2
file containing sqlite3 database.
"""
ModelMT.__init__(self)
# Opened/saved database location in filesystem. Could be compressed.
self.cat_fname = filename
# Temporary (usually in /tmp) working database.
self.tmp_filename = None
self.config = {}
# SQLAlchemy session object for internal use
self._session = None
# Flag indicates, that db was compressed
# TODO: make it depend on configuration
self.compressed = False
self.db_unsaved = None
self.discs = DiscsModel()
self.files = FilesModel()
if self.cat_fname:
self.open(self.cat_fname)
def open(self, filename):
"""
Open catalog file and read db
Arguments:
@filename - see MainModel __init__ docstring.
Returns: Bool - true for success, false otherwise.
"""
LOG.debug("filename: '%s'", filename)
self.unsaved_project = False
if not os.path.exists(filename):
LOG.warn("db file '%s' doesn't exist.", filename)
return False
if not os.path.isfile(filename):
LOG.warn("db file '%s' is not a regular file.", filename)
return False
self.cat_fname = filename
if self._open_or_decompress():
return self.discs.refresh(self._session)
else:
return False
def save(self, filename=None):
"""
Save tared directory at given catalog fielname
Arguments:
@filename - see MainModel __init__ docstring.
Returns: tuple:
Bool - true for success, false otherwise.
String or None - error message
"""
if not filename and not self.cat_fname:
LOG.debug("no filename detected!")
return False, None
if filename:
if not '.sqlite' in filename:
filename += '.sqlite'
else:
filename = filename[:filename.rindex('.sqlite')] + '.sqlite'
if 'compress' in self.config and self.config['compress']:
filename += '.bz2'
self.cat_fname = filename
val, err = self._compress_and_save()
if not val:
self.cat_fname = None
return val, err
def new(self):
"""
Create new catalog
"""
self.cleanup()
self._create_temp_db_file()
self._create_schema()
self.discs.clear()
self.files.clear()
self.db_unsaved = False
def cleanup(self):
"""
Remove temporary directory tree from filesystem
"""
if self._session:
self._session.close()
self._session = None
if self.tmp_filename is None:
return
try:
os.unlink(self.tmp_filename)
except OSError:
LOG.error("temporary db file doesn't exists!")
except TypeError:
# TODO: file does not exist - create? print error message?
LOG.error("temporary db file doesn't exists!")
else:
LOG.debug("file %s succesfully deleted", self.tmp_filename)
def _examine_file(self, filename):
"""
Try to recognize file.
Arguments:
@filename - String with full path to file to examine
Returns: 'sql', 'bz2' or None for file sqlite, compressed with bzip2
or other respectively
"""
try:
head_of_file = open(filename).read(15)
LOG.debug("head of file: %s", filter(lambda x: x in printable,
head_of_file))
except IOError:
LOG.exception("Error opening file '%s'!", filename)
self.cleanup()
self.cat_fname = None
self.tmp_filename = None
return None
if head_of_file == "SQLite format 3":
LOG.debug("File format: uncompressed sqlite")
return 'sql'
elif head_of_file[0:10] == "BZh91AY&SY":
LOG.debug("File format: bzip2")
return 'bz2'
else:
return None
def _open_or_decompress(self):
"""
Try to open file, which user thinks is our catalog file.
Returns: Bool - true for success, false otherwise
"""
filename = os.path.abspath(self.cat_fname)
LOG.info("catalog file: %s", filename)
if self._session:
self.cleanup()
self._create_temp_db_file()
LOG.debug("tmp database file: %s", str(self.tmp_filename))
examine = self._examine_file(filename)
if examine == "sql":
db_tmp = open(self.tmp_filename, "wb")
db_tmp.write(open(filename).read())
db_tmp.close()
elif examine == "bz2":
open_file = bz2.BZ2File(filename)
try:
db_tmp = open(self.tmp_filename, "w")
db_tmp.write(open_file.read())
db_tmp.close()
open_file.close()
except IOError:
self.cleanup()
self.cat_fname = None
self.internal_dirname = None
LOG.exception("File is probably not a bz2!")
return False
if self._examine_file(self.tmp_filename) is not 'sql':
LOG.error("Error opening file '%s' - not a catalog file!",
self.tmp_filename)
self.cleanup()
self.cat_fname = None
self.tmp_filename = None
return False
else:
LOG.error("Error opening file '%s' - not a catalog file!",
self.tmp_filename)
self.cleanup()
self.cat_fname = None
self.internal_dirname = None
return False
connect(os.path.abspath(self.tmp_filename))
self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
return True
def _create_temp_db_file(self):
"""
Create new DB file, populate schema.
"""
fd, self.tmp_filename = mkstemp()
LOG.debug("new db filename: %s" % self.tmp_filename)
# close file descriptor, otherwise it can be source of app crash!
# http://www.logilab.org/blogentry/17873
os.close(fd)
def _create_schema(self):
"""
"""
self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
connect(os.path.abspath(self.tmp_filename))
root = File()
root.id = 1
root.filename = 'root'
root.size = 0
root.source = 0
root.type = 0
root.parent_id = 1
self._session.add(root)
self._session.commit()
def _compress_and_save(self):
"""
Create (and optionaly compress) tar archive from working directory and
write it to specified file.
"""
# flush all changes
self._session.commit()
try:
if 'compress' in self.config and self.config['compress']:
output_file = bz2.BZ2File(self.cat_fname, "w")
else:
output_file = open(self.cat_fname, "w")
LOG.debug("save (and optionally compress) successed")
except IOError, (errno, strerror):
LOG.error("error saving or compressing file", errno, strerror)
return False, strerror
dbpath = open(self.tmp_filename)
output_file.write(dbpath.read())
dbpath.close()
output_file.close()
self.db_unsaved = False
return True, None

View File

@@ -1,25 +0,0 @@
"""
Project: pyGTKtalog
Description: pyGTK common utility functions
Type: tility
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-07 13:30:37
"""
def get_tv_item_under_cursor(treeview):
"""
Get item (most probably id of the row) form tree view under cursor.
Arguments:
@treeview - gtk.TreeView
Returns:
Item in first column of TreeModel, which TreeView is connected with,
None in other cases
"""
path, column = treeview.get_cursor()
if path and column:
model = treeview.get_model()
tm_iter = model.get_iter(path)
item_id = model.get_value(tm_iter, 0)
return item_id
return None

View File

@@ -1,665 +0,0 @@
"""
Project: pyGTKtalog
Description: Filesystem scan and file automation layer
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-03-27
"""
import os
import sys
import re
from datetime import datetime
import mimetypes
from pygtktalog.dbobjects import File, Image, TYPE
from pygtktalog.thumbnail import ThumbCreator
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
from pygtktalog.video import Video
LOG = get_logger(__name__)
PAT = re.compile("(\[[^\]]*\]"
".*\(\d\d\d\d\))"
"\s[^\[]*\[.{8}\]"
".[a-zA-Z0-9]*$")
#PAT = re.compile(r'(?P<group>\[[^\]]*\]\s)?'
# r'(?P<title>.*)\s'
# r'(?P<year>\(\d{4}\))\s'
# r'(?P<kind>.*)'
# r'(?P<checksum>\[[A-Z0-9]{8}\])'
# r'\.(?P<extension>(avi|asf|mpeg|mpg|mp4|ogm|ogv|mkv|mov|wmv'
# r'|rm|rmvb|flv|jpg|png|gif|nfo))\.?(conf)?$')
class NoAccessError(Exception):
pass
class Scan(object):
"""
Retrieve and identify all files recursively on given path
"""
def __init__(self, path):
"""
Initialize
@Arguments:
@path - string with initial root directory to scan
"""
self.abort = False
self.path = path.rstrip(os.path.sep)
self._files = []
self._existing_files = [] # for re-use purpose in adding
self._existing_branch = [] # for branch storage, mainly for updating
self._session = Session()
self.files_count = self._get_files_count()
self.current_count = 0
def add_files(self):
"""
Returns list, which contain object, modification date and file
size.
"""
self._files = []
self._existing_branch = []
LOG.debug("given path: %s" % self.path)
# See, if file exists. If not it would raise OSError exception
os.stat(self.path)
if not os.access(self.path, os.R_OK | os.X_OK) \
or not os.path.isdir(self.path):
raise NoAccessError("Access to %s is forbidden" % self.path)
directory = os.path.basename(self.path)
path = os.path.dirname(self.path)
if not self._recursive(None, directory, path, 0):
return None
# add only first item from _files, because it is a root of the other,
# so other will be automatically added aswell.
self._session.add(self._files[0])
self._session.commit()
return self._files
def update_files(self, node_id):
"""
Updtate DB contents of provided node.
"""
self.current_count = 0
old_node = self._session.query(File).get(node_id)
if old_node is None:
LOG.warning("No such object in db: %s", node_id)
return
parent = old_node.parent
self._files = []
self._existing_branch = old_node.get_all_children()
self._existing_branch.insert(0, old_node)
# Break the chain of parent-children relations
for fobj in self._existing_branch:
fobj.parent = None
update_path = os.path.join(old_node.filepath, old_node.filename)
# refresh objects
self._get_all_files()
LOG.debug("path for update: %s" % update_path)
# See, if file exists. If not it would raise OSError exception
os.stat(update_path)
if not os.access(update_path, os.R_OK | os.X_OK) \
or not os.path.isdir(update_path):
raise NoAccessError("Access to %s is forbidden" % update_path)
directory = os.path.basename(update_path)
path = os.path.dirname(update_path)
if not self._recursive(parent, directory, path, 0):
return None
# update branch
#self._session.merge(self._files[0])
self._session.query(File).filter(File.parent==None).delete()
self._session.commit()
return self._files
def _get_dirsize(self, path):
"""
Returns sum of all files under specified path (also in subdirs)
"""
size = 0
for root, dirs, files in os.walk(path):
for fname in files:
try:
size += os.stat(os.path.join(root, fname)).st_size
except OSError:
LOG.warning("Cannot access file "
"%s" % os.path.join(root, fname))
return size
def _gather_information(self, fobj):
"""
Try to guess type and gather information about File object if possible
"""
mimedict = {'audio': self._audio,
'video': self._video,
'image': self._image}
fp = os.path.join(fobj.filepath.encode(sys.getfilesystemencoding()),
fobj.filename.encode(sys.getfilesystemencoding()))
mimeinfo = mimetypes.guess_type(fp)
if mimeinfo[0] and mimeinfo[0].split("/")[0] in mimedict.keys():
mimedict[mimeinfo[0].split("/")[0]](fobj, fp)
else:
LOG.debug("Filetype not supported " + str(mimeinfo) + " " + fp)
pass
def _audio(self, fobj, filepath):
return
def _image(self, fobj, filepath):
#Thumbnail(filepath, fobj)
return
def _video(self, fobj, filepath):
"""
Make captures for a movie. Save it under uniq name.
"""
result = PAT.search(fobj.filename)
if result:
self._check_related(fobj, result.groups()[0])
vid = Video(filepath)
fobj.description = vid.get_formatted_tags()
preview_fn = vid.capture()
if preview_fn:
Image(preview_fn, fobj)
def _check_related(self, fobj, pattern):
"""
Try to search for related files which belongs to specified File
object and pattern. If found, additional objects are created.
"""
for filen in os.listdir(fobj.filepath):
if pattern in filen and \
os.path.splitext(filen)[1] in (".jpg", ".png", ".gif"):
full_fname = os.path.join(fobj.filepath, filen)
LOG.debug('found cover file: %s' % full_fname)
Image(full_fname, fobj)
if not fobj.thumbnail:
fthumb = ThumbCreator(full_fname).generate()
fobj.thumbnail = fthumb.getvalue()
fthumb.close()
def _name_matcher(self, fpath, fname, media=False):
"""
Try to match special pattern to filename which may be looks like this:
[aXXo] Batman (1989) [D3ADBEEF].avi
[aXXo] Batman (1989) [D3ADBEEF].avi.conf
[aXXo] Batman (1989) cover [BEEFD00D].jpg
[aXXo] Batman (1989) cover2 [FEEDD00D].jpg
[aXXo] Batman (1989) trailer [B00B1337].avi
or
Batman (1989) [D3ADBEEF].avi (and so on)
For media=False it will return True for filename, that matches
pattern, and there are at least one corresponding media files (avi,
mpg, mov and so on) _in case the filename differs from media_. This is
usfull for not storing covers, nfo, conf files in the db.
For kind == 2 it will return all images and other files that should be
gather due to video file examinig as a dict of list (conf, nfo and
images).
"""
# TODO: dokonczyc to na podstawie tego cudowanego patternu u gory.
return
def _get_all_files(self):
self._existing_files = self._session.query(File).all()
def _mk_file(self, fname, path, parent, ftype=TYPE['file']):
"""
Create and return File object
"""
fullpath = os.path.join(path, fname)
fname = fname.decode(sys.getfilesystemencoding())
path = path.decode(sys.getfilesystemencoding())
if ftype == TYPE['link']:
fname = fname + " -> " + os.readlink(fullpath)
fob = {'filename': fname,
'path': path,
'ftype': ftype}
try:
fob['date'] = datetime.fromtimestamp(os.stat(fullpath).st_mtime)
fob['size'] = os.stat(fullpath).st_size
except OSError:
# in case of dead softlink, we will have no time and size
fob['date'] = None
fob['size'] = 0
fobj = self._get_old_file(fob, ftype)
if fobj:
LOG.debug("found existing file in db: %s" % str(fobj))
fobj.size = fob['size'] # TODO: update whole tree sizes (for directories/discs)
fobj.filepath = fob['path']
fobj.type = fob['ftype']
else:
fobj = File(**fob)
if parent is None:
fobj.parent_id = 1
else:
fobj.parent = parent
self._files.append(fobj)
return fobj
def _recursive(self, parent, fname, path, size):
"""
Do the walk through the file system
@Arguments:
@parent - directory File object which is parent for the current
scope
@fname - string that hold filename
@path - full path for further scanning
@size - size of the object
"""
if self.abort:
return False
fullpath = os.path.join(path, fname)
parent = self._mk_file(fname, path, parent, TYPE['dir'])
parent.size = self._get_dirsize(fullpath)
parent.type = TYPE['dir']
root, dirs, files = os.walk(fullpath).next()
for fname in files:
fpath = os.path.join(root, fname)
self.current_count += 1
LOG.debug("Processing %s [%s/%s]", fname, self.current_count,
self.files_count)
result = PAT.search(fname)
test_ = False
if result and os.path.splitext(fpath)[1] in ('.jpg', '.gif',
'.png'):
newpat = result.groups()[0]
matching_files = []
for fn_ in os.listdir(root):
if newpat in fn_:
matching_files.append(fn_)
if len(matching_files) > 1:
LOG.debug('found cover "%s" in group: %s, skipping', fname,
str(matching_files))
test_ = True
if test_:
continue
if os.path.islink(fpath):
fob = self._mk_file(fname, root, parent, TYPE['link'])
else:
fob = self._mk_file(fname, root, parent)
existing_obj = self._object_exists(fob)
if existing_obj:
fob.tags = existing_obj.tags
fob.thumbnail = existing_obj.thumbnail
fob.images = [img.get_copy() \
for img in existing_obj.images] # TODO: many-to-many?
else:
LOG.debug("gather information for %s",
os.path.join(root, fname))
self._gather_information(fob)
size += fob.size
if fob not in self._existing_files:
self._existing_files.append(fob)
for dirname in dirs:
dirpath = os.path.join(root, dirname)
if not os.access(dirpath, os.R_OK|os.X_OK):
LOG.info("Cannot access directory %s" % dirpath)
continue
if os.path.islink(dirpath):
fob = self._mk_file(dirname, root, parent, TYPE['link'])
else:
LOG.debug("going into %s" % os.path.join(root, dirname))
self._recursive(parent, dirname, fullpath, size)
LOG.debug("size of items: %s" % parent.size)
return True
def _get_old_file(self, fdict, ftype):
"""
Search for object with provided data in dictionary in stored branch
(which is updating). Return such object on success, remove it from
list.
"""
for index, obj in enumerate(self._existing_branch):
if ftype == TYPE['link'] and fdict['filename'] == obj.filename:
return self._existing_branch.pop(index)
elif fdict['filename'] == obj.filename and \
fdict['date'] == obj.date and \
ftype == TYPE['file'] and \
fdict['size'] in (obj.size, 0):
obj = self._existing_branch.pop(index)
obj.size = fdict['size']
return obj
elif fdict['filename'] == obj.filename:
obj = self._existing_branch.pop(index)
obj.size = fdict['date']
return obj
return False
def _object_exists(self, fobj):
"""
Perform check if current File object already exists in collection. If
so, return first matching one, None otherwise.
"""
for efobj in self._existing_files:
if efobj.size == fobj.size \
and efobj.type == fobj.type \
and efobj.date == fobj.date \
and efobj.filename == fobj.filename:
return efobj
return None
def _get_files_count(self):
count = 0
for root, dirs, files in os.walk(self.path):
count += len(files)
LOG.debug("count of files: %s", count)
return count
class asdScan(object):
"""
Retrieve and identify all files recursively on given path
"""
def __init__(self, path, tree_model):
self.path = path
self.abort = False
self.label = None
self.DIR = None
self.source = None
def scan(self):
"""
scan content of the given path
"""
self.busy = True
# count files in directory tree
LOG.debug("Calculating number of files in directory tree...")
step = 0
try:
for root, dirs, files in os.walk(self.path):
step += len(files)
except Exception, ex:
LOG.warning("exception on file %s: %s: %s" \
% (self.path, ex.__class__.__name__, str(ex)))
pass
step = 1 / float(step or 1)
self.count = 0
def _recurse(parent_id, name, path, date, size, filetype,
discs_tree_iter=None):
"""recursive scans given path"""
if self.abort:
return -1
_size = size
if parent_id == 1:
sql = """INSERT INTO
files(parent_id, filename, filepath, date,
size, type, source)
VALUES(?,?,?,?,?,?,?)"""
print(sql, (parent_id, name, path, date, size,
filetype, self.source))
else:
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (parent_id, name, path,
date, size, filetype))
sql = """SELECT seq FROM sqlite_sequence WHERE name='files'"""
print(sql)
currentid = None #db_cursor.fetchone()[0]
try:
root, dirs, files = os.walk(path).next()
except:
LOG.warning("Cannot access ", path)
return 0
#############
# directories
for i in dirs:
j = i #j = self.__decode_filename(i)
current_dir = os.path.join(root, i)
try:
st = os.stat(current_dir)
st_mtime = st.st_mtime
except OSError:
st_mtime = 0
# do NOT follow symbolic links
if os.path.islink(current_dir):
l = self.__decode_filename(os.readlink(current_dir))
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j + " -> " + l,
current_dir, st_mtime, 0,
self.LIN))
dirsize = 0
else:
myit = None
dirsize = _recurse(currentid, j, current_dir,
st_mtime, 0, self.DIR, myit)
if dirsize == -1:
break
else:
_size = _size + dirsize
########
# files:
for i in files:
if self.abort:
break
self.count = self.count + 1
current_file = os.path.join(root, i)
try:
st = os.stat(current_file)
st_mtime = st.st_mtime
st_size = st.st_size
except OSError:
st_mtime = 0
st_size = 0
_size = _size + st_size
j = i #self.__decode_filename(i)
# do NOT follow symbolic links
if os.path.islink(current_file):
l = self.__decode_filename(os.readlink(current_file))
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j + " -> " + l,
current_file, st_mtime, 0,
self.LIN))
else:
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j, current_file,
st_mtime, st_size, self.FIL))
if self.count % 32 == 0:
update = True
else:
update = False
###########################
# fetch details about files
if self.config.confd['retrive']:
update = True
exif = None
sql = """SELECT seq FROM sqlite_sequence
WHERE name='files'"""
print(sql)
fileid = 1 # dummy!
ext = i.split('.')[-1].lower()
# Video
if ext in self.MOV:
v = Video(current_file)
cfn = v.capture()
img = Img(cfn, self.image_path)
th = img.save()
if th:
sql = """INSERT INTO
thumbnails(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th + "_t"))
sql = """INSERT INTO images(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th))
os.unlink(cfn)
# Images - thumbnails and exif data
if self.config.confd['thumbs'] and ext in self.IMG:
thumb = Thumbnail(current_file, self.image_path)
th, exif = thumb.save()
if th:
sql = """INSERT INTO
thumbnails(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th))
# exif - store data in exif table
jpg = ['jpg', 'jpeg']
if self.config.confd['exif'] and ext in jpg:
p = None
if self.config.confd['thumbs'] and exif:
p = ParseExif(exif_dict=exif)
else:
p = ParseExif(exif_file=current_file)
if not p.exif_dict:
p = None
if p:
p = p.parse()
p = list(p)
p.insert(0, fileid)
sql = """INSERT INTO exif (file_id,
camera,
date,
aperture,
exposure_program,
exposure_bias,
iso,
focal_length,
subject_distance,
metering_mode,
flash,
light_source,
resolution,
orientation)
values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"""
print(sql, (tuple(p)))
# gthumb - save comments from gThumb program
if self.config.confd['gthumb']:
gt = GthumbCommentParser(root, i)
cmnts = gt.parse()
if cmnts:
sql = """insert into gthumb(file_id,
note,
place,
date)
values(?,?,?,?)"""
print(sql, (fileid,
cmnts['note'],
cmnts['place'],
cmnts['date']))
if 'keywords' in cmnts:
# TODO: add gthumb keywords to tags
pass
# Extensions - user defined actions
if ext in self.config.confd['extensions'].keys():
cmd = self.config.confd['extensions'][ext]
arg = current_file.replace('"', '\\"')
output = os.popen(cmd % arg).readlines()
desc = ''
for line in output:
desc += line
sql = """UPDATE files SET description=?
WHERE id=?"""
print(sql, (desc, fileid))
### end of scan
if update:
self.statusmsg = "Scannig: %s" % current_file
self.progress = step * self.count
sql = """UPDATE files SET size=? WHERE id=?"""
print(sql, (_size, currentid))
if self.abort:
return -1
else:
return _size
if _recurse(1, self.label, self.path, 0, 0, self.DIR) == -1:
LOG.debug("interrupted self.abort = True")
else:
LOG.debug("recursive goes without interrupt")
if self.currentid:
LOG.debug("removing old branch")
self.statusmsg = "Removing old branch..."
self.currentid = None
self.busy = False
# refresh discs tree
self.statusmsg = "Idle"
self.progress = 0
self.abort = False

View File

@@ -1,106 +0,0 @@
"""
Project: pyGTKtalog
Description: Create thumbnail for sepcified image by its filename
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-05-15
"""
import os
import sys
from cStringIO import StringIO
from tempfile import mkstemp
import Image
from pygtktalog.logger import get_logger
from pygtktalog import EXIF
LOG = get_logger(__name__)
class ThumbCreator(object):
"""
Class for generate/extract thumbnail from image file
"""
def __init__(self, filename):
self.thumb_x = 160
self.thumb_y = 160
self.filename = filename.decode(sys.getfilesystemencoding())
self.fobj = StringIO()
def generate(self):
"""
Generate and return file-like object with thumbnail
"""
exif = {}
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
3: Image.ROTATE_180, # Rotated 180
4: Image.FLIP_TOP_BOTTOM, # Mirrored vertical
5: Image.ROTATE_90, # Mirrored horizontal then
# rotated 90 CCW
6: Image.ROTATE_270, # Rotated 90 CW
7: Image.ROTATE_270, # Mirrored horizontal then
# rotated 90 CW
8: Image.ROTATE_90} # Rotated 90 CCW
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
exif = self._get_exif()
file_desc, thumb_fn = mkstemp(suffix=".jpg")
os.close(file_desc)
if 'JPEGThumbnail' not in exif:
LOG.debug("no exif thumb for file %s; creating." % self.filename)
thumb = self._scale_image()
if thumb:
thumb.save(self.fobj, "JPEG")
else:
LOG.debug("exif thumb for filename %s" % self.filename)
exif_thumbnail = exif['JPEGThumbnail']
self.fobj.write(exif_thumbnail)
self.fobj.seek(0)
if 'Image Orientation' in exif:
orient = exif['Image Orientation'].values[0]
if orient > 1 and orient in orientations:
thumb_image = Image.open(self.fobj)
tmp_thumb_img = thumb_image.transpose(orientations[orient])
if orient in flips:
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
self.fobj.seek(0)
self.fobj.truncate()
tmp_thumb_img.save(self.fobj, 'JPEG')
return self.fobj
def _get_exif(self):
"""
Get exif (if available), return as a dict
"""
image_file = open(self.filename, 'rb')
try:
exif = EXIF.process_file(image_file)
except Exception:
exif = {}
LOG.info("Exif crashed on '%s'." % self.filename)
finally:
image_file.close()
return exif
def _scale_image(self):
"""
Create thumbnail. returns image object or None
"""
try:
image_thumb = Image.open(self.filename).convert('RGB')
except:
return None
it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y:
image_thumb.thumbnail((self.thumb_x, self.thumb_y),
Image.ANTIALIAS)
return image_thumb

View File

@@ -1,281 +0,0 @@
"""
Project: pyGTKtalog
Description: Gather video file information, make "screenshot" with content
of the movie file. Uses external tools like mplayer.
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-04
"""
import os
import shutil
from tempfile import mkdtemp, mkstemp
import math
import Image
from pygtktalog.misc import float_to_string
from pygtktalog.logger import get_logger
LOG = get_logger("Video")
class Video(object):
"""Class for retrive midentify script output and put it in dict.
Usually there is no need for such a detailed movie/clip information.
Midentify script belongs to mplayer package.
"""
def __init__(self, filename, out_width=1024):
"""
Init class instance.
Arguments:
@filename - Filename of a video file (required).
@out_width - width of final image to be scaled to.
"""
self.filename = filename
self.out_width = out_width
self.tags = {}
output = self._get_movie_info()
attrs = {'ID_VIDEO_WIDTH': ['width', int],
'ID_VIDEO_HEIGHT': ['height', int],
# length is in seconds
'ID_LENGTH': ['length', lambda x: int(x.split(".")[0])],
'ID_START_TIME': ['start', self._get_start_pos],
'ID_DEMUXER': ['container', self._return_lower],
'ID_VIDEO_FORMAT': ['video_format', self._return_lower],
'ID_VIDEO_CODEC': ['video_codec', self._return_lower],
'ID_AUDIO_CODEC': ['audio_codec', self._return_lower],
'ID_AUDIO_FORMAT': ['audio_format', self._return_lower],
'ID_AUDIO_NCH': ['audio_no_channels', int]}
# TODO: what about audio/subtitle language/existence?
for key in output:
if key in attrs:
self.tags[attrs[key][0]] = attrs[key][1](output[key])
if 'length' in self.tags and self.tags['length'] > 0:
start = self.tags.get('start', 0)
length = self.tags['length'] - start
hours = length / 3600
seconds = length - hours * 3600
minutes = seconds / 60
seconds -= minutes * 60
length_str = "%02d:%02d:%02d" % (hours, minutes, seconds)
self.tags['duration'] = length_str
def capture(self):
"""
Extract images for given video filename and montage it into one, big
picture, similar to output from Windows Media Player thing, but without
captions and time (who need it anyway?).
Returns: image filename or None
NOTE: You should remove returned file manually, or move it in some
other place, otherwise it stays in filesystem.
"""
if not ('length' in self.tags and 'width' in self.tags):
# no length or width
return None
if not (self.tags['length'] > 0 and self.tags['width'] > 0):
# zero length or wight
return None
# Calculate number of pictures. Base is equivalent 72 pictures for
# 1:30:00 movie length
scale = int(10 * math.log(self.tags['length'], math.e) - 11)
if scale < 1:
return None
no_pictures = self.tags['length'] / scale
if no_pictures > 8:
no_pictures = (no_pictures / 8) * 8 # only multiple of 8, please.
else:
# for really short movies
no_pictures = 4
tempdir = mkdtemp()
file_desc, image_fn = mkstemp(suffix=".jpg")
os.close(file_desc)
self._make_captures(tempdir, no_pictures)
self._make_montage(tempdir, image_fn, no_pictures)
shutil.rmtree(tempdir)
return image_fn
def get_formatted_tags(self):
"""
Return formatted tags as a string
"""
out_tags = u''
if 'container' in self.tags:
out_tags += u"Container: %s\n" % self.tags['container']
if 'width' in self.tags and 'height' in self.tags:
out_tags += u"Resolution: %sx%s\n" % (self.tags['width'],
self.tags['height'])
if 'duration' in self.tags:
out_tags += u"Duration: %s\n" % self.tags['duration']
if 'video_codec' in self.tags:
out_tags += "Video codec: %s\n" % self.tags['video_codec']
if 'video_format' in self.tags:
out_tags += "Video format: %s\n" % self.tags['video_format']
if 'audio_codec' in self.tags:
out_tags += "Audio codec: %s\n" % self.tags['audio_codec']
if 'audio_format' in self.tags:
out_tags += "Audio format: %s\n" % self.tags['audio_format']
if 'audio_no_channels' in self.tags:
out_tags += "Audio channels: %s\n" % self.tags['audio_no_channels']
return out_tags
def _get_movie_info(self):
"""
Gather movie file information with midentify shell command.
Returns: dict of command output. Each dict element represents pairs:
variable=value, for example output from midentify will be:
ID_VIDEO_ID=0
ID_AUDIO_ID=1
....
ID_AUDIO_CODEC=mp3
ID_EXIT=EOF
so method returns dict:
{'ID_VIDEO_ID': '0',
'ID_AUDIO_ID': 1,
....
'ID_AUDIO_CODEC': 'mp3',
'ID_EXIT': 'EOF'}
"""
output = os.popen('midentify "%s"' % self.filename).readlines()
return_dict = {}
for line in output:
line = line.strip()
key = line.split('=')
if len(key) > 1:
return_dict[key[0]] = line.replace("%s=" % key[0], "")
return return_dict
def _make_captures(self, directory, no_pictures):
"""
Make screens with mplayer into given directory
Arguments:
@directory - full output directory name
@no_pictures - number of pictures to take
"""
step = float(self.tags['length'] / (no_pictures + 1))
current_time = 0
for dummy in range(1, no_pictures + 1):
current_time += step
time = float_to_string(current_time)
cmd = "mplayer \"%s\" -ao null -brightness 0 -hue 0 " \
"-saturation 0 -contrast 0 -vf-clr -vo jpeg:outdir=\"%s\" -ss %s" \
" -frames 1 2>/dev/null"
os.popen(cmd % (self.filename, directory, time)).readlines()
try:
shutil.move(os.path.join(directory, "00000001.jpg"),
os.path.join(directory, "picture_%s.jpg" % time))
except IOError, (errno, strerror):
LOG.error('error capturing file from movie "%s" at position '
'%s. Errors: %s, %s', self.filename, time, errno,
strerror)
def _make_montage(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
"""
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 _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 _get_start_pos(self, chain):
"""
Return integer for starting point of the movie
"""
try:
return int(chain.split(".")[0])
except:
return 0
def __str__(self):
str_out = ''
for key in self.tags:
str_out += "%20s: %s\n" % (key, self.tags[key])
return str_out

View File

@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Sat Aug 29 15:13:44 2009 -->
<glade-interface>
<widget class="GtkWindow" id="top_details">
<child>
<widget class="GtkNotebook" id="notebook_details">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkHBox" id="fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow4">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTextView" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="left_margin">2</property>
<property name="right_margin">2</property>
<property name="cursor_visible">False</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkViewport" id="thumb_box">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<child>
<widget class="GtkImage" id="thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xpad">3</property>
<property name="ypad">3</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">6</property>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="nb_fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">File info</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="img_container">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkIconView" id="images">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Double click to open image</property>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Images</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="exifinfo">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTreeView" id="exif_tree">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="rules_hint">True</property>
</widget>
</child>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">EXIF</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,76 +0,0 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_discs">
<child>
<widget class="GtkTreeView" id="discs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_discs_button_press_event"/>
<signal name="cursor_changed" handler="on_discs_cursor_changed"/>
<signal name="row_activated" handler="on_discs_row_activated"/>
<signal name="key_release_event" handler="on_discs_key_release_event"/>
</widget>
</child>
</widget>
<widget class="GtkMenu" id="discs_popup">
<child>
<widget class="GtkMenuItem" id="expand_all">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Expand all nodes</property>
<property name="label" translatable="yes">_Expand all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_expand_all_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="collapse_all">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Collapse all nodes</property>
<property name="label" translatable="yes">_Collapse all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_collapse_all_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="update">
<property name="visible">True</property>
<property name="label" translatable="yes">_Update</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_update_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename">
<property name="visible">True</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete">
<property name="visible">True</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="statistics">
<property name="visible">True</property>
<property name="label" translatable="yes">_Statistics</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_statistics_activate"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,122 +0,0 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_files">
<child>
<widget class="GtkTreeView" id="files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_files_button_press_event"/>
<signal name="cursor_changed" handler="on_files_cursor_changed"/>
<signal name="row_activated" handler="on_files_row_activated"/>
<signal name="drag_data_get" handler="on_files_drag_data_get"/>
<signal name="key_release_event" handler="on_files_key_release_event"/>
</widget>
</child>
</widget>
<widget class="GtkMenu" id="files_popup">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkMenuItem" id="add_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Add tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_tag1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Remo_ve tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_tag_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Add _Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Re_move Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator1">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_image">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="has_tooltip">True</property>
<property name="tooltip" translatable="yes">Add images to file. If file have no thumbnail,
thumbnail from first image will be generated.</property>
<property name="label" translatable="yes">Add _Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_image">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Rem_ove All Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator2">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="edit">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_edit2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete3_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename2_activate"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_tags">
<child>
<widget class="GtkTextView" id="tag_cloud_textview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="wrap_mode">word</property>
<property name="cursor_visible">False</property>
<signal name="visibility_notify_event" handler="on_tag_cloud_textview_visibility_notify_event"/>
<signal name="drag_motion" handler="on_tag_cloud_textview_drag_motion"/>
<signal name="event_after" handler="on_tag_cloud_textview_event_after"/>
<signal name="drag_drop" handler="on_tag_cloud_textview_drag_drop"/>
<signal name="drag_leave" handler="on_tag_cloud_textview_drag_leave"/>
<signal name="drag_data_received" handler="on_tag_cloud_textview_drag_data_received"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,210 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Fri Oct 23 18:28:53 2009 -->
<glade-interface>
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<widget class="GtkMenuBar" id="menubar1">
<property name="visible">True</property>
<child>
<widget class="GtkMenuItem" id="menuitem1">
<property name="visible">True</property>
<property name="label" translatable="yes">_File</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu1">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem1">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-new</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem2">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-open</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem3">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-save</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem4">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-save-as</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separatormenuitem1">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem5">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-quit</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem2">
<property name="visible">True</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu2">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem6">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-cut</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem7">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-copy</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem8">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-paste</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem9">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-delete</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem3">
<property name="visible">True</property>
<property name="label" translatable="yes">_View</property>
<property name="use_underline">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem4">
<property name="visible">True</property>
<property name="label" translatable="yes">_Help</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu3">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem10">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-about</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
</packing>
</child>
<child>
<widget class="GtkHPaned" id="hpaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<widget class="GtkViewport" id="viewport1">
<property name="visible">True</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<child>
<widget class="GtkTreeView" id="treeview1">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
</child>
</widget>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<widget class="GtkTextView" id="textview1">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<child>
<widget class="GtkStatusbar" id="statusbar1">
<property name="visible">True</property>
<property name="spacing">2</property>
<property name="has_resize_grip">False</property>
</widget>
</child>
<child>
<widget class="GtkProgressBar" id="progressbar1">
<property name="visible">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,189 +0,0 @@
"""
Project: pyGTKtalog
Description: View for main window
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import os.path
from gtkmvc import View
def get_glade(glade_filename):
"""
Return full path to specified glade file
"""
return os.path.join(os.path.dirname(__file__), "glade", glade_filename)
class MainView(View):
"""
Create window from glade file. Note, that glade file is placed in view
module under glade subdirectory.
"""
glade = get_glade("main.glade")
top = "main"
def __init__(self, top="main"):
"""
Initialize view
"""
View.__init__(self)
self.app_sensitive = None
self['tag_path_box'].hide()
self.discs = DiscsView()
#self['scrolledwindow_discs'].add_with_viewport(\
# self.discs.get_top_widget())
self['scrolledwindow_discs'].add(self.discs.get_top_widget())
self.files = FilesView()
self['scrolledwindow_files'].add_with_viewport(\
self.files.get_top_widget())
self.details = DetailsView()
self['vpaned1'].add2(self.details.get_top_widget())
def set_widgets_scan_sensitivity(self, sensitive=True):
"""
Activate/deactivate selected widgets while scanning is active
"""
pass
def set_widgets_app_sensitivity(self, sensitive=True):
"""
Enable/disable widgets for empty application. Usefull for first run
of an application (without any db filename as an argument).
"""
if self.app_sensitive is sensitive:
return
for widget in ['scrolledwindow_discs', 'scrolledwindow_files',
'tb_save', 'tb_addcd', 'tb_adddir', 'tb_find',
'edit1', 'catalog1', 'save1', 'save_as1', 'import',
'export']:
self[widget].set_sensitive(sensitive)
# widgets from subclasses
self.details['notebook_details'].set_sensitive(sensitive)
class DiscsView(View):
"""
Separate Discs TreeView subview.
"""
glade = get_glade("discs.glade")
top = 'discs'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
self.menu = DiscsPopupView()
class DiscsPopupView(View):
"""
Separate Discs PopUp subview.
"""
glade = get_glade("discs.glade")
top = 'discs_popup'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
def set_update_sensitivity(self, state):
"""
Set sensitivity for 'update' popup menu item
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
self['update'].set_sensitive(state)
def set_menu_items_sensitivity(self, state):
"""
Set sensitivity for couple of popup menu items, which should be
disabled if user right-clicks on no item in treeview.
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
for item in ['update', 'rename', 'delete', 'statistics']:
self[item].set_sensitive(state)
class FilesView(View):
"""
Separate subview of Files TreeView as a table.
"""
glade = get_glade("files.glade")
top = 'files'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
self.menu = FilesPopupView()
class FilesPopupView(View):
"""
Separate Files PopUp subview.
"""
glade = get_glade("files.glade")
top = 'files_popup'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
def set_menu_items_sensitivity(self, state):
"""
Set sensitivity for couple of popup menu items, which should be
disabled if user right-clicks on no item in treeview.
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
for item in ["add_tag", "delete_tag", "add_thumb", "remove_thumb",
"add_image", "remove_image", "edit", "delete", "rename"]:
self[item].set_sensitive(state)
class TagcloudView(View):
"""
Textview subview with clickable tags.
"""
glade = get_glade("tagcloud.glade")
top = 'tag_cloud_textview'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
class DetailsView(View):
"""
Notebook subview containing tabs with details and possibly Exif, images
assocated with object and alternatively thumbnail.
"""
glade = get_glade("details.glade")
top = 'notebook_details'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)

View File

@@ -1,36 +0,0 @@
"""
Project: pyGTKtalog
Description: Menu for the main window
Type: interface
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-03-14 21:31:57
"""
import gtk
from gtkmvc import View
class MainMenu(View):
def __init__(self):
View.__init__(self)
self['mainMenu'] = gtk.MenuBar()
self['file_menu'] = gtk.MenuItem(_("_File"))
accel_group = gtk.AccelGroup()
menu_items = (("/_File", None, None, 0, "<Branch>"),
("/File/_New", "<control>N", None, 0, None),
("/File/_Open", "<control>O", None, 0, None),
("/File/_Save", "<control>S", None, 0, None),
("/File/Save _As", None, None, 0, None),
("/File/sep1", None, None, 0, "<Separator>"),
("/File/Quit", "<control>Q", gtk.main_quit, 0, None),
("/_Options", None, None, 0, "<Branch>"),
("/Options/Test", None, None, 0, None),
("/_Help", None, None, 0, "<LastBranch>"),
("/_Help/About", None, None, 0, None),)
item_factory = gtk.ItemFactory(gtk.MenuBar, "<main>", accel_group)
item_factory.create_items(menu_items)

View File

@@ -1,18 +0,0 @@
"""
Project: pyGTKtalog
Description: Toolbar for the main window
Type: interface
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-04-20 18:47:49
"""
import gtk
from gtkmvc import View
class ToolBar(View):
def __init__(self):
View.__init__(self)
self['maintoolbar'] = gtk.Toolbar()

View File

@@ -1,7 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--*- mode: xml -*-->
<glade-interface> <glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="main"> <widget class="GtkWindow" id="main">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="title" translatable="yes">pyGTKtalog</property> <property name="title" translatable="yes">pyGTKtalog</property>
@@ -11,7 +11,6 @@
<widget class="GtkVBox" id="vbox1"> <widget class="GtkVBox" id="vbox1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child> <child>
<widget class="GtkMenuBar" id="mainMenubar"> <widget class="GtkMenuBar" id="mainMenubar">
<property name="visible">True</property> <property name="visible">True</property>
@@ -24,8 +23,8 @@
<widget class="GtkMenu" id="file1_menu"> <widget class="GtkMenu" id="file1_menu">
<child> <child>
<widget class="GtkImageMenuItem" id="new1"> <widget class="GtkImageMenuItem" id="new1">
<property name="label">gtk-new</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-new</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_new_activate"/> <signal name="activate" handler="on_new_activate"/>
@@ -33,8 +32,8 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="open1"> <widget class="GtkImageMenuItem" id="open1">
<property name="label">gtk-open</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-open</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_open_activate"/> <signal name="activate" handler="on_open_activate"/>
@@ -42,8 +41,8 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="save1"> <widget class="GtkImageMenuItem" id="save1">
<property name="label">gtk-save</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-save</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_save_activate"/> <signal name="activate" handler="on_save_activate"/>
@@ -51,8 +50,8 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="save_as1"> <widget class="GtkImageMenuItem" id="save_as1">
<property name="label">gtk-save-as</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-save-as</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_save_as_activate"/> <signal name="activate" handler="on_save_as_activate"/>
@@ -99,8 +98,8 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="quit1"> <widget class="GtkImageMenuItem" id="quit1">
<property name="label">gtk-quit</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-quit</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_quit_activate"/> <signal name="activate" handler="on_quit_activate"/>
@@ -120,12 +119,12 @@
<widget class="GtkMenu" id="edit1_menu"> <widget class="GtkMenu" id="edit1_menu">
<child> <child>
<widget class="GtkImageMenuItem" id="delete1"> <widget class="GtkImageMenuItem" id="delete1">
<property name="label">gtk-delete</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-delete</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_delete1_activate"/> <signal name="activate" handler="on_delete1_activate"/>
<accelerator key="Delete" signal="activate"/> <accelerator key="Delete" modifiers="" signal="activate"/>
</widget> </widget>
</child> </child>
<child> <child>
@@ -135,12 +134,12 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="find1"> <widget class="GtkImageMenuItem" id="find1">
<property name="label">gtk-find</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-find</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_find_activate"/> <signal name="activate" handler="on_find_activate"/>
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="f" modifiers="GDK_CONTROL_MASK" signal="activate"/>
</widget> </widget>
</child> </child>
<child> <child>
@@ -150,8 +149,8 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="properties1"> <widget class="GtkImageMenuItem" id="properties1">
<property name="label">gtk-preferences</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-preferences</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_preferences_activate"/> <signal name="activate" handler="on_preferences_activate"/>
@@ -174,7 +173,7 @@
<property name="label" translatable="yes">Add _CD/DVD</property> <property name="label" translatable="yes">Add _CD/DVD</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="activate" handler="on_add_cd_activate"/> <signal name="activate" handler="on_add_cd_activate"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="e" modifiers="GDK_CONTROL_MASK" signal="activate"/>
</widget> </widget>
</child> </child>
<child> <child>
@@ -183,7 +182,7 @@
<property name="label" translatable="yes">Add _Directory</property> <property name="label" translatable="yes">Add _Directory</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="activate" handler="on_add_directory_activate"/> <signal name="activate" handler="on_add_directory_activate"/>
<accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="d" modifiers="GDK_CONTROL_MASK" signal="activate"/>
</widget> </widget>
</child> </child>
<child> <child>
@@ -239,11 +238,17 @@
</child> </child>
<child> <child>
<widget class="GtkImageMenuItem" id="cancel1"> <widget class="GtkImageMenuItem" id="cancel1">
<property name="label">gtk-cancel</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label" translatable="yes">Cancel</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_cancel_clicked"/> <signal name="activate" handler="on_cancel_clicked"/>
<child internal-child="image">
<widget class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="stock">gtk-cancel</property>
<property name="icon_size">1</property>
</widget>
</child>
</widget> </widget>
</child> </child>
</widget> </widget>
@@ -273,6 +278,33 @@
<signal name="activate" handler="on_status_bar1_activate"/> <signal name="activate" handler="on_status_bar1_activate"/>
</widget> </widget>
</child> </child>
<child>
<widget class="GtkSeparatorMenuItem" id="separatormenuitem4">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkRadioMenuItem" id="list1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Show as a list</property>
<property name="label" translatable="yes">List</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_as_radio">True</property>
</widget>
</child>
<child>
<widget class="GtkRadioMenuItem" id="thumbnails1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Show as thumbnails</property>
<property name="label" translatable="yes">Thumbnails</property>
<property name="use_underline">True</property>
<property name="draw_as_radio">True</property>
<property name="group">list1</property>
</widget>
</child>
</widget> </widget>
</child> </child>
</widget> </widget>
@@ -286,8 +318,8 @@
<widget class="GtkMenu" id="help1_menu"> <widget class="GtkMenu" id="help1_menu">
<child> <child>
<widget class="GtkImageMenuItem" id="about1"> <widget class="GtkImageMenuItem" id="about1">
<property name="label">gtk-about</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="label">gtk-about</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="activate" handler="on_about1_activate"/> <signal name="activate" handler="on_about1_activate"/>
@@ -301,13 +333,12 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="position">0</property>
</packing> </packing>
</child> </child>
<child> <child>
<widget class="GtkToolbar" id="maintoolbar"> <widget class="GtkToolbar" id="maintoolbar">
<property name="visible">True</property> <property name="visible">True</property>
<property name="toolbar_style">both</property> <property name="toolbar_style">GTK_TOOLBAR_BOTH</property>
<child> <child>
<widget class="GtkToolButton" id="tb_new"> <widget class="GtkToolButton" id="tb_new">
<property name="visible">True</property> <property name="visible">True</property>
@@ -317,7 +348,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -329,7 +359,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -341,7 +370,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -355,6 +383,7 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">False</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -368,7 +397,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -381,7 +409,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -393,7 +420,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -407,6 +433,7 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">False</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -420,7 +447,6 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -432,7 +458,18 @@
</widget> </widget>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="homogeneous">True</property> </packing>
</child>
<child>
<widget class="GtkToolButton" id="debugbtn">
<property name="visible">True</property>
<property name="label" translatable="yes">Debug</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-dialog-info</property>
<signal name="clicked" handler="on_debugbtn_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
</packing> </packing>
</child> </child>
</widget> </widget>
@@ -453,24 +490,18 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property> <property name="editable">False</property>
<property name="shadow_type">etched-in</property> <property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
<property name="caps_lock_warning">False</property>
<property name="secondary_icon_stock">gtk-clear</property>
<property name="secondary_icon_activatable">True</property>
<property name="secondary_icon_sensitive">True</property>
</widget> </widget>
<packing>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<widget class="GtkButton" id="clear"> <widget class="GtkButton" id="clear">
<property name="label">gtk-clear</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">gtk-clear</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<property name="response_id">0</property>
<signal name="clicked" handler="on_clear_clicked"/> <signal name="clicked" handler="on_clear_clicked"/>
</widget> </widget>
<packing> <packing>
@@ -495,14 +526,23 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child> <child>
<widget class="GtkScrolledWindow" id="scrolledwindow_discs"> <widget class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">automatic</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">automatic</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<child> <child>
<placeholder/> <widget class="GtkTreeView" id="discs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_discs_button_press_event"/>
<signal name="row_activated" handler="on_discs_row_activated"/>
<signal name="cursor_changed" handler="on_discs_cursor_changed"/>
<signal name="key_release_event" handler="on_discs_key_release_event"/>
</widget>
</child> </child>
</widget> </widget>
</child> </child>
@@ -513,8 +553,8 @@
<property name="label" translatable="yes">Discs</property> <property name="label" translatable="yes">Discs</property>
</widget> </widget>
<packing> <packing>
<property name="tab_fill">False</property>
<property name="type">tab</property> <property name="type">tab</property>
<property name="tab_fill">False</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -522,10 +562,23 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">automatic</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">automatic</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<child> <child>
<placeholder/> <widget class="GtkTextView" id="tag_cloud_textview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="cursor_visible">False</property>
<signal name="visibility_notify_event" handler="on_tag_cloud_textview_visibility_notify_event"/>
<signal name="drag_leave" handler="on_tag_cloud_textview_drag_leave"/>
<signal name="event_after" handler="on_tag_cloud_textview_event_after"/>
<signal name="drag_motion" handler="on_tag_cloud_textview_drag_motion"/>
<signal name="drag_data_received" handler="on_tag_cloud_textview_drag_data_received"/>
<signal name="drag_drop" handler="on_tag_cloud_textview_drag_drop"/>
</widget>
</child> </child>
</widget> </widget>
<packing> <packing>
@@ -539,9 +592,9 @@
<property name="label" translatable="yes">Tags</property> <property name="label" translatable="yes">Tags</property>
</widget> </widget>
<packing> <packing>
<property name="type">tab</property>
<property name="position">1</property> <property name="position">1</property>
<property name="tab_fill">False</property> <property name="tab_fill">False</property>
<property name="type">tab</property>
</packing> </packing>
</child> </child>
</widget> </widget>
@@ -554,16 +607,24 @@
<widget class="GtkVPaned" id="vpaned1"> <widget class="GtkVPaned" id="vpaned1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="orientation">vertical</property>
<child> <child>
<widget class="GtkScrolledWindow" id="scrolledwindow_files"> <widget class="GtkScrolledWindow" id="scrolledwindow2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">automatic</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">in</property> <property name="shadow_type">GTK_SHADOW_IN</property>
<child> <child>
<placeholder/> <widget class="GtkTreeView" id="files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<signal name="drag_data_get" handler="on_files_drag_data_get"/>
<signal name="button_press_event" handler="on_files_button_press_event"/>
<signal name="row_activated" handler="on_files_row_activated"/>
<signal name="cursor_changed" handler="on_files_cursor_changed"/>
<signal name="key_release_event" handler="on_files_key_release_event"/>
</widget>
</child> </child>
</widget> </widget>
<packing> <packing>
@@ -572,7 +633,144 @@
</packing> </packing>
</child> </child>
<child> <child>
<placeholder/> <widget class="GtkNotebook" id="notebook_details">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkHBox" id="fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow4">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTextView" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="left_margin">2</property>
<property name="right_margin">2</property>
<property name="cursor_visible">False</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkViewport" id="thumb_box">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<signal name="button_press_event" handler="on_thumb_box_button_press_event"/>
<child>
<widget class="GtkImage" id="thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xpad">3</property>
<property name="ypad">3</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">6</property>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="nb_fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">File info</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="img_container">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkIconView" id="images">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Double click to open image</property>
<signal name="button_press_event" handler="on_images_button_press_event"/>
<signal name="item_activated" handler="on_images_item_activated"/>
<signal name="key_release_event" handler="on_images_key_release_event"/>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Images</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="exifinfo">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTreeView" id="exif_tree">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="headers_clickable">True</property>
<property name="rules_hint">True</property>
</widget>
</child>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">EXIF</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child> </child>
</widget> </widget>
<packing> <packing>
@@ -603,9 +801,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="has_resize_grip">False</property> <property name="has_resize_grip">False</property>
</widget> </widget>
<packing>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<widget class="GtkProgressBar" id="progressbar1"> <widget class="GtkProgressBar" id="progressbar1">
@@ -627,6 +822,63 @@
</widget> </widget>
</child> </child>
</widget> </widget>
<widget class="GtkMenu" id="discs_popup">
<child>
<widget class="GtkMenuItem" id="expand_all1">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Expand all nodes</property>
<property name="label" translatable="yes">_Expand all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_expand_all1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="collapse_all1">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Collapse all nodes</property>
<property name="label" translatable="yes">_Collapse all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_collapse_all1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator4">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="update1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Update</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_update1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete2">
<property name="visible">True</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="statistics1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Statistics</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_statistics1_activate"/>
</widget>
</child>
</widget>
<widget class="GtkWindow" id="win_exifinfo"> <widget class="GtkWindow" id="win_exifinfo">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child> <child>
@@ -639,6 +891,108 @@
</widget> </widget>
</child> </child>
</widget> </widget>
<widget class="GtkMenu" id="files_popup">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkMenuItem" id="add_tag1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Add tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_tag1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Remo_ve tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_tag_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator11">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_thumb1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Add _Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_thumb1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Re_move Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator7">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_image1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Add images to file. If file have no thumbnail,
thumbnail from first image will be generated.</property>
<property name="label" translatable="yes">Add _Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_image1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Rem_ove All Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator8">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="edit2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_edit2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete3_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename2_activate"/>
</widget>
</child>
</widget>
<widget class="GtkWindow" id="image_show"> <widget class="GtkWindow" id="image_show">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="title" translatable="yes">pyGTKtalog - Image</property> <property name="title" translatable="yes">pyGTKtalog - Image</property>

4
runtests.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
# run unittests
cd src/test
python run_tests.py

View File

@@ -862,6 +862,10 @@ class MainController(Controller):
self.__popup_menu(event) self.__popup_menu(event)
def on_expand_all1_activate(self, menu_item):
self.view['discs'].expand_all()
return
def on_export_activate(self, menu_item): def on_export_activate(self, menu_item):
"""export db file and coressponding images to tar.bz2 archive""" """export db file and coressponding images to tar.bz2 archive"""
dialog = Dialogs.ChooseFilename(None, _("Choose export file")) dialog = Dialogs.ChooseFilename(None, _("Choose export file"))

View File

@@ -3,20 +3,20 @@
Description: Misc functions used more than once in src Description: Misc functions used more than once in src
Type: lib Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-05 Created: 2008-12-15
""" """
def float_to_string(float_length): def float_to_string(float_length):
""" """
Parse float digit into time string Parse float digit into time string
Arguments: Arguments:
@number - digit to be converted into time. @string - width of generated image. If actual image width
exceeds this number scale is performed.
Returns HH:MM:SS formatted string Returns HH:MM:SS formatted string
""" """
hour = int(float_length / 3600) hour = int(float_length / 3600);
float_length -= hour*3600 float_length -= hour*3600
minutes = int(float_length / 60) minutes = int(float_length / 60)
float_length -= minutes * 60 float_length -= minutes * 60
sec = int(float_length) sec = int(float_length)
return "%02d:%02d:%02d" % (hour, minutes, sec) return "%02d:%02d:%02d" % (hour, minutes, sec)

View File

@@ -1,31 +1,50 @@
""" # This Python file uses the following encoding: utf-8
Project: pyGTKtalog #
Description: Thumbnail helper # Author: Roman 'gryf' Dobosz gryf@elysium.pl
Type: library #
Author: Roman 'gryf' Dobosz, gryf73@gmail.com # Copyright (C) 2007 by Roman 'gryf' Dobosz
Created: 2012-02-19 #
""" # This file is part of pyGTKtalog.
from hashlib import sha256 #
from cStringIO import StringIO # 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
# -------------------------------------------------------------------------
from tempfile import mkstemp
from hashlib import sha512
from shutil import move
from os import path
import sys
from lib import EXIF from lib import EXIF
import Image import Image
class Thumbnail(object): class Thumbnail(object):
"""Class for generate/extract thumbnail from image file""" """Class for generate/extract thumbnail from image file"""
def __init__(self, fp, base=''): def __init__(self, filename=None, base=''):
self.thumb_x = 160 self.thumb_x = 160
self.thumb_y = 160 self.thumb_y = 160
self.filename = filename
self.base = base self.base = base
self.sha256 = sha256(fp.read(10485760)).hexdigest() self.sha512 = sha512(open(filename).read()).hexdigest()
fp.seek(0) self.thumbnail_path = path.join(self.base, self.sha512 + "_t")
self.fp = fp
def save(self): def save(self):
"""Save thumbnail into specific directory structure """Save thumbnail into specific directory structure
return exif obj and fp to thumbnail""" return filename base and exif object or None"""
exif = {} exif = {}
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
3: Image.ROTATE_180, # Rotated 180 3: Image.ROTATE_180, # Rotated 180
@@ -38,46 +57,60 @@ class Thumbnail(object):
8: Image.ROTATE_90} # Rotated 90 CCW 8: Image.ROTATE_90} # Rotated 90 CCW
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT} flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
image_file = open(self.filename, 'rb')
try: try:
exif = EXIF.process_file(self.fp) exif = EXIF.process_file(image_file)
except: except:
self.fp.seek(0) if __debug__:
print "exception", sys.exc_info()[0], "raised with file:"
print self.filename
finally:
image_file.close()
if path.exists(self.thumbnail_path):
if __debug__:
print "file", self.filename, "with hash", self.sha512, "exists"
return self.sha512, exif
thumb_file = StringIO()
if 'JPEGThumbnail' in exif: if 'JPEGThumbnail' in exif:
thumb_file.write(exif['JPEGThumbnail']) if __debug__:
print self.filename, "exif thumb"
exif_thumbnail = exif['JPEGThumbnail']
thumb_file = open(self.thumbnail_path, 'wb')
thumb_file.write(exif_thumbnail)
thumb_file.close()
if 'Image Orientation' in exif: if 'Image Orientation' in exif:
orient = exif['Image Orientation'].values[0] orient = exif['Image Orientation'].values[0]
if orient > 1 and orient in orientations: if orient > 1 and orient in orientations:
tmp_thumb_img = StringIO() temp_image_path = mkstemp()[1]
thumb_image = Image.open(self.fp) thumb_image = Image.open(self.thumbnail_path)
tmp_thumb_img = thumb_image.transpose(orientations[orient]) tmp_thumb_img = thumb_image.transpose(orientations[orient])
if orient in flips: if orient in flips:
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient]) tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
if tmp_thumb_img: if tmp_thumb_img:
thumb_file.seek(0) tmp_thumb_img.save(temp_image_path, 'JPEG')
tmp_thumb_img.save(thumb_file, 'JPEG') move(temp_image_path, self.thumbnail_path)
tmp_thumb_img.close() return self.sha512, exif
else: else:
if __debug__:
print self.filename, "no exif thumb"
thumb = self.__scale_image() thumb = self.__scale_image()
if thumb: if thumb:
thumb.save(self.thumbnail_path, "JPEG") thumb.save(self.thumbnail_path, "JPEG")
return self.sha512, exif
return exif, thumb_file return None, exif
def __scale_image(self): def __scale_image(self):
"""create thumbnail. returns image object or None""" """create thumbnail. returns image object or None"""
try: try:
image_thumb = Image.open(self.fp).convert('RGB') image_thumb = Image.open(self.filename).convert('RGB')
except: except:
return None return None
it_x, it_y = image_thumb.size it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y: if it_x > self.thumb_x or it_y > self.thumb_y:
image_thumb.thumbnail((self.thumb_x, self.thumb_y), image_thumb.thumbnail((self.thumb_x, self.thumb_y), Image.ANTIALIAS)
Image.ANTIALIAS)
return image_thumb return image_thumb

170
src/lib/video.py Normal file
View File

@@ -0,0 +1,170 @@
"""
Project: pyGTKtalog
Description: Gather video file information, make "screenshot" with content
of the movie file. Uses external tools like mplayer and
ImageMagick tools (montage, convert).
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-04
"""
import os
import shutil
from tempfile import mkdtemp, mkstemp
import math
from misc import float_to_string
class Video(object):
"""Class for retrive midentify script output and put it in dict.
Usually there is no need for such a detailed movie/clip information.
Midentify script belongs to mplayer package.
"""
def __init__(self, filename):
"""Init class instance. Filename of a video file is required."""
self.filename = filename
self.tags = {}
output = os.popen('midentify "%s"' % self.filename).readlines()
attrs = {'ID_VIDEO_WIDTH': ['width', int],
'ID_VIDEO_HEIGHT': ['height', int],
'ID_LENGTH': ['length', lambda x: int(x.split(".")[0])],
# length is in seconds
'ID_DEMUXER': ['container', str],
'ID_VIDEO_FORMAT': ['video_format', str],
'ID_VIDEO_CODEC': ['video_codec', str],
'ID_AUDIO_CODEC': ['audio_codec', str],
'ID_AUDIO_FORMAT': ['audio_format', str],
'ID_AUDIO_NCH': ['audio_no_channels', int],}
for line in output:
line = line.strip()
for attr in attrs:
if attr in line:
self.tags[attrs[attr][0]] = \
attrs[attr][1](line.replace("%s=" % attr, ""))
if 'length' in self.tags:
if self.tags['length'] > 0:
hours = self.tags['length'] / 3600
seconds = self.tags['length'] - hours * 3600
minutes = seconds / 60
seconds -= minutes * 60
length_str = "%02d:%02d:%02d" % (hours, minutes, seconds)
self.tags['duration'] = length_str
def capture(self, out_width=1024):
"""
Extract images for given video filename and montage it into one, big
picture, similar to output from Windows Media Player thing, but without
captions and time (who need it anyway?).
Arguments:
@out_width - width of generated image. If actual image width
exceeds this number scale is performed.
Returns: image filename or None
NOTE: You should remove returned file manually, or move it in some
other place, otherwise it stays in filesystem.
"""
if not (self.tags.has_key('length') or self.tags.has_key('width')):
return None
# Calculate number of pictures. Base is equivalent 72 pictures for
# 1:30:00 movie length
scale = int(10 * math.log(self.tags['length'], math.e) - 11)
no_pictures = self.tags['length'] / scale
if no_pictures > 8:
# for really short movies
no_pictures = (no_pictures / 8 ) * 8 # only multiple of 8, please.
if not no_pictures:
# movie too short or length is 0
return None
if no_pictures < 4:
no_pictures = 4
tempdir = mkdtemp()
file_desc, image_fn = mkstemp()
os.close(file_desc)
self.__make_captures(tempdir, no_pictures)
self.__make_montage(tempdir, image_fn, no_pictures, out_width)
shutil.rmtree(tempdir)
return image_fn
def __make_captures(self, directory, no_pictures):
"""
Make screens with mplayer into given directory
Arguments:
@directory - full output directory name
@no_pictures - number of pictures to take
"""
step = float(self.tags['length']/(no_pictures + 1))
current_time = 0
for dummy in range(1, no_pictures + 1):
current_time += step
time = float_to_string(current_time)
cmd = "mplayer \"%s\" -ao null -brightness 0 -hue 0 " \
"-saturation 0 -contrast 0 -vf-clr -vo jpeg:outdir=\"%s\" -ss %s" \
" -frames 1 2>/dev/null"
os.popen(cmd % (self.filename, directory, time)).readlines()
shutil.move(os.path.join(directory, "00000001.jpg"),
os.path.join(directory, "picture_%s.jpg" % time))
def __make_montage(self, directory, image_fn, no_pictures, out_width):
"""
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
@out_width - width of final image to be scaled to.
"""
scale = False
row_length = 4
if no_pictures < 8:
row_length = 2
if (self.tags['width'] * row_length) > out_width:
scale = True
else:
for i in [8, 6, 5]:
if (no_pictures % i) == 0 and \
(i * self.tags['width']) <= out_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
# 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()
# 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 __str__(self):
str_out = ''
for key in self.tags:
str_out += "%20s: %s\n" % (key, self.tags[key])
return str_out

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
src/test/mocks/m.avi Normal file

Binary file not shown.

BIN
src/test/mocks/m.mkv Normal file

Binary file not shown.

BIN
src/test/mocks/m.mpg Normal file

Binary file not shown.

BIN
src/test/mocks/m.ogm Normal file

Binary file not shown.

BIN
src/test/mocks/m1.avi Normal file

Binary file not shown.

View File

@@ -0,0 +1,44 @@
|== INFO ON TEST IMAGES ==|
Unless stated otherwise, all images come from Wikipedia Commons - http://commons.wikimedia.org
For author and license information please refer to their respective description pages.
All images were scaled down using the GIMP v 2.4.5 since all we care about is the Exif info.
Tested before and after scaling to ensure Exif data was not modified (except for 'software' field).
== Filename == == Wiki Filename ==
= Camera Makes and Models =
Canon_40D.jpg Iguana_iguana_male_head.jpg
Canon_40D_photoshop_import.jpg Anolis_equestris_-_bright_close_3-4.jpg
Canon_DIGITAL_IXUS_400.jpg Ducati749.jpg
Fujifilm_FinePix6900ZOOM.jpg Hylidae_cinerea.JPG
Fujifilm_FinePix_E500.jpg VlaamseGaaiVeertje1480.JPG
Kodak_CX7530.jpg Red-headed_Rock_Agama.jpg
Konica_Minolta_DiMAGE_Z3.jpg Knechtova01.jpg
Nikon_COOLPIX_P1.jpg Miyagikotsu-castle6861.JPG
Nikon_D70.jpg Anolis_carolinensis_brown.jpg
Olympus_C8080WZ.jpg Pterois_volitans_Manado-e.jpg
Panasonic_DMC-FZ30.jpg Rømø_-_St.Klement_-_Kanzel_3.jpg
Pentax_K10D.jpg Mrs._Herbert_Stevens_May_2008.jpg
Ricoh_Caplio_RR330.jpg Steveston_dusk.JPG
Samsung_Digimax_i50_MP3.jpg Villa_di_Poggio_a_Caiano,_sala_neoclassica_4.JPG
Sony_HDR-HC3.jpg Positive_roll_film.jpg
WWL_(Polaroid)_ION230.jpg PoudriereBoisSousRoche3.jpg
= Other Stuff =
long_description.jpg US_10th_Mountain_Division_soldiers_in_Afghanistan.jpg
Contributions :
PaintTool_sample.jpg -- Submitted by Jan Trofimov (OXIj)
If you come across an image which is incorrectly parsed or errors out, consider
contributing it to the test cases : ianare@gmail.com

45
src/test/run_tests.py Executable file
View File

@@ -0,0 +1,45 @@
"""
Project: pyGTKtalog
Description: Test harvester and runner.
Type: exec
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import sys
import unittest
from os import path, chdir
import glob
def setup_path():
"""Sets up the python include paths to include needed directories"""
this_path = path.abspath(path.dirname(__file__))
sys.path = [path.join(this_path, "../../src")] + sys.path
sys.path = [path.join(this_path, "../../src/test/unit")] + sys.path
return
def build_suite():
"""Build suite test from files in unit directory. Filenames with test
suites should always end with "_test.py"."""
modules = []
for fname in glob.glob1('unit', '*_test.py'):
class_name = fname[:-8]
if "_" in class_name:
splited = class_name.split("_")
class_name = 'Test'
for word in splited:
class_name += word.capitalize()
else:
class_name = "Test" + class_name.capitalize()
modules.append(fname[:-3])
modules = map(__import__, modules)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
if __name__ == "__main__":
chdir(path.abspath(path.curdir))
setup_path()
unittest.main(defaultTest="build_suite")

View File

@@ -0,0 +1,17 @@
"""
Project: pyGTKtalog
Description: This is simple dummy test for... testing purposes :)
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import unittest
class TestDummy(unittest.TestCase):
"""Fake test class"""
def test_dummy_method(self):
"""Test simple assertion"""
self.assertTrue(True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,17 @@
"""
Project: pyGTKtalog
Description: This is another dummy test.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import unittest
class TestFooBar(unittest.TestCase):
"""Fake test class"""
def test_dummy_method(self):
"""Test simple assertion"""
self.assertTrue(True)
if __name__ == "__main__":
unittest.main()

View File

@@ -7,9 +7,7 @@
""" """
import unittest import unittest
import os import os
from lib.video import Video
from pygtktalog.video import Video
class TestVideo(unittest.TestCase): class TestVideo(unittest.TestCase):
"""Class for retrive midentify script output and put it in dict. """Class for retrive midentify script output and put it in dict.
@@ -25,7 +23,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 +39,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 +52,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.assertTrue(avi.tags['audio_no_channels'] in (1, 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'], 'mp4v') self.assertEqual(avi.tags['video_format'], 'mp4v')
self.assertEqual(avi.tags['length'], 4) self.assertEqual(avi.tags['length'], 4)
self.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3')) self.assertEqual(avi.tags['audio_codec'], 'a52')
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.assertTrue(avi.tags['container'] in ('mkv', 'lavfpref')) self.assertEqual(avi.tags['container'], 'mkv')
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,50 +82,24 @@ 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.assertTrue(avi.tags['audio_no_channels'] in (1, 2)) self.assertEqual(avi.tags['audio_no_channels'], 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.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3')) self.assertEqual(avi.tags['audio_codec'], 'a52')
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.assertTrue(avi.tags['container'] in ('ogg', 'lavfpref')) self.assertEqual(avi.tags['container'], 'ogg')
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"""
avi = Video("mocks/m.avi") avi = Video("mocks/m.avi")
filename = avi.capture() filename = avi.capture()
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.assertAlmostEqual(file_size/10000.0, 0.9, 0) self.assertEqual(file_size, 9077)
os.unlink(filename) os.unlink(filename)
for length in (480, 380, 4):
avi.tags['length'] = length
filename = avi.capture()
self.assertTrue(filename is not None)
os.unlink(filename)
avi.tags['length'] = 3
self.assertTrue(avi.capture() is None)
avi.tags['length'] = 4
avi.tags['width'] = 0
self.assertTrue(avi.capture() is None)
avi.tags['width'] = 1025
filename = avi.capture()
self.assertTrue(filename is not None)
os.unlink(filename)
del(avi.tags['length'])
self.assertTrue(avi.capture() is None)
self.assertTrue(len(str(avi)) > 0)
if __name__ == "__main__": if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main() unittest.main()

View File

View File

@@ -1,28 +0,0 @@
"""
Project: pyGTKtalog
Description: Tests for DataBase class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-07-19
"""
import unittest
import os
from pygtktalog.dbcommon import connect, Meta, Session, Base
class TestDataBase(unittest.TestCase):
"""
Class responsible for database connection and schema creation
"""
def test_connect(self):
"""
Test connection to database. Memory and file method will be tested.
"""
connect(":memory:")
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

View File

@@ -1,77 +0,0 @@
"""
Project: pyGTKtalog
Description: Test simple dialogs
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-19
"""
import unittest
import os
import gtk
from pygtktalog.dialogs import Dialog, yesno, okcancel, info, warn, error
class MessageDialogMock(gtk.MessageDialog):
"""Mock class for MessageDialog, which shouldn't be displayed in a test"""
def run(self):
"""Carefull! only for MESSAGE_INFO return value is RESPONSE_OK!"""
if self.get_property('message-type') == gtk.MESSAGE_INFO:
return gtk.RESPONSE_OK
else:
return gtk.RESPONSE_CANCEL
class TestDialog(unittest.TestCase):
"""Tests for Dialog class"""
def test_dialog_create(self):
"""Test dialog creation and run method"""
# overwrite MessageDialog class in gtk module
gtk.MessageDialog = MessageDialogMock
dialog = Dialog(gtk.MESSAGE_INFO, 'msg', 'secondarymsg', 'title')
self.assertTrue(dialog.buttons == gtk.BUTTONS_OK, "dialog should have"
" gtk.BUTTONS_OK")
self.assertTrue(dialog.run(), "dialog should return True")
dialog = Dialog(gtk.MESSAGE_QUESTION, 'msg', 'secondarymsg', 'title')
self.assertFalse(dialog.run(), "dialog should return False")
# NOTE: dialog should be run before test against buttons attribute
self.assertTrue(dialog.buttons == gtk.BUTTONS_YES_NO,
"dialog should have gtk.BUTTONS_YES_NO")
dialog = Dialog(gtk.MESSAGE_QUESTION, 'msg', 'secondarymsg', 'title')
dialog.buttons = gtk.BUTTONS_OK
dialog.ok_default = True
self.assertFalse(dialog.run(), "dialog should return True")
def test_error(self):
"""Test error function"""
result = error('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_warn(self):
"""Test warn function"""
result = warn('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_info(self):
"""Test info function"""
result = info('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_yesno(self):
"""Test yesno function"""
result = yesno('msg', 'secondarymsg', 'title')
self.assertFalse(result, "Should return False")
def test_okcancel(self):
"""Test yesno function"""
result = okcancel('msg', 'secondarymsg', 'title')
self.assertFalse(result, "Should return False")
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

View File

@@ -1,8 +0,0 @@
"""
Project: pyGTKtalog
Description: Tests for main view class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-03-14
"""

View File

@@ -1,33 +0,0 @@
"""
Project: pyGTKtalog
Description: Tests for misc functions.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-09
"""
import unittest
import os
import pygtktalog.misc as pgtkmisc
class TestMiscModule(unittest.TestCase):
"""
Tests functions from misc module
"""
def test_float_to_string(self):
"""
test conversion between digits to formated output
"""
self.assertEqual(pgtkmisc.float_to_string(10), '00:00:10')
self.assertEqual(pgtkmisc.float_to_string(76), '00:01:16')
self.assertEqual(pgtkmisc.float_to_string(22222), '06:10:22')
self.assertRaises(TypeError, pgtkmisc.float_to_string)
self.assertRaises(TypeError, pgtkmisc.float_to_string, None)
self.assertRaises(TypeError, pgtkmisc.float_to_string, '10')
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

View File

@@ -1,124 +0,0 @@
"""
Project: pyGTKtalog
Description: Tests for scan files.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-03-26
"""
import os
import unittest
from pygtktalog import scan
from pygtktalog.dbobjects import File
from pygtktalog.dbcommon import connect, Session
TEST_DIR = "/home/share/_test_/test_dir"
TEST_DIR_PERMS = "/home/share/_test_/test_dir_permissions/"
class TestScan(unittest.TestCase):
"""
Test cases for scan functionality
1. execution scan function:
1.1 simple case - should pass
1.2 non-existent directory passed
1.3 file passed
1.4 directory has permission that forbids file listing
2. rescan directory; looking for changes
2.0 don't touch records for changed files (same directories, same
filename, same type and size)
2.1 search for files of the same type, same size.
2.2 change parent node for moved files (don't insert new)
3. adding new directory tree which contains same files like already stored
in the database
"""
def setUp(self):
connect()
root = File()
root.id = 1
root.filename = 'root'
root.size = 0
root.source = 0
root.type = 0
root.parent_id = 1
sess = Session()
sess.add(root)
sess.commit()
def test_happy_scenario(self):
"""
make scan, count items
"""
scanob = scan.Scan(os.path.abspath(os.path.join(__file__,
"../../../mocks")))
scanob = scan.Scan(TEST_DIR)
result_list = scanob.add_files()
self.assertEqual(len(result_list), 143)
self.assertEqual(len(result_list[0].children), 8)
# check soft links
self.assertEqual(len([x for x in result_list if x.type == 3]), 2)
def test_wrong_and_nonexistent(self):
"""
Check for accessing non existent directory, regular file instead of
the directory, or file.directory with no access to it.
"""
scanobj = scan.Scan('/nonexistent_directory_')
self.assertRaises(OSError, scanobj.add_files)
scanobj.path = '/root'
self.assertRaises(scan.NoAccessError, scanobj.add_files)
scanobj.path = '/bin/sh'
self.assertRaises(scan.NoAccessError, scanobj.add_files)
# dir contains some non accessable items. Should just pass, and on
# logs should be messages about it
scanobj.path = TEST_DIR_PERMS
scanobj.add_files()
def test_abort_functionality(self):
scanobj = scan.Scan(TEST_DIR)
scanobj.abort = True
self.assertEqual(None, scanobj.add_files())
def test_double_scan(self):
"""
Do the scan twice.
"""
ses = Session()
self.assertEqual(len(ses.query(File).all()), 1)
scanob = scan.Scan(TEST_DIR)
scanob.add_files()
# note: we have 144 elements in db, because of root element
self.assertEqual(len(ses.query(File).all()), 144)
scanob2 = scan.Scan(TEST_DIR)
scanob2.add_files()
# it is perfectly ok, since we don't update collection, but just added
# same directory twice.
self.assertEqual(len(ses.query(File).all()), 287)
file_ob = scanob._files[2]
file2_ob = scanob2._files[2]
# File objects are different
self.assertTrue(file_ob is not file2_ob)
# While Image objects points to the same file
self.assertTrue(file_ob.images[0].filename == \
file2_ob.images[0].filename)
# they are different objects
self.assertTrue(file_ob.images[0] is not file2_ob.images[0])
ses.close()
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()