Skip to content

Instantly share code, notes, and snippets.

@danielealbano
Last active September 3, 2023 16:43
Show Gist options
  • Save danielealbano/d7f067e52bfae76f402f97c0ad97e095 to your computer and use it in GitHub Desktop.
Save danielealbano/d7f067e52bfae76f402f97c0ad97e095 to your computer and use it in GitHub Desktop.
Google Photo - Photoframe | Is a simple python3 application to transform any device in a photoframe connected to a google photos account! It's very simple, I am using it on a RPi Zero W with the SDL2 rebuilt to run without Xorg. Works on Linux, Windows, Mac OS X and any other device that has an internet connection and can run python 3 and the SDL.
# To run this application you need to create a project in your google cloud console and enable the Photos library API, then you will need to create the credentials setting the type of client to "Other UI" and enabling the access to the user data.
# At this point you will need to download the client_id.json file, rename it in client_secrets.json and place it in the same folder of the application.
# Upon the first start the application will request you to open a link in a browser, authenticate and then write the code back into the console
# requirements
# - oauth2client
# - pysdl2
# - google-api-python-client
import sys
import logging
import sdl2
import sdl2.ext
import sdl2.video
import random
import urllib.request
import tempfile
import threading
import time
from sdl2 import rect, render
from sdl2.ext.compat import isiterable
from googleapiclient import sample_tools
from datetime import datetime
class SoftwareRenderer(sdl2.ext.SoftwareSpriteRenderSystem):
def __init__(self, window):
super(SoftwareRenderer, self).__init__(window)
def render(self, sprites, x=None, y=None):
sdl2.ext.fill(self.surface, sdl2.ext.Color(0, 0, 0))
super().render(sprites, x, y)
class TextureRenderer(sdl2.ext.TextureSpriteRenderSystem):
def __init__(self, target):
super(TextureRenderer, self).__init__(target)
def render(self, sprites, x=None, y=None):
"""Overrides the render method of sdl2.ext.TextureSpriteRenderSystem to
use "SDL_RenderCopyEx" instead of "SDL_RenderCopy" to allow sprite
rotation:
http://wiki.libsdl.org/SDL_RenderCopyEx
"""
r = rect.SDL_Rect(0, 0, 0, 0)
if isiterable(sprites):
rcopy = render.SDL_RenderCopyEx
renderer = self.sdlrenderer
x = x or 0
y = y or 0
for sp in sprites:
r.x = x + sp.x
r.y = y + sp.y
r.w, r.h = sp.size
if rcopy(renderer, sp.texture, None, r, sp.angle, None, render.SDL_FLIP_NONE) == -1:
raise SDLError()
else:
r.x = sprites.x
r.y = sprites.y
r.w, r.h = sprites.size
if x is not None and y is not None:
r.x = x
r.y = y
render.SDL_RenderCopyEx(self.sdlrenderer,
sprites.texture,
None,
r,
sprites.angle,
None,
render.SDL_FLIP_NONE)
render.SDL_RenderPresent(self.sdlrenderer)
class GoogleMediaItemImageSprite(sdl2.ext.Entity):
def __init__(self, world, sprite, posx=0, posy=0, orientation=None):
size = world.systems[0]._renderer.rendertarget.size
angle = 0
if orientation == 'portrait':
angle = -90
self.sprite = sprite
self.sprite.angle = angle
self.sprite.x = int((size[0] - sprite.size[0]) / 2)
self.sprite.y = int((size[1] - sprite.size[1]) / 2)
class GooglePhotosMediaItems:
def __init__(self, albumId, screenSize):
self._albumId = albumId
self._screenSize = screenSize
self._cache = []
self._images = {}
def refresh(self):
service = self._getService()
pageToken = None
mediaItems = []
while True:
response = service.mediaItems().search(body={
'albumId': self._albumId,
'pageToken': pageToken,
'pageSize': 100
}).execute()
pageToken = \
response['nextPageToken'] \
if 'nextPageToken' in response else None
for mediaItem in response['mediaItems']:
mediaItems.append(mediaItem)
if pageToken is None:
break
self._cache = mediaItems
@property
def list(self):
return self._cache
def fetchNewImageUrl(self, size, orientation):
item = random.choice(self._cache)
w = int(item['mediaMetadata']['width'])
h = int(item['mediaMetadata']['height'])
logging.info("New image fetched {filename} is {w}x{h}".format(
filename=item['filename'],
w=w,
h=h
))
if orientation == 'portrait':
size = (size[1], size[0])
if w > h:
new_w = size[0]
new_h = int((float(h) / float(w)) * float(new_w))
else:
new_h = size[1]
new_w = int((float(w) / float(h)) * float(new_h))
logging.info("Requesting image with size {new_w}x{new_h}".format(
new_w=new_w,
new_h=new_h
))
return '{url}=w{width}-h{height}'.format(
url=item['baseUrl'],
width=new_w,
height=new_h
)
def _getService(self):
service, flags = sample_tools.init(
['', '--noauth_local_webserver'],
'photoslibrary',
'v1',
__doc__,
__file__,
scope=[
"https://www.googleapis.com/auth/photoslibrary",
"https://www.googleapis.com/auth/photoslibrary.readonly",
"https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata"
])
return service
class AppUiSdl:
def __init__(self, screenSize, orientation):
self._screenSize = screenSize
self._orientation = orientation
self._window = None
self._sprite_renderer = None
self._world = None
self._factory = None
self._renderer = None
self._running = False
self._resource = False
self._googleMediaItemImageSprite = None
self._setup()
def _setup(self):
sdl2.ext.init()
self._window = sdl2.ext.Window(
"Google Photo Frame - v1.0",
flags=sdl2.video.SDL_WINDOW_OPENGL, # | sdl2.video.SDL_WINDOW_FULLSCREEN,
size=(self._screenSize['width'], self._screenSize['height']))
self._renderer = sdl2.ext.Renderer(self._window)
self._factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=self._renderer)
self._sprite_renderer = TextureRenderer(self._renderer)
self._world = sdl2.ext.World()
self._world.add_system(self._sprite_renderer)
sdl2.SDL_ShowCursor(0)
@property
def isRunning(self):
return self._running
@property
def windowSize(self):
return self._window.size
def start(self):
self._running = True
self._window.show()
def stop(self):
self._running = False
self._window.hide()
def displayImage(self, path):
if self._googleMediaItemImageSprite is not None:
self._googleMediaItemImageSprite.delete()
self._googleMediaItemImageSprite = None
self._googleMediaItemImageSprite = GoogleMediaItemImageSprite(
world=self._world,
sprite=self._factory.from_image(path),
orientation=self._orientation
)
def processEvents(self):
if self._running is False:
return
for event in sdl2.ext.get_events():
if event.type == sdl2.SDL_QUIT:
self._running = False
break
self._world.process()
class DownloadImageThread:
def __init__(self):
self._thread = threading.Thread(
name='DownloadImageThread',
target=self._threadMain,
daemon=True)
self._url = None
self._imagePath = None
self._fetchImageFinished = False
self._fetchImageStart = False
self._thread.start()
def fetch(self, url, imagePath):
self._url = url
self._imagePath = imagePath
self._fetchImageFinished = False
self._fetchImageStart = True
@property
def hasFinished(self):
return self._fetchImageFinished
@property
def url(self):
return self._url
@property
def imagePath(self):
return self._imagePath
def _threadMain(self):
while True:
if self._fetchImageStart is False:
time.sleep(1)
continue
logging.info('Starting to download {url} in {imagePath}'.format(url=self._url, imagePath=self._imagePath))
urllib.request.urlretrieve(self._url, self._imagePath)
logging.info('{url} downloaded in {imagePath}'.format(url=self._url, imagePath=self._imagePath))
self._fetchImageStart = False
self._fetchImageFinished = True
class App:
def __init__(self, config):
self._config = config
self._lastPhotoChangedOn = None
self._tmpdirname = tempfile.TemporaryDirectory()
self._updatingImage = False
self._googlePhotosMediaItems = GooglePhotosMediaItems(albumId=config['albumId'], screenSize=config['screenSize'])
self._ui = AppUiSdl(screenSize=config['screenSize'], orientation=config['orientation'])
self._downloadImageThread = DownloadImageThread()
def main(self):
self._ui.start()
imageName = "image-to-display.jpg"
logging.info('Starting')
while self._ui.isRunning:
time.sleep(0.1)
self._ui.processEvents()
if self._updatingImage is True:
if self._downloadImageThread.hasFinished is True:
self._ui.displayImage(self._downloadImageThread.imagePath)
logging.info('New photo displayed')
self._lastPhotoChangedOn = datetime.now()
self._updatingImage = False
else:
if ((self._lastPhotoChangedOn is None) or ((datetime.now() - self._lastPhotoChangedOn).total_seconds() > self._config['showFor'])):
logging.info(
'Need to update the picture, last update {lastUpdate}'.format(
lastUpdate=self._lastPhotoChangedOn
))
self._updatingImage = True
self._googlePhotosMediaItems.refresh()
self._downloadImageThread.fetch(
url=self._googlePhotosMediaItems.fetchNewImageUrl(
size=self._ui.windowSize,
orientation=self._config['orientation']
),
imagePath='{}/{}'.format(self._tmpdirname.name, imageName)
)
config = {
'albumId': '__ALBUM_ID__',
'showFor': 45, # seconds
'orientation': 'portrait',
'screenSize': {
'width': 1024,
'height': 600
}
}
def main():
logging.basicConfig(level=logging.INFO, format='[%(asctime)s][%(levelname)s][%(threadName)s] %(message)s')
global config
app = App(config)
app.main()
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment