diff --git a/ayet-kosesi/README.md b/ayet-kosesi/README.md index eca7b4e1..07896b1c 100644 --- a/ayet-kosesi/README.md +++ b/ayet-kosesi/README.md @@ -1,6 +1,6 @@ # Ayet Köşesi -Ayet Köşesi is a Noctalia desktop widget that displays a daily verse from the public Akıl Kuran API. +Ayet Köşesi is a Noctalia desktop widget powered by Akıl Kuran's public **Günün Ayeti** API. ## Plugin @@ -10,54 +10,14 @@ Ayet Köşesi is a Noctalia desktop widget that displays a daily verse from the ## Requirements -- `curl` must be installed and available on `PATH`; it is used to refresh the Akıl Kuran logo. - -## External dependencies - -The plugin depends on the `curl` command. +- `curl` must be installed and available on `PATH`; it is used only to refresh the Akıl Kuran logo. ## Usage Add the `ayet` desktop widget from Noctalia's desktop widget settings. -The widget displays a daily verse, the selected translation author, and an Akıl Kuran logo. - -Click **Akıl Kuran'da aç** to open the displayed verse on akilkuran.com. - -## Translation selection - -The plugin can use a local translation/meal ID. - -Create: - -`~/.config/ayet-kosesi/meal-id` - -and put the numeric meal ID inside. - -Example: - -`14` - -When no local ID is present, the API default translation is used. - -## Network and storage - -Verse data is requested from the public Akıl Kuran API: - -`https://akilkuran.com/api/quran/surah/` - -The Akıl Kuran logo is refreshed once per day and cached locally. - -When the network is unavailable, the existing cached logo and cached verse remain available. - -The plugin does not access user accounts, browser storage, cookies, Firebase credentials, or private Akıl Kuran source code. - -The plugin does not download or execute remote code. - -## Desktop widget - -Entry ID: +The plugin polls `https://akilkuran.com/api/daily-verse` once per minute. The API is the single source of truth for Akıl Kuran's current automatic/manual Günün Ayeti. -`ayet` +The optional `author` query parameter is used to request the same daily verse in a selected public translation. The plugin first checks `~/.config/ayet-kosesi/meal-id`; if absent, it executes `~/.local/bin/akilkuran-meal-id`. If no local meal is available, it falls back to a language-based default (Turkish 105, English 32). -No panel IPC command or launcher prefix is provided by this plugin. +The plugin stores only its own verse/logo cache under Noctalia's plugin data directory. It does not access Akıl Kuran user accounts, browser storage, cookies, Firebase credentials, or private source code. diff --git a/ayet-kosesi/ayet.luau b/ayet-kosesi/ayet.luau index 33474ac1..ac948cc4 100644 --- a/ayet-kosesi/ayet.luau +++ b/ayet-kosesi/ayet.luau @@ -1,31 +1,25 @@ --!nonstrict -- Ayet Köşesi --- A Noctalia desktop widget that shows one deterministic daily verse --- from the public Akıl Kuran API. +-- A Noctalia desktop widget powered by Akıl Kuran's public +-- /api/daily-verse endpoint. -local API = "https://akilkuran.com/api/quran/surah/" -local AUTHORS_API = "https://akilkuran.com/api/quran/authors" +local DAILY_API = "https://akilkuran.com/api/daily-verse" local LOGO_URL = "https://akilkuran.com/apple-touch-icon.png?v=3" local verse = nil local loading = false -local lastDay = nil +local mealReadInProgress = false +local lastReference = nil local lastMealId = nil local lastLanguage = nil local dataFile = nil - local logoFile = nil local logoStateFile = nil local logoLoading = false local lastLogoDay = nil -local authorsFile = nil -local authors = nil -local authorsLoading = false -local lastAuthorsFetch = 0 - local function todayKey() return os.date("%Y-%j") end @@ -38,26 +32,22 @@ local function homePath() return home end -local function mealIdPath() +local function readConfiguredMealId() local home = homePath() if home == nil then return nil end - return home .. "/.config/ayet-kosesi/meal-id" -end - -local function readMealId() - local path = mealIdPath() - if path == nil then - return nil - end + local path = home .. "/.config/ayet-kosesi/meal-id" local raw = noctalia.readFile(path) + if raw == nil then return nil end - local id = tonumber(tostring(raw):gsub("^%s+", ""):gsub("%s+$", "")) + local cleaned = tostring(raw):gsub("^%s+", ""):gsub("%s+$", "") + local id = tonumber(cleaned) + if id ~= nil and id > 0 then return math.floor(id) end @@ -87,148 +77,133 @@ local function systemLanguage() return "en" end --- Keep sensible defaults for the two languages currently provided by --- Akıl Kuran's public author catalogue. Other languages are resolved --- dynamically from the catalogue when available. local DEFAULT_AUTHOR_BY_LANGUAGE = { tr = 105, en = 32, } -local function findAuthorForLanguage(language) - if authors == nil then - return nil +local function fallbackMealId(language) + return DEFAULT_AUTHOR_BY_LANGUAGE[language] +end + +local function resolveMealId(callback) + local configured = readConfiguredMealId() + if configured ~= nil then + callback(configured) + return end - local preferred = DEFAULT_AUTHOR_BY_LANGUAGE[language] + local home = homePath() + if home == nil then + callback(nil) + return + end - if preferred ~= nil then - for _, author in ipairs(authors) do - if tonumber(author.id) == preferred - and tostring(author.language or ""):lower() == language then - return preferred - end - end + local helper = home .. "/.local/bin/akilkuran-meal-id" + + if mealReadInProgress then + return end - for _, author in ipairs(authors) do - if tostring(author.language or ""):lower() == language then - local id = tonumber(author.id) + mealReadInProgress = true + + local accepted = noctalia.runAsync( + { helper }, + function(result) + mealReadInProgress = false + + local id = nil + + if result ~= nil and result.exitCode == 0 then + local out = tostring(result.stdout or "") + :gsub("^%s+", "") + :gsub("%s+$", "") + local parsed = tonumber(out) - if id ~= nil and id > 0 then - return math.floor(id) + if parsed ~= nil and parsed > 0 then + id = math.floor(parsed) + end end + + callback(id) end - end + ) - return nil + if not accepted then + mealReadInProgress = false + callback(nil) + end end -local function saveAuthorsCache() - if authorsFile == nil or authors == nil then +local function saveVerse(key) + if dataFile == nil or verse == nil then return end local encoded = noctalia.json.encode({ - authors = authors, - fetchedAt = os.time(), + day = key, + mealId = lastMealId, + language = lastLanguage, + reference = lastReference, + verse = verse, }) if encoded ~= nil then - noctalia.writeFile(authorsFile, encoded) + noctalia.writeFile(dataFile, encoded) end end -local function loadAuthorsCache() - if authorsFile == nil then +local function loadCachedVerse() + if dataFile == nil then return end - local raw = noctalia.readFile(authorsFile) - + local raw = noctalia.readFile(dataFile) if raw == nil then return end local decoded = noctalia.json.decode(raw) + if decoded == nil or decoded.verse == nil then + return + end + + verse = decoded.verse + lastMealId = tonumber(decoded.mealId) + lastLanguage = decoded.language + lastReference = decoded.reference - if decoded ~= nil and decoded.authors ~= nil then - authors = decoded.authors - lastAuthorsFetch = tonumber(decoded.fetchedAt) or 0 + if lastReference == nil and verse.url ~= nil then + lastReference = tostring(verse.url):match( + "akilkuran%.com/(%d+/%d+)" + ) end end -local fetchVerse - -local function fetchAuthors() - if authorsLoading then +local function saveLogoState(key) + if logoStateFile == nil then return end - local now = os.time() + local encoded = noctalia.json.encode({ day = key }) + if encoded ~= nil then + noctalia.writeFile(logoStateFile, encoded) + end +end - -- Refresh the author catalogue at most once per day when we have one. - if authors ~= nil - and lastAuthorsFetch > 0 - and now - lastAuthorsFetch < 86400 then +local function loadLogoState() + if logoStateFile == nil then return end - -- Avoid hammering the endpoint when the network is unavailable. - if lastAuthorsFetch > 0 - and now - lastAuthorsFetch < 600 then + local raw = noctalia.readFile(logoStateFile) + if raw == nil then return end - authorsLoading = true - lastAuthorsFetch = now - - local accepted = noctalia.http( - { - url = AUTHORS_API, - method = "GET", - headers = { - "Accept: application/json", - }, - }, - function(response) - authorsLoading = false - - if response.ok - and response.status >= 200 - and response.status < 300 then - - local decoded = noctalia.json.decode(response.body) - - if decoded ~= nil - and decoded.data ~= nil - and #decoded.data > 0 then - - authors = decoded.data - saveAuthorsCache() - - local language = systemLanguage() - local mealId = findAuthorForLanguage(language) - - if mealId ~= lastMealId then - fetchVerse(todayKey(), mealId, language) - end - else - noctalia.log("Ayet Köşesi author verisi çözülemedi.") - end - else - noctalia.log( - "Ayet Köşesi authors HTTP hatası: " - .. tostring(response.status) - ) - end - - render() - end - ) - - if not accepted then - authorsLoading = false + local decoded = noctalia.json.decode(raw) + if decoded ~= nil then + lastLogoDay = decoded.day end end @@ -384,99 +359,16 @@ local function render() ) end -local function saveVerse(key, value) - if dataFile == nil then - return - end - local encoded = noctalia.json.encode({ - day = key, - mealId = lastMealId, - language = lastLanguage, - verse = value, - }) - - if encoded ~= nil then - noctalia.writeFile(dataFile, encoded) - end -end - -local function saveLogoState(key) - if logoStateFile == nil then - return - end - - local encoded = noctalia.json.encode({ day = key }) - - if encoded ~= nil then - noctalia.writeFile(logoStateFile, encoded) - end -end - -local function useResponse(key, body, mealId, language) - local decoded, err = noctalia.json.decode(body) - - if decoded == nil then - noctalia.log("Ayet Köşesi JSON çözülemedi: " .. tostring(err)) - return false - end - - local data = decoded.data - - if data == nil or data.verses == nil or #data.verses == 0 then - noctalia.log("Ayet Köşesi verse verisi bulunamadı.") - return false - end - - local verses = data.verses - local y = tonumber(os.date("%Y")) or 0 - local d = tonumber(os.date("%j")) or 1 - local seed = y * 1000 + d - local index = (seed % #verses) + 1 - local item = verses[index] - - if item == nil or item.translation == nil then - noctalia.log("Ayet Köşesi çeviri verisi bulunamadı.") - return false - end - - local surahId = item.surah_id or data.id - local verseNumber = item.verse_number or 0 - - verse = { - surah = data.name_translation_tr or data.name or "?", - number = verseNumber, - text = item.translation.text or "", - author = (item.translation.author and item.translation.author.name) or "", - url = "https://akilkuran.com/" - .. tostring(surahId) - .. "/" - .. tostring(verseNumber), - } - - lastDay = key - lastMealId = mealId - lastLanguage = language - - saveVerse(key, verse) - return true -end - -fetchVerse = function(key, mealId, language) +local function fetchDailyVerse(key, mealId, language) if loading then return end loading = true - language = language or systemLanguage() render() - local year = tonumber(os.date("%Y")) or 0 - local day = tonumber(os.date("%j")) or 1 - local seed = year * 1000 + day - local surah = (seed % 114) + 1 - - local url = API .. tostring(surah) + local url = DAILY_API if mealId ~= nil and mealId > 0 then url = url .. "?author=" .. tostring(mealId) @@ -493,34 +385,83 @@ fetchVerse = function(key, mealId, language) function(response) loading = false - if response.ok - and response.status >= 200 - and response.status < 300 then - - if not useResponse( - key, - response.body, - mealId, - language - ) then - noctalia.log( - "Ayet Köşesi yanıtı kullanılamadı; cache korunuyor." - ) - end - else + if not response.ok + or response.status < 200 + or response.status >= 300 then noctalia.log( - "Akıl Kuran HTTP hatası: " + "Ayet Köşesi Günün Ayeti API hatası: " .. tostring(response.status) ) + render() + return + end + + local decoded, err = noctalia.json.decode(response.body) + if decoded == nil then + noctalia.log( + "Ayet Köşesi API JSON çözülemedi: " .. tostring(err) + ) + render() + return end + local data = decoded.data + if data == nil + or data.surahId == nil + or data.verseNum == nil + or data.translation == nil then + noctalia.log("Ayet Köşesi API beklenen veriyi döndürmedi.") + render() + return + end + + local surahId = tonumber(data.surahId) + local verseNumber = tonumber(data.verseNum) + if surahId == nil or verseNumber == nil + or surahId < 1 or verseNumber < 1 then + noctalia.log("Ayet Köşesi API ayet referansı geçersiz.") + render() + return + end + + local translation = data.translation + local author = translation.author or {} + local reference = tostring(data.ref or ( + tostring(surahId) .. ":" .. tostring(verseNumber) + )) + + verse = { + surah = tostring(data.surahName or "?"), + number = verseNumber, + text = tostring(translation.text or ""), + author = tostring(translation.authorName or author.name or ""), + url = tostring(data.url or ( + "https://akilkuran.com/" + .. tostring(surahId) + .. "/" + .. tostring(verseNumber) + )), + } + + lastReference = reference + lastMealId = tonumber(translation.authorId) + or tonumber(author.id) + or mealId + lastLanguage = tostring( + translation.language + or author.language + or language + or "" + ) + + saveVerse(key) render() end ) if not accepted then loading = false - noctalia.log("Ayet Köşesi HTTP isteği başlatılamadı.") + noctalia.log("Ayet Köşesi Günün Ayeti isteği başlatılamadı.") render() end end @@ -550,11 +491,10 @@ local function refreshLogo(key) if result ~= nil and result.exitCode == 0 then lastLogoDay = key saveLogoState(key) - noctalia.log("Ayet Köşesi logosu güncellendi.") render() else noctalia.log( - "Ayet Köşesi logosu güncellenemedi; mevcut cache korunuyor." + "Ayet Köşesi logosu güncellenemedi; cache korunuyor." ) end end @@ -584,95 +524,33 @@ function update() if dataFile == nil then local dir = noctalia.pluginDataDir() - if dir ~= nil then - dataFile = dir .. "/verse.json" - logoFile = dir .. "/akilkuran-logo.png" - logoStateFile = dir .. "/logo.json" - authorsFile = dir .. "/authors.json" - - local raw = noctalia.readFile(dataFile) - - if raw ~= nil then - local decoded = noctalia.json.decode(raw) - - if decoded ~= nil and decoded.verse ~= nil then - verse = decoded.verse - lastDay = decoded.day - lastMealId = tonumber(decoded.mealId) - lastLanguage = decoded.language - end - end - - loadAuthorsCache() - - local logoRaw = noctalia.readFile(logoStateFile) - - if logoRaw ~= nil then - local logoDecoded = noctalia.json.decode(logoRaw) - - if logoDecoded ~= nil then - lastLogoDay = logoDecoded.day - end - end - end - end - - local newDay = lastDay ~= key - local language = systemLanguage() - local explicitMealId = readMealId() - local logoNeedsRefresh = lastLogoDay ~= key - - if logoNeedsRefresh then - refreshLogo(key) - end - - -- A local meal-id file is an explicit user choice and wins over - -- automatic language detection. - if explicitMealId ~= nil then - local mealChanged = explicitMealId ~= lastMealId - local languageChanged = language ~= lastLanguage - - if verse == nil - or newDay - or mealChanged - or languageChanged then - fetchVerse(key, explicitMealId, language) + if dir == nil then + render() return end - render() - return - end + dataFile = dir .. "/verse.json" + logoFile = dir .. "/akilkuran-logo.png" + logoStateFile = dir .. "/logo.json" - -- No explicit meal: resolve a translation from the system language. - if authors == nil then - loadAuthorsCache() + loadCachedVerse() + loadLogoState() end - if authors == nil then - fetchAuthors() - - if verse == nil or newDay then - render() - end - - return + if lastLogoDay ~= key then + refreshLogo(key) end - local mealId = findAuthorForLanguage(language) - - local mealChanged = mealId ~= lastMealId - local languageChanged = language ~= lastLanguage + local language = systemLanguage() + resolveMealId(function(explicitMealId) + local currentKey = todayKey() + local mealId = explicitMealId or fallbackMealId(language) - if verse == nil - or newDay - or mealChanged - or languageChanged then - fetchVerse(key, mealId, language) - return - end + -- Poll the public API every minute. The API itself follows Akıl Kuran's + -- real automatic/manual Günün Ayeti logic and handles its own caching. + fetchDailyVerse(currentKey, mealId, language) + end) - fetchAuthors() render() end diff --git a/ayet-kosesi/plugin.toml b/ayet-kosesi/plugin.toml index c91294eb..18e47683 100644 --- a/ayet-kosesi/plugin.toml +++ b/ayet-kosesi/plugin.toml @@ -1,13 +1,13 @@ id = "alitura1/ayet-kosesi" name = "Ayet Köşesi" -version = "1.0.0" +version = "1.4.0" plugin_api = 24 author = "alitura1" icon = "book" license = "MIT" dependencies = ["curl"] tags = ["desktop", "time", "utility"] -description = "Akıl Kuran'dan günlük ayeti masaüstünde gösteren Noctalia eklentisi." +description = "Akıl Kuran'ın public Günün Ayeti API'sinden günlük ayeti ve seçili meali masaüstünde gösteren Noctalia eklentisi." [[desktop_widget]] id = "ayet" diff --git a/ayet-kosesi/thumbnail.webp b/ayet-kosesi/thumbnail.webp index 4b43d849..28bdb846 100644 Binary files a/ayet-kosesi/thumbnail.webp and b/ayet-kosesi/thumbnail.webp differ