daily-wallpaper (#8)

Co-authored-by: nzlov <me@nzlov.com>
This commit is contained in:
nzlov
2026-07-14 22:38:34 -04:00
committed by GitHub
co-authored by nzlov
parent 8c379ea2a2
commit 6e7764043d
5 changed files with 328 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Daily Wallpaper
Fetches a daily wallpaper from Bing or NASA and applies it through the Noctalia 5 wallpaper API.
The service checks on startup and then every 10 minutes. It downloads at most one image per source and Bing locale per day, stores its files in a dedicated `daily-wallpaper` directory, and removes cached images older than 5 days. Repeated failures are logged, but error notifications are limited to once per day.
Settings:
- Source: Bing or NASA.
- Locale: Bing market locale such as `en-US`, `de-DE`, or `fr-FR`. NASA ignores this setting.
+278
View File
@@ -0,0 +1,278 @@
--!nonstrict
-- Daily Wallpaper service for Noctalia 5.
-- Fetches one Bing or NASA image per day, caches it locally, and applies it via
-- the native Noctalia wallpaper API.
local CHECK_INTERVAL_MS = 10 * 60 * 1000
local RETENTION_SECONDS = 5 * 24 * 60 * 60
local PLUGIN_CACHE_DIR = "daily-wallpaper"
local checking = false
local lastAppliedPath = ""
local lastCheckedKey = ""
local lastErrorNotificationDate = ""
noctalia.setUpdateInterval(CHECK_INTERVAL_MS)
local function todayString()
return os.date("%Y-%m-%d")
end
local function normalizeLocale(value)
if type(value) ~= "string" then
return "en-us"
end
local locale = noctalia.string.trim(value):lower():gsub("_", "-")
if locale == "" or locale:find("[^%w%-]") then
return "en-us"
end
return locale
end
local function source()
local configured = noctalia.getConfig("source")
if configured == "nasa" then
return "nasa"
end
return "bing"
end
local function downloadDir()
return noctalia.expandPath(`{noctalia.wallpaperDirectory()}/{PLUGIN_CACHE_DIR}`)
end
local function cachePath(prefix, date)
return `{downloadDir()}/{prefix}-{date}.jpg`
end
local function decodeHtml(value)
local decoded = value:gsub("&amp;", "&")
decoded = decoded:gsub("&quot;", '"')
decoded = decoded:gsub("&#39;", "'")
return decoded
end
local function absoluteNasaUrl(url)
if url == nil or url == "" then
return ""
end
url = decodeHtml(url)
if url:match("^https?://") then
return url
end
if url:match("^//") then
return `https:{url}`
end
if url:sub(1, 1) == "/" then
return `https://www.nasa.gov{url}`
end
return `https://www.nasa.gov/{url}`
end
local function applyWallpaper(path, downloaded)
noctalia.setWallpaper(path)
lastAppliedPath = path
lastCheckedKey = `{source()}:{todayString()}`
noctalia.state.set("status", {
state = "applied",
path = path,
downloaded = downloaded,
})
noctalia.log(`Daily Wallpaper applied: {path}`)
end
local function cleanupOldWallpapers(dir)
local entries = noctalia.listDir(dir)
if type(entries) ~= "table" then
return
end
local cutoff = os.time() - RETENTION_SECONDS
for _, name in ipairs(entries) do
if name:match("^bing%-.+%-%d%d%d%d%-%d%d%-%d%d%.jpg$") or name:match("^nasa%-%d%d%d%d%-%d%d%-%d%d%.jpg$") then
local path = `{dir}/{name}`
local info = noctalia.fileInfo(path)
if type(info) == "table" and type(info.mtime) == "number" and info.mtime < cutoff then
noctalia.removeFile(path)
end
end
end
end
local function fail(message)
checking = false
noctalia.state.set("status", {
state = "error",
message = message,
})
local date = todayString()
if lastErrorNotificationDate ~= date then
lastErrorNotificationDate = date
noctalia.notifyError(noctalia.tr("notify.failed"), message)
end
noctalia.log(`Daily Wallpaper failed: {message}`)
end
local function downloadAndApply(primaryUrl, fallbackUrl, dest)
local function tryDownload(url, fallback)
if type(url) ~= "string" or url == "" then
if fallback ~= "" then
tryDownload(fallback, "")
else
fail("No wallpaper URL found")
end
return
end
local started = noctalia.download(url, dest, function(ok)
if ok then
checking = false
cleanupOldWallpapers(downloadDir())
applyWallpaper(dest, true)
noctalia.notify(noctalia.tr("notify.applied"), dest)
return
end
if fallback ~= "" then
tryDownload(fallback, "")
else
fail("Download failed")
end
end)
if not started then
fail("Could not start wallpaper download")
end
end
tryDownload(primaryUrl, fallbackUrl or "")
end
local function resolveBing(locale, done)
local url = `https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt={noctalia.string.urlEncode(locale)}`
local started = noctalia.http({ url = url }, function(res)
if not res.ok or res.status < 200 or res.status >= 300 then
fail(`Bing request failed: HTTP {res.status}`)
return
end
local parsed, err = noctalia.json.decode(res.body)
if parsed == nil then
fail(`Bing response parse failed: {err or "invalid JSON"}`)
return
end
local first = parsed.images and parsed.images[1]
local urlBase = first and first.urlbase
if type(urlBase) ~= "string" or urlBase == "" then
fail("Bing response did not include an image URL")
return
end
done(`bing-{locale}`, `https://www.bing.com{urlBase}_UHD.jpg`, `https://www.bing.com{urlBase}_1920x1080.jpg`)
end)
if not started then
fail("Could not start Bing request")
end
end
local function resolveNasa(done)
local started = noctalia.http({ url = "https://www.nasa.gov/image-of-the-day/" }, function(res)
if not res.ok or res.status < 200 or res.status >= 300 then
fail(`NASA request failed: HTTP {res.status}`)
return
end
local page = res.body
local primary = page:match('class="[^"]-hds%-gallery%-image[^"]-"[^>]->.-<img[^>]-src="([^"]+)"')
or page:match('<img[^>]-class="[^"]-hds%-gallery%-image[^"]-"[^>]-src="([^"]+)"')
local fallback = page:match('<meta%s+property="og:image"%s+content="([^"]+)"')
or page:match('<meta%s+name="twitter:image"%s+content="([^"]+)"')
primary = absoluteNasaUrl(primary or "")
fallback = absoluteNasaUrl(fallback or "")
if primary == "" and fallback == "" then
fail("NASA page did not include an image URL")
return
end
done("nasa", primary, fallback)
end)
if not started then
fail("Could not start NASA request")
end
end
local function fetchAndApply(force)
if checking then
return
end
local selectedSource = source()
local date = todayString()
local locale = normalizeLocale(noctalia.getConfig("locale"))
local key = `{selectedSource}:{date}`
if not force and key == lastCheckedKey then
return
end
local cachePrefix = selectedSource == "bing" and `bing-{locale}` or selectedSource
local dest = cachePath(cachePrefix, date)
if not force and dest == lastAppliedPath then
lastCheckedKey = key
return
end
local dir = downloadDir()
local ok, err = noctalia.mkdirAll(dir)
if not ok then
fail(`Could not create download directory: {err or dir}`)
return
end
checking = true
noctalia.state.set("status", {
state = "checking",
source = selectedSource,
})
if not force and noctalia.fileExists(dest) then
checking = false
cleanupOldWallpapers(dir)
applyWallpaper(dest, false)
return
end
local function onResolved(prefix, primaryUrl, fallbackUrl)
local resolvedDest = cachePath(prefix, date)
downloadAndApply(primaryUrl, fallbackUrl, resolvedDest)
end
if selectedSource == "nasa" then
resolveNasa(onResolved)
else
resolveBing(locale, onResolved)
end
end
function update()
fetchAndApply(false)
end
function onConfigChanged()
lastCheckedKey = ""
fetchAndApply(true)
end
function onIpc(event, _payload)
if event == "refresh" then
lastCheckedKey = ""
fetchAndApply(true)
end
end
fetchAndApply(false)
+31
View File
@@ -0,0 +1,31 @@
id = "nzlov/daily-wallpaper"
name = "Daily Wallpaper"
version = "1.0.0"
min_noctalia = "5.0.0"
author = "nzlov"
license = "MIT"
dependencies = []
tags = ["wallpaper", "desktop"]
icon = "photo"
description = "Fetches a new daily wallpaper from Bing or NASA."
[[setting]]
key = "source"
type = "select"
label_key = "settings.source.label"
default = "bing"
options = [
{ value = "bing", label_key = "settings.source.options.bing" },
{ value = "nasa", label_key = "settings.source.options.nasa" },
]
[[setting]]
key = "locale"
type = "string"
label_key = "settings.locale.label"
description_key = "settings.locale.description"
default = ""
[[service]]
id = "service"
entry = "daily_wallpaper.luau"
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+9
View File
@@ -0,0 +1,9 @@
{
"notify.applied": "Wallpaper applied",
"notify.failed": "Daily wallpaper failed",
"settings.locale.description": "Bing market locale such as en-US, de-DE, or fr-FR. Empty uses en-US.",
"settings.locale.label": "Locale",
"settings.source.label": "Source",
"settings.source.options.bing": "Bing",
"settings.source.options.nasa": "NASA"
}