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

9 Commits

Author SHA1 Message Date
c257d6ceeb Fix the tests. 2022-09-25 16:16:26 +02:00
28499868d2 Fix imports in video module, correct path for images 2022-09-25 16:15:15 +02:00
5f13fd7d7a Fix tox settings 2022-09-25 16:14:10 +02:00
a1a17158bb Moved pygtktalog to pycatalog.
Also, clean up setup things and imports.
2022-09-25 15:59:41 +02:00
10e7e87031 Make code py3 compatible. 2022-09-25 15:55:03 +02:00
3141add678 Another portion of fixes.
In this patch, most notable changes are:
- use python3 exclusively for tox
- fix up broken tests
- a couple of style issues fixes here and there
2019-02-24 19:53:35 +01:00
dadeebe8a1 Removing outdated pavement script
All tasks regarding build distro packages should be recreated either
using setup.py/distutils or paver again, but for now, there is no way to
have pavement at its current state.
2019-02-24 19:50:35 +01:00
6c6f01781a Several small fixes, mostly for style issues 2019-02-24 18:25:34 +01:00
07690f9c94 Added basic Python3 support 2019-02-24 13:21:51 +01:00
24 changed files with 268 additions and 899 deletions

View File

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

View File

@@ -1,132 +1,74 @@
pyGTKtalog
pycatalog
==========
Pygtktalog is Linux/FreeBSD program for indexing CD, DVD, BR or directories on
filesystem. It is similar to `gtktalog`_ or `gwhere`_. There is no coincidence
in name of application, because it's meant to be replacement (in some way) for
gtktalog, which seems to be dead project for years.
Pycatalog is a commandline Linux/FreeBSD program for indexing CD, DVD, BR or
directories on filesystem. It is similar to `gtktalog`_ or `gwhere`_. There is
no coincidence in name of application, because it's meant to be replacement
(in some way) for gtktalog, which seems to be dead project for years.
Current version is 2.0.
Note, that even if it share same code base with pyGTKtalog, which was meant to
be desktop application, now pycatalog is pure console app, just for use with
commandline. You can find last version of pyGTKtalog under ``pyGTKtalog``
branch, although bear in mind, that it was written with `python 2.7`_ and
pyGTK_, which both are dead now.
FEATURES
Current version is 3.0.
Features
--------
* Scan for files in selected media
* Support for grouping files depending on file name (expected patterns in file
names)
* Get/generate thumbnails from EXIF and other images
* Store selected EXIF tags
* Add/edit description and notes
* Fetch comments for images made in `gThumb`_
* Add/remove unlimited images to any file or directory
* `Tagging files`_
* And more :)
Frontends
---------
New version of pyGTKtalog was meant to use multiple interfaces.
#. First for the new incarnation of pyGTKtalog is… command line tool for
accessing catalog dbs. With ``cmdcatalog.py`` it's possible to:
* create new catalog
* update it
* list
* find files
* fsck (for maintenance for orphaned thumbs/images)
#. ``gtktalog.py``. This is written from scratch frontend in pygtk. Still work
in progress.
Requirements
------------
pyGTKtalog requires python and following libraries:
pycatalog requires python and following libraries:
* `python 2.7`_
* `sqlalchemy 1.0`_
* `pygtk 2.24`_ (only for ``gtktalog.py``)
* `pillow`_ for image manipulation
* `python 3.10`_ and up
* `sqlalchemy 1.4`_
* `exifread`_ for parse EXIF information
It may work on other (lower) version of libraries, and it should work with
higher versions of libraries, although it will not work on Python 3 yet, nor
GTK3.
pyGTKtalog extensively uses external programs in unix spirit, however there is
Pycatalog extensively uses external programs in unix spirit, however there is
small possibility of using it Windows (probably with limitations) and quite big
possibility to run it on other sophisticated unix-like systems (i.e.
BeOS/ZETA/Haiku, QNX or MacOSX).
Programs that are used:
* ``mencoder`` (provided by `mplayer`_ package)
* ``montage``, ``convert`` from `ImageMagick`_
* ``midentify`` (provided by `mplayer`_ package)
For development process following programs are used:
* `gettext`_
* `intltool`_
* `nose`_
* `coverage`_
* `paver`_
* `tox`_
INSTALATION
Instalation
-----------
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 pycatalog and simply run::
$ paver run
That's it. Alternatively, if you like to put it in more system wide place, all
you have to do is:
#. put pyGTKtalog directory into your destination of choice (/usr/local/share,
#. put pycatalog directory into your destination of choice (/usr/local/share,
/opt or ~/ is typical bet)
#. copy pyGTKtalog shell script to /usr/bin, /usr/local/bin or in
#. copy pycatalog shell script to /usr/bin, /usr/local/bin or in
other place, where PATH variable is pointing or you feel like.
#. then modify pyGTKtalog line 6 to match right ``pygtktalog.py`` directory
#. then modify pycatalog line 6 to match right ``pycatalog.py`` directory
Then, just run pyGTKtalog script.
Technical details
-----------------
Catalog file is plain sqlite database (optionally compressed with bzip2). All
images are stored in location pointed by db entry in ``config`` table - it is
assumed, that images directory will be placed within the root directory, where
the main db lies.
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.
DEVELOPMENT
-----------
Several tools has been used to develop pyGTKtalog.
Paver
^^^^^
I've choose `Paver`_ as make equivalent. Inside main project directory there is
``pavement.py`` script, which provides several tasks, that can be helpful in a work
with sources. Paver is also used to generate standard ``setup.py``.
Then, just run pycatalog script.
LICENSE
=======
@@ -137,18 +79,12 @@ file in top-level directory.
.. _coverage: http://nedbatchelder.com/code/coverage/
.. _exifread: https://github.com/ianare/exif-py
.. _gettext: http://www.gnu.org/software/gettext/gettext.html
.. _gthumb: http://gthumb.sourceforge.net
.. _gtktalog: http://www.nongnu.org/gtktalog/
.. _gwhere: http://www.gwhere.org/home.php3
.. _imagemagick: http://imagemagick.org/script/index.php
.. _intltool: http://www.gnome.org/
.. _mplayer: http://mplayerhq.hu
.. _nose: http://code.google.com/p/python-nose/
.. _paver: https://pythonhosted.org/paver/
.. _pillow: https://python-pillow.org/
.. _pygtk 2.24: http://www.pygtk.org
.. _python 2.7: http://www.python.org/
.. _sqlalchemy 1.0: http://www.sqlalchemy.org
.. _python 3.10: http://www.python.org/
.. _sqlalchemy 1.4: http://www.sqlalchemy.org
.. _tagging files: http://en.wikipedia.org/wiki/tag_%28metadata%29
.. _tox: https://testrun.org/tox

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', 'tests/.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 tests" % _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

