#!/usr/bin/env python # GPixPod - the free and open source way to manage photos on your POD! # Copyright (C) 2006 Flavio Gargiulo (FLAGAR.com) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os, sys, shutil, time from utils import * from imgconvs import * from struct import * class MH: """ Process the MH database """ formats = {'mhfd':'4s16I64x', 'mhsd':'4s3I80x', 'mhli':'4s2I80x', 'mhii':'4s12I100x', 'mhod':'4s2I1H2B8x', 'mhni':'4s6I2h2H40x', 'mhla':'4s2I80x', 'mhba':'4s15I84x', 'mhia':'4s4I20x', 'mhlf':'4s2I80x', 'mhif':'4s5I100x'} lengths = dict(map(lambda(x): (x[0], calcsize(x[1])), formats.items())) latest = {'mhfd':0, 'mhsd':0, 'mhli':0, 'mhii':0, 'mhoc':0, 'mhni':0, 'mhod':0, 'mhos':0, 'mhla':0, 'mhba':0, 'mhia':0, 'mhlf':0, 'mhif':0} parents = {'mhfd':'mhfd', 'mhsd':'mhfd', 'mhli':'mhsd', 'mhii':'mhli', 'mhoe':'mhii', 'mhoc':'mhii', 'mhni':'mhoc', 'mhod':'mhni', 'mhos':'mhod', 'mhla':'mhsd', 'mhba':'mhla', 'mhob':'mhba', 'mhoa':'mhob', 'mhia':'mhba', 'mhlf':'mhsd', 'mhif':'mhlf'} # MHOE: container MHOD for full res.; MHOC: container MHOD for thumb; MHOD: string MHOD type-3; MHOS: string MHOD type-3 subheader; # MHOB: string MHOD type-1; MHOA: string MHOD type-1 subheader. #thumbdbsizes = ((712, 480), (320, 240), (123, 88), (53, 41)) MOVED TO __INIT__ #thumbsizes = ((720, 480), (320, 240), (130, 88), (50, 41)) MOVED TO __INIT__ #thumbdims = tuple(map(lambda(x): x[0]*x[1]*2, thumbsizes)) MOVED TO __INIT__ #thumbfnames = (1019, 1024, 1015, 1036) # Default thumbnails filename parts removed_photos_ids = [] new_photos_added = [] thumbs_extracted = False utilities_path = './utilities/' def __init__(self, dir='/mnt/ipod/Photos/Photo Database'): """ Try to open the Photo Database file """ # Figuring out the full filename and main path if dir.find('Photo Database') == -1: self.dbname = os.path.join(dir, 'Photo Database') self.dbdir = dir else: self.dbname = dir self.dbdir = dir.replace('Photo Database', '') # Detecting iPod model and setting thumbnail infos self.ipod_model = getIpodModel(self.dbdir.replace('Photos', '')) if self.ipod_model == '5G': # Supported for sure: developer's one self.thumbdbsizes = ((712, 480), (320, 240), (123, 88), (53, 41)) self.thumbsizes = ((720, 480), (320, 240), (130, 88), (50, 41)) self.thumbfnames = (1019, 1024, 1015, 1036) elif self.ipod_model == 'Photo': # Tested and verified self.thumbdbsizes = ((712, 452), (235, 146), (130, 87), (43, 30)) self.thumbsizes = ((720, 480), (220, 176), (130, 88), (42, 30)) self.thumbfnames = (1019, 1013, 1015, 1009) elif self.ipod_model == 'Color': # Guessed (same as Photo?) self.thumbdbsizes = ((712, 452), (235, 146), (130, 87), (43, 30)) self.thumbsizes = ((720, 480), (220, 176), (130, 88), (42, 30)) self.thumbfnames = (1019, 1013, 1015, 1009) elif self.ipod_model == 'Nano': # Tested and verified self.thumbdbsizes = ((176, 132), (45, 37)) self.thumbsizes = ((176, 132), (42, 37)) self.thumbfnames = (1023, 1032) else: # Using iPod 5G values as default for other, unsupported models. The GUI will warn. self.thumbdbsizes = ((712, 480), (320, 240), (123, 88), (53, 41)) self.thumbsizes = ((720, 480), (320, 240), (130, 88), (50, 41)) self.thumbfnames = (1019, 1024, 1015, 1036) self.thumbdims = tuple(map(lambda(x): x[0]*x[1]*2, self.thumbsizes)) # Photo Database opening try: self.db = open(self.dbname, 'rb') except IOError: print "Directory does not contain a Photo Database file" print "A new one will be created based on the path specified" # FromScratch opening should be modelled better as in exceptions as in questions before overwriting self._FromScratch() if not os.path.isdir(self.dbdir): os.makedirs(self.dbdir) # Creating thumbnails files if they don't exist yet if not os.path.isdir(os.path.join(self.dbdir, 'Thumbs')): os.mkdir(os.path.join(self.dbdir, 'Thumbs')) for x in self.thumbfnames: t = open(os.path.join(self.dbdir, 'Thumbs', 'F%s_1.ithmb' % x), 'ab') t.close() self._SaveDB() self.db = open(self.dbname, 'rb') self._FirstScan() self._ThumbsExtract() self.WriteDatabaseLog() def _FromScratch(self): """ Define a new self.dblines from scratch to generate a new empty database file """ # Template for an iPod video 5G self.dblines = [] self.dblines.append(['mhfd', ['mhfd', 132, 1252, 0, 1, 3, 0, 100, 0, 0, 0, 0, 2, 0, 0, 0, 0], 0]) self.dblines.append(['mhsd', ['mhsd', 96, 188, 1], 0]) self.dblines.append(['mhli', ['mhli', 92, 0], 1]) self.dblines.append(['mhsd', ['mhsd', 96, 380, 2], 0]) self.dblines.append(['mhla', ['mhla', 92, 1], 3]) self.dblines.append(['mhba', ['mhba', 148, 192, 1, 0, 100, 0, 65536, 0, 0, 0, 0, 0, 0, 0, 100], 4]) self.dblines.append(['mhob', ['mhod', 24, 44, 1, 0, 1], 5]) self.dblines.append(['mhoa', [7, 1, 0, 'Library'.encode('utf-8')], 6]) self.dblines.append(['mhsd', ['mhsd', 96, 684, 3], 0]) self.dblines.append(['mhlf', ['mhlf', 92, 4], 5]) self.dblines.append(['mhif', ['mhif', 124, 124, 0, 1019, 691200], 6]) self.dblines.append(['mhif', ['mhif', 124, 124, 0, 1015, 22880], 6]) self.dblines.append(['mhif', ['mhif', 124, 124, 0, 1024, 153600], 6]) self.dblines.append(['mhif', ['mhif', 124, 124, 0, 1036, 4100], 6]) # Modifying template for iPod Photo/Color if self.ipod_model == 'Photo' or self.ipod_model == 'Color': self.dblines[-1][1][5] = 2520 # Modifying dimension of the 42x30 thumbnail self.dblines[-2][1][5] = 77440 # Modifying dimension of the 220x176 thumbnail # Updating thumbnails filenames parts self.dblines[-4][1][4] = self.thumbfnames[0] self.dblines[-3][1][4] = self.thumbfnames[2] self.dblines[-2][1][4] = self.thumbfnames[1] self.dblines[-1][1][4] = self.thumbfnames[3] # Modifying template for iPod Nano elif self.ipod_model == 'Nano': del self.dblines[-4:-2] # deleting missing thumbnails types # Updating thumbnails filename parts self.dblines[-2][1][4] = self.thumbfnames[0] self.dblines[-1][1][4] = self.thumbfnames[1] self.dblines[-1][1][5] = 3108 # Modifying dimension of the 42x37 thumbnail self.dblines[-2][1][5] = 46464 # Modifying dimension of the 176x132 thumbnail self.dblines[-3][1][2] = 2 # Modifying MHLF indicating only 2 thumbnails types self.dblines[-4][1][2] -= 248 # Updating parent MHSD total length, after having deleted 2 MHIF self.dblines[0][1][2] -= 248 # Updating MHFD, too def _FirstScan(self): """ Scan the database file """ self.db.seek(0, 2); dbsize = self.db.tell() self.db.seek(0); self.dblines = [] while self.db.tell() < dbsize: dbline = unpack('4s', self.db.read(4))[0] self.db.seek(-4, 1) self.dblines.append([dbline, list(unpack(self.formats[dbline], self.db.read(self.lengths[dbline]))), self.latest[self.parents[dbline]]]) if dbline == 'mhod': mhod_type = self.dblines[-1][1][3] if mhod_type == 5: self.dblines[-1][0] = 'mhoe' elif mhod_type == 2: self.dblines[-1][0] = 'mhoc' elif mhod_type == 1: self.dblines[-1][0] = 'mhob' mhodsubname = 'mhoa' elif mhod_type == 3: mhodsubname = 'mhos' mhod_name = self.dblines[-1][0] self.dblines[-1][2] = self.latest[self.parents[mhod_name]] self.latest[mhod_name] = len(self.dblines)-1 if mhod_name == 'mhod' or mhod_name == 'mhob': # string MHOD mhodslen = unpack('I', self.db.read(4))[0]; self.db.seek(-4, 1) mhodsformat = '3I'+str(mhodslen)+'s'+str(self.dblines[-1][1][5])+'x' self.dblines.append([mhodsubname, list(unpack(mhodsformat, self.db.read(calcsize(mhodsformat)))), self.latest[mhod_name]]) self.latest[mhodsubname] = len(self.dblines)-1 else: self.latest[dbline] = len(self.dblines)-1 self._RetrieveThumbnailsFilenameParts() self.db.close() def _RetrieveThumbnailsFilenameParts(self): """ Retrieve thumbnail filename parts (to name files in the Thumbs directory) """ self.thumbfnames = [] dblines_len = len(self.dblines) # iPod Photo, Color and 5G seem to have 4 different thumbnails types; iPod Nano seems to have just 2 types. for x in range(dblines_len-4, dblines_len): if self.dblines[x][0] == 'mhif': self.thumbfnames.append((self.dblines[x][1][5], self.dblines[x][1][4])) self.thumbfnames = dict(self.thumbfnames) def WriteDatabaseLog(self): """ Write a .gpixpod.log debug file in the Photos directory """ logfile = open(os.path.join(self.dbdir, '.gpixpod.log'), 'w+') for dbline in self.dblines: print >> logfile, dbline logfile.close() def _SaveDB(self, filename=None): """ Write all the changes back to the database file """ if filename == None: filename = self.dbname if filename == self.dbname and os.path.isfile(filename): # if saving to the same file, creating backup backup_filename = os.path.join(self.dbdir, 'Photo Database.gpixpodbak') if os.path.isfile(backup_filename): os.remove(backup_filename) os.rename(self.dbname, backup_filename) self._PrepareToSave() db = open(filename, 'w+b') try: for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhos' or self.dblines[x][0] == 'mhoa': db.write(pack('3I'+str(self.dblines[x][1][0])+'s'+str(self.dblines[x-1][1][5])+'x', *self.dblines[x][1])) elif self.dblines[x][0] == 'mhoe' or self.dblines[x][0] == 'mhoc' or self.dblines[x][0] == 'mhob': db.write(pack(self.formats['mhod'], *self.dblines[x][1])) else: db.write(pack(self.formats[self.dblines[x][0]], *self.dblines[x][1])) except error: print x, self.dblines[x][0] db.close() def Save(self, filename=None): """ Save all """ self._UpdateThumbsOffsets() self._ThumbsRebuild() self._SaveDB(filename) def Albums(self): """ Get current photo albums """ result = [] for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhba': albumpics = [] picsn = self.dblines[x][1][4] for y in range(0, picsn): albumpics.append(self.dblines[x+3+y][1][4]) album = Album(self.dblines[x+2][1][3].decode("utf-8"), albumpics) result.append(album) return result def Photos(self): """ Get current photos details """ result = [] for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': photo = Photo(self.dblines[x][1][4], self.dblines[x+4][1][3].decode("utf-16-le")) result.append(photo) return result def AddAlbum(self, name): """ Add a new album """ namelen = len(name) namepad = getPadding(namelen) mhob_totlen = 24+12+namelen+namepad for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': self.dblines[x][1][2] += 1 # Incrementing number of albums self.dblines[x-1][1][2] += 148+mhob_totlen # Incrementing total length of parent MHSD mhla = x if self.dblines[x][0] == 'mhba': last_mhba = x # Getting the latest album list offset: needed to generate new IDs self.dblines[0][1][7] += 1 # Incrementing next ID field in MHFD # If iPod Nano, assuming 4 elements at the end of the list: MHSD, MHLF and 2 MHIF if self.ipod_model == 'Nano': afteralb = 4 # Else assuming 6 elements at the end of the list: MHSD, MHLF and 4 MHIF else: afteralb = 6 self.dblines.insert(-afteralb, ['mhba', ['mhba', 148, 148+mhob_totlen, 1, 0, (self.dblines[last_mhba][1][5]+1), 0, 393216, 0, 0, 0, 0, 0, 0, 0, \ (self.dblines[last_mhba][1][15]+self.dblines[last_mhba][1][4]+1)], mhla]) self.dblines.insert(-afteralb, ['mhob', ['mhod', 24, mhob_totlen, 1, 0, namepad], len(self.dblines)-6]) self.dblines.insert(-afteralb, ['mhoa', [namelen, 1, 0, name], len(self.dblines)-6]) def _GetMHNIs(self, mhii): """ Get MHNI offsets for the specified MHII and for the current iPod model """ if self.ipod_model == '5G': return (mhii+6, mhii+10, mhii+14, mhii+18) elif self.ipod_model == 'Nano': return (mhii+6, mhii+10) else: # Using iPod 5G MHNI offsets as default return (mhii+6, mhii+10, mhii+14, mhii+18) def _ThumbsExtract(self): """ Extract all the thumbs in a specified hidden directory with a subdirectory per size format """ thumb_dirs = [] thumbdims = list(self.thumbdims) thumbsizes_len = len(self.thumbsizes) for t in range(0, thumbsizes_len): thumb_dirs.append(os.path.join(self.dbdir, '.SingleThumbs', '%sx%s' % (self.thumbsizes[t][0], self.thumbsizes[t][1]))) if not os.path.isdir(thumb_dirs[-1]): os.makedirs(thumb_dirs[-1]) for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': current_id = self.dblines[x][1][4] for y in self._GetMHNIs(x): d = thumbdims.index(self.dblines[y][1][6]) thumbs = open(os.path.join(self.dbdir, 'Thumbs', 'F%s_1.ithmb' % self.thumbfnames[self.dblines[y][1][6]]), 'rb') thumb = open(os.path.join(thumb_dirs[d], str(current_id)), 'w+b') thumbs.seek(self.dblines[y][1][5]) thumb.write(thumbs.read(self.dblines[y][1][6])) thumb.close() thumbs.close() self.thumbs_extracted = True def _ThumbsRebuild(self): """ Rebuild the thumbnails file from the single thumbs previously extracted """ maindir = os.path.join(self.dbdir, '.SingleThumbs') for t in range(0, len(self.thumbsizes)): tdir = os.path.join(maindir, '%sx%s' % (self.thumbsizes[t][0], self.thumbsizes[t][1])) tdirlist = os.listdir(tdir) tdirlist.sort() thumbs = open(os.path.join(self.dbdir, 'Thumbs', 'F%s_1.ithmb' % self.thumbfnames[self.thumbdims[t]]), 'w+b') for f in tdirlist: thumbname = os.path.join(tdir, f) thumb = open(thumbname, 'rb') thumbs.write(thumb.read()) thumb.close() os.remove(thumbname) thumbs.close() os.rmdir(tdir) os.rmdir(maindir) self.thumbs_extracted = False def _UpdateThumbsOffsets(self): """ Update the offsets for each thumbnail in MHNI definitions for all photos """ thumboffsets = [0, 0, 0, 0] thumbdims = list(self.thumbdims) thumbsizes_len = len(self.thumbsizes) for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': for y in self._GetMHNIs(x): d = thumbdims.index(self.dblines[y][1][6]) self.dblines[y][1][5] = thumboffsets[d] thumboffsets[d] += self.dblines[y][1][6] def RemoveThumbsCache(self): """ Remove the cache of the single thumbnails """ maindir = os.path.join(self.dbdir, '.SingleThumbs') for t in self.thumbsizes: td = os.path.join(maindir, '%sx%s' % (t[0], t[1])) for tf in os.listdir(td): os.remove(os.path.join(td, tf)) os.rmdir(td) os.rmdir(maindir) def _RemovePhotoThumbs(self, photoid): """ Remove photo thumbs """ for t in self.thumbsizes: thumbdir = os.path.join(self.dbdir, '.SingleThumbs', '%sx%s' % (t[0], t[1])) os.remove(os.path.join(thumbdir, str(photoid))) def _RemovePhotoFromList(self, photoid): """ Remove a photo from the photos list (MHLI) """ for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': if self.dblines[x][1][4] == photoid: photo = x elif self.dblines[x][1][4] > photoid: #self.dblines[x][1][4] -= 1 # Adjusting IDs for next photos MOVED TO NEW FUNCTION: it CREATED problems when removing!!! self.dblines[x][1][5] -= 1 # for t in (x+6, x+10, x+14, x+18): # self.dblines[t][1][5] -= self.dblines[t][1][6] # Adjusting thumbnails offsets for next images if self.dblines[x][0] == 'mhba': self.dblines[x][1][5] -= 1 # Related decrement of subsequent album IDs self.dblines[1][1][2] -= self.dblines[photo][1][2] # Decrementing total length of parent MHSD self.dblines[2][1][2] -= 1 # Decrementing total number of pictures self.dblines[0][1][7] -= 1 # Decrementing next ID field in MHFD # Removing thumbnails self._RemovePhotoThumbs(photoid) # Deleting full resolution image self._DeleteFullResolution(self.dblines[photo+4][1][3]) del self.dblines[photo:photo+(4+4*len(self.thumbsizes)+1)] def _AddPhotoThumbs(self, filename, photoid): """ Add photo thumbs """ thumbsizes_len = len(self.thumbsizes) for t in range(0, thumbsizes_len): thumb = open(os.path.join(self.dbdir, '.SingleThumbs', '%sx%s' % (self.thumbsizes[t][0], self.thumbsizes[t][1]), str(photoid)), 'w+b') if t == 0 and thumbsizes_len != 2: # then is not TV photo neither iPod Nano imgdata = toInterlacedUYVY(filename) else: if t == 0 and self.ipod_model == 'Nano': imgdata = toRGB565(filename, self.thumbsizes[t][0], self.thumbsizes[t][1], False) elif t == 1 and (self.ipod_model == 'Photo' or self.ipod_model == 'Color'): imgdata = toRGB565(filename, self.thumbsizes[t][0], self.thumbsizes[t][1], True, True) else: imgdata = toRGB565(filename, self.thumbsizes[t][0], self.thumbsizes[t][1]) thumb.write(imgdata) thumb.close() def _AddPhotoToList(self, filename): """ Add a new photo to the photos list (MHLI) """ name = os.path.basename(filename) # check maximum filename string length FIXME picdate = time.localtime() albums = 0 last_mhii = None for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': last_mhii = x if self.dblines[x][0] == 'mhba': self.dblines[x][1][5] += 1 # Related increment of subsequent album IDs albums += 1 mhli = 2 if last_mhii == None: mhpos = mhii = 3 photo_id = 100 else: mhpos = mhii = last_mhii+4+4*len(self.thumbsizes)+1 photo_id = self.dblines[last_mhii][1][4]+1 # Copying full resolution image self._CopyFullResolution(filename) fullresdb = (':Full Resolution:%s:%s:%s:%s' % (picdate[0], picdate[1], picdate[2], name)).encode('utf-16-le') fullresdb_len = len(fullresdb) fullresdb_pad = getPadding(fullresdb_len) fres = fullresdb_len+fullresdb_pad # Incrementing total length of parent MHSD self.dblines[1][1][2] += 288+(len(self.thumbdbsizes)*180)+fres # Generating thumbnails self._AddPhotoThumbs(filename, photo_id) # Photo header (MHII) self.dblines.insert(mhpos, ['mhii', ['mhii', 152, 288+(len(self.thumbdbsizes)*180)+fres, (1+len(self.thumbdbsizes)), photo_id, photo_id+albums, 0, 0, 0, 0, appleTime(), appleTime(), 0], mhli]) self.dblines[mhli][1][2] +=1 # Total number of pictures self.dblines[0][1][7] += 1 # Incrementing next ID field in MHFD # Full resolution self.dblines.insert(mhpos+1, ['mhoe', ['mhod', 24, 136+fres, 5, 0, 0], mhpos]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhni', ['mhni', 76, 112+fres, 1, 1, 0, 0, 0, 0, 0, 0], mhpos]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhod', ['mhod', 24, 36+fres, 3, 0, fullresdb_pad], mhpos]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhos', [fullresdb_len, 2, 0, fullresdb], mhpos]) mhpos+=1 # Thumbs for thumb in range(0, len(self.thumbdbsizes)): if last_mhii == None: offset = 0 else: offset = self.dblines[last_mhii+6+4*thumb][1][5]+self.thumbdims[thumb] self.dblines.insert(mhpos+1, ['mhoc', ['mhod', 24, 180, 2, 0, 0], mhii]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhni', ['mhni', 76, 156, 1, self.thumbfnames[self.thumbdims[thumb]], offset, self.thumbdims[thumb], 0, 0, self.thumbdbsizes[thumb][1], self.thumbdbsizes[thumb][0]], mhpos]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhod', ['mhod', 24, 80, 3, 0, 2], mhpos]); mhpos+=1 self.dblines.insert(mhpos+1, ['mhos', [42, 2, 0, (':Thumbs:F%s_1.ithmb' % self.thumbfnames[self.thumbdims[thumb]]).encode('utf-16-le')], mhpos]); mhpos+=1 return photo_id def _CopyFullResolution(self, filename): """ Copy the full resolution photo to destination """ name = os.path.basename(filename) picdate = time.localtime() dest_dir = os.path.join(self.dbdir, 'Full Resolution', *map(str, picdate[:3])) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) shutil.copyfile(filename, os.path.join(dest_dir, name)) def _DeleteFullResolution(self, fullres_path): """ Delete the full resolution photo from path (Mac, relative) specified """ filename = fullres_path.decode('utf-16-le') filename = os.path.join(self.dbdir, *filename.split(':')[1:]) try: if os.path.isfile(filename): os.remove(filename) except OSError: print 'Problems removing %s.' % filename def AddPhotoToAlbum(self, photo_id, album_id=0): # Default: adding to library """ Add a photo to an album """ counter = 0 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': self.dblines[x-1][1][2] += 40 # Incrementing total length of parent MHSD if self.dblines[x][0] == 'mhba': if counter == album_id: mhba = x break counter+=1 self.dblines[mhba][1][2] += 40 # Incrementing total length of parent album self.dblines[mhba][1][4] += 1 # Incrementing number of pictures for parent album self.dblines.insert(mhba+2+self.dblines[mhba][1][4], ['mhia', ['mhia', 40, 40, 0, photo_id], mhba]) def _RemovePhotoFromAlbums(self, photo_id): """ Remove a photo from all the albums which link to it """ photos_to_remove = [] for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': mhla = x if self.dblines[x][0] == 'mhba': current_mhba = x if self.dblines[x][0] == 'mhia': if self.dblines[x][1][4] == photo_id: photos_to_remove.append(x) self.dblines[current_mhba][1][2] -= 40 self.dblines[current_mhba][1][4] -= 1 self.dblines[mhla-1][1][2] -= 40 #elif self.dblines[x][1][4] > photo_id: # Moved to UpdatePhotoIDs: Created problems when removing! # self.dblines[x][1][4] -= 1 photos_to_remove.sort(reverse=True) for photo in photos_to_remove: del self.dblines[photo] def AddPhoto(self, filename, album_id): """ Add a new photo to the file database """ photo_id = self._AddPhotoToList(filename) self.AddPhotoToAlbum(photo_id, 0) # First adding to the library if album_id != 0: self.AddPhotoToAlbum(photo_id, album_id) def RemovePhoto(self, photo_id): """ Remove a photo from the file database """ self._RemovePhotoFromAlbums(photo_id) self._RemovePhotoFromList(photo_id) def _UpdatePhotoIDs(self): """ Update IDs of the photos avoiding more than +1 differences between numbers """ mhii = 100 IDs = {} for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': IDs[self.dblines[x][1][4]] = mhii self.dblines[x][1][4] = mhii mhii += 1 if self.dblines[x][0] == 'mhia': self.dblines[x][1][4] = IDs[self.dblines[x][1][4]] def RemoveAlbum(self, album_id): """ Remove an existing album """ if album_id == 0: print "Could not remove photo library!" sys.exit() try: mhba = 0 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': mhla = x if self.dblines[x][0] == 'mhba': if mhba == album_id: self.dblines[mhla][1][2] -= 1 # Decrementing number of albums in album list (MHLA) self.dblines[mhla-1][1][2] -= self.dblines[x][1][2] # Decrementing album total length from parent MHSD del self.dblines[x:x+self.dblines[x][1][4]+3] # Actually deleting slice of the album, related MHOD and children MHIAs break mhba += 1 except IndexError: print "Index error in RemoveAlbum: seems not fatal, but should be fixed" def RemoveAlbumAndPhotos(self, album_id): """ Remove an existing album actually deleting all its photos, too """ albums = self.Albums() album_pics = albums[album_id].pics for pic in album_pics: self.RemovePhoto(pic) mhba = 0 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': mhla = x if self.dblines[x][0] == 'mhba': if mhba == album_id: self.dblines[mhla][1][2] -= 1 # Decrementing number of albums in album list (MHLA) self.dblines[mhla-1][1][2] -= self.dblines[x][1][2] # Decrementing album total length from parent MHSD del self.dblines[x:x+3] # Actually deleting slice of the album, related MHOD and string subheader break mhba += 1 def RenameAlbum(self, album_id, new_album_name): # Maybe we should check the length of the provided name... """ Rename a photo album """ new_album_name = new_album_name.encode('utf-8') new_album_name_len = len(new_album_name) new_album_name_pad = getPadding(new_album_name_len) mhba = 0 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhla': mhla = x if self.dblines[x][0] == 'mhba': if mhba == album_id: old_length = self.dblines[x+2][1][0] + self.dblines[x+1][1][5] # Old string name + padding length new_length = new_album_name_len + new_album_name_pad if old_length != new_length: diff_length = new_length - old_length # Needed to update parent containers' total lengths self.dblines[mhla-1][1][2] += diff_length # Updating parent MHSD self.dblines[x][1][2] += diff_length # Updating parent album self.dblines[x+1][1][2] += diff_length # Updating parent string MHOD (MHOB) self.dblines[x+1][1][5] = new_album_name_pad self.dblines[x+2][1][0] = new_album_name_len self.dblines[x+2][1][3] = new_album_name mhba += 1 def FindLostPhotos(self, in_library=False): """ Find photos currently not associated to any album """ lostphotos = [] mhba = 0 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhii': lostphotos.append(self.dblines[x][1][4]) if self.dblines[x][0] == 'mhba': if in_library == True: if mhba == 0: process = True else: process = False else: if mhba > 0: process = True else: process = False mhba += 1 if self.dblines[x][0] == 'mhia' and process == True: lostphotos.remove(self.dblines[x][1][4]) return lostphotos def _PrepareToSave(self): """ Adjust the contents list to be written to the database file, updating total lengths, number of children, etc. """ # Adjusting total length of MHFD self.dblines[0][1][2] = 132 for x in range(0, len(self.dblines)): if self.dblines[x][0] == 'mhsd': self.dblines[0][1][2] += self.dblines[x][1][2] self._UpdatePhotoIDs() # STRUCTURES class Album: """ Album structure """ def __init__(self, name, pics): """ Fill the album structure """ self.name = name self.pics = pics class Photo: """ Photo structure """ def __init__(self, id, fullres): " Fill the photo structure """ self.id = id self.fullres = fullres if __name__ == '__main__': try: DB = MH('./Photos') for album in DB.Albums(): print album.name, '-', len(album.pics), 'pictures:', album.pics for photo in DB.Photos(): print photo.id, photo.fullres except KeyboardInterrupt: print "Interruption requested by user"