Last active
November 11, 2024 04:52
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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