78
scripts/cmdcatalog.py → pycatalog/__init__.py Executable file → Normal file
View File

@@ -1,20 +1,18 @@
#!/usr/bin/env python
"""
Fast and ugly CLI interface for pyGTKtalog
Fast and ugly CLI interface
"""
import argparse
import errno
import os
import re
import sys
from sqlalchemy import or_
from pygtktalog import scan
from pygtktalog import misc
from pygtktalog import dbobjects as dbo
from pygtktalog.dbcommon import connect, Session
from pygtktalog import logger
from pycatalog import scan
from pycatalog import misc
from pycatalog import dbobjects as dbo
from pycatalog.dbcommon import connect, Session
from pycatalog import logger
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
@@ -24,6 +22,7 @@ BOLD_SEQ = '\033[1m'
LOG = logger.get_logger(__name__)
def colorize(txt, color):
"""Pretty print with colors to console."""
color_map = {'black': BLACK,
@@ -40,7 +39,7 @@ def colorize(txt, color):
def asserdb(func):
def wrapper(args):
if not os.path.exists(args.db):
print colorize("File `%s' does not exists!" % args.db, 'red')
print(colorize("File `%s' does not exists!" % args.db, 'red'))
sys.exit(1)
func(args)
return wrapper
@@ -151,7 +150,7 @@ class Iface(object):
node = self.root
msg = "Content of path `/':"
print colorize(msg, 'white')
print(colorize(msg, 'white'))
if recursive:
items = self._walk(node)
@@ -168,7 +167,7 @@ class Iface(object):
else:
filenames = sorted(items.keys())
print '\n'.join(filenames)
print('\n'.join(filenames))
def update(self, path, dir_to_update=None):
"""
@@ -179,8 +178,8 @@ class Iface(object):
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
node = self._resolve_path(path)
if node == self.root:
print colorize('Cannot update entire db, since root was provided '
'as path.', 'red')
print(colorize('Cannot update entire db, since root was provided '
'as path.', 'red'))
return
if not dir_to_update:
@@ -189,8 +188,8 @@ class Iface(object):
if not os.path.exists(dir_to_update):
raise OSError("Path to updtate doesn't exists: %s", dir_to_update)
print colorize("Updating node `%s' against directory "
"`%s'" % (path, dir_to_update), 'white')
print(colorize("Updating node `%s' against directory "
"`%s'" % (path, dir_to_update), 'white'))
if not self.dry_run:
scanob = scan.Scan(dir_to_update)
# scanob.update_files(node.id)
@@ -215,8 +214,8 @@ class Iface(object):
self.sess.add(config)
self.sess.commit()
print colorize("Creating new db against directory `%s'" % dir_to_add,
'white')
print(colorize("Creating new db against directory `%s'" % dir_to_add,
'white'))
if not self.dry_run:
if data_dir == ':same_as_db:':
misc.calculate_image_path(None, True)
@@ -234,7 +233,7 @@ class Iface(object):
if not os.path.exists(dir_to_add):
raise OSError("Path to add doesn't exists: %s", dir_to_add)
print colorize("Adding directory `%s'" % dir_to_add, 'white')
print(colorize("Adding directory `%s'" % dir_to_add, 'white'))
if not self.dry_run:
scanob = scan.Scan(dir_to_add)
scanob.add_files()
@@ -273,30 +272,29 @@ class Iface(object):
result = []
for word in search_words:
phrase = u'%%%s%%' % word.decode('utf-8')
phrase = u'%%%s%%' % word
query = query.filter(dbo.File.filename.like(phrase))
for item in query.all():
result.append(self._get_full_path(item))
if not result:
print "No results for `%s'" % ' '.join(search_words)
print("No results for `%s'" % ' '.join(search_words))
return
result.sort()
for item in result:
print self._annotate(item, search_words)
print(self._annotate(item, search_words))
def fsck(self):
"""Fsck orphaned images/thumbs"""
image_path = self.sess.query(dbo.Config).\
filter(dbo.Config.key=='image_path').one().value
image_path = (self.sess.query(dbo.Config)
.filter(dbo.Config.key == 'image_path')).one().value
if image_path == ':same_as_db:':
image_path = misc.calculate_image_path(None, False)
files_to_remove = []
obj_to_remove = []
# remove images/thumbnails which doesn't have file relation
for name, obj in (("images", dbo.Image),
@@ -318,20 +316,20 @@ class Iface(object):
fname).lstrip('/')
if '_t' in fname:
obj = self.sess.query(dbo.Thumbnail)\
.filter(dbo.Thumbnail.filename==fname_).all()
obj = (self.sess.query(dbo.Thumbnail)
.filter(dbo.Thumbnail.filename == fname_)).all()
if obj:
continue
obj = self.sess.query(dbo.Image)\
.filter(dbo.Image.filename==\
fname_.replace('_t.', '.')).all()
obj = (self.sess.query(dbo.Image)
.filter(dbo.Image.filename ==
fname_.replace('_t.', '.'))).all()
if obj:
continue
else:
obj = self.sess.query(dbo.Image)\
.filter(dbo.Image.filename==fname_).all()
obj = (self.sess.query(dbo.Image)
.filter(dbo.Image.filename == fname_)).all()
if obj:
continue
@@ -342,9 +340,9 @@ class Iface(object):
sys.stdout.flush()
if self.dry_run:
print "Following files are not associated to any items in the DB:"
print("Following files are not associated to any items in the DB:")
for filename in sorted(files_to_remove):
print filename
print(filename)
self.sess.rollback()
else:
_remove_files(image_path, files_to_remove)
@@ -417,13 +415,14 @@ def add_dir(args):
obj.close()
@asserdb
def create_db(args):
"""List"""
__import__('pdb').set_trace()
obj = Iface(args.db, args.pretend, args.debug)
obj.create(args.dir_to_add, args.imagedir)
obj.close()
@asserdb
def search(args):
"""Find"""
@@ -431,6 +430,7 @@ def search(args):
obj.find(args.search_words)
obj.close()
@asserdb
def cleanup(args):
"""Cleanup"""
@@ -439,7 +439,6 @@ def cleanup(args):
obj.close()
def main():
"""Main"""
parser = argparse.ArgumentParser()
@@ -472,7 +471,7 @@ def main():
create.add_argument('dir_to_add')
create.add_argument('-i', '--imagedir', help="Directory where to put "
"images for the database. Popular, but deprecated "
"choice is `~/.pygtktalog/images'. Currnet default "
"choice is `~/.pycatalog/images'. Current default "
"is special string `:same_as_db:' which will try to "
"create directory with the same name as the db with "
"data suffix", default=':same_as_db:')
@@ -510,7 +509,12 @@ def main():
fsck.set_defaults(func=cleanup)
args = parser.parse_args()
args.func(args)
if 'func' in args:
args.func(args)
else:
parser.print_help()
if __name__ == '__main__':
main()

View File

@@ -9,14 +9,9 @@ from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from pygtktalog.logger import get_logger
from pycatalog.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)

View File

@@ -12,10 +12,10 @@ from sqlalchemy import Column, Table, Integer, Text
from sqlalchemy import DateTime, ForeignKey, Sequence
from sqlalchemy.orm import relation, backref
from pygtktalog.dbcommon import Base
from pygtktalog.thumbnail import ThumbCreator
from pygtktalog.logger import get_logger
from pygtktalog.misc import mk_paths
from pycatalog.dbcommon import Base
from pycatalog.thumbnail import ThumbCreator
from pycatalog.logger import get_logger
from pycatalog.misc import mk_paths
LOG = get_logger(__name__)
@@ -63,8 +63,7 @@ class File(Base):
self.source = src
def __repr__(self):
return "<File('%s', %s)>" % (self.filename.encode('utf-8'),
str(self.id))
return "<File('%s', %s)>" % (self.filename, str(self.id))
def get_all_children(self):
"""
@@ -149,8 +148,8 @@ class Thumbnail(Base):
if not os.path.exists(os.path.join(img_path, *new_name)):
shutil.move(thumb, os.path.join(img_path, *new_name))
else:
LOG.info("Thumbnail already exists (%s: %s)" % \
(fname, "/".join(new_name)))
LOG.info("Thumbnail already exists (%s: %s)",
fname, "/".join(new_name))
os.unlink(thumb)
def __repr__(self):
@@ -203,7 +202,6 @@ class Image(Base):
else:
LOG.info("Thumbnail already generated %s" % "/".join(new_name))
def get_copy(self):
"""
Create the very same object as self with exception of id field

View File

@@ -27,6 +27,7 @@ COLORS = {'WARNING': YELLOW,
'CRITICAL': WHITE,
'ERROR': RED}
def cprint(txt, color):
color_map = {"black": BLACK,
"red": RED,
@@ -36,7 +37,7 @@ def cprint(txt, color):
"magenta": MAGENTA,
"cyan": CYAN,
"white": WHITE}
print COLOR_SEQ % (30 + color_map[color]) + txt + RESET_SEQ
print(COLOR_SEQ % (30 + color_map[color]) + txt + RESET_SEQ)
class DummyFormater(logging.Formatter):
@@ -58,13 +59,11 @@ class ColoredFormatter(logging.Formatter):
record.levelname = levelname_color
return logging.Formatter.format(self, record)
log_obj = None
#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='INFO', to_file=True, to_console=True):
# def get_logger(module_name, level='DEBUG', to_file=True, to_console=True):
#def get_logger(module_name, level='DEBUG', to_file=False):
"""
Prepare and return log object. Standard formatting is used for all logs.
Arguments:
@@ -72,22 +71,20 @@ def get_logger(module_name, level='INFO', to_file=True, to_console=True):
@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
.pycatalog 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 = os.path.join(os.path.expanduser("~"), ".pycatalog", "app.log")
log = logging.getLogger(module_name)
log.setLevel(LEVEL[level])
if to_console:
#path = "/dev/null"
console_handler = logging.StreamHandler(sys.stderr)
console_formatter = ColoredFormatter("%(filename)s:%(lineno)s - "
"%(levelname)s - %(message)s")
"%(levelname)s - %(message)s")
console_handler.setFormatter(console_formatter)
log.addHandler(console_handler)

