Skip to content

Instantly share code, notes, and snippets.

@quinnaissance
Last active January 27, 2025 10:44
Show Gist options
  • Select an option

  • Save quinnaissance/4d7eeab657f2d0220bb34de754f1f59d to your computer and use it in GitHub Desktop.

Select an option

Save quinnaissance/4d7eeab657f2d0220bb34de754f1f59d to your computer and use it in GitHub Desktop.
Scriptable widget for Immich library
const API_KEY = "API_KEY"
const SERVER_URL = "SERVER_URL" // requires http/https and port (if relevant)
const REFRESH_EVERY = 6 // Hours
/* Headers for later request */
let reqBaseHeaders = {
'x-api-key': API_KEY,
'Accept': 'application/json'
}
/* Sleep function */
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/* Generate the Scriptable widget */
async function createWidget() {
const widget = new ListWidget()
widget.backgroundColor = Color.black()
let imageId = ''
try {
const asset = await getRandomAsset()
if (asset) {
imageId = asset.id
const req = new Request(`${SERVER_URL}/api/assets/${asset.id}/original`)
req.headers = reqBaseHeaders;
const image = await req.loadImage()
const imageStack = widget.addStack()
// imageStack.cornerRadius = 10
const imageElement = imageStack.addImage(image)
imageElement.applyFillingContentMode()
imageElement.centerAlignImage()
// imageElement.containerRelativeShape = true
// imageElement.cornerRadius = 10
}
// Set to refresh every 6hrs
widget.refreshAfterDate = new Date(Date.now() + 1000 * 60 * 60 * REFRESH_EVERY)
} catch (error) {
widget.addText('Unable to fetch original of asset ${imageId}')
let errText = widget.addText(error.toString())
errText.font = Font.systemFont(10);
widget.addSpacer() // Stick text to top
widget.backgroundColor = Color.darkGray()
}
return widget
}
/* Pick a random timeline bucket, and return a suitable image */
async function getRandomAsset() {
const req = new Request(`${SERVER_URL}/api/search/random`)
req.method = 'POST'
req.headers = {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'Accept': 'application/json'
}
req.body = JSON.stringify({
"size": 1,
"type": "IMAGE",
'withExif': true
})
try {
// Loop until appropriate image is found
do {
var finalImage = await req.loadJSON()
var model = finalImage[0].exifInfo.lensModel
if (model === null) sleep(100) // filter out screenshots
} while (model === null)
return finalImage[0]
} catch (error) {
widget.addText('Unable to complete /search/random request')
let errText = widget.addText(error.toString())
errText.font = Font.systemFont(10);
widget.addSpacer() // Stick text to top
widget.backgroundColor = Color.darkGray()
}
}
// Create and run widget
let widget = await createWidget()
Script.setWidget(widget)
Script.complete()
@quinnaissance
Copy link
Copy Markdown
Author

quinnaissance commented Dec 9, 2024

I have no idea what I'm doing so any contributions are much appreciated.

What is this?

  • Basically it will just run JavaScript that fetches a random image using the Immich API, and display it in a widget.
    • This is to imitate the native iOS widgets that display a random photo from your photo library.

How to use

  1. Download Scriptable from the App Store
  2. Create a new script, paste the code above, modify API_KEY and SERVER_URL appropriately
  3. Add a new Scriptable widget to your home screen
  4. For Script select your new script
  5. Tap out of the widget config
  6. The picture should load in the widget. Sometimes it requires swiping back and forth on your home screen pages a couple times, or tapping on the widget.

Other

  • Notes:
    • Currently, the search criteria will filter out images with a null lensModel value (i.e. screenshots), and videos.
  • Possible TODOs:
    • Option to filter images by aspect ratio
      • Could use config.widgetFamily to get the widget's aspect ratio
    • Tapping widget opens the desired photo in immich (not really sure how iOS Universal Links work)
  • Known Issues:
    • Some images are much crisper than others. I don't know why. Possibly related to applyFillingContentMode()'s image scaling algo
      • 2x1 widget in general is quite blurry. 2x2 and 1x1 seem to be decent
    • Corner chamfer is very crude. Seems to apply to both WidgetStack.cornerRadius and `WidgetStack. function does not seem to adapt to widget size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment