feat: Now Playing + World Clock + Daily Quote packs (72 → 75 rust packs)
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 "Сейчас играет: <Artist> — <Title>" 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.
This commit is contained in:
parent
4efe306b3a
commit
4d3d664abd
6 changed files with 233 additions and 0 deletions
19
resources/commands/daily_quote/command.toml
Normal file
19
resources/commands/daily_quote/command.toml
Normal file
|
|
@ -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 = ["цитата дня", "надихни мене"]
|
||||||
31
resources/commands/daily_quote/quote.lua
Normal file
31
resources/commands/daily_quote/quote.lua
Normal file
|
|
@ -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 .. ".")
|
||||||
24
resources/commands/now_playing/command.toml
Normal file
24
resources/commands/now_playing/command.toml
Normal file
|
|
@ -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 = ["що грає", "що зараз грає"]
|
||||||
46
resources/commands/now_playing/now.lua
Normal file
46
resources/commands/now_playing/now.lua
Normal file
|
|
@ -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))
|
||||||
18
resources/commands/world_clock/command.toml
Normal file
18
resources/commands/world_clock/command.toml
Normal file
|
|
@ -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 = ["котра година у", "скільки часу в"]
|
||||||
95
resources/commands/world_clock/time_in.lua
Normal file
95
resources/commands/world_clock/time_in.lua
Normal file
|
|
@ -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))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue