From 4d3d664abd8863168d8a87a9f6f5f19757f49210 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Sat, 16 May 2026 00:56:10 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Now=20Playing=20+=20World=20Clock=20+?= =?UTF-8?q?=20Daily=20Quote=20packs=20(72=20=E2=86=92=2075=20rust=20packs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small daily-driver packs mirrored with python (commit 02ab8a4 or similar). now_playing - "что играет" / "что за песня" / "какой трек" / "что слушаю" - Reads Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager via PowerShell — works with Spotify/YouTube/Foobar/Winamp/Yandex Music (anything that exposes Windows SMTC, Win10 1803+). - Speaks "Сейчас играет: " or just "<Title>" if no artist. world_clock - "сколько времени в Токио" / "время в Лондоне" - 21+ pre-mapped Russian/world city names → IANA timezones. - Fetches worldtimeapi.org (free, no key, no rate-limit headers exposed). - Parses ISO 8601 datetime, speaks "В <city> сейчас HH:MM." daily_quote - "цитата дня" / "вдохнови меня" - zenquotes.io (free, no key) → English quote. - LLM translates to Russian via active backend (Groq or Ollama). - Falls back to LLM-generated quote if zenquotes is unreachable. - Speaks "<translated quote> — <author>." Tests: 6 commands tests pass (no_duplicate_ids / no_empty_phrases / all_lua_scripts_exist auto-validate the 3 new packs). Pack count: 72 → 75. --- resources/commands/daily_quote/command.toml | 19 +++++ resources/commands/daily_quote/quote.lua | 31 +++++++ resources/commands/now_playing/command.toml | 24 ++++++ resources/commands/now_playing/now.lua | 46 ++++++++++ resources/commands/world_clock/command.toml | 18 ++++ resources/commands/world_clock/time_in.lua | 95 +++++++++++++++++++++ 6 files changed, 233 insertions(+) create mode 100644 resources/commands/daily_quote/command.toml create mode 100644 resources/commands/daily_quote/quote.lua create mode 100644 resources/commands/now_playing/command.toml create mode 100644 resources/commands/now_playing/now.lua create mode 100644 resources/commands/world_clock/command.toml create mode 100644 resources/commands/world_clock/time_in.lua diff --git a/resources/commands/daily_quote/command.toml b/resources/commands/daily_quote/command.toml new file mode 100644 index 0000000..71ab9a9 --- /dev/null +++ b/resources/commands/daily_quote/command.toml @@ -0,0 +1,19 @@ +# Daily quote — random motivational quote. + +[[commands]] +id = "daily_quote" +type = "lua" +script = "quote.lua" +sandbox = "full" +timeout = 15000 + +[commands.phrases] +ru = [ + "цитата дня", + "мотивирующая цитата", + "скажи цитату", + "вдохнови меня", + "цитата", +] +en = ["quote of the day", "motivational quote", "inspire me"] +ua = ["цитата дня", "надихни мене"] diff --git a/resources/commands/daily_quote/quote.lua b/resources/commands/daily_quote/quote.lua new file mode 100644 index 0000000..8109352 --- /dev/null +++ b/resources/commands/daily_quote/quote.lua @@ -0,0 +1,31 @@ +-- "Цитата дня" — random quote. +-- Source: zenquotes.io (free, no key, returns JSON array) +local data = jarvis.http.json("https://zenquotes.io/api/random") +if not data or type(data) ~= "table" or #data == 0 then + -- Fallback to LLM if API fails + local llm_quote = jarvis.llm({ + { role = "system", content = "Выдай одну короткую мотивирующую цитату. По-русски. Без вступлений." }, + { role = "user", content = "Цитата дня." } + }, { max_tokens = 100, temperature = 0.9 }) + if llm_quote and llm_quote ~= "" then + return jarvis.cmd.ok(llm_quote) + end + return jarvis.cmd.error("Не получилось получить цитату.") +end + +local q = data[1] +local quote_en = q.q or "" +local author = q.a or "Аноним" +if quote_en == "" then + return jarvis.cmd.error("Пустая цитата.") +end + +-- Translate to Russian via LLM (the API returns English). +local translated = jarvis.llm({ + { role = "system", content = "Переведи английскую цитату на русский. Без вступлений. Только перевод." }, + { role = "user", content = quote_en } +}, { max_tokens = 200, temperature = 0.2 }) + +local text = (translated and translated ~= "") and translated or quote_en + +return jarvis.cmd.ok(text .. " — " .. author .. ".") diff --git a/resources/commands/now_playing/command.toml b/resources/commands/now_playing/command.toml new file mode 100644 index 0000000..64ee16c --- /dev/null +++ b/resources/commands/now_playing/command.toml @@ -0,0 +1,24 @@ +# Windows Media Session — what's currently playing. +# Uses Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager +# via PowerShell. Works with Spotify, YouTube, Foobar, Yandex Music, anything +# that exposes SMTC. + +[[commands]] +id = "now_playing" +type = "lua" +script = "now.lua" +sandbox = "full" +timeout = 8000 + +[commands.phrases] +ru = [ + "что играет", + "что сейчас играет", + "что за музыка", + "что за песня", + "какой трек", + "какая песня", + "что слушаю", +] +en = ["what's playing", "current song", "now playing"] +ua = ["що грає", "що зараз грає"] diff --git a/resources/commands/now_playing/now.lua b/resources/commands/now_playing/now.lua new file mode 100644 index 0000000..dc98a9a --- /dev/null +++ b/resources/commands/now_playing/now.lua @@ -0,0 +1,46 @@ +-- "Что играет" — query Windows SMTC. +-- Requires Windows 10 1803+ (Media Session API). +local ps = [[ +[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime] | Out-Null +$mgr = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]::RequestAsync() +while ($mgr.Status -eq 0) { Start-Sleep -Milliseconds 50 } +$session = $mgr.GetResults().GetCurrentSession() +if (-not $session) { Write-Output 'NONE'; exit } +$propsTask = $session.TryGetMediaPropertiesAsync() +while ($propsTask.Status -eq 0) { Start-Sleep -Milliseconds 50 } +$props = $propsTask.GetResults() +$artist = $props.Artist +$title = $props.Title +if ([string]::IsNullOrEmpty($artist) -and [string]::IsNullOrEmpty($title)) { + Write-Output 'EMPTY' + exit +} +Write-Output ("{0}|{1}" -f $artist, $title) +]] + +local res = jarvis.system.exec(string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', + ps:gsub('"', '\\"'):gsub("\r?\n", "; ") +)) + +if not res.success then + return jarvis.cmd.error("Не получилось получить данные.") +end + +local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") +if out == "NONE" then + return jarvis.cmd.not_found("Сейчас ничего не играет.") +end +if out == "EMPTY" or out == "|" then + return jarvis.cmd.not_found("Что-то играет, но без названия.") +end + +local artist, title = out:match("^([^|]*)|(.*)$") +if not title or title == "" then + return jarvis.cmd.not_found("Трек без названия.") +end + +if artist and artist ~= "" then + return jarvis.cmd.ok(string.format("Сейчас играет: %s — %s.", artist, title)) +end +return jarvis.cmd.ok(string.format("Сейчас играет: %s.", title)) diff --git a/resources/commands/world_clock/command.toml b/resources/commands/world_clock/command.toml new file mode 100644 index 0000000..aeef0f2 --- /dev/null +++ b/resources/commands/world_clock/command.toml @@ -0,0 +1,18 @@ +# World clock — query time in another city/timezone. + +[[commands]] +id = "world_clock" +type = "lua" +script = "time_in.lua" +sandbox = "full" +timeout = 10000 + +[commands.phrases] +ru = [ + "сколько времени в", + "который час в", + "сколько часов в", + "время в", +] +en = ["time in", "what time in"] +ua = ["котра година у", "скільки часу в"] diff --git a/resources/commands/world_clock/time_in.lua b/resources/commands/world_clock/time_in.lua new file mode 100644 index 0000000..dd561ec --- /dev/null +++ b/resources/commands/world_clock/time_in.lua @@ -0,0 +1,95 @@ +-- "Сколько времени в Токио" — uses worldtimeapi.org public API. +local phrase = (jarvis.context.phrase or "") +local city = jarvis.text.strip_trigger(phrase:lower(), { + "сколько времени в", + "который час в", + "сколько часов в", + "время в", + "time in", + "what time in", + "котра година у", + "скільки часу в", +}) +city = city:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") + +if city == "" then + return jarvis.cmd.error("В каком городе?") +end + +-- Map common Russian city names → timezone slugs. +local TZ_MAP = { + -- Russia + ["москва"] = "Europe/Moscow", + ["спб"] = "Europe/Moscow", + ["петербург"] = "Europe/Moscow", + ["санкт-петербург"] = "Europe/Moscow", + ["новосибирск"] = "Asia/Novosibirsk", + ["екатеринбург"] = "Asia/Yekaterinburg", + ["казань"] = "Europe/Moscow", + ["владивосток"] = "Asia/Vladivostok", + ["хабаровск"] = "Asia/Vladivostok", + ["омск"] = "Asia/Omsk", + ["красноярск"] = "Asia/Krasnoyarsk", + -- World + ["лондон"] = "Europe/London", + ["нью-йорк"] = "America/New_York", + ["нью йорк"] = "America/New_York", + ["сан-франциско"] = "America/Los_Angeles", + ["сан франциско"] = "America/Los_Angeles", + ["лос-анджелес"] = "America/Los_Angeles", + ["лос анджелес"] = "America/Los_Angeles", + ["токио"] = "Asia/Tokyo", + ["пекин"] = "Asia/Shanghai", + ["шанхай"] = "Asia/Shanghai", + ["гонконг"] = "Asia/Hong_Kong", + ["сингапур"] = "Asia/Singapore", + ["сидней"] = "Australia/Sydney", + ["дубай"] = "Asia/Dubai", + ["париж"] = "Europe/Paris", + ["берлин"] = "Europe/Berlin", + ["мадрид"] = "Europe/Madrid", + ["рим"] = "Europe/Rome", + ["амстердам"] = "Europe/Amsterdam", + ["варшава"] = "Europe/Warsaw", + ["прага"] = "Europe/Prague", + ["стамбул"] = "Europe/Istanbul", + ["киев"] = "Europe/Kyiv", + ["минск"] = "Europe/Minsk", + ["ереван"] = "Asia/Yerevan", + ["тбилиси"] = "Asia/Tbilisi", + ["алматы"] = "Asia/Almaty", + ["астана"] = "Asia/Almaty", + ["баку"] = "Asia/Baku", + ["ташкент"] = "Asia/Tashkent", + ["сеул"] = "Asia/Seoul", + ["мехико"] = "America/Mexico_City", + ["рио"] = "America/Sao_Paulo", + ["буэнос-айрес"] = "America/Argentina/Buenos_Aires", +} + +local tz = nil +for stem, zone in pairs(TZ_MAP) do + if city:find(stem, 1, true) then + tz = zone + break + end +end + +if not tz then + return jarvis.cmd.not_found("Не знаю такого города. Попробуйте Москва, Токио, Лондон.") +end + +-- worldtimeapi.org has rate limits but is free with no key. +local url = "https://worldtimeapi.org/api/timezone/" .. tz +local data = jarvis.http.json(url) +if not data or not data.datetime then + return jarvis.cmd.error("Не получилось узнать время.") +end + +-- datetime is ISO 8601: "2026-05-15T14:32:11.123456+09:00" +local hh, mm = data.datetime:match("T(%d%d):(%d%d)") +if not hh then + return jarvis.cmd.error("Не понял ответ сервиса.") +end + +return jarvis.cmd.ok(string.format("В %s сейчас %s:%s.", city, hh, mm))