Files
twitch-subathon-plugin/subathon/subathon.py

244 lines
7.4 KiB
Python

from asyncio import Event
from datetime import datetime, timedelta
from data.queries import JOINTYPE
from utils.lib import utc_now, strfdelta
from modules.profiles.profiles.profiles import ProfilesRegistry
from . import logger
from .data import (
SubathonData,
Subathon,
RunningSubathon,
SubathonContribution,
SubathonGoal,
)
class ActiveSubathon:
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
self.subathondata = subathondata
self.runningdata = runningdata
@property
def running(self):
return self.runningdata is not None
@property
def name(self):
return self.subathondata.name or "Subathon"
async def check_cap(self):
if not (cap := self.subathondata.timecap):
return False
else:
score = await self.get_score()
time_earned = self.get_score_time(score)
total_time = self.subathondata.initial_time + time_earned
return total_time >= cap
async def check_finished(self):
"""
Return True if the subathon duration has exceeded its earned time.
"""
return await self.get_remaining() <= 0
async def pause(self):
"""
Pause the active subathon.
If the subathon is in 'overtime', i.e. it has already finished,
then the total time is saved as the earned time not including the overtime.
"""
if not self.running:
raise ValueError("This subathon is not running!")
assert self.runningdata is not None
new_duration = await self.get_duration()
await self.subathondata.update(duration=new_duration)
await self.runningdata.delete()
self.runningdata = None
async def resume(self):
if self.running:
raise ValueError("This subathon is already running!")
self.runningdata = await RunningSubathon.create(
subathon_id=self.subathondata.subathon_id
)
async def get_score(self) -> float:
rows = await SubathonContribution.fetch_where(
subathon_id=self.subathondata.subathon_id
)
return sum(row.score for row in rows)
def get_score_time(self, score: float) -> int:
# Get time contributed by this score
return int(score * self.subathondata.score_time)
async def get_duration(self) -> int:
"""
Number of seconds that this subathon has run for.
Includes current running session if it exists.
If the subathon is already finished (in overtime)
returns the earned time instead of the true duration.
"""
duration = self.subathondata.duration
# Add running duration if required
if self.runningdata:
now = utc_now()
added = int((now - self.runningdata.last_started).total_seconds())
duration += added
earned = await self.get_earned()
return min(duration, earned)
async def get_earned(self) -> int:
"""
Number of seconds earned in the subathon so far.
Includes initial time.
Takes into account the cap.
"""
score = await self.get_score()
time_earned = self.get_score_time(score)
total_time = self.subathondata.initial_time + time_earned
if cap := self.subathondata.timecap:
total_time = min(total_time, cap)
return total_time
async def get_remaining(self) -> int:
"""
Number of seconds remaining on the subathon timer.
Will be 0 or slightly negative if finished.
"""
total_time = await self.get_earned()
return total_time - await self.get_duration() - 1
async def get_ending(self):
"""
Ending time of the subathon.
May be in the past if the subathon has already ended.
"""
now = utc_now()
remaining = await self.get_remaining()
return now + timedelta(seconds=remaining)
async def add_contribution(
self, profileid: int | None, score: float, event_id: int | None
) -> SubathonContribution:
return await SubathonContribution.create(
subathon_id=self.subathondata.subathon_id,
profileid=profileid,
score=score,
event_id=event_id,
)
def fetch_contributions(self, **kwargs):
query = SubathonContribution.fetch_where(
subathon_id=self.subathondata.subathon_id, **kwargs
).order_by("created_at")
return query
async def get_goals(self) -> list[SubathonGoal]:
goals = await SubathonGoal.fetch_where(
subathon_id=self.subathondata.subathon_id
).order_by("required_score")
return goals
async def get_total_subs(self) -> int:
"""
Get the total number of sub points
contributed to this subathon.
"""
pointcol = (
"CASE "
"WHEN tier = 1000 THEN 1 "
"WHEN tier = 2000 THEN 2 "
"WHEN tier = 3000 THEN 6 "
"END"
)
total_points = 0
result = await (
SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id
)
.join(
"subscribe_events",
using=("event_id",),
join_type=JOINTYPE.INNER,
)
.select(total=f"SUM({pointcol})")
.with_no_adapter()
)
total_points += result["total"]
result = await (
SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id
)
.join(
"subscribe_message_events",
using=("event_id",),
join_type=JOINTYPE.INNER,
)
.select(total=f"SUM({pointcol})")
.with_no_adapter()
)
total_points += result["total"]
result = await (
SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id
)
.join(
"subscribe_gift_events",
using=("event_id",),
join_type=JOINTYPE.INNER,
)
.select(total=f"SUM(gifted_count * ({pointcol}))")
.with_no_adapter()
)
total_points += result["total"]
return total_points
async def get_total_bits(self) -> int:
"""
Get the total number of bits
contributed to this subathon.
"""
result = await (
SubathonContribution.table.select_where(
subathoid=self.subathondata.subathon_id
)
.join(
"bits_events",
using=("event_id",),
join_type=JOINTYPE.INNER,
)
.select(total="SUM(bits)")
.with_no_adapter()
)
return result["total"]
class SubathonRegistry:
def __init__(self, data: SubathonData, profiler: ProfilesRegistry):
self.data = data
self.profiler = profiler
async def init(self):
await self.data.init()
async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
if rows:
subathondata = rows[0]
running = await RunningSubathon.fetch(subathondata.subathon_id)
subba = ActiveSubathon(subathondata, running)
return subba