1 | """ |
||
2 | Methods for interacting with information Elodie caches about stored media. |
||
3 | """ |
||
4 | from builtins import map |
||
5 | from builtins import object |
||
6 | |||
7 | import hashlib |
||
8 | import json |
||
9 | import os |
||
10 | import sys |
||
11 | |||
12 | from math import radians, cos, sqrt |
||
13 | from shutil import copyfile |
||
14 | from time import strftime |
||
15 | |||
16 | from elodie import constants |
||
17 | |||
18 | |||
19 | class Db(object): |
||
20 | |||
21 | """A class for interacting with the JSON files created by Elodie.""" |
||
22 | |||
23 | def __init__(self): |
||
24 | # verify that the application directory (~/.elodie) exists, |
||
25 | # else create it |
||
26 | if not os.path.exists(constants.application_directory): |
||
27 | os.makedirs(constants.application_directory) |
||
28 | |||
29 | # If the hash db doesn't exist we create it. |
||
30 | # Otherwise we only open for reading |
||
31 | if not os.path.isfile(constants.hash_db): |
||
32 | with open(constants.hash_db, 'a'): |
||
33 | os.utime(constants.hash_db, None) |
||
34 | |||
35 | self.hash_db = {} |
||
36 | |||
37 | # We know from above that this file exists so we open it |
||
38 | # for reading only. |
||
39 | with open(constants.hash_db, 'r') as f: |
||
40 | try: |
||
41 | self.hash_db = json.load(f) |
||
42 | except ValueError: |
||
43 | pass |
||
44 | |||
45 | # If the location db doesn't exist we create it. |
||
46 | # Otherwise we only open for reading |
||
47 | if not os.path.isfile(constants.location_db): |
||
48 | with open(constants.location_db, 'a'): |
||
49 | os.utime(constants.location_db, None) |
||
50 | |||
51 | self.location_db = [] |
||
52 | |||
53 | # We know from above that this file exists so we open it |
||
54 | # for reading only. |
||
55 | with open(constants.location_db, 'r') as f: |
||
56 | try: |
||
57 | self.location_db = json.load(f) |
||
58 | except ValueError: |
||
59 | pass |
||
60 | |||
61 | def add_hash(self, key, value, write=False): |
||
62 | """Add a hash to the hash db. |
||
63 | |||
64 | :param str key: |
||
65 | :param str value: |
||
66 | :param bool write: If true, write the hash db to disk. |
||
67 | """ |
||
68 | self.hash_db[key] = value |
||
69 | if(write is True): |
||
70 | self.update_hash_db() |
||
71 | |||
72 | # Location database |
||
73 | # Currently quite simple just a list of long/lat pairs with a name |
||
74 | # If it gets many entries a lookup might take too long and a better |
||
75 | # structure might be needed. Some speed up ideas: |
||
76 | # - Sort it and inter-half method can be used |
||
77 | # - Use integer part of long or lat as key to get a lower search list |
||
78 | # - Cache a small number of lookups, photos are likely to be taken in |
||
79 | # clusters around a spot during import. |
||
80 | def add_location(self, latitude, longitude, place, write=False): |
||
81 | """Add a location to the database. |
||
82 | |||
83 | :param float latitude: Latitude of the location. |
||
84 | :param float longitude: Longitude of the location. |
||
85 | :param str place: Name for the location. |
||
86 | :param bool write: If true, write the location db to disk. |
||
87 | """ |
||
88 | data = {} |
||
89 | data['lat'] = latitude |
||
90 | data['long'] = longitude |
||
91 | data['name'] = place |
||
92 | self.location_db.append(data) |
||
93 | if(write is True): |
||
94 | self.update_location_db() |
||
95 | |||
96 | def backup_hash_db(self): |
||
97 | """Backs up the hash db.""" |
||
98 | if os.path.isfile(constants.hash_db): |
||
99 | mask = strftime('%Y-%m-%d_%H-%M-%S') |
||
100 | backup_file_name = '%s-%s' % (constants.hash_db, mask) |
||
101 | copyfile(constants.hash_db, backup_file_name) |
||
102 | return backup_file_name |
||
103 | |||
104 | def check_hash(self, key): |
||
105 | """Check whether a hash is present for the given key. |
||
106 | |||
107 | :param str key: |
||
108 | :returns: bool |
||
109 | """ |
||
110 | return key in self.hash_db |
||
111 | |||
112 | View Code Duplication | def checksum(self, file_path, blocksize=65536): |
|
0 ignored issues
–
show
Duplication
introduced
by
Loading history...
|
|||
113 | """Create a hash value for the given file. |
||
114 | |||
115 | See http://stackoverflow.com/a/3431835/1318758. |
||
116 | |||
117 | :param str file_path: Path to the file to create a hash for. |
||
118 | :param int blocksize: Read blocks of this size from the file when |
||
119 | creating the hash. |
||
120 | :returns: str or None |
||
121 | """ |
||
122 | hasher = hashlib.sha256() |
||
123 | with open(file_path, 'rb') as f: |
||
124 | buf = f.read(blocksize) |
||
125 | |||
126 | while len(buf) > 0: |
||
127 | hasher.update(buf) |
||
128 | buf = f.read(blocksize) |
||
129 | return hasher.hexdigest() |
||
130 | return None |
||
131 | |||
132 | def get_hash(self, key): |
||
133 | """Get the hash value for a given key. |
||
134 | |||
135 | :param str key: |
||
136 | :returns: str or None |
||
137 | """ |
||
138 | if(self.check_hash(key) is True): |
||
139 | return self.hash_db[key] |
||
140 | return None |
||
141 | |||
142 | def get_location_name(self, latitude, longitude, threshold_m): |
||
143 | """Find a name for a location in the database. |
||
144 | |||
145 | :param float latitude: Latitude of the location. |
||
146 | :param float longitude: Longitude of the location. |
||
147 | :param int threshold_m: Location in the database must be this close to |
||
148 | the given latitude and longitude. |
||
149 | :returns: str, or None if a matching location couldn't be found. |
||
150 | """ |
||
151 | last_d = sys.maxsize |
||
152 | name = None |
||
153 | for data in self.location_db: |
||
154 | # As threshold is quite small use simple math |
||
155 | # From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # noqa |
||
156 | # convert decimal degrees to radians |
||
157 | |||
158 | lon1, lat1, lon2, lat2 = list(map( |
||
159 | radians, |
||
160 | [longitude, latitude, data['long'], data['lat']] |
||
161 | )) |
||
162 | |||
163 | r = 6371000 # radius of the earth in m |
||
164 | x = (lon2 - lon1) * cos(0.5 * (lat2 + lat1)) |
||
165 | y = lat2 - lat1 |
||
166 | d = r * sqrt(x * x + y * y) |
||
167 | # Use if closer then threshold_km reuse lookup |
||
168 | if(d <= threshold_m and d < last_d): |
||
169 | name = data['name'] |
||
170 | last_d = d |
||
171 | |||
172 | return name |
||
173 | |||
174 | def get_location_coordinates(self, name): |
||
175 | """Get the latitude and longitude for a location. |
||
176 | |||
177 | :param str name: Name of the location. |
||
178 | :returns: tuple(float), or None if the location wasn't in the database. |
||
179 | """ |
||
180 | for data in self.location_db: |
||
181 | if data['name'] == name: |
||
182 | return (data['lat'], data['long']) |
||
183 | |||
184 | return None |
||
185 | |||
186 | def all(self): |
||
187 | """Generator to get all entries from self.hash_db |
||
188 | |||
189 | :returns tuple(string) |
||
190 | """ |
||
191 | for checksum, path in self.hash_db.items(): |
||
192 | yield (checksum, path) |
||
193 | |||
194 | def reset_hash_db(self): |
||
195 | self.hash_db = {} |
||
196 | |||
197 | def update_hash_db(self): |
||
198 | """Write the hash db to disk.""" |
||
199 | with open(constants.hash_db, 'w') as f: |
||
200 | json.dump(self.hash_db, f) |
||
201 | |||
202 | def update_location_db(self): |
||
203 | """Write the location db to disk.""" |
||
204 | with open(constants.location_db, 'w') as f: |
||
205 | json.dump(self.location_db, f) |
||
206 |