From 14d3b4d66b872153a3abf70b5a9c8474fec4f44e Mon Sep 17 00:00:00 2001 From: DeftDawg Date: Mon, 27 Oct 2025 23:44:55 -0400 Subject: [PATCH] - Add Wikimedia Commons app with Pic of the Day (POTD), Pic from on this day (in the past), Random Previous Pic of the day, and Random Pic --- frameos/src/apps/apps.nim | 4 + frameos/src/apps/data/wikicommons/app.nim | 168 ++++++++++++++++++ frameos/src/apps/data/wikicommons/config.json | 46 +++++ 3 files changed, 218 insertions(+) create mode 100644 frameos/src/apps/data/wikicommons/app.nim create mode 100644 frameos/src/apps/data/wikicommons/config.json diff --git a/frameos/src/apps/apps.nim b/frameos/src/apps/apps.nim index fdf8c5f9..5b93c4f6 100644 --- a/frameos/src/apps/apps.nim +++ b/frameos/src/apps/apps.nim @@ -20,6 +20,7 @@ import apps/data/resizeImage/app_loader as data_resizeImage_loader import apps/data/rotateImage/app_loader as data_rotateImage_loader import apps/data/rstpSnapshot/app_loader as data_rstpSnapshot_loader import apps/data/unsplash/app_loader as data_unsplash_loader +import apps/data/wikicommons/app_loader as data_wikicommons_loader import apps/data/xmlToJson/app_loader as data_xmlToJson_loader import apps/logic/breakIfRendering/app_loader as logic_breakIfRendering_loader import apps/logic/ifElse/app_loader as logic_ifElse_loader @@ -56,6 +57,7 @@ proc initApp*(keyword: string, node: DiagramNode, scene: FrameScene): AppRoot = of "data/rotateImage": data_rotateImage_loader.init(node, scene) of "data/rstpSnapshot": data_rstpSnapshot_loader.init(node, scene) of "data/unsplash": data_unsplash_loader.init(node, scene) + of "data/wikicommons": data_wikicommons_loader.init(node, scene) of "data/xmlToJson": data_xmlToJson_loader.init(node, scene) of "logic/breakIfRendering": logic_breakIfRendering_loader.init(node, scene) of "logic/ifElse": logic_ifElse_loader.init(node, scene) @@ -93,6 +95,7 @@ proc setAppField*(keyword: string, app: AppRoot, field: string, value: Value) = of "data/rotateImage": data_rotateImage_loader.setField(app, field, value) of "data/rstpSnapshot": data_rstpSnapshot_loader.setField(app, field, value) of "data/unsplash": data_unsplash_loader.setField(app, field, value) + of "data/wikicommons": data_wikicommons_loader.setField(app, field, value) of "data/xmlToJson": data_xmlToJson_loader.setField(app, field, value) of "logic/breakIfRendering": logic_breakIfRendering_loader.setField(app, field, value) of "logic/ifElse": logic_ifElse_loader.setField(app, field, value) @@ -145,6 +148,7 @@ proc getApp*(keyword: string, app: AppRoot, context: ExecutionContext): Value = of "data/rotateImage": data_rotateImage_loader.get(app, context) of "data/rstpSnapshot": data_rstpSnapshot_loader.get(app, context) of "data/unsplash": data_unsplash_loader.get(app, context) + of "data/wikicommons": data_wikicommons_loader.get(app, context) of "data/xmlToJson": data_xmlToJson_loader.get(app, context) of "render/calendar": render_calendar_loader.get(app, context) of "render/color": render_color_loader.get(app, context) diff --git a/frameos/src/apps/data/wikicommons/app.nim b/frameos/src/apps/data/wikicommons/app.nim new file mode 100644 index 00000000..9419b976 --- /dev/null +++ b/frameos/src/apps/data/wikicommons/app.nim @@ -0,0 +1,168 @@ +import pixie +import strformat +import strutils +import httpclient +import frameos/apps +import frameos/types +import frameos/utils/image +import nre +import times +import random + +type + AppConfig* = object + mode*: string + submode*: string + saveAssets*: string + + App* = ref object of AppRoot + appConfig*: AppConfig + +proc init*(self: App) = + randomize() + self.appConfig.mode = self.appConfig.mode.strip() + self.appConfig.submode = self.appConfig.submode.strip() + +proc error*(self: App, context: ExecutionContext, message: string): Image = + self.logError(message) + result = renderError(if context.hasImage: context.image.width else: self.frameConfig.renderWidth(), + if context.hasImage: context.image.height else: self.frameConfig.renderHeight(), message) + +proc getImageUrlFromFilePage(client: HttpClient, fileUrl: string): string = + # Fetch the file page and extract the original file URL + let response = client.request(fileUrl, httpMethod = HttpGet) + if response.code != Http200: + raise newException(CatchableError, &"Error fetching file page: {response.status}") + + let body = response.body + # Look for Original file + let m = body.match(re"""]*>Original file""") + if m.isSome: + let href = m.get.captures[0] + result = if href.startsWith("http"): href else: "https:" & href + else: + raise newException(CatchableError, "Could not find original file link") + +proc getPotdInfo(client: HttpClient, year: int, month: int, day: int): tuple[url: string, description: string] = + let url = &"https://commons.wikimedia.org/wiki/Template:Potd/{year}-{month:02d}" + let response = client.request(url, httpMethod = HttpGet) + if response.code != Http200: + raise newException(CatchableError, &"Error fetching POTD template: {response.status}") + + let body = response.body + # Find the div with id="day" + let divPattern = &"id=\"{day}\"" + let divStart = body.find(divPattern) + if divStart == -1: + raise newException(CatchableError, &"No POTD for {year}-{month:02d}-{day:02d}") + + # Find the end of the div, assuming it's the next + let divEnd = body.find("", divStart) + if divEnd == -1: + raise newException(CatchableError, "Malformed HTML") + + let divContent = body[divStart..divEnd] + + # Extract href + let mHref = divContent.match(re"""""") + if mHref.isNone: + raise newException(CatchableError, "No image link found") + let href = mHref.get.captures[0] + let fileUrl = "https://commons.wikimedia.org" & href + + # Extract description + let mDesc = divContent.match(re"""
([^<]*)
""") + let description = if mDesc.isSome: mDesc.get.captures[0] else: "" + + result = (fileUrl, description) + +proc getRandomImage(client: HttpClient): tuple[url: string, description: string] = + # Fetch random file page + let url = "https://commons.wikimedia.org/wiki/Special:Random/File" + let response = client.request(url, httpMethod = HttpGet) + if response.code != Http200: + raise newException(CatchableError, &"Error fetching random file: {response.status}") + + let body = response.body + # Extract href + let mHref = body.match(re"""
]*>Original file""") + if mHref.isNone: + raise newException(CatchableError, "Could not find original file link") + + let href = mHref.get.captures[0] + let imageUrl = if href.startsWith("http"): href else: "https:" & href + + # For description, from title + let mTitle = body.match(re"""([^<]+)""") + let description = if mTitle.isSome: mTitle.get.captures[0] else: "" + + result = (imageUrl, description) + +proc get*(self: App, context: ExecutionContext): Image = + let width = if context.hasImage: context.image.width else: self.frameConfig.renderWidth() + let height = if context.hasImage: context.image.height else: self.frameConfig.renderHeight() + + try: + var client = newHttpClient(timeout = 60000) + defer: client.close() + + var imageUrl: string + var description: string + + if self.appConfig.mode == "random": + (imageUrl, description) = getRandomImage(client) + else: + # potd mode + let now = now() + var year, month, day: int + case self.appConfig.submode: + of "day": + year = now.year + month = now.month.int + day = now.monthday.int + of "onthisday": + year = rand(2008..now.year) + month = now.month.int + day = now.monthday.int + of "month": + year = now.year + month = now.month.int + day = rand(1..31) + of "random": + year = rand(2008..now.year) + month = rand(1..12) + day = rand(1..31) + else: + return self.error(context, "Invalid submode") + + var retries = 0 + while retries < 5: + try: + let (fileUrl, desc) = getPotdInfo(client, year, month, day) + description = desc + imageUrl = getImageUrlFromFilePage(client, fileUrl) + break + except: + if self.appConfig.submode == "random": + # Retry with new random date + year = rand(2008..now.year) + month = rand(1..12) + day = rand(1..31) + retries += 1 + else: + raise + + if self.frameConfig.debug: + self.log(&"Image URL: {imageUrl}") + + # Download the image + let imageResponse = client.request(imageUrl, httpMethod = HttpGet) + if imageResponse.code != Http200: + return self.error(context, &"Error fetching image: {imageResponse.status}") + + if self.appConfig.saveAssets == "auto" or self.appConfig.saveAssets == "always": + discard self.saveAsset(&"wikicommons {width}x{height}", ".jpg", imageResponse.body, self.appConfig.saveAssets == "auto") + + result = decodeImage(imageResponse.body) + except CatchableError as e: + return self.error(context, "Error fetching image from Wikimedia Commons: " & $e.msg) diff --git a/frameos/src/apps/data/wikicommons/config.json b/frameos/src/apps/data/wikicommons/config.json new file mode 100644 index 00000000..d6af8db4 --- /dev/null +++ b/frameos/src/apps/data/wikicommons/config.json @@ -0,0 +1,46 @@ +{ + "name": "Wikimedia Commons", + "description": "Images from Wikimedia Commons", + "category": "data", + "version": "1.0.0", + "fields": [ + { + "name": "mode", + "type": "select", + "value": "potd", + "options": ["potd", "random"], + "required": false, + "label": "Mode", + "hint": "Picture of the Day or Random Image" + }, + { + "name": "submode", + "type": "select", + "value": "day", + "options": ["day", "onthisday", "month", "random"], + "required": false, + "label": "Sub-mode (for POTD)", + "hint": "Type of Picture of the Day selection" + }, + { + "name": "saveAssets", + "type": "select", + "value": "never", + "options": ["auto", "always", "never"], + "label": "Save asset", + "hint": "Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\\n\\nYou can later use the 'Local image' app to view saved assets.\\n\\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved." + } + ], + "output": [ + { + "name": "image", + "type": "image" + } + ], + "cache": { + "enabled": true, + "inputEnabled": true, + "durationEnabled": true, + "duration": "3600" + } +}