View File

@@ -9,8 +9,8 @@ import os
import errno
from zlib import crc32
import pygtktalog.dbcommon
from pygtktalog.logger import get_logger
import pycatalog.dbcommon
from pycatalog.logger import get_logger
LOG = get_logger(__name__)
@@ -29,10 +29,11 @@ def float_to_string(float_length):
sec = int(float_length)
return "%02d:%02d:%02d" % (hour, minutes, sec)
def calculate_image_path(dbpath=None, create=False):
"""Calculate image path out of provided path or using current connection"""
if not dbpath:
dbpath = pygtktalog.dbcommon.DbFilename
dbpath = pycatalog.dbcommon.DbFilename
if dbpath == ":memory:":
raise OSError("Cannot create image path out of in-memory db!")
@@ -50,7 +51,7 @@ def calculate_image_path(dbpath=None, create=False):
if not os.path.exists(images_dir):
try:
os.mkdir(images_dir)
except OSError, err:
except OSError as err:
if err.errno != errno.EEXIST:
raise
elif not os.path.exists(images_dir):
@@ -58,9 +59,10 @@ def calculate_image_path(dbpath=None, create=False):
return os.path.abspath(images_dir)
def mk_paths(fname, img_path):
"""Make path for provided pathname by calculating crc32 out of file"""
with open(fname) as fobj:
with open(fname, 'r+b') as fobj:
new_path = "%x" % (crc32(fobj.read(10*1024*1024)) & 0xffffffff)
new_path = [new_path[i:i + 2] for i in range(0, len(new_path), 2)]

