mirror of
https://github.com/gryf/pygtktalog.git
synced 2026-03-27 14:33:34 +01:00
Compare commits
11 Commits
c257d6ceeb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 85ab034a36 | |||
| 4b02641481 | |||
| 51e3bfa441 | |||
| c74174fc8f | |||
| 54c24b18b1 | |||
| 7281f9bbbb | |||
| 002ff724ea | |||
| cd1482e4a1 | |||
| 028571e9c1 | |||
| b284f328b3 | |||
| 01fd964e0d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@ __pycache__/
|
|||||||
tags
|
tags
|
||||||
MANIFEST
|
MANIFEST
|
||||||
.cache
|
.cache
|
||||||
pygtktalog.egg-info
|
pycatalog.egg-info
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pycatalog requires python and following libraries:
|
|||||||
* `python 3.10`_ and up
|
* `python 3.10`_ and up
|
||||||
* `sqlalchemy 1.4`_
|
* `sqlalchemy 1.4`_
|
||||||
* `exifread`_ for parse EXIF information
|
* `exifread`_ for parse EXIF information
|
||||||
|
* `mutagen`_ for extracting tags from audio files
|
||||||
|
|
||||||
Pycatalog 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
|
small possibility of using it Windows (probably with limitations) and quite big
|
||||||
@@ -88,3 +89,4 @@ file in top-level directory.
|
|||||||
.. _sqlalchemy 1.4: http://www.sqlalchemy.org
|
.. _sqlalchemy 1.4: http://www.sqlalchemy.org
|
||||||
.. _tagging files: http://en.wikipedia.org/wiki/tag_%28metadata%29
|
.. _tagging files: http://en.wikipedia.org/wiki/tag_%28metadata%29
|
||||||
.. _tox: https://testrun.org/tox
|
.. _tox: https://testrun.org/tox
|
||||||
|
.. _mutagen: https://github.com/quodlibet/mutagen
|
||||||
|
|||||||
@@ -4,59 +4,29 @@ Fast and ugly CLI interface
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
|
||||||
from pycatalog import scan
|
from pycatalog import scan
|
||||||
from pycatalog import misc
|
from pycatalog import misc
|
||||||
from pycatalog import dbobjects as dbo
|
from pycatalog import dbobjects as dbo
|
||||||
from pycatalog.dbcommon import connect, Session
|
from pycatalog import dbcommon
|
||||||
from pycatalog import logger
|
from pycatalog import logger
|
||||||
|
|
||||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
|
LOG = logger.get_logger()
|
||||||
|
|
||||||
RESET_SEQ = '\033[0m'
|
|
||||||
COLOR_SEQ = '\033[1;%dm'
|
|
||||||
BOLD_SEQ = '\033[1m'
|
|
||||||
|
|
||||||
LOG = logger.get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def colorize(txt, color):
|
|
||||||
"""Pretty print with colors to console."""
|
|
||||||
color_map = {'black': BLACK,
|
|
||||||
'red': RED,
|
|
||||||
'green': GREEN,
|
|
||||||
'yellow': YELLOW,
|
|
||||||
'blue': BLUE,
|
|
||||||
'magenta': MAGENTA,
|
|
||||||
'cyan': CYAN,
|
|
||||||
'white': WHITE}
|
|
||||||
return COLOR_SEQ % color_map[color] + txt + RESET_SEQ
|
|
||||||
|
|
||||||
|
|
||||||
def asserdb(func):
|
|
||||||
def wrapper(args):
|
|
||||||
if not os.path.exists(args.db):
|
|
||||||
print(colorize("File `%s' does not exists!" % args.db, 'red'))
|
|
||||||
sys.exit(1)
|
|
||||||
func(args)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
TYPE_MAP = {0: 'd', 1: 'd', 2: 'f', 3: 'l'}
|
TYPE_MAP = {0: 'd', 1: 'd', 2: 'f', 3: 'l'}
|
||||||
|
|
||||||
|
|
||||||
class Iface(object):
|
class Iface(object):
|
||||||
"""Main class which interacts with the pyGTKtalog modules"""
|
"""Main class which interacts with the pyGTKtalog modules"""
|
||||||
def __init__(self, dbname, pretend=False, debug=False):
|
def __init__(self, dbname, pretend=False, debug=False, use_color=True):
|
||||||
"""Init"""
|
"""Init"""
|
||||||
self.engine = connect(dbname)
|
self.engine = dbcommon.connect(dbname)
|
||||||
self.sess = Session()
|
self.sess = dbcommon.Session()
|
||||||
self.dry_run = pretend
|
self.dry_run = pretend
|
||||||
self.root = None
|
self.root = None
|
||||||
self._dbname = dbname
|
self._dbname = dbname
|
||||||
|
self.use_color = use_color
|
||||||
if debug:
|
if debug:
|
||||||
scan.LOG.setLevel('DEBUG')
|
scan.LOG.setLevel('DEBUG')
|
||||||
LOG.setLevel('DEBUG')
|
LOG.setLevel('DEBUG')
|
||||||
@@ -95,11 +65,11 @@ class Iface(object):
|
|||||||
"""Make the path to the item in the DB"""
|
"""Make the path to the item in the DB"""
|
||||||
orig_node = node
|
orig_node = node
|
||||||
if node.parent == node:
|
if node.parent == node:
|
||||||
return {u'/': (u' ', 0, u' ')}
|
return {'/': (u' ', 0, u' ')}
|
||||||
|
|
||||||
ext = ''
|
ext = ''
|
||||||
if node.parent.type == dbo.TYPE['root']:
|
if node.parent.type == dbo.TYPE['root'] and self.use_color:
|
||||||
ext = colorize(' (%s)' % node.filepath, 'white')
|
ext = misc.colorize(' (%s)' % node.filepath, 'white')
|
||||||
|
|
||||||
path = []
|
path = []
|
||||||
path.append(node.filename)
|
path.append(node.filename)
|
||||||
@@ -139,8 +109,11 @@ class Iface(object):
|
|||||||
self.sess.commit()
|
self.sess.commit()
|
||||||
self.sess.close()
|
self.sess.close()
|
||||||
|
|
||||||
def list(self, path=None, recursive=False, long_=False):
|
def list(self, path=None, recursive=False, long_=False, mode='plain'):
|
||||||
"""Simulate ls command for the provided item path"""
|
"""Simulate ls command for the provided item path"""
|
||||||
|
if mode == 'mc':
|
||||||
|
self.use_color = False
|
||||||
|
|
||||||
self.root = self.sess.query(dbo.File)
|
self.root = self.sess.query(dbo.File)
|
||||||
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
||||||
if path:
|
if path:
|
||||||
@@ -150,16 +123,37 @@ class Iface(object):
|
|||||||
node = self.root
|
node = self.root
|
||||||
msg = "Content of path `/':"
|
msg = "Content of path `/':"
|
||||||
|
|
||||||
print(colorize(msg, 'white'))
|
if mode != 'mc':
|
||||||
|
print(misc.colorize(msg, 'white'))
|
||||||
|
|
||||||
if recursive:
|
if recursive:
|
||||||
items = self._walk(node)
|
items = self._walk(node)
|
||||||
else:
|
else:
|
||||||
items = self._list(node)
|
items = self._list(node)
|
||||||
|
|
||||||
if long_:
|
if mode == 'mc':
|
||||||
filenames = []
|
filenames = []
|
||||||
format_str = (u'{} {:>%d,} {} {}' %
|
format_str = ('{} 1 {} {} {:>%d} {} {}' %
|
||||||
|
len(str(sorted([i[1] for i in
|
||||||
|
items.values()])[-1])))
|
||||||
|
for fname in sorted(items.keys()):
|
||||||
|
type_, size, date = items[fname]
|
||||||
|
if type_ == 'd':
|
||||||
|
perms = 'drwxrwxrwx'
|
||||||
|
elif type_ == 'l':
|
||||||
|
perms = 'lrw-rw-rw-'
|
||||||
|
elif type_ == 'f':
|
||||||
|
perms = '-rw-rw-rw-'
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
filenames.append(format_str
|
||||||
|
.format(perms, os.getuid(),
|
||||||
|
os.getgid(), size,
|
||||||
|
date.strftime('%d/%m/%Y %H:%M:%S'),
|
||||||
|
fname))
|
||||||
|
elif long_:
|
||||||
|
filenames = []
|
||||||
|
format_str = ('{} {:>%d,} {} {}' %
|
||||||
_get_highest_size_length(items))
|
_get_highest_size_length(items))
|
||||||
for fname in sorted(items.keys()):
|
for fname in sorted(items.keys()):
|
||||||
type_, size, date = items[fname]
|
type_, size, date = items[fname]
|
||||||
@@ -178,8 +172,8 @@ class Iface(object):
|
|||||||
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
||||||
node = self._resolve_path(path)
|
node = self._resolve_path(path)
|
||||||
if node == self.root:
|
if node == self.root:
|
||||||
print(colorize('Cannot update entire db, since root was provided '
|
print(misc.colorize('Cannot update entire db, since root was '
|
||||||
'as path.', 'red'))
|
'provided as path.', 'red'))
|
||||||
return
|
return
|
||||||
|
|
||||||
if not dir_to_update:
|
if not dir_to_update:
|
||||||
@@ -188,14 +182,14 @@ class Iface(object):
|
|||||||
if not os.path.exists(dir_to_update):
|
if not os.path.exists(dir_to_update):
|
||||||
raise OSError("Path to updtate doesn't exists: %s", dir_to_update)
|
raise OSError("Path to updtate doesn't exists: %s", dir_to_update)
|
||||||
|
|
||||||
print(colorize("Updating node `%s' against directory "
|
print(misc.colorize("Updating node `%s' against directory "
|
||||||
"`%s'" % (path, dir_to_update), 'white'))
|
"`%s'" % (path, dir_to_update), 'white'))
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
scanob = scan.Scan(dir_to_update)
|
scanob = scan.Scan(dir_to_update)
|
||||||
# scanob.update_files(node.id)
|
# scanob.update_files(node.id)
|
||||||
scanob.update_files(node.id, self.engine)
|
scanob.update_files(node.id, self.engine)
|
||||||
|
|
||||||
def create(self, dir_to_add, data_dir):
|
def create(self, dir_to_add, label=None):
|
||||||
"""Create new database"""
|
"""Create new database"""
|
||||||
self.root = dbo.File()
|
self.root = dbo.File()
|
||||||
self.root.id = 1
|
self.root.id = 1
|
||||||
@@ -205,27 +199,17 @@ class Iface(object):
|
|||||||
self.root.type = 0
|
self.root.type = 0
|
||||||
self.root.parent_id = 1
|
self.root.parent_id = 1
|
||||||
|
|
||||||
config = dbo.Config()
|
|
||||||
config.key = 'image_path'
|
|
||||||
config.value = data_dir
|
|
||||||
|
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
self.sess.add(self.root)
|
self.sess.add(self.root)
|
||||||
self.sess.add(config)
|
|
||||||
self.sess.commit()
|
self.sess.commit()
|
||||||
|
|
||||||
print(colorize("Creating new db against directory `%s'" % dir_to_add,
|
print(misc.colorize("Creating new db against directory `%s'" %
|
||||||
'white'))
|
dir_to_add, 'white'))
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
if data_dir == ':same_as_db:':
|
|
||||||
misc.calculate_image_path(None, True)
|
|
||||||
else:
|
|
||||||
misc.calculate_image_path(data_dir, True)
|
|
||||||
|
|
||||||
scanob = scan.Scan(dir_to_add)
|
scanob = scan.Scan(dir_to_add)
|
||||||
scanob.add_files(self.engine)
|
scanob.add_files(label=label)
|
||||||
|
|
||||||
def add(self, dir_to_add):
|
def add(self, dir_to_add, label=None):
|
||||||
"""Add new directory to the db"""
|
"""Add new directory to the db"""
|
||||||
self.root = self.sess.query(dbo.File)
|
self.root = self.sess.query(dbo.File)
|
||||||
self.root = self.root.filter(dbo.File.type == 0).first()
|
self.root = self.root.filter(dbo.File.type == 0).first()
|
||||||
@@ -233,10 +217,10 @@ class Iface(object):
|
|||||||
if not os.path.exists(dir_to_add):
|
if not os.path.exists(dir_to_add):
|
||||||
raise OSError("Path to add doesn't exists: %s", 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(misc.colorize("Adding directory `%s'" % dir_to_add, 'white'))
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
scanob = scan.Scan(dir_to_add)
|
scanob = scan.Scan(dir_to_add)
|
||||||
scanob.add_files()
|
scanob.add_files(label=label)
|
||||||
|
|
||||||
def _annotate(self, item, search_words):
|
def _annotate(self, item, search_words):
|
||||||
"""
|
"""
|
||||||
@@ -256,12 +240,12 @@ class Iface(object):
|
|||||||
if idx in indexes:
|
if idx in indexes:
|
||||||
if not highlight:
|
if not highlight:
|
||||||
highlight = True
|
highlight = True
|
||||||
result.append(COLOR_SEQ % WHITE)
|
result.append(misc.COLOR_SEQ % misc.WHITE)
|
||||||
result.append(char)
|
result.append(char)
|
||||||
else:
|
else:
|
||||||
if highlight:
|
if highlight:
|
||||||
highlight = False
|
highlight = False
|
||||||
result.append(RESET_SEQ)
|
result.append(misc.RESET_SEQ)
|
||||||
result.append(char)
|
result.append(char)
|
||||||
|
|
||||||
return "".join(result)
|
return "".join(result)
|
||||||
@@ -286,120 +270,25 @@ class Iface(object):
|
|||||||
for item in result:
|
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
|
|
||||||
|
|
||||||
if image_path == ':same_as_db:':
|
|
||||||
image_path = misc.calculate_image_path(None, False)
|
|
||||||
|
|
||||||
files_to_remove = []
|
|
||||||
|
|
||||||
# remove images/thumbnails which doesn't have file relation
|
|
||||||
for name, obj in (("images", dbo.Image),
|
|
||||||
("thumbnails", dbo.Thumbnail)):
|
|
||||||
self._purge_orphaned_objects(obj, "Scanning %s " % name)
|
|
||||||
|
|
||||||
# find all image files not associate with either Image (image/thumb)
|
|
||||||
# or Thumbnail (thumb) objects
|
|
||||||
sys.stdout.write(40 * " " + "\r")
|
|
||||||
count = 0
|
|
||||||
for root, dirs, files in os.walk(image_path):
|
|
||||||
for fname in files:
|
|
||||||
sys.stdout.write("Scanning files " +
|
|
||||||
"| / - \\".split()[count % 4] + "\r")
|
|
||||||
sys.stdout.flush()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
fname_ = os.path.join(root.split(image_path)[1],
|
|
||||||
fname).lstrip('/')
|
|
||||||
|
|
||||||
if '_t' in fname:
|
|
||||||
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()
|
|
||||||
if obj:
|
|
||||||
continue
|
|
||||||
|
|
||||||
else:
|
|
||||||
obj = (self.sess.query(dbo.Image)
|
|
||||||
.filter(dbo.Image.filename == fname_)).all()
|
|
||||||
if obj:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files_to_remove.append(os.path.join(root, fname))
|
|
||||||
|
|
||||||
LOG.debug("Found %d orphaned files", len(files_to_remove))
|
|
||||||
sys.stdout.write(40 * " " + "\r")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
if self.dry_run:
|
|
||||||
print("Following files are not associated to any items in the DB:")
|
|
||||||
for filename in sorted(files_to_remove):
|
|
||||||
print(filename)
|
|
||||||
self.sess.rollback()
|
|
||||||
else:
|
|
||||||
_remove_files(image_path, files_to_remove)
|
|
||||||
self.sess.commit()
|
|
||||||
|
|
||||||
def _purge_orphaned_objects(self, sa_class, msg):
|
|
||||||
"""Return tuple of lists of images that are orphaned"""
|
|
||||||
|
|
||||||
ids_to_remove = []
|
|
||||||
|
|
||||||
for count, item in enumerate(self.sess.query(sa_class).all()):
|
|
||||||
sys.stdout.write(msg + "| / - \\".split()[count % 4] + "\r")
|
|
||||||
if not item.file:
|
|
||||||
self.sess.delete(item)
|
|
||||||
ids_to_remove.append(item.id)
|
|
||||||
del item
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
LOG.debug("Found %d orphaned object of class %s",
|
|
||||||
len(ids_to_remove), sa_class.__name__)
|
|
||||||
self.sess.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_files(image_path, filenames):
|
|
||||||
"""Remove files and empty directories in provided location"""
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for count, fname in enumerate(filenames, start=1):
|
|
||||||
os.unlink(fname)
|
|
||||||
|
|
||||||
LOG.info("Removed %d orphaned files", count)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for root, dirs, _ in os.walk(image_path):
|
|
||||||
for dirname in dirs:
|
|
||||||
try:
|
|
||||||
os.rmdir(os.path.join(root, dirname))
|
|
||||||
count += 1
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
LOG.info("Removed %d empty directories", count)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_highest_size_length(item_dict):
|
def _get_highest_size_length(item_dict):
|
||||||
highest = len(str(sorted([i[1] for i in item_dict.values()])[-1]))
|
highest = len(str(sorted([i[1] for i in item_dict.values()])[-1]))
|
||||||
return highest + highest / 3
|
return highest + highest / 3
|
||||||
|
|
||||||
|
|
||||||
@asserdb
|
@misc.asserdb
|
||||||
def list_db(args):
|
def list_db(args):
|
||||||
"""List"""
|
"""List"""
|
||||||
|
if args.mode == 'mc':
|
||||||
|
LOG.setLevel(100) # supress logging
|
||||||
|
|
||||||
obj = Iface(args.db, False, args.debug)
|
obj = Iface(args.db, False, args.debug)
|
||||||
obj.list(path=args.path, recursive=args.recursive, long_=args.long)
|
obj.list(path=args.path, recursive=args.recursive, long_=args.long,
|
||||||
|
mode=args.mode)
|
||||||
obj.close()
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
@asserdb
|
@misc.asserdb
|
||||||
def update_db(args):
|
def update_db(args):
|
||||||
"""Update"""
|
"""Update"""
|
||||||
obj = Iface(args.db, args.pretend, args.debug)
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
@@ -407,23 +296,22 @@ def update_db(args):
|
|||||||
obj.close()
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
@asserdb
|
@misc.asserdb
|
||||||
def add_dir(args):
|
def add_dir(args):
|
||||||
"""Add"""
|
"""Add"""
|
||||||
obj = Iface(args.db, args.pretend, args.debug)
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
obj.add(args.dir_to_add)
|
obj.add(args.dir_to_add, args.label)
|
||||||
obj.close()
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
def create_db(args):
|
def create_db(args):
|
||||||
"""List"""
|
"""List"""
|
||||||
__import__('pdb').set_trace()
|
|
||||||
obj = Iface(args.db, args.pretend, args.debug)
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
obj.create(args.dir_to_add, args.imagedir)
|
obj.create(args.dir_to_add, args.label)
|
||||||
obj.close()
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
@asserdb
|
@misc.asserdb
|
||||||
def search(args):
|
def search(args):
|
||||||
"""Find"""
|
"""Find"""
|
||||||
obj = Iface(args.db, False, args.debug)
|
obj = Iface(args.db, False, args.debug)
|
||||||
@@ -431,14 +319,6 @@ def search(args):
|
|||||||
obj.close()
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
@asserdb
|
|
||||||
def cleanup(args):
|
|
||||||
"""Cleanup"""
|
|
||||||
obj = Iface(args.db, False, args.debug)
|
|
||||||
obj.fsck()
|
|
||||||
obj.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main"""
|
"""Main"""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -447,6 +327,12 @@ def main():
|
|||||||
list_ = subparser.add_parser('list')
|
list_ = subparser.add_parser('list')
|
||||||
list_.add_argument('db')
|
list_.add_argument('db')
|
||||||
list_.add_argument('path', nargs='?')
|
list_.add_argument('path', nargs='?')
|
||||||
|
list_.add_argument('-m', '--mode', help='List items using mode. By '
|
||||||
|
'default is simply plain mode, other possibility is to '
|
||||||
|
'use "mc" mode, which is suitable to use with extfs '
|
||||||
|
'plugin', default='plain')
|
||||||
|
list_.add_argument('-c', '--color', help='Use colors for listing',
|
||||||
|
action='store_true', default=False)
|
||||||
list_.add_argument('-l', '--long', help='Show size, date and type',
|
list_.add_argument('-l', '--long', help='Show size, date and type',
|
||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
list_.add_argument('-r', '--recursive', help='list items in '
|
list_.add_argument('-r', '--recursive', help='list items in '
|
||||||
@@ -469,12 +355,8 @@ def main():
|
|||||||
create = subparser.add_parser('create')
|
create = subparser.add_parser('create')
|
||||||
create.add_argument('db')
|
create.add_argument('db')
|
||||||
create.add_argument('dir_to_add')
|
create.add_argument('dir_to_add')
|
||||||
create.add_argument('-i', '--imagedir', help="Directory where to put "
|
create.add_argument('-l', '--label', help='Add label as the root item of '
|
||||||
"images for the database. Popular, but deprecated "
|
'the added directory')
|
||||||
"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:')
|
|
||||||
create.add_argument('-p', '--pretend', help="Don't do the action, just "
|
create.add_argument('-p', '--pretend', help="Don't do the action, just "
|
||||||
"give the info what would gonna to happen.",
|
"give the info what would gonna to happen.",
|
||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
@@ -490,6 +372,8 @@ def main():
|
|||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
add.add_argument('-d', '--debug', help='Turn on debug',
|
add.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
|
add.add_argument('-l', '--label', help='Add label as the root item of the '
|
||||||
|
'added directory')
|
||||||
add.set_defaults(func=add_dir)
|
add.set_defaults(func=add_dir)
|
||||||
|
|
||||||
find = subparser.add_parser('find')
|
find = subparser.add_parser('find')
|
||||||
@@ -499,15 +383,6 @@ def main():
|
|||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
find.set_defaults(func=search)
|
find.set_defaults(func=search)
|
||||||
|
|
||||||
fsck = subparser.add_parser('fsck')
|
|
||||||
fsck.add_argument('db')
|
|
||||||
fsck.add_argument('-p', '--pretend', help="Don't do the action, just give"
|
|
||||||
" the info what would gonna to happen.",
|
|
||||||
action='store_true', default=False)
|
|
||||||
fsck.add_argument('-d', '--debug', help='Turn on debug',
|
|
||||||
action='store_true', default=False)
|
|
||||||
fsck.set_defaults(func=cleanup)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if 'func' in args:
|
if 'func' in args:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Base = declarative_base(metadata=Meta)
|
|||||||
Session = sessionmaker()
|
Session = sessionmaker()
|
||||||
DbFilename = None
|
DbFilename = None
|
||||||
|
|
||||||
LOG = get_logger("dbcommon")
|
LOG = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def connect(filename=None):
|
def connect(filename=None):
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ from sqlalchemy import DateTime, ForeignKey, Sequence
|
|||||||
from sqlalchemy.orm import relation, backref
|
from sqlalchemy.orm import relation, backref
|
||||||
|
|
||||||
from pycatalog.dbcommon import Base
|
from pycatalog.dbcommon import Base
|
||||||
from pycatalog.thumbnail import ThumbCreator
|
|
||||||
from pycatalog.logger import get_logger
|
from pycatalog.logger import get_logger
|
||||||
from pycatalog.misc import mk_paths
|
|
||||||
|
|
||||||
|
|
||||||
LOG = get_logger(__name__)
|
LOG = get_logger()
|
||||||
|
|
||||||
tags_files = Table("tags_files", Base.metadata,
|
tags_files = Table("tags_files", Base.metadata,
|
||||||
Column("file_id", Integer, ForeignKey("files.id")),
|
Column("file_id", Integer, ForeignKey("files.id")),
|
||||||
@@ -49,8 +47,6 @@ class File(Base):
|
|||||||
backref=backref('parent', remote_side="File.id"),
|
backref=backref('parent', remote_side="File.id"),
|
||||||
order_by=[type, filename])
|
order_by=[type, filename])
|
||||||
tags = relation("Tag", secondary=tags_files, order_by="Tag.tag")
|
tags = relation("Tag", secondary=tags_files, order_by="Tag.tag")
|
||||||
thumbnail = relation("Thumbnail", backref="file")
|
|
||||||
images = relation("Image", backref="file", order_by="Image.filename")
|
|
||||||
|
|
||||||
def __init__(self, filename=None, path=None, date=None, size=None,
|
def __init__(self, filename=None, path=None, date=None, size=None,
|
||||||
ftype=None, src=None):
|
ftype=None, src=None):
|
||||||
@@ -108,7 +104,7 @@ class Tag(Base):
|
|||||||
tag = Column(Text)
|
tag = Column(Text)
|
||||||
group = relation('Group', backref=backref('tags', remote_side="Group.id"))
|
group = relation('Group', backref=backref('tags', remote_side="Group.id"))
|
||||||
|
|
||||||
files = relation("File", secondary=tags_files)
|
files = relation("File", secondary=tags_files, back_populates="tags")
|
||||||
|
|
||||||
def __init__(self, tag=None, group=None):
|
def __init__(self, tag=None, group=None):
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
@@ -118,111 +114,6 @@ class Tag(Base):
|
|||||||
return "<Tag('%s', %s)>" % (str(self.tag), str(self.id))
|
return "<Tag('%s', %s)>" % (str(self.tag), str(self.id))
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(Base):
|
|
||||||
"""Thumbnail for the file"""
|
|
||||||
__tablename__ = "thumbnails"
|
|
||||||
id = Column(Integer, Sequence("thumbnail_id_seq"), primary_key=True)
|
|
||||||
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
|
||||||
filename = Column(Text)
|
|
||||||
|
|
||||||
def __init__(self, filename=None, img_path=None, file_obj=None):
|
|
||||||
self.filename = filename
|
|
||||||
self.file = file_obj
|
|
||||||
self.img_path = img_path
|
|
||||||
if filename and file_obj and img_path:
|
|
||||||
self.save(self.filename, img_path)
|
|
||||||
|
|
||||||
def save(self, fname, img_path):
|
|
||||||
"""
|
|
||||||
Create file related thumbnail, add it to the file object.
|
|
||||||
"""
|
|
||||||
new_name = mk_paths(fname, img_path)
|
|
||||||
ext = os.path.splitext(self.filename)[1]
|
|
||||||
if ext:
|
|
||||||
new_name.append("".join([new_name.pop(), ext]))
|
|
||||||
|
|
||||||
thumb = ThumbCreator(self.filename).generate()
|
|
||||||
name, ext = os.path.splitext(new_name.pop())
|
|
||||||
new_name.append("".join([name, "_t", ext]))
|
|
||||||
self.filename = os.path.sep.join(new_name)
|
|
||||||
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))
|
|
||||||
os.unlink(thumb)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Thumbnail('%s', %s)>" % (str(self.filename), str(self.id))
|
|
||||||
|
|
||||||
|
|
||||||
class Image(Base):
|
|
||||||
"""Images and their thumbnails"""
|
|
||||||
__tablename__ = "images"
|
|
||||||
id = Column(Integer, Sequence("images_id_seq"), primary_key=True)
|
|
||||||
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
|
||||||
filename = Column(Text)
|
|
||||||
|
|
||||||
def __init__(self, filename=None, img_path=None, file_obj=None, move=True):
|
|
||||||
self.filename = None
|
|
||||||
self.file = file_obj
|
|
||||||
self.img_path = img_path
|
|
||||||
if filename and img_path:
|
|
||||||
self.filename = filename
|
|
||||||
self.save(filename, img_path, move)
|
|
||||||
|
|
||||||
def save(self, fname, img_path, move=True):
|
|
||||||
"""
|
|
||||||
Save and create coressponding thumbnail (note: it differs from file
|
|
||||||
related thumbnail!)
|
|
||||||
"""
|
|
||||||
new_name = mk_paths(fname, img_path)
|
|
||||||
ext = os.path.splitext(self.filename)[1]
|
|
||||||
|
|
||||||
if ext:
|
|
||||||
new_name.append("".join([new_name.pop(), ext]))
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join(img_path, *new_name)):
|
|
||||||
if move:
|
|
||||||
shutil.move(self.filename, os.path.join(img_path, *new_name))
|
|
||||||
else:
|
|
||||||
shutil.copy(self.filename, os.path.join(img_path, *new_name))
|
|
||||||
else:
|
|
||||||
LOG.warning("Image with same CRC already exists "
|
|
||||||
"('%s', '%s')" % (self.filename, "/".join(new_name)))
|
|
||||||
|
|
||||||
self.filename = os.path.sep.join(new_name)
|
|
||||||
|
|
||||||
name, ext = os.path.splitext(new_name.pop())
|
|
||||||
new_name.append("".join([name, "_t", ext]))
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join(img_path, *new_name)):
|
|
||||||
thumb = ThumbCreator(os.path.join(img_path, self.filename))
|
|
||||||
shutil.move(thumb.generate(), os.path.join(img_path, *new_name))
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
img = Image()
|
|
||||||
img.filename = self.filename
|
|
||||||
return img
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail(self):
|
|
||||||
"""
|
|
||||||
Return path to thumbnail for this image
|
|
||||||
"""
|
|
||||||
path, fname = os.path.split(self.filename)
|
|
||||||
base, ext = os.path.splitext(fname)
|
|
||||||
return os.path.join(path, base + "_t" + ext)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Image('%s', %s)>" % (str(self.filename), str(self.id))
|
|
||||||
|
|
||||||
|
|
||||||
class Exif(Base):
|
class Exif(Base):
|
||||||
"""Selected EXIF information"""
|
"""Selected EXIF information"""
|
||||||
__tablename__ = "exif"
|
__tablename__ = "exif"
|
||||||
|
|||||||
@@ -63,11 +63,10 @@ class ColoredFormatter(logging.Formatter):
|
|||||||
log_obj = None
|
log_obj = None
|
||||||
|
|
||||||
|
|
||||||
def get_logger(module_name, level='INFO', to_file=True, to_console=True):
|
def get_logger(level='INFO', to_file=True, to_console=True):
|
||||||
"""
|
"""
|
||||||
Prepare and return log object. Standard formatting is used for all logs.
|
Prepare and return log object. Standard formatting is used for all logs.
|
||||||
Arguments:
|
Arguments:
|
||||||
@module_name - String name for Logger object.
|
|
||||||
@level - Log level (as string), one of DEBUG, INFO, WARN, ERROR and
|
@level - Log level (as string), one of DEBUG, INFO, WARN, ERROR and
|
||||||
CRITICAL.
|
CRITICAL.
|
||||||
@to_file - If True, additionally stores full log in file inside
|
@to_file - If True, additionally stores full log in file inside
|
||||||
@@ -75,10 +74,13 @@ def get_logger(module_name, level='INFO', to_file=True, to_console=True):
|
|||||||
is only redirected to stderr.
|
is only redirected to stderr.
|
||||||
Returns: object of logging.Logger class
|
Returns: object of logging.Logger class
|
||||||
"""
|
"""
|
||||||
|
global log_obj
|
||||||
|
if log_obj:
|
||||||
|
return log_obj
|
||||||
|
|
||||||
path = os.path.join(os.path.expanduser("~"), ".pycatalog", "app.log")
|
path = os.path.join(os.path.expanduser("~"), ".pycatalog", "app.log")
|
||||||
|
|
||||||
log = logging.getLogger(module_name)
|
log = logging.getLogger("pycatalog")
|
||||||
log.setLevel(LEVEL[level])
|
log.setLevel(LEVEL[level])
|
||||||
|
|
||||||
if to_console:
|
if to_console:
|
||||||
@@ -104,4 +106,5 @@ def get_logger(module_name, level='INFO', to_file=True, to_console=True):
|
|||||||
dummy_handler.setFormatter(dummy_formatter)
|
dummy_handler.setFormatter(dummy_formatter)
|
||||||
log.addHandler(dummy_handler)
|
log.addHandler(dummy_handler)
|
||||||
|
|
||||||
|
log_obj = log
|
||||||
return log
|
return log
|
||||||
|
|||||||
@@ -6,13 +6,30 @@
|
|||||||
Created: 2009-04-05
|
Created: 2009-04-05
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import errno
|
import sys
|
||||||
from zlib import crc32
|
|
||||||
|
|
||||||
import pycatalog.dbcommon
|
from pycatalog import logger
|
||||||
from pycatalog.logger import get_logger
|
|
||||||
|
|
||||||
LOG = get_logger(__name__)
|
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
|
||||||
|
|
||||||
|
RESET_SEQ = '\033[0m'
|
||||||
|
COLOR_SEQ = '\033[1;%dm'
|
||||||
|
BOLD_SEQ = '\033[1m'
|
||||||
|
|
||||||
|
LOG = logger.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def colorize(txt, color):
|
||||||
|
"""Pretty print with colors to console."""
|
||||||
|
color_map = {'black': BLACK,
|
||||||
|
'red': RED,
|
||||||
|
'green': GREEN,
|
||||||
|
'yellow': YELLOW,
|
||||||
|
'blue': BLUE,
|
||||||
|
'magenta': MAGENTA,
|
||||||
|
'cyan': CYAN,
|
||||||
|
'white': WHITE}
|
||||||
|
return COLOR_SEQ % color_map[color] + txt + RESET_SEQ
|
||||||
|
|
||||||
|
|
||||||
def float_to_string(float_length):
|
def float_to_string(float_length):
|
||||||
@@ -27,51 +44,13 @@ def float_to_string(float_length):
|
|||||||
minutes = int(float_length / 60)
|
minutes = int(float_length / 60)
|
||||||
float_length -= minutes * 60
|
float_length -= minutes * 60
|
||||||
sec = int(float_length)
|
sec = int(float_length)
|
||||||
return "%02d:%02d:%02d" % (hour, minutes, sec)
|
return f"{hour:02}:{minutes:02}:{sec:02}"
|
||||||
|
|
||||||
|
|
||||||
def calculate_image_path(dbpath=None, create=False):
|
def asserdb(func):
|
||||||
"""Calculate image path out of provided path or using current connection"""
|
def wrapper(args):
|
||||||
if not dbpath:
|
if not os.path.exists(args.db):
|
||||||
dbpath = pycatalog.dbcommon.DbFilename
|
print(colorize("File `%s' does not exists!" % args.db, 'red'))
|
||||||
if dbpath == ":memory:":
|
sys.exit(1)
|
||||||
raise OSError("Cannot create image path out of in-memory db!")
|
func(args)
|
||||||
|
return wrapper
|
||||||
dir_, file_ = (os.path.dirname(dbpath), os.path.basename(dbpath))
|
|
||||||
file_base, dummy = os.path.splitext(file_)
|
|
||||||
images_dir = os.path.join(dir_, file_base + "_images")
|
|
||||||
else:
|
|
||||||
if dbpath and "~" in dbpath:
|
|
||||||
dbpath = os.path.expanduser(dbpath)
|
|
||||||
if dbpath and "$" in dbpath:
|
|
||||||
dbpath = os.path.expandvars(dbpath)
|
|
||||||
images_dir = dbpath
|
|
||||||
|
|
||||||
if create:
|
|
||||||
if not os.path.exists(images_dir):
|
|
||||||
try:
|
|
||||||
os.mkdir(images_dir)
|
|
||||||
except OSError as err:
|
|
||||||
if err.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
elif not os.path.exists(images_dir):
|
|
||||||
raise OSError("%s: No such directory" % images_dir)
|
|
||||||
|
|
||||||
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, '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)]
|
|
||||||
full_path = os.path.join(img_path, *new_path[:-1])
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.makedirs(full_path)
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.EEXIST:
|
|
||||||
LOG.debug("Directory %s already exists." % full_path)
|
|
||||||
|
|
||||||
return new_path
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Project: pyGTKtalog
|
|
||||||
Description: pyGTK common utility functions
|
|
||||||
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.
|
|
||||||
Arguments:
|
|
||||||
@treeview - gtk.TreeView
|
|
||||||
Returns:
|
|
||||||
Item in first column of TreeModel, which TreeView is connected with,
|
|
||||||
None in other cases
|
|
||||||
"""
|
|
||||||
path, column = treeview.get_cursor()
|
|
||||||
if path and column:
|
|
||||||
model = treeview.get_model()
|
|
||||||
tm_iter = model.get_iter(path)
|
|
||||||
item_id = model.get_value(tm_iter, 0)
|
|
||||||
return item_id
|
|
||||||
return None
|
|
||||||
@@ -10,14 +10,16 @@ import re
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
import pycatalog.misc
|
import exifread
|
||||||
from pycatalog.dbobjects import File, Image, Thumbnail, Config, TYPE
|
import mutagen
|
||||||
from pycatalog.dbcommon import Session
|
|
||||||
|
from pycatalog.dbobjects import File, TYPE
|
||||||
|
from pycatalog import dbcommon
|
||||||
from pycatalog.logger import get_logger
|
from pycatalog.logger import get_logger
|
||||||
from pycatalog.video import Video
|
from pycatalog.video import Video
|
||||||
|
|
||||||
|
|
||||||
LOG = get_logger(__name__)
|
LOG = get_logger()
|
||||||
RE_FN_START = re.compile(r'(?P<fname_start>'
|
RE_FN_START = re.compile(r'(?P<fname_start>'
|
||||||
r'(\[[^\]]*\]\s)?'
|
r'(\[[^\]]*\]\s)?'
|
||||||
r'([^(]*)\s'
|
r'([^(]*)\s'
|
||||||
@@ -27,7 +29,6 @@ RE_FN_START = re.compile(r'(?P<fname_start>'
|
|||||||
|
|
||||||
class NoAccessError(Exception):
|
class NoAccessError(Exception):
|
||||||
"""No access exception"""
|
"""No access exception"""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Scan(object):
|
class Scan(object):
|
||||||
@@ -45,13 +46,11 @@ class Scan(object):
|
|||||||
self._files = []
|
self._files = []
|
||||||
self._existing_files = [] # for re-use purpose in adding
|
self._existing_files = [] # for re-use purpose in adding
|
||||||
self._existing_branch = [] # for branch storage, mainly for updating
|
self._existing_branch = [] # for branch storage, mainly for updating
|
||||||
self._session = Session()
|
self._session = dbcommon.Session()
|
||||||
self.files_count = self._get_files_count()
|
self.files_count = self._get_files_count()
|
||||||
self.current_count = 0
|
self.current_count = 0
|
||||||
|
|
||||||
self._set_image_path()
|
def add_files(self, label=None):
|
||||||
|
|
||||||
def add_files(self, engine=None):
|
|
||||||
"""
|
"""
|
||||||
Returns list, which contain object, modification date and file
|
Returns list, which contain object, modification date and file
|
||||||
size.
|
size.
|
||||||
@@ -75,6 +74,8 @@ class Scan(object):
|
|||||||
|
|
||||||
# add only first item from _files, because it is a root of the other,
|
# add only first item from _files, because it is a root of the other,
|
||||||
# so other will be automatically added aswell.
|
# so other will be automatically added aswell.
|
||||||
|
if label:
|
||||||
|
self._files[0].filename = label
|
||||||
self._session.add(self._files[0])
|
self._session.add(self._files[0])
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
return self._files
|
return self._files
|
||||||
@@ -214,66 +215,35 @@ class Scan(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _audio(self, fobj, filepath):
|
def _audio(self, fobj, filepath):
|
||||||
# LOG.warning('audio')
|
tags = mutagen.File(filepath)
|
||||||
|
if not tags:
|
||||||
return
|
return
|
||||||
|
fobj.description = tags.pprint()
|
||||||
|
|
||||||
def _image(self, fobj, filepath):
|
def _image(self, fobj, filepath):
|
||||||
# LOG.warning('image')
|
"""Read exif if exists, add it to description"""
|
||||||
|
with open(filepath, 'rb') as obj:
|
||||||
|
exif = exifread.process_file(obj)
|
||||||
|
if not exif:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
data = []
|
||||||
|
# longest key + 2, since we need a colon and a space after it
|
||||||
|
longest_key = max([len(k) for k in exif]) + 2
|
||||||
|
for key in exif:
|
||||||
|
if 'thumbnail' in key.lower() and isinstance(exif[key], bytes):
|
||||||
|
data.append(f"{key + ':' :<{longest_key}}thumbnail present")
|
||||||
|
continue
|
||||||
|
data.append(f"{key + ':' :<{longest_key}}{exif[key]}")
|
||||||
|
fobj.description = "\n".join(data)
|
||||||
|
|
||||||
def _video(self, fobj, filepath):
|
def _video(self, fobj, filepath):
|
||||||
"""
|
"""
|
||||||
Make captures for a movie. Save it under uniq name.
|
Make captures for a movie. Save it under uniq name.
|
||||||
"""
|
"""
|
||||||
result = RE_FN_START.match(fobj.filename)
|
|
||||||
if result:
|
|
||||||
self._check_related(fobj, result.groupdict()['fname_start'])
|
|
||||||
|
|
||||||
vid = Video(filepath)
|
vid = Video(filepath)
|
||||||
|
|
||||||
fobj.description = vid.get_formatted_tags()
|
fobj.description = vid.get_formatted_tags()
|
||||||
|
|
||||||
preview_fn = vid.capture()
|
|
||||||
if preview_fn:
|
|
||||||
Image(preview_fn, self.img_path, fobj)
|
|
||||||
|
|
||||||
def _check_related(self, fobj, filename_start):
|
|
||||||
"""
|
|
||||||
Try to search for related files which belongs to specified File
|
|
||||||
object and pattern. If found, additional File objects are created.
|
|
||||||
|
|
||||||
For example, if we have movie file named like:
|
|
||||||
[aXXo] Batman (1989) [D3ADBEEF].avi
|
|
||||||
[aXXo] Batman (1989) trailer [B00B1337].avi
|
|
||||||
Batman (1989) [D3ADBEEF].avi
|
|
||||||
Batman [D3ADBEEF].avi
|
|
||||||
|
|
||||||
And for example file '[aXXo] Batman (1989) [D3ADBEEF].avi' might have
|
|
||||||
some other accompanied files, like:
|
|
||||||
|
|
||||||
[aXXo] Batman (1989) [D3ADBEEF].avi.conf
|
|
||||||
[aXXo] Batman (1989) [DEADC0DE].nfo
|
|
||||||
[aXXo] Batman (1989) cover [BEEFD00D].jpg
|
|
||||||
[aXXo] Batman (1989) poster [FEEDD00D].jpg
|
|
||||||
|
|
||||||
Which can be atuomatically asociated with the movie.
|
|
||||||
|
|
||||||
This method find such files, and for some of them (currently images)
|
|
||||||
will perform extra actions - like creating corresponding Image objects.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for fname in os.listdir(fobj.filepath):
|
|
||||||
extension = os.path.splitext(fname)[1]
|
|
||||||
if fname.startswith(filename_start) and \
|
|
||||||
extension in ('.jpg', '.gif', '.png'):
|
|
||||||
full_fname = os.path.join(fobj.filepath, fname)
|
|
||||||
LOG.debug('found corresponding image file: %s', full_fname)
|
|
||||||
|
|
||||||
Image(full_fname, self.img_path, fobj, False)
|
|
||||||
|
|
||||||
if not fobj.thumbnail:
|
|
||||||
Thumbnail(full_fname, self.img_path, fobj)
|
|
||||||
|
|
||||||
def _get_all_files(self):
|
def _get_all_files(self):
|
||||||
"""Gather all File objects"""
|
"""Gather all File objects"""
|
||||||
self._existing_files = self._session.query(File).all()
|
self._existing_files = self._session.query(File).all()
|
||||||
@@ -471,17 +441,6 @@ class Scan(object):
|
|||||||
LOG.debug("count of files: %s", count)
|
LOG.debug("count of files: %s", count)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def _set_image_path(self):
|
|
||||||
"""Get or calculate the images path"""
|
|
||||||
image_path = (self._session.query(Config)
|
|
||||||
.filter(Config.key == "image_path")).one()
|
|
||||||
if image_path.value == ":same_as_db:":
|
|
||||||
image_path = pycatalog.misc.calculate_image_path()
|
|
||||||
else:
|
|
||||||
image_path = pycatalog.misc.calculate_image_path(image_path.value)
|
|
||||||
|
|
||||||
self.img_path = image_path
|
|
||||||
|
|
||||||
|
|
||||||
def _get_dirsize(path):
|
def _get_dirsize(path):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
"""
|
|
||||||
Project: pyGTKtalog
|
|
||||||
Description: Create thumbnail for sepcified image
|
|
||||||
Type: lib
|
|
||||||
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
|
|
||||||
Created: 2011-05-15
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from tempfile import mkstemp
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
import exifread
|
|
||||||
|
|
||||||
from pycatalog.logger import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
LOG = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbCreator(object):
|
|
||||||
"""
|
|
||||||
Class for generate/extract thumbnail from image file
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename):
|
|
||||||
self.thumb_x = 160
|
|
||||||
self.thumb_y = 160
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
"""
|
|
||||||
Save thumbnail into temporary file
|
|
||||||
"""
|
|
||||||
exif = {}
|
|
||||||
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
|
|
||||||
3: Image.ROTATE_180, # Rotated 180
|
|
||||||
4: Image.FLIP_TOP_BOTTOM, # Mirrored vertical
|
|
||||||
5: Image.ROTATE_90, # Mirrored horizontal then
|
|
||||||
# rotated 90 CCW
|
|
||||||
6: Image.ROTATE_270, # Rotated 90 CW
|
|
||||||
7: Image.ROTATE_270, # Mirrored horizontal then
|
|
||||||
# rotated 90 CW
|
|
||||||
8: Image.ROTATE_90} # Rotated 90 CCW
|
|
||||||
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
|
|
||||||
|
|
||||||
exif = self._get_exif()
|
|
||||||
file_desc, thumb_fn = mkstemp(suffix=".jpg")
|
|
||||||
os.close(file_desc)
|
|
||||||
|
|
||||||
if exif and 'JPEGThumbnail' in exif and exif['JPEGThumbnail']:
|
|
||||||
LOG.debug("exif thumb for filename %s" % self.filename)
|
|
||||||
exif_thumbnail = exif['JPEGThumbnail']
|
|
||||||
thumb = open(thumb_fn, 'wb')
|
|
||||||
thumb.write(exif_thumbnail)
|
|
||||||
thumb.close()
|
|
||||||
else:
|
|
||||||
LOG.debug("no exif thumb")
|
|
||||||
if self.is_image_smaller():
|
|
||||||
shutil.copyfile(self.filename, thumb_fn)
|
|
||||||
else:
|
|
||||||
thumb = self._scale_image()
|
|
||||||
if thumb:
|
|
||||||
thumb.save(thumb_fn, "JPEG")
|
|
||||||
|
|
||||||
if exif and 'Image Orientation' in exif:
|
|
||||||
orient = exif['Image Orientation'].values[0]
|
|
||||||
if orient > 1 and orient in orientations:
|
|
||||||
thumb_image = Image.open(thumb_fn)
|
|
||||||
tmp_thumb_img = thumb_image.transpose(orientations[orient])
|
|
||||||
|
|
||||||
if orient in flips:
|
|
||||||
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
|
|
||||||
|
|
||||||
tmp_thumb_img.save(thumb_fn, 'JPEG')
|
|
||||||
|
|
||||||
return thumb_fn
|
|
||||||
|
|
||||||
def is_image_smaller(self):
|
|
||||||
"""Check if image is smaller than desired dimention, return boolean"""
|
|
||||||
image = Image.open(self.filename)
|
|
||||||
im_x, im_y = image.size
|
|
||||||
image.close()
|
|
||||||
return im_x <= self.thumb_x and im_y <= self.thumb_y
|
|
||||||
|
|
||||||
def _get_exif(self):
|
|
||||||
"""
|
|
||||||
Get exif (if available), return as a dict
|
|
||||||
"""
|
|
||||||
image_file = open(self.filename, 'rb')
|
|
||||||
try:
|
|
||||||
exif = exifread.process_file(image_file)
|
|
||||||
except Exception:
|
|
||||||
exif = {}
|
|
||||||
LOG.info("Exif crashed on '%s'." % self.filename)
|
|
||||||
finally:
|
|
||||||
image_file.close()
|
|
||||||
|
|
||||||
return exif
|
|
||||||
|
|
||||||
def _scale_image(self):
|
|
||||||
"""
|
|
||||||
Create thumbnail. returns image object or None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
image_thumb = Image.open(self.filename).convert('RGB')
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
it_x, it_y = image_thumb.size
|
|
||||||
if it_x > self.thumb_x or it_y > self.thumb_y:
|
|
||||||
image_thumb.thumbnail((self.thumb_x, self.thumb_y),
|
|
||||||
Image.ANTIALIAS)
|
|
||||||
return image_thumb
|
|
||||||
@@ -17,7 +17,7 @@ from pycatalog.misc import float_to_string
|
|||||||
from pycatalog.logger import get_logger
|
from pycatalog.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
LOG = get_logger("Video")
|
LOG = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class Video(object):
|
class Video(object):
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
Pillow
|
|
||||||
exifread
|
|
||||||
sqlalchemy
|
|
||||||
Reference in New Issue
Block a user