1
0
mirror of https://github.com/gryf/pygtktalog.git synced 2026-03-28 15:03:35 +01:00

7 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
56c77ae9a4 added EXIF patch and misc in lib
removed some cruft
2009-04-07 20:25:00 +00:00
c46d29a5bb added video module - replacement for midentify
added some mocks for images tests
2009-04-07 19:42:44 +00:00
5e8c33f05a changes in tests, upgrade to new EXIF module, etc 2009-04-07 19:40:15 +00:00
fb920f58bc * Removed cruft.
* Version change to 1.0.2.
2008-12-15 20:54:16 +00:00
292d290723 * Added shell script for preparing distfile.
* Added unitests.
 * Added mocks for video files.
 * Added midentify module.
2008-12-15 20:46:26 +00:00
0adcdaba8d * Lots of changes, code cleanup, import/export in GUI. 2008-12-15 20:40:24 +00:00
61 changed files with 3426 additions and 1891 deletions

72
README
View File

@@ -1,5 +1,5 @@
pyGTKtalog 1.0
==================
==============
pyGTKtalog is Linux/FreeBSD program for indexing CD/DVD or directories on
filesystem. It is similar to gtktalog <http://www.nongnu.org/gtktalog/> or
@@ -24,9 +24,8 @@ REQUIREMENTS
pyGTKtalog is written in python with following dependencies:
- python 2.4 or higher
- pygtk 2.10 or higher <http://www.pygtk.org>
- pysqlite2 <http://pysqlite.org/> (unnecessary, if python 2.5 is used)
- python 2.5 or higher
- pygtk 2.12 or higher <http://www.pygtk.org>
Optional modules:
@@ -75,6 +74,7 @@ There are still minor aims for versions 1.x to be done:
- implement advanced search
For version 2.0:
- Export/Import
- Icon grid in files view
- command line support: query, adding media to collection etc
- internationalization
@@ -82,7 +82,8 @@ For version 2.0:
- user definied group of tags (represented by color in cloud tag)
- hiding specified files - configurable, like dot prefixed, cfg and manualy
selected
- tests
- warning about existing image in media directory
Removed:
- filetypes handling (movies, images, archives, documents etc). Now it have
common, unified external "plugin" system - simple text output from command
@@ -103,17 +104,58 @@ Removed:
NOTES
=====
Catalog file is tared and gziped sqlite database and directories with images and
thumbnails. If there are more images, the size of catalog file will grow. So be
carefull with adding big images in your catalog file!
Catalog file is plain sqlite database (optionally compressed with bzip2). All
images are stored in ~/.pygtktalog/images directory. Names for images are
generated sha512 hash from image file itself. There is small possibility for two
identical hash for different image files. However, no images are overwritten.
Thumbnail filename for each image is simply concatenation of image filename in
images directory and '_t' string.
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
deleted. There are some issues with converting. All thumbnails will be lost. All
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
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
~/.pygtktalog/images). Functionality for exporting images and corresponding db
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:
sha512("filename" + "file size" + "file modification date").hexdigest()
for thumbnails:
sha512("filename" + "file size" + "file modification date").hexdigest() + "_t"
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.
Of course there are some limits for such approach. There is relatively small
possibility to generate two filenames that are the same in two cases:
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.
There is also converter form old database to new. In fact no image are stored in
archive with katalog. All thumnails will be lost. All images without big image
will be lost. There ar serious changes with 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 design prevent from deleting eny
file from media directory (placed in ~/.pygtktalog/images). Functionality for
exporting images and corresponding db file is planned.
BUGS
====

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,213 +0,0 @@
#!/usr/bin/python
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
import sys
import os
import shutil
import tarfile
try:
import sqlite3 as sqlite
except ImportError:
from pysqlite2 import dbapi2 as sqlite
from datetime import datetime
class OldModel(object):
"""Create, load, save, manipulate db file which is container for data"""
def __init__(self):
"""initialize"""
self.db_cursor = None
self.db_connection = None
self.internal_dirname = None
def cleanup(self):
"""remove temporary directory tree from filesystem"""
self.__close_db_connection()
if self.internal_dirname != None:
try:
shutil.rmtree(self.internal_dirname)
except OSError:
pass
return
def open(self, filename=None):
"""try to open db file"""
self.__create_internal_dirname()
self.filename = filename
try:
tar = tarfile.open(filename, "r:gz")
except:
try:
tar = tarfile.open(filename, "r")
except:
self.internal_dirname = None
return False
os.chdir(self.internal_dirname)
try:
tar.extractall()
if __debug__:
print "OldModel 73: extracted tarfile into",
print self.internal_dirname
except AttributeError:
# python 2.4 tarfile module lacks of method extractall()
directories = []
for tarinfo in tar:
if tarinfo.isdir():
# Extract directory with a safe mode, so that
# all files below can be extracted as well.
try:
os.makedirs(os.path.join('.', tarinfo.name), 0700)
except EnvironmentError:
pass
directories.append(tarinfo)
else:
tar.extract(tarinfo, '.')
# Reverse sort directories.
directories.sort(lambda a, b: cmp(a.name, b.name))
directories.reverse()
# Set correct owner, mtime and filemode on directories.
for tarinfo in directories:
try:
os.chown(os.path.join('.', tarinfo.name),
tarinfo.uid, tarinfo.gid)
os.utime(os.path.join('.', tarinfo.name),
(0, tarinfo.mtime))
except OSError:
if __debug__:
print "OldModel 103: open(): setting corrext owner,",
print "mtime etc"
tar.close()
self.__connect_to_db()
return True
# private class functions
def __connect_to_db(self):
"""initialize db connection and store it in class attributes"""
self.db_connection = sqlite.connect("%s" % \
(self.internal_dirname + '/db.sqlite'),
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
self.db_cursor = self.db_connection.cursor()
return
def __close_db_connection(self):
"""close db conection"""
if self.db_cursor != None:
self.db_cursor.close()
self.db_cursor = None
if self.db_connection != None:
self.db_connection.close()
self.db_connection = None
return
def __create_internal_dirname(self):
"""create temporary directory for working thumb/image files and
database"""
# TODO: change this stupid rutine into tempfile mkdtemp method
self.cleanup()
self.internal_dirname = "/tmp/pygtktalog%d" % \
datetime.now().microsecond
try:
os.mkdir(self.internal_dirname)
except IOError, (errno, strerror):
print "OldModel 138: __create_internal_dirname(): ", strerror
return
def setup_path():
"""Sets up the python include paths to include needed directories"""
import os.path
from src.utils.globals import TOPDIR
sys.path = [os.path.join(TOPDIR, "src")] + sys.path
return
if __name__ == "__main__":
"""run the stuff"""
if len(sys.argv) != 3:
print "Usage: %s old_katalog_filename new_katalog_filename" % \
sys.argv[0]
print "All available pictures will be exported aswell, however",
print "thumbnails without"
print "images will be lost."
sys.exit()
# 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__)
os.chdir(libraries_dir)
setup_path()
from shutil import copy
from utils.img import Img
from models.m_main import MainModel as NewModel
model = OldModel()
new_model = NewModel()
if not model.open(os.path.join(execution_dir, sys.argv[1])):
print "cannot open katalog in 1.0RC1 format"
sys.exit()
model.db_cursor.execute("""create table
images2(id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER,
filename TEXT);""")
model.db_cursor.execute("delete from thumbnails")
result = model.db_cursor.execute("select file_id, filename from images")
# (id, filename)
# (4921, u'images/13/39.jpg')
for row in result.fetchall():
if row[1] and os.path.exists(os.path.join(model.internal_dirname,
row[1])):
im = Img(os.path.join(model.internal_dirname, row[1]),
new_model.image_path)
image = im.save()
sql = "insert into images2(file_id, filename) values (?, ?)"
model.db_cursor.execute(sql, (row[0], image))
model.db_cursor.execute("select id from thumbnails where file_id=?", (row[0], ))
thumb = model.db_cursor.fetchone()
if not (thumb and thumb[0]):
sql = "insert into thumbnails(file_id, filename) values (?, ?)"
model.db_cursor.execute(sql, (row[0], image))
model.db_connection.commit()
model.db_cursor.execute("drop table images")
model.db_cursor.execute("alter table images2 rename to images")
copy(os.path.join(model.internal_dirname, 'db.sqlite'),
os.path.join(execution_dir, sys.argv[2]))
# remove stuff
model.cleanup()

303
locale/pl.po Normal file
View File

@@ -0,0 +1,303 @@
# Polish translations for PACKAGE package
# 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 ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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.py:38
msgid ""
"WARNING: You'll need Python Imaging Library (PIL), if you want to make "
"thumbnails!"
msgstr ""
#: src/ctrls/c_main.py:284
msgid "Delete thumbnails"
msgstr "Usunięcie miniaturek"
#: src/ctrls/c_main.py:284
msgid "Delete thumbnails?"
msgstr "Usunąć mniniaturki?"
#: src/ctrls/c_main.py:285
msgid "Thumbnails for selected items will be permanently removed from catalog."
msgstr "Miniaturki zaznaczonych obiektów zostaną trwale usunięte z katalogu."
#: src/ctrls/c_main.py:308 src/ctrls/c_main.py:731 src/ctrls/c_main.py:736
msgid "Delete images"
msgstr "Usnięcie obrazów"
#: 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"
msgstr "Zakończ aplikację"
#: src/ctrls/c_main.py:492
msgid "Do you really want to quit?"
msgstr "Czy naprawdę chcesz zakończyć?"
#: src/ctrls/c_main.py:493 src/ctrls/c_main.py:506
msgid "Current database is not saved, any changes will be lost."
msgstr "Bieżąca baza nie została zachowana, wszystkie zmiany zostaną utracone."
#: src/ctrls/c_main.py:504 src/ctrls/c_main.py:638
msgid "Unsaved data"
msgstr "Niezapisane dane"
#: src/ctrls/c_main.py:505
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 ""
#: src/ctrls/c_main.py:640
msgid "Pressing <b>Ok</b> will abandon catalog."
msgstr ""
#: src/ctrls/c_main.py:664
msgid "Error opening file"
msgstr ""
#: src/ctrls/c_main.py:665
#, python-format
msgid "Cannot open file %s."
msgstr ""
#: src/ctrls/c_main.py:731
msgid "No images selected"
msgstr ""
#: src/ctrls/c_main.py:732
msgid "You have to select at least one image to delete."
msgstr ""
#: src/ctrls/c_main.py:736
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 ""

301
locale/pygtktalog.pot Normal file
View File

@@ -0,0 +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.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: pygtktalog.py:38
msgid ""
"WARNING: You'll need Python Imaging Library (PIL), if you want to make "
"thumbnails!"
msgstr ""
#: src/ctrls/c_main.py:284
msgid "Delete thumbnails"
msgstr ""
#: src/ctrls/c_main.py:284
msgid "Delete thumbnails?"
msgstr ""
#: src/ctrls/c_main.py:285
msgid "Thumbnails for selected items will be permanently removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:308 src/ctrls/c_main.py:731 src/ctrls/c_main.py:736
msgid "Delete images"
msgstr ""
#: src/ctrls/c_main.py:308
msgid "Delete all images?"
msgstr ""
#: src/ctrls/c_main.py:309
msgid "All images for selected items will be permanently removed from catalog."
msgstr ""
#: src/ctrls/c_main.py:342
msgid "Image view"
msgstr ""
#: src/ctrls/c_main.py:342
msgid "No Image"
msgstr ""
#: src/ctrls/c_main.py:343
msgid "Image file does not exist."
msgstr ""
#: src/ctrls/c_main.py:491
msgid "Quit application"
msgstr ""
#: src/ctrls/c_main.py:492
msgid "Do you really want to quit?"
msgstr ""
#: src/ctrls/c_main.py:493 src/ctrls/c_main.py:506
msgid "Current database is not saved, any changes will be lost."
msgstr ""
#: src/ctrls/c_main.py:504 src/ctrls/c_main.py:638
msgid "Unsaved data"
msgstr ""
#: src/ctrls/c_main.py:505
msgid "Do you want to abandon changes?"
msgstr ""
#: src/ctrls/c_main.py:541
msgid "Error mounting device"
msgstr ""
#: src/ctrls/c_main.py:542
#, python-format
msgid "Cannot mount device pointed to %s"
msgstr ""
#: src/ctrls/c_main.py:544
#, python-format
msgid ""
"Last mount message:\n"
"%s"
msgstr ""
#: src/ctrls/c_main.py:616
msgid "Error writing file"
msgstr ""
#: src/ctrls/c_main.py:617
#, python-format
msgid "Cannot write file %s."
msgstr ""
#: src/ctrls/c_main.py:639
msgid "There is not saved database"
msgstr ""
#: src/ctrls/c_main.py:640
msgid "Pressing <b>Ok</b> will abandon catalog."
msgstr ""
#: src/ctrls/c_main.py:664
msgid "Error opening file"
msgstr ""
#: src/ctrls/c_main.py:665
#, python-format
msgid "Cannot open file %s."
msgstr ""
#: src/ctrls/c_main.py:731
msgid "No images selected"
msgstr ""
#: src/ctrls/c_main.py:732
msgid "You have to select at least one image to delete."
msgstr ""
#: src/ctrls/c_main.py:736
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 ""

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

View File

@@ -1,109 +1,63 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
"""
Project: pyGTKtalog
Description: Application main launch file.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2007-05-01
"""
import sys
import os
try:
import gtk
except ImportError:
print "You need to install pyGTK v2.10.x or newer."
sys.exit(1)
import locale
import gettext
import gtk
import pygtk
pygtk.require("2.0")
def setup_path():
"""Sets up the python include paths to include needed directories"""
import os.path
from src.utils.globals import TOPDIR
sys.path = [os.path.join(TOPDIR, "src")] + sys.path
return
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"""
import sys
import gtkmvc
gtkmvc.require("1.2.0")
try:
from models.m_config import ConfigModel
except ImportError:
print "Some fundamental files are missing.",
print "Try runnig pyGTKtalog in his root directory"
sys.exit(1)
conf = ConfigModel()
conf.load()
try:
import pygtk
#tell pyGTK, if possible, that we want GTKv2
pygtk.require("2.0")
except ImportError:
#Some distributions come with GTK2, but not pyGTK
pass
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
print "pyGTKtalog uses SQLite DB.\nYou'll need to get it and the",
print "python bindings as well.",
print "http://www.sqlite.org"
print "http://initd.org/tracker/pysqlite"
sys.exit(1)
if conf.confd['exportxls']:
try:
import pyExcelerator
except ImportError:
print "WARNING: You'll need pyExcelerator, if you want to export",
print "DB to XLS format."
print "http://sourceforge.net/projects/pyexcelerator"
if conf.confd['thumbs'] and conf.confd['retrive']:
try:
import Image
except ImportError:
print "WARNING: You'll need Python Imaging Library (PIL), if you",
print "want to make\nthumbnails!"
print _("WARNING: You'll need Python Imaging Library (PIL), if "
"you want to make thumbnails!")
raise
return
if __name__ == "__main__":
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__)
os.chdir(libraries_dir)
if libraries_dir:
os.chdir(libraries_dir)
# Setup i18n
locale.setlocale(locale.LC_ALL, '')
gettext.install(APPL_SHORT_NAME, 'locale', unicode=True)
setup_path()
check_requirements()
from models.m_main import MainModel
from ctrls.c_main import MainController
from views.v_main import MainView
model = MainModel()
if len(sys.argv) > 1:
model.open(os.path.join(execution_dir, sys.argv[1]))
@@ -116,3 +70,6 @@ if __name__ == "__main__":
model.config.save()
model.cleanup()
gtk.main_quit
if __name__ == "__main__":
run()

View File

@@ -62,6 +62,27 @@
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="import">
<property name="visible">True</property>
<property name="label" translatable="yes">Import</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_import_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="export">
<property name="visible">True</property>
<property name="label" translatable="yes">Export</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_export_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator13">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="recent_files1">
<property name="visible">True</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

@@ -22,27 +22,26 @@
# -------------------------------------------------------------------------
__version__ = "1.0 RC2"
LICENCE = open('LICENCE').read()
import os.path
from os import popen
from utils import deviceHelper
from gtkmvc import Controller
from c_config import ConfigController
from views.v_config import ConfigView
from c_search import SearchController
from views.v_search import SearchView
import views.v_dialogs as Dialogs
from views.v_image import ImageView
import gtk
import pango
from gtkmvc import Controller
from lib import device_helper
from lib.globs import APPL_VERSION
from c_config import ConfigController
from views.v_config import ConfigView
from c_search import SearchController
from views.v_search import SearchView
import views.v_dialogs as Dialogs
from views.v_image import ImageView
class MainController(Controller):
"""Controller for main application window"""
scan_cd = False
@@ -253,7 +252,7 @@ class MainController(Controller):
except TypeError:
if __debug__:
print "c_main.py: on_edit2_activate(): 0",
print "zaznaczonych wierszy"
print "selected rows"
return
val = self.model.get_file_info(id)
@@ -282,11 +281,9 @@ class MainController(Controller):
def on_remove_thumb1_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete thumbnails'
question = 'Delete thumbnails?'
description = "Thumbnails for selected items will be permanently"
description += " removed from catalog."
obj = Dialogs.Qst(title, question, description)
obj = Dialogs.Qst(_("Delete thumbnails"), _("Delete thumbnails?"),
_("Thumbnails for selected items will be "
"permanently removed from catalog."))
if not obj.run():
return
try:
@@ -308,11 +305,9 @@ class MainController(Controller):
def on_remove_image1_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete images'
question = 'Delete all images?'
description = 'All images for selected items will be permanently'
description += ' removed from catalog.'
obj = Dialogs.Qst(title, question, description)
obj = Dialogs.Qst(_("Delete images"), _("Delete all images?"),
_("All images for selected items will be "
"permanently removed from catalog."))
if not obj.run():
return
try:
@@ -344,8 +339,8 @@ class MainController(Controller):
else:
ImageView(img)
else:
Dialogs.Inf("Image view", "No Image",
"Image file does not exist.")
Dialogs.Inf(_("Image view"), _("No Image"),
_("Image file does not exist."))
def on_rename1_activate(self, widget):
model, iter = self.view['discs'].get_selection().get_selected()
@@ -493,11 +488,10 @@ class MainController(Controller):
# check if any unsaved project is on go.
if self.model.unsaved_project and \
self.model.config.confd['confirmquit']:
title = 'Quit application - pyGTKtalog'
question = 'Do you really want to quit?'
description = "Current database is not saved, any changes will "
description += "be lost."
if not Dialogs.Qst(title, question, description).run():
if not Dialogs.Qst(_("Quit application") + " - pyGTKtalog",
_("Do you really want to quit?"),
_("Current database is not saved, any changes "
"will be lost.")).run():
return
self.__store_settings()
self.model.cleanup()
@@ -507,10 +501,10 @@ class MainController(Controller):
def on_new_activate(self, widget):
"""Create new database file"""
if self.model.unsaved_project:
title = 'Unsaved data - pyGTKtalog'
question = "Do you want to abandon changes?"
desc = "Current database is not saved, any changes will be lost."
if not Dialogs.Qst(title, question, desc).run():
if not Dialogs.Qst(_("Unsaved data") + " - pyGTKtalog",
_("Do you want to abandon changes?"),
_("Current database is not saved, any changes "
"will be lost.")).run():
return
self.model.new()
@@ -526,9 +520,9 @@ class MainController(Controller):
def on_add_cd_activate(self, widget, label=None, current_id=None):
"""Add directory structure from cd/dvd disc"""
mount = deviceHelper.volmount(self.model.config.confd['cd'])
if mount == 'ok':
guessed_label = deviceHelper.volname(self.model.config.confd['cd'])
mount, msg = device_helper.volmount(self.model.config.confd['cd'])
if mount:
guessed_label = device_helper.volname(self.model.config.confd['cd'])
if not label:
label = Dialogs.InputDiskLabel(guessed_label).run()
if label:
@@ -541,13 +535,13 @@ class MainController(Controller):
self.model.unsaved_project = True
self.__set_title(filepath=self.model.filename, modified=True)
else:
deviceHelper.volumount(self.model.config.confd['cd'])
device_helper.volumount(self.model.config.confd['cd'])
return True
else:
Dialogs.Wrn("Error mounting device - pyGTKtalog",
"Cannot mount device pointed to %s" %
Dialogs.Wrn(_("Error mounting device") + " - pyGTKtalog",
_("Cannot mount device pointed to %s") %
self.model.config.confd['cd'],
"Last mount message:\n%s" % mount)
_("Last mount message:\n%s") % msg)
return False
def on_add_directory_activate(self, widget, path=None, label=None,
@@ -571,7 +565,7 @@ class MainController(Controller):
# NOTE: about
def on_about1_activate(self, widget):
"""Show about dialog"""
Dialogs.Abt("pyGTKtalog", __version__, "About",
Dialogs.Abt("pyGTKtalog", "%d.%d.%d" % APPL_VERSION, "About",
["Roman 'gryf' Dobosz"], LICENCE)
return
@@ -600,6 +594,7 @@ class MainController(Controller):
def on_save_activate(self, widget):
"""Save catalog to file"""
# FIXME: Traceback when recent is null
if self.model.filename:
self.model.save()
self.__set_title(filepath=self.model.filename)
@@ -619,8 +614,8 @@ class MainController(Controller):
self.model.config.add_recent(path)
self.__set_title(filepath=path)
else:
Dialogs.Err("Error writing file - pyGTKtalog",
"Cannot write file %s." % path, "%s" % err)
Dialogs.Err(_("Error writing file") + " - pyGTKtalog",
_("Cannot write file %s.") % path, "%s" % err)
def on_stat1_activate(self, menu_item, selected_id=None):
data = self.model.get_stats(selected_id)
@@ -641,9 +636,9 @@ class MainController(Controller):
"""Open catalog file"""
confirm = self.model.config.confd['confirmabandon']
if self.model.unsaved_project and confirm:
obj = Dialogs.Qst('Unsaved data - pyGTKtalog',
'There is not saved database',
'Pressing "Ok" will abandon catalog.')
obj = Dialogs.Qst(_("Unsaved data") + " - pyGTKtalog",
_("There is not saved database"),
_("Pressing <b>Ok</b> will abandon catalog."))
if not obj.run():
return
@@ -667,8 +662,8 @@ class MainController(Controller):
if path:
if not self.model.open(path):
Dialogs.Err("Error opening file - pyGTKtalog",
"Cannot open file %s." % path)
Dialogs.Err(_("Error opening file") + " - pyGTKtalog",
_("Cannot open file %s.") % path)
else:
self.__generate_recent_menu()
self.__activate_ui(path)
@@ -734,13 +729,14 @@ class MainController(Controller):
"""delete selected images (with thumbnails)"""
list_of_paths = self.view['images'].get_selected_items()
if not list_of_paths:
Dialogs.Inf("Delete images", "No images selected",
"You have to select at least one image to delete.")
Dialogs.Inf(_("Delete images"), _("No images selected"),
_("You have to select at least one image to delete."))
return
if self.model.config.confd['delwarn']:
obj = Dialogs.Qst('Delete images', 'Delete selected images?',
'Selected images will be permanently removed from catalog.')
obj = Dialogs.Qst(_("Delete images"), _("Delete selected images?"),
_("Selected images will be permanently removed"
" from catalog."))
if not obj.run():
return
@@ -769,7 +765,7 @@ class MainController(Controller):
def on_img_save_activate(self, menu_item):
"""export images (not thumbnails) into desired direcotry"""
dialog = Dialogs.SelectDirectory("Choose directory to save images")
dialog = Dialogs.SelectDirectory(_("Choose directory to save images"))
filepath = dialog.run()
if not filepath:
@@ -794,14 +790,13 @@ class MainController(Controller):
count += 1
if count > 0:
Dialogs.Inf("Save images",
"%d images was succsefully saved." % count,
"Images are placed in directory:\n%s." % filepath)
Dialogs.Inf(_("Save images"),
_("%d images was succsefully saved.") % count,
_("Images are placed in directory:\n%s.") % filepath)
else:
description = "Images probably don't have real images - only"
description += " thumbnails."
Dialogs.Inf("Save images",
"No images was saved.",
description = _("Images probably don't have real images - only"
" thumbnails.")
Dialogs.Inf(_("Save images"), _("No images was saved."),
description)
return
@@ -810,12 +805,12 @@ class MainController(Controller):
list_of_paths = self.view['images'].get_selected_items()
if not list_of_paths:
Dialogs.Inf("Set thumbnail", "No image selected",
"You have to select one image to set as thumbnail.")
Dialogs.Inf(_("Set thumbnail"), _("No image selected"),
_("You have to select one image to set as thumbnail."))
return
if len(list_of_paths) >1:
Dialogs.Inf("Set thumbnail", "To many images selected",
"You have to select one image to set as thumbnail.")
Dialogs.Inf(_("Set thumbnail"), _("To many images selected"),
_("You have to select one image to set as thumbnail."))
return
model = self.view['images'].get_model()
@@ -871,6 +866,16 @@ class MainController(Controller):
self.view['discs'].expand_all()
return
def on_export_activate(self, menu_item):
"""export db file and coressponding images to tar.bz2 archive"""
dialog = Dialogs.ChooseFilename(None, _("Choose export file"))
filepath = dialog.run()
if not filepath:
return
# TODO: dokończyć ten shit
def on_collapse_all1_activate(self, menu_item):
self.view['discs'].collapse_all()
return
@@ -1009,8 +1014,8 @@ class MainController(Controller):
def on_delete_tag_activate(self, menu_item):
ids = self.__get_tv_selection_ids(self.view['files'])
if not ids:
Dialogs.Inf("Remove tags", "No files selected",
"You have to select some files first.")
Dialogs.Inf(_("Remove tags"), _("No files selected"),
_("You have to select some files first."))
return
tags = self.model.get_tags_by_file_id(ids)
@@ -1018,8 +1023,9 @@ class MainController(Controller):
d = Dialogs.TagsRemoveDialog(tags)
retcode, retval = d.run()
if retcode=="ok" and not retval:
Dialogs.Inf("Remove tags", "No tags selected",
"You have to select any tag to remove from files.")
Dialogs.Inf(_("Remove tags"), _("No tags selected"),
_("You have to select any tag to remove from"
" files."))
return
elif retcode == "ok" and retval:
self.model.delete_tags(ids, retval)
@@ -1044,7 +1050,7 @@ class MainController(Controller):
def on_add_image1_activate(self, menu_item):
dialog = Dialogs.LoadImageFile(True)
msg = "Don't copy images. Generate only thumbnails."
msg = _("Don't copy images. Generate only thumbnails.")
toggle = gtk.CheckButton(msg)
toggle.show()
dialog.dialog.set_extra_widget(toggle)
@@ -1108,15 +1114,15 @@ class MainController(Controller):
return
if not selected_iter:
Dialogs.Inf("Delete disc", "No disc selected",
"You have to select disc first before you " +\
"can delete it")
Dialogs.Inf(_("Delete disc"), _("No disc selected"),
_("You have to select disc first before you "
"can delete it"))
return
if self.model.config.confd['delwarn']:
name = model.get_value(selected_iter, 1)
obj = Dialogs.Qst('Delete %s' % name, 'Delete %s?' % name,
'Object will be permanently removed.')
obj = Dialogs.Qst(_("Delete %s") % name, _("Delete %s?") % name,
_("Object will be permanently removed."))
if not obj.run():
return
@@ -1159,14 +1165,14 @@ class MainController(Controller):
return
if not list_of_paths:
Dialogs.Inf("Delete files", "No files selected",
"You have to select at least one file to delete.")
Dialogs.Inf(_("Delete files"), _("No files selected"),
_("You have to select at least one file to delete."))
return
if self.model.config.confd['delwarn']:
description = "Selected files and directories will be "
description += "permanently\n removed from catalog."
obj = Dialogs.Qst("Delete files", "Delete files?", description)
obj = Dialogs.Qst(_("Delete files"), _("Delete files?"),
_("Selected files and directories will be "
"permanently\n removed from catalog."))
if not obj.run():
return
@@ -1207,10 +1213,9 @@ class MainController(Controller):
def on_th_delete_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete thumbnail'
question = 'Delete thumbnail?'
dsc = "Current thumbnail will be permanently removed from catalog."
obj = Dialogs.Qst(title, question, dsc)
obj = Dialogs.Qst(_("Delete thumbnail"), _("Delete thumbnail?"),
_("Current thumbnail will be permanently removed"
" from catalog."))
if not obj.run():
return
path, column = self.view['files'].get_cursor()
@@ -1354,20 +1359,22 @@ class MainController(Controller):
# umount/eject cd
ejectapp = self.model.config.confd['ejectapp']
if self.model.config.confd['eject'] and ejectapp:
msg = deviceHelper.eject_cd(ejectapp,
msg = device_helper.eject_cd(ejectapp,
self.model.config.confd['cd'])
if msg != 'ok':
Dialogs.Wrn("error ejecting device - pyGTKtalog",
"Cannot eject device pointed to %s" %
title = _("Error ejecting device")
Dialogs.Wrn(title + " - pyGTKtalog",
_("Cannot eject device pointed to %s") %
self.model.config.confd['cd'],
"Last eject message:\n%s" % msg)
_("Last eject message:\n%s") % msg)
else:
msg = deviceHelper.volumount(self.model.config.confd['cd'])
msg = device_helper.volumount(self.model.config.confd['cd'])
if msg != 'ok':
Dialogs.Wrn("error unmounting device - pyGTKtalog",
"Cannot unmount device pointed to %s" %
title = _("Error unmounting device")
Dialogs.Wrn(title + " - pyGTKtalog",
_("Cannot unmount device pointed to %s") %
self.model.config.confd['cd'],
"Last umount message:\n%s" % msg)
_("Last umount message:\n%s") % msg)
return
def property_progress_value_change(self, model, old, new):

1781
src/lib/EXIF.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
diff -au EXIF.py EXIF_mine.py
--- EXIF.py 2008-07-31 15:53:50.000000000 +0200
+++ EXIF_mine.py 2009-02-25 18:39:48.000000000 +0100
@@ -251,40 +251,54 @@
2: 'CenterWeightedAverage',
3: 'Spot',
4: 'MultiSpot',
- 5: 'Pattern'}),
+ 5: 'Pattern',
+ 6: 'Partial',
+ 255: 'other'}),
0x9208: ('LightSource',
{0: 'Unknown',
1: 'Daylight',
2: 'Fluorescent',
- 3: 'Tungsten',
- 9: 'Fine Weather',
- 10: 'Flash',
+ 3: 'Tungsten (incandescent light)',
+ 4: 'Flash',
+ 9: 'Fine weather',
+ 10: 'Cloudy weather',
11: 'Shade',
- 12: 'Daylight Fluorescent',
- 13: 'Day White Fluorescent',
- 14: 'Cool White Fluorescent',
- 15: 'White Fluorescent',
- 17: 'Standard Light A',
- 18: 'Standard Light B',
- 19: 'Standard Light C',
+ 12: 'Daylight fluorescent (D 5700 - 7100K)',
+ 13: 'Day white fluorescent (N 4600 - 5400K)',
+ 14: 'Cool white fluorescent (W 3900 - 4500K)',
+ 15: 'White fluorescent (WW 3200 - 3700K)',
+ 17: 'Standard light A',
+ 18: 'Standard light B',
+ 19: 'Standard light C',
20: 'D55',
21: 'D65',
22: 'D75',
- 255: 'Other'}),
+ 23: 'D50',
+ 24: 'ISO studio tungsten',
+ 255: 'other light source',}),
0x9209: ('Flash',
- {0: 'No',
- 1: 'Fired',
- 5: 'Fired (?)', # no return sensed
- 7: 'Fired (!)', # return sensed
- 9: 'Fill Fired',
- 13: 'Fill Fired (?)',
- 15: 'Fill Fired (!)',
- 16: 'Off',
- 24: 'Auto Off',
- 25: 'Auto Fired',
- 29: 'Auto Fired (?)',
- 31: 'Auto Fired (!)',
- 32: 'Not Available'}),
+ {0: 'Flash did not fire',
+ 1: 'Flash fired',
+ 5: 'Strobe return light not detected',
+ 7: 'Strobe return light detected',
+ 9: 'Flash fired, compulsory flash mode',
+ 13: 'Flash fired, compulsory flash mode, return light not detected',
+ 15: 'Flash fired, compulsory flash mode, return light detected',
+ 16: 'Flash did not fire, compulsory flash mode',
+ 24: 'Flash did not fire, auto mode',
+ 25: 'Flash fired, auto mode',
+ 29: 'Flash fired, auto mode, return light not detected',
+ 31: 'Flash fired, auto mode, return light detected',
+ 32: 'No flash function',
+ 65: 'Flash fired, red-eye reduction mode',
+ 69: 'Flash fired, red-eye reduction mode, return light not detected',
+ 71: 'Flash fired, red-eye reduction mode, return light detected',
+ 73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
+ 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
+ 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
+ 89: 'Flash fired, auto mode, red-eye reduction mode',
+ 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
+ 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'}),
0x920A: ('FocalLength', ),
0x9214: ('SubjectArea', ),
0x927C: ('MakerNote', ),

86
src/lib/device_helper.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Project: pyGTKtalog
Description: Simple functions for device management.
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import os
import locale
import gettext
from src.lib.globs import APPL_SHORT_NAME
locale.setlocale(locale.LC_ALL, '')
gettext.install(APPL_SHORT_NAME, 'locale', unicode=True)
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
label = None
if dev != None:
try:
disk = open(dev, "rb")
disk.seek(32808)
label = disk.read(32).strip()
disk.close()
except IOError:
return None
return label
def volmount(mntp):
"""
Mount device.
@param mountpoint
@returns tuple with bool status of mount, and string with error message
"""
_in, _out, _err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
return False, inf[0].strip()
else:
return True, ''
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in, _out, _err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
return inf[0].strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device = parts
if device[0] == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess device name by mountpoint from fstab"""
fstab = open("/etc/fstab")
device = None
for line in fstab.readlines():
output = line.split()
# lengtht of single valid fstab line is at least 5
if len(output) > 5 and output[1] == mntp and output[0][0] != '#':
device = output[0]
fstab.close()
return device
def eject_cd(eject_app, cdrom):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in, _out, _err = os.popen3("%s %s" % (eject_app, cdrom))
inf = _err.readlines()
if len(inf) > 0 and inf[0].strip() != '':
return inf[0].strip()
return 'ok'
return _("Eject program not specified")

View File

@@ -25,15 +25,17 @@
import os.path
import sys
if sys.argv[0]: top_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
else: top_dir = "."
if sys.argv[0]:
top_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
top_dir = "."
# ----------------------------------------------------------------------
TOPDIR = top_dir
RESOURCES_DIR = os.path.join(TOPDIR, "resources")
GLADE_DIR = os.path.join(RESOURCES_DIR, "glade")
STYLES_DIR = os.path.join(RESOURCES_DIR, "styles")
APPL_SHORT_NAME = "pycolector"
APPL_VERSION = (1, 0, 0)
APPL_SHORT_NAME = "pygtktalog"
APPL_VERSION = (1, 0, 2)
# ----------------------------------------------------------------------

View File

@@ -27,7 +27,7 @@ import os
from datetime import date
class GthumbCommentParser(object):
"""Read and return comments created eith gThumb program"""
def __init__(self, image_path, image_filename):
self.path = image_path
self.filename = image_filename
@@ -36,16 +36,16 @@ class GthumbCommentParser(object):
"""Return dictionary with apropriate fields, or None if no comment
available"""
try:
gf = gzip.open(os.path.join(self.path,
'.comments', self.filename + '.xml'))
gzf = gzip.open(os.path.join(self.path, '.comments',
self.filename + '.xml'))
except:
return None
try:
xml = gf.read()
gf.close()
xml = gzf.read()
gzf.close()
except:
gf.close()
gzf.close()
return None
if not xml:

View File

@@ -23,11 +23,9 @@
# -------------------------------------------------------------------------
from shutil import copy
from os import path, mkdir
from os import path
from hashlib import sha512
from tempfile import mkstemp
from utils import EXIF
import Image
class Img(object):
@@ -46,14 +44,14 @@ class Img(object):
"""Save image and asociated thumbnail into specific directory structure
returns filename for image"""
image_filename = path.join(self.base, self.sha512)
thumbnail = path.join(self.base, self.sha512 + "_t")
# check wheter image already exists
if path.exists(image_filename) and path.exists(thumbnail):
if __debug__:
print "image", self.filename, "with hash", self.sha512, "already exist"
print "image", self.filename, "with hash",
print self.sha512, "already exist"
return self.sha512
if not path.exists(thumbnail):

22
src/lib/misc.py Normal file
View File

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

View File

@@ -28,7 +28,7 @@ from shutil import move
from os import path
import sys
from utils import EXIF
from lib import EXIF
import Image
class Thumbnail(object):

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

View File

@@ -27,28 +27,27 @@ import sys
import shutil
import bz2
import math
import sqlite3 as sqlite
from tempfile import mkstemp
from datetime import datetime
import threading as _threading
import gtk
import gobject
from gtkmvc.model_mt import ModelMT
from pysqlite2 import dbapi2 as sqlite
from datetime import datetime
import threading as _threading
from m_config import ConfigModel
try:
from utils.thumbnail import Thumbnail
from utils.img import Img
from lib.thumbnail import Thumbnail
from lib.img import Img
except:
pass
from utils.parse_exif import ParseExif
from utils.gthumb import GthumbCommentParser
from lib.parse_exif import ParseExif
from lib.gthumb import GthumbCommentParser
from utils.no_thumb import no_thumb as no_thumb_img
from lib.no_thumb import no_thumb as no_thumb_img
from lib.video import Video
class MainModel(ModelMT):
"""Create, load, save, manipulate db file which is container for data"""
@@ -87,6 +86,7 @@ class MainModel(ModelMT):
# images extensions - only for PIL and EXIF
IMG = ['jpg', 'jpeg', 'gif', 'png', 'tif', 'tiff', 'tga', 'pcx', 'bmp',
'xbm', 'xpm', 'jp2', 'jpx', 'pnm']
MOV = ['avi', 'mpg', 'mpeg', 'mkv', 'wmv', 'ogm', 'mov']
def __init__(self):
"""initialize"""
@@ -592,6 +592,14 @@ class MainModel(ModelMT):
return
if filename:
if not '.sqlite' in filename:
filename += '.sqlite'
else:
filename = filename[:filename.rindex('.sqlite')] + '.sqlite'
if self.config.confd['compress']:
filename += '.bz2'
self.filename = filename
val, err = self.__compress_and_save()
if not val:
@@ -1742,6 +1750,23 @@ class MainModel(ModelMT):
ext = i.split('.')[-1].lower()
# Video
if ext in self.MOV:
#import rpdb2; rpdb2.start_embedded_debugger('pass')
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(?, ?)"""
db_cursor.execute(sql, (fileid, th+"_t"))
sql = """INSERT INTO images(file_id, filename)
VALUES(?, ?)"""
db_cursor.execute(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)

0
src/test/__init__.py Normal file
View File

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

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()

105
src/test/unit/video_test.py Normal file
View File

@@ -0,0 +1,105 @@
"""
Project: pyGTKtalog
Description: Tests for Video class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import unittest
import os
from lib.video import Video
class TestVideo(unittest.TestCase):
"""Class for retrive midentify script output and put it in dict.
Usually there is no need for such a detailed movie/clip information.
Video script belongs to mplayer package.
"""
def test_avi(self):
"""test mock avi file, should return dict with expected values"""
avi = Video("mocks/m.avi")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'XVID')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffodivx')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_avi2(self):
"""test another mock avi file, should return dict with expected
values"""
avi = Video("mocks/m1.avi")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'H264')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffh264')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_mkv(self):
"""test mock mkv file, should return dict with expected values"""
avi = Video("mocks/m.mkv")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'mp4v')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'a52')
self.assertEqual(avi.tags['video_codec'], 'ffodivx')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'mkv')
def test_mpg(self):
"""test mock mpg file, should return dict with expected values"""
avi = Video("mocks/m.mpg")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertFalse(avi.tags.has_key('audio_format'))
self.assertEqual(avi.tags['width'], 128)
self.assertFalse(avi.tags.has_key('audio_no_channels'))
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], '0x10000001')
self.assertFalse(avi.tags.has_key('lenght'))
self.assertFalse(avi.tags.has_key('audio_codec'))
self.assertEqual(avi.tags['video_codec'], 'ffmpeg1')
self.assertFalse(avi.tags.has_key('duration'))
self.assertEqual(avi.tags['container'], 'mpeges')
def test_ogm(self):
"""test mock ogm file, should return dict with expected values"""
avi = Video("mocks/m.ogm")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 160)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 120)
self.assertEqual(avi.tags['video_format'], 'H264')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'a52')
self.assertEqual(avi.tags['video_codec'], 'ffh264')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'ogg')
def test_capture(self):
"""test capture with some small movie"""
avi = Video("mocks/m.avi")
filename = avi.capture()
self.assertTrue(filename != None)
self.assertTrue(os.path.exists(filename))
file_size = os.stat(filename)[6]
self.assertEqual(file_size, 9077)
os.unlink(filename)
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
"""
device (cd, dvd) helper
"""
import os
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
if dev != None:
try:
a = open(dev,"rb")
a.seek(32808)
b = a.read(32).strip()
a.close()
except:
return None
return b
return None
def volmount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for i in inf:
i.strip()
return i
else:
return 'ok'
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for error in inf:
error.strip()
if error.strip()[:7] == 'umount:':
return error.strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device, txt1, mount_point, txt2, filesystem, options = parts
if device == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess mountpoint from fstab"""
fstab = open("/etc/fstab")
for line in fstab.readlines():
a = line.split()
try:
if a[1] == mntp and a[0][0] != '#':
fstab.close()
return a[0]
except:
pass
fstab.close()
return None
def eject_cd(eject_app, cd):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in,_out,_err = os.popen3("%s %s" % (eject_app, cd))
inf = _err.readlines()
error = ''
for error in inf:
error.strip()
if error !='':
return error
return 'ok'
return "Eject program not specified"

View File

@@ -1,112 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
"""
device (cd, dvd) helper
"""
import os
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
if dev != None:
try:
a = open(dev,"rb")
a.seek(32808)
b = a.read(32).strip()
a.close()
except:
return None
return b
return None
def volmount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for i in inf:
i.strip()
return i
else:
return 'ok'
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for error in inf:
error.strip()
if error.strip()[:7] == 'umount:':
return error.strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device, txt1, mount_point, txt2, filesystem, options = parts
if device == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess mountpoint from fstab"""
fstab = open("/etc/fstab")
for line in fstab.readlines():
a = line.split()
try:
if a[1] == mntp and a[0][0] != '#':
fstab.close()
return a[0]
except:
pass
fstab.close()
return None
def eject_cd(eject_app, cd):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in,_out,_err = os.popen3("%s %s" % (eject_app, cd))
inf = _err.readlines()
error = ''
for error in inf:
error.strip()
if error !='':
return error
return 'ok'
return "Eject program not specified"