View File

@@ -1,11 +1,12 @@
"""
Project: pyGTKtalog
Description: pyGTK common utility functions
Type: tility
Type: utility
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.
@@ -22,4 +23,3 @@ def get_tv_item_under_cursor(treeview):
item_id = model.get_value(tm_iter, 0)
return item_id
return None

View File

@@ -6,16 +6,15 @@
Created: 2011-03-27
"""
import os
import sys
import re
from datetime import datetime
import mimetypes
import pygtktalog.misc
from pygtktalog.dbobjects import File, Image, Thumbnail, Config, TYPE
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
from pygtktalog.video import Video
import pycatalog.misc
from pycatalog.dbobjects import File, Image, Thumbnail, Config, TYPE
from pycatalog.dbcommon import Session
from pycatalog.logger import get_logger
from pycatalog.video import Video
LOG = get_logger(__name__)
@@ -26,7 +25,6 @@ RE_FN_START = re.compile(r'(?P<fname_start>'
r'(\[[A-Fa-f0-9]{8}\])\..*')
class NoAccessError(Exception):
"""No access exception"""
pass
@@ -114,7 +112,7 @@ class Scan(object):
# number of objects to retrieve at once. Limit is 999. Let's do a
# little bit below.
num = 900
steps = len(all_ids) / num + 1
steps = len(all_ids) // num + 1
for step in range(steps):
all_obj.extend(self._session
.query(File)
@@ -155,7 +153,7 @@ class Scan(object):
# in case of such, better get me a byte string. It is not perfect
# though, since it WILL crash if the update_path would contain some
# unconvertable characters.
update_path = update_path.encode("utf-8")
update_path = update_path
# refresh objects
LOG.debug("Refreshing objects")
@@ -181,8 +179,8 @@ class Scan(object):
# self._session.merge(self._files[0])
LOG.debug("Deleting objects whitout parent: %s",
str(self._session.query(File)
.filter(File.parent==None).all()))
self._session.query(File).filter(File.parent==None).delete()
.filter(File.parent.is_(None)).all()))
self._session.query(File).filter(File.parent.is_(None)).delete()
self._session.commit()
return self._files
@@ -199,8 +197,7 @@ class Scan(object):
'.ogm': 'video',
'.ogv': 'video'}
fp = os.path.join(fobj.filepath.encode(sys.getfilesystemencoding()),
fobj.filename.encode(sys.getfilesystemencoding()))
fp = os.path.join(fobj.filepath, fobj.filename)
mimeinfo = mimetypes.guess_type(fp)
if mimeinfo[0]:
@@ -208,7 +205,7 @@ class Scan(object):
ext = os.path.splitext(fp)[1]
if mimeinfo and mimeinfo in mimedict.keys():
if mimeinfo and mimeinfo in mimedict:
mimedict[mimeinfo](fobj, fp)
elif ext and ext in extdict:
mimedict[extdict[ext]](fobj, fp)
@@ -287,13 +284,8 @@ class Scan(object):
"""
fullpath = os.path.join(path, fname)
fname = fname.decode(sys.getfilesystemencoding(),
errors="replace")
path = path.decode(sys.getfilesystemencoding(),
errors="replace")
if ftype == TYPE['link']:
fname = fname + " -> " + os.readlink(fullpath).decode('utf-8')
fname = fname + " -> " + os.readlink(fullpath)
fob = {'filename': fname,
'path': path,
@@ -378,7 +370,7 @@ class Scan(object):
LOG.info("Scanning `%s' [%s/%s]", fullpath, self.current_count,
self.files_count)
root, dirs, files = os.walk(fullpath).next()
root, dirs, files = next(os.walk(fullpath))
for fname in files:
fpath = os.path.join(root, fname)
extension = os.path.splitext(fname)[1]
@@ -423,7 +415,7 @@ class Scan(object):
for dirname in dirs:
dirpath = os.path.join(root, dirname)
if not os.access(dirpath, os.R_OK|os.X_OK):
if not os.access(dirpath, os.R_OK | os.X_OK):
LOG.info("Cannot access directory %s", dirpath)
continue
@@ -481,12 +473,12 @@ class Scan(object):
def _set_image_path(self):
"""Get or calculate the images path"""
image_path = self._session.query(Config) \
.filter(Config.key=="image_path").one()
image_path = (self._session.query(Config)
.filter(Config.key == "image_path")).one()
if image_path.value == ":same_as_db:":
image_path = pygtktalog.misc.calculate_image_path()
image_path = pycatalog.misc.calculate_image_path()
else:
image_path = pygtktalog.misc.calculate_image_path(image_path.value)
image_path = pycatalog.misc.calculate_image_path(image_path.value)
self.img_path = image_path
@@ -507,4 +499,3 @@ def _get_dirsize(path):
os.path.join(root, fname))
LOG.debug("_get_dirsize, %s: %d", path, size)
return size

View File

@@ -13,7 +13,7 @@ import shutil
from PIL import Image
import exifread
from pygtktalog.logger import get_logger
from pycatalog.logger import get_logger
LOG = get_logger(__name__)
@@ -105,7 +105,7 @@ class ThumbCreator(object):
"""
try:
image_thumb = Image.open(self.filename).convert('RGB')
except:
except Exception:
return None
it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y:

View File

