Lõbusad käsud Linuxis
Autor
Aleksandra Sepp, AK21
10.november 2017
Täiendanud: Edmund Laugasson, 05.11.2025
Sissejuhatus
Kas teadsite, et Linux ei olegi ainult administraatorite ja professionaalide jaoks mõeldud? Te võite mängida sõnamänge, rääkida oma Linuxi terminaliga või hoopis luua ilusaid ASCII pilte ja palju muud. Seda kõike saab ise katsetada, teades õigeid käske. Antud artiklis tutvume mõnede käskudega, mis teevad tuju heaks ning aitavad saada positiivsele lainele peale pingelist tööpäeva.
Kui teil ei ole tarkvara paigaldamiseks vajalikku superkasutaja õigustes Linuxiga arvutit käepärast, siis on VirtualBoxi virtuaalmasin võimalik alla laadida ja importida siit aadressilt.
Ennem uue tarkvara paigaldamist tuleb uuendada varamu pakettide nimekirja käsuga:
sudo apt-get update
Nüüd võime alustada!
Lõbusate käskude tuntuimad näited
Cowsay
"Cowsay" on ilmselt kõige tuntuim lõbus käsk Linux'is. Ta tagastab kasutaja tekstisisendit lehma või mõnel muul kasutaja poolt valitud looma kujul. Paigaldame cowsay järgmise käsuga:
sudo apt-get install cowsay
Tulemus:
Selleks, et näha kogu loomade nimekirja, kasutame käsu:
cowsay -l
Tulemus:
Valime meelepärast looma - näiteks koalat, ja soovime, et ta ütleks, et eukalüptid on toredad. Kasutame selleks järgmist lauset:
cowsay -f koala Eucalypts are cool!
Tulemus:
Laiendamisvõimalus:
kausta /usr/share/cowsay/cows/ saab salvestada faile lisaks, näiteks aadressilt https://github.com/paulkaefer/cowsay-files
Graafiline versioon xcowsay
Paigaldamine
sudo apt install xcowsay
Käivitamine käsureal:
xcowsay Linux on vahva!
Tulemus
CMatrix
CMatrix võimaldab teil oma terminalis näha maatriksfilmi sarnaselt ekraanisäästjatele. Paigaldamine teostatakse käsuga:
sudo apt-get install cmatrix
Paneme käima käsuga:
cmatrix
Tulemus:
hollywood
Hollywoodi filmide stiilis häkkerikonsool. Tasub ennem käivitamist terminaliaken suuremaks teha, siis mahub rohkem asju kaadrisse.
Paigaldamine
sudo apt install hollywood
Käivitamine
hollywood
Väljumiseks CTRL+C, vajadusel vajutada korduvalt.
Tulemus
bb
Rikkaliku väljundiga ASCII demo. Võimalus ka heli taustaks lubada.
Paigaldamiseks
sudo apt install bb
Käivitamine
bb
Erinevad tulemused:
oneko
Väike kass, mis käsurealt käivitamisel järgneb hiirekursorile. Liigutada hiirekursorit. Kui paigale jätta, siis kass liigub kursorini ja jääb siis ka istuma ning ootele, millal taas hiirekursorit liigutatakse.
Paigaldamine
sudo apt install oneko
Käivitamine
oneko
Väljumiseks: CTRL+C
Tulemus
x11-apps
Graafikaserveriga X.org kaasatulevad rakendused.
Lisateave x11-apps kohta
apt show x11-apps
Description: X applications
This package provides a miscellaneous assortment of X applications
that ship with the X Window System, including:
- atobm, bitmap, and bmtoa, tools for manipulating bitmap images;
- ico, a demo program animating polyhedrons;
- oclock and xclock, graphical clocks;
- rendercheck, a program to test render extension implementations;
- transset, a tool to set opacity property on a window;
- xbiff, a tool which tells you when you have new email;
- xcalc, a scientific calculator desktop accessory;
- xclipboard, a tool to manage cut-and-pasted text selections;
- xconsole, which monitors system console messages;
- xcursorgen, a tool for creating X cursor files from PNGs;
- xditview, a viewer for ditroff output;
- xedit, a simple text editor for X;
- xeyes, a demo program in which a pair of eyes track the pointer;
- xgc, a graphics demo;
- xload, a monitor for the system load average;
- xlogo, a demo program that displays the X logo;
- xmag, which magnifies parts of the X screen;
- xman, a manual page browser;
- xmore, a text pager;
- xwd, a utility for taking window dumps ("screenshots") of the X session;
- xwud, a viewer for window dumps created by xwd;
- Xmark, x11perf, and x11perfcomp, tools for benchmarking graphical
operations under the X Window System;
.
The xbiff, xcalc, xconsole, xedit and xman programs use bitmap images
provided by the xbitmaps package.
Siit nimekirjast võib testida erinevaid rakendusi. Allpool on paar näidet.
xeyes
Graafilised silmad, mis jälgivad hiirekursorit, kui seda liigutada.
Paigaldamine vajadusel, tihti on juba paigaldatud kuna graafikaserver X.org on paigaldatud.
sudo apt install x11-apps
Käivitamine
xeyes
Väljumiseks: CTRL+C
Tulemus
xgc
Graafika demo.
Käivitamine terminalis
xgc
Tulemus
Fortune
Fortune käsu abil saate lugeda oma terminalis juhuslikke tsitaate ja naljakaid ennustusi. Paigaldame enda süsteemi fortune paketi:
sudo apt-get install fortune
Käivitame käsuga:
fortune -s
Tulemus:
Figlet
Figlet'it kasutatakse suurte bännerite tegemiseks. Kõigepealt paigaldame Figlet'it käsuga:
sudo apt-get install figlet
Käivitame käsu abil:
figlet Ta-daa!
Tulemus:
TOIlet
Sarnane käsk figlet käsule on TOIlet
sudo apt install toilet
Käivitamine:
toilet ta-daa
Tulemus
Lolcat
Lolcati kasutatakse Linuxi terminalis peamiselt teksti vikerkaare värvidega värvimiseks. Paigaldame käsuga:
sudo apt-get install lolcat
Kõigepealt vaatame olemasolevaid valikuid kasutamiseks, abi info kuvamiseks kasutame järgmist käsku:
lolcat -h
Tulemus:
ls
ls on klassikaline UNIX'i naljamäng. Iga kord, kui kasutaja kogemata sisestab "ls" asemel "sl" (Steam Locomotive), ilmub ekraanile vedur. Seda muidugi, kui sl on enne seda paigaldatud:
sudo apt-get install sl
sl
Tulemus:
Lendamine
sl -F
parrot.live
https://github.com/hugomd/parrot.live https://github.com/hugomd/ascii-live
curl parrot.live
curl -v parrot.live
Veel võimalusi
curl ascii.live/list
{"frames":["forrest","parrot","clock","nyan","rick"]} #saadaolevad käsud
Saadaolevate käskude sisestamine
curl ascii.live/forrest curl ascii.live/parrot curl ascii.live/clock curl ascii.live/nyan curl ascii.live/rick
Lõke käsureal
Paigaldamine
sudo apt install libaa-bin
Erinevad rakendused:
dpkg -L libaa-bin | grep /usr/bin/
- aafire – ASCII „kamin“.
- aainfo – näitab, milliseid juhtprogramme (driver) ja parameetreid aalib kasutab.
- aasavefont – salvestab fondi faili.
- aatest – testib aalib’i võimekust
Kasutamine
aafire
Tulemus
Värviline
Paigaldamine
sudo apt install caca-utils
Paigaldatud rakendused:
dpkg -L caca-utils | grep /usr/bin/
- cacademo - matrix-stiilis ekraanisäästja
- cacafire - värviline lõke
- cacaplay - esitab libcaca faile, vt man cacaplay
- cacaserver - telnet server libcaca jaoks, vt man cacaserver
- cacaview - ASCII piltide lehitseja, vt man cacaview
- img2txt - pildi teisendamine erinevateks tekstipõhisteks failideks, vt man img2txt
- cacaclock (leidub uutes Debian harudes), näitab ASCII-kella, väljumiseks q, vormingut saab määrata (strftime):
cacaclock -d '%H.%M.%S'
käivitamine:
cacafire
Väljumiseks CTRL+C
RIG
RIG - Random Identity Generator
Juhusliku väljamõeldud identiteedi loomine, vajadusel ka mitu korraga jt võimalused.
Paigaldamine
sudo apt install rig
Käivitamine
rig
Abiteave, väljumine: q
man rig
Tulemus
Doug Peterson 161 Main St Ames, IA 50010 (515) xxx-xxxx
nyancat
Paigaldamiseks
sudo apt install nyancat
Käivitamine
nyancat
Väljumiseks CTRL+C
Tulemus
matemaatika
pii arvutamine
Matemaatikas kasutatakse konstanti pii, mille komakohtade arv on tavaliselt kasutusel kaks kohta 3,14; kuid võimalik ka rohkem arvutada.
Paigaldamine
sudo apt install pi
Käivitamine (50 kohta peale koma) ja tulemus:
pi 50 3.1415926535897932384626433832795028841971693993751
algarvud
Terminalis
factor 100
vastus:
100: 2 2 5 5
tähendabki, et 100 = 2×2×5×5 (kõik on algarvud).
Kui sisestad algarvu, prindib factor lihtsalt arvu iseenda tegurina, nt:
factor 97 97: 97
Mugav nipp: eksponentidena kuvamiseks kasuta -h (--exponents):
factor -h 3000 3000: 2^3 3 5^3
Taustaks: GNU factor testib algarvulisust Baillie–PSW heuristikaga ja faktoreerib seejärel; väljundis on alati algarvud kasvavas järjekorras.
Kui tahad ainult “on algarv?” kontrolli, siis lihtne kontroll on: kas väljundis on pärast koolonit ainult sama arv? Näiteks bash’is:
n=97; out=$(factor "$n"); [ "${out#*: }" = "$n" ] && echo "algarv" || echo "pole algarv"
Lisalugemist
- https://en.wikipedia.org/wiki/Factor_(Unix)
- https://www.gnu.org/software/coreutils/manual/html_node/factor-invocation.html#factor-invocation
eSpeak-NG
Tekst->kõne süntees (TTS - Text-to-Speech).
Paigaldamine (ng - new generation)
sudo apt install espeak-ng
Kasutamise abiteave
man espeak-ng
Käivitamine (helivaljust tasub tõsta, et oleks kuulda):
espeak-ng -v et "Tere! Kuidas läheb?" espeak-ng -v et --stdout "Tere! Kuidas läheb?" | aplay
et on eesti keele hääldus. Tulemuseks on kuulda nimetatud lause.
Failist teksti etteandmine:
espeak-ng -v et -f tekst.txt # või, kui failis on SSML: espeak-ng -v et -m -f tekst.ssml
„-f“ loeb failist; „-m“ lubab SSML/XML parsimise (SSML - Speech Synthesis Markup Language, XML - Extensible Markup Language).
Variant on ka kasutada mbrola teeki, mis pisut pehmem, eesti meeshääl on mbrola-ee1
sudo apt install mbrola mbrola-ee1
Kasutamine:
espeak-ng -v mb-ee1 "Tere! Kuidas läheb?"
Kiiruse muutmine (-s, words-per-minute):
espeak-ng -v mb-ee1 -s 140 "Tere! Kuidas läheb?"
- -s 140 aeglasem
- -s 220 kiirem
Sõnadevaheline paus (-g, word gap)
espeak-ng -v mb-ee1 -s 140 -g 5 "Tere! Kuidas läheb?"
- -g 5 "Paus 50 ms iga sõna vahel."
- -g 12 "Paus ~120 ms iga sõna vahel."
Hääletooni muutmine (-p pitch, 0..99, vaikimisi 50)
espeak-ng -v mb-ee1 -s 140 -g 5 -p 77 "Tere! Kuidas läheb?" espeak-ng -v mb-ee1 -s 140 -g 1 -p 77 -f tekst.txt
Kui vaja tooni muuta lõikude kaupa, kasuta SSML'i <prosody pitch="…"> ja lippu -m:
espeak-ng -v mb-ee1 -m -f tekst.ssml
SSML'is (Speech Synthesis Markup Language) toetatakse pitch väärtust (nt high, +20%, -10%).
Soovi korral saab ka salvestada helifaili:
espeak-ng -v et -w out.wav "Tere! Kuidas läheb?" espeak-ng -v et --stdout "Tere! Kuidas läheb?" > out.wav
--stdout annab WAV-päisega voo
-w tekitab WAV‑faili (16-bit, 22050 Hz). Kui vajad MP3, konverteeri näiteks FFmpeg'iga:
ffmpeg -i out.wav out.mp3
Kuulamiseks:
aplay out.wav Playing WAVE 'väljund.wav' : Signed 16 bit Little Endian, Rate 22050 Hz, Mono
Stereo versioon FFmpeg'i abil:
espeak-ng -v et --stdout "Tere! Kuidas läheb?" | ffmpeg -f wav -i - -ac 2 out_stereo.wav espeak-ng -v et --stdout "Tere! Kuidas läheb?" | ffmpeg -f wav -i - -ac 2 -c:a libmp3lame out.mp3 espeak-ng -v et --stdout "Tere! Kuidas läheb?" | ffmpeg -f wav -i - -ac 2 -c:a libvorbis -qscale:a 5 out.ogg espeak-ng -v et --stdout "Tere! Kuidas läheb?" | ffmpeg -f wav -i - -ac 2 -c:a flac -compression_level 5 out.flac espeak-ng -v et --stdout "Tere! Kuidas läheb?" | ffmpeg -f wav -i - -ac 2 -c:a libopus -b:a 96k -vbr on -application audio out.opus
- -ac 2 dubleerib kanali (et oleks stereo); saad kohe MP3/OGG/FLAC/Opus, vms.
- -qscale:a 5 ~160 kb/s kvaliteedipõhine režiim (Vorbis’e tavaline praktika)
- FLAC on kadudeta; tihendusaste mõjutab vaid faili suurust/kiirust, mitte kvaliteeti
- libopus on eelistatud; 64–128 kb/s on tüüpiline vahemik, 96 kb/s on hea vaikimisi üldheli jaoks
või ka SoX'i abil:
espeak-ng -v et --stdout "Tere! Kuidas läheb?" | sox -t wav - -c 2 out_stereo.wav
Millal kumb?
- FFmpeg – kui tahad ühest torust teha kõik (kanalite arv, kodeerimine MP3/OGG/FLAC, normaliseerimine, pan/route, jpm). Aktiivselt arendatud ja tohutu filterpank. Sobib hästi eSpeak-NG → MP3/OGG.
- SoX – väga lihtne lausekuju (syntax) kiireteks, väikesteks töötlusteks (kanalite dubleerimine, “remix”, miksimine). Hea käsurea “šveitsi nuga”, kuid projekt ise on praktiliselt soiku jäänud. NB! Mitmed projektid on SoX'ist loobunud just hoolduse, platvormitõrgete tõttu.
Kuulamine
aplay out_stereo.wav Playing WAVE 'out_stereo.wav' : Signed 16 bit Little Endian, Rate 22050 Hz, Stereo
Paremat tulemust annab väga võimekas käsurea meediamängija mpv (olemas ka Androidile):
mpv out_stereo.mp3 (+) Audio --aid=1 (mp3 2ch 22050Hz) AO: [pipewire] 22050Hz stereo 2ch floatp Exiting... (End of file)
mpv out_stereo.wav (+) Audio --aid=1 (pcm_s16le 2ch 22050Hz) AO: [pipewire] 22050Hz stereo 2ch s16 Exiting... (End of file)
mpv out.opus (+) Audio --aid=1 (opus 2ch 48000Hz) AO: [pipewire] 48000Hz stereo 2ch floatp Exiting... (End of file)
Graafilisi kasutajaliideseid espeak'ile
- Gespeaker, allalaadimine
- Espeak-QtGui
- Speech Note (kaasaegsem)
Paigaldamine (üksjagu mahukad failid):
flatpak install flathub net.mkiol.SpeechNote
ID Branch Op Remote Download
1. [✓] org.freedesktop.Platform.GL.default 24.08 u flathub 49,7 MB / 145,4 MB
2. [✓] org.freedesktop.Platform.GL.default 24.08extra u flathub 23,3 MB / 145,4 MB
3. [✓] org.freedesktop.Platform.VAAPI.Intel 24.08 u flathub 13,1 MB / 15,0 MB
4. [✓] org.kde.Platform.Locale 5.15-24.08 u flathub 18,6 kB / 409,6 MB
5. [✓] org.kde.Platform 5.15-24.08 u flathub 5,7 MB / 379,4 MB
6. [—] net.mkiol.SpeechNote stable i flathub 489,2 MB / 1,2 GB
Käivitamine
flatpak run net.mkiol.SpeechNote
Siin vajalik keelte tugi paigaldada, näiteks:
- Speech to Text: Eesti (WhisperCpp/et)
- Text to Speech: Eesti (Coqui CV VITS Female/et)
Veebipõhine ja naturaalse eesti keele kõlaga: Neurokõne, saab ka *.wav failina alla laadida.
apt
"Pühademuna" apt käsus, mis muidu tarkvarahalduseks mõeldud (tarkvarapakettide paigaldamine, eemaldamine, jne). Vanemates Ubuntu, Debian versioonides on apt-get, uuemates apt.
apt-get
apt-get moo
apt
apt moo
Pitsa käsureal
xxczaki/pizza-cli on valmis käsurea-tööriist, millega Eestist (Wolt/Bolt Food, Domino’s Tallinn) päriselt pitsat tellida ei saa.
Miks?
- xxczaki/pizza-cli on demo „just for fun!“ — see loob vaid JSON-faili, ei tee päris tellimust.
- On olemas CLI-d, mis kasutavad Domino’s API-t, kuid need sihivad USA/teisi turge (nt harrybrwn/apizza) ja ei tööta Eesti domeenidega.
- Woltil on API-d ainult partneritele, kaupmeestele (tellimused, menüü, asukoht), mitte tavakasutajale tellimiseks.
- Bolt Foodil on samuti integratsiooni-API restoranidele/POS-idele, mitte avalik „customer ordering“ API. Bolt ütleb ka avalikult, et neil ei ole avalikke API-sid lõppkasutajale.
- Domino Tallinn toimib ametliku veebiga (ja rakendustega), mitte CLI kaudu.
Näide: Domino Tallinn pitsa käsurealt
Playwrighti skript (Node.js):
#!/usr/bin/env node
// Minimal Playwright-based CLI to add pizzas to cart on Domino's Estonia site
// and navigate to checkout. It does NOT bypass login/2FA or auto-submit payment.
// First run `init` to log in manually; the session is saved to auth.json.
// Usage examples:
// node dominos-cli-playwright.mjs init
// node dominos-cli-playwright.mjs order --city tallinn --lang et \
// --items "Pepperoni:L" "Hawaii:M" --address "Sinu aadress 1" --note "Uksekell 12"
import { chromium } from 'playwright';
import fs from 'fs';
const AUTH_FILE = './auth.json';
function parseArgs() {
const args = process.argv.slice(2);
const cmd = args[0] || 'help';
const opts = { city: 'tallinn', lang: 'et', items: [], address: '', note: '' };
for (let i = 1; i < args.length; i++) {
const [k, v] = args[i].startsWith('--') ? args[i].slice(2).split('=') : [args[i], null];
switch (k) {
case 'city': opts.city = v || args[++i]; break;
case 'lang': opts.lang = v || args[++i]; break;
case 'items': {
// allow space-separated after --items or multiple --items flags
if (v) {
opts.items.push(v);
} else {
while (args[i+1] && !args[i+1].startsWith('--')) opts.items.push(args[++i]);
}
break;
}
case 'address': opts.address = v || args[++i]; break;
case 'note': opts.note = v || args[++i]; break;
default:
break;
}
}
return { cmd, opts };
}
async function ensureAuth(browser, baseUrl) {
// If we already have auth, return storageState
if (fs.existsSync(AUTH_FILE)) return AUTH_FILE;
const ctx = await browser.newContext();
const page = await ctx.newPage();
console.log('\nOpening login page. Please log in manually, including any 2FA.');
await page.goto(baseUrl);
// Open login (button text varies by language)
const loginSelectors = [
'text=Logi sisse',
'text=Login',
'text=Sign in'
];
for (const sel of loginSelectors) {
const el = await page.$(sel);
if (el) { await el.click(); break; }
}
console.log('Waiting up to 5 minutes for you to finish login...');
await page.waitForTimeout(10000);
// Poll for profile indicator (avatar icon; site-specific, so fall back to manual)
try {
await page.waitForSelector('text=Logi välja,Logout,Sign out', { timeout: 300000 });
} catch (e) {
console.warn('Could not detect post-login state automatically. If you DID log in, we will still save cookies.');
}
await ctx.storageState({ path: AUTH_FILE });
await ctx.close();
console.log(`Saved session to ${AUTH_FILE}`);
return AUTH_FILE;
}
async function addItemByName(page, name, sizeCode) {
// Navigate to pizzas list and search by name, then choose size
// sizeCode: 'S' | 'M' | 'L' | 'XL' (if available)
// Open pizza list
await page.goto(`${page.url().split('/').slice(0, 3).join('/')}/et/tallinn/pitsa/`);
// Accept cookies if banner exists
const cookieBtn = await page.$('button:has-text("Nõustu")');
if (cookieBtn) await cookieBtn.click();
// Try built-in search
const searchBox = await page.$('input[placeholder*="Otsi"], input[placeholder*="Search"]');
if (searchBox) {
await searchBox.fill(name);
await page.keyboard.press('Enter');
await page.waitForLoadState('networkidle');
}
// Click pizza card containing the name
const card = await page.waitForSelector(`a:has-text("${name}")`, { timeout: 20000 });
await card.click();
// Choose size
const sizeMap = { 'S':'S', 'M':'M', 'L':'L', 'XL':'XL' };
const size = sizeMap[sizeCode?.toUpperCase?.()] || 'L';
const sizeBtn = await page.$(`button:has-text("${size}")`);
if (sizeBtn) await sizeBtn.click();
// Add to cart (button text varies by language)
const addSelectors = [
'button:has-text("Lisa korvi")',
'button:has-text("Add to cart")',
'button:has-text("Add")'
];
for (const sel of addSelectors) {
const btn = await page.$(sel);
if (btn) { await btn.click(); break; }
}
await page.waitForTimeout(1000);
}
async function proceedToCheckout(page, address, note) {
// Open cart
const cartSelectors = ['a:has-text("Ostukorv")', 'a[aria-label="Cart"]', 'a[href*="cart"]'];
for (const sel of cartSelectors) { const el = await page.$(sel); if (el) { await el.click(); break; } }
// Delivery mode and address may require interaction; attempt simple flow then pause for manual edits
const deliverySelectors = ['button:has-text("Kojutoomine")', 'button:has-text("Delivery")'];
for (const sel of deliverySelectors) { const el = await page.$(sel); if (el) { await el.click(); break; } }
if (address) {
const addrInput = await page.$('input[name*="address"], input[placeholder*="Aadress"], input[placeholder*="Address"]');
if (addrInput) {
await addrInput.fill(address);
await page.keyboard.press('Enter');
await page.waitForTimeout(1500);
}
}
if (note) {
const noteBox = await page.$('textarea[placeholder*="märkused" i], textarea[placeholder*="notes" i]');
if (noteBox) await noteBox.fill(note);
}
console.log('At checkout. Please review payment and place the order manually.');
}
async function main() {
const { cmd, opts } = parseArgs();
const baseUrl = `https://dominospizza.ee/${opts.lang}/${opts.city}/`;
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
console.log(`\nDomino's Estonia CLI (Playwright)\n\nCommands:\n init\n Open site and let you log in; saves cookies to ${AUTH_FILE}.\n\n order --city tallinn --lang et \\\n --items "Pepperoni:L" "Hawaii:M" \\\n --address "Sinu aadress 1" --note "Uksekell 12"\n Adds items to cart and navigates to checkout.\n\nNotes:\n • Selectors are best-effort and may need tweaks if the site changes.\n • Payment is never submitted automatically.\n`);
process.exit(0);
}
const browser = await chromium.launch({ headless: false });
if (cmd === 'init') {
await ensureAuth(browser, baseUrl);
await browser.close();
return;
}
if (cmd === 'order') {
const storageState = fs.existsSync(AUTH_FILE) ? AUTH_FILE : null;
const context = await browser.newContext(storageState ? { storageState } : {});
const page = await context.newPage();
await page.goto(baseUrl);
// Try to add each requested item
for (const spec of opts.items) {
const [name, size = 'L'] = spec.split(':');
console.log(`Adding ${name} (${size})...`);
await addItemByName(page, name, size);
}
await proceedToCheckout(page, opts.address, opts.note);
// keep the browser open so user can finish payment
console.log('Done. Leaving browser open.');
return; // do not close browser
}
console.error('Unknown command. Use --help for usage.');
await browser.close();
}
main().catch(err => { console.error(err); process.exit(1); });
Mida skript teeb?
- See skript avab Domino Eesti veebilehe (Tallinn) ja laseb esimesel korral käsitsi sisse logida (koos 2FA-ga), salvestab sessiooni auth.json-i;
- järgmistel kordadel lisab nimede järgi (nt “Pepperoni”, “Hawaii”) valitud suuruses pitsad korvi ja viib checkouti juurde, et saaksid makse käsitsi kinnitada (skript ei kinnita ega tee makset sinu eest).
Märkus: see on „best-effort“ automatiseerimine — veebilehe elemendid võivad muutuda. Domino Eesti ametlik veeb on siin, nii et sihtkoht on korrektne.
Kuidas kasutada
Eeldused (Linux):
# Node 18+ ja Playwright
sudo apt-get install -y nodejs npm # (või nvm)
npm init -y
npm i playwright
npx playwright install --with-deps chromium
Laadi kanvasest fail alla nimega dominos-cli-playwright.mjs (see on juba avatud siin vestluse kõrval). Tee see käivitatavaks:
chmod u+x dominos-cli-playwright.mjs
Esmane sisselogimine (salvestab auth.json):
node dominos-cli-playwright.mjs init
- Avaneb veebilehitseja. Logi sisse oma Domino Eesti kontoga (vajadusel 2FA).
- Sulge veebilehitseja aken alles siis, kui skript teatab, et sessioon on salvestatud.
Tellimus terminalist (lisab korvi ja avab checkouti):
# Näide: 1x Pepperoni (L), 1x Hawaii (M), eesti keel, Tallinn
node dominos-cli-playwright.mjs order --city tallinn --lang et \
--items "Pepperoni:L" "Hawaii:M" \
--address "Näide tn 1, Tallinn" --note "Uksekell 12"
Seejärel vaata korv üle ja kinnita makse käsitsi.
Tähtsad tähelepanekud
- TOS & turvalisus: skript ei väldi CAPTCHA-sid ega 2FA-d ning ei tee makset automaatselt — see aitab vaid korvi täita ja checkouti jõuda.
- Asukohad & ametlik kanal: Domino tegutseb Tallinnas ametliku veebilehe ja rakendustedega; vajadusel võid sama brändi leida ka Wolti kaudu, kuid Wolti/Bolt Foodi kliendipoolset avalikku API-t pole (nii et veebiklõpsu-automatiseerimine on ainuke realistlik CLI-tee).
Täiustatud skript
#!/usr/bin/env node
// Domino's Estonia Advanced CLI (Playwright)
// ------------------------------------------------------------
// What it does
// - Logs in (manual, once) and stores session (auth.json)
// - Adds pizzas/sides by *precise* preferences (size, crust, toppings)
// - Applies multiple coupon codes
// - Chooses Delivery vs Pickup (and preferred store for Pickup)
// - Optional Reorder from order history
// - Optional scheduling (if site supports time slots)
// - Opens Checkout for manual payment (never submits payment)
//
// Usage (examples):
// node dominos-cli-playwright.mjs init
// node dominos-cli-playwright.mjs order \
// --lang et --city tallinn --delivery \
// --address "Näide tn 1, Tallinn" \
// --items "Pepperoni:L:hand-tossed" "Hawaii:M:thin" \
// --add "Pepperoni:+jalapeno" --rm "Hawaii:-ananas" \
// --coupon ALE --coupon MORE
// node dominos-cli-playwright.mjs reorder --latest
// node dominos-cli-playwright.mjs order --pickup --store "Tondi"
// node dominos-cli-playwright.mjs order --config prefs.json
//
// prefs.json shape:
// {
// "lang": "et", "city": "tallinn",
// "delivery": true, "pickup": false,
// "address": "Näide tn 1, Tallinn",
// "note": "Uksekell 12",
// "store": "Tondi",
// "items": [
// {"name":"Pepperoni","size":"L","crust":"hand-tossed","add":["jalapeno"],"remove":[]},
// {"name":"Hawaii","size":"M","crust":"thin","remove":["ananas"]}
// ],
// "coupons":["ALE","MORE"],
// "schedule": {"type":"today", "time":"19:00"} // best-effort; site dependent
// }
import { chromium } from 'playwright';
import fs from 'fs';
const AUTH_FILE = './auth.json';
function parseArgs() {
const args = process.argv.slice(2);
const cmd = args[0] || 'help';
const opts = {
lang: 'et', city: 'tallinn', delivery: true, pickup: false,
address: '', note: '', store: '', items: [], coupons: [],
addMap: {}, rmMap: {}, schedule: null, latest: false, config: ''
};
for (let i = 1; i < args.length; i++) {
const a = args[i];
const [k, vRaw] = a.startsWith('--') ? a.slice(2).split('=') : [a, null];
const v = vRaw ?? args[i+1];
switch (k) {
case 'lang': opts.lang = v; if(!vRaw) i++; break;
case 'city': opts.city = v; if(!vRaw) i++; break;
case 'delivery': opts.delivery = true; opts.pickup = false; break;
case 'pickup': opts.pickup = true; opts.delivery = false; break;
case 'address': opts.address = v; if(!vRaw) i++; break;
case 'note': opts.note = v; if(!vRaw) i++; break;
case 'store': opts.store = v; if(!vRaw) i++; break;
case 'coupon': (opts.coupons.push(v), (!vRaw && i++)); break;
case 'items': {
// supports: --items "Pepperoni:L:thin" "Hawaii:M"
if (vRaw) opts.items.push(vRaw);
else { while (args[i+1] && !args[i+1].startsWith('--')) opts.items.push(args[++i]); }
break;
}
case 'add': {
// --add "Pepperoni:+jalapeno" may be repeated
const [pizza, mod] = (v ?? '').split(':');
if (!opts.addMap[pizza]) opts.addMap[pizza] = [];
if (mod?.startsWith('+')) opts.addMap[pizza].push(mod.slice(1));
if(!vRaw) i++;
break;
}
case 'rm': case 'remove': {
const [pizza, mod] = (v ?? '').split(':');
if (!opts.rmMap[pizza]) opts.rmMap[pizza] = [];
if (mod?.startsWith('-')) opts.rmMap[pizza].push(mod.slice(1));
if(!vRaw) i++;
break;
}
case 'latest': opts.latest = true; break;
case 'config': opts.config = v; if(!vRaw) i++; break;
default: break;
}
}
if (opts.config && fs.existsSync(opts.config)) {
try {
const cfg = JSON.parse(fs.readFileSync(opts.config, 'utf8'));
Object.assign(opts, cfg);
} catch (e) { console.warn('Failed to read config json:', e.message); }
}
return { cmd, opts };
}
function baseUrl(lang, city) {
return `https://dominospizza.ee/${lang}/${city}/`;
}
async function withContext(browser, storagePath) {
const ctx = await browser.newContext(storagePath && fs.existsSync(storagePath) ? { storageState: storagePath } : {});
const page = await ctx.newPage();
return { ctx, page };
}
async function acceptCookies(page) {
const candidates = [
'button:has-text("Nõustu")',
'button:has-text("Noustu")', // typo guard
'button:has-text("Accept")',
'button[aria-label*="Accept"]'
];
for (const sel of candidates) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
}
async function ensureAuth(browser, startUrl) {
if (fs.existsSync(AUTH_FILE)) return AUTH_FILE;
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto(startUrl, { waitUntil: 'domcontentloaded' });
await acceptCookies(page);
// Try open login modal/menu (site copy varies by lang)
const loginTriggers = ['text=Logi sisse', 'text=Login', 'text=Sign in', 'button:has-text("Logi sisse")'];
for (const sel of loginTriggers) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
console.log('Please complete login (and 2FA if prompted). You have 5 minutes.');
try { await page.waitForSelector('text=Logi välja,Logout,Sign out', { timeout: 300000 }); } catch {}
await ctx.storageState({ path: AUTH_FILE });
await ctx.close();
console.log(`Saved session to ${AUTH_FILE}`);
return AUTH_FILE;
}
async function navigateToMenu(page, lang, city) {
const url = baseUrl(lang, city) + 'pitsa/';
await page.goto(url, { waitUntil: 'networkidle' });
await acceptCookies(page);
}
async function searchAndOpenPizza(page, pizzaName) {
// Try built-in search if exists; otherwise click card by text
const searchSel = 'input[placeholder*="Otsi" i], input[placeholder*="Search" i]';
const box = await page.$(searchSel);
if (box) {
await box.fill(''); await box.type(pizzaName); await page.keyboard.press('Enter');
await page.waitForLoadState('networkidle').catch(()=>{});
}
const card = await page.waitForSelector(`a:has-text("${pizzaName}")`, { timeout: 20000 });
await card.click();
}
async function chooseSizeAndCrust(page, size, crust) {
const sizeCode = (size || 'L').toUpperCase();
const sizeLabels = { 'S':'S', 'M':'M', 'L':'L', 'XL':'XL' };
const sizeText = sizeLabels[sizeCode] || 'L';
const sizeBtn = await page.$(`button:has-text("${sizeText}")`);
if (sizeBtn) await sizeBtn.click().catch(()=>{});
if (crust) {
const crustBtns = await page.$$('button');
for (const b of crustBtns) {
const t = (await b.textContent() || '').toLowerCase();
if (t.includes(crust.toLowerCase())) { await b.click().catch(()=>{}); break; }
}
}
}
async function applyToppings(page, add=[], remove=[]) {
// Look for toggles/checkboxes by ingredient keywords
const norm = s => s.toLowerCase();
for (const ing of add) {
const sel = `label:has-text("${ing}")`; // modern sites often use label for topping
const el = await page.$(sel).catch(()=>null);
if (el) await el.click().catch(()=>{});
}
for (const ing of remove) {
const sel = `label:has-text("${ing}")`;
const el = await page.$(sel).catch(()=>null);
if (el) await el.click().catch(()=>{}); // toggle off if preselected
}
}
async function addCurrentToCart(page) {
const addSelectors = ['button:has-text("Lisa korvi")', 'button:has-text("Add to cart")', 'button:has-text("Add")'];
for (const sel of addSelectors) { const btn = await page.$(sel); if (btn) { await btn.click().catch(()=>{}); await page.waitForTimeout(800); return; } }
throw new Error('Add to cart button not found');
}
async function openCart(page) {
const cartSelectors = ['a:has-text("Ostukorv")', 'a[aria-label*="Cart" i]', 'a[href*="cart" i]', 'button[aria-label*="Cart" i]'];
for (const sel of cartSelectors) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); await page.waitForLoadState('networkidle').catch(()=>{}); return; } }
}
async function setFulfilment(page, { delivery, pickup, address, store }) {
if (pickup) {
const pickupBtns = ['button:has-text("Kaasa")', 'button:has-text("Pickup")'];
for (const sel of pickupBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (store) {
const storeOpeners = ['button:has-text("Vali restoran")', 'button:has-text("Choose store")'];
for (const sel of storeOpeners) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
// pick store by contains text
const storeBtn = await page.$(`button:has-text("${store}")`).catch(()=>null);
if (storeBtn) await storeBtn.click().catch(()=>{});
}
} else if (delivery) {
const delBtns = ['button:has-text("Kojutoomine")', 'button:has-text("Delivery")'];
for (const sel of delBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (address) {
const addrInput = await page.$('input[name*="address" i], input[placeholder*="Aadress" i], input[placeholder*="Address" i]').catch(()=>null);
if (addrInput) {
await addrInput.fill(address);
await page.keyboard.press('Enter');
await page.waitForTimeout(1500);
}
}
}
}
async function applyCoupons(page, coupons=[]) {
if (!coupons?.length) return;
const openers = ['button:has-text("Sisesta sooduskood")', 'button:has-text("Enter promo code")'];
for (const sel of openers) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
for (const code of coupons) {
const input = await page.$('input[placeholder*="kood" i], input[placeholder*="code" i]').catch(()=>null);
const applyBtn = await page.$('button:has-text("Rakenda"), button:has-text("Apply")').catch(()=>null);
if (input) { await input.fill(code); }
if (applyBtn) { await applyBtn.click().catch(()=>{}); }
await page.waitForTimeout(1000);
}
}
async function maybeSchedule(page, schedule) {
if (!schedule) return;
const timeBtns = ['button:has-text("Ajasta")', 'button:has-text("Schedule")'];
for (const sel of timeBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (schedule.time) {
const picker = await page.$('input[type="time"], input[name*="time" i]').catch(()=>null);
if (picker) { await picker.fill(schedule.time).catch(()=>{}); }
}
}
async function doOrder(opts) {
const browser = await chromium.launch({ headless: false });
const startUrl = baseUrl(opts.lang, opts.city);
await ensureAuth(browser, startUrl);
const { ctx, page } = await withContext(browser, AUTH_FILE);
await page.goto(startUrl, { waitUntil: 'networkidle' });
await acceptCookies(page);
// Add requested items
for (const spec of opts.items || []) {
// allow inline spec "Name:Size:Crust" strings too
const parsed = typeof spec === 'string' ? (()=>{ const [name,size,crust] = spec.split(':'); return {name, size, crust}; })() : spec;
const name = parsed.name; const size = parsed.size || 'L'; const crust = parsed.crust || '';
const add = (opts.addMap?.[name] || parsed.add || []);
const remove = (opts.rmMap?.[name] || parsed.remove || []);
console.log(`Adding ${name} (${size}${crust?','+crust:''})`);
await navigateToMenu(page, opts.lang, opts.city);
await searchAndOpenPizza(page, name);
await chooseSizeAndCrust(page, size, crust);
await applyToppings(page, add, remove);
await addCurrentToCart(page);
}
await openCart(page);
await setFulfilment(page, opts);
if (opts.note) {
const noteArea = await page.$('textarea[placeholder*="märkused" i], textarea[placeholder*="notes" i]').catch(()=>null);
if (noteArea) await noteArea.fill(opts.note).catch(()=>{});
}
await applyCoupons(page, opts.coupons);
await maybeSchedule(page, opts.schedule);
console.log('At checkout — review & complete payment manually.');
// Leave browser open for manual completion
}
async function doReorder(opts) {
const browser = await chromium.launch({ headless: false });
await ensureAuth(browser, baseUrl(opts.lang, opts.city));
const { page } = await withContext(browser, AUTH_FILE);
await page.goto(baseUrl(opts.lang, opts.city), { waitUntil: 'domcontentloaded' });
await acceptCookies(page);
// Try to open profile/orders
const profileBtns = ['a:has-text("Minu konto")','a:has-text("My account")','button:has-text("Konto")'];
for (const sel of profileBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const ordersLinks = ['a:has-text("Tellimused")','a:has-text("Orders")'];
for (const sel of ordersLinks) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
// Click the first/desired order's Reorder button
const reorderBtn = await page.$('button:has-text("Telli uuesti"), button:has-text("Reorder")').catch(()=>null);
if (reorderBtn) {
await reorderBtn.click().catch(()=>{});
await openCart(page);
await setFulfilment(page, opts);
await applyCoupons(page, opts.coupons);
console.log('Reorder loaded to cart. Complete manually.');
} else {
console.warn('Could not find a Reorder button. You may need to open a specific past order manually.');
}
}
async function main() {
const { cmd, opts } = parseArgs();
if (cmd === 'help' || cmd === '--help' || cmd === '-h' || !['init','order','reorder'].includes(cmd)) {
console.log(`
Domino's Estonia Advanced CLI (Playwright)
Commands:
init
Open site and let you log in; saves cookies to ${AUTH_FILE}.
order [--config prefs.json] [--delivery|--pickup] [--store NAME] \
[--address "ADDR"] [--items "Name:Size:Crust" ...] \
[--add "Name:+topping"] [--rm "Name:-topping"] \
[--coupon CODE] [--lang et|en] [--city tallinn]
reorder [--latest] [--coupon CODE] [--lang et|en] [--city tallinn]
`);
process.exit(0);
}
const browser = await chromium.launch({ headless: false });
if (cmd === 'init') {
await ensureAuth(browser, baseUrl(opts.lang, opts.city));
await browser.close();
return;
}
await browser.close(); // new launches inside flows to keep contexts isolated
if (cmd === 'order') {
await doOrder(opts);
return;
}
if (cmd === 'reorder') {
await doReorder(opts);
return;
}
}
main().catch(err => { console.error(err); process.exit(1); });
See täiustatud skript toetab:
- täpseid eelistusi (suurus, koorik, lisandid/mahavõtud),
- delivery vs pickup (sh poe valik pickupi jaoks),
- mitut kupongikoodi,
- reorder minevikutellimustest,
- best-effort ajastust.
Kiirjuhend
# eeldused
npm i playwright
npx playwright install --with-deps chromium
# esmane login
node dominos-cli-playwright.mjs init
# näide: delivery + kupongid + lisandid/mahavõtud
node dominos-cli-playwright.mjs order --lang et --city tallinn --delivery \
--address "Näide tn 1, Tallinn" --note "Uksekell 12" \
--items "Pepperoni:L:hand-tossed" "Hawaii:M:thin" \
--add "Pepperoni:+jalapeno" --rm "Hawaii:-ananas" \
--coupon ALE --coupon MORE
# pickup kindlast poest
node dominos-cli-playwright.mjs order --pickup --store "Tondi"
# reorder (viimati tehtu), lisa kupong
node dominos-cli-playwright.mjs reorder --latest --coupon ALE
Märkus: skript avab ametliku Domino’s Estonia saidi (Tallinn) ja liigub menüüs/checkoutis; kupongid nagu ALE, MORE jms on päriselt olemasolevate promode näited nende lehel (promo-/uudiste-lehed). Wolti/Bolt Foodi API-d on suunatud partneritele (POS/integratsioon), mitte klienditellimuste CLI-ks — seetõttu kasutame Playwrighti.
Näitena veel eelistuste fail
{
"lang": "et",
"city": "tallinn",
"delivery": true,
"pickup": false,
"address": "Näide tn 1, Tallinn",
"note": "Uksekell 12",
"store": "Tammsaare",
"items": [
{ "name": "Pepperoni", "size": "L", "crust": "hand-tossed", "add": ["jalapeno"], "remove": [] },
{ "name": "Hawaii", "size": "M", "crust": "thin", "add": [], "remove": ["ananas"] },
{ "name": "5 Cheeses", "size": "L", "crust": "thin" }
],
"coupons": ["MORE", "TEISIPAEV", "LUNCH"],
"schedule": { "type": "today", "time": "19:00" }
}
See sobib otse käsuga --config dominos-prefs.json.
Mida sinna panna ja miks:
- store: "Tammsaare" — Tallinnas on Domino’s Tammsaare/Mustakivi filiaalid (nt Woltis nähtavad; Tammsaare aadressi kinnitab ka kaardikanne).
- coupons: ["MORE","TEISIPAEV","LUNCH"] — need vastavad Domino’s Tallinn promo-lehtedel toodud koodidele: 2+1 (MORE), Crazy Tuesday (TEISIPAEV) ja lõunapakkumine (LUNCH). Vaata nende täpseid tingimusi promo-lehelt.
- Menüü/põhi-URL ja eesti/inglise lehed on samad, mida skript kasutab (/et/tallinn, /en/tallinn).
Kuidas kasutada
# kasutamine prefs.json'iga
node dominos-cli-playwright.mjs order --config dominos-prefs.json
Lisateave:
Kupongid / promokoodid
- LUNCH (Lunch Offer) — ametlik promo-leht koos tingimustega.
- TEISIPÄEV (Crazy Tuesday) — ametlik promo/uudis, tingimused (2 M või L ühe hinnaga).
- MORE (2+1 offer) — ametlik promo-leht koos piirangutega
Teised kampaaniad (nt MEGA Weekend, Cutting Prices, „Üheskoos on maitsvam!“) — vajadusel vaata sealt jooksvaid koode ja tingimusi.
Kõik uudised/pakkumised ühes kohas: ametlik “News & Promotions” koondleht.
Poed / aadressid (pickup’i jaoks)
- Tammsaare (Mustamäe): ametlik kontaktinfo Facebooki „About“ ja ka Waze kaardikande järgi: A. H. Tammsaare tee 104a, Tallinn
- Mustakivi (Lasnamäe): avamisteated ja aadress Mustakivi tee 17 (Lasnamäe Prisma keskus) ametlikes postitustes ning keskuse lehel: link1, link2, link3, link4, link5
- Platvormid (abiks poe valikul): Woltis näeb Tammsaare/Mustakivi asukohti ning „Schedule order“ märget (kasulik ajastuseks): link1, link2, link3
Kohaletoimetus, pickup ja ajastus
Ametlik rakendus (Google Play kirjeldus): kinnitab, et saab tellida delivery (30 min) või pickup (15 min) ja valida menüüst; hea viide, et mõlemad täitmisviisid on toetatud.
Ajastuse näide: Woltis on „Schedule order“ (Domino’s Tammsaare/Mustakivi), mida saab kasutada, kui soovid tellimust kindlale ajale. (Domino’s oma veebil ajastus võib varieeruda.) link1, link2
Kuidas need väljad prefs.json-is allikatelt täita
- lang, city → võta keel/tee struktuur ametlikult avalehest.
- items[].name/size/crust → nimed, suurused (S–XL) ja koorikud ametlikult menüült; „Half & Half“ korral kasuta vastavat lehte.
- coupons[] → võta koodid ja tingimused promo-lehtedelt (LUNCH, TEISIPÄEV, MORE jne).
- pickup/delivery, store, address → kasuta poodide aadresse (Tammsaare 104a; Mustakivi 17) ning märgi, kas soovid pickup’i või kojutoomist.
- schedule → kui tahad ajastada, on seda mugav teha Wolti „Schedule order“ abil (või kui Domino’s veebis on parasjagu vastav valik nähtav).
Täiustatud prefs.json
{
"lang": "et",
"city": "tallinn",
"delivery": true,
"pickup": false,
"address": "A. H. Tammsaare tee 104a, 12918 Tallinn",
"note": "Uksekell 12",
"store": "Tammsaare",
"store_addresses": {
"Tammsaare": {
"address": "A. H. Tammsaare tee 104a, 12918 Tallinn",
"phone": "+372 633 3303"
},
"Mustakivi": {
"address": "Mustakivi tee 17, 13912 Tallinn"
}
},
"items": [
{ "name": "Pepperoni", "size": "L", "crust": "hand-tossed", "add": ["jalapeno"], "remove": [] },
{ "name": "Hawaii", "size": "M", "crust": "thin", "add": [], "remove": ["ananas"] },
{ "name": "5 Cheeses", "size": "L", "crust": "thin", "add": [], "remove": [] }
],
"coupons": ["TEISIPAEV", "LUNCH"],
"schedule": { "type": "today", "time": "19:00" }
}
- Täpsed poeandmed: Tammsaare — A. H. Tammsaare tee 104a, 12918 Tallinn, tel. +372 633 3303 (Waze + Facebook „About“ kinnitused).
- Mustakivi — Mustakivi tee 17, Tallinn (ametlikud postitused kinnitavad Lasnamäe avamist ja aadressi).
- Menüü ja nimetused (Pepperoni, Hawaii, 5 Cheeses; suurused/koorikud): ametlik menüü Tallinna lehel (ET/EN).
- Kupongid, mis on koodipõhised: TEISIPAEV (Crazy Tuesday, 2 M või L ühe hinnaga, tingimused lehel).
- LUNCH (Lunch Offer, kood „LUNCH“, täpsemad tingimused ja upgradede hinnad promolehel).
- Ajastuse tugi: schedule väli alles; Domino’s enda veebil ajastus võib erineda, kuid pakkumised ja menüü on kinnitatud ametlikelt lehtedelt. Üldised promod/uuemad soodukad on koondatud „News & Promotions“ alla.
Eraldi dominos-prefs-mustakivi.json
{
"lang": "et",
"city": "tallinn",
"delivery": false,
"pickup": true,
"store": "Mustakivi",
"store_addresses": {
"Tammsaare": {
"address": "A. H. Tammsaare tee 104a, 12918 Tallinn",
"phone": "+372 633 3303"
},
"Mustakivi": {
"address": "Mustakivi tee 17, 13912 Tallinn"
}
},
"address": "Mustakivi tee 17, 13912 Tallinn",
"note": "Võtan ise järgi (pickup)",
"items": [
{ "name": "Pepperoni", "size": "XL", "crust": "thin", "add": ["jalapeno"], "remove": [] },
{ "name": "5 Cheeses", "size": "XL", "crust": "thin", "add": [], "remove": [] }
],
"coupons": ["TEISIPAEV", "LUNCH"],
"schedule": { "type": "today", "time": "19:30" }
}
Kasutamine
# pickup Mustakivi poest, XL thin eelistustega
node dominos-cli-playwright.mjs order --config dominos-prefs-mustakivi.json
Veel üks dominos-prefs-mustakivi.json näide
{
"lang": "et",
"city": "tallinn",
"delivery": false,
"pickup": true,
"store": "Mustakivi",
"store_addresses": {
"Tammsaare": {
"address": "A. H. Tammsaare tee 104a, 12918 Tallinn",
"phone": "+372 633 3303"
},
"Mustakivi": {
"address": "Mustakivi tee 17, 11415 Tallinn"
}
},
"address": "Mustakivi tee 17, 11415 Tallinn",
"note": "Võtan ise järgi (pickup)",
"items": [
{ "name": "Hypnotica", "size": "XL", "crust": "thin", "add": [], "remove": [] },
{ "name": "Carbonara", "size": "XL", "crust": "thin", "add": [], "remove": [] },
{ "name": "5 Cheeses", "size": "L", "crust": "thin", "add": [], "remove": [] }
],
"half_and_half": {
"size": "XL",
"crust": "thin",
"left": "Carbonara",
"right": "Hypnotica",
"note": "Half & Half (pool-pool) – vajab half_pizza lehe voo tuge"
},
"coupons": ["TEISIPAEV", "LUNCH"],
"schedule": { "type": "today", "time": "19:30" }
}
- Hypnotica on Domino’s Eesti menüüs (koostisosad: spinat, mozzarella, pepperoni, peekon, veiseliha, röstitud sibul, koore- ja BBQ-kaste); näha nii hinnakoondis kui ka Bolt Foodis. link1, link2
- Carbonara on olemas (koorekastme, singi, peekoni, seente, punase sibulaga) — kirjeldus ja komplektid nähtavad Bolt Foodis.
- Half & Half (pool-pool) on ametlik erileht Tallinnas (/half_pizza/), kust saab ehitada kahe maitsega pitsa.
Täiustatud skript koos eelnevate eelistustega
#!/usr/bin/env node
// Domino's Estonia Advanced CLI (Playwright) — now with Half & Half support
// ------------------------------------------------------------
// What it does
// - Logs in (manual, once) and stores session (auth.json)
// - Adds pizzas/sides by precise preferences (size, crust, toppings)
// - NEW: Builds Half & Half pizzas from two flavours on the /half_pizza/ flow
// - Applies multiple coupon codes
// - Chooses Delivery vs Pickup (and preferred store for Pickup)
// - Optional Reorder from order history
// - Optional scheduling (if site supports time slots)
// - Opens Checkout for manual payment (never submits payment)
//
// Usage (examples):
// node dominos-cli-playwright.mjs init
// node dominos-cli-playwright.mjs order \
// --config dominos-prefs.json
// node dominos-cli-playwright.mjs order \
// --lang et --city tallinn --delivery \
// --items "Pepperoni:L:hand-tossed" \
// --coupon ALE
// node dominos-cli-playwright.mjs reorder --latest
//
// Config additions for Half & Half:
// "half_and_half": {
// "size": "XL",
// "crust": "thin",
// "left": "Carbonara",
// "right": "Hypnotica",
// "note": "optional note"
// }
import { chromium } from 'playwright';
import fs from 'fs';
const AUTH_FILE = './auth.json';
function parseArgs() {
const args = process.argv.slice(2);
const cmd = args[0] || 'help';
const opts = {
lang: 'et', city: 'tallinn', delivery: true, pickup: false,
address: '', note: '', store: '', items: [], coupons: [],
addMap: {}, rmMap: {}, schedule: null, latest: false, config: '',
half_and_half: null
};
for (let i = 1; i < args.length; i++) {
const a = args[i];
const [k, vRaw] = a.startsWith('--') ? a.slice(2).split('=') : [a, null];
const v = vRaw ?? args[i+1];
switch (k) {
case 'lang': opts.lang = v; if(!vRaw) i++; break;
case 'city': opts.city = v; if(!vRaw) i++; break;
case 'delivery': opts.delivery = true; opts.pickup = false; break;
case 'pickup': opts.pickup = true; opts.delivery = false; break;
case 'address': opts.address = v; if(!vRaw) i++; break;
case 'note': opts.note = v; if(!vRaw) i++; break;
case 'store': opts.store = v; if(!vRaw) i++; break;
case 'coupon': (opts.coupons.push(v), (!vRaw && i++)); break;
case 'items': {
if (vRaw) opts.items.push(vRaw);
else { while (args[i+1] && !args[i+1].startsWith('--')) opts.items.push(args[++i]); }
break;
}
case 'add': {
const [pizza, mod] = (v ?? '').split(':');
if (!opts.addMap[pizza]) opts.addMap[pizza] = [];
if (mod?.startsWith('+')) opts.addMap[pizza].push(mod.slice(1));
if(!vRaw) i++;
break;
}
case 'rm': case 'remove': {
const [pizza, mod] = (v ?? '').split(':');
if (!opts.rmMap[pizza]) opts.rmMap[pizza] = [];
if (mod?.startsWith('-')) opts.rmMap[pizza].push(mod.slice(1));
if(!vRaw) i++;
break;
}
case 'latest': opts.latest = true; break;
case 'config': opts.config = v; if(!vRaw) i++; break;
default: break;
}
}
if (opts.config && fs.existsSync(opts.config)) {
try {
const cfg = JSON.parse(fs.readFileSync(opts.config, 'utf8'));
Object.assign(opts, cfg);
} catch (e) { console.warn('Failed to read config json:', e.message); }
}
return { cmd, opts };
}
function baseUrl(lang, city) {
return `https://dominospizza.ee/${lang}/${city}/`;
}
async function withContext(browser, storagePath) {
const ctx = await browser.newContext(storagePath && fs.existsSync(storagePath) ? { storageState: storagePath } : {});
const page = await ctx.newPage();
return { ctx, page };
}
async function acceptCookies(page) {
const candidates = [
'button:has-text("Nõustu")',
'button:has-text("Accept")',
'button[aria-label*="Accept" i]'
];
for (const sel of candidates) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
}
async function ensureAuth(browser, startUrl) {
if (fs.existsSync(AUTH_FILE)) return AUTH_FILE;
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto(startUrl, { waitUntil: 'domcontentloaded' });
await acceptCookies(page);
const loginTriggers = ['text=Logi sisse', 'text=Login', 'text=Sign in', 'button:has-text("Logi sisse")'];
for (const sel of loginTriggers) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
console.log('Please complete login (and 2FA if prompted). You have 5 minutes.');
try { await page.waitForSelector('text=Logi välja,Logout,Sign out', { timeout: 300000 }); } catch {}
await ctx.storageState({ path: AUTH_FILE });
await ctx.close();
console.log(`Saved session to ${AUTH_FILE}`);
return AUTH_FILE;
}
async function navigateToMenu(page, lang, city) {
const url = baseUrl(lang, city) + 'pitsa/';
await page.goto(url, { waitUntil: 'networkidle' });
await acceptCookies(page);
}
async function navigateToHalfAndHalf(page, lang, city) {
const url = baseUrl(lang, city) + 'half_pizza/';
await page.goto(url, { waitUntil: 'networkidle' });
await acceptCookies(page);
}
async function searchAndOpenPizza(page, pizzaName) {
const searchSel = 'input[placeholder*="Otsi" i], input[placeholder*="Search" i]';
const box = await page.$(searchSel);
if (box) {
await box.fill(''); await box.type(pizzaName); await page.keyboard.press('Enter');
await page.waitForLoadState('networkidle').catch(()=>{});
}
const card = await page.waitForSelector(`a:has-text("${pizzaName}")`, { timeout: 20000 });
await card.click();
}
async function chooseSizeAndCrust(page, size, crust) {
const sizeCode = (size || 'L').toUpperCase();
const sizeLabels = { 'S':'S', 'M':'M', 'L':'L', 'XL':'XL' };
const sizeText = sizeLabels[sizeCode] || 'L';
const sizeBtn = await page.$(`button:has-text("${sizeText}")`);
if (sizeBtn) await sizeBtn.click().catch(()=>{});
if (crust) {
const crustBtns = await page.$$('button');
for (const b of crustBtns) {
const t = (await b.textContent() || '').toLowerCase();
if (t.includes(crust.toLowerCase())) { await b.click().catch(()=>{}); break; }
}
}
}
async function applyToppings(page, add=[], remove=[]) {
for (const ing of add) {
const el = await page.$(`label:has-text("${ing}")`).catch(()=>null);
if (el) await el.click().catch(()=>{});
}
for (const ing of remove) {
const el = await page.$(`label:has-text("${ing}")`).catch(()=>null);
if (el) await el.click().catch(()=>{}); // toggle off
}
}
async function addCurrentToCart(page) {
const addSelectors = ['button:has-text("Lisa korvi")', 'button:has-text("Add to cart")', 'button:has-text("Add")'];
for (const sel of addSelectors) { const btn = await page.$(sel); if (btn) { await btn.click().catch(()=>{}); await page.waitForTimeout(800); return; } }
throw new Error('Add to cart button not found');
}
async function openCart(page) {
const cartSelectors = ['a:has-text("Ostukorv")', 'a[aria-label*="Cart" i]', 'a[href*="cart" i]', 'button[aria-label*="Cart" i]'];
for (const sel of cartSelectors) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); await page.waitForLoadState('networkidle').catch(()=>{}); return; } }
}
async function setFulfilment(page, { delivery, pickup, address, store }) {
if (pickup) {
const pickupBtns = ['button:has-text("Kaasa")', 'button:has-text("Pickup")'];
for (const sel of pickupBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (store) {
const storeOpeners = ['button:has-text("Vali restoran")', 'button:has-text("Choose store")'];
for (const sel of storeOpeners) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const storeBtn = await page.$(`button:has-text("${store}")`).catch(()=>null);
if (storeBtn) await storeBtn.click().catch(()=>{});
}
} else if (delivery) {
const delBtns = ['button:has-text("Kojutoomine")', 'button:has-text("Delivery")'];
for (const sel of delBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (address) {
const addrInput = await page.$('input[name*="address" i], input[placeholder*="Aadress" i], input[placeholder*="Address" i]').catch(()=>null);
if (addrInput) {
await addrInput.fill(address);
await page.keyboard.press('Enter');
await page.waitForTimeout(1500);
}
}
}
}
async function applyCoupons(page, coupons=[]) {
if (!coupons?.length) return;
const openers = ['button:has-text("Sisesta sooduskood")', 'button:has-text("Enter promo code")'];
for (const sel of openers) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
for (const code of coupons) {
const input = await page.$('input[placeholder*="kood" i], input[placeholder*="code" i]').catch(()=>null);
const applyBtn = await page.$('button:has-text("Rakenda"), button:has-text("Apply")').catch(()=>null);
if (input) { await input.fill(code); }
if (applyBtn) { await applyBtn.click().catch(()=>{}); }
await page.waitForTimeout(1000);
}
}
// ---------------- Half & Half flow ----------------
async function buildHalfAndHalf(page, lang, city, hh) {
// Navigate to the official Half & Half builder page
await navigateToHalfAndHalf(page, lang, city);
// Choose left and right flavours by name
const leftOpeners = ['button:has-text("Vali vasak pool")', 'button:has-text("Choose left")'];
for (const sel of leftOpeners) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const leftCard = await page.$(`button:has-text("${hh.left}") , a:has-text("${hh.left}")`).catch(()=>null);
if (leftCard) await leftCard.click().catch(()=>{});
const rightOpeners = ['button:has-text("Vali parem pool")', 'button:has-text("Choose right")'];
for (const sel of rightOpeners) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const rightCard = await page.$(`button:has-text("${hh.right}") , a:has-text("${hh.right}")`).catch(()=>null);
if (rightCard) await rightCard.click().catch(()=>{});
// Choose size & crust
await chooseSizeAndCrust(page, hh.size || 'L', hh.crust || '');
// Add to cart
await addCurrentToCart(page);
}
async function maybeSchedule(page, schedule) {
if (!schedule) return;
const timeBtns = ['button:has-text("Ajasta")', 'button:has-text("Schedule")'];
for (const sel of timeBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
if (schedule.time) {
const picker = await page.$('input[type="time"], input[name*="time" i]').catch(()=>null);
if (picker) { await picker.fill(schedule.time).catch(()=>{}); }
}
}
async function doOrder(opts) {
const browser = await chromium.launch({ headless: false });
const startUrl = baseUrl(opts.lang, opts.city);
await ensureAuth(browser, startUrl);
const { ctx, page } = await withContext(browser, AUTH_FILE);
await page.goto(startUrl, { waitUntil: 'networkidle' });
await acceptCookies(page);
// Half & Half first (if any), then single-flavour items
if (opts.half_and_half && opts.half_and_half.left && opts.half_and_half.right) {
const hh = opts.half_and_half;
console.log(`Adding Half & Half: ${hh.left} | ${hh.right} (${hh.size||'L'}${hh.crust?','+hh.crust:''})`);
await buildHalfAndHalf(page, opts.lang, opts.city, hh);
}
for (const spec of opts.items || []) {
const parsed = typeof spec === 'string' ? (()=>{ const [name,size,crust] = spec.split(':'); return {name, size, crust}; })() : spec;
const name = parsed.name; const size = parsed.size || 'L'; const crust = parsed.crust || '';
const add = (opts.addMap?.[name] || parsed.add || []);
const remove = (opts.rmMap?.[name] || parsed.remove || []);
console.log(`Adding ${name} (${size}${crust?','+crust:''})`);
await navigateToMenu(page, opts.lang, opts.city);
await searchAndOpenPizza(page, name);
await chooseSizeAndCrust(page, size, crust);
await applyToppings(page, add, remove);
await addCurrentToCart(page);
}
await openCart(page);
await setFulfilment(page, opts);
if (opts.note) {
const noteArea = await page.$('textarea[placeholder*="märkused" i], textarea[placeholder*="notes" i]').catch(()=>null);
if (noteArea) await noteArea.fill(opts.note).catch(()=>{});
}
await applyCoupons(page, opts.coupons);
await maybeSchedule(page, opts.schedule);
console.log('At checkout — review & complete payment manually.');
// Leave browser open for manual completion
}
async function doReorder(opts) {
const browser = await chromium.launch({ headless: false });
await ensureAuth(browser, baseUrl(opts.lang, opts.city));
const { page } = await withContext(browser, AUTH_FILE);
await page.goto(baseUrl(opts.lang, opts.city), { waitUntil: 'domcontentloaded' });
await acceptCookies(page);
const profileBtns = ['a:has-text("Minu konto")','a:has-text("My account")','button:has-text("Konto")'];
for (const sel of profileBtns) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const ordersLinks = ['a:has-text("Tellimused")','a:has-text("Orders")'];
for (const sel of ordersLinks) { const el = await page.$(sel); if (el) { await el.click().catch(()=>{}); break; } }
const reorderBtn = await page.$('button:has-text("Telli uuesti"), button:has-text("Reorder")').catch(()=>null);
if (reorderBtn) {
await reorderBtn.click().catch(()=>{});
await openCart(page);
await setFulfilment(page, opts);
await applyCoupons(page, opts.coupons);
console.log('Reorder loaded to cart. Complete manually.');
} else {
console.warn('Could not find a Reorder button. You may need to open a specific past order manually.');
}
}
async function main() {
const { cmd, opts } = parseArgs();
if (cmd === 'help' || cmd === '--help' || cmd === '-h' || !['init','order','reorder'].includes(cmd)) {
console.log(`\nDomino's Estonia Advanced CLI (Playwright)\nCommands:\n init\n Open site and let you log in; saves cookies to ${AUTH_FILE}.\n order [--config prefs.json] [--delivery|--pickup] [--store NAME] \\\n [--address "ADDR"] [--items "Name:Size:Crust" ...] \\\n [--coupon CODE] [--lang et|en] [--city tallinn]\n (Supports \"half_and_half\" in JSON config)\n reorder [--latest] [--coupon CODE] [--lang et|en] [--city tallinn]\n`);
process.exit(0);
}
const browser = await chromium.launch({ headless: false });
if (cmd === 'init') {
await ensureAuth(browser, baseUrl(opts.lang, opts.city));
await browser.close();
return;
}
await browser.close(); // isolate contexts in flows
if (cmd === 'order') {
await doOrder(opts);
return;
}
if (cmd === 'reorder') {
await doReorder(opts);
return;
}
}
main().catch(err => { console.error(err); process.exit(1); });
Täiustatud skript, et Half & Half (pool-pool) töötab nüüd päriselt:
- lisatud buildHalfAndHalf() voog, mis avab ametliku Half & Half ehitaja lehe ja valib vasakule/paremale maitse, suuruse ja kooriku, siis lisab korvi;
- order käsul loetakse half_and_half blokk sinu JSON-ist ja lisatakse enne üksikuid pitsasid;
- senised funktsioonid (kupongid, pickup/delivery, ajastus) jäid alles.
Kuidas kasutada Half & Half’i
- avada kanvases fail dominos-prefs-mustakivi.json (või dominos-prefs.json) ja jäta alles half_and_half blokk, nt:
"half_and_half": {
"size": "XL",
"crust": "thin",
"left": "Carbonara",
"right": "Hypnotica",
"note": "Half & Half – pool-pool"
}
- käivitada
node dominos-cli-playwright.mjs order --config dominos-prefs-mustakivi.json
Käskude kombineerimine
Veelgi põnevam on see, et kui kõik varem vaadatud programmid on paigaldatud, saab nad omavahel kombineerida! Vaatame mõned näited:
Figlet ja lolcat
Lolcat ja cowsay
Cowsay, fortune ja lolcat
fortune | cowsay | lolcat
Star Wars
Käsureal (terminalis, avaneb Linuxis enamasti ka CTRL+ALT+T abil) sisestada:
telnet starwarstel.net
telnet telehack.com
Viimases (telehack.com) tuleb eraldi käivitada käsk starwars ja valikus on ka palju teisi käske. Välja saab logida CTRL+D, exit+Enter abil. Mistahes käsu katkestamiseks CTRL+C.
Võimalus veebis vaadata, uued episoodid ja ka teised projektid - https://www.asciimation.co.nz/
Operatsioonisüsteemi logo ja teave terminalis
neofetch
Võimalusterohke Neofetch suudab näidata operatsioonisüsteemi teavet ja logo ning ASCII faili käsureal:
sudo apt install neofetch
käivitamine terminalis:
neofetch
screenfetch
Screenfetch on teine sarnane rakendus
sudo apt install screenfetch
käivitamine terminalis:
screenfetch
linuxlogo
Veel on linuxlogo
sudo apt install linuxlogo
Käivitamine
linuxlogo
Tulemus
Veel sarnaseid rakendusi
- hyfetch, neowofetch, fastfetch, cpufetch
- loetelu erinevatest sarnastest fetch-programmidest
- veebilehitseja teabe vaatamiseks webfetch
- Archey 4
Pühad
Jõulud
kuusk käsureal
curl https://raw.githubusercontent.com/sergiolepore/ChristBASHTree/master/tree-EN.sh | bash
Teine võimalus - ctree. Selle artikli lõpus veel palju viiteid erinevatele lõbusatele käskudele Linuxis.
Kolmas võimalus
Paigaldamine
wget -d -c -O "christmas.sh" https://gist.githubusercontent.com/ostechnix/66cecb6bbb9e35a492bccb9c6ecc5d9d/raw/1c281f20f9594297fd2a59197a3aff92db66f856/bashmastree.sh
Käivitamine
bash christmas.sh
Tulemus
Neljas võimalus
Python'is kirjutatud kuusepuu.
Paigaldamine
wget -q https://github.com/chicolucio/terminal-christmas-tree/raw/refs/heads/master/terminal_tree.py
Käivitamine
python terminal_tree.py
või ka
python3 terminal_tree.py
Tulemus
Järgmine, keerulisem variant.
Paigaldamine
perl -MCPAN -e 'install Acme::POE::Tree'
Käivitamine
perl -MAcme::POE::Tree -e 'Acme::POE::Tree->new()->run()'
sajab lund
Terminalis sajab lund.
Vajadusel paigaldada rakendused gawk, pv (sudo apt install gawk pv)
clear;while :;do echo $LINES $COLUMNS $(($RANDOM%$COLUMNS));sleep 0.1;done|gawk '{a[$3]=0;for(x in a) {o=a[x];a[x]=a[x]+1;printf "\033[%s;%sH ",o,x;printf "\033[%s;%sH*\033[0;0H",a[x],x;}}'
clear;while :;do echo $LINES $COLUMNS $(($RANDOM%$COLUMNS)) $(printf "\u2744\n");sleep 0.1;done|gawk '{a[$3]=0;for(x in a) {o=a[x];a[x]=a[x]+1;printf "\033[%s;%sH ",o,x;printf "\033[%s;%sH%s \033[0;0H",a[x],x,$4;}}'
yes $COLUMNS $LINES|pv -qL50|perl -ne'$|=1;($c,$r)=split;$s||=$"x($c*$r);print$s;$s=$"x$c.$s;substr$s,rand$c,1,"*";$s=substr$s,0,$c*$r+$c;'
for((I=0;J=--I;))do clear;for((D=LINES;S=++J**3%COLUMNS,--D;))do printf %*s.\\n $S;done;sleep 1;done
Tulemus
graafiline: sajab lund
Paigaldamine
sudo apt install xsnow
Käivitamine
xsnow
Tulemus
Lisalugemist
- https://trendoceans.com/snowfall-on-your-linux-desktop-this-christmas/
- https://fostips.com/xsnow-animated-snow-falling-desktop-linux/
Kokkuvõte
Antud artiklis uurisime käske, mida on põnev ise läbi proovida. Kuid on olemas veel selliseid, mille abil saab teiste kasutajate üle nalja teha. Näiteks bash-insulter'i abil, mis peale paigaldamist ehmatab kasutajat juhul, kui sisestatud parool või käsk polnud õige.
Artikli autor usub, et elus kõik peab olema tasakaalus. Põhiliste käskude teadmine Linux terminali jaoks on väga vajalik, kuid vahest peab ennast ümberlülitama ja lahutama meelt. Vahendid jäävad küll samaks, aga tulemus on palju lõbusam.
Peale töö lõpetamist on alati soovituslik tühjendada APT'i puhvri käsuga:
sudo apt-get clean
Kasutatud kirjandus
- http://smashingtips.com/linux/cool-terminal-commands-for-linux
- http://www.binarytides.com/linux-fun-commands/
- https://www.tecmint.com/lolcat-command-to-output-rainbow-of-colors-in-linux-terminal/
- https://www.tecmint.com/linux-funny-commands/
- https://www.linuxtechi.com/12-interesting-linux-commands-make-you-laugh/
- https://www.cyberciti.biz/howto/insult-linux-unix-bash-user-when-typing-wrong-command/
- https://itsfoss.com/christmas-linux-wallpaper/
- https://www.cyberciti.biz/open-source/command-line-hacks/linux-unix-desktop-fun-christmas-tree-for-your-terminal/
- https://www.wikihow.com/Watch-Star-Wars-on-Command-Prompt
- https://itsfoss.com/display-linux-logo-in-ascii/
- https://itsfoss.com/funny-linux-commands/
- https://itsfoss.com/ascii-art-linux-terminal/
- https://www.binarytides.com/linux-fun-commands/




























