Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Photo album slideshow
-
I wanted to use an older ipad as a photo frame (potential explodey battery issues aside), but found the built in photos slideshow remarkably limiting. No shuffle mode, frequently hangs, can't just display a single image at a time, gets confused easily with shared albums.
Remembered pythonista from many years ago, bought v3, had something going in a couple of hours:
import photos import time import ui import random albums = photos.get_albums() album = [x for x in albums if x.title=='Tvstream'][0] photos = album.assets random.shuffle(photos) iv = ui.ImageView() iv.content_mode=ui.CONTENT_SCALE_ASPECT_FIT iv.present('fullscreen') for photo in photos: iv.image = photo.get_ui_image() time.sleep(5)
Obviously very brittle and hardcoded, but the core is there, and its already working more reliably than the photos app slideshow! That said, I have a couple of questions that I couldn't see answers to in the forum or the docs:
-the fullscreen view has a large white bar at the top with a x on it. Can that be reduced in size, or even removed entirely? Assuming I then provide my own code to bring up a menu on tap to end the slideshow, skip to next image etc
-the album has videos in there too, can pythonista display videos via the ui module? -
@mestela said
-the fullscreen view has a large white bar at the top with a x on it. Can that be reduced in size, or even removed entirely? Assuming I then provide my own code to bring up a menu on tap to end the slideshow, skip to next image etc
Yes, via
iv.present('fullscreen', hide_title_bar=True)
As the close button is absent, To close, swipe down with two fingers
-
@mestela said
-the album has videos in there too, can pythonista display videos via the ui module?
Yes , but you should need ObjectiveC, try this quick and dirty script
Note that my album is named Pastels, replace that by your own album
import photos import time import ui import random import threading from objc_util import * class my_thread(threading.Thread): def __init__(self,asset, vo): threading.Thread.__init__(self) self.asset = asset self.vo = vo def run(self): asseturl = ObjCClass('AVURLAsset').alloc().initWithURL_options_(self.asset.ALAssetURL(),None) i=AVPlayerItem.playerItemWithAsset_(asseturl) p=AVPlayer.playerWithPlayerItem_(i) videolayer=AVPlayerLayer.playerLayerWithPlayer_(p) videolayer.frame=self.vo.bounds() self.vo.layer().addSublayer_(videolayer) p.play() AVPlayerItem=ObjCClass('AVPlayerItem') AVPlayer=ObjCClass('AVPlayer') AVPlayerLayer=ObjCClass('AVPlayerLayer') albums = photos.get_albums() album = [x for x in albums if x.title=='Pastels'][0] photos = album.assets random.shuffle(photos) iv = ui.ImageView() iv.content_mode=ui.CONTENT_SCALE_ASPECT_FIT iv.present('fullscreen', hide_title_bar=True) v = ui.View() v.frame = iv.frame v.hidden = True iv.add_subview(v) vo = ObjCInstance(v) for photo in photos: #print(photo) if photo.media_type == 'image': iv.image = photo.get_ui_image() time.sleep(0.2) elif photo.media_type == 'video': phasset = ObjCInstance(photo) v.hidden = False mythread = my_thread(phasset, vo) mythread.start() time.sleep(photo.duration) v.hidden = True
-
@mestela or an almost pure UI script
import photos import time import ui import random from objc_util import * albums = photos.get_albums() album = [x for x in albums if x.title=='Pastels'][0] photos = album.assets random.shuffle(photos) iv = ui.ImageView() iv.content_mode=ui.CONTENT_SCALE_ASPECT_FIT iv.present('fullscreen', hide_title_bar=True) v = ui.WebView() v.frame = iv.frame v.hidden = True iv.add_subview(v) for photo in photos: #print(photo) if photo.media_type == 'image': iv.image = photo.get_ui_image() time.sleep(0.2) elif photo.media_type == 'video': fil = str(ObjCInstance(photo).filename()) dir = str(ObjCInstance(photo).directory()) path = '/var/mobile/Media/' + dir + '/' + fil v.load_url(path) v.hidden = False time.sleep(photo.duration) v.hidden = True
-
Wow, thanks for the great replies @cvp ! I'll try these out tonight.
-
Eyy, that full screen option works great!
Video is proving tricky though. All my videos play directly from the photos app, but many don’t play through Pythonista. I’ve found a few other video player implementations on the forum, they all seem to behave the same.
When I print the video paths to the console, I’ve noticed a pattern. Videos that play have these shorter paths, and tend to be older (like filmed at least 4 years ago) :
/var/mobile/Media/DCIM/101APPLE/IMG_1684.MOV
/var/mobile/Media/DCIM/101APPLE/IMG_1821.MOVWhile newer ones look like this, and won’t play:
/var/mobile/Media/PhotoData/CPLAssets/group286/9683CE80-D66B-488B-B3C0-BCDBA800F11D.MOV
/var/mobile/Media/PhotoData/CPLAssets/group1/DF32BCF7-E210-406F-99A9-FDCC843117C1.MP4
/var/mobile/Media/PhotoData/CPLAssets/group21/3A29108B-320A-43E8-87BD-08A5CDEDAC11.MOVIs that a known issue?
-
@mestela said
/var/mobile/Media/PhotoData/CPLAssets/group....
A quick search via Google seems to say that if you find this kind of path, that will say that your asset is on iCloud and you only see a thumbnail. Could you try to force download on your device by playing it in the Photos app. And then, use Pythonista script. Anyway, I don't know this issue
And, if you use the first script, does it show all videos?
-
Hmm, all the videos play in the photos album fine. The original script didn’t play any of the videos either.
If there’s no known solution, maybe I can try copying the videos to a new album, see if that’s enough to make them ‘local’ and not iCloud based, if that’ll make the playback system happier.
-
@mestela said
The original script didn’t play any of the videos either.
Weird. For me, on IPad iOS 16.3.1 with Pythonista beta 340008, it works but my photos/vidéos are not on iCloud.
-
@mestela said
maybe I can try copying the videos to a new album
I can't understand , if your photos are synchronized with iCloud, why this copy could change something.
-
Seemed to work for this person:
https://developer.apple.com/forums/thread/676918
When I checked the asset url's, I noticed that if the video has been retrieved from iCloud, its filename has a ".medium" postfix like in this example:
file:///var/mobile/Media/PhotoData/Metadata/PhotoData/CPLAssets/group338/191B2348-5E19-4A8E-B15C-A843F9F7B5A3.medium.MP4
The weird thing is, if I use FileManager to copy the video in this url to another directory, create a new AVAsset from that file, and use that asset when creating the AVExportSession instance, the problem goes away.
No guarantees of course, but will give it a go over the weekend.
-
Worked on the rest of it, so fade up/fade down of the images, and a tap will skip to the next one.
Still lots wrong with it (timer needs to reset when you tap, needs a subtle progress bar, play/pause controls, needs a ui to choose the album, code layout is terrible, etc), but again, its now behaving more like I want a photo slideshow to run. Loving pythonista!
import ui import itertools import photos import time import random class MyView (ui.View): # get image assets albums = photos.get_albums() album = [x for x in albums if x.title == 'Tvstream'][0] assets = album.assets random.shuffle(assets) assets = [x for x in assets if x.media_type == 'image'] print('total assets: ' + str(len(assets))) i = 0 asset = assets[i] def draw(self): img_view.image=self.asset.get_ui_image() def touch_ended(self, touch): # Called when a touch begins. self.i += 1 self.asset = self.assets[self.i] print('asset is now: '+str(self.asset)) self.set_needs_display() def touch_moved(self, touch): # Called when a touch moves. pass def touch_began(self, touch): # Called when a touch ends. pass v = MyView() v.background_color = 'black' w,h = ui.get_screen_size() v.frame = (0,0,w,h) img_view = ui.ImageView() img_view.frame = v.frame img_view.content_mode = ui.CONTENT_SCALE_ASPECT_FIT img_view.image=v.asset.get_ui_image() v.add_subview(img_view) v.present('fullscreen',hide_title_bar=True) for asset in itertools.cycle(v.assets): start_time = time.time() current_time = start_time hold_time = 15 end_time = start_time + hold_time # interruptable timer iv = img_view while current_time < end_time: if iv.alpha < 1.0 and current_time-start_time<1.0: iv.alpha += 0.1 if end_time - current_time < 1.0: iv.alpha -= 0.1 time.sleep(0.05) current_time+=0.05 v.i += 1 v.asset = v.assets[v.i] v.set_needs_display()
-
@mestela If you want that the iOS status bar (hour/date/wifi/battery...) does not override your photo, please add this method to your class.
As your iPad is "older", I hope that its iOS is compatible with this code.
from objc_util import * . . . class MyView (ui.View): . . . def layout(self): insets = self.objc_instance.safeAreaInsets() self.frame = self.frame.inset(insets.top, insets.left, insets.bottom, insets.right)
-
eyy that works great, thanks!
update, now using the itertools.cycle properly so it runs endlessly without halting, the timer is properly reset on click, few other little things:
import ui import itertools import photos from pprint import pprint import time import random from objc_util import * class MyView (ui.View): hold_time = 15 albums = photos.get_albums() album = [x for x in albums if x.title == 'Tvstream'][0] assets = album.assets assets = [x for x in assets if x.media_type == 'image'] random.shuffle(assets) print('total assets: ' + str(len(assets))) assets = itertools.cycle(assets) asset = next(assets) start_time = time.time() current_time = start_time end_time = start_time + hold_time def layout(self): insets = self.objc_instance.safeAreaInsets() self.frame = self.frame.inset(insets.top, insets.left, insets.bottom, insets.right) def reset_timer(self): self.start_time = time.time() self.current_time = self.start_time self.end_time = self.start_time + self.hold_time def draw(self): iv.image=self.asset.get_ui_image() def touch_ended(self, touch): # Called when a touch begins. self.asset = next(self.assets) print('asset is now: '+str(self.asset)) self.reset_timer() self.set_needs_display() def touch_moved(self, touch): # Called when a touch moves. pass def touch_began(self, touch): # Called when a touch ends. pass v = MyView() v.background_color = 'black' w,h = ui.get_screen_size() v.frame = (0,0,w,h) iv = ui.ImageView() iv.frame = v.frame iv.content_mode = ui.CONTENT_SCALE_ASPECT_FIT iv.alpha=0.0 iv.image=v.asset.get_ui_image() v.add_subview(iv) v.reset_timer() v.present('fullscreen',hide_title_bar=True) v.asset = next(v.assets) while 1: # interuptable timer iv.alpha = 0.0 while v.current_time < v.end_time: if iv.alpha < 1.0 and v.current_time-v.start_time<1.0: iv.alpha += 0.1 if v.end_time - v.current_time < 1.0: iv.alpha -= 0.1 time.sleep(0.05) v.current_time+=0.05 v.reset_timer() v.asset = next(v.assets) v.set_needs_display()
-
insets = self.objc_instance.safeAreaInsets()
self.frame = self.frame.inset(insets.top, insets.left, insets.bottom, insets.right)Does this work?
self.frame = self.frame.inset(*self.objc_instance.safeAreaInsets())
-
@ccc no
self.frame = self.frame.inset(*self.objc_instance.safeAreaInsets())
TypeError: Rect.inset() argument after * must be an iterable, not UIEdgeInsets
-
Nice, thanks!
Funny that often when I lookup how to implement the next feature I'm interested in, I also find a way to simplify code I've written. The gestures module looks perfect for handling swipe events, in finding that I also discovered I should be using ui.animate and ui.delay. Too many cool things!
I'm also pleased that this redesign seems to be more stable; if I left the previous script run overnight, in the morning pythonista had inevitably crashed. This morning I came in to see the ipad was still happily showing images. :)
-
@mestela said
The gestures module looks perfect for handling swipe events
Not only it looks perfect, but it is perfect. I use it very often.
-
Yep, too easy. Now I have swipe working, but that in turn meant I had to replace the itertools endless cycle with a deque. Calling this done for now.
import ui import itertools import photos from pprint import pprint import time import random from objc_util import * import gestures from collections import deque class MyView (ui.View): hold_time = 15 albums = photos.get_albums() album = [x for x in albums if x.title == 'Tvstream'][0] assets = album.assets assets = [x for x in assets if x.media_type == 'image'] random.shuffle(assets) #assets = assets[-5:] print('total assets: ' + str(len(assets))) assets = deque(assets) asset = assets[0] start_time = time.time() current_time = start_time end_time = start_time + hold_time def layout(self): insets = self.objc_instance.safeAreaInsets() self.frame = self.frame.inset(insets.top, insets.left, insets.bottom, insets.right) def reset_timer(self): self.start_time = time.time() self.current_time = self.start_time self.end_time = self.start_time + self.hold_time def draw(self): iv.image=self.asset.get_ui_image() v = MyView() v.background_color = 'black' w,h = ui.get_screen_size() v.frame = (0,0,w,h) iv = ui.ImageView() iv.frame = v.frame iv.content_mode = ui.CONTENT_SCALE_ASPECT_FIT iv.alpha=0.0 iv.image=v.asset.get_ui_image() v.add_subview(iv) v.present('fullscreen',hide_title_bar=True) def swipe_left(data): v.assets.rotate(-1) v.asset = v.assets[0] v.reset_timer() v.set_needs_display() def swipe_right(data): v.assets.rotate(1) v.asset = v.assets[0] v.reset_timer() v.set_needs_display() gestures.swipe(v, swipe_left, direction=gestures.LEFT) gestures.swipe(v, swipe_right, direction=gestures.RIGHT) while 1: # interuptable timer iv.alpha = 0.0 while v.current_time < v.end_time: if iv.alpha < 1.0 and v.current_time-v.start_time<1.0: iv.alpha += 0.1 if v.end_time - v.current_time < 1.0: iv.alpha -= 0.1 time.sleep(0.05) v.current_time+=0.05 v.assets.rotate(-1) v.asset = v.assets[0] v.reset_timer() v.set_needs_display()
-
@mestela you can move two lines in your reset_timer() method
def reset_timer(self): self.asset = v.assets[0] # <------- self.start_time = time.time() self.current_time = self.start_time self.end_time = self.start_time + self.hold_time self.set_needs_display() # <-------
def swipe_left(data): v.assets.rotate(-1) #v.asset = v.assets[0] v.reset_timer() #v.set_needs_display() def swipe_right(data): v.assets.rotate(1) #v.asset = v.assets[0] v.reset_timer() #v.set_needs_display() . . . while 1: . . v.assets.rotate(-1) #v.asset = v.assets[0] v.reset_timer() #v.set_needs_display()