diff --git a/daily-wallpaper/README.md b/daily-wallpaper/README.md new file mode 100644 index 0000000..fbbe67f --- /dev/null +++ b/daily-wallpaper/README.md @@ -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. diff --git a/daily-wallpaper/daily_wallpaper.luau b/daily-wallpaper/daily_wallpaper.luau new file mode 100644 index 0000000..dce80ed --- /dev/null +++ b/daily-wallpaper/daily_wallpaper.luau @@ -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[^"]-"[^>]->.-]-src="([^"]+)"') + or page:match(']-class="[^"]-hds%-gallery%-image[^"]-"[^>]-src="([^"]+)"') + local fallback = page:match('