""" Fupper - Flickr upload tool for Series 60 Copyright (C) 2006 Teemu Harju 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. teemu.harju@gmail.com $Author$ $Rev$ $LastChangedDate$ """ import os import sys import math import time import sysinfo import thread import re import appuifw import e32 from topwindow import * from pys60flickrapi import FlickrAPI from pys60flickrapi import FlickrException from graphics import Image from key_codes import EKeyLeftArrow, EKeyRightArrow, EKeySelect, EKeyDownArrow, EKeyUpArrow from __main__ import animation, frame_pos, text_pos, APPNAME, APPABOUT, APPPATH APPLICATION = u'Fupper' ENTRIES = [(u'Phone Memory', u'Images from phone memory'), (u'Memory Card', u'Images from memory card')] DIRS = None if e32.s60_version_info >= (3,0): DIRS = [u'c:\\Data\\Images', u'e:\\Images'] else: DIRS = [u'c:\\Nokia\Images', u'e:\\Images'] LOGO_IMAGES = [] BACK_TO_MENU = -1 SHOW_PREVIEW = -2 AUTH_TEXT = u"Authentication request:\nGo to www.flickr.com/auth-21694." THUMBNAIL_SIZE = (80,60) THUMBNAIL_POS = (appuifw.app.body.size[0] - (THUMBNAIL_SIZE[0] + 40), 10) #----------------------------------------------------------------------- def __thumbnail_loaded_callback(index, thumb): print "Thumbnail loaded callback!" THUMBNAILS[index] = thumb CALLGATE = e32.ao_callgate(__thumbnail_loaded_callback) TOPWIN = TopWindow() TOPWIN.size = THUMBNAIL_SIZE TOPWIN.shadow = 3 TOPWIN.position = THUMBNAIL_POS THUMBNAILS = {} class Fupper: """ Base class for the Fupper application.""" #----------------------------------------------------------------------- def __init__(self): """ Constructor for Fupper class. It stores the Flickr API key and shared secret and manages the different states of the application as well as the menu structure. """ self.script_lock = e32.Ao_lock() self.thumb_lock = None # created in the thumbnail thread self.thumb_thread_id = None self.lb = appuifw.Listbox(ENTRIES, self.__lbox_observe) self.current_dir = None self.files = [] self.flickrAPIKey = "bb627fee1d9f3ee5ec8f278bf68e20a6" self.flickrSecret = "4237c2e13905d4e8" self.flickrAPI = FlickrAPI(self.flickrAPIKey, self.flickrSecret) self.flickrAuthToken = self.flickrAPI.checkToken(perms="write") # authenticate Fupper if no authToken was found from memory self.__authenticate() self.photosets = None # self.__get_photosets() self.rsp = None self.uploading_photo = 0 self.thumbnails_loaded = 0 self.thumbnail_lock = thread.allocate_lock() self.canvas = None self.img = None #self.thumbnails = {} #self.topwin = TopWindow() #self.topwin.size = THUMBNAIL_SIZE #self.topwin.shadow = 3 #self.topwin.position = THUMBNAIL_POS self.preview_running = 0 self.preview_loaded = 0 self.top_menu = [(u'Open', lambda:self.__lbox_observe(self.lb.current())), (u'About', lambda:appuifw.note(APPABOUT)), (u'Exit', self.__exit_key_handler) ] self.dir_menu = [(u'Show preview', lambda:self.__show_preview(self.lb.current())), (u'Upload to Flickr', lambda:self.__upload_to_flickr(self.lb.current())), (u'Back', lambda:self.__lbox_observe(BACK_TO_MENU)), (u'Exit', self.__exit_key_handler) ] self.preview_menu = [(u'Upload to Flickr', lambda:self.__upload_to_flickr(self.lb.current())), (u'Back', lambda:self.__preview_event_callback({'keycode': EKeyLeftArrow})) ] appuifw.app.menu = self.top_menu appuifw.app.exit_key_handler = self.__exit_key_handler #self.thumb_callgate = e32.ao_callgate(self.__thumbnail_loaded_callback) #----------------------------------------------------------------------- def run(self): """ Runs the Fupper application.""" self.lb.bind(EKeyLeftArrow, lambda: self.__lbox_observe(BACK_TO_MENU)) self.lb.bind(EKeyRightArrow, lambda: self.__lbox_observe(SHOW_PREVIEW)) self.lb.bind(EKeyUpArrow, lambda: self.__thumbnail_callback(EKeyUpArrow)) self.lb.bind(EKeyDownArrow, lambda: self.__thumbnail_callback(EKeyDownArrow)) self.__refresh() self.script_lock.wait() #----------------------------------------------------------------------- def __refresh(self, menu=None): """ Refreshes the UI of the application. menu -- list of tuples containing the menu """ appuifw.app.title = APPNAME if menu != None: appuifw.app.menu = menu appuifw.app.body = self.lb #----------------------------------------------------------------------- def __exit_key_handler(self): """ Exits the application.""" if appuifw.query(u'Are you sure you want to exit?', 'query'): self.preview_running = 0 self.script_lock.signal() appuifw.app.exit_key_handler = None appuifw.app.set_exit() else: pass #----------------------------------------------------------------------- def __lbox_observe(self, ind=None): """Monitors the listbox that is used to browse files etc in the user interface. ind -- index number used for different commands to the listbox """ if ind <> None: index = ind else: index = self.lb.current() print "Index: %s" % str(index) if index >= 0: if self.current_dir == None: self.current_dir = index print "self.current_dir = %s" % str(self.current_dir) print "Current directory: %s" % DIRS[self.current_dir] entries = [] for file in os.listdir(DIRS[index]): path = os.path.join(DIRS[index], file) # only .jpg and .png files are supported if os.path.isfile(path) and (path.endswith('.jpg') or path.endswith('.png')): self.files.append(file) entries.append((unicode(file), self.__get_file_modified_str(path) + self.__get_file_size_str(path))) if len(self.files) > 0: self.lb.set_list(entries) # start loading thumbnails print "Should start loading thumbnails now..." if not self.thumbnails_loaded and self.current_dir <> None: print "Starting the thumbnail thread..." self.thumb_thread_id = thread.start_new_thread(self.__load_thumbnails_thread, ()) self.__refresh(self.dir_menu) else: appuifw.note(u'No images in ' + ENTRIES[index][0]); self.current_dir = None else: self.__upload_to_flickr(self.lb.current()) elif index == BACK_TO_MENU: self.current_dir = None #self.thumb_lock.signal() thread.ao_waittid(self.thumb_thread_id) # wait for the thread to exit TOPWIN.hide() self.thumbnails_loaded = 0 THUMBNAILS = {} TOPWIN.images = [] print "Thumbnails cleared..." self.files = [] self.lb.set_list(ENTRIES) self.__refresh(self.top_menu) elif index == SHOW_PREVIEW: if self.current_dir == None: # we are in the main menu pass # do nothing else: self.__show_preview(self.lb.current()) #----------------------------------------------------------------------- def __get_file_size_str(self, path): """Returns the size of the file in kilobytes as a string formatted as XXX kB. path -- path to the file """ return unicode(self.__get_kbytes(os.stat(path).st_size)) + u' kB' #----------------------------------------------------------------------- def __get_kbytes(self, bytes): """Returns filesize in kilobytes. bytes -- number of bytes to be converted to kilobytes """ return bytes/1024 #----------------------------------------------------------------------- def __get_file_modified_str(self, path): """Returns a string of the date when the file was last modified. path -- path to the file """ return time.strftime("%d.%m.%Y %H:%M ", time.localtime(os.stat(path).st_mtime)); #----------------------------------------------------------------------- def __show_preview(self, index): """ Shows a preview of a photo. index -- index of the file in listbox self.lb """ self.preview_running = 1 appuifw.app.menu = self.preview_menu if self.canvas == None: self.canvas = appuifw.Canvas(redraw_callback=self.__handle_redraw, event_callback=self.__preview_event_callback) self.canvas.clear() self.img = None path = os.path.join(DIRS[self.current_dir], self.files[index]) imgsize = Image.inspect(path)['size'] self.img = Image.new(imgsize) self.preview_loaded = 0 self.img.load(path, callback=self.__preview_loaded_callback) appuifw.app.body = self.canvas while self.preview_running: frame = 0 while not self.preview_loaded: self.canvas.blit(animation[frame%17], frame_pos) frame = frame + 1 e32.ao_sleep(0.2) self.__handle_redraw(()) e32.ao_yield() self.__refresh(self.dir_menu) #----------------------------------------------------------------------- def __preview_loaded_callback(self, err): """Callback function for image preview. This function is called when the image to be previewed has been loaded completely. """ self.preview_loaded = 1 #----------------------------------------------------------------------- def __handle_redraw(self, rect): """Redraws the canvas where the image preview is shown.""" self.canvas.blit(self.img, target=((0,0), self.canvas.size), scale=1) #----------------------------------------------------------------------- def __preview_event_callback(self, arg): """Callback function that handles the key events while the preview of the photo is running. """ if arg['keycode'] == EKeyLeftArrow: self.preview_running = 0 elif arg['keycode'] == EKeySelect: self.__upload_to_flickr(self.lb.current()) else: pass #----------------------------------------------------------------------- def __load_thumbnails_thread(self): """Loads thumbnail images form given entries. This function is run in background while browsing the list of image files. """ global thumb print "Thumb thread started.." #if not self.thumb_lock: # self.thumb_lock = e32.Ao_lock() index = 0 for filename in self.files: print "Loading file %s" % filename temp = Image.open(unicode(os.path.join(DIRS[self.current_dir], filename))) # scaling should be faster than resizing so we use that here thumb = Image.new(THUMBNAIL_SIZE) thumb.blit(temp, target=((0,0), THUMBNAIL_SIZE), scale=1) print "Calling CALLGATE..." CALLGATE(index, thumb) print "CALLGATE returned..." index = index + 1 self.thumbnails_loaded = 1 print "Thumbnails loaded!" #self.thumb_lock.wait() #print "Thumb lock released!" #----------------------------------------------------------------------- def __thumbnail_callback(self, key): """A callback function that is called whenever the selected element in the directory listing changes. This function then sets the thumbnail to the topwindow if it has been loaded by __load_thumbnails(). """ if self.current_dir <> None: index = self.lb.current() last_index = len(self.files) - 1 if key == EKeyUpArrow and index == 0: # top of the list moving up index = last_index elif key == EKeyDownArrow and index == last_index: # at the end and going down index = 0 elif key == EKeyUpArrow: index = index - 1 elif key == EKeyDownArrow: index = index + 1 else: pass # we should not end up here at all if index in THUMBNAILS: TOPWIN.images = [(THUMBNAILS[index], (0,0))] if not TOPWIN.visible: TOPWIN.show() else: if TOPWIN.visible: TOPWIN.hide() TOPWIN.images = [] # clear thumbnails #----------------------------------------------------------------------- def __verify_token(self, token): """Verifys the given minitoken. First of all this function checks if the given minitoken is of correct format and then gets the full authentication token from Flickr. The token is stored in the memory so authentication is needed only once. token -- string of the authentication token """ if re.match("^[0-9]{3}-[0-9]{3}-[0-9]{3}$", token): try: self.flickrAuthToken = self.flickrAPI.getFullToken(token) return 1 except FlickrException, e: appuifw.note(unicode(e.msg), 'error') return 0 except IOError, e: appuifw.note(u'Connection failed!', 'error') else: return 0 #----------------------------------------------------------------------- def __authenticate(self): """Authenticates the application to Flickr. Basically only asks user to insert the minitoken found from Flickr site and calls __verify_token to check and store the token for later use. If user cancels, the application exits. """ if self.flickrAuthToken: pass # do nothing, app is authenticated else: proceed = appuifw.query(AUTH_TEXT, 'query') if proceed: miniToken = appuifw.query(u'Give the mini-token:', 'text') while not self.__verify_token(miniToken): miniToken = appuifw.query(u'Invalid token! Try again:', 'text') if miniToken == 0: # user has pressed cancel return appuifw.note(u'Token accepted!') else: sys.exit() # user did not want to authenticate the app appuifw.note(u'Logged in to Flickr as %s.' % self.flickrAPI.fullname) #----------------------------------------------------------------------- def __upload_to_flickr(self, index): """Uploads photo to Flickr. index -- index of the photo in listbox self.lb """ if appuifw.query(u'Do you want to upload this image to Flickr?', 'query'): title = appuifw.query(u'Give title for the photo:', 'text') if title == None: title = self.files[index] # use filename if title not given desc = appuifw.query(u'Give description for the photo:', 'text') if desc == None: desc = "" tags = appuifw.query(u'Give tags for the photo:', 'text') if tags == None: tags = "fupper" else : tags = tags + " fupper" tags = tags.strip() is_public = is_friend = is_family = "0" if appuifw.query(u'Do you want to make this photo private?', 'query'): is_public = "0" # photo will be private if appuifw.query(u'Allow friends or family see this photo too?', 'query'): acl = appuifw.multi_selection_list([u'Visible to friends', u'Visible to family']) if len(acl) == 0: appuifw.note(u'Only you can view the photo.') elif len(acl) == 2: # both choises have been selected is_friend = "1" is_family = "1" elif acl[0] == 0: is_friend = "1" else: is_family = "1" else: appuifw.note(u'Only you can view the photo.') else: appuifw.note(u'Photo will be public.') is_public = "1" photo = os.path.join(DIRS[self.current_dir], self.files[index]) appuifw.note(u'Uploading %s...' % self.files[index]) self.uploading_photo = 1 thread.start_new_thread(self.__upload_thread, (photo, title, desc, tags, is_public, is_friend, is_family)) frame = 0 canvas = appuifw.Canvas() canvas.clear() appuifw.app.body = canvas while self.uploading_photo: canvas.blit(animation[frame%17], frame_pos) canvas.text(text_pos, u"Uploading...") frame = frame + 1 e32.ao_sleep(0.2) if self.rsp == None: print("can't find file\n") else: self.flickrAPI.testFailure(self.rsp) appuifw.note(u'Photo uploaded!') self.__add_to_photoset(self.rsp.photoid[0].elementText) self.__refresh() else: pass #----------------------------------------------------------------------- def __upload_thread(self, filename, title, desc, tags, is_public, is_friend, is_family): """Thread used to upload the photo in the background filename -- name of the uploaded file title -- title of the photo desc -- description of the photo tags -- Flickr tags is_public -- whether the photo is public or not """ self.rsp = self.flickrAPI.upload(filename=filename, api_key=self.flickrAPIKey, auth_token=self.flickrAuthToken, title=title, description=desc, tags=tags, is_public=is_public, is_friend=is_friend, is_family=is_family) self.uploading_photo = 0 #----------------------------------------------------------------------- def __get_photosets(self): """Returns list of user's photosets returns -- list of tuples of photoset id and title """ rsp = self.flickrAPI.photosets_getList(api_key=self.flickrAPIKey, auth_token=self.flickrAuthToken) self.flickrAPI.testFailure(rsp) photoset_list = [] for photoset in rsp.photosets[0].photoset: photoset_list.append((photoset['id'], unicode(photoset.title[0].elementText))) return photoset_list #----------------------------------------------------------------------- def __add_to_photoset(self, photoid): """Asks whether the user wants to add the photo to some photoset and then adds it. photoid -- flickr photoid of the photo to be added """ if appuifw.query(u'Do you want to add this photo to some photoset?', 'query'): photosets = [] if self.photosets == None: appuifw.note(u'Getting photosets...') self.photosets = self.__get_photosets() for photoset in self.photosets: photosets.append(photoset[1]) index = appuifw.popup_menu(photosets) rsp = self.flickrAPI.photosets_addPhoto(api_key=self.flickrAPIKey, auth_token=self.flickrAuthToken, photoset_id=self.photosets[index][0], photo_id=photoid) self.flickrAPI.testFailure(rsp) appuifw.note(u'Photo added to photoset!') else: pass def run(): f = Fupper() if e32.s60_version_info >= (3,0): f.run() else: e32.ao_sleep(0, f.run) if __name__ == '__main__': run()