pooled extraction
This commit is contained in:
parent
209b34a8a7
commit
2e45ccc591
@ -8,6 +8,9 @@ from v6d3music.config import myroot
|
|||||||
from v6d3music.utils.presets import allowed_effects
|
from v6d3music.utils.presets import allowed_effects
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('effects_db', 'default_effects', 'set_default_effects', 'entries_effects_for_args',)
|
||||||
|
|
||||||
|
|
||||||
effects_db = Db(myroot / 'effects.db', kvfactory=KVJson())
|
effects_db = Db(myroot / 'effects.db', kvfactory=KVJson())
|
||||||
|
|
||||||
|
|
||||||
@ -26,6 +29,6 @@ async def set_default_effects(gid: int, effects: str | None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def entries_effects_for_args(args: list[str], gid: int) -> AsyncIterable[InfoCtx]:
|
async def entries_effects_for_args(args: list[str], gid: int) -> AsyncIterable[InfoCtx]:
|
||||||
for ctx in ArgCtx(default_effects, args, gid).sources:
|
for ctx in ArgCtx(default_effects(gid), args).sources:
|
||||||
async for it in ctx.entries():
|
async for it in ctx.entries():
|
||||||
yield it
|
yield it
|
||||||
|
117
v6d3music/core/ystate.py
Normal file
117
v6d3music/core/ystate.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
from typing import AsyncIterable, Iterable
|
||||||
|
|
||||||
|
from v6d3music.core.create_ytaudio import *
|
||||||
|
from v6d3music.core.ytaudio import *
|
||||||
|
from v6d3music.processing.pool import *
|
||||||
|
from v6d3music.utils.argctx import *
|
||||||
|
|
||||||
|
from v6d2ctx.context import Context
|
||||||
|
|
||||||
|
__all__ = ('YState',)
|
||||||
|
|
||||||
|
|
||||||
|
class YState:
|
||||||
|
def __init__(self, pool: Pool, ctx: Context, sources: Iterable[UrlCtx]) -> None:
|
||||||
|
self.pool = pool
|
||||||
|
self.ctx = ctx
|
||||||
|
self.sources: deque[UrlCtx] = deque(sources)
|
||||||
|
self.playlists: deque[asyncio.Future[list[InfoCtx]]] = deque()
|
||||||
|
self.entries: deque[InfoCtx | BaseException] = deque()
|
||||||
|
self.results: asyncio.Queue[asyncio.Future[YTAudio | None] | None] = asyncio.Queue()
|
||||||
|
self.es = AsyncExitStack()
|
||||||
|
self.descheduled: int = 0
|
||||||
|
|
||||||
|
def empty(self) -> bool:
|
||||||
|
return self.empty_processing() and self.results.empty()
|
||||||
|
|
||||||
|
def empty_processing(self) -> bool:
|
||||||
|
return not self.sources and not self.playlists and not self.entries
|
||||||
|
|
||||||
|
async def iterate(self) -> AsyncIterable[YTAudio]:
|
||||||
|
async with self.es:
|
||||||
|
for _ in range(self.pool.workers()):
|
||||||
|
await self.es.enter_async_context(YJD(self).at(self.pool))
|
||||||
|
while not self.empty():
|
||||||
|
future = await self.results.get()
|
||||||
|
if future is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
audio = await future
|
||||||
|
if audio is not None:
|
||||||
|
yield audio
|
||||||
|
except OSError:
|
||||||
|
raise Explicit('extraction error\nunknown ytdl error')
|
||||||
|
finally:
|
||||||
|
self.results.task_done()
|
||||||
|
|
||||||
|
async def playlist(self, source: UrlCtx) -> list[InfoCtx]:
|
||||||
|
return [info async for info in source.entries()]
|
||||||
|
|
||||||
|
async def result(self, entry: InfoCtx) -> YTAudio | None:
|
||||||
|
try:
|
||||||
|
return await create_ytaudio(self.ctx, entry)
|
||||||
|
except:
|
||||||
|
if not entry.ignore:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class YJD(JobDescriptor):
|
||||||
|
def __init__(self, state: YState) -> None:
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def _unpack_playlists(self) -> None:
|
||||||
|
while self.state.playlists and self.state.playlists[0].done():
|
||||||
|
playlist = self.state.playlists.popleft()
|
||||||
|
if (e := playlist.exception()) is not None:
|
||||||
|
self.state.playlists.clear()
|
||||||
|
self.state.sources.clear()
|
||||||
|
self.state.entries.append(e)
|
||||||
|
else:
|
||||||
|
for entry in playlist.result():
|
||||||
|
self.state.entries.append(entry)
|
||||||
|
|
||||||
|
async def run(self) -> JobDescriptor | None:
|
||||||
|
if self.state.empty_processing():
|
||||||
|
if self.state.results.empty():
|
||||||
|
self.state.results.put_nowait(None)
|
||||||
|
return None
|
||||||
|
elif self.state.entries:
|
||||||
|
entry = self.state.entries.popleft()
|
||||||
|
audiotask: asyncio.Future[YTAudio | None]
|
||||||
|
if isinstance(entry, BaseException):
|
||||||
|
audiotask = asyncio.Future()
|
||||||
|
audiotask.set_exception(entry)
|
||||||
|
else:
|
||||||
|
audiotask = asyncio.create_task(self.state.result(entry))
|
||||||
|
self.state.results.put_nowait(audiotask)
|
||||||
|
try:
|
||||||
|
await audiotask
|
||||||
|
except:
|
||||||
|
self.state.entries.clear()
|
||||||
|
self.state.playlists.clear()
|
||||||
|
self.state.sources.clear()
|
||||||
|
return self
|
||||||
|
elif self.state.sources:
|
||||||
|
source = self.state.sources.popleft()
|
||||||
|
playlisttask = asyncio.create_task(self.state.playlist(source))
|
||||||
|
self.state.playlists.append(playlisttask)
|
||||||
|
try:
|
||||||
|
await playlisttask
|
||||||
|
except:
|
||||||
|
self.state.sources.clear()
|
||||||
|
return self
|
||||||
|
finally:
|
||||||
|
self._unpack_playlists()
|
||||||
|
rescheduled = self.state.descheduled
|
||||||
|
self.state.descheduled = 0
|
||||||
|
for _ in range(rescheduled):
|
||||||
|
await self.state.es.enter_async_context(YJD(self.state).at(self.state.pool))
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
self.state.descheduled += 1
|
||||||
|
return None
|
@ -1,21 +1,17 @@
|
|||||||
from typing import AsyncIterable
|
from typing import AsyncIterable
|
||||||
|
|
||||||
from v6d3music.core.create_ytaudios import create_ytaudios
|
from v6d3music.core.entries_effects_for_args import *
|
||||||
from v6d3music.core.entries_effects_for_args import entries_effects_for_args
|
from v6d3music.core.ystate import *
|
||||||
from v6d3music.core.ytaudio import YTAudio
|
from v6d3music.core.ytaudio import YTAudio
|
||||||
from v6d3music.utils.argctx import InfoCtx
|
from v6d3music.processing.pool import *
|
||||||
|
from v6d3music.utils.argctx import *
|
||||||
|
|
||||||
from v6d2ctx.context import Context
|
from v6d2ctx.context import Context
|
||||||
|
|
||||||
|
|
||||||
async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
|
async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
|
||||||
tuples: list[InfoCtx] = []
|
|
||||||
assert ctx.guild is not None
|
assert ctx.guild is not None
|
||||||
async for it in entries_effects_for_args(args, ctx.guild.id):
|
argctx = ArgCtx(default_effects(ctx.guild.id), args)
|
||||||
tuples.append(it)
|
async with Pool(5) as pool:
|
||||||
if len(tuples) >= 5:
|
async for audio in YState(pool, ctx, argctx.sources).iterate():
|
||||||
async for audio in create_ytaudios(ctx, tuples):
|
|
||||||
yield audio
|
|
||||||
tuples.clear()
|
|
||||||
async for audio in create_ytaudios(ctx, tuples):
|
|
||||||
yield audio
|
yield audio
|
||||||
|
@ -30,15 +30,17 @@ body,
|
|||||||
|
|
||||||
#root-container {
|
#root-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: min(40em, 100%);
|
min-width: min(40em, 100%);
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebars {
|
.sidebars {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #111;
|
background: #050505;
|
||||||
}
|
}
|
||||||
|
219
v6d3music/processing/pool.py
Normal file
219
v6d3music/processing/pool.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from v6d3music.processing.yprocess import *
|
||||||
|
|
||||||
|
__all__ = ('Job', 'Pool', 'JobDescriptor',)
|
||||||
|
|
||||||
|
|
||||||
|
class Job:
|
||||||
|
def __init__(self, future: asyncio.Future[None]) -> None:
|
||||||
|
self.future = future
|
||||||
|
|
||||||
|
async def run(self) -> Union['Job', None]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class JobDescriptor:
|
||||||
|
async def run(self) -> Union['JobDescriptor', None]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def wrap(self) -> Job:
|
||||||
|
return DescriptorJob(asyncio.Future(), self)
|
||||||
|
|
||||||
|
def at(self, pool: 'Pool') -> 'JDC':
|
||||||
|
return JDC(self, pool)
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptorJob(Job):
|
||||||
|
def __init__(self, future: asyncio.Future[None], descriptor: JobDescriptor) -> None:
|
||||||
|
super().__init__(future)
|
||||||
|
self.__descriptor = descriptor
|
||||||
|
|
||||||
|
async def run(self) -> Job | None:
|
||||||
|
next_descriptor = await self.__descriptor.run()
|
||||||
|
if next_descriptor is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return DescriptorJob(self.future, next_descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
class Worker:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__queue: asyncio.Queue[Job | None] = asyncio.Queue()
|
||||||
|
self.__working = False
|
||||||
|
self.__busy = 0
|
||||||
|
|
||||||
|
def _put_nowait(self, job: Job | None) -> None:
|
||||||
|
self.__queue.put_nowait(job)
|
||||||
|
self.__busy += 1
|
||||||
|
|
||||||
|
async def _handle(self, job: Job) -> None:
|
||||||
|
try:
|
||||||
|
next_job = await job.run()
|
||||||
|
except BaseException as e:
|
||||||
|
job.future.set_exception(e)
|
||||||
|
else:
|
||||||
|
if next_job is None:
|
||||||
|
job.future.set_result(None)
|
||||||
|
else:
|
||||||
|
self._put_nowait(next_job)
|
||||||
|
|
||||||
|
async def _safe_handle(self, job: Job) -> None:
|
||||||
|
try:
|
||||||
|
await self._handle(job)
|
||||||
|
except asyncio.InvalidStateError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_shutdown(self) -> bool:
|
||||||
|
if self.__queue.empty():
|
||||||
|
self.__working = False
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self._put_nowait(None)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _on(self, job: Job | None) -> bool:
|
||||||
|
if job is None:
|
||||||
|
return self._on_shutdown()
|
||||||
|
else:
|
||||||
|
await self._handle(job)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _step(self) -> bool:
|
||||||
|
job = await self.__queue.get()
|
||||||
|
try:
|
||||||
|
return await self._on(job)
|
||||||
|
finally:
|
||||||
|
self.__busy -= 1
|
||||||
|
self.__queue.task_done()
|
||||||
|
|
||||||
|
async def _work(self) -> None:
|
||||||
|
while True:
|
||||||
|
if await self._step():
|
||||||
|
return
|
||||||
|
|
||||||
|
def _cancel_job(self, job: Job) -> None:
|
||||||
|
try:
|
||||||
|
job.future.set_exception(RuntimeError('task left in the worker after shutdown'))
|
||||||
|
except asyncio.InvalidStateError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _cancel_one(self, job: Job | None) -> None:
|
||||||
|
if job is not None:
|
||||||
|
self._cancel_job(job)
|
||||||
|
|
||||||
|
def _cancel_get(self) -> None:
|
||||||
|
job = self.__queue.get_nowait()
|
||||||
|
try:
|
||||||
|
self._cancel_one(job)
|
||||||
|
finally:
|
||||||
|
self.__queue.task_done()
|
||||||
|
|
||||||
|
def _cancel_all(self) -> None:
|
||||||
|
while not self.__queue.empty():
|
||||||
|
self._cancel_get()
|
||||||
|
|
||||||
|
async def _task(self) -> None:
|
||||||
|
await self._work()
|
||||||
|
if self.__working:
|
||||||
|
raise RuntimeError('worker left seemingly running after shutdown')
|
||||||
|
if not self.__queue.empty():
|
||||||
|
raise RuntimeError('worker failed to finish all its jobs')
|
||||||
|
|
||||||
|
def start(self) -> asyncio.Future[None]:
|
||||||
|
if self.__working:
|
||||||
|
raise RuntimeError('starting an already running worker')
|
||||||
|
self.__working = True
|
||||||
|
return asyncio.create_task(self._task())
|
||||||
|
|
||||||
|
def submit(self, job: Job | None) -> None:
|
||||||
|
if not self.__working:
|
||||||
|
raise RuntimeError('submitting to a non-working worker')
|
||||||
|
self._put_nowait(job)
|
||||||
|
|
||||||
|
def working(self) -> bool:
|
||||||
|
return self.__working
|
||||||
|
|
||||||
|
def busy(self) -> int:
|
||||||
|
return self.__busy
|
||||||
|
|
||||||
|
|
||||||
|
class Working:
|
||||||
|
def __init__(self, worker: Worker, task: asyncio.Future[None]) -> None:
|
||||||
|
self.__worker = worker
|
||||||
|
self.__task = task
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
self.__worker.submit(None)
|
||||||
|
await self.__task
|
||||||
|
|
||||||
|
def submit(self, job: Job) -> None:
|
||||||
|
self.__worker.submit(job)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls) -> 'Working':
|
||||||
|
worker = Worker()
|
||||||
|
task = worker.start()
|
||||||
|
return cls(worker, task)
|
||||||
|
|
||||||
|
def working(self) -> bool:
|
||||||
|
return self.__worker.working()
|
||||||
|
|
||||||
|
def busy(self) -> int:
|
||||||
|
return self.__worker.busy()
|
||||||
|
|
||||||
|
|
||||||
|
class Pool:
|
||||||
|
def __init__(self, workers: int) -> None:
|
||||||
|
if workers < 1:
|
||||||
|
raise ValueError('non-positive number of workers')
|
||||||
|
self.__workers = workers
|
||||||
|
self.__working = False
|
||||||
|
self.__open = False
|
||||||
|
|
||||||
|
async def __aenter__(self) -> 'Pool':
|
||||||
|
if self.__open:
|
||||||
|
raise RuntimeError('starting an already open pool')
|
||||||
|
if self.__working:
|
||||||
|
raise RuntimeError('starting an already running pool')
|
||||||
|
self.__working = True
|
||||||
|
self.__pool = set(Working.start() for _ in range(self.__workers))
|
||||||
|
self.__open = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if not self.__working:
|
||||||
|
raise RuntimeError('stopping a non-working pool')
|
||||||
|
if not self.__open:
|
||||||
|
raise RuntimeError('stopping a closed pool')
|
||||||
|
self.__open = False
|
||||||
|
for working in self.__pool:
|
||||||
|
await working.close()
|
||||||
|
del self.__pool
|
||||||
|
self.__working = False
|
||||||
|
|
||||||
|
def submit(self, job: Job) -> None:
|
||||||
|
if not self.__working:
|
||||||
|
raise RuntimeError('submitting to a non-working pool')
|
||||||
|
if not self.__open:
|
||||||
|
raise RuntimeError('submitting to a closed pool')
|
||||||
|
min(self.__pool, key=lambda working: working.busy()).submit(job)
|
||||||
|
|
||||||
|
def workers(self) -> int:
|
||||||
|
return self.__workers
|
||||||
|
|
||||||
|
|
||||||
|
class JDC:
|
||||||
|
def __init__(self, descriptor: JobDescriptor, pool: Pool) -> None:
|
||||||
|
self.__descriptor = descriptor
|
||||||
|
self.__pool = pool
|
||||||
|
|
||||||
|
async def __aenter__(self) -> 'JDC':
|
||||||
|
job = self.__descriptor.wrap()
|
||||||
|
self.__future = job.future
|
||||||
|
self.__pool.submit(job)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.__future
|
@ -1,17 +1,22 @@
|
|||||||
from typing import AsyncIterable, Callable, Any
|
from typing import Any, AsyncIterable, Callable
|
||||||
|
|
||||||
from v6d3music.utils.effects_for_preset import effects_for_preset
|
from v6d3music.utils.effects_for_preset import effects_for_preset
|
||||||
from v6d3music.utils.entries_for_url import entries_for_url
|
from v6d3music.utils.entries_for_url import entries_for_url
|
||||||
from v6d3music.utils.options_for_effects import options_for_effects
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
from v6d3music.utils.sparq import sparq
|
from v6d3music.utils.sparq import sparq
|
||||||
|
|
||||||
|
__all__ = ('InfoCtx', 'UrlCtx', 'ArgCtx',)
|
||||||
|
|
||||||
|
|
||||||
class InfoCtx:
|
class InfoCtx:
|
||||||
def __init__(self, info: dict[str, Any], effects: str | None, already_read: int, tor: bool) -> None:
|
def __init__(
|
||||||
|
self, info: dict[str, Any], effects: str | None, already_read: int, tor: bool, ignore: bool
|
||||||
|
) -> None:
|
||||||
self.info = info
|
self.info = info
|
||||||
self.effects = effects
|
self.effects = effects
|
||||||
self.already_read = already_read
|
self.already_read = already_read
|
||||||
self.tor = tor
|
self.tor = tor
|
||||||
|
self.ignore = ignore
|
||||||
|
|
||||||
|
|
||||||
class UrlCtx:
|
class UrlCtx:
|
||||||
@ -20,14 +25,19 @@ class UrlCtx:
|
|||||||
self.effects = effects
|
self.effects = effects
|
||||||
self.already_read = 0
|
self.already_read = 0
|
||||||
self.tor = False
|
self.tor = False
|
||||||
|
self.ignore = False
|
||||||
|
|
||||||
async def entries(self) -> AsyncIterable[InfoCtx]:
|
async def entries(self) -> AsyncIterable[InfoCtx]:
|
||||||
|
try:
|
||||||
async for info in entries_for_url(self.url, self.tor):
|
async for info in entries_for_url(self.url, self.tor):
|
||||||
yield InfoCtx(info, self.effects, self.already_read, self.tor)
|
yield InfoCtx(info, self.effects, self.already_read, self.tor, self.ignore)
|
||||||
|
except:
|
||||||
|
if not self.ignore:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class ArgCtx:
|
class ArgCtx:
|
||||||
def __init__(self, default_effects: Callable[[int], str | None], args: list[str], gid: int) -> None:
|
def __init__(self, default_effects: str | None, args: list[str]) -> None:
|
||||||
self.sources: list[UrlCtx] = []
|
self.sources: list[UrlCtx] = []
|
||||||
while args:
|
while args:
|
||||||
match args:
|
match args:
|
||||||
@ -36,7 +46,7 @@ class ArgCtx:
|
|||||||
case [url, '+', preset, *args]:
|
case [url, '+', preset, *args]:
|
||||||
effects = effects_for_preset(preset)
|
effects = effects_for_preset(preset)
|
||||||
case [url, *args]:
|
case [url, *args]:
|
||||||
effects = default_effects(gid)
|
effects = default_effects
|
||||||
case _:
|
case _:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
ctx = UrlCtx(url, effects)
|
ctx = UrlCtx(url, effects)
|
||||||
@ -56,4 +66,9 @@ class ArgCtx:
|
|||||||
ctx.tor = True
|
ctx.tor = True
|
||||||
case [*args]:
|
case [*args]:
|
||||||
pass
|
pass
|
||||||
|
match args:
|
||||||
|
case ['ignore', *args]:
|
||||||
|
ctx.ignore = True
|
||||||
|
case [*args]:
|
||||||
|
pass
|
||||||
self.sources.append(ctx)
|
self.sources.append(ctx)
|
||||||
|
Loading…
Reference in New Issue
Block a user