@@ -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