@@ -6,14 +6,15 @@
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-04
"""
import math
import os
import shutil
from tempfile import mkdtemp, mkstemp
import math
import tempfile
from PIL import Image
from pygtktalog.misc import float_to_string
from pygtktalog.logger import get_logger
from pycatalog.misc import float_to_string
from pycatalog.logger import get_logger
LOG = get_logger("Video")
@@ -49,7 +50,7 @@ class Video(object):
'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?
# TODO: what about audio/subtitle language/existence?
for key in output:
if key in attrs:
@@ -58,9 +59,9 @@ class Video(object):
if 'length' in self.tags and self.tags['length'] > 0:
start = self.tags.get('start', 0)
length = self.tags['length'] - start
hours = length / 3600
hours = length // 3600
seconds = length - hours * 3600
minutes = seconds / 60
minutes = seconds // 60
seconds -= minutes * 60
length_str = "%02d:%02d:%02d" % (hours, minutes, seconds)
self.tags['duration'] = length_str
@@ -92,16 +93,16 @@ class Video(object):
if scale < 1:
return None
no_pictures = self.tags['length'] / scale
no_pictures = self.tags['length'] // scale
if no_pictures > 8:
no_pictures = (no_pictures / 8) * 8 # only multiple of 8, please.
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")
tempdir = tempfile.mkdtemp()
file_desc, image_fn = tempfile.mkstemp(suffix=".jpg")
os.close(file_desc)
self._make_captures(tempdir, no_pictures)
self._make_montage(tempdir, image_fn, no_pictures)
@@ -113,16 +114,16 @@ class Video(object):
"""
Return formatted tags as a string
"""
out_tags = u''
out_tags = ''
if 'container' in self.tags:
out_tags += u"Container: %s\n" % self.tags['container']
out_tags += "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'])
out_tags += "Resolution: %sx%s\n" % (self.tags['width'],
self.tags['height'])
if 'duration' in self.tags:
out_tags += u"Duration: %s\n" % self.tags['duration']
out_tags += "Duration: %s\n" % self.tags['duration']
if 'video_codec' in self.tags:
out_tags += "Video codec: %s\n" % self.tags['video_codec']
@@ -178,20 +179,21 @@ class Video(object):
@directory - full output directory name
@no_pictures - number of pictures to take
"""
step = float(self.tags['length'] / (no_pictures + 1))
step = 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 -mc 0 -vf-clr -vo jpeg:outdir=\"%s\" -ss %s" \
" -frames 1 2>/dev/null"
cmd = ('mplayer "%s" -ao null -brightness 0 -hue 0 '
'-saturation 0 -contrast 0 -mc 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):
except IOError as exc:
errno, strerror = exc.args
LOG.error('error capturing file from movie "%s" at position '
'%s. Errors: %s, %s', self.filename, time, errno,
strerror)
@@ -206,7 +208,7 @@ class Video(object):
@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"); \
pycatalog.video import Video; v = Video("/home/gryf/t/a.avi"); \
v.capture()'
1 loops, best of 1: 18.8 sec per loop
"""
@@ -216,13 +218,13 @@ class Video(object):
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:
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)
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))
@@ -231,10 +233,10 @@ class Video(object):
ifn_list = os.listdir(directory)
ifn_list.sort()
img_list = [Image.open(os.path.join(directory, fn)).resize(dim) \
for fn in ifn_list]
img_list = [Image.open(os.path.join(directory, fn)).resize(dim)
for fn in ifn_list]
rows = no_pictures / row_length
rows = no_pictures // row_length
cols = row_length
isize = (cols * dim[0] + cols + 1,
rows * dim[1] + rows + 1)
@@ -250,7 +252,7 @@ class Video(object):
bbox = (left, upper, right, lower)
try:
img = img_list.pop(0)
except:
except Exception:
break
inew.paste(img, bbox)
inew.save(image_fn, 'JPEG')
@@ -271,7 +273,7 @@ class Video(object):
"""
try:
return int(chain.split(".")[0])
except:
except Exception:
return 0
def __str__(self):

View File

@@ -1,58 +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__ = "2.0.0"
__appname__ = "pyGTKtalog"
__copyright__ = u"\u00A9 Roman 'gryf' Dobosz"
__summary__ = "%s is simple tool for managing file collections." % __appname__
__web__ = "http://github.com/gryf/pygtktalog"
import os
import sys
import locale
import gettext
import __builtin__
from pygtktalog.logger import get_logger
__all__ = ['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,248 +0,0 @@
# -*- coding: utf-8 -*-
import gtk
from pygtktalog import logger
UI = """
<ui>
<menubar name="MenuBar">
<menu action="File">
<menuitem action="New"/>
<menuitem action="Open"/>
<menuitem action="Save"/>
<menuitem action="Save As"/>
<separator/>
<menuitem action="Import"/>
<menuitem action="Export"/>
<separator/>
<menuitem action="Recent"/>
<separator/>
<menuitem action="Quit"/>
</menu>
<menu action="Edit">
<menuitem action="Delete"/>
<separator/>
<menuitem action="Find"/>
<separator/>
<menuitem action="Preferences"/>
</menu>
<menu action="Catalog">
<menuitem action="Add_CD"/>
<menuitem action="Add_Dir"/>
<separator/>
<menuitem action="Delete_all_images"/>
<menuitem action="Delete_all_thumbnails"/>
<menuitem action="Save_all_images"/>
<separator/>
<menuitem action="Catalog_statistics"/>
<separator/>
<menuitem action="Cancel"/>
</menu>
<menu action="View">
<menuitem action="Toolbar"/>
<menuitem action="Statusbar"/>
</menu>
<menu action="Help">
<menuitem action="About"/>
</menu>
</menubar>
<toolbar name="ToolBar">
<toolitem action="New"/>
<toolitem action="Open"/>
<toolitem action="Save"/>
<separator/>
<toolitem action="Add_CD"/>
<toolitem action="Add_Dir"/>
<toolitem action="Find"/>
<separator/>
<toolitem action="Cancel"/>
<toolitem action="Quit"/>
<toolitem action="Debug"/>
</toolbar>
</ui>
"""
LOG = logger.get_logger(__name__)
LOG.setLevel(2)
class ConnectedWidgets(object):
"""grouped widgets"""
def __init__(self, toolbar, menu):
super(ConnectedWidgets, self).__init__()
self.toolbar = toolbar
self.menu = menu
def hide(self):
self.toolbar.hide()
self.menu.hide()
def show(self):
self.toolbar.show()
self.menu.show()
def set_sensitive(self, state):
self.toolbar.set_sensitive(state)
self.menu.set_sensitive(state)
class MainWindow(object):
def __init__(self, debug=False):
"""Initialize window"""
LOG.debug("initialize")
self.window = gtk.Window()
self.window.set_default_size(650, -1)
self.window.set_title("pygtktalog")
self.window.connect("delete-event", self.on_quit)
self.recent = None
self.toolbar = None
self.statusbar = None
self.cancel = None
self.debug = None
vbox = gtk.VBox(False, 0)
self._setup_menu_toolbar(vbox)
# TODO:
# 1. toolbar with selected tags
# 2. main view (splitter)
# 3. treeview with tag cloud (left split)
# 4. splitter (right split)
# 5. file list (upper split)
# 6. details w images and thumb (lower split)
# 7. status bar (if needed…)
hbox = gtk.HBox(False, 0)
vbox.add(hbox)
self.window.add(vbox)
self.window.show_all()
self.debug.hide()
def fake_recent(self):
recent_menu = gtk.Menu()
for i in "one two techno foo bar baz".split():
item = gtk.MenuItem(i)
item.connect_object("activate", self.on_recent,
"/some/fake/path/" + i)
recent_menu.append(item)
item.show()
self.recent.set_submenu(recent_menu)
def _setup_menu_toolbar(self, vbox):
"""Create menu/toolbar using uimanager."""
actions = [('File', None, '_File'),
('New', gtk.STOCK_NEW, '_New', None, 'Create new catalog', self.on_new),
('Open', gtk.STOCK_OPEN, '_Open', None, 'Open catalog file', self.on_open),
('Save', gtk.STOCK_SAVE, '_Save', None, 'Save catalog file', self.on_save),
('Save As', gtk.STOCK_SAVE_AS, '_Save As', None, None, self.on_save),
('Import', None, '_Import', None, None, self.on_import),
('Export', None, '_Export', None, None, self.on_export),
('Recent', None, '_Recent files'),
('Quit', gtk.STOCK_QUIT, '_Quit', None, 'Quit the Program', self.on_quit),
('Edit', None, '_Edit'),
('Delete', gtk.STOCK_DELETE, '_Delete', None, None, self.on_delete),
('Find', gtk.STOCK_FIND, '_Find', None, 'Find file', self.on_find),
('Preferences', gtk.STOCK_PREFERENCES, '_Preferences'),
('Catalog', None, '_Catalog'),
('Add_CD', gtk.STOCK_CDROM, '_Add CD', None, 'Add CD/DVD/BR to catalog'),
('Add_Dir', gtk.STOCK_DIRECTORY, '_Add Dir', None, 'Add directory to catalog'),
('Delete_all_images', None, '_Delete all images'),
('Delete_all_thumbnails', None, '_Delete all thumbnails'),
('Save_all_images', None, '_Save all images…'),
('Catalog_statistics', None, '_Catalog statistics'),
('Cancel', gtk.STOCK_CANCEL, '_Cancel'),
('View', None, '_View'),
('Help', None, '_Help'),
('About', gtk.STOCK_ABOUT, '_About'),
('Debug', gtk.STOCK_DIALOG_INFO, 'Debug')]
toggles = [('Toolbar', None, '_Toolbar'),
('Statusbar', None, '_Statusbar')]
mgr = gtk.UIManager()
accelgrp = mgr.get_accel_group()
self.window.add_accel_group(accelgrp)
agrp = gtk.ActionGroup("Actions")
agrp.add_actions(actions)
agrp.add_toggle_actions(toggles)
mgr.insert_action_group(agrp, 0)
mgr.add_ui_from_string(UI)
help_widget = mgr.get_widget("/MenuBar/Help")
help_widget.set_right_justified(True)
self.recent = mgr.get_widget("/MenuBar/File/Recent")
self.fake_recent()
menubar = mgr.get_widget("/MenuBar")
vbox.pack_start(menubar)
self.toolbar = mgr.get_widget("/ToolBar")
vbox.pack_start(self.toolbar)
menu_cancel = mgr.get_widget('/MenuBar/Catalog/Cancel')
toolbar_cancel = mgr.get_widget('/ToolBar/Cancel')
self.cancel = ConnectedWidgets(toolbar_cancel, menu_cancel)
self.cancel.set_sensitive(False)
self.debug = mgr.get_widget('/ToolBar/Debug')
self.toolbar = mgr.get_widget('/MenuBar/View/Toolbar')
self.statusbar = mgr.get_widget('/MenuBar/View/Statusbar')
def on_new(self, *args, **kwargs):
LOG.debug("On new")
return
def on_open(self, *args, **kwargs):
LOG.debug("On open")
return
def on_save(self, *args, **kwargs):
LOG.debug("On save")
return
def on_save_as(self, *args, **kwargs):
LOG.debug("On save as")
return
def on_import(self, *args, **kwargs):
LOG.debug("On import")
return
def on_export(self, *args, **kwargs):
LOG.debug("On export")
return
def on_recent(self, *args, **kwargs):
LOG.debug("On recent")
print args, kwargs
def on_quit(self, *args, **kwargs):
LOG.debug("on quit")
gtk.main_quit()
def on_delete(self, *args, **kwargs):
LOG.debug("On delete")
return
def on_find(self, *args, **kwargs):
LOG.debug("On find")
return
def on_about(self, event, menuitem):
LOG.debug("about", event, menuitem)
return
def run():
gui = MainWindow()
gtk.mainloop()

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env python
"""
Project: pyGTKtalog
Description: Main gui file launcher
Type: UI
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2016-08-19
"""
import sys
import tempfile
import os
from pygtktalog.dbobjects import File, Config
from pygtktalog.dbcommon import connect, Session
from pygtktalog.gtk2 import gui
class App(object):
"""Main app class"""
def __init__(self, dbname):
"""Initialze"""
self._dbname = None
self.sess = Session()
if dbname:
self._dbname = dbname
self.engine = connect(dbname)
else:
self._create_tmp_db()
self.root = None
self._dbname = dbname
def _create_tmp_db(self):
"""Create temporatry db, untill user decide to save it"""
fdsc, self._tmpdb = tempfile.mkstemp()
os.close(fdsc)
self.engine = connect(self._tmpdb)
self.root = File()
self.root.id = 1
self.root.filename = 'root'
self.root.size = 0
self.root.source = 0
self.root.type = 0
self.root.parent_id = 1
config = Config()
config.key = "image_path"
config.value = ":same_as_db:"
self.sess.add(self.root)
self.sess.add(config)
self.sess.commit()
def run(self):
"""Initialize gui"""
gui.run()
def main():
db = sys.argv if len(sys.argv) == 2 else None
app = App(db)
app.run()
if __name__ == "__main__":
main()

41
setup.cfg Normal file
View File

@@ -0,0 +1,41 @@
[metadata]
name = pycatalog
summary = Catalog application for keeping content list of disks and discs
description_file = README.rst
author = Roman Dobosz
author_email = gryf73@gmail.com
home_page = https://github.com/gryf/pycatalog
license = BSD
keywords = catalog, gwhere, collection
classifier =
Development Status :: 4 - Beta
Environment :: Console
Intended Audience :: End Users/Desktop
License :: OSI Approved :: BSD License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Database
Topic :: Desktop Environment
[install]
record = install.log
[options.entry_points]
console_scripts =
pycatalog = pycatalog:main
[files]
packages =
pycatalog
[options]
install_requires =
pillow
sqlalchemy
[bdist_wheel]
universal = 1

View File

@@ -1,30 +1,5 @@
#!/usr/bin/env python2
"""
Setup for the pyGTKtalog project
"""
from distutils.core import setup
#!/usr/bin/env python
import setuptools
setup(name='pygtktalog',
packages=['pygtktalog'],
version='2.0',
description='Catalog application with GTK interface',
author='Roman Dobosz',
author_email='gryf73@gmail.com',
url='https://github.com/gryf/pygtktalog',
download_url='https://github.com/gryf/pygtktalog.git',
keywords=['catalog', 'gwhere', 'where is it', 'collection', 'GTK'],
requires=['Pillow', 'sqlalchemy'],
scripts=['scripts/cmdcatalog.py'],
classifiers=['Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 2 :: Only',
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Topic :: Multimedia :: Graphics'],
long_description=open('README.rst').read(),
options={'test': {'verbose': False,
'coverage': False}})
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)

View File

@@ -1,4 +1,4 @@
pytest
pytest-cov
pytest-pep8
flake8
coverage

View File

@@ -8,7 +8,7 @@
import unittest
import os
from pygtktalog.dbcommon import connect, Meta, Session, Base
from pycatalog.dbcommon import connect
class TestDataBase(unittest.TestCase):

View File

@@ -8,7 +8,7 @@
import unittest
import os
import pygtktalog.misc as pgtkmisc
import pycatalog.misc as pgtkmisc
class TestMiscModule(unittest.TestCase):

View File

@@ -10,9 +10,9 @@ import shutil
import tempfile
import unittest
from pygtktalog import scan
from pygtktalog.dbobjects import File, Config, Image
from pygtktalog.dbcommon import connect, Session
from pycatalog import scan
from pycatalog.dbobjects import File, Config, Image
from pycatalog.dbcommon import connect, Session
def populate_with_mock_files(dir_):
@@ -23,7 +23,7 @@ def populate_with_mock_files(dir_):
files_no = 0
for file_ in files1:
with open(os.path.join(dir_, file_), "wb") as fobj:
fobj.write("\xde\xad\xbe\xef" * len(file_))
fobj.write(b"\xde\xad\xbe\xef" * len(file_))
files_no += 1
os.symlink(os.path.join(dir_, files1[-1]), os.path.join(dir_, 'link.jpg'))
@@ -32,7 +32,7 @@ def populate_with_mock_files(dir_):
os.mkdir(os.path.join(dir_, 'directory'))
for file_ in files2:
with open(os.path.join(dir_, 'directory', file_), "wb") as fobj:
fobj.write("\xfe\xad\xfa\xce" * len(file_))
fobj.write(b"\xfe\xad\xfa\xce" * len(file_))
files_no += 1
return files_no
@@ -178,8 +178,8 @@ class TestScan(unittest.TestCase):
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)
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])