View File

@@ -24,11 +24,11 @@
from gtkmvc import View
import os.path
import utils.globals
import lib.globs
class ConfigView(View):
"""Preferences window from glade file """
GLADE = os.path.join(utils.globals.GLADE_DIR, "config.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "config.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)

View File

@@ -25,7 +25,7 @@
import gtk
import gobject
import os
import utils.globals
import lib.globs
class Qst(object):
"""Show simple dialog for questions
@@ -128,7 +128,7 @@ class InputDiskLabel(object):
"""Sepcific dialog for quering user for a disc label"""
def __init__(self, label=""):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.label = ""
if label!= None:
self.label = label
@@ -148,7 +148,7 @@ class InputNewName(object):
"""Sepcific dialog for quering user for a disc label"""
def __init__(self, name=""):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.label = ""
self.name = name
@@ -169,7 +169,7 @@ class PointDirectoryToAdd(object):
URI="file://"+os.path.abspath(os.path.curdir)
def __init__(self,volname='',dirname=''):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.gladexml = gtk.glade.XML(self.gladefile, "addDirDialog")
self.volname = self.gladexml.get_widget("dirvolname")
self.volname.set_text(volname)
@@ -257,15 +257,15 @@ class SelectDirectory(object):
dialog.destroy()
return retval
class ChooseDBFilename(object):
"""Sepcific dialog for quering user for selecting filename for database"""
class ChooseFilename(object):
"""Dialog for quering user for selecting filename"""
URI=None
def __init__(self, path=None):
def __init__(self, path=None, title=''):
self.path = path
self.dialog = gtk.FileChooserDialog(
title="Save catalog as...",
title="",
action=gtk.FILE_CHOOSER_ACTION_SAVE,
buttons=(
gtk.STOCK_CANCEL,
@@ -276,7 +276,44 @@ class ChooseDBFilename(object):
self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
self.dialog.set_default_response(gtk.RESPONSE_OK)
self.dialog.set_do_overwrite_confirmation(True)
self.dialog.set_title('Save catalog to file...')
self.dialog.set_title(title)
f = gtk.FileFilter()
f.set_name("Catalog files")
f.add_pattern("*.sqlite")
f.add_pattern("*.sqlite.bz2")
self.dialog.add_filter(f)
f = gtk.FileFilter()
f.set_name("All files")
f.add_pattern("*.*")
self.dialog.add_filter(f)
def run(self):
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)
response = self.dialog.run()
if response == gtk.RESPONSE_OK:
filename = self.dialog.get_filename()
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
else:
self.dialog.destroy()
return None
pass
class ChooseDBFilename(ChooseFilename):
"""Sepcific dialog for quering user for selecting filename for database"""
URI=None
def __init__(self, path=None):
ChooseFilename.__init__(self)
self.dialog.set_title('Save catalog as...')
f = gtk.FileFilter()
f.set_name("Catalog files")
@@ -298,11 +335,6 @@ class ChooseDBFilename(object):
response = self.dialog.run()
if response == gtk.RESPONSE_OK:
filename = self.dialog.get_filename()
print filename, ' do ',
if filename[-11:].lower() != '.sqlite.bz2' and \
filename[-7:].lower() != '.sqlite':
filename = filename + '.sqlite.bz2'
print filename
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
@@ -454,7 +486,7 @@ class StatsDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, values={}):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.values = values
def run(self):
@@ -497,7 +529,7 @@ class TagsDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
def run(self):
gladexml = gtk.glade.XML(self.gladefile, "tagsDialog")
@@ -516,7 +548,7 @@ class TagsRemoveDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, tag_dict=None):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.tag_dict = tag_dict
def run(self):
@@ -585,7 +617,7 @@ class EditDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, values={}):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.values = values
def run(self):

View File

@@ -23,14 +23,14 @@
# -------------------------------------------------------------------------
import os.path
import utils.globals
import lib.globs
from gtkmvc import View
class MainView(View):
"""This handles only the graphical representation of the
application. The widgets set is loaded from glade file"""
GLADE = os.path.join(utils.globals.GLADE_DIR, "main.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "main.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)

View File

@@ -24,12 +24,12 @@
from gtkmvc import View
import os.path
import utils.globals
import lib.globs
class SearchView(View):
"""Search window from glade file """
GLADE = os.path.join(utils.globals.GLADE_DIR, "search.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "search.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)