hh resume booster: free auto-bump for hh.ru (Playwright, Linux/Win/Mac)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# НИКОГДА не коммитьте и не пересылайте сессию — это полный доступ к аккаунту hh!
|
||||||
|
session/
|
||||||
|
config.json
|
||||||
|
*.log
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# hh resume booster
|
||||||
|
|
||||||
|
Бесплатное автоматическое «поднятие» резюме в поиске hh.ru — по расписанию, на Linux, Windows и macOS.
|
||||||
|
|
||||||
|
## Как это работает
|
||||||
|
|
||||||
|
На hh **любое сохранение резюме заново поднимает его в выдаче поиска** — причём
|
||||||
|
**без 4-часового кулдауна**, который есть у ручной кнопки «Поднять в поиске».
|
||||||
|
Скрипт вносит невидимую правку в блок **«О себе»** (тумблерит один двойной пробел
|
||||||
|
в середине текста — в вёрстке страницы он схлопывается, так что глазами он не виден
|
||||||
|
и содержимое резюме не меняется) и сохраняет. Это бесплатный аналог платного
|
||||||
|
сервиса «Автоподнятие».
|
||||||
|
|
||||||
|
Резюме на вашем аккаунте определяются **автоматически**. Планировщик ОС гоняет
|
||||||
|
скрипт каждые 2 часа.
|
||||||
|
|
||||||
|
> ⚠️ **Требование:** у резюме должен быть непустой блок «О себе» (иначе прятать
|
||||||
|
> правку негде — такое резюме скрипт пропустит и напишет об этом в лог).
|
||||||
|
|
||||||
|
## 🔐 Безопасность — прочитайте
|
||||||
|
|
||||||
|
Файл **`session/storage.json`** — это ваши cookie-сессии hh, то есть **полный доступ
|
||||||
|
к аккаунту**. Никогда не выкладывайте его в git и не пересылайте архив ПОСЛЕ логина.
|
||||||
|
В поставке этого файла нет — вы создаёте его локально сам через `login.py`.
|
||||||
|
`.gitignore` уже исключает `session/`, `config.json` и логи.
|
||||||
|
|
||||||
|
Автоматизация браузером — «серая зона» правил hh (ToS). Риск низкий (вы правите
|
||||||
|
своё же резюме), но частить не стоит: 2 часа — норм, каждые 5 минут — палево.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Требования
|
||||||
|
|
||||||
|
- **Python 3.9+**
|
||||||
|
- Интернет и аккаунт соискателя на hh.ru
|
||||||
|
- ~400 МБ на браузер Playwright (chromium)
|
||||||
|
|
||||||
|
Проверить питон: `python3 --version` (на Windows — `python --version`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Установка (суть одна, команды под каждую ОС ниже)
|
||||||
|
|
||||||
|
Распакуйте архив, зайдите в папку `hh-resume-booster`, создайте виртуальное
|
||||||
|
окружение, поставьте зависимости и браузер.
|
||||||
|
|
||||||
|
### 🐧 Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hh-resume-booster
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
playwright install --with-deps chromium # --with-deps ставит системные библиотеки (нужен sudo)
|
||||||
|
```
|
||||||
|
Если `--with-deps` ругается на права — выполните `sudo $(which playwright) install-deps`
|
||||||
|
один раз, затем `playwright install chromium`.
|
||||||
|
|
||||||
|
### 🪟 Windows (PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd hh-resume-booster
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
pip install -r requirements.txt
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
Если PowerShell не даёт активировать окружение — запустите один раз:
|
||||||
|
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
|
||||||
|
|
||||||
|
### 🍎 macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hh-resume-booster
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
(Если нет питона: `brew install python`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Логин (создаём сессию)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python login.py # Windows: python login.py | Linux/mac: python login.py
|
||||||
|
```
|
||||||
|
Откроется настоящий браузер. **Войдите в hh.ru** любым способом (почта/пароль, код
|
||||||
|
из письма/смс, госуслуги). Скрипт сам поймает вход и сохранит `session/storage.json`,
|
||||||
|
после чего окно можно закрыть. Появится `✅ Готово`.
|
||||||
|
|
||||||
|
> **Сервер без экрана (headless Linux)?** `login.py` требует графику. Запустите его
|
||||||
|
> на своём компьютере, затем скопируйте получившийся `session/storage.json` на сервер
|
||||||
|
> в такую же папку `session/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Пробный прогон вручную
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bump_resumes.py
|
||||||
|
```
|
||||||
|
Ожидаемый вывод — по строке на резюме, например:
|
||||||
|
```
|
||||||
|
[2026-08-24 20:08] 30ee2ebd…: about-added-ok | поднято✓ (next-manual=00:08)
|
||||||
|
```
|
||||||
|
`next-manual=HH:MM` = метка «Можно сегодня в …» на странице резюме; она равна
|
||||||
|
времени_правки + 4ч и **двигается при каждом прогоне** — это и есть подтверждение,
|
||||||
|
что резюме подняли. Всё пишется в `bump.log`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Автозапуск каждые 2 часа
|
||||||
|
|
||||||
|
### 🍎 macOS — launchd
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scheduler/install-mac.sh
|
||||||
|
```
|
||||||
|
- Статус: `launchctl list | grep hhbump`
|
||||||
|
- Остановить: `launchctl bootout gui/$(id -u)/com.stackops.hhbump`
|
||||||
|
|
||||||
|
### 🐧 Linux — systemd (user timer)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scheduler/install-linux.sh
|
||||||
|
```
|
||||||
|
- Статус: `systemctl --user list-timers | grep hhbump`
|
||||||
|
- Разовый прогон: `systemctl --user start hhbump.service`
|
||||||
|
- Остановить: `systemctl --user disable --now hhbump.timer`
|
||||||
|
|
||||||
|
<details><summary>Нет systemd? Вариант через cron</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
# добавить строку (замените /ПОЛНЫЙ/ПУТЬ):
|
||||||
|
0 */2 * * * /ПОЛНЫЙ/ПУТЬ/hh-resume-booster/scheduler/run.sh >> /ПОЛНЫЙ/ПУТЬ/hh-resume-booster/bump.cron.log 2>&1
|
||||||
|
```
|
||||||
|
(сделайте `chmod +x scheduler/run.sh`)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 🪟 Windows — Планировщик заданий
|
||||||
|
|
||||||
|
В PowerShell **из папки пакета**:
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scheduler\install-windows.ps1
|
||||||
|
```
|
||||||
|
- Статус: `schtasks /Query /TN hhbump`
|
||||||
|
- Разовый прогон: `schtasks /Run /TN hhbump`
|
||||||
|
- Удалить: `schtasks /Delete /TN hhbump /F`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Настройка (необязательно)
|
||||||
|
|
||||||
|
Скопируйте `config.example.json` → `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resume_ids": ["30ee2ebd...", "fb175217..."],
|
||||||
|
"headless": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `resume_ids` — если хотите поднимать только конкретные резюме (иначе — все на аккаунте).
|
||||||
|
RID виден в URL резюме: `hh.ru/resume/ЭТОТ_ID`.
|
||||||
|
- `headless: false` — показывать браузер во время прогона (для отладки).
|
||||||
|
|
||||||
|
**Сменить частоту:** поменяйте `StartInterval` (секунды) в plist / `OnUnitActiveSec`
|
||||||
|
в `hhbump.timer` / интервал в задаче Windows и переустановите соответствующим
|
||||||
|
`install-*` скриптом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Диагностика
|
||||||
|
|
||||||
|
| Симптом в `bump.log` | Что делать |
|
||||||
|
|---|---|
|
||||||
|
| `STOP: нет session/storage.json` | запустите `python login.py` |
|
||||||
|
| `STOP: сессия мертва (hhrole=...)` | сессия протухла (живёт ~30 дней) → `python login.py` заново |
|
||||||
|
| `empty-about-skip` | добавьте текст в блок «О себе» этого резюме |
|
||||||
|
| `no-about-field` / `ERR` | hh поменяли вёрстку или таймаут — проверьте вручную |
|
||||||
|
|
||||||
|
**Важно:** прогон случается, только когда компьютер **не спит**. launchd/systemd
|
||||||
|
схлопывают пропуски в один прогон при пробуждении (свежак к утру — это ок).
|
||||||
|
Нужен режим 24/7 без пропусков — держите машину бодрой или поставьте на VPS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Состав пакета
|
||||||
|
|
||||||
|
```
|
||||||
|
hh-resume-booster/
|
||||||
|
├── bump_resumes.py # сам бустер (автоопределение резюме)
|
||||||
|
├── login.py # логин в hh -> session/storage.json
|
||||||
|
├── requirements.txt
|
||||||
|
├── config.example.json
|
||||||
|
├── README.md
|
||||||
|
├── session/ # сюда ляжет storage.json (в архиве пусто)
|
||||||
|
└── scheduler/
|
||||||
|
├── run.sh / run.bat # обёртки для планировщиков
|
||||||
|
├── install-mac.sh # launchd
|
||||||
|
├── install-linux.sh # systemd user-timer
|
||||||
|
├── install-windows.ps1 # Task Scheduler
|
||||||
|
├── com.stackops.hhbump.plist # шаблон launchd
|
||||||
|
├── hhbump.service / .timer # шаблоны systemd
|
||||||
|
```
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
hh resume booster — бесплатное «поднятие» резюме в выдаче hh.ru.
|
||||||
|
|
||||||
|
Как это работает: на hh ЛЮБОЕ сохранение резюме заново поднимает его в поиске,
|
||||||
|
причём БЕЗ 4-часового кулдауна ручной кнопки «Поднять в поиске». Скрипт вносит
|
||||||
|
невидимую правку в блок «О себе» (тумблерит один двойной пробел в середине текста —
|
||||||
|
в вёрстке он схлопывается, глазами не виден) и сохраняет. Это бесплатный аналог
|
||||||
|
платного «Автоподнятия».
|
||||||
|
|
||||||
|
Резюме определяются автоматически из вашей сессии (можно переопределить в config.json).
|
||||||
|
Запускать по расписанию (launchd / systemd / Task Scheduler) — см. README.md.
|
||||||
|
|
||||||
|
Требуется: session/storage.json (создаётся через `python login.py`).
|
||||||
|
"""
|
||||||
|
import asyncio, json, re, datetime, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
BASE = Path(__file__).resolve().parent
|
||||||
|
SESSION = BASE / "session" / "storage.json"
|
||||||
|
CONFIG = BASE / "config.json"
|
||||||
|
LOG = BASE / "bump.log"
|
||||||
|
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
def cfg():
|
||||||
|
if CONFIG.exists():
|
||||||
|
try: return json.loads(CONFIG.read_text(encoding="utf-8"))
|
||||||
|
except Exception: pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def toggle_space(text):
|
||||||
|
"""Невидимый mid-text тумблер: убрать первый двойной пробел ИЛИ добавить его
|
||||||
|
после первого пробела за 15-м символом (не старт и не конец текста)."""
|
||||||
|
if " " in text:
|
||||||
|
return text.replace(" ", " ", 1), "removed"
|
||||||
|
m = re.search(r'\S \S', text[15:])
|
||||||
|
if m:
|
||||||
|
pos = 15 + m.start() + 1
|
||||||
|
return text[:pos] + " " + text[pos:], "added"
|
||||||
|
return None, "empty-skip" # пустой/слишком короткий «О себе» — не трогаем
|
||||||
|
|
||||||
|
async def discover_rids(pg):
|
||||||
|
await pg.goto("https://hh.ru/applicant/resumes", wait_until="domcontentloaded", timeout=60000)
|
||||||
|
await pg.wait_for_timeout(2500)
|
||||||
|
html = await pg.content()
|
||||||
|
# RID = 38 hex; /resume/edit/... не матчится (после /resume/ идёт 'edit')
|
||||||
|
return list(dict.fromkeys(re.findall(r'/resume/([0-9a-f]{30,})', html)))
|
||||||
|
|
||||||
|
async def boost_one(pg, rid):
|
||||||
|
# 1) правка «О себе» = поднятие
|
||||||
|
await pg.goto(f"https://hh.ru/resume/edit/{rid}/about", wait_until="domcontentloaded", timeout=60000)
|
||||||
|
await pg.wait_for_timeout(2200)
|
||||||
|
ta = pg.locator('[data-qa="resume-editor-about"], textarea[data-qa*="about"], textarea').first
|
||||||
|
if await ta.count() == 0:
|
||||||
|
return "no-about-field"
|
||||||
|
cur = await ta.input_value()
|
||||||
|
new, how = toggle_space(cur)
|
||||||
|
if new is None:
|
||||||
|
return "empty-about-skip (добавьте текст в «О себе»)"
|
||||||
|
await ta.click(); await ta.fill(new); await pg.wait_for_timeout(500)
|
||||||
|
saved = False
|
||||||
|
for sel in ['[data-qa="resume-partial-edit-save"]', 'button:has-text("Сохранить")']:
|
||||||
|
l = pg.locator(sel).first
|
||||||
|
if await l.count() > 0 and await l.is_visible():
|
||||||
|
await l.click(); saved = True; break
|
||||||
|
await pg.wait_for_timeout(3000)
|
||||||
|
ok = saved and f"/resume/{rid}" in pg.url and "edit" not in pg.url
|
||||||
|
# 2) подтверждение: метка «Можно сегодня в HH:MM» = время правки + 4ч (двигается => подняли)
|
||||||
|
await pg.goto(f"https://hh.ru/resume/{rid}", wait_until="domcontentloaded", timeout=60000)
|
||||||
|
await pg.wait_for_timeout(2000)
|
||||||
|
body = await pg.inner_text("body")
|
||||||
|
m = re.search(r'[Мм]ожно[^0-9]{0,25}(\d\d:\d\d)', body)
|
||||||
|
tag = f"next-manual={m.group(1)}" if m else "no-timer-label"
|
||||||
|
return f"about-{how}-{'ok' if ok else '?'} | поднято✓ ({tag})"
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
if not SESSION.exists():
|
||||||
|
print("STOP: нет session/storage.json — сначала запустите `python login.py`")
|
||||||
|
sys.exit(2)
|
||||||
|
conf = cfg()
|
||||||
|
ss = json.loads(SESSION.read_text(encoding="utf-8"))
|
||||||
|
out = []
|
||||||
|
async with async_playwright() as p:
|
||||||
|
b = await p.chromium.launch(headless=conf.get("headless", True), args=["--no-sandbox"])
|
||||||
|
ctx = await b.new_context(storage_state=ss, user_agent=UA, locale="ru-RU",
|
||||||
|
timezone_id="Europe/Moscow", viewport={"width": 1440, "height": 1200})
|
||||||
|
pg = await ctx.new_page()
|
||||||
|
role = {c["name"]: c["value"] for c in await ctx.cookies("https://hh.ru")}.get("hhrole")
|
||||||
|
if role != "applicant":
|
||||||
|
out.append(f"STOP: сессия мертва (hhrole={role}) — перелогиньтесь: python login.py")
|
||||||
|
else:
|
||||||
|
rids = conf.get("resume_ids") or await discover_rids(pg)
|
||||||
|
if not rids:
|
||||||
|
out.append("STOP: резюме не найдены на аккаунте")
|
||||||
|
for rid in rids:
|
||||||
|
try:
|
||||||
|
r = await boost_one(pg, rid)
|
||||||
|
out.append(f"{rid[:8]}…: {r}")
|
||||||
|
except Exception as e:
|
||||||
|
out.append(f"{rid[:8]}…: ERR {str(e)[:70]}")
|
||||||
|
await b.close()
|
||||||
|
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
line = f"[{ts}] " + " || ".join(out)
|
||||||
|
print(line)
|
||||||
|
try:
|
||||||
|
with LOG.open("a", encoding="utf-8") as fh:
|
||||||
|
fh.write(line + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Скопируйте в config.json, если нужно переопределить. Всё опционально.",
|
||||||
|
"resume_ids": [],
|
||||||
|
"headless": true
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Логин в hh.ru → сохраняет сессию в session/storage.json.
|
||||||
|
|
||||||
|
Открывает НАСТОЯЩИЙ браузер (не headless). Войдите в свой аккаунт hh любым способом
|
||||||
|
(почта/пароль, код из смс/письма, госуслуги — как обычно). Скрипт сам поймает момент
|
||||||
|
входа по cookie `hhrole=applicant` и сохранит сессию. После этого можно закрывать.
|
||||||
|
|
||||||
|
На сервере без экрана — запустите login.py на своём компьютере, затем скопируйте
|
||||||
|
файл session/storage.json на сервер (см. README).
|
||||||
|
"""
|
||||||
|
import asyncio, json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
BASE = Path(__file__).resolve().parent
|
||||||
|
SESSION = BASE / "session" / "storage.json"
|
||||||
|
SESSION.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("Открываю браузер… войдите в hh.ru в открывшемся окне.")
|
||||||
|
print("Как только вход выполнен — жду до 5 минут и сохраняю сессию автоматически.")
|
||||||
|
async with async_playwright() as p:
|
||||||
|
b = await p.chromium.launch(headless=False)
|
||||||
|
ctx = await b.new_context(locale="ru-RU", timezone_id="Europe/Moscow")
|
||||||
|
pg = await ctx.new_page()
|
||||||
|
await pg.goto("https://hh.ru/account/login", wait_until="domcontentloaded", timeout=60000)
|
||||||
|
ok = False
|
||||||
|
for _ in range(150): # 150 * 2s = 5 минут
|
||||||
|
await pg.wait_for_timeout(2000)
|
||||||
|
try:
|
||||||
|
role = {c["name"]: c["value"] for c in await ctx.cookies("https://hh.ru")}.get("hhrole")
|
||||||
|
except Exception:
|
||||||
|
role = None
|
||||||
|
if role == "applicant":
|
||||||
|
ok = True
|
||||||
|
break
|
||||||
|
if ok:
|
||||||
|
await ctx.storage_state(path=str(SESSION))
|
||||||
|
print(f"\n✅ Готово. Сессия сохранена: {SESSION}")
|
||||||
|
print(" Теперь можно ставить на расписание (см. README.md).")
|
||||||
|
else:
|
||||||
|
print("\n❌ Не увидел вход (cookie hhrole=applicant) за 5 минут. Запустите ещё раз.")
|
||||||
|
await b.close()
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
playwright>=1.40
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key> <string>com.stackops.hhbump</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>__DIR__/.venv/bin/python</string>
|
||||||
|
<string>__DIR__/bump_resumes.py</string>
|
||||||
|
</array>
|
||||||
|
<key>StartInterval</key> <integer>7200</integer>
|
||||||
|
<key>RunAtLoad</key> <true/>
|
||||||
|
<key>StandardOutPath</key> <string>__DIR__/bump.out.log</string>
|
||||||
|
<key>StandardErrorPath</key><string>__DIR__/bump.err.log</string>
|
||||||
|
<key>ProcessType</key> <string>Background</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=hh resume booster (одноразовый прогон)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=__DIR__
|
||||||
|
ExecStart=__DIR__/.venv/bin/python __DIR__/bump_resumes.py
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Запуск hh resume booster каждые 2 часа
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=2h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Linux: ставит systemd user-timer (каждые 2ч) с автоподстановкой путей.
|
||||||
|
set -e
|
||||||
|
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
mkdir -p "$HOME/.config/systemd/user"
|
||||||
|
sed "s#__DIR__#$DIR#g" "$DIR/scheduler/hhbump.service" > "$HOME/.config/systemd/user/hhbump.service"
|
||||||
|
cp "$DIR/scheduler/hhbump.timer" "$HOME/.config/systemd/user/hhbump.timer"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now hhbump.timer
|
||||||
|
# чтобы таймер работал и когда вы не залогинены в графическую сессию:
|
||||||
|
loginctl enable-linger "$USER" 2>/dev/null || true
|
||||||
|
echo "✅ Установлено. Проверка: systemctl --user list-timers | grep hhbump"
|
||||||
|
echo " Разовый прогон сейчас: systemctl --user start hhbump.service"
|
||||||
|
echo " Остановить: systemctl --user disable --now hhbump.timer"
|
||||||
|
echo " Логи: $DIR/bump.log (и: journalctl --user -u hhbump.service)"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# macOS: ставит launchd-агент (каждые 2ч) с автоподстановкой путей.
|
||||||
|
set -e
|
||||||
|
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PLIST="$HOME/Library/LaunchAgents/com.stackops.hhbump.plist"
|
||||||
|
mkdir -p "$HOME/Library/LaunchAgents"
|
||||||
|
sed "s#__DIR__#$DIR#g" "$DIR/scheduler/com.stackops.hhbump.plist" > "$PLIST"
|
||||||
|
launchctl bootout "gui/$(id -u)/com.stackops.hhbump" 2>/dev/null || true
|
||||||
|
launchctl bootstrap "gui/$(id -u)" "$PLIST"
|
||||||
|
echo "✅ Установлено. Проверка: launchctl list | grep hhbump"
|
||||||
|
echo " Остановить: launchctl bootout gui/\$(id -u)/com.stackops.hhbump"
|
||||||
|
echo " Логи: $DIR/bump.log"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Windows: регистрирует задачу в Планировщике (каждые 2 часа).
|
||||||
|
# Запуск: правой кнопкой -> "Выполнить с помощью PowerShell", ИЛИ в PowerShell:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scheduler\install-windows.ps1
|
||||||
|
$Root = (Resolve-Path "$PSScriptRoot\..").Path
|
||||||
|
$Bat = Join-Path $Root "scheduler\run.bat"
|
||||||
|
$Action = New-ScheduledTaskAction -Execute $Bat
|
||||||
|
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
|
||||||
|
-RepetitionInterval (New-TimeSpan -Hours 2) -RepetitionDuration ([TimeSpan]::MaxValue)
|
||||||
|
$Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries
|
||||||
|
Register-ScheduledTask -TaskName "hhbump" -Action $Action -Trigger $Trigger `
|
||||||
|
-Settings $Settings -Description "hh resume booster" -Force
|
||||||
|
Write-Host "OK. Проверка: schtasks /Query /TN hhbump"
|
||||||
|
Write-Host "Разовый прогон: schtasks /Run /TN hhbump"
|
||||||
|
Write-Host "Удалить: schtasks /Delete /TN hhbump /F"
|
||||||
|
Write-Host "Логи: $Root\bump.log"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
REM Обёртка для Планировщика заданий Windows.
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
".venv\Scripts\python.exe" bump_resumes.py
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Обёртка для планировщиков (launchd/cron/systemd) — идёт в корень пакета и запускает бустер.
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
exec ./.venv/bin/python bump_resumes.py
|
||||||
Reference in New Issue
Block a user