View File

@@ -7,10 +7,12 @@
"""
import os
import unittest
from unittest import mock
import io
import PIL
from pygtktalog.video import Video
from pycatalog.video import Video
DATA = {"m1.avi": """ID_VIDEO_ID=0
@@ -130,7 +132,7 @@ ID_AUDIO_RATE=22050
ID_AUDIO_NCH=1
ID_AUDIO_CODEC=ffac3
ID_EXIT=EOF""",
"m.wmv":"""ID_AUDIO_ID=1
"m.wmv": """ID_AUDIO_ID=1
ID_VIDEO_ID=2
ID_FILENAME=m.wmv
ID_DEMUXER=asf
@@ -198,6 +200,7 @@ class Readlines(object):
def readlines(self):
return self.data.split('\n')
def mock_popen(command):
key = None
if 'midentify' in command:
@@ -205,22 +208,25 @@ def mock_popen(command):
elif 'jpeg:outdir' in command:
# simulate capture for mplayer
img_dir = command.split('"')[-2]
img = PIL.Image.new('RGBA', (320, 200))
img = PIL.Image.new('RGB', (320, 200))
with open(os.path.join(img_dir, "00000001.jpg"), "wb") as fobj:
img.save(fobj)
return Readlines(key)
os.popen = mock_popen
# os.popen = mock_popen
class TestVideo(unittest.TestCase):
"""test class for retrive midentify script output"""
def test_avi(self):
@mock.patch('os.popen')
def test_avi(self, popen):
"""test mock avi file, should return dict with expected values"""
avi = Video("m.avi")
fname = "m.avi"
popen.return_value = io.StringIO(DATA[fname])
avi = Video(fname)
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
@@ -233,10 +239,13 @@ class TestVideo(unittest.TestCase):
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_avi2(self):
@mock.patch('os.popen')
def test_avi2(self, popen):
"""test another mock avi file, should return dict with expected
values"""
avi = Video("m1.avi")
fname = "m1.avi"
popen.return_value = io.StringIO(DATA[fname])
avi = Video(fname)
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
@@ -249,9 +258,12 @@ class TestVideo(unittest.TestCase):
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_mkv(self):
@mock.patch('os.popen')
def test_mkv(self, popen):
"""test mock mkv file, should return dict with expected values"""
mkv = Video("m.mkv")
fname = "m.mkv"
popen.return_value = io.StringIO(DATA[fname])
mkv = Video(fname)
self.assertTrue(len(mkv.tags) != 0, "result should have lenght > 0")
self.assertEqual(mkv.tags['audio_format'], '8192')
self.assertEqual(mkv.tags['width'], 128)
@@ -264,24 +276,30 @@ class TestVideo(unittest.TestCase):
self.assertEqual(mkv.tags['duration'], '00:00:04')
self.assertTrue(mkv.tags['container'] in ('mkv', 'lavfpref'))
def test_mpg(self):
@mock.patch('os.popen')
def test_mpg(self, popen):
"""test mock mpg file, should return dict with expected values"""
mpg = Video("m.mpg")
fname = "m.mpg"
popen.return_value = io.StringIO(DATA[fname])
mpg = Video(fname)
self.assertTrue(len(mpg.tags) != 0, "result should have lenght > 0")
self.assertFalse(mpg.tags.has_key('audio_format'))
self.assertFalse('audio_format' in mpg.tags)
self.assertEqual(mpg.tags['width'], 128)
self.assertFalse(mpg.tags.has_key('audio_no_channels'))
self.assertFalse('audio_no_channels' in mpg.tags)
self.assertEqual(mpg.tags['height'], 96)
self.assertEqual(mpg.tags['video_format'], '0x10000001')
self.assertFalse(mpg.tags.has_key('lenght'))
self.assertFalse(mpg.tags.has_key('audio_codec'))
self.assertFalse('lenght' in mpg.tags)
self.assertFalse('audio_codec' in mpg.tags)
self.assertEqual(mpg.tags['video_codec'], 'ffmpeg1')
self.assertFalse(mpg.tags.has_key('duration'))
self.assertFalse('duration' in mpg.tags)
self.assertEqual(mpg.tags['container'], 'mpeges')
def test_ogm(self):
@mock.patch('os.popen')
def test_ogm(self, popen):
"""test mock ogm file, should return dict with expected values"""
ogm = Video("m.ogm")
fname = "m.ogm"
popen.return_value = io.StringIO(DATA[fname])
ogm = Video(fname)
self.assertTrue(len(ogm.tags) != 0, "result should have lenght > 0")
self.assertEqual(ogm.tags['audio_format'], '8192')
self.assertEqual(ogm.tags['width'], 160)
@@ -294,9 +312,12 @@ class TestVideo(unittest.TestCase):
self.assertEqual(ogm.tags['duration'], '00:00:04')
self.assertTrue(ogm.tags['container'] in ('ogg', 'lavfpref'))
def test_wmv(self):
@mock.patch('os.popen')
def test_wmv(self, popen):
"""test mock wmv file, should return dict with expected values"""
wmv = Video("m.wmv")
fname = "m.wmv"
popen.return_value = io.StringIO(DATA[fname])
wmv = Video(fname)
self.assertTrue(len(wmv.tags) != 0, "result should have lenght > 0")
self.assertEqual(wmv.tags['audio_format'], '353')
self.assertEqual(wmv.tags['width'], 852)
@@ -309,9 +330,12 @@ class TestVideo(unittest.TestCase):
self.assertEqual(wmv.tags['duration'], '01:17:32')
self.assertEqual(wmv.tags['container'], 'asf')
def test_mp4(self):
@mock.patch('os.popen')
def test_mp4(self, popen):
"""test mock mp4 file, should return dict with expected values"""
mp4 = Video("m.mp4")
fname = "m.mp4"
popen.return_value = io.StringIO(DATA[fname])
mp4 = Video(fname)
self.assertTrue(len(mp4.tags) != 0, "result should have lenght > 0")
self.assertEqual(mp4.tags['audio_format'], 'mp4a')
self.assertEqual(mp4.tags['width'], 720)
@@ -324,21 +348,31 @@ class TestVideo(unittest.TestCase):
self.assertEqual(mp4.tags['duration'], '00:01:09')
self.assertEqual(mp4.tags['container'], 'lavfpref')
def test_capture(self):
@mock.patch('shutil.move')
@mock.patch('pycatalog.video.Image')
@mock.patch('os.listdir')
@mock.patch('shutil.rmtree')
@mock.patch('os.close')
@mock.patch('tempfile.mkstemp')
@mock.patch('tempfile.mkdtemp')
@mock.patch('os.popen')
def test_capture(self, popen, mkdtemp, mkstemp, fclose, rmtree, listdir,
img, move):
"""test capture with some small movie and play a little with tags"""
avi = Video("m.avi")
fname = 'm.avi'
popen.return_value = io.StringIO(DATA[fname])
mkdtemp.return_value = '/tmp'
mkstemp.return_value = (10, 'foo.jpg')
listdir.return_value = ['a.jpg', 'b.jpg', 'c.jpg', 'd.jpg']
avi = Video(fname)
filename = avi.capture()
self.assertTrue(filename != None)
self.assertTrue(os.path.exists(filename))
file_size = os.stat(filename)[6]
self.assertAlmostEqual(file_size/10000.0, 0.151, 0)
os.unlink(filename)
self.assertIsNotNone(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)
@@ -351,9 +385,8 @@ class TestVideo(unittest.TestCase):
avi.tags['width'] = 1025
filename = avi.capture()
self.assertTrue(filename is not None)
os.unlink(filename)
del(avi.tags['length'])
del avi.tags['length']
self.assertTrue(avi.capture() is None)
self.assertTrue(len(str(avi)) > 0)

View File

@@ -1,18 +1,19 @@
[tox]
envlist = cleanup,py27,pep8
envlist = cleanup,py3,pep8
usedevelop = True
[testenv]
basepython = python3
usedevelop=True
setenv = COVERAGE_FILE = .coverage
commands = py.test --cov=pygtktalog --cov-report=term-missing
commands = py.test --cov=pycatalog --cov-report=term-missing
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:pep8]
usedevelop=True
commands = py.test --pep8 -m pep8
commands = flake8
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt