1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-25 08:01:39 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
Virgil Dupras
573d088088 pe v2.7.0 2012-08-11 12:00:16 -04:00
Virgil Dupras
75b08125c0 [#201 state:fixed] Added an EXIF Timestamp column in PE. 2012-08-10 16:34:27 -04:00
Virgil Dupras
20320f539f [#199 state:fixed] Added a mtime column to PE's cache DB so that we can purge outdated caches. 2012-08-10 15:58:37 -04:00
Virgil Dupras
24771af955 Removed obsolete hgignore entries. 2012-08-10 15:15:38 -04:00
Virgil Dupras
2bfe9960f1 Fixed typo in changelog. 2012-08-10 11:13:23 -04:00
Virgil Dupras
215bcb0d76 Added tag me6.5.0 for changeset 8f478379ec62 2012-08-10 11:07:03 -04:00
Virgil Dupras
2dbf8b80ae Updated hsaudiotag version req. 2012-08-10 10:30:50 -04:00
Virgil Dupras
470cd92030 me v6.5.0 2012-08-10 10:23:35 -04:00
Virgil Dupras
111edc3ce5 Fixed a bug causing groups with more than one ref file in it to appear (which looks weird and messes with selection).
Contents scans already weeded them out, bu t they were still possible with name-based scans. Now, the Scanner removes them all.
2012-08-09 11:16:06 -04:00
Virgil Dupras
df30a31782 Refactoring: Began to phase out to the use of hscommon.io in favor of Path methods. 2012-08-09 10:53:24 -04:00
Virgil Dupras
91f3a59523 Fixed add_directory() test which were broken. 2012-08-09 10:22:04 -04:00
Virgil Dupras
3441e51c0e [#200 state:fixed] Fixed a KeyError wihle parsing iTunes XML. 2012-08-09 10:01:44 -04:00
Virgil Dupras
a99c40b5d8 Updated PO files from POTs. 2012-08-09 09:58:14 -04:00
Virgil Dupras
5b4de58c38 Oops, I had forgot one of the credits file. 2012-08-08 16:53:08 -04:00
Virgil Dupras
cd83b16dbd Added Kyrill Detinov to credits for russian loc improvements. 2012-08-08 16:39:17 -04:00
Virgil Dupras
b67db988ab Improvement to the Russian loc by Kyrill Detinov. 2012-08-08 16:32:14 -04:00
Virgil Dupras
7ebea44cb0 Added tag se3.6.0 for changeset 0f18c4498a6c 2012-08-08 11:27:04 -04:00
33 changed files with 1100 additions and 1000 deletions

View File

@@ -8,8 +8,6 @@ run.py
*.pyd *.pyd
*.waf* *.waf*
.lock-waf* .lock-waf*
*.xcodeproj/xcuserdata
*.xcodeproj/project.xcworkspace/xcuserdata
conf.json conf.json
build build
dist dist
@@ -18,16 +16,6 @@ installer_tmp-cache
cocoa/autogen cocoa/autogen
cocoa/*/Info.plist cocoa/*/Info.plist
cocoa/*/build cocoa/*/build
cocoa/*/*.app
cs.lproj
de.lproj
fr.lproj
it.lproj
hy.lproj
ru.lproj
uk.lproj
zh_CN.lproj
pt_BR.lproj
qt/base/*_rc.py qt/base/*_rc.py
help/*/conf.py help/*/conf.py
help/*/changelog.rst help/*/changelog.rst

View File

@@ -78,3 +78,5 @@ c3d9f91dc9c9d60f370c72bc211f09be3e4fc18d se3.5.0
e772f1de86744999ffbbe5845554417965b1dfba me6.4.1 e772f1de86744999ffbbe5845554417965b1dfba me6.4.1
c8a9a4d355927e509f514308c82306192bc71f92 pe2.6.0 c8a9a4d355927e509f514308c82306192bc71f92 pe2.6.0
a618e954f01e4bbdbe9a03e5667a67d62be995a7 me6.4.2 a618e954f01e4bbdbe9a03e5667a67d62be995a7 me6.4.2
0f18c4498a6c7529bf77207db70aed8a5ec96ee4 se3.6.0
8f478379ec62fd1329d527aafb1ab0f2410f3a79 me6.5.0

View File

@@ -14,7 +14,6 @@ from appscript import app, its, k, CommandError, ApplicationNotFoundError
from . import tunes from . import tunes
from cocoa import as_fetch, proxy from cocoa import as_fetch, proxy
from hscommon import io
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.path import Path from hscommon.path import Path
from hscommon.util import remove_invalid_xml from hscommon.util import remove_invalid_xml
@@ -78,21 +77,21 @@ def get_itunes_database_path():
return Path(plistpath) return Path(plistpath)
def get_itunes_songs(plistpath): def get_itunes_songs(plistpath):
if not io.exists(plistpath): if not plistpath.exists():
return [] return []
s = io.open(plistpath, 'rt', encoding='utf-8').read() s = plistpath.open('rt', encoding='utf-8').read()
# iTunes sometimes produces XML files with invalid characters in it. # iTunes sometimes produces XML files with invalid characters in it.
s = remove_invalid_xml(s, replace_with='') s = remove_invalid_xml(s, replace_with='')
plist = plistlib.readPlistFromBytes(s.encode('utf-8')) plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = [] result = []
for song_data in plist['Tracks'].values(): for song_data in plist['Tracks'].values():
if song_data['Track Type'] != 'File':
continue
try: try:
if song_data['Track Type'] != 'File':
continue
song = ITunesSong(song_data) song = ITunesSong(song_data)
except KeyError: # No "Location" or "Track ID" key in track except KeyError: # No "Track Type", "Location" or "Track ID" key in track
continue continue
if io.exists(song.path): if song.path.exists():
result.append(song) result.append(song)
return result return result

View File

@@ -22,6 +22,7 @@ http://www.hardcoded.net/licenses/bsd_license
{@"size", 63, 16, 0, YES, nil}, {@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil}, {@"extension", 40, 16, 0, YES, nil},
{@"dimensions", 73, 16, 0, YES, nil}, {@"dimensions", 73, 16, 0, YES, nil},
{@"exif_timestamp", 120, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil}, {@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 58, 16, 0, YES, nil}, {@"percentage", 58, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil}, {@"dupe_count", 80, 16, 0, YES, nil},

View File

@@ -15,7 +15,6 @@ import time
import shutil import shutil
from send2trash import send2trash from send2trash import send2trash
from hscommon import io
from hscommon.reg import RegistrableApplication from hscommon.reg import RegistrableApplication
from hscommon.notify import Broadcaster from hscommon.notify import Broadcaster
from hscommon.path import Path from hscommon.path import Path
@@ -168,7 +167,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
if not io.exists(dupe.path): if not dupe.path.exists():
return return
logging.debug("Sending '%s' to trash", dupe.path) logging.debug("Sending '%s' to trash", dupe.path)
str_path = str(dupe.path) str_path = str(dupe.path)
@@ -253,7 +252,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
result = [] result = []
for file in files: for file in files:
try: try:
inode = io.stat(file.path).st_ino inode = file.path.stat().st_ino
except OSError: except OSError:
# The file was probably deleted or something # The file was probably deleted or something
continue continue
@@ -324,8 +323,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
if dest_type == DestType.Relative: if dest_type == DestType.Relative:
source_base = source_base[location_path:] source_base = source_base[location_path:]
dest_path = dest_path + source_base dest_path = dest_path + source_base
if not io.exists(dest_path): if not dest_path.exists():
io.makedirs(dest_path) dest_path.makedirs()
# Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes.
dest_path = dest_path + source_path[-1] dest_path = dest_path + source_path[-1]
logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path) logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path)

View File

@@ -10,7 +10,6 @@ from xml.etree import ElementTree as ET
import logging import logging
from jobprogress import job from jobprogress import job
from hscommon import io
from hscommon.path import Path from hscommon.path import Path
from hscommon.util import FileOrPath from hscommon.util import FileOrPath
@@ -73,9 +72,9 @@ class Directories:
file.is_ref = state == DirectoryState.Reference file.is_ref = state == DirectoryState.Reference
filepaths.add(file.path) filepaths.add(file.path)
yield file yield file
subpaths = [from_path + name for name in io.listdir(from_path)] subpaths = [from_path + name for name in from_path.listdir()]
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it # it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths] subfolders = [p for p in subpaths if not p.islink() and p.isdir() and p not in filepaths]
for subfolder in subfolders: for subfolder in subfolders:
for file in self._get_files(subfolder, j): for file in self._get_files(subfolder, j):
yield file yield file
@@ -106,7 +105,7 @@ class Directories:
""" """
if path in self: if path in self:
raise AlreadyThereError() raise AlreadyThereError()
if not io.exists(path): if not path.exists():
raise InvalidPathError() raise InvalidPathError()
self._dirs = [p for p in self._dirs if p not in path] self._dirs = [p for p in self._dirs if p not in path]
self._dirs.append(path) self._dirs.append(path)
@@ -115,7 +114,7 @@ class Directories:
def get_subfolders(path): def get_subfolders(path):
"""returns a sorted list of paths corresponding to subfolders in `path`""" """returns a sorted list of paths corresponding to subfolders in `path`"""
try: try:
names = [name for name in io.listdir(path) if io.isdir(path + name)] names = [name for name in path.listdir() if (path + name).isdir()]
names.sort(key=lambda x:x.lower()) names.sort(key=lambda x:x.lower())
return [path + name for name in names] return [path + name for name in names]
except EnvironmentError: except EnvironmentError:

View File

@@ -14,7 +14,6 @@
import hashlib import hashlib
import logging import logging
from hscommon import io
from hscommon.util import nonone, get_file_ext from hscommon.util import nonone, get_file_ext
NOT_SET = object() NOT_SET = object()
@@ -89,12 +88,12 @@ class File:
def _read_info(self, field): def _read_info(self, field):
if field in ('size', 'mtime'): if field in ('size', 'mtime'):
stats = io.stat(self.path) stats = self.path.stat()
self.size = nonone(stats.st_size, 0) self.size = nonone(stats.st_size, 0)
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field == 'md5partial': elif field == 'md5partial':
try: try:
fp = io.open(self.path, 'rb') fp = self.path.open('rb')
offset, size = self._get_md5partial_offset_and_size() offset, size = self._get_md5partial_offset_and_size()
fp.seek(offset) fp.seek(offset)
partialdata = fp.read(size) partialdata = fp.read(size)
@@ -105,7 +104,7 @@ class File:
pass pass
elif field == 'md5': elif field == 'md5':
try: try:
fp = io.open(self.path, 'rb') fp = self.path.open('rb')
md5 = hashlib.md5() md5 = hashlib.md5()
CHUNK_SIZE = 8192 CHUNK_SIZE = 8192
filedata = fp.read(CHUNK_SIZE) filedata = fp.read(CHUNK_SIZE)
@@ -130,19 +129,19 @@ class File:
#--- Public #--- Public
@classmethod @classmethod
def can_handle(cls, path): def can_handle(cls, path):
return not io.islink(path) and io.isfile(path) return not path.islink() and path.isfile()
def rename(self, newname): def rename(self, newname):
if newname == self.name: if newname == self.name:
return return
destpath = self.path[:-1] + newname destpath = self.path[:-1] + newname
if io.exists(destpath): if destpath.exists():
raise AlreadyExistsError(newname, self.path[:-1]) raise AlreadyExistsError(newname, self.path[:-1])
try: try:
io.rename(self.path, destpath) self.path.rename(destpath)
except EnvironmentError: except EnvironmentError:
raise OperationError(self) raise OperationError(self)
if not io.exists(destpath): if not destpath.exists():
raise OperationError(self) raise OperationError(self)
self.path = destpath self.path = destpath
@@ -180,7 +179,7 @@ class Folder(File):
if field in {'size', 'mtime'}: if field in {'size', 'mtime'}:
size = sum((f.size for f in self._all_items()), 0) size = sum((f.size for f in self._all_items()), 0)
self.size = size self.size = size
stats = io.stat(self.path) stats = self.path.stat()
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field in {'md5', 'md5partial'}: elif field in {'md5', 'md5partial'}:
# What's sensitive here is that we must make sure that subfiles' # What's sensitive here is that we must make sure that subfiles'
@@ -199,14 +198,14 @@ class Folder(File):
@property @property
def subfolders(self): def subfolders(self):
if self._subfolders is None: if self._subfolders is None:
subpaths = [self.path + name for name in io.listdir(self.path)] subpaths = [self.path + name for name in self.path.listdir()]
subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p)] subfolders = [p for p in subpaths if not p.islink() and p.isdir()]
self._subfolders = [Folder(p) for p in subfolders] self._subfolders = [Folder(p) for p in subfolders]
return self._subfolders return self._subfolders
@classmethod @classmethod
def can_handle(cls, path): def can_handle(cls, path):
return not io.islink(path) and io.isdir(path) return not path.islink() and path.isdir()
def get_file(path, fileclasses=[File]): def get_file(path, fileclasses=[File]):
@@ -225,7 +224,7 @@ def get_files(path, fileclasses=[File]):
raise raise
try: try:
paths = [combine_paths(path, name) for name in io.listdir(path)] paths = [combine_paths(path, name) for name in path.listdir()]
result = [] result = []
for path in paths: for path in paths:
file = get_file(path, fileclasses=fileclasses) file = get_file(path, fileclasses=fileclasses)

View File

@@ -11,7 +11,6 @@ import re
import os.path as op import os.path as op
from jobprogress import job from jobprogress import job
from hscommon import io
from hscommon.util import dedupe, rem_file_ext, get_file_ext from hscommon.util import dedupe, rem_file_ext, get_file_ext
from hscommon.trans import tr from hscommon.trans import tr
@@ -129,6 +128,11 @@ class Scanner:
matches = self._getmatches(files, j) matches = self._getmatches(files, j)
logging.info('Found %d matches' % len(matches)) logging.info('Found %d matches' % len(matches))
j.set_progress(100, tr("Removing false matches")) j.set_progress(100, tr("Removing false matches"))
# In removing what we call here "false matches", we first want to remove, if we scan by
# folders, we want to remove folder matches for which the parent is also in a match (they're
# "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the
# option isn't enabled, we want matches for which both files exist and, lastly, we don't
# want matches with both files as ref.
if self.scan_type == ScanType.Folders and matches: if self.scan_type == ScanType.Folders and matches:
allpath = {m.first.path for m in matches} allpath = {m.first.path for m in matches}
allpath |= {m.second.path for m in matches} allpath |= {m.second.path for m in matches}
@@ -143,7 +147,8 @@ class Scanner:
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove] matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
if not self.mix_file_kind: if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)] matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
if self.ignore_list: if self.ignore_list:
j = j.start_subjob(2) j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list")) iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))

View File

@@ -330,18 +330,22 @@ class TestCaseDupeGuruWithResults:
app = self.app app = self.app
# any other path that isn't a parent or child of the already added path # any other path that isn't a parent or child of the already added path
otherpath = Path(op.dirname(__file__)) otherpath = Path(op.dirname(__file__))
eq_(app.add_directory(otherpath), 0) app.add_directory(otherpath)
eq_(len(app.directories), 2) eq_(len(app.directories), 2)
def test_addDirectory_already_there(self, do_setup): def test_addDirectory_already_there(self, do_setup):
app = self.app app = self.app
otherpath = Path(op.dirname(__file__)) otherpath = Path(op.dirname(__file__))
eq_(app.add_directory(otherpath), 0) app.add_directory(otherpath)
eq_(app.add_directory(otherpath), 1) app.add_directory(otherpath)
eq_(len(app.view.messages), 1)
assert "already" in app.view.messages[0]
def test_addDirectory_does_not_exist(self, do_setup): def test_addDirectory_does_not_exist(self, do_setup):
app = self.app app = self.app
eq_(2,app.add_directory('/does_not_exist')) app.add_directory('/does_not_exist')
eq_(len(app.view.messages), 1)
assert "exist" in app.view.messages[0]
def test_ignore(self, do_setup): def test_ignore(self, do_setup):
app = self.app app = self.app

View File

@@ -24,6 +24,9 @@ from ..gui.prioritize_dialog import PrioritizeDialog
class DupeGuruView: class DupeGuruView:
JOB = nulljob JOB = nulljob
def __init__(self):
self.messages = []
def start_job(self, jobid, func, args=()): def start_job(self, jobid, func, args=()):
try: try:
func(self.JOB, *args) func(self.JOB, *args)
@@ -37,7 +40,7 @@ class DupeGuruView:
pass pass
def show_message(self, msg): def show_message(self, msg):
pass self.messages.append(msg)
def ask_yes_no(self, prompt): def ask_yes_no(self, prompt):
return True # always answer yes return True # always answer yes

View File

@@ -1,2 +1,2 @@
__version__ = '6.4.2' __version__ = '6.5.0'
__appname__ = 'dupeGuru Music Edition' __appname__ = 'dupeGuru Music Edition'

View File

@@ -1,2 +1,2 @@
__version__ = '2.6.0' __version__ = '2.7.0'
__appname__ = 'dupeGuru Picture Edition' __appname__ = 'dupeGuru Picture Edition'

View File

@@ -24,7 +24,7 @@ def get_delta_dimensions(value, ref_value):
class DupeGuru(DupeGuruBase): class DupeGuru(DupeGuruBase):
NAME = __appname__ NAME = __appname__
METADATA_TO_READ = ['size', 'mtime', 'dimensions'] METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
def __init__(self, view, appdata): def __init__(self, view, appdata):
DupeGuruBase.__init__(self, view, appdata) DupeGuruBase.__init__(self, view, appdata)
@@ -54,6 +54,7 @@ class DupeGuru(DupeGuruBase):
'size': format_size(size, 0, 1, False), 'size': format_size(size, 0, 1, False),
'extension': dupe.extension, 'extension': dupe.extension,
'dimensions': format_dimensions(dimensions), 'dimensions': format_dimensions(dimensions),
'exif_timestamp': dupe.exif_timestamp,
'mtime': format_timestamp(mtime, delta and m), 'mtime': format_timestamp(mtime, delta and m),
'percentage': format_perc(percentage), 'percentage': format_perc(percentage),
'dupe_count': format_dupe_count(dupe_count), 'dupe_count': format_dupe_count(dupe_count),

View File

@@ -7,6 +7,7 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import os import os
import os.path as op
import logging import logging
import sqlite3 as sqlite import sqlite3 as sqlite
@@ -30,7 +31,7 @@ def colors_to_string(colors):
# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff)) # result.append((number >> 16, (number >> 8) & 0xff, number & 0xff))
# return result # return result
class Cache(object): class Cache:
"""A class to cache picture blocks. """A class to cache picture blocks.
""" """
def __init__(self, db=':memory:'): def __init__(self, db=':memory:'):
@@ -72,29 +73,34 @@ class Cache(object):
result = self.con.execute(sql).fetchall() result = self.con.execute(sql).fetchall()
return result[0][0] return result[0][0]
def __setitem__(self, key, value): def __setitem__(self, path_str, blocks):
value = colors_to_string(value) blocks = colors_to_string(blocks)
if key in self: if op.exists(path_str):
sql = "update pictures set blocks = ? where path = ?" mtime = int(os.stat(path_str).st_mtime)
else: else:
sql = "insert into pictures(blocks,path) values(?,?)" mtime = 0
if path_str in self:
sql = "update pictures set blocks = ?, mtime = ? where path = ?"
else:
sql = "insert into pictures(blocks,mtime,path) values(?,?,?)"
try: try:
self.con.execute(sql, [value, key]) self.con.execute(sql, [blocks, mtime, path_str])
except sqlite.OperationalError: except sqlite.OperationalError:
logging.warning('Picture cache could not set value for key %r', key) logging.warning('Picture cache could not set value for key %r', path_str)
except sqlite.DatabaseError as e: except sqlite.DatabaseError as e:
logging.warning('DatabaseError while setting value for key %r: %s', key, str(e)) logging.warning('DatabaseError while setting value for key %r: %s', path_str, str(e))
def _create_con(self, second_try=False): def _create_con(self, second_try=False):
def create_tables(): def create_tables():
sql = "create table pictures(path TEXT, blocks TEXT)" logging.debug("Creating picture cache tables.")
self.con.execute(sql); self.con.execute("drop table if exists pictures");
sql = "create index idx_path on pictures (path)" self.con.execute("drop index if exists idx_path");
self.con.execute(sql) self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)");
self.con.execute("create index idx_path on pictures (path)")
self.con = sqlite.connect(self.dbname, isolation_level=None) self.con = sqlite.connect(self.dbname, isolation_level=None)
try: try:
self.con.execute("select * from pictures where 1=2") self.con.execute("select path, mtime, blocks from pictures where 1=2")
except sqlite.OperationalError: # new db except sqlite.OperationalError: # new db
create_tables() create_tables()
except sqlite.DatabaseError as e: # corrupted db except sqlite.DatabaseError as e: # corrupted db
@@ -134,3 +140,23 @@ class Cache(object):
cur = self.con.execute(sql) cur = self.con.execute(sql)
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
def purge_outdated(self):
"""Go through the cache and purge outdated records.
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
the db.
"""
todelete = []
sql = "select rowid, path, mtime from pictures"
cur = self.con.execute(sql)
for rowid, path_str, mtime in cur:
if mtime and op.exists(path_str):
picture_mtime = os.stat(path_str).st_mtime
if int(picture_mtime) <= mtime:
# not outdated
continue
todelete.append(rowid)
if todelete:
sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete))
self.con.execute(sql)

View File

@@ -55,6 +55,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
# MemoryError happens when trying to read an image file, which is freed from memory by the # MemoryError happens when trying to read an image file, which is freed from memory by the
# time that MemoryError is raised. # time that MemoryError is raised.
cache = Cache(cache_path) cache = Cache(cache_path)
cache.purge_outdated()
prepared = [] # only pictures for which there was no error getting blocks prepared = [] # only pictures for which there was no error getting blocks
try: try:
for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")): for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")):

View File

@@ -6,26 +6,18 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import logging
from collections import defaultdict from collections import defaultdict
from itertools import combinations from itertools import combinations
from hscommon import io
from hscommon.trans import tr from hscommon.trans import tr
from core.engine import Match from core.engine import Match
from . import exif
def getmatches(files, match_scaled, j): def getmatches(files, match_scaled, j):
timestamp2pic = defaultdict(set) timestamp2pic = defaultdict(set)
for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")): for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")):
try: timestamp = picture.exif_timestamp
with io.open(picture.path, 'rb') as fp: timestamp2pic[timestamp].add(picture)
exifdata = exif.get_fields(fp)
timestamp = exifdata['DateTimeOriginal']
timestamp2pic[timestamp].add(picture)
except Exception:
logging.info("Couldn't read EXIF of picture: %s", picture.path)
if '0000:00:00 00:00:00' in timestamp2pic: # very likely false matches if '0000:00:00 00:00:00' in timestamp2pic: # very likely false matches
del timestamp2pic['0000:00:00 00:00:00'] del timestamp2pic['0000:00:00 00:00:00']
matches = [] matches = []

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from hscommon import io import logging
from hscommon.util import get_file_ext from hscommon.util import get_file_ext
from core import fs from core import fs
from . import exif from . import exif
@@ -15,6 +15,7 @@ class Photo(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO = fs.File.INITIAL_INFO.copy()
INITIAL_INFO.update({ INITIAL_INFO.update({
'dimensions': (0,0), 'dimensions': (0,0),
'exif_timestamp': '',
}) })
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys()) __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
@@ -30,7 +31,7 @@ class Photo(fs.File):
def _get_orientation(self): def _get_orientation(self):
if not hasattr(self, '_cached_orientation'): if not hasattr(self, '_cached_orientation'):
try: try:
with io.open(self.path, 'rb') as fp: with self.path.open('rb') as fp:
exifdata = exif.get_fields(fp) exifdata = exif.get_fields(fp)
# the value is a list (probably one-sized) of ints # the value is a list (probably one-sized) of ints
orientations = exifdata['Orientation'] orientations = exifdata['Orientation']
@@ -49,6 +50,13 @@ class Photo(fs.File):
self.dimensions = self._plat_get_dimensions() self.dimensions = self._plat_get_dimensions()
if self._get_orientation() in {5, 6, 7, 8}: if self._get_orientation() in {5, 6, 7, 8}:
self.dimensions = (self.dimensions[1], self.dimensions[0]) self.dimensions = (self.dimensions[1], self.dimensions[0])
elif field == 'exif_timestamp':
try:
with self.path.open('rb') as fp:
exifdata = exif.get_fields(fp)
self.exif_timestamp = exifdata['DateTimeOriginal']
except Exception:
logging.info("Couldn't read EXIF of picture: %s", self.path)
def get_blocks(self, block_count_per_side): def get_blocks(self, block_count_per_side):
return self._plat_get_blocks(block_count_per_side, self._get_orientation()) return self._plat_get_blocks(block_count_per_side, self._get_orientation())

View File

@@ -20,6 +20,7 @@ class ResultTable(ResultTableBase):
Column('size', coltr("Size (KB)"), optional=True), Column('size', coltr("Size (KB)"), optional=True),
Column('extension', coltr("Kind"), visible=False, optional=True), Column('extension', coltr("Kind"), visible=False, optional=True),
Column('dimensions', coltr("Dimensions"), optional=True), Column('dimensions', coltr("Dimensions"), optional=True),
Column('exif_timestamp', coltr("EXIF Timestamp"), visible=False, optional=True),
Column('mtime', coltr("Modification"), visible=False, optional=True), Column('mtime', coltr("Modification"), visible=False, optional=True),
Column('percentage', coltr("Match %"), optional=True), Column('percentage', coltr("Match %"), optional=True),
Column('dupe_count', coltr("Dupe Count"), visible=False, optional=True), Column('dupe_count', coltr("Dupe Count"), visible=False, optional=True),

View File

@@ -141,5 +141,5 @@ class TestCaseCacheSQLEscape:
try: try:
del c["foo'bar"] del c["foo'bar"]
except KeyError: except KeyError:
self.fail() assert False

View File

@@ -1,3 +1,15 @@
=== 6.5.0 (2012-08-10)
* Added "Export to CSV". (#189)
* Added "Replace with symlinks" to complement "Replace with hardlinks". [Mac, Linux] (#194)
* dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204)
* Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198)
* Fixed result table cells which mistakenly became writable in v6.4.0. [Mac] (#203)
* Fixed "Rename Selected" which was broken since v6.4.0. [Mac] (#202)
* Fixed a bug where "Reset to Defaults" in the Columns menu wouldn't refresh menu items' marked state.
* Improved OGG metadata reading.
* Improved Russian localization by Kyrill Detinov.
=== 6.4.2 (2012-07-07) === 6.4.2 (2012-07-07)
* Fixed iTunes integration which was broken since iTunes 10.6.3. [Mac] * Fixed iTunes integration which was broken since iTunes 10.6.3. [Mac]

View File

@@ -1,3 +1,17 @@
=== 2.7.0 (2012-08-11)
* Added "Export to CSV". (#189)
* Added "Replace with symlinks" to complement "Replace with hardlinks". [Mac, Linux] (#194)
* Added "Exif Timestamp" column. (#201)
* dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204)
* Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198)
* Fixed result table cells which mistakenly became writable in v2.6.0. [Mac] (#203)
* Fixed "Rename Selected" which was broken since v2.6.0. [Mac] (#202)
* Fixed a bug where "Reset to Defaults" in the Columns menu wouldn't refresh menu items' marked state.
* Fixed a bug where outdated picture cache entries would result in false matches. (#199)
* Added Brazilian localization by Victor Figueiredo.
* Improved Russian localization by Kyrill Detinov.
=== 2.6.0 (2012-06-06) === 2.6.0 (2012-06-06)
* Added Aperture support. (#42) * Added Aperture support. (#42)

View File

@@ -18,6 +18,8 @@ Unten befindet sich die Liste aller Menschen, die direkt oder indirekt zu dupeGu
| **Igor Pavlov, Russian localization** | **Igor Pavlov, Russian localization**
| **Kyrill Detinov, Russian localization**
| **Yuri Petrashko, Ukrainian localization** | **Yuri Petrashko, Ukrainian localization**
| **Nickolas Pohilets, Ukrainian localization** | **Nickolas Pohilets, Ukrainian localization**

View File

@@ -20,6 +20,8 @@ Below is the list of people who contributed, directly or indirectly to dupeGuru.
| **Igor Pavlov, Russian localization** | **Igor Pavlov, Russian localization**
| **Kyrill Detinov, Russian localization**
| **Yuri Petrashko, Ukrainian localization** | **Yuri Petrashko, Ukrainian localization**
| **Nickolas Pohilets, Ukrainian localization** | **Nickolas Pohilets, Ukrainian localization**

View File

@@ -19,6 +19,8 @@ Voici la liste des contributeurs de dupeGuru. Merci!
| **Igor Pavlov, localisation russe** | **Igor Pavlov, localisation russe**
| **Kyrill Detinov, localisation russe**
| **Yuri Petrashko, localisation ukrainienne** | **Yuri Petrashko, localisation ukrainienne**
| **Nickolas Pohilets, localisation ukrainienne** | **Nickolas Pohilets, localisation ukrainienne**

View File

@@ -20,6 +20,8 @@
| **Igor Pavlov, Russian localization** | **Igor Pavlov, Russian localization**
| **Kyrill Detinov, Russian localization**
| **Yuri Petrashko, Ukrainian localization** | **Yuri Petrashko, Ukrainian localization**
| **Nickolas Pohilets, Ukrainian localization** | **Nickolas Pohilets, Ukrainian localization**

View File

@@ -20,6 +20,8 @@
| **Igor Pavlov, Russian localization** | **Igor Pavlov, Russian localization**
| **Kyrill Detinov, Russian localization**
| **Yuri Petrashko, Ukrainian localization** | **Yuri Petrashko, Ukrainian localization**
| **Nickolas Pohilets, Ukrainian localization** | **Nickolas Pohilets, Ukrainian localization**

View File

@@ -20,6 +20,8 @@
| **Igor Pavlov, Russian localization** | **Igor Pavlov, Russian localization**
| **Kyrill Detinov, Russian localization**
| **Yuri Petrashko, Ukrainian localization** | **Yuri Petrashko, Ukrainian localization**
| **Nickolas Pohilets, Ukrainian localization** | **Nickolas Pohilets, Ukrainian localization**

View File

@@ -839,7 +839,8 @@ msgstr "Remplacer les fichiers effacés par des liens"
msgid "" msgid ""
"After having deleted a duplicate, place a link targeting the reference file " "After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file." "to replace the deleted file."
msgstr "Après avoir effacé un fichier, remplacer celui-ci par un lien vers le " msgstr ""
"Après avoir effacé un fichier, remplacer celui-ci par un lien vers le "
"fichier référence." "fichier référence."
#: qt/base/deletion_options.py:45 #: qt/base/deletion_options.py:45

177
locale/ru/LC_MESSAGES/columns.po Executable file → Normal file
View File

@@ -1,120 +1,121 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Russian translation\n" "Project-Id-Version: dupeGuru\n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Igor Pavlov <IgorPavlov87@mail.ru>\n" "Last-Translator: Kyrill Detinov <lazy.kent@opensuse.org>\n"
"Language-Team: Igor Pavlov <IgorPavlov87@mail.ru>\n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"X-Poedit-Country: RUSSIAN FEDERATION\n" "X-Poedit-Country: RUSSIAN FEDERATION\n"
"X-Poedit-Language: Russian\n" "X-Poedit-Language: Russian\n"
#: core_me/result_table.py:28
msgid "Album"
msgstr "Альбом"
#: core_me/result_table.py:27
msgid "Artist"
msgstr "Исполнитель"
#: core_me/prioritize.py:22 core_me/result_table.py:22
msgid "Bitrate"
msgstr "Битрейт"
#: core_me/result_table.py:32
msgid "Comment"
msgstr "Комментарий"
#: core_pe/prioritize.py:16 core_pe/result_table.py:22
msgid "Dimensions"
msgstr "Размеры"
#: core_me/result_table.py:35 core_pe/result_table.py:25
#: core_se/result_table.py:25
msgid "Dupe Count"
msgstr "Количество дубликатов"
#: core_me/prioritize.py:16
msgid "Duration"
msgstr "Продолжительность"
#: core/gui/problem_table.py:18
msgid "Error Message"
msgstr "Сообщение об ошибке"
#: core/gui/ignore_list_table.py:18 core/gui/ignore_list_table.py:19 #: core/gui/ignore_list_table.py:18 core/gui/ignore_list_table.py:19
#: core/gui/problem_table.py:17 #: core/gui/problem_table.py:17
msgid "File Path" msgid "File Path"
msgstr "Путь к файлу" msgstr "Путь к файлу"
#: core/gui/problem_table.py:18 #: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18
msgid "Error Message" #: core_se/result_table.py:18
msgstr "Сообщение об ошибке" msgid "Filename"
msgstr "Имя файла"
#: core/prioritize.py:72 core_me/result_table.py:19 core_pe/result_table.py:19
#: core_se/result_table.py:19
msgid "Folder"
msgstr "Каталог"
#: core_me/result_table.py:29
msgid "Genre"
msgstr "Жанр"
#: core/prioritize.py:63 core_me/result_table.py:24 core_pe/result_table.py:21 #: core/prioritize.py:63 core_me/result_table.py:24 core_pe/result_table.py:21
#: core_se/result_table.py:21 #: core_se/result_table.py:21
msgid "Kind" msgid "Kind"
msgstr "Тип" msgstr "Тип"
#: core/prioritize.py:72 core_me/result_table.py:19 core_pe/result_table.py:19
#: core_se/result_table.py:19
msgid "Folder"
msgstr "Папка"
#: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18
#: core_se/result_table.py:18
msgid "Filename"
msgstr "Имя файла"
#: core/prioritize.py:147
msgid "Size"
msgstr "Размер"
#: core/prioritize.py:153 core_me/result_table.py:25
#: core_pe/result_table.py:23 core_se/result_table.py:22
msgid "Modification"
msgstr "Модификация"
#: core_me/prioritize.py:16
msgid "Duration"
msgstr "Продолжительность"
#: core_me/prioritize.py:22 core_me/result_table.py:22
msgid "Bitrate"
msgstr "Битрейт"
#: core_me/prioritize.py:28
msgid "Samplerate"
msgstr "Частота оцифровки"
#: core_me/result_table.py:20
msgid "Size (MB)"
msgstr "Размер (Мб)"
#: core_me/result_table.py:21
msgid "Time"
msgstr "Время"
#: core_me/result_table.py:23
msgid "Sample Rate"
msgstr "Частота"
#: core_me/result_table.py:26
msgid "Title"
msgstr "Название"
#: core_me/result_table.py:27
msgid "Artist"
msgstr "Артист"
#: core_me/result_table.py:28
msgid "Album"
msgstr "Альбом"
#: core_me/result_table.py:29
msgid "Genre"
msgstr "Жанр"
#: core_me/result_table.py:30
msgid "Year"
msgstr "Год"
#: core_me/result_table.py:31
msgid "Track Number"
msgstr "Номер дорожки"
#: core_me/result_table.py:32
msgid "Comment"
msgstr "Комментарий"
#: core_me/result_table.py:33 core_pe/result_table.py:24 #: core_me/result_table.py:33 core_pe/result_table.py:24
#: core_se/result_table.py:23 #: core_se/result_table.py:23
msgid "Match %" msgid "Match %"
msgstr "Совпадение %" msgstr "Совпадение %"
#: core_me/result_table.py:34 core_se/result_table.py:24 #: core/prioritize.py:153 core_me/result_table.py:25
msgid "Words Used" #: core_pe/result_table.py:23 core_se/result_table.py:22
msgstr "Слов, используемых" msgid "Modification"
msgstr "Время изменения"
#: core_me/result_table.py:35 core_pe/result_table.py:25 #: core_me/result_table.py:23
#: core_se/result_table.py:25 msgid "Sample Rate"
msgid "Dupe Count" msgstr "Частота"
msgstr "Dupe графа"
#: core_pe/prioritize.py:16 core_pe/result_table.py:22 #: core_me/prioritize.py:28
msgid "Dimensions" msgid "Samplerate"
msgstr "Размеры" msgstr "Частота оцифровки"
#: core/prioritize.py:147
msgid "Size"
msgstr "Размер"
#: core_pe/result_table.py:20 core_se/result_table.py:20 #: core_pe/result_table.py:20 core_se/result_table.py:20
msgid "Size (KB)" msgid "Size (KB)"
msgstr "Размер (KB)" msgstr "Размер (КБ)"
#: core_me/result_table.py:20
msgid "Size (MB)"
msgstr "Размер (МБ)"
#: core_me/result_table.py:21
msgid "Time"
msgstr "Время"
#: core_me/result_table.py:26
msgid "Title"
msgstr "Название"
#: core_me/result_table.py:31
msgid "Track Number"
msgstr "Номер дорожки"
#: core_me/result_table.py:34 core_se/result_table.py:24
msgid "Words Used"
msgstr "Использованные слова"
#: core_me/result_table.py:30
msgid "Year"
msgstr "Год"

280
locale/ru/LC_MESSAGES/core.po Executable file → Normal file
View File

@@ -1,37 +1,28 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Russian translation\n" "Project-Id-Version: dupeGuru\n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Igor Pavlov <IgorPavlov87@mail.ru>\n" "Last-Translator: Kyrill Detinov <lazy.kent@opensuse.org>\n"
"Language-Team: Igor Pavlov <IgorPavlov87@mail.ru>\n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Country: RUSSIAN FEDERATION\n" "Language: ru_RU\n"
"X-Poedit-Language: Russian\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
#: core/app.py:97 #: core/results.py:120
msgid "will only be able to delete, move or copy 10 duplicates at once" msgid " filter: %s"
msgstr "" msgstr "фильтр: %s"
"сможете только для удаления, перемещения или копирования 10 копий сразу"
#: core/app.py:273 #: core/results.py:113
msgid "" msgid "%d / %d (%s / %s) duplicates marked."
"You cannot delete, move or copy more than 10 duplicates at once in demo " msgstr "%d / %d (%s / %s) дубликатов отмечено."
"mode."
msgstr ""
"Вы не можете удалять, перемещать или копировать более 10 дубликатов сразу в "
"демонстрационном режиме."
#: core/app.py:543 #: core/engine.py:196 core/engine.py:223
msgid "Collecting files to scan" msgid "%d matches found"
msgstr "Сбор файлов для сканирования" msgstr "%d совпадений найдено"
#: core/app.py:554
msgid "The selected directories contain no scannable file."
msgstr "Выбранных директорий не содержат сканируемых файлов."
#: core/app.py:593 #: core/app.py:593
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
@@ -39,31 +30,56 @@ msgstr "%s. (%d отменено)"
#: core/engine.py:178 core/engine.py:215 #: core/engine.py:178 core/engine.py:215
msgid "0 matches found" msgid "0 matches found"
msgstr "0 сопоставлений найдено" msgstr "0 совпадений найдено"
#: core/engine.py:196 core/engine.py:223 #: core/app.py:244
msgid "%d matches found" msgid "All marked files were copied sucessfully."
msgstr "%d сопоставлений найдено" msgstr "Все отмеченные файлы были скопированы успешно."
#: core/engine.py:208 core/scanner.py:80 #: core/app.py:245
msgid "Read size of %d/%d files" msgid "All marked files were moved sucessfully."
msgstr "Реалний размер %d/%d файлов" msgstr "Все отмеченные файлы были перемещены успешно."
#: core/engine.py:360 #: core/app.py:246
msgid "Grouped %d/%d matches" msgid "All marked files were sucessfully sent to Trash."
msgstr "Группировка %d/%d совпадений" msgstr "Все отмеченные файлы были успешно отправлены в Корзину."
#: core/prioritize.py:68 #: core/app.py:293
msgid "None" msgid ""
msgstr "Ни один" "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr ""
"Все выбранные %d совпадений будут игнорироваться при всех последующих "
"проверках. Продолжить?"
#: core_pe/matchblock.py:60
msgid "Analyzed %d/%d pictures"
msgstr "Анализируется %d/%d изображений"
#: core/app.py:543
msgid "Collecting files to scan"
msgstr "Сбор файлов для сканирования"
#: core/gui/ignore_list_dialog.py:24
msgid "Do you really want to remove all %d items from the ignore list?"
msgstr ""
"Вы действительно хотите удалить все элементы %d из списка игнорирования?"
#: core/prioritize.py:97
msgid "Doesn't end with number"
msgstr "Не заканчивается номером"
#: core/scanner.py:171
msgid "Doing group prioritization"
msgstr "Выполняется приоритезация групп"
#: core/prioritize.py:96 #: core/prioritize.py:96
msgid "Ends with number" msgid "Ends with number"
msgstr "Заканчивается номером" msgstr "Заканчивается номером"
#: core/prioritize.py:97 #: core/engine.py:360
msgid "Doesn't end with number" msgid "Grouped %d/%d matches"
msgstr "Не заканчивается с номером" msgstr "Группировка %d/%d совпадений"
#: core/prioritize.py:132 #: core/prioritize.py:132
msgid "Highest" msgid "Highest"
@@ -71,59 +87,95 @@ msgstr "Наивысший"
#: core/prioritize.py:132 #: core/prioritize.py:132
msgid "Lowest" msgid "Lowest"
msgstr "Самая низкая" msgstr "Самый низкий"
#: core/prioritize.py:159 #: core/prioritize.py:159
msgid "Newest" msgid "Newest"
msgstr "Новейший" msgstr "Новейший"
#: core/app.py:231
msgid "No duplicates found."
msgstr "Дубликаты не найдены."
#: core/prioritize.py:68
msgid "None"
msgstr "Ни один"
#: core/prioritize.py:159 #: core/prioritize.py:159
msgid "Oldest" msgid "Oldest"
msgstr "Старейшие" msgstr "Старейшие"
#: core/results.py:113 # Not sure.
msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) дубликаты обозначены."
#: core/results.py:120
msgid " filter: %s"
msgstr "филтр. %s"
#: core/scanner.py:100
msgid "Read metadata of %d/%d files"
msgstr "Прочитано метаданных %d/%d файлов"
#: core/scanner.py:131
msgid "Removing false matches"
msgstr "Удаление ложных совпадениях"
#: core/scanner.py:149
msgid "Processed %d/%d matches against the ignore list"
msgstr "Обработано %d/%d матчей против игнор-лист"
#: core/scanner.py:171
msgid "Doing group prioritization"
msgstr "Делая группы приоритетов"
#: core_pe/matchblock.py:60
msgid "Analyzed %d/%d pictures"
msgstr "Анализируется %d/%d фотографии"
#: core_pe/matchblock.py:152 #: core_pe/matchblock.py:152
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Исполняет %d/%d совпадениях кусоков" msgstr "Выполнено %d/%d совпадений блоков"
#: core_pe/matchblock.py:157 #: core_pe/matchblock.py:157
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Подготовка для сравнения" msgstr "Подготовка для сравнения"
#: core_pe/matchblock.py:192 # Not sure.
msgid "Verified %d/%d matches" #: core/scanner.py:149
msgstr "Проверенные %d/%d совпадениях" msgid "Processed %d/%d matches against the ignore list"
msgstr "Обработано %d/%d совпадений используя список игнорирования"
#: core_pe/matchexif.py:21 #: core_pe/matchexif.py:21
msgid "Read EXIF of %d/%d pictures" msgid "Read EXIF of %d/%d pictures"
msgstr "Прочитано EXIF %d/%d из фотографии" msgstr "Прочитана EXIF-информация %d/%d фотографий"
#: core/scanner.py:100
msgid "Read metadata of %d/%d files"
msgstr "Прочитаны метаданные %d/%d файлов"
#: core/engine.py:208 core/scanner.py:80
msgid "Read size of %d/%d files"
msgstr "Подсчитан размер %d/%d файлов"
#: core/scanner.py:131
msgid "Removing false matches"
msgstr "Удаление ложных совпадений"
#: core/app.py:354
msgid "Select a directory to {} marked files to"
msgstr "Выберите каталог {} для отмеченных файлов"
#: core/app.py:554
msgid "The selected directories contain no scannable file."
msgstr "Выбранные каталоги не содержат файлов для сканирования."
#: core_pe/matchblock.py:192
msgid "Verified %d/%d matches"
msgstr "Проверено %d/%d совпадений"
#: core/app.py:492 core/app.py:503
msgid "You are about to remove %d files from results. Continue?"
msgstr "Вы собираетесь удалить %d файлов из результата поиска. Продолжить?"
#: core/app.py:273
msgid ""
"You cannot delete, move or copy more than 10 duplicates at once in demo "
"mode."
msgstr ""
"Вы не можете удалять, перемещать или копировать более 10 дубликатов за один "
"раз в демонстрационном режиме."
#: core/app.py:405
msgid "You have no custom command set up. Set it up in your preferences."
msgstr "Вы не создали пользовательскую команду. Задайте её в настройках."
#: core/app.py:353
msgid "copy"
msgstr "копирование"
#: core/app.py:353
msgid "move"
msgstr "перемещение"
#: core/app.py:97
msgid "will only be able to delete, move or copy 10 duplicates at once"
msgstr ""
"вы сможете удалить, переместить или скопировать только 10 дубликатов за один"
" раз"
#: core/app.py:38 #: core/app.py:38
msgid "There are no marked duplicates. Nothing has been done." msgid "There are no marked duplicates. Nothing has been done."
@@ -133,72 +185,6 @@ msgstr ""
msgid "There are no selected duplicates. Nothing has been done." msgid "There are no selected duplicates. Nothing has been done."
msgstr "" msgstr ""
#: core/app.py:231
msgid "No duplicates found."
msgstr "Дубликаты не найдены."
#: core/app.py:244
msgid "All marked files were copied sucessfully."
msgstr "Все выбранные файлы были скопированы успешно."
#: core/app.py:245
msgid "All marked files were moved sucessfully."
msgstr "Все выбранные файлы были перемещены успешно."
#: core/app.py:246
msgid "All marked files were sucessfully sent to Trash."
msgstr "Все выбранные файлы были успешно отправлены в корзину."
#: core/app.py:293
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr ""
"Все выбранные %d матчей будут игнорироваться во всех последующих проверок. "
"Продолжить?"
#: core/gui/ignore_list_dialog.py:24
msgid "Do you really want to remove all %d items from the ignore list?"
msgstr "Вы действительно хотите удалить все элементы %d из черного списка?"
#: core/app.py:405
msgid "You have no custom command set up. Set it up in your preferences."
msgstr ""
"У вас нет пользовательской команды создали. Установите его в ваших "
"предпочтениях."
#: core/app.py:492 core/app.py:503
msgid "You are about to remove %d files from results. Continue?"
msgstr "Вы собираетесь удалить файлы %d из результата поиска. Продолжить?"
#: core/app.py:353
msgid "copy"
msgstr "копия"
#: core/app.py:353
msgid "move"
msgstr "перемещение"
#: core/app.py:354
msgid "Select a directory to {} marked files to"
msgstr "Выберите каталог на {} отмеченные файлы"
#: core/gui/deletion_options.py:21
msgid "You are sending {} file(s) to the Trash."
msgstr ""
#: core/prioritize.py:98
msgid "Longest"
msgstr ""
#: core/prioritize.py:99
msgid "Shortest"
msgstr ""
#: core/app.py:523
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr ""
#: core/app.py:284 #: core/app.py:284
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "" msgstr ""
@@ -210,3 +196,19 @@ msgstr ""
#: core/app.py:380 #: core/app.py:380
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "" msgstr ""
#: core/app.py:523
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr ""
#: core/gui/deletion_options.py:21
msgid "You are sending {} file(s) to the Trash."
msgstr ""
#: core/prioritize.py:98
msgid "Longest"
msgstr ""
#: core/prioritize.py:99
msgid "Shortest"
msgstr ""

1413
locale/ru/LC_MESSAGES/ui.po Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ class ResultsModel(ResultsModelBase):
Column('size', defaultWidth=60), Column('size', defaultWidth=60),
Column('extension', defaultWidth=40), Column('extension', defaultWidth=40),
Column('dimensions', defaultWidth=100), Column('dimensions', defaultWidth=100),
Column('exif_timestamp', defaultWidth=120),
Column('mtime', defaultWidth=120), Column('mtime', defaultWidth=120),
Column('percentage', defaultWidth=60), Column('percentage', defaultWidth=60),
Column('dupe_count', defaultWidth=80), Column('dupe_count', defaultWidth=80),

View File

@@ -2,6 +2,6 @@ jobprogress>=1.0.4
Send2Trash3k>=1.2.0 Send2Trash3k>=1.2.0
sphinx>=1.1.3 sphinx>=1.1.3
polib>=0.7.0 polib>=0.7.0
hsaudiotag3k>=1.1.2 hsaudiotag3k>=1.1.3
pytest>=2.0.0 pytest>=2.0.0
pytest-monkeyplus>=1.0.0 pytest-monkeyplus>=1.0.0