mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 08:01:39 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573d088088 | ||
|
|
75b08125c0 | ||
|
|
20320f539f | ||
|
|
24771af955 | ||
|
|
2bfe9960f1 | ||
|
|
215bcb0d76 | ||
|
|
2dbf8b80ae | ||
|
|
470cd92030 | ||
|
|
111edc3ce5 | ||
|
|
df30a31782 | ||
|
|
91f3a59523 | ||
|
|
3441e51c0e | ||
|
|
a99c40b5d8 | ||
|
|
5b4de58c38 | ||
|
|
cd83b16dbd | ||
|
|
b67db988ab | ||
|
|
7ebea44cb0 |
12
.hgignore
12
.hgignore
@@ -8,8 +8,6 @@ run.py
|
||||
*.pyd
|
||||
*.waf*
|
||||
.lock-waf*
|
||||
*.xcodeproj/xcuserdata
|
||||
*.xcodeproj/project.xcworkspace/xcuserdata
|
||||
conf.json
|
||||
build
|
||||
dist
|
||||
@@ -18,16 +16,6 @@ installer_tmp-cache
|
||||
cocoa/autogen
|
||||
cocoa/*/Info.plist
|
||||
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
|
||||
help/*/conf.py
|
||||
help/*/changelog.rst
|
||||
2
.hgtags
2
.hgtags
@@ -78,3 +78,5 @@ c3d9f91dc9c9d60f370c72bc211f09be3e4fc18d se3.5.0
|
||||
e772f1de86744999ffbbe5845554417965b1dfba me6.4.1
|
||||
c8a9a4d355927e509f514308c82306192bc71f92 pe2.6.0
|
||||
a618e954f01e4bbdbe9a03e5667a67d62be995a7 me6.4.2
|
||||
0f18c4498a6c7529bf77207db70aed8a5ec96ee4 se3.6.0
|
||||
8f478379ec62fd1329d527aafb1ab0f2410f3a79 me6.5.0
|
||||
|
||||
@@ -14,7 +14,6 @@ from appscript import app, its, k, CommandError, ApplicationNotFoundError
|
||||
from . import tunes
|
||||
|
||||
from cocoa import as_fetch, proxy
|
||||
from hscommon import io
|
||||
from hscommon.trans import trget
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import remove_invalid_xml
|
||||
@@ -78,21 +77,21 @@ def get_itunes_database_path():
|
||||
return Path(plistpath)
|
||||
|
||||
def get_itunes_songs(plistpath):
|
||||
if not io.exists(plistpath):
|
||||
if not plistpath.exists():
|
||||
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.
|
||||
s = remove_invalid_xml(s, replace_with='')
|
||||
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
|
||||
result = []
|
||||
for song_data in plist['Tracks'].values():
|
||||
try:
|
||||
if song_data['Track Type'] != 'File':
|
||||
continue
|
||||
try:
|
||||
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
|
||||
if io.exists(song.path):
|
||||
if song.path.exists():
|
||||
result.append(song)
|
||||
return result
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
{@"size", 63, 16, 0, YES, nil},
|
||||
{@"extension", 40, 16, 0, YES, nil},
|
||||
{@"dimensions", 73, 16, 0, YES, nil},
|
||||
{@"exif_timestamp", 120, 16, 0, YES, nil},
|
||||
{@"mtime", 120, 16, 0, YES, nil},
|
||||
{@"percentage", 58, 16, 0, YES, nil},
|
||||
{@"dupe_count", 80, 16, 0, YES, nil},
|
||||
|
||||
@@ -15,7 +15,6 @@ import time
|
||||
import shutil
|
||||
|
||||
from send2trash import send2trash
|
||||
from hscommon import io
|
||||
from hscommon.reg import RegistrableApplication
|
||||
from hscommon.notify import Broadcaster
|
||||
from hscommon.path import Path
|
||||
@@ -168,7 +167,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self.results.perform_on_marked(op, True)
|
||||
|
||||
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
|
||||
if not io.exists(dupe.path):
|
||||
if not dupe.path.exists():
|
||||
return
|
||||
logging.debug("Sending '%s' to trash", dupe.path)
|
||||
str_path = str(dupe.path)
|
||||
@@ -253,7 +252,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
result = []
|
||||
for file in files:
|
||||
try:
|
||||
inode = io.stat(file.path).st_ino
|
||||
inode = file.path.stat().st_ino
|
||||
except OSError:
|
||||
# The file was probably deleted or something
|
||||
continue
|
||||
@@ -324,8 +323,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
if dest_type == DestType.Relative:
|
||||
source_base = source_base[location_path:]
|
||||
dest_path = dest_path + source_base
|
||||
if not io.exists(dest_path):
|
||||
io.makedirs(dest_path)
|
||||
if not dest_path.exists():
|
||||
dest_path.makedirs()
|
||||
# Add filename to dest_path. For file move/copy, it's not required, but for folders, yes.
|
||||
dest_path = dest_path + source_path[-1]
|
||||
logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path)
|
||||
|
||||
@@ -10,7 +10,6 @@ from xml.etree import ElementTree as ET
|
||||
import logging
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon import io
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import FileOrPath
|
||||
|
||||
@@ -73,9 +72,9 @@ class Directories:
|
||||
file.is_ref = state == DirectoryState.Reference
|
||||
filepaths.add(file.path)
|
||||
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
|
||||
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 file in self._get_files(subfolder, j):
|
||||
yield file
|
||||
@@ -106,7 +105,7 @@ class Directories:
|
||||
"""
|
||||
if path in self:
|
||||
raise AlreadyThereError()
|
||||
if not io.exists(path):
|
||||
if not path.exists():
|
||||
raise InvalidPathError()
|
||||
self._dirs = [p for p in self._dirs if p not in path]
|
||||
self._dirs.append(path)
|
||||
@@ -115,7 +114,7 @@ class Directories:
|
||||
def get_subfolders(path):
|
||||
"""returns a sorted list of paths corresponding to subfolders in `path`"""
|
||||
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())
|
||||
return [path + name for name in names]
|
||||
except EnvironmentError:
|
||||
|
||||
25
core/fs.py
25
core/fs.py
@@ -14,7 +14,6 @@
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from hscommon import io
|
||||
from hscommon.util import nonone, get_file_ext
|
||||
|
||||
NOT_SET = object()
|
||||
@@ -89,12 +88,12 @@ class File:
|
||||
|
||||
def _read_info(self, field):
|
||||
if field in ('size', 'mtime'):
|
||||
stats = io.stat(self.path)
|
||||
stats = self.path.stat()
|
||||
self.size = nonone(stats.st_size, 0)
|
||||
self.mtime = nonone(stats.st_mtime, 0)
|
||||
elif field == 'md5partial':
|
||||
try:
|
||||
fp = io.open(self.path, 'rb')
|
||||
fp = self.path.open('rb')
|
||||
offset, size = self._get_md5partial_offset_and_size()
|
||||
fp.seek(offset)
|
||||
partialdata = fp.read(size)
|
||||
@@ -105,7 +104,7 @@ class File:
|
||||
pass
|
||||
elif field == 'md5':
|
||||
try:
|
||||
fp = io.open(self.path, 'rb')
|
||||
fp = self.path.open('rb')
|
||||
md5 = hashlib.md5()
|
||||
CHUNK_SIZE = 8192
|
||||
filedata = fp.read(CHUNK_SIZE)
|
||||
@@ -130,19 +129,19 @@ class File:
|
||||
#--- Public
|
||||
@classmethod
|
||||
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):
|
||||
if newname == self.name:
|
||||
return
|
||||
destpath = self.path[:-1] + newname
|
||||
if io.exists(destpath):
|
||||
if destpath.exists():
|
||||
raise AlreadyExistsError(newname, self.path[:-1])
|
||||
try:
|
||||
io.rename(self.path, destpath)
|
||||
self.path.rename(destpath)
|
||||
except EnvironmentError:
|
||||
raise OperationError(self)
|
||||
if not io.exists(destpath):
|
||||
if not destpath.exists():
|
||||
raise OperationError(self)
|
||||
self.path = destpath
|
||||
|
||||
@@ -180,7 +179,7 @@ class Folder(File):
|
||||
if field in {'size', 'mtime'}:
|
||||
size = sum((f.size for f in self._all_items()), 0)
|
||||
self.size = size
|
||||
stats = io.stat(self.path)
|
||||
stats = self.path.stat()
|
||||
self.mtime = nonone(stats.st_mtime, 0)
|
||||
elif field in {'md5', 'md5partial'}:
|
||||
# What's sensitive here is that we must make sure that subfiles'
|
||||
@@ -199,14 +198,14 @@ class Folder(File):
|
||||
@property
|
||||
def subfolders(self):
|
||||
if self._subfolders is None:
|
||||
subpaths = [self.path + name for name in io.listdir(self.path)]
|
||||
subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p)]
|
||||
subpaths = [self.path + name for name in self.path.listdir()]
|
||||
subfolders = [p for p in subpaths if not p.islink() and p.isdir()]
|
||||
self._subfolders = [Folder(p) for p in subfolders]
|
||||
return self._subfolders
|
||||
|
||||
@classmethod
|
||||
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]):
|
||||
@@ -225,7 +224,7 @@ def get_files(path, fileclasses=[File]):
|
||||
raise
|
||||
|
||||
try:
|
||||
paths = [combine_paths(path, name) for name in io.listdir(path)]
|
||||
paths = [combine_paths(path, name) for name in path.listdir()]
|
||||
result = []
|
||||
for path in paths:
|
||||
file = get_file(path, fileclasses=fileclasses)
|
||||
|
||||
@@ -11,7 +11,6 @@ import re
|
||||
import os.path as op
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon import io
|
||||
from hscommon.util import dedupe, rem_file_ext, get_file_ext
|
||||
from hscommon.trans import tr
|
||||
|
||||
@@ -129,6 +128,11 @@ class Scanner:
|
||||
matches = self._getmatches(files, j)
|
||||
logging.info('Found %d matches' % len(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:
|
||||
allpath = {m.first.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]
|
||||
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 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:
|
||||
j = j.start_subjob(2)
|
||||
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
||||
|
||||
@@ -330,18 +330,22 @@ class TestCaseDupeGuruWithResults:
|
||||
app = self.app
|
||||
# any other path that isn't a parent or child of the already added path
|
||||
otherpath = Path(op.dirname(__file__))
|
||||
eq_(app.add_directory(otherpath), 0)
|
||||
app.add_directory(otherpath)
|
||||
eq_(len(app.directories), 2)
|
||||
|
||||
def test_addDirectory_already_there(self, do_setup):
|
||||
app = self.app
|
||||
otherpath = Path(op.dirname(__file__))
|
||||
eq_(app.add_directory(otherpath), 0)
|
||||
eq_(app.add_directory(otherpath), 1)
|
||||
app.add_directory(otherpath)
|
||||
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):
|
||||
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):
|
||||
app = self.app
|
||||
|
||||
@@ -24,6 +24,9 @@ from ..gui.prioritize_dialog import PrioritizeDialog
|
||||
class DupeGuruView:
|
||||
JOB = nulljob
|
||||
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
|
||||
def start_job(self, jobid, func, args=()):
|
||||
try:
|
||||
func(self.JOB, *args)
|
||||
@@ -37,7 +40,7 @@ class DupeGuruView:
|
||||
pass
|
||||
|
||||
def show_message(self, msg):
|
||||
pass
|
||||
self.messages.append(msg)
|
||||
|
||||
def ask_yes_no(self, prompt):
|
||||
return True # always answer yes
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '6.4.2'
|
||||
__version__ = '6.5.0'
|
||||
__appname__ = 'dupeGuru Music Edition'
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '2.6.0'
|
||||
__version__ = '2.7.0'
|
||||
__appname__ = 'dupeGuru Picture Edition'
|
||||
@@ -24,7 +24,7 @@ def get_delta_dimensions(value, ref_value):
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
NAME = __appname__
|
||||
METADATA_TO_READ = ['size', 'mtime', 'dimensions']
|
||||
METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
|
||||
|
||||
def __init__(self, view, appdata):
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
@@ -54,6 +54,7 @@ class DupeGuru(DupeGuruBase):
|
||||
'size': format_size(size, 0, 1, False),
|
||||
'extension': dupe.extension,
|
||||
'dimensions': format_dimensions(dimensions),
|
||||
'exif_timestamp': dupe.exif_timestamp,
|
||||
'mtime': format_timestamp(mtime, delta and m),
|
||||
'percentage': format_perc(percentage),
|
||||
'dupe_count': format_dupe_count(dupe_count),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import os
|
||||
import os.path as op
|
||||
import logging
|
||||
import sqlite3 as sqlite
|
||||
|
||||
@@ -30,7 +31,7 @@ def colors_to_string(colors):
|
||||
# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff))
|
||||
# return result
|
||||
|
||||
class Cache(object):
|
||||
class Cache:
|
||||
"""A class to cache picture blocks.
|
||||
"""
|
||||
def __init__(self, db=':memory:'):
|
||||
@@ -72,29 +73,34 @@ class Cache(object):
|
||||
result = self.con.execute(sql).fetchall()
|
||||
return result[0][0]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
value = colors_to_string(value)
|
||||
if key in self:
|
||||
sql = "update pictures set blocks = ? where path = ?"
|
||||
def __setitem__(self, path_str, blocks):
|
||||
blocks = colors_to_string(blocks)
|
||||
if op.exists(path_str):
|
||||
mtime = int(os.stat(path_str).st_mtime)
|
||||
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:
|
||||
self.con.execute(sql, [value, key])
|
||||
self.con.execute(sql, [blocks, mtime, path_str])
|
||||
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:
|
||||
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_tables():
|
||||
sql = "create table pictures(path TEXT, blocks TEXT)"
|
||||
self.con.execute(sql);
|
||||
sql = "create index idx_path on pictures (path)"
|
||||
self.con.execute(sql)
|
||||
logging.debug("Creating picture cache tables.")
|
||||
self.con.execute("drop table if exists pictures");
|
||||
self.con.execute("drop index if exists idx_path");
|
||||
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)
|
||||
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
|
||||
create_tables()
|
||||
except sqlite.DatabaseError as e: # corrupted db
|
||||
@@ -134,3 +140,23 @@ class Cache(object):
|
||||
cur = self.con.execute(sql)
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
# time that MemoryError is raised.
|
||||
cache = Cache(cache_path)
|
||||
cache.purge_outdated()
|
||||
prepared = [] # only pictures for which there was no error getting blocks
|
||||
try:
|
||||
for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")):
|
||||
|
||||
@@ -6,26 +6,18 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from itertools import combinations
|
||||
|
||||
from hscommon import io
|
||||
from hscommon.trans import tr
|
||||
|
||||
from core.engine import Match
|
||||
from . import exif
|
||||
|
||||
def getmatches(files, match_scaled, j):
|
||||
timestamp2pic = defaultdict(set)
|
||||
for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")):
|
||||
try:
|
||||
with io.open(picture.path, 'rb') as fp:
|
||||
exifdata = exif.get_fields(fp)
|
||||
timestamp = exifdata['DateTimeOriginal']
|
||||
timestamp = picture.exif_timestamp
|
||||
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
|
||||
del timestamp2pic['0000:00:00 00:00:00']
|
||||
matches = []
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from hscommon import io
|
||||
import logging
|
||||
from hscommon.util import get_file_ext
|
||||
from core import fs
|
||||
from . import exif
|
||||
@@ -15,6 +15,7 @@ class Photo(fs.File):
|
||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||
INITIAL_INFO.update({
|
||||
'dimensions': (0,0),
|
||||
'exif_timestamp': '',
|
||||
})
|
||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||
|
||||
@@ -30,7 +31,7 @@ class Photo(fs.File):
|
||||
def _get_orientation(self):
|
||||
if not hasattr(self, '_cached_orientation'):
|
||||
try:
|
||||
with io.open(self.path, 'rb') as fp:
|
||||
with self.path.open('rb') as fp:
|
||||
exifdata = exif.get_fields(fp)
|
||||
# the value is a list (probably one-sized) of ints
|
||||
orientations = exifdata['Orientation']
|
||||
@@ -49,6 +50,13 @@ class Photo(fs.File):
|
||||
self.dimensions = self._plat_get_dimensions()
|
||||
if self._get_orientation() in {5, 6, 7, 8}:
|
||||
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):
|
||||
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
||||
|
||||
@@ -20,6 +20,7 @@ class ResultTable(ResultTableBase):
|
||||
Column('size', coltr("Size (KB)"), optional=True),
|
||||
Column('extension', coltr("Kind"), visible=False, 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('percentage', coltr("Match %"), optional=True),
|
||||
Column('dupe_count', coltr("Dupe Count"), visible=False, optional=True),
|
||||
|
||||
@@ -141,5 +141,5 @@ class TestCaseCacheSQLEscape:
|
||||
try:
|
||||
del c["foo'bar"]
|
||||
except KeyError:
|
||||
self.fail()
|
||||
assert False
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
* Fixed iTunes integration which was broken since iTunes 10.6.3. [Mac]
|
||||
|
||||
@@ -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)
|
||||
|
||||
* Added Aperture support. (#42)
|
||||
|
||||
@@ -18,6 +18,8 @@ Unten befindet sich die Liste aller Menschen, die direkt oder indirekt zu dupeGu
|
||||
|
||||
| **Igor Pavlov, Russian localization**
|
||||
|
||||
| **Kyrill Detinov, Russian localization**
|
||||
|
||||
| **Yuri Petrashko, Ukrainian localization**
|
||||
|
||||
| **Nickolas Pohilets, Ukrainian localization**
|
||||
|
||||
@@ -20,6 +20,8 @@ Below is the list of people who contributed, directly or indirectly to dupeGuru.
|
||||
|
||||
| **Igor Pavlov, Russian localization**
|
||||
|
||||
| **Kyrill Detinov, Russian localization**
|
||||
|
||||
| **Yuri Petrashko, Ukrainian localization**
|
||||
|
||||
| **Nickolas Pohilets, Ukrainian localization**
|
||||
|
||||
@@ -19,6 +19,8 @@ Voici la liste des contributeurs de dupeGuru. Merci!
|
||||
|
||||
| **Igor Pavlov, localisation russe**
|
||||
|
||||
| **Kyrill Detinov, localisation russe**
|
||||
|
||||
| **Yuri Petrashko, localisation ukrainienne**
|
||||
|
||||
| **Nickolas Pohilets, localisation ukrainienne**
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
| **Igor Pavlov, Russian localization**
|
||||
|
||||
| **Kyrill Detinov, Russian localization**
|
||||
|
||||
| **Yuri Petrashko, Ukrainian localization**
|
||||
|
||||
| **Nickolas Pohilets, Ukrainian localization**
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
| **Igor Pavlov, Russian localization**
|
||||
|
||||
| **Kyrill Detinov, Russian localization**
|
||||
|
||||
| **Yuri Petrashko, Ukrainian localization**
|
||||
|
||||
| **Nickolas Pohilets, Ukrainian localization**
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
| **Igor Pavlov, Russian localization**
|
||||
|
||||
| **Kyrill Detinov, Russian localization**
|
||||
|
||||
| **Yuri Petrashko, Ukrainian localization**
|
||||
|
||||
| **Nickolas Pohilets, Ukrainian localization**
|
||||
|
||||
@@ -839,7 +839,8 @@ msgstr "Remplacer les fichiers effacés par des liens"
|
||||
msgid ""
|
||||
"After having deleted a duplicate, place a link targeting the reference 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."
|
||||
|
||||
#: qt/base/deletion_options.py:45
|
||||
|
||||
177
locale/ru/LC_MESSAGES/columns.po
Executable file → Normal file
177
locale/ru/LC_MESSAGES/columns.po
Executable file → Normal file
@@ -1,120 +1,121 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Russian translation\n"
|
||||
"Project-Id-Version: dupeGuru\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Igor Pavlov <IgorPavlov87@mail.ru>\n"
|
||||
"Language-Team: Igor Pavlov <IgorPavlov87@mail.ru>\n"
|
||||
"Last-Translator: Kyrill Detinov <lazy.kent@opensuse.org>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%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-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/problem_table.py:17
|
||||
msgid "File Path"
|
||||
msgstr "Путь к файлу"
|
||||
|
||||
#: core/gui/problem_table.py:18
|
||||
msgid "Error Message"
|
||||
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: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_se/result_table.py:21
|
||||
msgid "Kind"
|
||||
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_se/result_table.py:23
|
||||
msgid "Match %"
|
||||
msgstr "Совпадение %"
|
||||
|
||||
#: core_me/result_table.py:34 core_se/result_table.py:24
|
||||
msgid "Words Used"
|
||||
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/result_table.py:35 core_pe/result_table.py:25
|
||||
#: core_se/result_table.py:25
|
||||
msgid "Dupe Count"
|
||||
msgstr "Dupe графа"
|
||||
#: core_me/result_table.py:23
|
||||
msgid "Sample Rate"
|
||||
msgstr "Частота"
|
||||
|
||||
#: core_pe/prioritize.py:16 core_pe/result_table.py:22
|
||||
msgid "Dimensions"
|
||||
msgstr "Размеры"
|
||||
#: core_me/prioritize.py:28
|
||||
msgid "Samplerate"
|
||||
msgstr "Частота оцифровки"
|
||||
|
||||
#: core/prioritize.py:147
|
||||
msgid "Size"
|
||||
msgstr "Размер"
|
||||
|
||||
#: core_pe/result_table.py:20 core_se/result_table.py:20
|
||||
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
280
locale/ru/LC_MESSAGES/core.po
Executable file → Normal file
@@ -1,37 +1,28 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Russian translation\n"
|
||||
"Project-Id-Version: dupeGuru\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Igor Pavlov <IgorPavlov87@mail.ru>\n"
|
||||
"Language-Team: Igor Pavlov <IgorPavlov87@mail.ru>\n"
|
||||
"Last-Translator: Kyrill Detinov <lazy.kent@opensuse.org>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Poedit-Country: RUSSIAN FEDERATION\n"
|
||||
"X-Poedit-Language: Russian\n"
|
||||
"Language: ru_RU\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
|
||||
msgid "will only be able to delete, move or copy 10 duplicates at once"
|
||||
msgstr ""
|
||||
"сможете только для удаления, перемещения или копирования 10 копий сразу"
|
||||
#: core/results.py:120
|
||||
msgid " filter: %s"
|
||||
msgstr "фильтр: %s"
|
||||
|
||||
#: core/app.py:273
|
||||
msgid ""
|
||||
"You cannot delete, move or copy more than 10 duplicates at once in demo "
|
||||
"mode."
|
||||
msgstr ""
|
||||
"Вы не можете удалять, перемещать или копировать более 10 дубликатов сразу в "
|
||||
"демонстрационном режиме."
|
||||
#: core/results.py:113
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) дубликатов отмечено."
|
||||
|
||||
#: core/app.py:543
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Сбор файлов для сканирования"
|
||||
|
||||
#: core/app.py:554
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Выбранных директорий не содержат сканируемых файлов."
|
||||
#: core/engine.py:196 core/engine.py:223
|
||||
msgid "%d matches found"
|
||||
msgstr "%d совпадений найдено"
|
||||
|
||||
#: core/app.py:593
|
||||
msgid "%s (%d discarded)"
|
||||
@@ -39,31 +30,56 @@ msgstr "%s. (%d отменено)"
|
||||
|
||||
#: core/engine.py:178 core/engine.py:215
|
||||
msgid "0 matches found"
|
||||
msgstr "0 сопоставлений найдено"
|
||||
msgstr "0 совпадений найдено"
|
||||
|
||||
#: core/engine.py:196 core/engine.py:223
|
||||
msgid "%d matches found"
|
||||
msgstr "%d сопоставлений найдено"
|
||||
#: core/app.py:244
|
||||
msgid "All marked files were copied sucessfully."
|
||||
msgstr "Все отмеченные файлы были скопированы успешно."
|
||||
|
||||
#: core/engine.py:208 core/scanner.py:80
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Реалний размер %d/%d файлов"
|
||||
#: core/app.py:245
|
||||
msgid "All marked files were moved sucessfully."
|
||||
msgstr "Все отмеченные файлы были перемещены успешно."
|
||||
|
||||
#: core/engine.py:360
|
||||
msgid "Grouped %d/%d matches"
|
||||
msgstr "Группировка %d/%d совпадений"
|
||||
#: core/app.py:246
|
||||
msgid "All marked files were sucessfully sent to Trash."
|
||||
msgstr "Все отмеченные файлы были успешно отправлены в Корзину."
|
||||
|
||||
#: core/prioritize.py:68
|
||||
msgid "None"
|
||||
msgstr "Ни один"
|
||||
#: core/app.py:293
|
||||
msgid ""
|
||||
"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
|
||||
msgid "Ends with number"
|
||||
msgstr "Заканчивается номером"
|
||||
|
||||
#: core/prioritize.py:97
|
||||
msgid "Doesn't end with number"
|
||||
msgstr "Не заканчивается с номером"
|
||||
#: core/engine.py:360
|
||||
msgid "Grouped %d/%d matches"
|
||||
msgstr "Группировка %d/%d совпадений"
|
||||
|
||||
#: core/prioritize.py:132
|
||||
msgid "Highest"
|
||||
@@ -71,59 +87,95 @@ msgstr "Наивысший"
|
||||
|
||||
#: core/prioritize.py:132
|
||||
msgid "Lowest"
|
||||
msgstr "Самая низкая"
|
||||
msgstr "Самый низкий"
|
||||
|
||||
#: core/prioritize.py:159
|
||||
msgid "Newest"
|
||||
msgstr "Новейший"
|
||||
|
||||
#: core/app.py:231
|
||||
msgid "No duplicates found."
|
||||
msgstr "Дубликаты не найдены."
|
||||
|
||||
#: core/prioritize.py:68
|
||||
msgid "None"
|
||||
msgstr "Ни один"
|
||||
|
||||
#: core/prioritize.py:159
|
||||
msgid "Oldest"
|
||||
msgstr "Старейшие"
|
||||
|
||||
#: core/results.py:113
|
||||
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 фотографии"
|
||||
|
||||
# Not sure.
|
||||
#: core_pe/matchblock.py:152
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Исполняет %d/%d совпадениях кусоков"
|
||||
msgstr "Выполнено %d/%d совпадений блоков"
|
||||
|
||||
#: core_pe/matchblock.py:157
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Подготовка для сравнения"
|
||||
|
||||
#: core_pe/matchblock.py:192
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "Проверенные %d/%d совпадениях"
|
||||
# Not sure.
|
||||
#: core/scanner.py:149
|
||||
msgid "Processed %d/%d matches against the ignore list"
|
||||
msgstr "Обработано %d/%d совпадений используя список игнорирования"
|
||||
|
||||
#: core_pe/matchexif.py:21
|
||||
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
|
||||
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."
|
||||
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
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr ""
|
||||
@@ -210,3 +196,19 @@ msgstr ""
|
||||
#: core/app.py:380
|
||||
msgid "Select a destination for your exported CSV"
|
||||
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
1413
locale/ru/LC_MESSAGES/ui.po
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ class ResultsModel(ResultsModelBase):
|
||||
Column('size', defaultWidth=60),
|
||||
Column('extension', defaultWidth=40),
|
||||
Column('dimensions', defaultWidth=100),
|
||||
Column('exif_timestamp', defaultWidth=120),
|
||||
Column('mtime', defaultWidth=120),
|
||||
Column('percentage', defaultWidth=60),
|
||||
Column('dupe_count', defaultWidth=80),
|
||||
|
||||
@@ -2,6 +2,6 @@ jobprogress>=1.0.4
|
||||
Send2Trash3k>=1.2.0
|
||||
sphinx>=1.1.3
|
||||
polib>=0.7.0
|
||||
hsaudiotag3k>=1.1.2
|
||||
hsaudiotag3k>=1.1.3
|
||||
pytest>=2.0.0
|
||||
pytest-monkeyplus>=1.0.0
|
||||
|
||||
Reference in New Issue
Block a user