socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Sequential execution queue — one browser, one tab at a time.
|
||||
|
||||
Every platform flow drives abang's real browser window. Two flows at once
|
||||
would fight over focus, file inputs and dialogs, so everything funnels through
|
||||
one worker task here.
|
||||
|
||||
q = PostQueue(inter_platform_delay=(3.0, 6.0))
|
||||
await q.start()
|
||||
result = await q.submit("threads", coro_factory)
|
||||
|
||||
`submit` returns that job's result (or raises what the job raised). Jobs run
|
||||
strictly in submission order.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
class PostQueue:
|
||||
def __init__(self, inter_platform_delay=(3.0, 6.0)):
|
||||
lo, hi = inter_platform_delay
|
||||
if lo < 0 or hi < lo:
|
||||
raise ValueError(
|
||||
f"inter_platform_delay must be (lo, hi) with 0 <= lo <= hi, got {lo, hi}"
|
||||
)
|
||||
self.delay_lo, self.delay_hi = lo, hi
|
||||
self._queue = asyncio.Queue()
|
||||
self._worker = None
|
||||
self._last_finished = None
|
||||
self.history = [] # (iso_time, label, status)
|
||||
|
||||
async def start(self):
|
||||
if self._worker is None:
|
||||
self._worker = asyncio.create_task(self._run())
|
||||
return self
|
||||
|
||||
async def stop(self):
|
||||
if self._worker:
|
||||
self._worker.cancel()
|
||||
try:
|
||||
await self._worker
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._worker = None
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._queue.qsize()
|
||||
|
||||
async def submit(self, label, factory):
|
||||
"""Queue `factory()` (a zero-arg coroutine function) and await its result."""
|
||||
if self._worker is None:
|
||||
raise RuntimeError("PostQueue.submit() before start()")
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
await self._queue.put((label, factory, future))
|
||||
return await future
|
||||
|
||||
async def _run(self):
|
||||
while True:
|
||||
label, factory, future = await self._queue.get()
|
||||
await self._space_out()
|
||||
try:
|
||||
result = await factory()
|
||||
if not future.done():
|
||||
future.set_result(result)
|
||||
status = result.get("status") if isinstance(result, dict) else "ok"
|
||||
except asyncio.CancelledError:
|
||||
if not future.done():
|
||||
future.cancel()
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 — hand it to the caller intact
|
||||
if not future.done():
|
||||
future.set_exception(exc)
|
||||
status = f"raised {type(exc).__name__}"
|
||||
finally:
|
||||
self._last_finished = time.time()
|
||||
self._queue.task_done()
|
||||
self.history.append(
|
||||
(time.strftime("%Y-%m-%dT%H:%M:%S"), label, status)
|
||||
)
|
||||
|
||||
async def _space_out(self):
|
||||
"""Rate-limit gap between consecutive platform jobs."""
|
||||
if self._last_finished is None:
|
||||
return
|
||||
gap = random.uniform(self.delay_lo, self.delay_hi)
|
||||
elapsed = time.time() - self._last_finished
|
||||
if elapsed < gap:
|
||||
await asyncio.sleep(gap - elapsed)
|
||||
Reference in New Issue
Block a user