@@ -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.
|
||||||
@@ -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("&", "&")
|
||||||
|
decoded = decoded:gsub(""", '"')
|
||||||
|
decoded = decoded:gsub("'", "'")
|
||||||
|
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)
|
||||||
@@ -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 |
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user