Compare commits
2 Commits
main
...
71ed0c604c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71ed0c604c | ||
|
|
f335145976 |
@@ -37,7 +37,7 @@ from .. import utils, loader
|
||||
from ..types import BotInlineCall, InlineCall
|
||||
|
||||
logger = logging.getLogger("Limoka")
|
||||
__version__ = (1, 5, 6)
|
||||
__version__ = (1, 5, 5)
|
||||
|
||||
|
||||
def _parse_version_from_source(source: str):
|
||||
@@ -879,9 +879,6 @@ class Limoka(loader.Module):
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as head_error:
|
||||
logger.debug(f"_validate_url: HEAD failed ({type(head_error).__name__}), will try GET for {url}")
|
||||
|
||||
def _is_supported_media(content_type: str) -> bool:
|
||||
return content_type.startswith("image/") or content_type.startswith("video/mp4")
|
||||
|
||||
# If HEAD didn't work or returned non-200, try GET
|
||||
if ct is None:
|
||||
max_retries = 2
|
||||
@@ -900,7 +897,7 @@ class Limoka(loader.Module):
|
||||
try:
|
||||
data = await response.content.read(2048)
|
||||
mime = filetype.guess_mime(data, mime=True)
|
||||
if mime and _is_supported_media(mime):
|
||||
if mime and mime.startswith("image/"):
|
||||
return url
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
@@ -916,7 +913,7 @@ class Limoka(loader.Module):
|
||||
return None
|
||||
|
||||
# Check Content-Type from successful request
|
||||
if ct and _is_supported_media(ct):
|
||||
if ct and ct.startswith("image/"):
|
||||
return url
|
||||
elif ct:
|
||||
self._invalid_banners.add(url)
|
||||
|
||||
@@ -523,10 +523,6 @@ class Limoka(loader.Module):
|
||||
async def _validate_url(self, url: str) -> Optional[str]:
|
||||
if not url or url in self._invalid_banners:
|
||||
return None
|
||||
|
||||
def _is_supported_media(content_type: str) -> bool:
|
||||
return content_type.startswith("image/") or content_type.startswith("video/mp4")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(
|
||||
@@ -536,7 +532,7 @@ class Limoka(loader.Module):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
if not _is_supported_media(ct):
|
||||
if not ct.startswith("image/"):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
return url
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# requires: Pillow numpy
|
||||
# meta developer: @SunnexGB
|
||||
# meta banner: https://i.pinimg.com/control1/1200x/24/8d/40/248d40b6afa5bd3c3764556b50635691.jpg
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
import io
|
||||
import logging
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class ASCII(loader.Module):
|
||||
"""Convert images to braille ASCII"""
|
||||
|
||||
strings = {
|
||||
"name": "ASCII",
|
||||
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Library not loaded</b>",
|
||||
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Reply to image</b>",
|
||||
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Processing...</b>",
|
||||
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Empty result</b>",
|
||||
"result": "<pre>{art}</pre>",
|
||||
"Failed_to_load_library": "Failed to load library",
|
||||
"Conversion_error": "Conversion error",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Конвертирует картинку в braille ASCII",
|
||||
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Библиотека не была загружена</b>",
|
||||
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Ответьте на картинку</b>",
|
||||
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Обработка...</b>",
|
||||
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Пустой результат</b>",
|
||||
"result": "<pre>{art}</pre>",
|
||||
"Failed_to_load_library": "Не удалось загрузить библиотеку",
|
||||
"Conversion_error": "Ошибка конвертации",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue("width", 50),
|
||||
loader.ConfigValue("threshold", 0.65),
|
||||
loader.ConfigValue("contrast", 2.0),
|
||||
loader.ConfigValue("chars", 464),
|
||||
loader.ConfigValue("invert", False),
|
||||
)
|
||||
self.lib = None
|
||||
|
||||
async def client_ready(self):
|
||||
try:
|
||||
self.lib = await self.import_lib("https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/ASCII/ascii-lib.py", suspend_on_error=True)
|
||||
except Exception:
|
||||
logger.exception(self.strings["Failed_to_load_library"])
|
||||
self.lib = None
|
||||
|
||||
@loader.command(ru_doc="- Отрисовать ASCII-ART (аргумент -f, отправляет файлом)")
|
||||
async def dotcmd(self, message: Message):
|
||||
"""- Draw ASCII-ART (argument -f, sends as a file)"""
|
||||
if not self.lib:
|
||||
return await utils.answer(message, self.strings["no_lib"])
|
||||
args = utils.get_args_raw(message)
|
||||
force_file = "-f" in args.lower()
|
||||
reply = await message.get_reply_message() or message
|
||||
if not reply or not (
|
||||
reply.photo
|
||||
or (
|
||||
reply.document
|
||||
and str(getattr(reply.document, "mime_type", "")).startswith("image/")
|
||||
)
|
||||
):
|
||||
return await utils.answer(message, self.strings["no_image"])
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
image_bytes = await reply.download_media(bytes)
|
||||
art = self.lib.convert(
|
||||
image_bytes,
|
||||
width=self.config["width"],
|
||||
threshold=self.config["threshold"],
|
||||
contrast_boost=self.config["contrast"],
|
||||
invert=self.config["invert"],
|
||||
target_chars=self.config["chars"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(self.strings["Conversion_error"])
|
||||
return await utils.answer(msg, f"<pre>{e}</pre>")
|
||||
if not art or not art.strip():
|
||||
return await utils.answer(msg, self.strings["empty"])
|
||||
formatted_art = self.strings("result").format(art=art)
|
||||
if force_file or len(formatted_art) > 4096:
|
||||
file = io.BytesIO(art.encode("utf-8"))
|
||||
file.name = "ascii.txt"
|
||||
await message.client.send_file(message.peer_id, file)
|
||||
await msg.delete()
|
||||
else:
|
||||
await utils.answer(msg, formatted_art)
|
||||
@@ -1,110 +0,0 @@
|
||||
# requires: Pillow numpy
|
||||
# Дикие оправдания по поводу именно этого ассета а точнее кода в нем,честно я не знаю что сказать была попытка переписать JS на Py и как бы особых проблем не было,
|
||||
# до момента пост-обработки на помощь я позвал Claude и он не решил мою проблему от слова совсем,так как в целом я своего рода призираю пилоу,а модуль мне хотелось
|
||||
# написать я примерно вайб-кодил около 50 минут и я уверен из за этого будет возможно много проблем,в итоге благодаря немного копанию в коде,я нашел проблему и уже
|
||||
# начал ее решать,НО я опять же вообще не понимал как сделать то что мне нужно,в интернете были сюрсы но будто бы тот или иной мне не подходили? Я не знаю почему я
|
||||
# дропнул эту идею. Потом я стал искать в JS-е что там вообще можно сделать,в итоге я там импортировал модель какую то блядскую не нужную и опять впустую время
|
||||
# потратил,думал что тут определено есть решение и снова пошел к ии,вывод опятьь 0 помощи,я не знаю почему я так вцепился лишь в 1 идею.Как бы я мог упростить все,
|
||||
# даже наверное просто попросив какую то флагмен ии написать модуль и переписать его,но я уже на тот момент по моему мнению сделал много и не хотел ни каким образом
|
||||
# оставлять это,поэтому через время я нашел сайты которые в целом давали возможность настраивать фильтр,была переделана логика(в целом ее переделал на 60 процентов
|
||||
# клод,я просто убирал мусор который он испражнял.И вот дальше точно бред я убил более дня на решение проблем которые были решены мной,но результат мне не нравился
|
||||
# И ОПЯТЬ я пошел просить помощи у гугла,потом понял что возможно даже будет легко(по факту легко,но я ленивый) пока искал,мне перехотелось и я уже потом пытался
|
||||
# сделать режимы в модуле,что оказалось ужасом ведь они работали,но при возможности гармонично вписать их в код были конфликты И Я В ОЧЕРЕДНОЙ РАЗ ПОШЕЛ К ИИ,спойлер
|
||||
# он не смог написать лучше чем я,в итоге я отбросил эту идею и думаю в целом никак больше не апдейтать модуль по крупному.
|
||||
# Да это были оправдания,но зато какие!
|
||||
import io
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter, ImageEnhance, ImageOps
|
||||
from .. import loader
|
||||
|
||||
BASE = 0x2800
|
||||
INVERT_MAP = {chr(BASE + c): chr(BASE + (c ^ 0xFF)) for c in range(256)}
|
||||
|
||||
|
||||
class AsciiLib(loader.Library):
|
||||
developer = "@SunnexGB"
|
||||
|
||||
def resize(self, img):
|
||||
if img.width > 768:
|
||||
img = img.resize((768, int(img.height * 768 / img.width)), Image.LANCZOS)
|
||||
w = img.width - img.width % 4
|
||||
h = img.height - img.height % 4
|
||||
if w != img.width or h != img.height:
|
||||
img = img.resize((w, h), Image.LANCZOS)
|
||||
return img
|
||||
|
||||
def mode(self, img, threshold, contrast):
|
||||
gray = img.convert("L")
|
||||
edges = ImageOps.invert(gray.filter(ImageFilter.FIND_EDGES))
|
||||
contrast_img = ImageEnhance.Contrast(img).enhance(contrast).convert("L")
|
||||
e = np.array(edges, dtype=np.float32) / 255.0
|
||||
c = np.array(contrast_img, dtype=np.float32) / 255.0
|
||||
blended = Image.fromarray((e * c * 255).astype(np.uint8), "L")
|
||||
t = int(threshold * 255)
|
||||
processed = blended.point(lambda p: 255 if p > t else 0, "L")
|
||||
return processed, t
|
||||
|
||||
def braille(self, img, threshold, width):
|
||||
cw = width * 2
|
||||
o = -(-round(cw * img.height / img.width) // 4)
|
||||
ch = 4 * o
|
||||
px = np.array(img.resize((cw, ch), Image.LANCZOS).convert("L"))
|
||||
order = [(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(3,0),(3,1)]
|
||||
rows = []
|
||||
for rs in range(0, ch, 4):
|
||||
line = []
|
||||
for cs in range(0, cw, 2):
|
||||
grays = [
|
||||
int(px[rs+dy, cs+dx]) if (rs+dy < ch and cs+dx < cw) else 255
|
||||
for dy, dx in order
|
||||
]
|
||||
bits = list(reversed([1 if g < threshold else 0 for g in grays]))
|
||||
code = int("".join(str(b) for b in bits), 2)
|
||||
line.append(chr(BASE + code))
|
||||
rows.append("".join(line))
|
||||
return rows
|
||||
|
||||
def trim(self, lines):
|
||||
blank = "\u2800"
|
||||
while lines and all(c == blank for c in lines[0]):
|
||||
lines = lines[1:]
|
||||
while lines and all(c == blank for c in lines[-1]):
|
||||
lines = lines[:-1]
|
||||
if not lines:
|
||||
return lines
|
||||
left = min(next((i for i,c in enumerate(r) if c!=blank), len(r)) for r in lines)
|
||||
right = min(next((i for i,c in enumerate(reversed(r)) if c!=blank), len(r)) for r in lines)
|
||||
return [r[left: len(r)-right if right else len(r)] for r in lines]
|
||||
|
||||
def invert(self, lines):
|
||||
return ["".join(INVERT_MAP.get(c,c) for c in l) for l in lines]
|
||||
|
||||
def fit(self, img, threshold, chars, width):
|
||||
lo, hi = 5, 200
|
||||
best = ""
|
||||
for _ in range(14):
|
||||
mid = (lo + hi)//2
|
||||
lines = self.trim(self.braille(img, threshold, mid))
|
||||
art = "\n".join(lines)
|
||||
if len(art) <= chars:
|
||||
best = art
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
return best
|
||||
|
||||
def convert(self, data, width=50, threshold=0.65, contrast_boost=2.0, invert=False, target_chars=0):
|
||||
buf = io.BytesIO(data)
|
||||
img = Image.open(buf)
|
||||
img.load()
|
||||
buf.close()
|
||||
img = img.convert("RGB")
|
||||
img = self.resize(img)
|
||||
processed, t = self.mode(img, threshold, contrast_boost)
|
||||
if target_chars > 0:
|
||||
art = self.fit(processed, t, target_chars, width)
|
||||
else:
|
||||
art = "\n".join(self.trim(self.braille(processed, t, width)))
|
||||
if invert and art:
|
||||
art = "\n".join(self.invert(art.split("\n")))
|
||||
return art
|
||||
@@ -1,832 +0,0 @@
|
||||
{
|
||||
"prologue": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "prologue"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_1",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Мне опять снился сон."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "<i>Этот</i> сон..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Каждую ночь одно и то же."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но наутро, как обычно, всё забудется."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, оно и к лучшему..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Останутся только туманные воспоминания о приоткрытых, словно приглашающих куда-то воротах, рядом с которыми в камне застыли два пионера."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ещё странная девочка...{w} которая постоянно спрашивает:"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "anim_prolog1_off",
|
||||
"action": "load_asset",
|
||||
"location": "anim/anim_prolog1_off",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "dialogue",
|
||||
"char_id": "dreamgirl",
|
||||
"character": "...",
|
||||
"text": "Ты пойдёшь со мной?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Пойду?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но куда?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И зачем?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и где я вообще нахожусь?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Конечно, случись всё на самом деле, наяву, стоило бы непременно испугаться."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как же иначе!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но это – всего лишь сон.{w} Тот самый, который я вижу каждую ночь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ведь всё это неспроста!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Необязательно знать <i>где</i> и <i>почему</i>, чтобы понять – что-то происходит."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Нечто, отчаянно требующее моего внимания."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь всё окружающее меня здесь – реально!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Реально настолько, насколько реальны вещи в моей квартире; я бы мог открыть ворота, услышать скрип петель, смахнуть рукой осыпающуюся ржавчину, потянуть носом свежий прохладный воздух и поёжиться от холода."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Мог бы, но для этого надо сдвинуться с места, сделать шаг, пошевелить рукой..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ведь это сон – я понимаю, но что дальше, что изменит моё <i>понимание</i>?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь здесь – словно по ту сторону потрескавшегося экрана старого телевизора, который из последних сил борется с помехами и силится показать зрителям всё, не упустив ни малейшей детали."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но вот картинка теряет чёткость...{w} Наверное, скоро просыпаться."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, спросить у неё что-то?{w} У девочки."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как же её зовут..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Например про звёзды..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Хотя почему про звёзды?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Можно же спросить про ворота!{w} Да, про ворота!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вот она удивится."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или лучше про букву <i>ё</i>."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Хорошая была буква..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как будто её больше нет!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И какое отношение буквы, ворота и звёзды имеют к этому месту?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь если мне каждую ночь снится <i>этот</i> сон, который потом всё равно забудется, надо искать разгадку здесь и сейчас!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А вот, если присмотреться, можно увидеть Магелланово Облако..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Словно попал в южное полушарие!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Во сне всегда больше волнуют мелочи: неестественный цвет травы, невозможная кривизна прямых или своё перекошенное отражение – а реальная опасность, готовая оборвать всё здесь и сейчас, кажется пустяком."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Естественно, ведь <i>здесь</i> нельзя умереть."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я точно знаю – я делал это сотни раз."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но если нельзя умереть, нет смысла жить?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Надо будет спросить у девочки: она местная – должна знать!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да, именно!{w} Спросить, например, про сову."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Больно уж птица странная..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А впрочем, неважно..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "dialogue",
|
||||
"char_id": "dreamgirl",
|
||||
"character": "...",
|
||||
"text": "Ты пойдёшь со мной?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И каждый раз надо отвечать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иначе никак, иначе сон не закончится, а я – не проснусь."
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"id": "prologue_choice_1"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Каждый раз так сложно решить, что же ответить."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Где я, что я здесь делаю, кто она такая?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И почему от ответа на этот вопрос зависит так много в моей жизни?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или не зависит?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь это просто сон..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Просто сон..."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "black",
|
||||
"action": "load_asset",
|
||||
"location": "bg/black",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/black.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "1_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "2_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "3_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "4_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "5_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Экран монитора смотрел на меня словно живой."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иногда мне правда казалось, что он обладает сознанием, своими мыслями и желаниями, стремлениями; умеет чувствовать, любить и страдать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Словно в наших отношениях инструмент не он – неодушевлённый кусок пластика и текстолита, – а я."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, в этом есть доля правды, ведь компьютер на 90% обеспечивает моё общение с внешним миром."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Анонимные имиджборды, иногда какие-то чаты, редко – аська или джаббер, ещё реже – форумы."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А людей, сидящих по ту сторону сетевого кабеля, попросту не существует!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Все они – всего лишь плод его больной фантазии, ошибка в программном коде или баг ядра, зажившего собственной жизнью."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_15",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_15",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_3",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_4",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Если посмотреть со стороны на моё существование, то такие мысли покажутся не столь уж бредовыми, а какой-нибудь психолог наверняка поставит мне кучу заумных диагнозов и, возможно, выпишет направление в жёлтый дом."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_5",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_14",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_14",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_11",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_11",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Маленькая квартирка без следов какого бы то ни было ремонта или даже подобия порядка, и вечно одинаковый вид из окна на серый, день и ночь куда-то бегущий мегаполис, – вот условия моей жизни."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Конечно, всё начиналось не так..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я родился, пошёл в школу, закончил её – всё как у людей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Поступил в институт, где кое-как промучился полтора курса."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Работал на паре-тройке разных работ.{w} Иногда даже и неплохо, иногда даже получая за это достойные деньги."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Однако всё это казалось чужим, словно списанным с биографии другого человека."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я не ощущал полноту жизни – она словно зациклилась и продолжала идти по кругу.{w} Как в фильме «День сурка»."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Только у меня не было выбора, как именно провести этот день, и каждый раз всё повторялось по одной и той же схеме.{w} Схеме пустоты, уныния и отчаяния."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Последние несколько лет я просто целыми днями сидел за компьютером."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иногда подворачивались какие-то халтурки, иногда помогали родители."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "В общем, на жизнь хватало."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Это и немудрено, ведь потребности у меня небольшие."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "На улицу я практически не выхожу, а всё моё общение с людьми сводится к интернет-переписке с <i>анонимами</i>, у которых нет ни реального имени, ни пола, ни возраста."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Короче говоря, достаточно типичная жизнь достаточно типичного асоциального человека своего времени.{w} Этакий Обломов XXI века."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, маститый писатель напишет обо мне роман, который станет классикой современной литературы.{w} Или напишу я сам…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Впрочем нет, что себя обманывать – уже не раз пытался, но меня не хватало даже на короткий рассказ."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Изучал я и множество других вещей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Рисовать – не дано от природы.{w} Программирование – надоело.{w} Иностранные языки – долго и скучно…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Любил я разве что читать, но даже при этом никогда бы не назвал себя эрудированным человеком."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Возможно, я был асом в просмотре аниме и гроссмейстером неумелых шуточек в интернете."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Плати мне за это деньги, я бы обрадовался (да и заработал неплохо), но вряд ли так просто можно заполнить пустоту в душе."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "semen_room_window",
|
||||
"action": "load_asset",
|
||||
"location": "bg/semen_room_window",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сегодня очередной типичный день моей типичной жизни типичного неудачника."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И именно сегодня мне нужно ехать на встречу институтских товарищей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "По правде говоря, совершенно не хотелось."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и какой смысл, если вместе с ними я отучился всего ничего?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Однако меня всё же уговорил друг, бывший одногруппник, один из немногих, с кем я поддерживал контакт не только в интернете."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_1",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_3",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_4",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_5",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_6",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_6",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_8",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_8",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_7",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_7",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "bus_stop",
|
||||
"action": "load_asset",
|
||||
"location": "bg/bus_stop",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вечер. Мороз.{w} Остановка и ожидание автобуса."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я никогда не любил зиму.{w} Впрочем, и жаркое лето – тоже не моя стихия."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Просто не вижу смысла выделять какое-то одно время года – не столь важно, какая погода на улице, если ты целыми днями сидишь дома."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Автобус сегодня задерживался так сильно, что я уже был готов плюнуть на всё и потратить последнюю пару сотен на такси (совсем не ехать мне почему-то в голову не пришло)."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "В мозгу, как всегда, роились миллионы мыслей, из которых совершенно невозможно выудить хотя бы одну стоящую."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Такую, которую можно закончить, привести в порядок, облечь в форму идеи и претворить в жизнь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, заняться бизнесом?{w} Но откуда я возьму деньги?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или пойти опять работать в офис?{w} Нет уж!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может, стоит попробовать фриланс?{w} Да что я умею, и кому я нужен…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вдруг мне вспомнилось детство…{w} Или скорее юношество – 15-17 лет."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Почему именно это время?{w} Не знаю."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, потому что тогда всё было проще."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Было проще принимать такие сложные сейчас и такие простые тогда решения."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Проснувшись с утра, я чётко знал, как пройдёт мой день, а выходных ждал с нетерпением – смогу отдохнуть, заняться любимыми делами: компьютер, футбол, встречи с друзьями."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А потом, когда наступит новая неделя, вновь примусь за учёбу."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь раньше не возникало этих мучительных вопросов «зачем», «кому это надо», «что изменится, если я это сделаю» или «что не изменится»."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Простой поток жизни, такой привычный для любого нормального человека и такой чуждый для меня теперешнего."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Время беззаботного детства…{w} Тогда же я и встретил свою первую любовь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Стёрлись из памяти её внешность, характер."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как строчка из профиля в социальной сети осталось лишь имя, да те чувства, которые захлёстывали меня, когда я был с ней.{w} Теплота, нежность, желание заботиться, защитить…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Жаль, что это продолжалось так недолго."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сейчас я уже с трудом могу себе представить что-то подобное."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, и хочется познакомиться с девушкой, только не знаю, как начать диалог, о чём вообще с ней говорить, чем её заинтересовать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и подходящих девушек я давно не встречал.{w} Хотя где мне их встретить…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_9",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_9",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Звук работающего двигателя вернул меня к реальности."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Подъехал автобус."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "«Какой-то он не такой» – мелькнула мысль."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Впрочем, какая разница – по этому маршруту ходит только 410-ый."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_10",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_10",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_11",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_11",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_13",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_13",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "intro_xx",
|
||||
"action": "load_asset",
|
||||
"location": "bg/intro_xx",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Огни пролетают мимо, их холодный свет словно зажигает внутри давно погасшие чувства."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или не зажигает, а просто пробуждает…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь «они» уже давно живут во мне, то затихая, то просыпаясь вновь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Какая-то очень известная мелодия играла в радиоприёмнике у водителя.{w} Но я её не слушал."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я смотрел в запотевшее окно автобуса на проезжающие мимо машины."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь люди куда-то спешат, ведь им что-то нужно, и, погружённые в свои дела, они не задумываются о вопросах, мучающих меня."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, у них тоже есть свои серьёзные проблемы, а может, им живётся куда легче."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Знать наверняка нельзя, так как все люди разные.{w} Или не разные?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Бывает, поступки человека легко предсказуемы, но, пытаясь заглянуть к нему в душу, видишь лишь непроглядную тьму."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Автобус приближался к центру, и мои мысли прервал яркий свет огней большого города."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сотни рекламных вывесок, тысячи машин, миллионы людей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я смотрел на это светопреставление, и мне почему-то безумно захотелось спать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Глаза закрылись всего на полсекунды и…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "black",
|
||||
"action": "load_asset",
|
||||
"location": "bg/black",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "opening",
|
||||
"kind": "opening",
|
||||
"name": "opening",
|
||||
"action": "load_asset",
|
||||
"location": "opening/opening",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/opening/opening.mp4?raw=true"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"prologue_choice_1": {
|
||||
"question": "Иначе никак, иначе сон не закончится, а я – не проснусь. — что выбрать?",
|
||||
"chapter": "prologue",
|
||||
"options": {
|
||||
"Да, я пойду с тобой": {
|
||||
"effects": {}
|
||||
},
|
||||
"Нет, я останусь здесь": {
|
||||
"effects": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"endings": {
|
||||
"labels": [
|
||||
"main_good_ending",
|
||||
"main_bad_ending",
|
||||
"sl_good_ending",
|
||||
"sl_bad_ending",
|
||||
"dv_good_ending",
|
||||
"dv_bad_ending",
|
||||
"un_good_ending",
|
||||
"un_bad_ending",
|
||||
"us_good_ending",
|
||||
"us_bad_ending",
|
||||
"mi_ending",
|
||||
"uv_ending",
|
||||
"harem_ending"
|
||||
],
|
||||
"routes": {
|
||||
"sl": {
|
||||
"good": "sl_good_ending",
|
||||
"bad": "sl_bad_ending",
|
||||
"point_key": "sl_points"
|
||||
},
|
||||
"dv": {
|
||||
"good": "dv_good_ending",
|
||||
"bad": "dv_bad_ending",
|
||||
"point_key": "dv_points"
|
||||
},
|
||||
"un": {
|
||||
"good": "un_good_ending",
|
||||
"bad": "un_bad_ending",
|
||||
"point_key": "un_points"
|
||||
},
|
||||
"us": {
|
||||
"good": "us_good_ending",
|
||||
"bad": "us_bad_ending",
|
||||
"point_key": "us_points"
|
||||
},
|
||||
"mi": {
|
||||
"single": "mi_ending",
|
||||
"point_key": "mi_points"
|
||||
},
|
||||
"uv": {
|
||||
"single": "uv_ending",
|
||||
"point_key": "uv_points"
|
||||
}
|
||||
},
|
||||
"fallback": "main_bad_ending"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 519 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 281 KiB |
@@ -1,400 +0,0 @@
|
||||
2g1c
|
||||
2 girls 1 cup
|
||||
acrotomophilia
|
||||
alabama hot pocket
|
||||
alaskan pipeline
|
||||
anal
|
||||
anilingus
|
||||
anus
|
||||
apeshit
|
||||
arsehole
|
||||
ass
|
||||
asshole
|
||||
assmunch
|
||||
auto erotic
|
||||
autoerotic
|
||||
babeland
|
||||
baby batter
|
||||
baby juice
|
||||
ball gag
|
||||
ball gravy
|
||||
ball kicking
|
||||
ball licking
|
||||
ball sack
|
||||
ball sucking
|
||||
bangbros
|
||||
bangbus
|
||||
bareback
|
||||
barely legal
|
||||
barenaked
|
||||
bastard
|
||||
bastardo
|
||||
bastinado
|
||||
bbw
|
||||
bdsm
|
||||
beaner
|
||||
beaners
|
||||
beaver cleaver
|
||||
beaver lips
|
||||
beastiality
|
||||
bestiality
|
||||
big black
|
||||
big breasts
|
||||
big knockers
|
||||
big tits
|
||||
bimbos
|
||||
birdlock
|
||||
bitch
|
||||
bitches
|
||||
black cock
|
||||
blonde action
|
||||
blonde on blonde action
|
||||
blowjob
|
||||
blow job
|
||||
blow your load
|
||||
blue waffle
|
||||
blumpkin
|
||||
bollocks
|
||||
bondage
|
||||
boner
|
||||
boob
|
||||
boobs
|
||||
booty call
|
||||
brown showers
|
||||
brunette action
|
||||
bukkake
|
||||
bulldyke
|
||||
bullet vibe
|
||||
bullshit
|
||||
bung hole
|
||||
bunghole
|
||||
busty
|
||||
butt
|
||||
buttcheeks
|
||||
butthole
|
||||
camel toe
|
||||
camgirl
|
||||
camslut
|
||||
camwhore
|
||||
carpet muncher
|
||||
carpetmuncher
|
||||
chocolate rosebuds
|
||||
cialis
|
||||
circlejerk
|
||||
cleveland steamer
|
||||
clit
|
||||
clitoris
|
||||
clover clamps
|
||||
clusterfuck
|
||||
cock
|
||||
cocks
|
||||
coprolagnia
|
||||
coprophilia
|
||||
cornhole
|
||||
coon
|
||||
coons
|
||||
creampie
|
||||
cum
|
||||
cumming
|
||||
cumshot
|
||||
cumshots
|
||||
cunnilingus
|
||||
cunt
|
||||
darkie
|
||||
date rape
|
||||
daterape
|
||||
deep throat
|
||||
deepthroat
|
||||
dendrophilia
|
||||
dick
|
||||
dildo
|
||||
dingleberry
|
||||
dingleberries
|
||||
dirty pillows
|
||||
dirty sanchez
|
||||
doggie style
|
||||
doggiestyle
|
||||
doggy style
|
||||
doggystyle
|
||||
dog style
|
||||
dolcett
|
||||
domination
|
||||
dominatrix
|
||||
dommes
|
||||
donkey punch
|
||||
double dong
|
||||
double penetration
|
||||
dp action
|
||||
dry hump
|
||||
dvda
|
||||
eat my ass
|
||||
ecchi
|
||||
ejaculation
|
||||
erotic
|
||||
erotism
|
||||
escort
|
||||
eunuch
|
||||
fag
|
||||
faggot
|
||||
fecal
|
||||
felch
|
||||
fellatio
|
||||
feltch
|
||||
female squirting
|
||||
femdom
|
||||
figging
|
||||
fingerbang
|
||||
fingering
|
||||
fisting
|
||||
foot fetish
|
||||
footjob
|
||||
frotting
|
||||
fuck
|
||||
fuck buttons
|
||||
fuckin
|
||||
fucking
|
||||
fucktards
|
||||
fudge packer
|
||||
fudgepacker
|
||||
futanari
|
||||
gangbang
|
||||
gang bang
|
||||
gay sex
|
||||
genitals
|
||||
giant cock
|
||||
girl on
|
||||
girl on top
|
||||
girls gone wild
|
||||
goatcx
|
||||
goatse
|
||||
god damn
|
||||
gokkun
|
||||
golden shower
|
||||
goodpoop
|
||||
goo girl
|
||||
goregasm
|
||||
grope
|
||||
group sex
|
||||
g-spot
|
||||
guro
|
||||
hand job
|
||||
handjob
|
||||
hard core
|
||||
hardcore
|
||||
hentai
|
||||
homoerotic
|
||||
honkey
|
||||
hooker
|
||||
horny
|
||||
hot carl
|
||||
hot chick
|
||||
how to kill
|
||||
how to murder
|
||||
huge fat
|
||||
humping
|
||||
incest
|
||||
intercourse
|
||||
jack off
|
||||
jail bait
|
||||
jailbait
|
||||
jelly donut
|
||||
jerk off
|
||||
jigaboo
|
||||
jiggaboo
|
||||
jiggerboo
|
||||
jizz
|
||||
juggs
|
||||
kike
|
||||
kinbaku
|
||||
kinkster
|
||||
kinky
|
||||
knobbing
|
||||
leather restraint
|
||||
leather straight jacket
|
||||
lemon party
|
||||
livesex
|
||||
lolita
|
||||
lovemaking
|
||||
make me come
|
||||
male squirting
|
||||
masturbate
|
||||
masturbating
|
||||
masturbation
|
||||
menage a trois
|
||||
milf
|
||||
missionary position
|
||||
mong
|
||||
motherfucker
|
||||
mound of venus
|
||||
mr hands
|
||||
muff diver
|
||||
muffdiving
|
||||
nambla
|
||||
nawashi
|
||||
negro
|
||||
neonazi
|
||||
nigga
|
||||
nigger
|
||||
nig nog
|
||||
nimphomania
|
||||
nipple
|
||||
nipples
|
||||
nsfw
|
||||
nsfw images
|
||||
nude
|
||||
nudity
|
||||
nutten
|
||||
nympho
|
||||
nymphomania
|
||||
octopussy
|
||||
omorashi
|
||||
one cup two girls
|
||||
one guy one jar
|
||||
orgasm
|
||||
orgy
|
||||
paedophile
|
||||
paki
|
||||
panties
|
||||
panty
|
||||
pedobear
|
||||
pedophile
|
||||
pegging
|
||||
penis
|
||||
phone sex
|
||||
piece of shit
|
||||
pikey
|
||||
pissing
|
||||
piss pig
|
||||
pisspig
|
||||
playboy
|
||||
pleasure chest
|
||||
pole smoker
|
||||
ponyplay
|
||||
poof
|
||||
poon
|
||||
poontang
|
||||
punany
|
||||
poop chute
|
||||
poopchute
|
||||
porn
|
||||
porno
|
||||
pornography
|
||||
prince albert piercing
|
||||
pthc
|
||||
pubes
|
||||
pussy
|
||||
queaf
|
||||
queef
|
||||
quim
|
||||
raghead
|
||||
raging boner
|
||||
rape
|
||||
raping
|
||||
rapist
|
||||
rectum
|
||||
reverse cowgirl
|
||||
rimjob
|
||||
rimming
|
||||
rosy palm
|
||||
rosy palm and her 5 sisters
|
||||
rusty trombone
|
||||
sadism
|
||||
santorum
|
||||
scat
|
||||
schlong
|
||||
scissoring
|
||||
semen
|
||||
sex
|
||||
sexcam
|
||||
sexo
|
||||
sexy
|
||||
sexual
|
||||
sexually
|
||||
sexuality
|
||||
shaved beaver
|
||||
shaved pussy
|
||||
shemale
|
||||
shibari
|
||||
shit
|
||||
shitblimp
|
||||
shitty
|
||||
shota
|
||||
shrimping
|
||||
skeet
|
||||
slanteye
|
||||
slut
|
||||
s&m
|
||||
smut
|
||||
snatch
|
||||
snowballing
|
||||
sodomize
|
||||
sodomy
|
||||
spastic
|
||||
spic
|
||||
splooge
|
||||
splooge moose
|
||||
spooge
|
||||
spread legs
|
||||
spunk
|
||||
strap on
|
||||
strapon
|
||||
strappado
|
||||
strip club
|
||||
style doggy
|
||||
suck
|
||||
sucks
|
||||
suicide girls
|
||||
sultry women
|
||||
swastika
|
||||
swinger
|
||||
tainted love
|
||||
taste my
|
||||
tea bagging
|
||||
threesome
|
||||
throating
|
||||
thumbzilla
|
||||
tied up
|
||||
tight white
|
||||
tit
|
||||
tits
|
||||
titties
|
||||
titty
|
||||
tongue in a
|
||||
topless
|
||||
tosser
|
||||
towelhead
|
||||
tranny
|
||||
tribadism
|
||||
tub girl
|
||||
tubgirl
|
||||
tushy
|
||||
twat
|
||||
twink
|
||||
twinkie
|
||||
two girls one cup
|
||||
undressing
|
||||
upskirt
|
||||
urethra play
|
||||
urophilia
|
||||
vagina
|
||||
venus mound
|
||||
viagra
|
||||
vibrator
|
||||
violet wand
|
||||
vorarephilia
|
||||
voyeur
|
||||
voyeurweb
|
||||
voyuer
|
||||
vulva
|
||||
wank
|
||||
wetback
|
||||
wet dream
|
||||
white power
|
||||
whore
|
||||
worldsex
|
||||
wrapping men
|
||||
wrinkled starfish
|
||||
yaoi
|
||||
yellow showers
|
||||
yiffy
|
||||
zoophilia
|
||||
|
Before Width: | Height: | Size: 609 B |
|
Before Width: | Height: | Size: 609 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="30" height="22" viewBox="0 0 30 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.5455 5.5L19.0909 11H23.1818C23.1818 15.5512 19.5136 19.25 15 19.25C13.6227 19.25 12.3136 18.9062 11.1818 18.2875L9.19091 20.295C10.8682 21.3675 12.8591 22 15 22C21.0273 22 25.9091 17.0775 25.9091 11H30L24.5455 5.5ZM6.81818 11C6.81818 6.44875 10.4864 2.75 15 2.75C16.3773 2.75 17.6864 3.09375 18.8182 3.7125L20.8091 1.705C19.1318 0.6325 17.1409 0 15 0C8.97273 0 4.09091 4.9225 4.09091 11H0L5.45455 16.5L10.9091 11H6.81818Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 554 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="23" height="25" viewBox="0 0 23 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6389 10.05H17.3611M7.91667 5.65H17.3611M21.25 17.75V6.75C21.25 3.45 19.5833 1.25 15.6944 1.25H6.80555C2.91667 1.25 1.25 3.45 1.25 6.75V17.75C1.25 21.05 2.91667 23.25 6.80555 23.25H15.6944C19.5833 23.25 21.25 21.05 21.25 17.75ZM15.1389 23.25V14.604C15.1389 14.12 14.5611 13.878 14.2056 14.197L11.6278 16.551C11.4167 16.749 11.0833 16.749 10.8722 16.551L8.29448 14.197C7.93893 13.878 7.36112 14.12 7.36112 14.604V23.25H15.1389Z" stroke="white" stroke-width="2.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 579 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3333 7.33333C16.2381 7.33333 14.6667 5.76191 14.6667 3.66667C14.6667 1.57143 16.2381 0 18.3333 0C20.4286 0 22 1.57143 22 3.66667C22 5.76191 20.4286 7.33333 18.3333 7.33333ZM3.66667 14.6667C1.57143 14.6667 0 13.0952 0 11C0 8.90476 1.57143 7.33333 3.66667 7.33333C5.76191 7.33333 7.33333 8.90476 7.33333 11C7.33333 13.0952 5.76191 14.6667 3.66667 14.6667ZM18.3333 22C16.2381 22 14.6667 20.4286 14.6667 18.3333C14.6667 16.2381 16.2381 14.6667 18.3333 14.6667C20.4286 14.6667 22 16.2381 22 18.3333C22 20.4286 20.4286 22 18.3333 22ZM4.1381 10.0676L18.8048 17.401L17.8724 19.2762L3.20571 11.9429L4.1381 10.0676ZM18.8048 4.59905L4.1381 11.9324L3.20571 10.0571L17.8724 2.72381L18.8048 4.59905Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 817 B |
|
Before Width: | Height: | Size: 539 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 23.5L15.3223 18.5362C15.8683 17.8294 16.8801 17.7008 17.5867 18.247L20.5257 20.5603C21.2323 21.1064 22.2441 20.9779 22.7902 20.2872L26.5 15.5M13.75 36.5H24.25C33 36.5 36.5 33 36.5 24.25V13.75C36.5 5 33 1.5 24.25 1.5H13.75C5 1.5 1.5 5 1.5 13.75V24.25C1.5 33 5 36.5 13.75 36.5Z" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 475 B |
|
Before Width: | Height: | Size: 520 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="61" height="54" viewBox="0 0 61 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.65352 50.6608C7.65352 48.8166 9.11479 47.3216 10.9173 47.3216H50.0828C51.8854 47.3216 53.3466 48.8166 53.3466 50.6608C53.3466 52.505 51.8854 54 50.0828 54H10.9173C9.11479 54 7.65352 52.505 7.65352 50.6608Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.5764 2.93407C27.4001 -0.978025 33.5996 -0.978025 37.4235 2.93407L45.8 11.504C46.7935 12.5204 48.3108 12.7723 49.5674 12.1295L51.887 10.943C56.8965 8.38037 62.4336 13.2937 60.6626 18.7299L54.6667 37.1327C53.3341 41.2232 49.5925 43.9824 45.378 43.9824H15.6221C11.4076 43.9824 7.66588 41.2232 6.33315 37.1327L0.337435 18.7299C-1.43373 13.2937 4.10349 8.38037 9.11308 10.943L11.4325 12.1295C12.689 12.7723 14.2066 12.5204 15.2 11.504L23.5764 2.93407Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 893 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="45" height="84" viewBox="0 0 45 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.2576 0.680354L22.5645 0.764917C22.704 0.811776 22.8388 0.867879 22.969 0.933227L23.4432 1.10181C24.2984 1.48492 24.7861 1.78787 24.9063 2.01065C24.9155 2.03847 25.0131 2.09906 25.1989 2.19241C25.394 2.31359 25.5008 2.402 25.5192 2.45765C25.5932 2.58762 25.5501 2.87923 25.39 3.33248C25.3243 3.4619 25.2538 3.62373 25.1786 3.81795L25.1079 4.02609L25.0516 4.13702C24.4029 5.69064 23.5858 7.43811 22.6004 9.37945C22.2905 10.0359 21.84 10.9234 21.2489 12.0419L18.025 18.3928L17.786 18.7948L17.5889 19.1831C17.2794 19.7469 17.1291 20.0659 17.1381 20.14C17.1472 20.2141 17.3421 20.3585 17.7231 20.573L17.96 20.7268L18.183 20.8388C18.3503 20.9228 18.4571 20.9881 18.5035 21.0346L18.8099 21.2581L19.1302 21.5233C19.3343 21.7186 19.4407 21.8765 19.4496 21.997C19.4582 22.1637 19.3128 22.4503 19.0132 22.8567L18.8166 23.106L18.6341 23.3276C18.4937 23.5123 18.3908 23.6462 18.3253 23.7293L17.7627 24.6997C17.547 25.0787 17.3553 25.2724 17.1877 25.281C17.1597 25.2902 16.7785 25.1451 16.044 24.8459C15.2909 24.5372 14.84 24.3456 14.6913 24.2709C14.1987 24.0467 13.5573 23.7478 12.7672 23.3742L12.307 23.1779L11.8888 22.9678C11.582 22.8369 11.345 22.7295 11.1777 22.6455L10.6479 22.3794L10.4668 22.2537L10.2717 22.1556C9.99285 22.0156 9.66287 21.8615 9.28173 21.6933C9.26314 21.6839 9.21198 21.6698 9.12824 21.651L8.86323 21.5527C8.62141 21.4776 8.4728 21.3798 8.4174 21.2592C8.39899 21.2035 8.42752 21.0554 8.50296 20.8149L8.61586 20.5235L8.71495 20.1905C8.90339 19.6354 9.04914 19.2794 9.15219 19.1223C9.33033 18.8174 9.53635 18.5264 9.77025 18.2494C10.0509 17.9263 10.2797 17.7512 10.4568 17.7241C10.6152 17.6877 10.8154 17.707 11.0572 17.782L11.3642 17.8666L11.6292 17.9649L11.9919 18.0775L12.3127 18.2038C12.5265 18.288 12.6847 18.321 12.7872 18.3029C12.8152 18.2937 12.9272 18.2339 13.1232 18.1236L13.1795 18.0126L13.1798 17.9432C13.1707 17.869 13.1756 17.8135 13.1944 17.7765C13.2693 17.6749 13.3348 17.5918 13.3909 17.5272C13.4377 17.481 13.4797 17.4441 13.5171 17.4165C13.601 17.389 13.6524 17.3568 13.6711 17.3198C13.7274 17.2089 13.798 17.0239 13.8828 16.7649L14.0098 16.4458L14.1509 16.099L14.7562 14.9065C15.0751 14.3242 15.291 13.8989 15.4038 13.6308C15.526 13.3441 15.7418 12.942 16.0511 12.4245L16.4447 11.7869C16.6041 11.5189 16.7448 11.2647 16.8668 11.0244L17.444 9.88729C17.5191 9.73938 17.6504 9.50369 17.8379 9.18023L18.0491 8.76422L18.2881 8.36223C18.654 7.68747 18.8278 7.29911 18.8096 7.19715C18.7728 7.08585 18.6052 7.07131 18.3069 7.15351C18.2044 7.17163 18.0972 7.199 17.9853 7.23562L17.7755 7.30427C17.2629 7.39489 16.9556 7.40296 16.8535 7.32846C16.7699 7.26329 16.664 6.96647 16.5357 6.43799C16.4993 6.23406 16.472 6.05796 16.454 5.90969L16.385 5.70101L16.2881 5.47832L16.3311 5.18671C16.3595 5.08493 16.3691 4.99697 16.3601 4.92283L16.3195 4.58922L16.2651 4.21386C16.2384 3.89882 16.2861 3.62112 16.4081 3.38077C16.5864 3.02948 17.3425 2.57392 18.6764 2.0141C18.9469 1.89474 19.2408 1.75232 19.5581 1.58683L19.9501 1.36605L20.328 1.17301C20.9158 0.888157 21.438 0.709572 21.8946 0.637252C21.9785 0.609792 22.0623 0.605489 22.146 0.624342L22.2576 0.680354Z" fill="white"/>
|
||||
<path d="M23.9091 76.2443C24.5784 76.534 25.4198 76.8754 26.4333 77.2684L27.7305 77.7458C28.2605 77.9424 28.7301 78.1203 29.1392 78.2793C29.2508 78.3353 29.4088 78.3915 29.6135 78.4479L30.0878 78.6165C30.4691 78.7384 30.706 78.869 30.7986 79.0083C30.882 79.1198 30.895 79.3468 30.8378 79.6893C30.7808 79.9855 30.6864 80.3093 30.5546 80.6608C30.5263 80.7625 30.4794 80.8318 30.4141 80.8686L30.2739 81.007L30.2173 81.1874L30.1886 81.3818C30.1507 81.5484 30.0944 81.6593 30.0197 81.7146L29.8794 81.853C29.8327 81.8992 29.8046 81.9315 29.7952 81.95C29.7483 82.0424 29.72 82.1442 29.7102 82.2553L29.6674 82.4774C29.6387 82.6718 29.6009 82.8153 29.554 82.9077C29.5164 82.9817 29.451 83.0416 29.3577 83.0876C29.3204 83.1152 29.283 83.1429 29.2456 83.1705C29.1895 83.2351 29.1287 83.309 29.0632 83.3921C29.0538 83.4106 29.0443 83.4522 29.0347 83.517L29.0201 83.6837C28.9916 83.8318 28.9074 83.9287 28.7675 83.9745C28.5717 84.0386 28.2972 83.9819 27.9441 83.8045L27.4143 83.5385C27.2007 83.408 27.0148 83.3146 26.8567 83.2584C26.5127 83.1089 26.0711 82.9219 25.5319 82.6975L24.8765 82.4032L24.1792 82.1225C23.4819 81.8419 22.6173 81.4773 21.5854 81.0287L20.2747 80.44C19.7169 80.2062 19.2381 80.0006 18.8384 79.823C18.6897 79.7484 18.4665 79.6595 18.1689 79.5564L17.7645 79.3881L17.3739 79.2616C16.7138 78.9996 16.3328 78.7851 16.231 78.618C16.2032 78.5808 16.199 78.4743 16.2183 78.2983C16.2191 78.1131 16.2337 77.9464 16.2622 77.7983C16.2907 77.6502 16.3426 77.4791 16.4179 77.2848L16.6296 76.7299C16.7897 76.2767 16.8843 75.9065 16.9134 75.6195L17.392 74.6765C17.5707 74.2326 17.6928 73.9691 17.7583 73.886C17.8612 73.7753 18.1039 73.6187 18.4865 73.4164L18.7245 73.2923L18.9903 73.1822L19.2283 73.0581L19.4521 72.9617C19.6947 72.8515 20.049 72.7278 20.5152 72.5907L21.4803 72.2749C21.5082 72.2658 21.5503 72.2289 21.6064 72.1643L21.6346 72.1088L21.7047 72.0396L21.8585 72.0124L22.0263 71.9575C22.1941 71.9026 22.334 71.8568 22.4459 71.8202L22.9077 71.5997L23.3836 71.3514C23.7848 71.1585 24.1114 70.9977 24.3633 70.869L24.839 70.6902L25.2867 70.4974L25.4968 70.3593L25.7209 70.1935C25.9171 70.0368 26.0758 69.9309 26.1971 69.8758C26.3092 69.7928 26.4678 69.7101 26.673 69.6275L27.1349 69.407C27.3962 69.2598 27.7089 69.0573 28.0731 68.7993L28.9835 68.1777C29.6466 67.6986 30.2444 67.2331 30.777 66.7813L31.0854 66.5186L31.4219 66.2003C31.6742 65.979 31.8752 65.7667 32.025 65.5635C32.4181 65.0649 32.5224 64.5836 32.338 64.1198C32.3196 64.0641 32.2732 64.0176 32.1989 63.9803L32.1014 63.8965L32.0319 63.8268L32.0043 63.7433L32.0045 63.6738C32.0047 63.6275 31.9816 63.5811 31.9352 63.5346C31.5267 63.2366 31.0616 63.0959 30.5401 63.1124C30.3538 63.1116 30.1675 63.1341 29.9811 63.1796L29.7293 63.262L29.5336 63.3029C29.3751 63.3394 29.2167 63.3758 29.0582 63.4122C28.853 63.4948 28.6758 63.5682 28.5266 63.6325C28.4986 63.6416 28.4565 63.6785 28.4004 63.7431L28.3444 63.7846L28.2742 63.8538C28.2463 63.8629 28.1857 63.8673 28.0926 63.867C27.9995 63.8666 27.9342 63.8803 27.8969 63.9079L27.4767 64.1841C27.2901 64.276 27.1224 64.3078 26.9735 64.2794C26.7223 64.2229 26.5179 64.097 26.3604 63.9019L26.1936 63.6789L26.0548 63.47C25.4059 62.7264 24.9472 62.1364 24.6789 61.7C24.6697 61.6722 24.6326 61.6304 24.5676 61.5746C24.5119 61.5466 24.4701 61.514 24.4423 61.4768L24.3457 61.1847C24.2905 61.0177 24.2911 60.8556 24.3476 60.6984C24.4041 60.5411 24.8104 60.2231 25.5666 59.7444L26.2948 59.2749C26.5563 59.0814 26.7291 58.9709 26.813 58.9434C26.9343 58.8883 27.1579 58.8383 27.4841 58.7932L27.8755 58.7114L28.3089 58.6158L28.6586 58.4782L29.0222 58.3824C29.423 58.2821 29.7725 58.214 30.0706 58.1781C30.406 58.1609 30.7599 58.153 31.1323 58.1544C31.57 58.1654 31.9749 58.1948 32.3473 58.2426C32.5241 58.2618 32.9008 58.3698 33.4774 58.5665C33.9795 58.7723 34.4861 58.9919 34.9974 59.2255C35.555 59.5056 35.9499 59.7387 36.1819 59.9248L36.1817 59.9943L36.2093 60.0778C36.2183 60.1519 36.2321 60.1936 36.2507 60.203L36.696 60.6354C36.8723 60.7936 37.0113 60.9562 37.1131 61.1234C37.5022 61.6436 37.7414 62.367 37.8309 63.2937C37.903 63.8868 37.8774 64.4887 37.7539 65.0996C37.6113 65.8401 37.3291 66.5568 36.9073 67.2499C36.8324 67.3515 36.7342 67.4762 36.6125 67.6239L36.2757 68.0116L35.3637 69.0501C34.9989 69.447 34.6763 69.7838 34.3959 70.0606C33.4428 70.8813 32.5462 71.5447 31.7061 72.0509L31.5661 72.1198L31.454 72.2027C31.3699 72.2765 31.2719 72.3317 31.1601 72.3683C30.5723 72.6532 29.7604 73.0807 28.7245 73.6509C28.4446 73.7888 28.062 73.9911 27.5767 74.2578L26.359 74.8644C25.3698 75.3885 24.5532 75.8485 23.9091 76.2443Z" fill="white"/>
|
||||
<path d="M39.4239 0L45 2.80102L5.57613 80.4516L0 77.6506L39.4239 0Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="40" height="58" viewBox="0 0 40 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4Z" fill="white"/>
|
||||
<path d="M2 56V34.4M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4ZM2 34.4V2" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 914 B |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,113 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<title>NoChess</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="web_chess">
|
||||
<div class="pgn_moves_board">
|
||||
<div class="move_board_bg"></div>
|
||||
<div class="moves_list">
|
||||
<div class="moves_list_mobile">
|
||||
</div>
|
||||
</div>
|
||||
<div class="moves_control_buttons">
|
||||
<div class="move_btn first_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/first.png" alt="first">
|
||||
</div>
|
||||
<div class="move_btn prev_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/prev.png" alt="prev">
|
||||
</div>
|
||||
<div class="move_btn next_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/next.png" alt="next">
|
||||
</div>
|
||||
<div class="move_btn last_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/last.png" alt="last">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="board-wrapper">
|
||||
<canvas id="chessBoard"></canvas>
|
||||
</div>
|
||||
<div class="main_board">
|
||||
<div class="main_board_bg"></div>
|
||||
<div class="player_block player_black">
|
||||
<div class="player_name_black">guest-acc</div>
|
||||
<div class="player_avatar avatar_black"></div>
|
||||
</div>
|
||||
<div class="divider_line top_line">
|
||||
<svg width="270" height="4" viewBox="0 0 270 4">
|
||||
<path d="M75 0H95V4H75V0Z" fill="white" />
|
||||
<path d="M50 0H70V4H50V0Z" fill="white" />
|
||||
<path d="M25 0H45V4H25V0Z" fill="white" />
|
||||
<path d="M0 0H20V4H0V0Z" fill="white" />
|
||||
<path d="M100 0H120V4H100V0Z" fill="white" />
|
||||
<path d="M125 0H145V4H125V0Z" fill="white" />
|
||||
<path d="M150 0H170V4H150V0Z" fill="white" />
|
||||
<path d="M175 0H195V4H175V0Z" fill="white" />
|
||||
<path d="M200 0H220V4H200V0Z" fill="white" />
|
||||
<path d="M225 0H245V4H225V0Z" fill="white" />
|
||||
<path d="M250 0H270V4H250V0Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="timer timer_black">--:--</div>
|
||||
<div class="board_menu">
|
||||
<div class="menu_btn" id="flip_board_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-flip.svg" alt="flip" />
|
||||
</div>
|
||||
<div class="menu_btn" id="history_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-history.svg" alt="history" />
|
||||
</div>
|
||||
<div class="menu_btn" id="share_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-share.svg" alt="share" />
|
||||
</div>
|
||||
<div class="menu_btn menu_more_btn" id="more_btn" aria-label="toggle menu">
|
||||
<span class="more_dots" aria-hidden="true">
|
||||
<span class="more_dot more_dot_top"></span>
|
||||
<span class="more_dot more_dot_mid"></span>
|
||||
<span class="more_dot more_dot_bottom"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timer timer_white">--:--</div>
|
||||
<div class="divider_line bottom_line">
|
||||
<svg width="270" height="4" viewBox="0 0 270 4">
|
||||
<path d="M75 0H95V4H75V0Z" fill="white" />
|
||||
<path d="M50 0H70V4H50V0Z" fill="white" />
|
||||
<path d="M25 0H45V4H25V0Z" fill="white" />
|
||||
<path d="M0 0H20V4H0V0Z" fill="white" />
|
||||
<path d="M100 0H120V4H100V0Z" fill="white" />
|
||||
<path d="M125 0H145V4H125V0Z" fill="white" />
|
||||
<path d="M150 0H170V4H150V0Z" fill="white" />
|
||||
<path d="M175 0H195V4H175V0Z" fill="white" />
|
||||
<path d="M200 0H220V4H200V0Z" fill="white" />
|
||||
<path d="M225 0H245V4H225V0Z" fill="white" />
|
||||
<path d="M250 0H270V4H250V0Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="player_block player_white">
|
||||
<div class="player_avatar avatar_white"></div>
|
||||
<div class="player_name_white">Sunnex <3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submenu_overlay" id="submenu_overlay"></div>
|
||||
<div class="submenu_panel" id="history_panel">
|
||||
<div class="submenu_title">History</div>
|
||||
<div class="history_games" id="history_games"></div>
|
||||
</div>
|
||||
<div class="submenu_panel" id="share_panel">
|
||||
<div class="submenu_title">Share PGN</div>
|
||||
<textarea id="share_pgn_text"></textarea>
|
||||
<div class="share_actions">
|
||||
<button id="load_pgn_btn" type="button">Load PGN</button>
|
||||
<button id="copy_pgn_btn" type="button">Copy PGN</button>
|
||||
</div>
|
||||
<div id="share_status"></div>
|
||||
</div>
|
||||
<script src="javascript.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,967 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'mr_GranstanderCleanG';
|
||||
src: url('https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/mr_granstandercleang.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1A1224;
|
||||
background-image: url('bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
font-family: 'mr_GranstanderCleanG', Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 573px) and (max-width: 1562px) {
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
width: 1562px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: 0;
|
||||
--desktop-scale: min(calc(100vw / 1562px), calc(100vh / 856px), 1);
|
||||
transform-origin: center center;
|
||||
transform: translate(-50%, -50%) scale(var(--desktop-scale));
|
||||
}
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 856px;
|
||||
height: 856px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pgn_moves_board {
|
||||
width: 333px;
|
||||
height: 856px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.move_board_bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #1E1E1E;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.move_board_bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 80px;
|
||||
height: 48px;
|
||||
background: linear-gradient(180deg, #00000000 0%, #00000040 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pgn_moves_board.has_scroll .move_board_bg::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.moves_list {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.moves_list_mobile {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.move_row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 1fr;
|
||||
align-items: center;
|
||||
min-height: 52px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.move_number {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.current_move {
|
||||
background: #B7B7B71A;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.move_white:empty,
|
||||
.move_black:empty {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.moves_control_buttons {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.move_btn {
|
||||
width: 75px;
|
||||
height: 37px;
|
||||
background: #2D2D2D;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 3.7px -1px #00000040;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.move_btn img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.move_btn:hover {
|
||||
background: #3A3A3A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.move_btn:active {
|
||||
background: #252525;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.main_board {
|
||||
width: 333px;
|
||||
height: 856px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main_board_bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.player_block {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player_black {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.player_white {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black {
|
||||
top: auto;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white {
|
||||
top: 20px;
|
||||
bottom: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_name_black {
|
||||
order: 2;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_name_white {
|
||||
order: 1;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_avatar {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.player_avatar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 189px;
|
||||
height: 189px;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: 0 auto;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: 5px solid #FFFFFF;
|
||||
border-radius: 50%;
|
||||
clip-path: circle(50% at 50% 50%);
|
||||
}
|
||||
|
||||
.avatar_black {
|
||||
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
|
||||
}
|
||||
|
||||
.avatar_white {
|
||||
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
|
||||
}
|
||||
|
||||
.player_name_black {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.player_name_white {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.divider_line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.top_line {
|
||||
top: 260px;
|
||||
}
|
||||
|
||||
.bottom_line {
|
||||
bottom: 260px;
|
||||
}
|
||||
|
||||
.divider_line svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.timer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 139px;
|
||||
height: 41px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.timer_black {
|
||||
top: 280px;
|
||||
}
|
||||
|
||||
.timer_white {
|
||||
bottom: 280px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_black {
|
||||
top: auto;
|
||||
bottom: 280px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_white {
|
||||
top: 280px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.board_menu {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.menu_btn {
|
||||
width: 50px;
|
||||
height: 42px;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.menu_btn:hover {
|
||||
background: #FFFFFF1A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.menu_btn:active {
|
||||
background: #00000080;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu_btn img {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.menu_more_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.more_dots {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.more_dot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #FFFFFF;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.more_dot_top {
|
||||
transform: translate(-50%, calc(-50% - var(--dot-gap, 10px)));
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.more_dot_mid {
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.more_dot_bottom {
|
||||
transform: translate(-50%, calc(-50% + var(--dot-gap, 10px)));
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.menu_more_btn.active .more_dot_top {
|
||||
transform: translate(calc(-50% - var(--dot-gap, 10px)), -50%);
|
||||
}
|
||||
|
||||
.menu_more_btn.active .more_dot_bottom {
|
||||
transform: translate(calc(-50% + var(--dot-gap, 10px)), -50%);
|
||||
}
|
||||
|
||||
.web_chess.mobile_menu_open .menu_more_btn .more_dot_mid {
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
|
||||
.submenu_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #00000080;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.submenu_overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.submenu_panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.98);
|
||||
width: min(540px, 92vw);
|
||||
max-height: 80vh;
|
||||
background: #1e1e1e;
|
||||
border-radius: 15px;
|
||||
border: 1px solid #FFFFFF14;
|
||||
padding: 18px;
|
||||
color: #FFFFFF;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.submenu_panel.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.submenu_title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.history_games {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.history_games.empty {
|
||||
min-height: 120px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history_empty {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history_game_btn {
|
||||
width: 100%;
|
||||
background: #2d2d2d;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #FFFFFF14;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history_game_btn:hover {
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
#share_pgn_text {
|
||||
width: 100%;
|
||||
min-height: 170px;
|
||||
max-height: 55vh;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #FFFFFF1A;
|
||||
background: #121212;
|
||||
color: #FFFFFF;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.share_actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#load_pgn_btn,
|
||||
#copy_pgn_btn {
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: #2d2d2d;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
#load_pgn_btn {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
#load_pgn_btn:hover,
|
||||
#copy_pgn_btn:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
#share_status {
|
||||
margin-top: 10px;
|
||||
min-height: 20px;
|
||||
color: #d9d9d9;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
#share_status.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#share_status.error {
|
||||
color: #ff8f8f;
|
||||
}
|
||||
|
||||
@media (max-width: 572px) {
|
||||
body {
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: clamp(72px, 17.7vw, 101px);
|
||||
background: linear-gradient(180deg, #00000000 0%, #0000008C 100%);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
--mobile-layout-width: 750px;
|
||||
--mobile-board-size: 750px;
|
||||
--mobile-board-top: 233px;
|
||||
--mobile-player-gap: clamp(24px, 5vw, 32px);
|
||||
--mobile-panel-height: clamp(64px, 17.13vw, 98px);
|
||||
--mobile-timer-height: clamp(30px, 7.17vw, 41px);
|
||||
--mobile-pgn-space: clamp(360px, 86vw, 492px);
|
||||
--mobile-layout-height: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-pgn-space));
|
||||
--mobile-fit-scale: min(1, calc((100vw - 2px) / var(--mobile-layout-width)), calc((100dvh - 2px) / var(--mobile-layout-height)));
|
||||
width: var(--mobile-layout-width);
|
||||
height: var(--mobile-layout-height);
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
display: block;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transform-origin: top center;
|
||||
transform: translateX(-50%) scale(var(--mobile-fit-scale));
|
||||
}
|
||||
|
||||
.web_chess::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: var(--mobile-board-top);
|
||||
transform: translateX(-50%);
|
||||
width: var(--mobile-board-size);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main_board {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.main_board_bg,
|
||||
.divider_line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.board_menu {
|
||||
top: 4.2vw;
|
||||
right: 3.1vw;
|
||||
left: auto;
|
||||
transform: none;
|
||||
gap: 2.2vw;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.28s ease, opacity 0.28s ease, background 0.2s;
|
||||
}
|
||||
|
||||
.web_chess:not(.mobile_menu_open) .board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 0;
|
||||
transform: translateX(11vw) scale(0.86);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.web_chess.mobile_menu_open .board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.menu_btn {
|
||||
width: 50px;
|
||||
height: 42px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.menu_btn img {
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.menu_more_btn {
|
||||
--dot-gap: 14px;
|
||||
display: flex;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.menu_more_btn .more_dots {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.menu_more_btn .more_dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.player_block {
|
||||
width: calc(100% - 4.2vw);
|
||||
left: 2.1vw;
|
||||
height: var(--mobile-panel-height);
|
||||
min-height: 64px;
|
||||
border-radius: clamp(10px, 2.6vw, 15px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(10px, 2.8vw, 18px);
|
||||
padding: 0 calc(clamp(96px, 24.3vw, 139px) + max(6vw, 22px)) 0 max(2.4vw, 12px);
|
||||
text-align: left;
|
||||
background: #00000054;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.player_black {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
|
||||
}
|
||||
|
||||
.player_black .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.player_black .player_name_black {
|
||||
order: 2;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.player_white {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.player_avatar {
|
||||
width: min(13.99vw, 80px);
|
||||
height: min(13.99vw, 80px);
|
||||
margin: 0;
|
||||
border-width: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.player_name_black,
|
||||
.player_name_white {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
max-width: min(39vw, 220px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: clamp(14px, 3.8vw, 20px);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.timer {
|
||||
left: auto;
|
||||
right: 6vw;
|
||||
transform: none;
|
||||
width: clamp(96px, 24.3vw, 139px);
|
||||
height: var(--mobile-timer-height);
|
||||
border-radius: 15px;
|
||||
font-size: clamp(12px, 3.8vw, 20px);
|
||||
background: #00000066;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 1px 1px #0000004D;
|
||||
z-index: 7;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.timer_black {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
}
|
||||
|
||||
.timer_white {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.pgn_moves_board {
|
||||
position: absolute;
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + clamp(24px, 5vw, 32px) + var(--mobile-panel-height) + clamp(18px, 4vw, 28px));
|
||||
left: 50%;
|
||||
width: calc(100% - clamp(10px, 2.7vw, 20px));
|
||||
height: clamp(180px, 56vw, 320px);
|
||||
z-index: 3;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity 0.24s ease;
|
||||
}
|
||||
|
||||
.move_board_bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moves_list {
|
||||
top: 0;
|
||||
bottom: clamp(66px, 14vw, 84px);
|
||||
left: 50%;
|
||||
right: auto;
|
||||
width: clamp(260px, 44vw, 360px);
|
||||
transform: translateX(-50%);
|
||||
padding: 0;
|
||||
transition: opacity 0.24s ease;
|
||||
}
|
||||
|
||||
.moves_list::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile {
|
||||
height: 140%;
|
||||
gap: 0.6vw;
|
||||
pointer-events: auto;
|
||||
padding-right: 0;
|
||||
align-items: center;
|
||||
transform: translateY(clamp(10px, 2.2vh, 18px));
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.move_row {
|
||||
min-height: 9vw;
|
||||
font-size: clamp(18px, 4.2vw, 24px);
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: auto auto auto;
|
||||
column-gap: clamp(10px, 2.8vw, 16px);
|
||||
}
|
||||
|
||||
.move_number {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
justify-self: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.current_move {
|
||||
min-height: 0;
|
||||
width: max-content;
|
||||
padding: clamp(4px, 1vw, 8px) clamp(6px, 1.4vw, 10px);
|
||||
}
|
||||
|
||||
.moves_control_buttons {
|
||||
left: 2.6vw;
|
||||
right: 2.6vw;
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
gap: 1.5vw;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.move_btn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: #00000054;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.menu_btn,
|
||||
.move_btn {
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
-webkit-tap-highlight-color: #00000000;
|
||||
}
|
||||
|
||||
.menu_btn:hover {
|
||||
background: #FFFFFF1A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.move_btn:hover {
|
||||
background-color: #00000054;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.menu_btn:active {
|
||||
background: #00000080;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu_more_btn:hover {
|
||||
background-color: #00000054;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.menu_more_btn:active {
|
||||
background-color: #00000054;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.move_btn:active {
|
||||
background-color: #00000054;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.move_btn img {
|
||||
width: 46%;
|
||||
height: 46%;
|
||||
}
|
||||
|
||||
.first_move_btn {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.last_move_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.web_chess:not(.mobile_moves_open) .moves_list {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
|
||||
bottom: auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
|
||||
bottom: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_name_white {
|
||||
order: 2;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_name_black {
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_black {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_white {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
# meta developer: @H_SunMods
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/7c43eb05-4387-48f8-bbb2-20c5fad2f85f.jpg
|
||||
# current ver
|
||||
__version__ = (1, 0, 1)
|
||||
|
||||
from .. import loader, utils
|
||||
from herokutl.types import Message
|
||||
from ..types import InlineCall
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import math
|
||||
|
||||
FHETA_URL = "https://api.fixyres.com/grates"
|
||||
VECTOR_URL = "https://vector-three-sooty.vercel.app/api/devstats"
|
||||
VECTOR_TOPMOD_URL = "https://vector-three-sooty.vercel.app/api/usertopmod?users="
|
||||
|
||||
@loader.tds
|
||||
class DevStats(loader.Module):
|
||||
"""developers stats module"""
|
||||
|
||||
strings = {
|
||||
"name": "DevStats",
|
||||
"loading": "<b>Loading...</b>",
|
||||
"no_data": "<b>Failed to fetch data. Try again later.</b>",
|
||||
"dev_header": "<b><i>Most popular developers:</i></b>\n\n",
|
||||
"devtop_not_found": "<b>Your not found.</b>",
|
||||
"topmod_not_found": "<b>No modules found.</b>",
|
||||
"no_usernames": "<b>No usernames configured.</b> Set them in <code>.fcfg DevStats usernames @username</code>",
|
||||
"select_page": "<b>Select page:</b>",
|
||||
"btn_prev": "◄",
|
||||
"btn_next": "►",
|
||||
"btn_back": "Back",
|
||||
"btn_close": "Close",
|
||||
"like_singl": "like",
|
||||
"just_likes": "likes",
|
||||
"just_dislikes": "dislikes",
|
||||
"devtop_desc": "Your rank in developer leaderboard",
|
||||
"topmod_desc": "Your most popular module and its rank",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль статистики разработчиков",
|
||||
"loading": "<b>Загрузка...</b>",
|
||||
"no_data": "<b>Не удалось получить данные. Попробуйте позже.</b>",
|
||||
"dev_header": "<b><i>Самые популярные разработчики:</i></b>\n\n",
|
||||
"devtop_not_found": "<b>Вы не были найдены.</b>",
|
||||
"topmod_not_found": "<b>Модули не найдены.</b>",
|
||||
"no_usernames": "<b>Юзернеймы не настроены.</b> Укажите в <code>.fcfg DevStats usernames @username</code>",
|
||||
"select_page": "<b>Выберите страницу:</b>",
|
||||
"btn_prev": "◄",
|
||||
"btn_next": "►",
|
||||
"btn_back": "Назад",
|
||||
"btn_close": "Закрыть",
|
||||
"like_singl": "Лайк",
|
||||
"just_likes": "Лайков",
|
||||
"just_dislikes": "Дизлайков",
|
||||
"devtop_desc": "Ваше место в рейтинге разработчиков",
|
||||
"topmod_desc": "Ваш самый популярный модуль и его место в топе",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"provider",
|
||||
"multi",
|
||||
"Data source: multi (fheta + vector combined) | fheta | vector",
|
||||
validator=loader.validators.Choice(["multi", "fheta", "vector"]),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"display_mode",
|
||||
"likes",
|
||||
"Display mode: likes | both",
|
||||
validator=loader.validators.Choice(["likes", "both"]),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"usernames",
|
||||
[],
|
||||
"Your usernames with @ for placeholders",
|
||||
validator=loader.validators.Series(loader.validators.String()),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"excluded_authors",
|
||||
["unknown"],
|
||||
"Authors to exclude from leaderboard",
|
||||
validator=loader.validators.Series(loader.validators.String()),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank1_emoji",
|
||||
"<tg-emoji emoji-id=5429387335626145566>👑</tg-emoji>",
|
||||
"Emoji for rank №1",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank2_emoji",
|
||||
"<tg-emoji emoji-id=5429351167706547656>🌟</tg-emoji>",
|
||||
"Emoji for rank №2",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank3_emoji",
|
||||
"<tg-emoji emoji-id=5429365839314830135>✨</tg-emoji>",
|
||||
"Emoji for rank №3",
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
utils.register_placeholder("devtop", self.placeholder_devtop, self.strings("devtop_desc"))
|
||||
utils.register_placeholder("topmod", self.placeholder_topmod, self.strings("topmod_desc"))
|
||||
|
||||
async def request_api(self, url: str, token: str = None):
|
||||
headers = {"Authorization": token} if token else {}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=15),
|
||||
) as resp:
|
||||
return await resp.json() if resp.status == 200 else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def aggregate_devs(self, data: dict) -> list:
|
||||
excluded = {u.lower() for u in self.config["excluded_authors"]}
|
||||
devs = {}
|
||||
items = data.items() if isinstance(data, dict) else (
|
||||
(e.get("url", i), e) for i, e in enumerate(data)
|
||||
)
|
||||
for _, info in items:
|
||||
author = info.get("author", "").lstrip("@")
|
||||
if not author or author.lower() in excluded:
|
||||
continue
|
||||
if author not in devs:
|
||||
devs[author] = {"likes": 0, "dislikes": 0}
|
||||
devs[author]["likes"] += int(info.get("likes", 0) or 0)
|
||||
devs[author]["dislikes"] += int(info.get("dislikes", 0) or 0)
|
||||
return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
def aggregate_vector(self, data: list) -> list:
|
||||
excluded = {u.lower() for u in self.config["excluded_authors"]}
|
||||
devs = {}
|
||||
for entry in data:
|
||||
author = entry.get("author", "").lstrip("@")
|
||||
if not author or author.lower() in excluded:
|
||||
continue
|
||||
if author not in devs:
|
||||
devs[author] = {"likes": 0, "dislikes": 0}
|
||||
devs[author]["likes"] += int(entry.get("likes", 0) or 0)
|
||||
devs[author]["dislikes"] += int(entry.get("dislikes", 0) or 0)
|
||||
return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
def merge_sources(self, fheta_devs: list, vector_devs: list) -> list:
|
||||
merged = {}
|
||||
for username, stats in fheta_devs:
|
||||
merged[username.lower()] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]}
|
||||
for username, stats in vector_devs:
|
||||
key = username.lower()
|
||||
if key in merged:
|
||||
merged[key]["likes"] += stats["likes"]
|
||||
merged[key]["dislikes"] += stats["dislikes"]
|
||||
else:
|
||||
merged[key] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]}
|
||||
result = [(v["name"], {"likes": v["likes"], "dislikes": v["dislikes"]}) for v in merged.values()]
|
||||
return sorted(result, key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
async def fetch_sorted_devs(self) -> list:
|
||||
provider = self.config["provider"]
|
||||
if provider == "fheta":
|
||||
data = await self.request_api(FHETA_URL)
|
||||
return self.aggregate_devs(data) if data else []
|
||||
if provider == "vector":
|
||||
data = await self.request_api(VECTOR_URL)
|
||||
return self.aggregate_vector(data) if isinstance(data, list) else []
|
||||
# multi
|
||||
fheta_data, vector_data = await asyncio.gather(
|
||||
self.request_api(FHETA_URL),
|
||||
self.request_api(VECTOR_URL),
|
||||
)
|
||||
fheta_devs = self.aggregate_devs(fheta_data) if fheta_data else []
|
||||
vector_devs = self.aggregate_vector(vector_data) if isinstance(vector_data, list) else []
|
||||
if not fheta_devs and not vector_devs:
|
||||
return []
|
||||
return self.merge_sources(fheta_devs, vector_devs)
|
||||
|
||||
def extract_module_name(self, key: str) -> str:
|
||||
return key.strip().split("/")[-1].removesuffix(".py")
|
||||
|
||||
def format_stats(self, likes: int, dislikes: int) -> str:
|
||||
mode = self.config["display_mode"]
|
||||
lw = self.strings["like_singl"] if likes == 1 else self.strings["just_likes"]
|
||||
if mode == "both":
|
||||
return f"({likes} {lw} | {dislikes} {self.strings['just_dislikes']})"
|
||||
return f"({likes} {lw})"
|
||||
|
||||
def dev_entry(self, rank: int, username: str, likes: int, dislikes: int) -> str:
|
||||
stats = self.format_stats(likes, dislikes)
|
||||
emoji = self.config[f"rank{rank}_emoji"] if rank <= 3 else ""
|
||||
safe = utils.escape_html(username)
|
||||
if emoji:
|
||||
return f"{rank}. @{safe} <b>{stats} | </b>{emoji}\n"
|
||||
return f"{rank}. @{safe} <b>{stats}</b>\n"
|
||||
|
||||
def kb_dev_page(self, sorted_devs: list, page: int) -> str:
|
||||
start = page * 10
|
||||
text = self.strings["dev_header"]
|
||||
for i, (username, stats) in enumerate(sorted_devs[start:start + 10]):
|
||||
rank = start + i + 1
|
||||
text += self.dev_entry(rank, username, stats["likes"], stats["dislikes"])
|
||||
return text
|
||||
|
||||
def nav_markup(self, page: int, total: int, on_prev, on_next, on_page) -> list:
|
||||
return [
|
||||
[
|
||||
{"text": self.strings["btn_prev"], "callback": on_prev},
|
||||
{"text": f"{page + 1}/{total}", "callback": on_page},
|
||||
{"text": self.strings["btn_next"], "callback": on_next},
|
||||
],
|
||||
[{"text": self.strings["btn_close"], "action": "close"}],
|
||||
]
|
||||
|
||||
def page_selector_markup(self, total: int, page_cb_factory, on_back) -> list:
|
||||
buttons, row = [], []
|
||||
for p in range(total):
|
||||
row.append({"text": str(p + 1), "callback": page_cb_factory(p)})
|
||||
if len(row) == 5:
|
||||
buttons.append(row)
|
||||
row = []
|
||||
if row:
|
||||
buttons.append(row)
|
||||
buttons.append([{"text": self.strings["btn_back"], "callback": on_back}])
|
||||
return buttons
|
||||
|
||||
async def placeholder_devtop(self) -> str:
|
||||
usernames = {u.lstrip("@").lower() for u in self.config["usernames"]}
|
||||
if not usernames:
|
||||
return self.strings["no_usernames"]
|
||||
sorted_devs = await self.fetch_sorted_devs()
|
||||
if not sorted_devs:
|
||||
return self.strings["no_data"]
|
||||
for rank, (username, _) in enumerate(sorted_devs, 1):
|
||||
if username.lower() in usernames:
|
||||
return f"{rank}"
|
||||
return self.strings["devtop_not_found"]
|
||||
|
||||
async def placeholder_topmod(self) -> str:
|
||||
usernames = {u.lstrip("@").lower() for u in self.config["usernames"]}
|
||||
if not usernames:
|
||||
return self.strings["no_usernames"]
|
||||
|
||||
provider = self.config["provider"]
|
||||
joined_usernames = ",".join(sorted(usernames))
|
||||
|
||||
if provider in {"vector", "multi"}:
|
||||
data = await self.request_api(f"{VECTOR_TOPMOD_URL}{joined_usernames}")
|
||||
if isinstance(data, dict) and data.get("name") and data.get("rank"):
|
||||
return f"{data['name']} ({data['rank']})"
|
||||
if provider == "vector":
|
||||
return self.strings["topmod_not_found"] if data else self.strings["no_data"]
|
||||
|
||||
data = await self.request_api(FHETA_URL)
|
||||
if not data:
|
||||
return self.strings["no_data"]
|
||||
all_sorted = sorted(
|
||||
[(self.extract_module_name(k), v) for k, v in data.items()],
|
||||
key=lambda x: int(x[1].get("likes", 0) or 0),
|
||||
reverse=True,
|
||||
)
|
||||
user_mods = [
|
||||
(name, val)
|
||||
for name, val in all_sorted
|
||||
if val.get("author", "").lstrip("@").lower() in usernames
|
||||
]
|
||||
if not user_mods:
|
||||
return self.strings["topmod_not_found"]
|
||||
top_name = user_mods[0][0]
|
||||
global_rank = next(
|
||||
(i + 1 for i, (name, _) in enumerate(all_sorted) if name == top_name),
|
||||
None,
|
||||
)
|
||||
return (
|
||||
f"{top_name} ({global_rank})"
|
||||
if global_rank
|
||||
else self.strings["topmod_not_found"]
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Статистика топ разработчиков")
|
||||
async def devstats(self, message: Message):
|
||||
"""Top Developers statistics"""
|
||||
await utils.answer(message, self.strings["loading"])
|
||||
sorted_devs = await self.fetch_sorted_devs()
|
||||
if not sorted_devs:
|
||||
return await utils.answer(message, self.strings["no_data"])
|
||||
total_pages = max(1, math.ceil(len(sorted_devs) / 10))
|
||||
state = {"page": 0}
|
||||
|
||||
def markup():
|
||||
return self.nav_markup(state["page"], total_pages, on_prev, on_next, on_page)
|
||||
|
||||
def render():
|
||||
return self.kb_dev_page(sorted_devs, state["page"])
|
||||
|
||||
async def on_prev(call: InlineCall):
|
||||
state["page"] = max(0, state["page"] - 1)
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
async def on_next(call: InlineCall):
|
||||
state["page"] = min(total_pages - 1, state["page"] + 1)
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
async def on_page(call: InlineCall):
|
||||
await call.edit(
|
||||
self.strings["select_page"],
|
||||
reply_markup=self.page_selector_markup(total_pages, make_page_cb, on_back),
|
||||
)
|
||||
|
||||
def make_page_cb(p):
|
||||
async def go(call: InlineCall):
|
||||
state["page"] = p
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
return go
|
||||
|
||||
async def on_back(call: InlineCall):
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
await utils.answer(message, render(), reply_markup=markup())
|
||||
@@ -1,168 +0,0 @@
|
||||
# requires: python-ffmpeg
|
||||
# meta developer: @SunnexGB
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg
|
||||
|
||||
# Note
|
||||
# This is a fork module from @KeyZenD.
|
||||
# Here is a link to the original module: https://github.com/KeyZenD/modules/blob/master/Circles.py
|
||||
|
||||
from .. import loader, utils
|
||||
from PIL import Image, ImageDraw, ImageOps, ImageFilter
|
||||
import io
|
||||
from telethon.tl.types import DocumentAttributeFilename
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
|
||||
@loader.tds
|
||||
class ForkCircles(loader.Module):
|
||||
"""rounds everything - reply to message"""
|
||||
strings = {
|
||||
"name": "ForkCircles",
|
||||
"processing_image": "<b>Processing image</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"processing_video": "<b>Processing video</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"no_reply": "<b><emoji document_id=5260249440450520061>🤚</emoji>|reply to image/sticker or video/gif!</b>",
|
||||
"download": "<b>downloading</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"ffprobe_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|error`ffmpeg is installed?</b>",
|
||||
"ffmpeg_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ffmpeg error`:</b> {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Округляет всё - ответом на сообщение",
|
||||
"processing_image": "<b>Обработка изображения</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"processing_video": "<b>Обработка видео</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"no_reply": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ответьте на изображение/стикер или видео/gif!</b>",
|
||||
"download": "<b>Скачивание</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"ffprobe_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|еррорь ffmpeg установил?</b>",
|
||||
"ffmpeg_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ffmpeg еррорь:</b> {error}",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.strings['name']
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
|
||||
@loader.sudo
|
||||
async def roundcmd(self, message):
|
||||
"""<Reply to image/sticker or video/gif>"""
|
||||
reply = None
|
||||
if message.is_reply:
|
||||
reply = await message.get_reply_message()
|
||||
data = await check_media(reply)
|
||||
if isinstance(data, bool):
|
||||
await utils.answer(message, self.strings['no_reply'])
|
||||
return
|
||||
else:
|
||||
await utils.answer(message, self.strings['no_reply'])
|
||||
return
|
||||
data, type = data
|
||||
if type == "img":
|
||||
await message.edit(self.strings['processing_image'])
|
||||
img = io.BytesIO()
|
||||
bytes = await message.client.download_file(data, img)
|
||||
im = Image.open(img)
|
||||
w, h = im.size
|
||||
img = Image.new("RGBA", (w,h), (0,0,0,0))
|
||||
img.paste(im, (0, 0))
|
||||
m = min(w, h)
|
||||
img = img.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2))
|
||||
w, h = img.size
|
||||
mask = Image.new('L', (w, h), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.ellipse((10, 10, w-10, h-10), fill=255)
|
||||
mask = mask.filter(ImageFilter.GaussianBlur(2))
|
||||
img = ImageOps.fit(img, (w, h))
|
||||
img.putalpha(mask)
|
||||
im = io.BytesIO()
|
||||
im.name = "img.webp"
|
||||
img.save(im)
|
||||
im.seek(0)
|
||||
await message.client.send_file(message.to_id, im, reply_to=reply)
|
||||
else:
|
||||
await message.edit(self.strings['processing_video'])
|
||||
await message.client.download_file(data, "video.mp4")
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=width,height', '-of', 'json', 'video.mp4'
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
return
|
||||
info = json.loads(proc.stdout or '{}')
|
||||
streams = info.get('streams', [])
|
||||
if not streams:
|
||||
return
|
||||
w = int(streams[0].get('width', 0))
|
||||
h = int(streams[0].get('height', 0))
|
||||
m = min(w, h)
|
||||
x = (w - m) // 2
|
||||
y = (h - m) // 2
|
||||
await message.edit(self.strings['download'])
|
||||
crop_filter = f"crop={m}:{m}:{x}:{y}"
|
||||
is_gif = getattr(reply, 'gif', False) or False
|
||||
if is_gif:
|
||||
cmd = [
|
||||
'ffmpeg', '-y', '-i', 'video.mp4',
|
||||
'-vf', crop_filter,
|
||||
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23',
|
||||
'-pix_fmt', 'yuv420p', '-an',
|
||||
'result.mp4'
|
||||
]
|
||||
else:
|
||||
cmd = [
|
||||
'ffmpeg', '-y', '-i', 'video.mp4',
|
||||
'-vf', crop_filter,
|
||||
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23',
|
||||
'-c:a', 'aac', '-strict', '-2',
|
||||
'result.mp4'
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
err = proc.stderr or ''
|
||||
lines = [l for l in err.splitlines() if l.strip()]
|
||||
filtered = []
|
||||
for l in lines:
|
||||
low = l.lower()
|
||||
if low.startswith('ffmpeg version') or low.startswith('built with') or low.startswith('configuration:'):
|
||||
continue
|
||||
filtered.append(l)
|
||||
if not filtered:
|
||||
safe = err[:300]
|
||||
else:
|
||||
safe = '\n'.join(filtered[-6:])
|
||||
await utils.answer(message, self.strings['ffmpeg_failed'].format(error=safe))
|
||||
return
|
||||
await message.client.send_file(message.to_id, 'result.mp4', video_note=(not is_gif), reply_to=reply)
|
||||
finally:
|
||||
if os.path.exists('video.mp4'):
|
||||
os.remove('video.mp4')
|
||||
if os.path.exists('result.mp4'):
|
||||
os.remove('result.mp4')
|
||||
await message.delete()
|
||||
|
||||
|
||||
async def check_media(reply):
|
||||
type = "img"
|
||||
if reply and reply.media:
|
||||
if reply.photo:
|
||||
data = reply.photo
|
||||
elif reply.document:
|
||||
if DocumentAttributeFilename(file_name='AnimatedSticker.tgs') in reply.media.document.attributes:
|
||||
return False
|
||||
if reply.gif or reply.video:
|
||||
type = "vid"
|
||||
if reply.audio or reply.voice:
|
||||
return False
|
||||
data = reply.media.document
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
if not data or data is None:
|
||||
return False
|
||||
else:
|
||||
return (data, type)
|
||||
@@ -1,171 +0,0 @@
|
||||
# meta developer: @H_SunMods
|
||||
# meta pic: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png
|
||||
# meta banner: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png
|
||||
# meta fhsdesc: Game, Игра, Hangman, Висилица
|
||||
# крутой баннер да?
|
||||
|
||||
#current version
|
||||
__version__ = ("d", "i", "e")
|
||||
|
||||
import random
|
||||
import aiohttp
|
||||
from .. import loader, utils
|
||||
from herokutl.types import Message
|
||||
from ..types import InlineCall
|
||||
|
||||
words = "https://github.com/SunnexGB/Heroku-Modules/raw/refs/heads/main/Assets/Hangman/words.txt"
|
||||
|
||||
@loader.tds
|
||||
class Hangman(loader.Module):
|
||||
"""Висилица"""
|
||||
|
||||
strings = {
|
||||
"name": "Hangman",
|
||||
"caption": "<b>{word}</b>",
|
||||
"won": "<b>{word}</b>",
|
||||
"over": "Игра окончена. Слово было: <b>{word}</b>",
|
||||
"already": "<b>Сосиски свои ебаные убрал от этой буквы!</b>",
|
||||
}
|
||||
|
||||
HangmanLives = [
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/full_hp.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/1.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/2.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/3.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/4.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/5.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/6.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/7.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/8.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/9.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png",
|
||||
]
|
||||
|
||||
async def client_ready(self):
|
||||
await self.load_words()
|
||||
|
||||
async def load_words(self):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(words) as resp:
|
||||
resp.raise_for_status()
|
||||
text = await resp.text()
|
||||
self.words = [
|
||||
word.strip().upper()
|
||||
for word in text.splitlines()
|
||||
if word.strip()
|
||||
]
|
||||
except Exception:
|
||||
self.words = ["СЛЕНДЕРМЕН", "КАЗИНО", "АЗАРТ"]
|
||||
|
||||
def field_w_letters(self, word, guessed):
|
||||
return " ".join(l if l in guessed else "_" for l in word)
|
||||
|
||||
def caption(self, state):
|
||||
return self.strings["caption"].format(
|
||||
word=self.field_w_letters(state["word"], state["guessed"]),
|
||||
)
|
||||
|
||||
def russian_latters(self, state, chat_id):
|
||||
guessed = state["guessed"]
|
||||
wrong = state["wrong"]
|
||||
return [
|
||||
[
|
||||
{"text": "А", "callback": self.on_letter, "args": (chat_id, "А"), **({"style": "success"} if "А" in guessed else {"style": "danger"} if "А" in wrong else {})},
|
||||
{"text": "Б", "callback": self.on_letter, "args": (chat_id, "Б"), **({"style": "success"} if "Б" in guessed else {"style": "danger"} if "Б" in wrong else {})},
|
||||
{"text": "В", "callback": self.on_letter, "args": (chat_id, "В"), **({"style": "success"} if "В" in guessed else {"style": "danger"} if "В" in wrong else {})},
|
||||
{"text": "Г", "callback": self.on_letter, "args": (chat_id, "Г"), **({"style": "success"} if "Г" in guessed else {"style": "danger"} if "Г" in wrong else {})},
|
||||
{"text": "Д", "callback": self.on_letter, "args": (chat_id, "Д"), **({"style": "success"} if "Д" in guessed else {"style": "danger"} if "Д" in wrong else {})},
|
||||
{"text": "Е", "callback": self.on_letter, "args": (chat_id, "Е"), **({"style": "success"} if "Е" in guessed else {"style": "danger"} if "Е" in wrong else {})},
|
||||
{"text": "Ё", "callback": self.on_letter, "args": (chat_id, "Ё"), **({"style": "success"} if "Ё" in guessed else {"style": "danger"} if "Ё" in wrong else {})},
|
||||
{"text": "Ж", "callback": self.on_letter, "args": (chat_id, "Ж"), **({"style": "success"} if "Ж" in guessed else {"style": "danger"} if "Ж" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "З", "callback": self.on_letter, "args": (chat_id, "З"), **({"style": "success"} if "З" in guessed else {"style": "danger"} if "З" in wrong else {})},
|
||||
{"text": "И", "callback": self.on_letter, "args": (chat_id, "И"), **({"style": "success"} if "И" in guessed else {"style": "danger"} if "И" in wrong else {})},
|
||||
{"text": "Й", "callback": self.on_letter, "args": (chat_id, "Й"), **({"style": "success"} if "Й" in guessed else {"style": "danger"} if "Й" in wrong else {})},
|
||||
{"text": "К", "callback": self.on_letter, "args": (chat_id, "К"), **({"style": "success"} if "К" in guessed else {"style": "danger"} if "К" in wrong else {})},
|
||||
{"text": "Л", "callback": self.on_letter, "args": (chat_id, "Л"), **({"style": "success"} if "Л" in guessed else {"style": "danger"} if "Л" in wrong else {})},
|
||||
{"text": "М", "callback": self.on_letter, "args": (chat_id, "М"), **({"style": "success"} if "М" in guessed else {"style": "danger"} if "М" in wrong else {})},
|
||||
{"text": "Н", "callback": self.on_letter, "args": (chat_id, "Н"), **({"style": "success"} if "Н" in guessed else {"style": "danger"} if "Н" in wrong else {})},
|
||||
{"text": "О", "callback": self.on_letter, "args": (chat_id, "О"), **({"style": "success"} if "О" in guessed else {"style": "danger"} if "О" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "П", "callback": self.on_letter, "args": (chat_id, "П"), **({"style": "success"} if "П" in guessed else {"style": "danger"} if "П" in wrong else {})},
|
||||
{"text": "Р", "callback": self.on_letter, "args": (chat_id, "Р"), **({"style": "success"} if "Р" in guessed else {"style": "danger"} if "Р" in wrong else {})},
|
||||
{"text": "С", "callback": self.on_letter, "args": (chat_id, "С"), **({"style": "success"} if "С" in guessed else {"style": "danger"} if "С" in wrong else {})},
|
||||
{"text": "Т", "callback": self.on_letter, "args": (chat_id, "Т"), **({"style": "success"} if "Т" in guessed else {"style": "danger"} if "Т" in wrong else {})},
|
||||
{"text": "У", "callback": self.on_letter, "args": (chat_id, "У"), **({"style": "success"} if "У" in guessed else {"style": "danger"} if "У" in wrong else {})},
|
||||
{"text": "Ф", "callback": self.on_letter, "args": (chat_id, "Ф"), **({"style": "success"} if "Ф" in guessed else {"style": "danger"} if "Ф" in wrong else {})},
|
||||
{"text": "Х", "callback": self.on_letter, "args": (chat_id, "Х"), **({"style": "success"} if "Х" in guessed else {"style": "danger"} if "Х" in wrong else {})},
|
||||
{"text": "Ц", "callback": self.on_letter, "args": (chat_id, "Ц"), **({"style": "success"} if "Ц" in guessed else {"style": "danger"} if "Ц" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "Ч", "callback": self.on_letter, "args": (chat_id, "Ч"), **({"style": "success"} if "Ч" in guessed else {"style": "danger"} if "Ч" in wrong else {})},
|
||||
{"text": "Ш", "callback": self.on_letter, "args": (chat_id, "Ш"), **({"style": "success"} if "Ш" in guessed else {"style": "danger"} if "Ш" in wrong else {})},
|
||||
{"text": "Щ", "callback": self.on_letter, "args": (chat_id, "Щ"), **({"style": "success"} if "Щ" in guessed else {"style": "danger"} if "Щ" in wrong else {})},
|
||||
{"text": "Ъ", "callback": self.on_letter, "args": (chat_id, "Ъ"), **({"style": "success"} if "Ъ" in guessed else {"style": "danger"} if "Ъ" in wrong else {})},
|
||||
{"text": "Ь", "callback": self.on_letter, "args": (chat_id, "Ь"), **({"style": "success"} if "Ь" in guessed else {"style": "danger"} if "Ь" in wrong else {})},
|
||||
{"text": "Ы", "callback": self.on_letter, "args": (chat_id, "Ы"), **({"style": "success"} if "Ы" in guessed else {"style": "danger"} if "Ы" in wrong else {})},
|
||||
{"text": "Э", "callback": self.on_letter, "args": (chat_id, "Э"), **({"style": "success"} if "Э" in guessed else {"style": "danger"} if "Э" in wrong else {})},
|
||||
{"text": "Ю", "callback": self.on_letter, "args": (chat_id, "Ю"), **({"style": "success"} if "Ю" in guessed else {"style": "danger"} if "Ю" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "Я", "callback": self.on_letter, "args": (chat_id, "Я"), **({"style": "success"} if "Я" in guessed else {"style": "danger"} if "Я" in wrong else {})},
|
||||
],
|
||||
]
|
||||
|
||||
async def on_letter(self, call: InlineCall, chat_id: int, letter: str):
|
||||
letter = letter.upper()
|
||||
state = self.get(f"pidor_{chat_id}", None)
|
||||
|
||||
if letter in state["guessed"] or letter in state["wrong"]:
|
||||
await call.answer(self.strings["already"], show_alert=True)
|
||||
return
|
||||
|
||||
if letter in state["word"]:
|
||||
state["guessed"].append(letter)
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
if "_" not in self.field_w_letters(state["word"], state["guessed"]):
|
||||
self.set(f"pidor_{chat_id}", None)
|
||||
await call.edit(self.strings["won"].format(word=state["word"]))
|
||||
return
|
||||
|
||||
await call.edit(self.caption(state), reply_markup=self.russian_latters(state, chat_id))
|
||||
|
||||
else:
|
||||
state["wrong"].append(letter)
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
wrong_count = len(state["wrong"])
|
||||
stage = min(wrong_count, len(self.HangmanLives) - 1)
|
||||
|
||||
if wrong_count >= 10:
|
||||
self.set(f"pidor_{chat_id}", None)
|
||||
await call.edit(
|
||||
self.strings["over"].format(word=state["word"]),
|
||||
photo=self.HangmanLives[stage],
|
||||
)
|
||||
return
|
||||
|
||||
await call.edit(
|
||||
self.caption(state),
|
||||
reply_markup=self.russian_latters(state, chat_id),
|
||||
photo=self.HangmanLives[stage],
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="(.oleg) Начать висилицу", alias="oleg")
|
||||
async def hangman(self, message: Message):
|
||||
"""(.oleg) Start hangman game"""
|
||||
chat_id = message.chat_id
|
||||
word = random.choice(self.words)
|
||||
state = {"word": word, "guessed": [], "wrong": []}
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
await self.inline.form(
|
||||
message=message,
|
||||
text=self.caption(state),
|
||||
reply_markup=self.russian_latters(state, chat_id),
|
||||
photo=self.HangmanLives[0],
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
# meta developer: @SunnexGB
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/e19c2179-d2e9-4206-b783-25d6c0eb72eb.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/e19c2179-d2e9-4206-b783-25d6c0eb72eb.jpg
|
||||
# meta fhsdesc: Плейсхолдер, placeholder, Time, Время, Статистика, Stats
|
||||
# крутой баннер да?
|
||||
#current version
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
import time
|
||||
from .. import loader, utils
|
||||
|
||||
@loader.tds
|
||||
class HerokuTime(loader.Module):
|
||||
"""shows how much heroku you use in total (since installing placeholder)"""
|
||||
|
||||
strings = {
|
||||
"name": "HerokuTime",
|
||||
"sec": "sec",
|
||||
"min": "min",
|
||||
"hour": "h",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"cls_doc": "показывает сколько вы используете всего хероку(с момента установки плейсхолдера)",
|
||||
"sec": "сек",
|
||||
"min": "мин",
|
||||
"hour": "ч",
|
||||
}
|
||||
|
||||
async def client_ready(self):
|
||||
if not self.get("start_time"):
|
||||
self.set("start_time", int(time.time()))
|
||||
utils.register_placeholder("alltime", self.get_uptime, "show heroku time usage")
|
||||
|
||||
def format_time(self, seconds: int) -> str:
|
||||
if seconds < 60:
|
||||
return f"{seconds} {self.strings('sec')}"
|
||||
minutes = seconds // 60
|
||||
if minutes < 60:
|
||||
return f"{minutes} {self.strings('min')} {seconds % 60} {self.strings('sec')}"
|
||||
hours = minutes // 60
|
||||
return f"{hours} {self.strings('hour')} {minutes % 60} {self.strings('min')} {seconds % 60} {self.strings('sec')}"
|
||||
|
||||
async def get_uptime(self):
|
||||
start_time = self.get("start_time")
|
||||
if not start_time:
|
||||
return f"0 {self.strings('sec')}"
|
||||
now = int(time.time())
|
||||
uptime = now - start_time
|
||||
return self.format_time(uptime)
|
||||
@@ -1,138 +0,0 @@
|
||||
# requires: aiohttp
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/c389e9b5-9ef1-495d-a37a-e993ef819b4a.mp4
|
||||
# meta developer: @SunnexGB
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
from .. import loader, utils
|
||||
import re
|
||||
import aiohttp
|
||||
|
||||
@loader.tds
|
||||
class Mikuru(loader.Module):
|
||||
"""Censors words with phrase Mikuru Asahina"""
|
||||
|
||||
strings = {
|
||||
"name": "Mikuru",
|
||||
"mikuru": "<tg-emoji emoji-id=5856940583934760252>🤤</tg-emoji> | <b><i>I cant say this is for u, b-because this is c-classified information</i></b>",
|
||||
"adult_mikuru": "<tg-emoji emoji-id=5859584252269564510>😥</tg-emoji> | <b><i>Y-you already k-know s-so much...</i></b>",
|
||||
"ignored": "<tg-emoji emoji-id=5859243730082468153>😏</tg-emoji> | <b><i>Y-you can talk about c-classified information in this chat</i></b>",
|
||||
"unignored": "<tg-emoji emoji-id=5859594216593691408>☺️</tg-emoji> | <b><i>You cant speak in this chat b-because c-classified information</i></b>",
|
||||
"classified_information": "<b>classified information</b>"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Цензурует слова фразой Микуру Асахины",
|
||||
"mikuru": "<tg-emoji emoji-id=5856940583934760252>🤤</tg-emoji> | <b><i>Я не могу сказать тебе это, п-п-потому что это с-секретные сведения</i></b>",
|
||||
"adult_mikuru": "<tg-emoji emoji-id=5859584252269564510>😥</tg-emoji> | <b><i>Т-ты и так м-много з-знаешь...</i></b> ",
|
||||
"ignored": "<tg-emoji emoji-id=5859243730082468153>😏</tg-emoji> | <b><i>Т-ты можешь говорить о с-секретных сведениях в этом чате</i></b>",
|
||||
"unignored": "<tg-emoji emoji-id=5859594216593691408>☺️</tg-emoji> | <b><i>Ты не можешь говорить в этом чате п-потому что с-секретные сведения</i></b>",
|
||||
"classified_information": "<b>секретные сведения</b>"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"Ignored_chats",
|
||||
[-1002410964167,
|
||||
-1002341345589,
|
||||
-1001697279580,
|
||||
-1001554874075,
|
||||
-1001984640085],
|
||||
"Ignored chats",
|
||||
validator=loader.validators.Series()
|
||||
),
|
||||
)
|
||||
self.bad_words = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.db = db
|
||||
await self.load_words()
|
||||
|
||||
async def load_words(self):
|
||||
cultural_words = [
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/Mikuru/cultural_words_ru.txt",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/49f6883d03d1d2c15c82bad55ee4d31f708870ed/Assets/Mikuru/cultural_words_en.txt"
|
||||
]
|
||||
|
||||
words = set()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for url in cultural_words:
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
continue
|
||||
text = await resp.text()
|
||||
for line in text.splitlines():
|
||||
w = line.strip().lower()
|
||||
if w:
|
||||
words.add(w)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if words:
|
||||
self.bad_words = re.compile(
|
||||
r"\b(" + "|".join(map(re.escape, words)) + r")\b",
|
||||
re.IGNORECASE
|
||||
)
|
||||
else:
|
||||
self.bad_words = None
|
||||
|
||||
@loader.command(ru_doc="- Начать цензурирование")
|
||||
async def mikuru(self, message):
|
||||
"""- lets go censoring"""
|
||||
state = self.db.get(self.name, "mikuru_state", False)
|
||||
if state:
|
||||
self.db.set(self.name, "mikuru_state", False)
|
||||
await utils.answer(message, self.strings("adult_mikuru"))
|
||||
else:
|
||||
self.db.set(self.name, "mikuru_state", True)
|
||||
await utils.answer(message, self.strings("mikuru"))
|
||||
|
||||
@loader.command(ru_doc="- Добавить в список игнорируемых чатов(не будет работать в этих чатах) <id/@>")
|
||||
async def ignore(self, message):
|
||||
"""- Add to list ignored chats(will not work in these chats) <id/@>"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
target = str(utils.get_chat_id(message))
|
||||
else:
|
||||
target = args.strip()
|
||||
ignored = list(self.config["Ignored_chats"])
|
||||
if target in ignored:
|
||||
ignored.remove(target)
|
||||
self.config["Ignored_chats"] = ignored
|
||||
await utils.answer(message, self.strings("unignored"))
|
||||
else:
|
||||
ignored.append(target)
|
||||
self.config["Ignored_chats"] = ignored
|
||||
await utils.answer(message, self.strings("ignored"))
|
||||
|
||||
async def watcher(self, message):
|
||||
if not self.db.get(self.name, "mikuru_state", False):
|
||||
return
|
||||
if not message.text:
|
||||
return
|
||||
if not message.out:
|
||||
return
|
||||
|
||||
chat_id = str(utils.get_chat_id(message))
|
||||
user_id = str(getattr(message.sender_id, "id", message.sender_id))
|
||||
|
||||
if chat_id in self.config["Ignored_chats"] or user_id in self.config["Ignored_chats"]:
|
||||
return
|
||||
if not self.bad_words:
|
||||
return
|
||||
|
||||
if self.bad_words.search(message.text):
|
||||
text = self.bad_words.sub(
|
||||
self.strings("classified_information"),
|
||||
message.text
|
||||
)
|
||||
try:
|
||||
await message.edit(text)
|
||||
except Exception:
|
||||
try:
|
||||
await message.delete()
|
||||
await message.respond(text)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,487 +0,0 @@
|
||||
# requires: aiohttp pyngrok
|
||||
# meta developer: @H_SunMods
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/965a3206-4609-4dff-beb0-6831f8b90e12.jpg
|
||||
# current ver
|
||||
__version__ = (0, 1, 0)
|
||||
|
||||
import json
|
||||
import socket
|
||||
import asyncio
|
||||
import secrets
|
||||
import logging
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
from aiohttp import ClientSession, ClientTimeout, web
|
||||
from herokutl.types import Message
|
||||
from pyngrok import conf, ngrok
|
||||
from .. import loader, utils
|
||||
from ..inline.types import InlineCall
|
||||
|
||||
logging.getLogger("pyngrok").setLevel(logging.WARNING)
|
||||
logging.getLogger("pyngrok.process").setLevel(logging.WARNING)
|
||||
logging.getLogger("pyngrok.process.ngrok").setLevel(logging.WARNING)
|
||||
|
||||
html_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/index.html"
|
||||
css_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/style.css"
|
||||
js_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/javascript.js"
|
||||
asset_root_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess"
|
||||
botfather_photo_url = "https://r2.fakecrime.bio/uploads/d3e16245-15a2-43f1-b176-493b4d9f1f21.jpg"
|
||||
|
||||
@loader.tds
|
||||
class NoChess(loader.Module):
|
||||
"""NoChess - web module that allows u to launch a web page either as a functional HTML page or as a Telegram Mini-App. This is an add-on for Chess module by @nullmod"""
|
||||
|
||||
# я пытался кароче сделать тут перевод делая реплейсы в зависимости от стрингов,но это не работает,поэтому да
|
||||
strings = {
|
||||
"name": "NoChess",
|
||||
"starting": "( ノ・ェ・ )ノ <b>Starting NoChess...</b>",
|
||||
"online": "(*˘︶˘*) <b>NoChess is running</b>",
|
||||
"already_running": "ʕᵕᴥᵕʔ <b>NoChess is already running</b>",
|
||||
"stopped": "・゚・(。>д<。)・゚・ NoChess stopped",
|
||||
"not_running": "(✿╹◡╹) NoChess is not running",
|
||||
"ngrok_missing": "Set a <code>ngrok_token</code>",
|
||||
"ngrok_error": "Ngrok start error: <code>{}</code>",
|
||||
"asset_read_error": "Failed to load web assets: <code>{}</code>",
|
||||
"open_button": "Open mini-app",
|
||||
"stop_button": "Stop",
|
||||
"about_text": "<b>Important read:</b>\nSometimes the server won't lift cause there's enough processes running, for example on HikkaHost, for this I just rebooted the server\nNext is that <code>cma</code> setups the app by a template and it's rly crooked, so you'll have to set some web app config settings yourself\nAnd also:\n 1. First launch will start straight with a site link, not as a web app\n 2. Use <code>nochess</code>, and then <code>cma</code> to setup the web app\n 3. After that restart the process by typing <code>nochess -kill</code> and <code>nochess</code> again\nYeah it's hacky as hell, but I was so over doing stuff that I started dumping some routine like working with files on ai, which I didn't like so I decided to quick-release the module before it's too late\nWell and maybe soon I'll make an update, right now it's some pre-alpha version, that's why the version name is like this, later I'll change it to 1.0.0, if people actually dig the module as an idea",
|
||||
"cma_start": "( ノ・ェ・ )ノ <b>Creating mini app in BotFather...</b>",
|
||||
"cma_need_url": "Set mini app web URL first or run <code>.nochess</code> to get it.",
|
||||
"cma_done": "(*˘︶˘*) <b>Done.</b>",
|
||||
"cma_error": "Error: <code>{}</code>",
|
||||
"RuntimeError": "inline bot username not found",
|
||||
"not_supported_platform": "(┬┬_┬┬) Unfortunately, it is impossible to install this module on this platform.\n\n(〜^∇^)〜 This is not an error, please do not contact support."
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "NoChess - Веб модуль который позволяет запускать веб-пейдж,как HTML страницу с функционалом,так же в виде Telegram Mini-App. Является дополнением к модулю Chess от @nullmod",
|
||||
"starting": "( ノ・ェ・ )ノ <b>Запуск NoChess...</b>",
|
||||
"online": "(*˘︶˘*) <b>NoChess запущен</b>",
|
||||
"already_running": "ʕᵕᴥᵕʔ <b>NoChess уже запущен</b>",
|
||||
"stopped": "・゚・(。>д<。)・゚・ NoChess остановлен",
|
||||
"not_running": "(✿╹◡╹) NoChess не запущен",
|
||||
"ngrok_missing": "Укажи <code>ngrok_token</code>",
|
||||
"ngrok_error": "Ошибка запуска ngrok: <code>{}</code>",
|
||||
"asset_read_error": "Не удалось загрузить веб-ассеты: <code>{}</code>",
|
||||
"open_button": "Открыть мини-приложение",
|
||||
"stop_button": "Остановить",
|
||||
"about_text": "<b>Важно к прочтению:</b>\nИногда сервер не может подниматься из за того что запущено достаточно процессов, например на HikkaHost,для этого я просто перезагружал сервер.\nДалее это то что <code>cma</code> сетапает приложение по шаблону и оч криво, поэтому вам придется выставлять некоторые настройки конфигурации веб приложения самим.\nА еще:\n 1. Первый запуск будет запускаться сразу ссылкой на сайт, а не как веб приложение.\n 2. Используйте <code>nochess</code>, а потом <code>cma</code> чтобы настроить веб приложение.\n 3. После чего перезапустите процесс написав <code>nochess -kill</code> и повторно <code>nochess</code>.\nДа это костыли, но мне уже настолько было в падлу что то делать что я уже стал спихивать рутину по типу работы с файлами на ии, что мне не понравилось и я решил быстро релизать модуль пока не стало поздно.\nНу и может быть в скором времени я уже сделаю апдейт, на данный момент это какая то пре-альфа версия, поэтому и название версии такое, в дальнейшем изменю на 1.0.0, если модуль вообще понравиться людям как идея.",
|
||||
"cma_start": "( ノ・ェ・ )ノ <b>Создаю эпку через BotFather...</b>",
|
||||
"cma_need_url": "Сначала укажи URL мини-эпки или запусти <code>.nochess</code>, чтобы получить его",
|
||||
"cma_done": "(*˘︶˘*) <b>Готово</b>",
|
||||
"cma_error": "Ошибка: <code>{}</code>",
|
||||
"RuntimeError": "юз инлайн бота не найден",
|
||||
"not_supported_platform": "(┬┬_┬┬) К сожалению, на эту платформу невозможно установить этот модуль.\n\n(〜^∇^)〜 Это не ошибка, пожалуйста, не обращайтесь в поддержку."
|
||||
}
|
||||
|
||||
async def client_ready(self):
|
||||
platform = utils.get_named_platform()
|
||||
if platform in ("HikkaHost"):
|
||||
raise loader.LoadError(self.strings("not_supported_platform"))
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"ngrok_token",
|
||||
None,
|
||||
"Token from ngrok.com | Токен полученый на ngrok.com",
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"mini_app_url",
|
||||
None,
|
||||
"Mini app direct url | Директ ссылка на ваше мини приложение",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"block_light",
|
||||
"#D8E3E7",
|
||||
"Light board block color | Цвет светлых полей на доске",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue("block_dark",
|
||||
"#7699AF",
|
||||
"Dark board block color | Цвет тёмных полей на доске",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"select_block",
|
||||
"#FF5A5A",
|
||||
"Selected block color | Цвет для выделения полей на доске",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"move_pieces_color",
|
||||
"#58B4FF",
|
||||
"Move highlight color | Цвет подсвечиваниях перехода на другую позицию",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"result_win",
|
||||
"#00BE16",
|
||||
"Winner color | Блок цвета победителя",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"result_lose",
|
||||
"#BE0000",
|
||||
"Loser color | Блок цвета проигравшего",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"result_draw",
|
||||
"#434343",
|
||||
"Draw color | Блок цвета при ничьей",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"arrow_color",
|
||||
"#BD3667",
|
||||
"Arrow color | Цвет стрелки",
|
||||
validator=loader.validators.String()
|
||||
),
|
||||
)
|
||||
|
||||
self.runner = None
|
||||
self.tunnel_url = None
|
||||
self.access_token = None
|
||||
self.games_cache = []
|
||||
self.games_dump = ""
|
||||
|
||||
def theme_config_dict(self):
|
||||
return {
|
||||
"block_light": self.config["block_light"],
|
||||
"block_dark": self.config["block_dark"],
|
||||
"select_block": self.config["select_block"],
|
||||
"move_pieces_color": self.config["move_pieces_color"],
|
||||
"result_win": self.config["result_win"],
|
||||
"result_lose": self.config["result_lose"],
|
||||
"result_draw": self.config["result_draw"],
|
||||
"arrow_color": self.config["arrow_color"],
|
||||
}
|
||||
|
||||
async def refresh_games_cache(self):
|
||||
chess = self.lookup("chess")
|
||||
if not chess or not getattr(chess, "games", None):
|
||||
self.games_cache = []
|
||||
self.games_dump = ""
|
||||
return
|
||||
|
||||
chunks = []
|
||||
items = list(chess.games.items())
|
||||
|
||||
def sort_key(item):
|
||||
key = str(item[0])
|
||||
return (0, int(key)) if key.isdigit() else (1, key)
|
||||
|
||||
for _, game in sorted(items, key=sort_key, reverse=True):
|
||||
node = None
|
||||
|
||||
if isinstance(game, dict):
|
||||
game_obj = game.get("game", {})
|
||||
if isinstance(game_obj, dict):
|
||||
node = game_obj.get("root_node") or game_obj.get("node")
|
||||
if node is None:
|
||||
node = game.get("root_node") or game.get("node")
|
||||
|
||||
if node is None and hasattr(game, "game"):
|
||||
game_obj = getattr(game, "game", None)
|
||||
if isinstance(game_obj, dict):
|
||||
node = game_obj.get("root_node") or game_obj.get("node")
|
||||
|
||||
if node is None and hasattr(game, "root_node"):
|
||||
node = getattr(game, "root_node", None)
|
||||
|
||||
if node is None and hasattr(game, "node"):
|
||||
node = getattr(game, "node", None)
|
||||
|
||||
if node:
|
||||
chunks.append(str(node).strip())
|
||||
|
||||
self.games_cache = [x for x in chunks if x]
|
||||
self.games_dump = "\n\n".join(self.games_cache)
|
||||
|
||||
async def get_me_json(self):
|
||||
me = await self.client.get_me()
|
||||
fallback_photo = "https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg"
|
||||
full_name = (getattr(me, "first_name", "") or "") + (
|
||||
(" " + getattr(me, "last_name", "")) if getattr(me, "last_name", None) else ""
|
||||
)
|
||||
return {
|
||||
"id": getattr(me, "id", None),
|
||||
"username": getattr(me, "username", None),
|
||||
"first_name": getattr(me, "first_name", None),
|
||||
"last_name": getattr(me, "last_name", None),
|
||||
"name": full_name.strip() or str(getattr(me, "id", "Unknown")),
|
||||
"photo": fallback_photo,
|
||||
"enemy_photo": fallback_photo,
|
||||
}
|
||||
|
||||
def check_access(self, request):
|
||||
token = request.query.get("token") or request.cookies.get("nochess_token")
|
||||
return bool(self.access_token and token == self.access_token)
|
||||
|
||||
def ensure_access_token(self):
|
||||
if self.access_token:
|
||||
return self.access_token
|
||||
self.access_token = self.get("access_token")
|
||||
if not self.access_token:
|
||||
self.access_token = secrets.token_urlsafe(32)
|
||||
self.set("access_token", self.access_token)
|
||||
return self.access_token
|
||||
|
||||
async def read_remote_asset(self, url):
|
||||
timeout = ClientTimeout(total=15)
|
||||
async with ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status != 200:
|
||||
raise RuntimeError(f"HTTP {response.status}: {url}")
|
||||
return await response.text()
|
||||
|
||||
async def load_web_assets(self):
|
||||
html = await self.read_remote_asset(html_raw)
|
||||
css = await self.read_remote_asset(css_raw)
|
||||
js = await self.read_remote_asset(js_raw)
|
||||
return html, css, js
|
||||
|
||||
def localication_script(self):
|
||||
return (
|
||||
"<script>(async()=>{"
|
||||
"try{const me=await fetch('/api/me').then(r=>r.json());window.nochess_profile=me;if(typeof setNoChessProfile==='function'){setNoChessProfile(me);}}catch(_e){}"
|
||||
"let rawGames=[];"
|
||||
"try{const d=await fetch('/api/games').then(r=>r.json());rawGames=Array.isArray(d.games)?d.games:[];}catch(_e){}"
|
||||
"const apply=()=>{if(typeof parsePgnToGameState!=='function'||typeof buildHistoryList!=='function')return false;"
|
||||
"parsed_games=(rawGames||[]).map(g=>parsePgnToGameState(g)).filter(Boolean);"
|
||||
"buildHistoryList();if(parsed_games.length>0&&typeof loadGame==='function')loadGame(0);return true;};"
|
||||
"if(apply())return;"
|
||||
"let attempts=0;const iv=setInterval(()=>{attempts++;if(apply()||attempts>40)clearInterval(iv);},250);"
|
||||
"})();</script>"
|
||||
)
|
||||
|
||||
def inject_runtime_config(self, html, css, js):
|
||||
asset_root = asset_root_raw.rstrip("/")
|
||||
if asset_root:
|
||||
css = css.replace("url('bg.png')", f"url('{asset_root}/other/bg.png')")
|
||||
theme_json = json.dumps(self.theme_config_dict(), ensure_ascii=False)
|
||||
bootstrap = (
|
||||
"<script>"
|
||||
f"window.nochess_theme={theme_json};"
|
||||
f"window.nochess_asset_root={json.dumps(asset_root)};"
|
||||
"</script>"
|
||||
)
|
||||
html = html.replace('<link rel="stylesheet" href="style.css">', f"<style>{css}</style>")
|
||||
html = html.replace('<script src="javascript.js"></script>', bootstrap + f"<script>{js}</script>")
|
||||
return html
|
||||
|
||||
async def handle_home(self, request):
|
||||
try:
|
||||
html, css, js = await self.load_web_assets()
|
||||
except Exception as error:
|
||||
return web.Response(
|
||||
text=self.strings["asset_read_error"].format(utils.escape_html(str(error))),
|
||||
status=500,
|
||||
)
|
||||
html = self.inject_runtime_config(html, css, js)
|
||||
html = html.replace("</body>", self.localication_script() + "</body>")
|
||||
response = web.Response(text=html, content_type="text/html")
|
||||
response.set_cookie(
|
||||
"nochess_token",
|
||||
self.access_token,
|
||||
max_age=86400,
|
||||
httponly=True,
|
||||
samesite="Lax",
|
||||
)
|
||||
return response
|
||||
|
||||
async def handle_games(self, request):
|
||||
if not self.check_access(request):
|
||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||
if not self.games_cache:
|
||||
await self.refresh_games_cache()
|
||||
return web.json_response({"games_dump": self.games_dump, "games": list(self.games_cache)})
|
||||
|
||||
async def handle_me(self, request):
|
||||
if not self.check_access(request):
|
||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||
return web.json_response(await self.get_me_json())
|
||||
|
||||
async def stop_server(self):
|
||||
was_running = bool(self.runner)
|
||||
try:
|
||||
ngrok.kill()
|
||||
except Exception:
|
||||
pass
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
self.runner = None
|
||||
self.tunnel_url = None
|
||||
return was_running
|
||||
|
||||
async def send_form(self, message, url):
|
||||
await self.inline.form(
|
||||
self.strings["online"],
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[{"text": self.strings["open_button"], "url": url}],
|
||||
[{"text": self.strings["stop_button"], "callback": self.stop_callback}],
|
||||
],
|
||||
)
|
||||
|
||||
async def stop_callback(self, call: InlineCall):
|
||||
was_running = await self.stop_server()
|
||||
await call.answer(
|
||||
self.strings["stopped"] if was_running else self.strings["not_running"],
|
||||
show_alert=False,
|
||||
)
|
||||
try:
|
||||
await call.delete()
|
||||
except Exception:
|
||||
try:
|
||||
await call.edit(self.strings["stopped"] if was_running else self.strings["not_running"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@loader.command(ru_doc="[-kill] Вызываь веб интерфейс для просмотра партии")
|
||||
async def nochess(self, message: Message):
|
||||
"""[-kill] Call web interface to view chess game"""
|
||||
try:
|
||||
return await self.nochess_args(message)
|
||||
except Exception as error:
|
||||
await self.stop_server()
|
||||
return await utils.answer(
|
||||
message,
|
||||
self.strings["ngrok_error"].format(utils.escape_html(str(error))),
|
||||
)
|
||||
|
||||
async def nochess_args(self, message: Message):
|
||||
args = (utils.get_args_raw(message) or "").strip().lower()
|
||||
if args == "-kill":
|
||||
was_running = await self.stop_server()
|
||||
return await utils.answer(message, self.strings["stopped"] if was_running else self.strings["not_running"])
|
||||
mini_url = (self.config["mini_app_url"] or "").strip().rstrip("/")
|
||||
is_tg_direct = mini_url.startswith("https://t.me/")
|
||||
if self.runner:
|
||||
if is_tg_direct:
|
||||
access = mini_url
|
||||
else:
|
||||
base = (self.tunnel_url or "").rstrip("/")
|
||||
access = f"{base}/?token={self.access_token}" if base and self.access_token else base
|
||||
await utils.answer(message, self.strings["already_running"])
|
||||
if access:
|
||||
await self.send_form(message, access)
|
||||
return
|
||||
if not self.config["ngrok_token"] and (not mini_url or is_tg_direct):
|
||||
return await utils.answer(message, self.strings["ngrok_missing"])
|
||||
await self.refresh_games_cache()
|
||||
await utils.answer(message, self.strings["starting"])
|
||||
self.ensure_access_token()
|
||||
sock = socket.socket()
|
||||
sock.bind(("", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
app = web.Application()
|
||||
app.router.add_get("/", self.handle_home)
|
||||
app.router.add_get("/api/games", self.handle_games)
|
||||
app.router.add_get("/api/me", self.handle_me)
|
||||
self.runner = web.AppRunner(app)
|
||||
await self.runner.setup()
|
||||
await web.TCPSite(self.runner, "127.0.0.1", port).start()
|
||||
try:
|
||||
if self.config["ngrok_token"]:
|
||||
conf.get_default().auth_token = self.config["ngrok_token"]
|
||||
tunnel = ngrok.connect(port)
|
||||
self.tunnel_url = tunnel.public_url.rstrip("/")
|
||||
else:
|
||||
self.tunnel_url = mini_url
|
||||
except Exception as error:
|
||||
await self.stop_server()
|
||||
return await utils.answer(
|
||||
message,
|
||||
self.strings["ngrok_error"].format(utils.escape_html(str(error))),
|
||||
)
|
||||
if is_tg_direct:
|
||||
access_url = mini_url
|
||||
else:
|
||||
base = (self.tunnel_url or "").rstrip("/")
|
||||
access_url = f"{base}/?token={self.access_token}" if base and self.access_token else base
|
||||
await self.send_form(message, access_url)
|
||||
|
||||
@loader.command(ru_doc="Создает и настраивает эпку")
|
||||
async def cma(self, message: Message):
|
||||
"""Create and setup mini-app"""
|
||||
raw_args = (utils.get_args_raw(message) or "").strip()
|
||||
parts = raw_args.split()
|
||||
web_url = ""
|
||||
short_name = "NoChess"
|
||||
if parts:
|
||||
web_url = parts[0]
|
||||
if len(parts) > 1:
|
||||
short_name = parts[1]
|
||||
if not web_url:
|
||||
candidate = (self.tunnel_url or "").strip()
|
||||
if not candidate:
|
||||
candidate = (self.config["mini_app_url"] or "").strip()
|
||||
if candidate.startswith("https://t.me/"):
|
||||
candidate = ""
|
||||
web_url = candidate
|
||||
if not web_url:
|
||||
return await utils.answer(message, self.strings["cma_need_url"])
|
||||
self.ensure_access_token()
|
||||
if web_url.startswith("http") and "t.me/" not in web_url:
|
||||
parsed = urlsplit(web_url)
|
||||
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||||
query["token"] = self.access_token
|
||||
web_url = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment))
|
||||
await utils.answer(message, self.strings["cma_start"])
|
||||
try:
|
||||
bot_username = (await self.inline.bot.get_me()).username
|
||||
bot_username = (bot_username or "").strip().lstrip("@")
|
||||
if not bot_username:
|
||||
raise RuntimeError(self.strings["RuntimeError"])
|
||||
await self.client.send_message("@BotFather", "/cancel")
|
||||
await asyncio.sleep(0.9)
|
||||
|
||||
async with self.client.conversation("@BotFather", timeout=120) as conv:
|
||||
await conv.send_message("/newapp")
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message(f"@{bot_username}")
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message("NoChessModule")
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message("NoChess")
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_file(botfather_photo_url)
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message("/empty")
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message(web_url)
|
||||
await conv.get_response()
|
||||
await asyncio.sleep(0.8)
|
||||
await conv.send_message(short_name)
|
||||
await conv.get_response()
|
||||
|
||||
direct_link = f"https://t.me/{bot_username}/{short_name}"
|
||||
module_ref = None
|
||||
try:
|
||||
module_ref = self.lookup("NoChess")
|
||||
except Exception:
|
||||
module_ref = None
|
||||
if module_ref:
|
||||
module_ref.config["mini_app_url"] = direct_link
|
||||
else:
|
||||
self.config["mini_app_url"] = direct_link
|
||||
await utils.answer(message, self.strings["cma_done"])
|
||||
except Exception as error:
|
||||
await utils.answer(message, self.strings["cma_error"].format(utils.escape_html(str(error))))
|
||||
|
||||
@loader.command(ru_doc="ВАЖНО К ПРОЧТЕНИЮ")
|
||||
async def about(self, message: Message):
|
||||
"""IMPORTANT READING"""
|
||||
await utils.answer(message, self.strings["about_text"])
|
||||
async def on_unload(self):
|
||||
await self.stop_server()
|
||||
@@ -1,124 +0,0 @@
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/54b3c78d-38cb-4970-b925-18b7ec2b268d.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/54b3c78d-38cb-4970-b925-18b7ec2b268d.jpg
|
||||
# requires: https://files.pythonhosted.org/packages/2f/66/31ecae67c373421db10f250a83d80653d6908f7d95080c46816102bd1fda/shazamio-0.8.1.tar.gz https://files.pythonhosted.org/packages/dd/4d/7ecffb341d646e016be76e36f5a42cb32f409c9ca21a57b68f067fad3fc7/python_ffmpeg-2.0.12.tar.gz
|
||||
# meta developer: @SunnexGB
|
||||
#current version
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
from .. import loader, utils
|
||||
import os
|
||||
import asyncio
|
||||
from shazamio import Shazam
|
||||
|
||||
@loader.tds
|
||||
class Shazamio(loader.Module):
|
||||
"""Music recognition module"""
|
||||
|
||||
strings = {
|
||||
"name": "Shazamio",
|
||||
"processing": "<b>Processing <emoji document_id=5325731315004218660>🫥</emoji></b>",
|
||||
"shazaming": "<b><emoji document_id=4967658551506895731>🔈</emoji>| Shazaming...</b>",
|
||||
"no_reply": "<emoji document_id=4970127715320464315>🚫</emoji>| <b>Reply to a video message.</b>",
|
||||
"no_video": "<b><emoji document_id=4970127715320464315>🚫</emoji>| Reply must be to a video message.</b>",
|
||||
"ffmpeg_error": "<b><emoji document_id=4970127715320464315>🚫</emoji>| Failed to read audio. Make sure ffmpeg is installed.</b>",
|
||||
"not_found": "<b><emoji document_id=4970239229851337393>✖️</emoji>| Sorry, could not recognize the song.</b>",
|
||||
"result": "<b><emoji document_id=4967689020004893467>🔈</emoji>| Song recognized:</b>\n\n"
|
||||
"<b><emoji document_id=4967689020004893467>🔈</emoji>Artist:</b><code>{artist}</code>\n"
|
||||
"<b><emoji document_id=4967925573918655510>🚮</emoji>Title:</b><code>{title}</code>",
|
||||
"result_url": "<b><emoji document_id=4967503352863654812>〰️</emoji>Song recognized:</b>\n\n"
|
||||
"<b><emoji document_id=4967925573918655510>🚮</emoji>Artist:</b><code>{artist}</code>\n"
|
||||
"<b><emoji document_id=4967689020004893467>🔈</emoji>Title:</b><code>{title}</code>\n\n"
|
||||
"<emoji document_id=4967826519087907994>🔗</emoji><a href=\"{url}\">Listen on Shazam</a>",
|
||||
"shazam_history": "<emoji document_id=4969829017524896906>〰️</emoji>| <b>Your last 10 recognised songs</b>", # i put it off for later and then forgot i wanted to implement it
|
||||
"no_history": "<emoji document_id=4970064390322652183>〰️</emoji>| <b>What do you want to see here?</b>", # i put it off for later and then forgot i wanted to implement it
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"name": "Shazamio",
|
||||
"_cls_doc": "Модуль для распознования музыки",
|
||||
"processing": "<b>Обработка <emoji document_id=5325731315004218660>🫥</emoji></b>",
|
||||
"shazaming": "<b><emoji document_id=4967658551506895731>🔈</emoji>| Шазамлю...</b>",
|
||||
"no_reply": "<emoji document_id=4970127715320464315>🚫</emoji>| <b>Ответьте на сообщение с видео.</b>",
|
||||
"no_video": "<b><emoji document_id=4970127715320464315>🚫</emoji>| Ответ должен быть на видео</b>",
|
||||
"ffmpeg_error": "<b><emoji document_id=4970127715320464315>🚫</emoji>| Неудачное чтение аудио. Убедитесь что <code>ffmpeg</code> установлен.<a href=\"https://t.me/heroku_talks/8/66067\">Инструкция по установке</a></b>",
|
||||
"not_found": "<b><emoji document_id=4970239229851337393>✖️</emoji>| Простите, песня не была найдена.</b>",
|
||||
"result": "<b><emoji document_id=4967689020004893467>🔈</emoji>| Песня найдена:</b>\n\n"
|
||||
"<b><emoji document_id=4967689020004893467>🔈</emoji>Исполнитель:</b><code>{artist}</code>\n"
|
||||
"<b><emoji document_id=4967925573918655510>🚮</emoji>Название:</b><code>{title}</code>",
|
||||
"result_url": "<b><emoji document_id=4967503352863654812>〰️</emoji>Песня найдена:</b>\n\n"
|
||||
"<b><emoji document_id=4967925573918655510>🚮</emoji>Исполнитель:</b><code>{artist}</code>\n"
|
||||
"<b><emoji document_id=4967689020004893467>🔈</emoji>Название:</b><code>{title}</code>\n\n"
|
||||
"<emoji document_id=4967826519087907994>🔗</emoji><a href=\"{url}\">Слушайте на Shazam</a>",
|
||||
"shazam_history": "<emoji document_id=4969829017524896906>〰️</emoji>| <b>Твои 10 последних распознаных треков</b>", # на потом,я забыл что я хотел это реализовать
|
||||
"no_history": "<emoji document_id=4970064390322652183>〰️</emoji>| <b>Ну и что ты тут хотел увидеть?</b>", # на потом,я забыл что я хотел это реализовать
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
"ffmpeg_path",
|
||||
"ffmpeg",
|
||||
"Path to ffmpeg executable",
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Распознать музыку (Ответом на видео)")
|
||||
async def shazam(self, message):
|
||||
"""Recognize music (Reply in video)"""
|
||||
reply = await message.get_reply_message()
|
||||
if not reply:
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
return
|
||||
|
||||
if not reply.video:
|
||||
await utils.answer(message, self.strings["no_video"])
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["processing"])
|
||||
downloaded_path = await message.client.download_media(reply.video)
|
||||
video_path = os.path.abspath(downloaded_path)
|
||||
base, _ = os.path.splitext(video_path)
|
||||
audio_path = f"{base}.mp3"
|
||||
|
||||
try:
|
||||
cmd = (
|
||||
f'{self.config["ffmpeg_path"]} -i "{video_path}" '
|
||||
f'-y -vn -ab 128k -ar 44100 -f mp3 "{audio_path}"'
|
||||
)
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await proc.communicate()
|
||||
|
||||
if not os.path.exists(audio_path):
|
||||
await utils.answer(message, self.strings["ffmpeg_error"])
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["shazaming"])
|
||||
shazam = Shazam()
|
||||
result = await shazam.recognize(audio_path)
|
||||
|
||||
track = result.get("track")
|
||||
if track:
|
||||
title = track.get("title", "Unknown Title")
|
||||
artist = track.get("subtitle", "Unknown Artist")
|
||||
url = track.get("url")
|
||||
|
||||
if url:
|
||||
text = self.strings["result_url"].format(
|
||||
title=title, artist=artist, url=url
|
||||
)
|
||||
else:
|
||||
text = self.strings["result"].format(
|
||||
title=title, artist=artist
|
||||
)
|
||||
|
||||
await utils.answer(message, text)
|
||||
else:
|
||||
await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
finally:
|
||||
if os.path.exists(video_path):
|
||||
os.remove(video_path)
|
||||
if os.path.exists(audio_path):
|
||||
os.remove(audio_path)
|
||||
@@ -1,191 +0,0 @@
|
||||
# Спасибо: snfsx, кезу, а так же Gemini
|
||||
# requires: httpx
|
||||
# meta developer: @SunnexGB
|
||||
# meta repo: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/spotisaver.py
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/ddf03169-09fe-4eb1-8eea-bad1a4cc4ada.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/ddf03169-09fe-4eb1-8eea-bad1a4cc4ada.jpg
|
||||
# meta fhsdesc: Spotify, downloader, music, музыка, спотифай,скачать музыку
|
||||
# это не должно было быть в релизе,но ладно я потом пофикшу все и вся в говнокоде.
|
||||
__version__ = (1, 1, 1)
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from .. import loader, utils
|
||||
from herokutl.types import Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://spotmate.online",
|
||||
"Referer": "https://spotmate.online/en1",
|
||||
}
|
||||
|
||||
@loader.tds
|
||||
class SpotiSaver(loader.Module):
|
||||
"""Downloading music from Spotify"""
|
||||
strings = {
|
||||
"name": "SpotiSaver",
|
||||
# "args": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> link to song is not specified</b>",
|
||||
"downloading": "<b><tg-emoji emoji-id=5443127283898405358>📥</tg-emoji> Downloading:</b> <code>{}</code>",
|
||||
"error": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Error, see logs!</b>",
|
||||
"done": "<b><tg-emoji emoji-id=5206607081334906820>✔️</tg-emoji> Done!</b>",
|
||||
"no_spotifymod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>SpotifyMod not found.</b>",
|
||||
"no_spotify": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>Nothing is playing on Spotify.</b>",
|
||||
"nf_id": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> ID key not found!</b>",
|
||||
"nf_track": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Song not found.</b>",
|
||||
"timeout": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> timeout! Try again.</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"name": "SpotiSaver",
|
||||
"_cls_doc": "Скачивание музыки из Spotify",
|
||||
# "args": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Ссылка на песню не указана</b>",
|
||||
"downloading": "<b><tg-emoji emoji-id=5443127283898405358>📥</tg-emoji> Скачиваю:</b> <code>{}</code>",
|
||||
"error": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Ерорь, смотри логи!</b>",
|
||||
"done": "<b><tg-emoji emoji-id=5206607081334906820>✔️</tg-emoji> Готово!</b>",
|
||||
"no_spotifymod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>SpotifyMod не найден.</b>",
|
||||
"no_spotify": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>В Spotify ничего не играет.</b>",
|
||||
"nf_id": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> ID песни не найден</b>",
|
||||
"nf_track": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Песня не найдена</b>",
|
||||
"timeout": "<b><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> Таймаут! Попробуй ещё раз.</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"TimeOut",
|
||||
60,
|
||||
"Response timeout in seconds | Время ожидания ответа в секундах",
|
||||
validator=loader.validators.Integer(minimum=30),
|
||||
)
|
||||
)
|
||||
|
||||
async def get_session(self, client: httpx.AsyncClient) -> str:
|
||||
res = await client.get("https://spotmate.online/en1", headers={
|
||||
"User-Agent": headers["User-Agent"],
|
||||
"Accept": "text/html",
|
||||
},
|
||||
timeout=self.config["TimeOut"])
|
||||
match = re.search(r'csrf-token[^>]*content="([^"]+)"', res.text)
|
||||
if not match:
|
||||
raise ValueError("CSRF token not found")
|
||||
return match.group(1)
|
||||
|
||||
async def get_current_spotify_url(self) -> str | None:
|
||||
spotifymod = self.lookup("SpotifyMod")
|
||||
if not spotifymod or not spotifymod.sp:
|
||||
return None
|
||||
current_playback = await asyncio.to_thread(spotifymod.sp.current_playback)
|
||||
if not current_playback or not current_playback.get("is_playing"):
|
||||
return None
|
||||
track_id = current_playback["item"]["id"]
|
||||
return f"https://open.spotify.com/track/{track_id}"
|
||||
|
||||
@loader.command(ru_doc="<ссылка> — Скачать трек из Spotify")
|
||||
async def spotsave(self, message: Message):
|
||||
"""<link> - Download track from Spotify"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
spotifymod = self.lookup("SpotifyMod")
|
||||
if not spotifymod or not spotifymod.sp:
|
||||
return await utils.answer(message, self.strings["no_spotifymod"])
|
||||
args = await self.get_current_spotify_url()
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_spotify"])
|
||||
if "track/" not in args:
|
||||
return await utils.answer(message, self.strings["nf_id"])
|
||||
track_url = args.split("?")[0]
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||
csrf = await self.get_session(client)
|
||||
hdrs = {**headers, "X-CSRF-TOKEN": csrf}
|
||||
info_res = await client.post(
|
||||
"https://spotmate.online/getTrackData",
|
||||
headers=hdrs,
|
||||
json={"spotify_url": track_url},
|
||||
timeout=self.config["TimeOut"],
|
||||
)
|
||||
|
||||
info = info_res.json()
|
||||
if info.get("type") != "track":
|
||||
return await utils.answer(message, self.strings["nf_track"])
|
||||
track_name = info.get("name", "Unknown")
|
||||
artists = ", ".join(a["name"] for a in info.get("artists", []))
|
||||
full_name = f"{artists} - {track_name}"
|
||||
track_id = info.get("id", track_url.split("/")[-1])
|
||||
conv_res = await client.post(
|
||||
"https://spotmate.online/convert",
|
||||
headers=hdrs,
|
||||
json={"urls": track_url},
|
||||
timeout=self.config["TimeOut"],
|
||||
)
|
||||
conv = conv_res.json()
|
||||
download_url = conv.get("url") or conv.get("download_url")
|
||||
task_id = conv.get("task_id") or conv.get("taskId")
|
||||
if not download_url and task_id:
|
||||
for _ in range(40):
|
||||
await asyncio.sleep(4.5)
|
||||
task_res = await client.get(
|
||||
f"https://spotmate.online/tasks/{task_id}",
|
||||
headers={**hdrs, "Accept": "application/json"},
|
||||
timeout=self.config["TimeOut"],
|
||||
)
|
||||
task = task_res.json()
|
||||
if task.get("error"):
|
||||
return await utils.answer(message, self.strings["error"])
|
||||
data = task.get("data") or task.get("result") or {}
|
||||
status = str(data.get("status") or data.get("state") or "").lower()
|
||||
if status == "finished":
|
||||
download_url = (
|
||||
data.get("url") or data.get("download_url")
|
||||
or (data.get("result") or {}).get("url")
|
||||
or (data.get("result") or {}).get("download_url")
|
||||
)
|
||||
break
|
||||
|
||||
if status in ("failed", "error", "expired", "cancelled"):
|
||||
return await utils.answer(message, self.strings["error"])
|
||||
|
||||
if not download_url:
|
||||
return await utils.answer(message, self.strings["timeout"])
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["downloading"].format(utils.escape_html(full_name)),
|
||||
)
|
||||
|
||||
file_res = await client.get(
|
||||
download_url,
|
||||
headers={"User-Agent": headers["User-Agent"], "Referer": "https://spotmate.online/en1"},
|
||||
timeout=self.config["TimeOut"],
|
||||
)
|
||||
|
||||
filename = f"{track_id}.mp3"
|
||||
with open(filename, "wb") as f:
|
||||
f.write(file_res.content)
|
||||
|
||||
await self.client.send_file(
|
||||
message.chat_id,
|
||||
filename,
|
||||
caption=self.strings["done"],
|
||||
reply_to=message.id,
|
||||
attributes=(
|
||||
[utils.get_audio_tag(filename, title=track_name, performer=artists)]
|
||||
if hasattr(utils, "get_audio_tag")
|
||||
else []
|
||||
),
|
||||
)
|
||||
|
||||
await message.delete()
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Download failed")
|
||||
await utils.answer(message, self.strings["error"])
|
||||
@@ -1,285 +0,0 @@
|
||||
# meta developer: @SunnexGB
|
||||
# requires: aiohttp
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/f49a9294-36ad-4fc4-801f-48cb049111d6.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/f49a9294-36ad-4fc4-801f-48cb049111d6.jpg
|
||||
# meta fhsdesc: Spotify, music, музыка, спотифай,Lyrics, слова, текст, трек, песня
|
||||
# все же я не знаю трек или сонг, так что пусть будет трек, а не сонг потому что интуитивнее поняттнее,наверное?
|
||||
# крутой баннер да?
|
||||
#current version
|
||||
__version__ = (1, 1, 2)
|
||||
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
from ..types import InlineCall
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SpotifyLyrics(loader.Module):
|
||||
"""life lyrics current song"""
|
||||
|
||||
strings = {
|
||||
"name": "SpotifyLyrics",
|
||||
"no_spotifymod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>SpotifyMod not found,but u can install it. You can also support developer: </b> @ke_mods",
|
||||
"no_auth": "<tg-emoji emoji-id=5429225166250984904>⁉️</tg-emoji><b> You not authorized in SpotifyMod, visit you Saved Messages.</b>",
|
||||
"no_spotify": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>Nothing is playing on Spotify.</b>",
|
||||
"no_lyrics": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>Lyrics not found for:</b> <code>{}</code>",
|
||||
"not_synced": "<i><tg-emoji emoji-id=5431445849026611010>⚠️</tg-emoji> Lyrics are not synchronized.</i>\n\n",
|
||||
"finished": "<tg-emoji emoji-id=5429638011392377649>‼️</tg-emoji> Playback ended or track changed.",
|
||||
"header": "<tg-emoji emoji-id=5429413328768224565>🎤</tg-emoji> <b>{} - {}</b>\n\n",
|
||||
"timeout": "<b><tg-emoji emoji-id=5429455831764584284>⏳</tg-emoji></b><b> Oopsi, looks like we've got a timeout here</b>.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"cls_doc": "Лайв слова текущей песни.",
|
||||
"no_spotifymod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>SpotifyMod не найден,но его можно установить. Вы также можете поддержать разработчика: </b> @ke_mods",
|
||||
"no_auth": "<tg-emoji emoji-id=5429225166250984904>⁉️</tg-emoji><b> Вы не авторизированы в SpotifyMod, перейдите в Избранное.</b>",
|
||||
"no_spotify": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>В Spotify ничего не играет.</b>",
|
||||
"no_lyrics": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>Текст не найден для:</b> <code>{}</code>",
|
||||
"not_synced": "<i><tg-emoji emoji-id=5431445849026611010>⚠️</tg-emoji> Текст не синхронизирован.</i>\n\n",
|
||||
"finished": "<tg-emoji emoji-id=5429638011392377649>‼️</tg-emoji> Воспроизведение завершено или трек сменился.",
|
||||
"header": "<tg-emoji emoji-id=5429413328768224565>🎤</tg-emoji> <b>{} - {}</b>\n\n",
|
||||
"timeout": "<b><tg-emoji emoji-id=5429455831764584284>⏳</tg-emoji></b><b> Упси, похоже кто то словил таймаут.</b>.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._active_tasks: dict = {}
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"emoji_current",
|
||||
"<tg-emoji emoji-id='5215679757366089921'>🤯</tg-emoji>",
|
||||
"Emoji for current line",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"dot",
|
||||
"♪",
|
||||
"instrumental_emoji or text",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"lyrics_delay",
|
||||
0.5,
|
||||
"delay in switching to a new timing sector with words",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"request_timeout",
|
||||
12,
|
||||
"timeout value",
|
||||
),
|
||||
)
|
||||
|
||||
async def install_spotifymod(self, call: InlineCall):
|
||||
mod_url = "https://raw.githubusercontent.com/radiocycle/Modules/refs/heads/master/SpotifyMod.py"
|
||||
try:
|
||||
m = self.lookup("Modules") or self.lookup("loader")
|
||||
await m.download_and_install(mod_url)
|
||||
await call.answer("SpotifyMod installed!", show_alert=True)
|
||||
mod = self.lookup("SpotifyMod")
|
||||
acs_tkn = mod.get("acs_tkn") if mod else None
|
||||
if not acs_tkn:
|
||||
await self.invoke("sauth", " ", "me")
|
||||
await call.edit(
|
||||
self.strings("no_auth"),
|
||||
reply_markup=[[{"text": "Хорошо", "callback": self.close}]],
|
||||
)
|
||||
else:
|
||||
await call.delete()
|
||||
except Exception as e:
|
||||
await call.answer(f"Error! Check logs.\n{e}", show_alert=True)
|
||||
|
||||
def close(self, call: InlineCall):
|
||||
return call.delete()
|
||||
|
||||
async def _get_lyrics(self, artist: str, track: str):
|
||||
clean_track = re.sub(r"\(.*?\)|\[.*?\]", "", track).strip()
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://lrclib.net/api/search",
|
||||
params={"track_name": clean_track, "artist_name": artist},
|
||||
timeout=aiohttp.ClientTimeout(total=(self.config["request_timeout"])),
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
res = await resp.json()
|
||||
return res[0] if res else None
|
||||
except asyncio.TimeoutError:
|
||||
return {"timeout": True}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _parse_synced(self, synced_text: str) -> list:
|
||||
lines = []
|
||||
for line in synced_text.split("\n"):
|
||||
m = re.search(r"\[(\d+):(\d+\.\d+)\](.*)", line)
|
||||
if m:
|
||||
mins, secs, text = m.groups()
|
||||
lines.append({
|
||||
"time": (int(mins) * 60 + float(secs)) * 1000,
|
||||
"text": text.strip(),
|
||||
})
|
||||
return lines
|
||||
|
||||
def _build_content(self, artist, track, lines, plain, progress_ms, not_synced_str):
|
||||
header = self.strings("header").format(
|
||||
utils.escape_html(artist),
|
||||
utils.escape_html(track),
|
||||
)
|
||||
if lines:
|
||||
curr_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if progress_ms >= line["time"]:
|
||||
curr_idx = i
|
||||
win_start = max(0, curr_idx - 1)
|
||||
win_end = min(len(lines), curr_idx + 6)
|
||||
rows = []
|
||||
for i in range(win_start, win_end):
|
||||
t = lines[i]["text"] or self.config["dot"]
|
||||
if i == curr_idx:
|
||||
rows.append(
|
||||
f"<b>{self.config['emoji_current']} {utils.escape_html(t)}</b>"
|
||||
)
|
||||
else:
|
||||
rows.append(f"<code>{utils.escape_html(t)}</code>")
|
||||
return header + "\n".join(rows)
|
||||
else:
|
||||
return header + not_synced_str + f"<blockquote expandable>{utils.escape_html((plain or '')[:4000])}</blockquote>"
|
||||
|
||||
def _markup(self, song_url):
|
||||
return [
|
||||
[{"text": "🔗 song.link", "url": song_url}],
|
||||
[{"text": "❌ Close", "callback": self._close_cb}],
|
||||
]
|
||||
|
||||
async def _close_cb(self, call):
|
||||
for track_id, task in list(self._active_tasks.items()):
|
||||
task.cancel()
|
||||
self._active_tasks.pop(track_id, None)
|
||||
try:
|
||||
await call.answer()
|
||||
await call.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def run_loop(self, form, mod, track_id, artist_name, track_name, song_url, lines, plain, not_synced_str):
|
||||
last_display = ""
|
||||
try:
|
||||
while True:
|
||||
pb = mod.sp.current_playback()
|
||||
if not pb or not pb.get("item") or pb["item"]["id"] != track_id:
|
||||
try:
|
||||
await form.edit(
|
||||
self.strings("finished"),
|
||||
reply_markup=[[{"text": "❌ Close", "callback": self._close_cb}]],
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
|
||||
prog = pb.get("progress_ms", 0)
|
||||
content = self._build_content(
|
||||
artist_name, track_name, lines, plain, prog, not_synced_str
|
||||
)
|
||||
|
||||
if content != last_display:
|
||||
try:
|
||||
await form.edit(content, reply_markup=self._markup(song_url))
|
||||
last_display = content
|
||||
except Exception:
|
||||
break
|
||||
|
||||
if not lines:
|
||||
break
|
||||
|
||||
await asyncio.sleep(self.config["lyrics_delay"])
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._active_tasks.pop(track_id, None)
|
||||
|
||||
@loader.command(ru_doc="- показать синхронизированный текст песни")
|
||||
async def snowlcmd(self, message: Message):
|
||||
"""- show synchronized lyrics for current Spotify track"""
|
||||
mod = self.lookup("SpotifyMod")
|
||||
if not mod:
|
||||
form = await self.inline.form("⏳", message=message)
|
||||
await form.edit(
|
||||
self.strings("no_spotifymod"),
|
||||
reply_markup=[[{"text": "Install SpotifyMod", "callback": self.install_spotifymod}]],
|
||||
)
|
||||
return
|
||||
|
||||
acs_tkn = mod.get("acs_tkn")
|
||||
if not acs_tkn:
|
||||
await self.invoke("sauth", " ", "me")
|
||||
form = await self.inline.form("⏳", message=message)
|
||||
await form.edit(
|
||||
self.strings("no_auth"),
|
||||
reply_markup=[[{"text": "Хорошо", "callback": self.close}]],
|
||||
)
|
||||
return
|
||||
|
||||
playback = mod.sp.current_playback()
|
||||
if not playback or not playback.get("item"):
|
||||
return await utils.answer(message, self.strings("no_spotify"))
|
||||
|
||||
track = playback["item"]
|
||||
track_id = track["id"]
|
||||
artist_name = track["artists"][0]["name"]
|
||||
track_name = track["name"]
|
||||
song_url = f"https://song.link/s/{track_id}"
|
||||
|
||||
old = self._active_tasks.pop(track_id, None)
|
||||
if old:
|
||||
old.cancel()
|
||||
|
||||
data = await self._get_lyrics(artist_name, track_name)
|
||||
if data and data.get("timeout"):
|
||||
return utils.answer(
|
||||
message,
|
||||
self.strings["timeout"]
|
||||
)
|
||||
if not data or data.get("instrumental"):
|
||||
track_and_artist = f"{utils.escape_html(track_name)} - {utils.escape_html(artist_name)}"
|
||||
return await utils.answer(
|
||||
message,
|
||||
self.strings("no_lyrics").format(track_and_artist),
|
||||
)
|
||||
|
||||
synced_raw = data.get("syncedLyrics")
|
||||
plain = data.get("plainLyrics", "")
|
||||
|
||||
lines = self._parse_synced(synced_raw) if synced_raw else []
|
||||
not_synced_str = self.strings("not_synced")
|
||||
|
||||
prog = playback.get("progress_ms", 0)
|
||||
initial_content = self._build_content(
|
||||
artist_name, track_name, lines, plain, prog, not_synced_str
|
||||
)
|
||||
|
||||
form = await self.inline.form(
|
||||
text=initial_content,
|
||||
message=message,
|
||||
reply_markup=self._markup(song_url),
|
||||
)
|
||||
|
||||
task = asyncio.ensure_future(
|
||||
self.run_loop(
|
||||
form=form,
|
||||
mod=mod,
|
||||
track_id=track_id,
|
||||
artist_name=artist_name,
|
||||
track_name=track_name,
|
||||
song_url=song_url,
|
||||
lines=lines,
|
||||
plain=plain,
|
||||
not_synced_str=not_synced_str,
|
||||
)
|
||||
)
|
||||
self._active_tasks[track_id] = task
|
||||
@@ -1,352 +0,0 @@
|
||||
# meta developer: @SunnexGB
|
||||
# requires: aiohttp
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/ab42b5e2-91f1-4ed1-8002-51b3184e3839.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/ab42b5e2-91f1-4ed1-8002-51b3184e3839.jpg
|
||||
# meta fhsdesc: YaMusic, music, музыка, яндекс музыка,Lyrics, слова, текст, трек, песня
|
||||
# все же я не знаю трек или сонг, так что пусть будет трек, а не сонг потому что интуитивнее поняттнее,наверное?
|
||||
# крутой баннер да?
|
||||
#current version
|
||||
__version__ = (1, 1, 2)
|
||||
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
from ..types import InlineCall
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
|
||||
@loader.tds
|
||||
class YandexLyrics(loader.Module):
|
||||
"""life lyrics current song"""
|
||||
|
||||
strings = {
|
||||
"name": "YandexLyrics",
|
||||
"no_YaMusicMod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>YaMusicMod not found,but u can install it. You can also support developer: </b> @codrago_m",
|
||||
"no_auth": "<tg-emoji emoji-id=5429225166250984904>⁉️</tg-emoji><b> You not authorized in SpotifyMod, visit you Saved Messages and setup token to continue.</b>",
|
||||
"no_ym": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>Nothing is playing on YaMusic.</b>",
|
||||
"no_lyrics": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>Lyrics not found for:</b> <code>{}</code>",
|
||||
"not_synced": "<i><tg-emoji emoji-id=5431445849026611010>⚠️</tg-emoji> Lyrics are not synchronized.</i>\n\n",
|
||||
"finished": "<tg-emoji emoji-id=5429638011392377649>‼️</tg-emoji> Playback ended or track changed.",
|
||||
"header": "<tg-emoji emoji-id=5429413328768224565>🎤</tg-emoji> <b>{} - {}</b>\n\n",
|
||||
"timeout": "<b><tg-emoji emoji-id=5429455831764584284>⏳</tg-emoji></b><b> Oopsi, looks like we've got a timeout here</b>.",
|
||||
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"cls_doc": "Лайв слова текущей песни.",
|
||||
"no_YaMusicMod": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>YaMusicMod не найден,но его можно установить. Вы также можете поддержать разработчика: </b> @codrago_m",
|
||||
"no_auth": "<tg-emoji emoji-id=5429225166250984904>⁉️</tg-emoji><b> Вы не авторизированы в YaMusicMod, перейдите в Избранное и установите токен для продолжения работы.</b>",
|
||||
"no_ym": "<tg-emoji emoji-id=5429164207780152924>😅</tg-emoji> <b>В YaMusic ничего не играет.</b>",
|
||||
"no_lyrics": "<tg-emoji emoji-id=5431402435497181911>💢</tg-emoji> <b>Текст не найден для:</b> <code>{}</code>",
|
||||
"not_synced": "<i><tg-emoji emoji-id=5431445849026611010>⚠️</tg-emoji> Текст не синхронизирован.</i>\n\n",
|
||||
"finished": "<tg-emoji emoji-id=5429638011392377649>‼️</tg-emoji> Воспроизведение завершено или трек сменился.",
|
||||
"header": "<tg-emoji emoji-id=5429413328768224565>🎤</tg-emoji> <b>{} - {}</b>\n\n",
|
||||
"timeout": "<b><tg-emoji emoji-id=5429455831764584284>⏳</tg-emoji></b><b> Упси, похоже кто то словил таймаут.</b>.",
|
||||
}
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._active_tasks: dict = {}
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"emoji_current",
|
||||
"<tg-emoji emoji-id='5215679757366089921'>🤯</tg-emoji>",
|
||||
"Emoji for current line",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"dot",
|
||||
"♪",
|
||||
"instrumental_emoji or text",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"lyrics_delay",
|
||||
0.5,
|
||||
"delay in switching to a new timing sector with words",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"request_timeout",
|
||||
12,
|
||||
"timeout value",
|
||||
),
|
||||
)
|
||||
|
||||
async def install_yamusic(self, call: InlineCall):
|
||||
mod_url = "https://raw.githubusercontent.com/coddrago/modules/main/YaMusic.py"
|
||||
try:
|
||||
m = self.lookup("Modules") or self.lookup("loader")
|
||||
await m.download_and_install(mod_url)
|
||||
await call.answer("YaMusicMod installed!", show_alert=True)
|
||||
mod = self.lookup("YaMusicMod")
|
||||
acs_tkn = mod.get("__config__")["token"] if mod else "****"
|
||||
if not acs_tkn:
|
||||
await self.invoke("yguide", " ", "me")
|
||||
await call.edit(
|
||||
self.strings("no_auth"),
|
||||
reply_markup=[[{"text": "Хорошо", "callback": self.close}]],
|
||||
)
|
||||
else:
|
||||
await call.delete()
|
||||
except Exception as e:
|
||||
await call.answer(f"Error! Check logs.\n{e}", show_alert=True)
|
||||
|
||||
def close(self, call: InlineCall):
|
||||
return call.delete()
|
||||
|
||||
async def _get_lyrics(self, artist: str, track: str):
|
||||
clean_track = re.sub(r"\(.*?\)|\[.*?\]", "", track).strip()
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://lrclib.net/api/search",
|
||||
params={"track_name": clean_track, "artist_name": artist},
|
||||
timeout=aiohttp.ClientTimeout(total=(self.config["request_timeout"])),
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
res = await resp.json()
|
||||
return res[0] if res else None
|
||||
except asyncio.TimeoutError:
|
||||
return {"timeout": True}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _parse_synced(self, synced_text: str) -> list:
|
||||
lines = []
|
||||
for line in synced_text.split("\n"):
|
||||
m = re.search(r"\[(\d+):(\d+\.\d+)\](.*)", line)
|
||||
if m:
|
||||
mins, secs, text = m.groups()
|
||||
lines.append({
|
||||
"time": (int(mins) * 60 + float(secs)) * 1000,
|
||||
"text": text.strip(),
|
||||
})
|
||||
return lines
|
||||
|
||||
def _build_content(self, artist, track, lines, plain, progress_ms, not_synced_str):
|
||||
header = self.strings("header").format(
|
||||
utils.escape_html(artist),
|
||||
utils.escape_html(track),
|
||||
)
|
||||
if lines:
|
||||
curr_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if progress_ms >= line["time"]:
|
||||
curr_idx = i
|
||||
win_start = max(0, curr_idx - 1)
|
||||
win_end = min(len(lines), curr_idx + 6)
|
||||
rows = []
|
||||
for i in range(win_start, win_end):
|
||||
t = lines[i]["text"] or self.config["dot"]
|
||||
if i == curr_idx:
|
||||
rows.append(
|
||||
f"<b>{self.config['emoji_current']} {utils.escape_html(t)}</b>"
|
||||
)
|
||||
else:
|
||||
rows.append(f"<code>{utils.escape_html(t)}</code>")
|
||||
return header + "\n".join(rows)
|
||||
else:
|
||||
return header + not_synced_str + f"<blockquote expandable>{utils.escape_html((plain or '')[:4000])}</blockquote>"
|
||||
|
||||
def _markup(self, song_url):
|
||||
return [
|
||||
[{"text": "🔗 song.link", "url": song_url}],
|
||||
[{"text": "❌ Close", "callback": self._close_cb}],
|
||||
]
|
||||
|
||||
async def _close_cb(self, call):
|
||||
for track_id, task in list(self._active_tasks.items()):
|
||||
task.cancel()
|
||||
self._active_tasks.pop(track_id, None)
|
||||
try:
|
||||
await call.answer()
|
||||
await call.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def run_loop(self, form, mod, track_id, artist_name, track_name, song_url, lines, plain, not_synced_str):
|
||||
last_display = ""
|
||||
try:
|
||||
while True:
|
||||
pb = await mod._YaMusicMod__get_now_playing()
|
||||
if not pb or not pb.get("track") or pb["track"]["track_id"] != track_id:
|
||||
try:
|
||||
await form.edit(
|
||||
self.strings("finished"),
|
||||
reply_markup=[[{"text": "❌ Close", "callback": self._close_cb}]],
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
|
||||
prog = pb.get("progress_ms", 0)
|
||||
content = self._build_content(
|
||||
artist_name, track_name, lines, plain, prog, not_synced_str
|
||||
)
|
||||
|
||||
if content != last_display:
|
||||
try:
|
||||
await form.edit(content, reply_markup=self._markup(song_url))
|
||||
last_display = content
|
||||
except Exception:
|
||||
break
|
||||
|
||||
if not lines:
|
||||
break
|
||||
|
||||
await asyncio.sleep(self.config["lyrics_delay"])
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._active_tasks.pop(track_id, None)
|
||||
|
||||
@loader.command(ru_doc="- показать синхронизированный текст песни")
|
||||
async def ynowlcmd(self, message: Message):
|
||||
"""- show synchronized lyrics for current YaMusic track"""
|
||||
mod = self.lookup("YaMusic")
|
||||
if not mod:
|
||||
form = await self.inline.form("⏳", message=message)
|
||||
await form.edit(
|
||||
self.strings("no_YaMusicMod"),
|
||||
reply_markup=[[{"text": "Install YaMusic", "callback": self.install_yamusic}]],
|
||||
)
|
||||
return
|
||||
|
||||
ya_token = mod.get("__config__")["token"]
|
||||
if not ya_token:
|
||||
await self.invoke("yguide", " ", "me")
|
||||
form = await self.inline.form("⏳", message=message)
|
||||
await form.edit(
|
||||
self.strings("no_auth"),
|
||||
reply_markup=[[{"text": "Хорошо", "callback": self.close}]],
|
||||
)
|
||||
return
|
||||
|
||||
playback = await mod._YaMusicMod__get_now_playing()
|
||||
if not playback or not playback.get("track"):
|
||||
return await utils.answer(message, self.strings("no_ym"))
|
||||
|
||||
track = playback["track"]
|
||||
track_id = track["track_id"]
|
||||
artist_name = ", ".join(track["artist"])
|
||||
track_name = track["title"]
|
||||
song_url = f"https://song.link/s/{track_id}"
|
||||
|
||||
old = self._active_tasks.pop(track_id, None)
|
||||
if old:
|
||||
old.cancel()
|
||||
|
||||
data = await self._get_lyrics(artist_name, track_name)
|
||||
if data and data.get("timeout"):
|
||||
return utils.answer(
|
||||
message,
|
||||
self.strings["timeout"]
|
||||
)
|
||||
if not data or data.get("instrumental"):
|
||||
track_and_artist = f"{utils.escape_html(track_name)} - {utils.escape_html(artist_name)}"
|
||||
return await utils.answer(
|
||||
message,
|
||||
self.strings("no_lyrics").format(track_and_artist),
|
||||
)
|
||||
|
||||
synced_raw = data.get("syncedLyrics")
|
||||
plain = data.get("plainLyrics", "")
|
||||
|
||||
lines = self._parse_synced(synced_raw) if synced_raw else []
|
||||
not_synced_str = self.strings("not_synced")
|
||||
|
||||
prog = playback.get("progress_ms", 0)
|
||||
initial_content = self._build_content(
|
||||
artist_name, track_name, lines, plain, prog, not_synced_str
|
||||
)
|
||||
|
||||
form = await self.inline.form(
|
||||
text=initial_content,
|
||||
message=message,
|
||||
reply_markup=self._markup(song_url),
|
||||
)
|
||||
|
||||
task = asyncio.create_task(
|
||||
self.run_loop(
|
||||
form=form,
|
||||
mod=mod,
|
||||
track_id=track_id,
|
||||
artist_name=artist_name,
|
||||
track_name=track_name,
|
||||
song_url=song_url,
|
||||
lines=lines,
|
||||
plain=plain,
|
||||
not_synced_str=not_synced_str,
|
||||
)
|
||||
)
|
||||
self._active_tasks[track_id] = task
|
||||
|
||||
# Fan-fantasizing
|
||||
# Fa-fa-fa-fantasizing
|
||||
# You and I-I-I
|
||||
# When I close my eyes (my eyes)
|
||||
# Nothings real
|
||||
# Fantasizing (fantasizing)
|
||||
# Bout you and I
|
||||
# Cos you only hit my line
|
||||
# When you wanna waste time
|
||||
# I know you're so busy
|
||||
# But trust me baby I'm not blind (blind)
|
||||
# Uh oh, you and I
|
||||
# We could never be
|
||||
# Uh oh you and I
|
||||
# Cos we will never be
|
||||
# Uh oh, you and I
|
||||
# No, we will never be
|
||||
# That pretty picture that I painted in my mind (mind)
|
||||
# So tell me what (tell me, tell me)
|
||||
# The view is like
|
||||
# With your head in the clouds
|
||||
# And tell me what (tell me, tell me)
|
||||
# It feels like to be right all the time
|
||||
# You say that you love me
|
||||
# But you don't even love yourself (no)
|
||||
# Wanna get in my head
|
||||
# But I ain't gonna let you close (no)
|
||||
# Tryna control me
|
||||
# But I ain't gon' play your game
|
||||
# No more
|
||||
# No I won't
|
||||
# When I close (when I close)
|
||||
# My eyes (my eyes)
|
||||
# Nothing's real (no)
|
||||
# Fantasizing (fantasize)
|
||||
# bout you-you-you-you-you
|
||||
# You-you-you-you
|
||||
# You-you-you-you
|
||||
# You-you-you
|
||||
# And I
|
||||
# You-you-you-you-you-you-you
|
||||
# You-you-you-you-you-you-you
|
||||
# You-you-you-you-you
|
||||
# And I
|
||||
# This is the last time I tell you
|
||||
# Don't come round my way if you're just gon' waste my time
|
||||
# And no, I won't be there for the long run
|
||||
# No, not I
|
||||
# But you never get (never get)
|
||||
# The message, do you?
|
||||
# You never seem (never seem)
|
||||
# To grip an understanding
|
||||
# That you emulate a ghost
|
||||
# I pointed out all of your flaws
|
||||
# But you still came up with excuses for em all
|
||||
# So typical (so typical)
|
||||
# You know it all
|
||||
# So of course, I'm the one that's wrong (right?)
|
||||
# When I close my eyes
|
||||
# Nothings real
|
||||
# Fan- fantasize
|
||||
# Fa-fa-fa-fa
|
||||
# Fantasizing
|
||||
# Fa-fantasizing bout you and I
|
||||
@@ -1,486 +0,0 @@
|
||||
# meta developer: @H_SunMods
|
||||
#meta banner: https://i.ibb.co/LdN9FXjc/logo.webp
|
||||
# __version__
|
||||
__version__ = ("alpha", "1.0", 0)
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
from urllib.request import Request, urlopen
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
from ..types import InlineCall
|
||||
|
||||
|
||||
prologue_dialogs_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/ddialogs/prologue_only.json"
|
||||
routes_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/ddialogs/routes_prologue.json"
|
||||
menu_background_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/in_telegram_images/Start_Menu.jpg"
|
||||
save_background_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/in_telegram_images/Save_Menu.png"
|
||||
|
||||
|
||||
@loader.tds
|
||||
class EverlastingSummer(loader.Module):
|
||||
"""Встретив Семёна, главного героя игры, вы никогда бы не обратили на него внимания. Просто обычный молодой человек среди тысяч, даже сотен тысяч таких, как он, в каждом обычном городе. Но однажды с ним происходит нечто совершенно необычное: он засыпает в автобусе зимой и просыпается... посреди жаркого лета. Перед ним - "Совёнок" - пионерский лагерь, а за ним - его прежняя жизнь. Чтобы понять, что с ним произошло, Семёну придется познакомиться с местными жителями (и, возможно, даже найти любовь), сориентироваться в сложном лабиринте человеческих отношений и своих собственных проблем, а также разгадать тайны лагеря. И ответить на главный вопрос - как вернуться? Стоит ли ему возвращаться?"""
|
||||
strings = {
|
||||
"name": "EverlastingSummer",
|
||||
"menu": "<b>Пролог</b>",
|
||||
"disclaimer": (
|
||||
"Игра является плодом фантазии её разработчиков\n"
|
||||
"и не ставит перед собой цели затронуть или иным\n"
|
||||
"образом оскорбить кого-либо по религиозному,расовому,\n"
|
||||
"социальному, экономическому или видовому признаку.\n"
|
||||
"Также любое ущемление чувства прекрасного, активной\n"
|
||||
"гражданской позиции или иных высоких душевных порывов\n"
|
||||
"игроков разработчики оставляют на их совести.\n"
|
||||
"Совпадения героев с вашими реальными (и воображаемыми)\n"
|
||||
"знакомыми,соседями,коллегами, тульпами считать случайным.\n"
|
||||
"Все героини достигли восемнадцатилетнего возраста,\n"
|
||||
"и они дали письменное согласие на участие в игре\n"
|
||||
"(выписка из истории болезни сценариста предоставляется по требованию).\n"
|
||||
"При разработке не пострадало ни одного маскота, животного или человека. Приятной игры!"
|
||||
),
|
||||
"bad": "<b>Не удалось загрузить сценарий</b>",
|
||||
"end": "<b>{}</b>",
|
||||
"save_header": "<b>Сохранения</b>",
|
||||
"load_header": "<b>Загрузить игру</b>",
|
||||
"default_route_question": "<b>Что выберете?</b>",
|
||||
"or_game": "Игра",
|
||||
"or_character": "Персонаж",
|
||||
"cutscene_text": "<tg-emoji emoji-id=5332542518955374404>💫</tg-emoji>",
|
||||
"opening_title": "<b>Опенинг</b>",
|
||||
"opening_next": "Пропустить опенинг",
|
||||
"saved": "игра сохранена в слот № {}",
|
||||
"loaded": "Сохранение № {} загружено",
|
||||
"empty": "Слот {} пуст",
|
||||
"rewrite": "Слот {} уже занят. Перезаписать?",
|
||||
"state_slots_from_menu": "slots_from_menu",
|
||||
"chapter_prologue": "prologue",
|
||||
"save_action": "save",
|
||||
"load_action": "load",
|
||||
"mode_ask_rewrite": "ask_rewrite",
|
||||
"mode_ended": "ended",
|
||||
"mode_menu": "menu",
|
||||
"mode_play": "play",
|
||||
"mode_slots": "slots",
|
||||
"type_label": "label",
|
||||
"type_jump": "jump",
|
||||
"type_scene": "scene",
|
||||
"type_dialogue": "dialogue",
|
||||
"type_narration": "narration",
|
||||
"type_route": "route",
|
||||
"type_opening": "opening",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(loader.ConfigValue("cut_speed", 3))
|
||||
self.dialogs_url = prologue_dialogs_url
|
||||
self.routes_url = routes_url
|
||||
self.menu_image = menu_background_url
|
||||
self.save_image = save_background_url
|
||||
self.dialogs_data = None
|
||||
self.routes_data = None
|
||||
self.label_index = {}
|
||||
|
||||
async def json_load(self, url: str):
|
||||
last_error = None
|
||||
for i in range(3):
|
||||
try:
|
||||
def run():
|
||||
req = Request(url, headers={"User-Agent": "Mozilla/5.0 HSunMods"})
|
||||
with urlopen(req, timeout=60) as x:
|
||||
return json.loads(x.read().decode("utf-8"))
|
||||
return await asyncio.to_thread(run)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if i < 2:
|
||||
await asyncio.sleep(1.5 * (i + 1))
|
||||
raise last_error
|
||||
|
||||
async def load_data(self, force: bool = False):
|
||||
if self.dialogs_data is not None and self.routes_data is not None and not force:
|
||||
return True
|
||||
try:
|
||||
self.dialogs_data = await self.json_load(self.dialogs_url)
|
||||
self.routes_data = await self.json_load(self.routes_url)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
prologue_nodes = self.dialogs_data.get(self.strings["chapter_prologue"])
|
||||
if not isinstance(prologue_nodes, list):
|
||||
return False
|
||||
self.dialogs_data = {self.strings["chapter_prologue"]: prologue_nodes}
|
||||
|
||||
self.label_index = {}
|
||||
for node_index, node in enumerate(prologue_nodes):
|
||||
if isinstance(node, dict) and node.get("type") == self.strings["type_label"]:
|
||||
self.label_index[node.get("name")] = (self.strings["chapter_prologue"], node_index)
|
||||
return True
|
||||
|
||||
def state_get(self):
|
||||
return self.get(
|
||||
"state",
|
||||
{
|
||||
"mode": self.strings["mode_menu"],
|
||||
"chapter": self.strings["chapter_prologue"],
|
||||
"idx": 0,
|
||||
"pending": None,
|
||||
"scene": {},
|
||||
"vars": {},
|
||||
},
|
||||
)
|
||||
|
||||
def state_set(self, state_data):
|
||||
self.set("state", state_data)
|
||||
|
||||
def slots_get(self):
|
||||
return self.get("slots", {})
|
||||
|
||||
def slots_set(self, slots_data):
|
||||
self.set("slots", slots_data)
|
||||
|
||||
async def ui(self, target, text, kb=None, photo=None):
|
||||
if isinstance(target, InlineCall):
|
||||
if photo:
|
||||
try:
|
||||
return await target.edit(text, reply_markup=kb, photo=photo)
|
||||
except TypeError:
|
||||
try:
|
||||
return await target.edit(text, reply_markup=kb, file=photo)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return await target.edit(text, reply_markup=kb)
|
||||
except Exception:
|
||||
raise
|
||||
if photo:
|
||||
try:
|
||||
return await utils.answer(target, text, reply_markup=kb, photo=photo)
|
||||
except Exception:
|
||||
pass
|
||||
return await utils.answer(target, text, reply_markup=kb)
|
||||
|
||||
def menu_kb(self):
|
||||
return [
|
||||
[{"text": "Начать пролог", "callback": self.new_game}],
|
||||
[{"text": "Сохранения", "callback": self.save_menu, "args": (self.strings["load_action"],)}],
|
||||
[{"text": "Дисклеймер", "callback": self.disclaimer_msg}],
|
||||
]
|
||||
|
||||
def start_kb(self):
|
||||
return [
|
||||
[{"text": "➤", "callback": self.next_step}],
|
||||
[{"text": "Сохранить", "callback": self.save_menu, "args": (self.strings["save_action"],)}],
|
||||
[{"text": "Меню", "callback": self.menu}],
|
||||
]
|
||||
|
||||
def save_kb(self, mode: str):
|
||||
slots = self.slots_get()
|
||||
row = []
|
||||
for i in range(1, 6):
|
||||
k = str(i)
|
||||
b = {"text": k, "callback": self.save_action, "args": (mode, i)}
|
||||
if k in slots:
|
||||
b["style"] = "success"
|
||||
row.append(b)
|
||||
return [row, [{"text": "Назад", "callback": self.back_from_saves}]]
|
||||
|
||||
def choice_kb(self, route_id: str):
|
||||
opts = self.routes_data.get(route_id, {}).get("options", {})
|
||||
rows = []
|
||||
for i, txt in enumerate(opts.keys()):
|
||||
rows.append([{"text": txt, "callback": self.pick_option, "args": (route_id, i)}])
|
||||
rows.append([{"text": "Сохранить", "callback": self.save_menu, "args": (self.strings["save_action"],)}])
|
||||
rows.append([{"text": "Меню", "callback": self.menu}])
|
||||
return rows
|
||||
|
||||
def opening_kb(self):
|
||||
return [[{"text": self.strings["opening_next"], "callback": self.opening_done}]]
|
||||
|
||||
def state_preservation(self, state_data):
|
||||
return {
|
||||
"chapter": state_data.get("chapter"),
|
||||
"idx": state_data.get("idx", 0),
|
||||
"part": state_data.get("part", 0),
|
||||
"pending": copy.deepcopy(state_data.get("pending")),
|
||||
"scene": copy.deepcopy(state_data.get("scene")),
|
||||
"vars": copy.deepcopy(state_data.get("vars", {})),
|
||||
"mode": self.strings["mode_play"],
|
||||
}
|
||||
|
||||
def scene_photo(self, state_data):
|
||||
u = (state_data.get("scene") or {}).get("raw_url")
|
||||
return u if isinstance(u, str) else None
|
||||
|
||||
def wait_text(self, t: str):
|
||||
return [x.strip() for x in t.split("{w}") if x.strip()] or [t]
|
||||
|
||||
def render_dialogs(self, state_data):
|
||||
pending_node = state_data.get("pending") or {}
|
||||
if pending_node.get("type") == self.strings["type_dialogue"]:
|
||||
who = (pending_node.get("character") or pending_node.get("char_id") or self.strings["or_character"]).strip()
|
||||
txt = " ".join(pending_node.get("parts", [])[: pending_node.get("part", 1)])
|
||||
return f"<b>{who}:</b>\n<blockquote>{txt}</blockquote>"
|
||||
if pending_node.get("type") == self.strings["type_narration"]:
|
||||
return " ".join(pending_node.get("parts", [])[: pending_node.get("part", 1)])
|
||||
return ""
|
||||
|
||||
def is_ending_label(self, name: str):
|
||||
return name in self.routes_data.get("endings", {}).get("labels", [])
|
||||
|
||||
async def go(self, target, state_data):
|
||||
cut_scene_speed_fallback = self.config["cut_speed"]
|
||||
while True:
|
||||
chapter_nodes = self.dialogs_data.get(self.strings["chapter_prologue"], [])
|
||||
node_index = state_data.get("idx", 0)
|
||||
if node_index >= len(chapter_nodes):
|
||||
ending_name = self.routes_data.get("endings", {}).get("fallback", "main_bad_ending")
|
||||
state_data["mode"] = self.strings["mode_ended"]
|
||||
state_data["ending"] = ending_name
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, self.strings["end"].format(ending_name), self.menu_kb(), self.scene_photo(state_data))
|
||||
return
|
||||
|
||||
current_node = chapter_nodes[node_index]
|
||||
node_type = current_node.get("type")
|
||||
|
||||
if node_type == self.strings["type_label"]:
|
||||
if self.is_ending_label(current_node.get("name")):
|
||||
state_data["mode"] = self.strings["mode_ended"]
|
||||
state_data["ending"] = current_node.get("name")
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, self.strings["end"].format(current_node.get("name")), self.menu_kb(), self.scene_photo(state_data))
|
||||
return
|
||||
state_data["idx"] = node_index + 1
|
||||
continue
|
||||
|
||||
if node_type == self.strings["type_jump"]:
|
||||
jump_target = self.label_index.get(current_node.get("label"))
|
||||
if jump_target:
|
||||
state_data["chapter"], state_data["idx"] = jump_target
|
||||
else:
|
||||
state_data["idx"] = node_index + 1
|
||||
continue
|
||||
|
||||
if node_type == self.strings["type_scene"]:
|
||||
state_data["scene"] = {
|
||||
"raw_url": current_node.get("raw_url"),
|
||||
"location": current_node.get("location"),
|
||||
"action": current_node.get("action"),
|
||||
"kind": current_node.get("kind"),
|
||||
"name": current_node.get("name"),
|
||||
}
|
||||
state_data["idx"] = node_index + 1
|
||||
next_node = chapter_nodes[state_data["idx"]] if state_data["idx"] < len(chapter_nodes) else None
|
||||
if isinstance(next_node, dict) and next_node.get("type") == self.strings["type_scene"]:
|
||||
scene_duration = current_node.get("duration")
|
||||
if scene_duration is None:
|
||||
if cut_scene_speed_fallback is None:
|
||||
scene_delay_seconds = 0.0
|
||||
else:
|
||||
try:
|
||||
scene_delay_seconds = float(cut_scene_speed_fallback)
|
||||
except Exception:
|
||||
scene_delay_seconds = 0.0
|
||||
else:
|
||||
try:
|
||||
scene_delay_seconds = float(scene_duration)
|
||||
except Exception:
|
||||
scene_delay_seconds = 0.0
|
||||
if scene_delay_seconds < 0:
|
||||
scene_delay_seconds = 0.0
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, self.strings["cutscene_text"], None, self.scene_photo(state_data))
|
||||
if scene_delay_seconds > 0:
|
||||
await asyncio.sleep(scene_delay_seconds)
|
||||
continue
|
||||
continue
|
||||
|
||||
if node_type in {self.strings["type_dialogue"], self.strings["type_narration"]}:
|
||||
state_data["pending"] = {
|
||||
"type": node_type,
|
||||
"parts": self.wait_text(current_node.get("text", "")),
|
||||
"part": 1,
|
||||
"char_id": current_node.get("char_id"),
|
||||
"character": current_node.get("character"),
|
||||
}
|
||||
state_data["mode"] = self.strings["mode_play"]
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, self.render_dialogs(state_data), self.start_kb(), self.scene_photo(state_data))
|
||||
return
|
||||
|
||||
if node_type == self.strings["type_route"]:
|
||||
route_id = current_node.get("id")
|
||||
route_question = self.routes_data.get(route_id, {}).get("question") or self.strings["default_route_question"]
|
||||
state_data["pending"] = {"type": self.strings["type_route"], "id": route_id}
|
||||
state_data["mode"] = self.strings["mode_play"]
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, route_question, self.choice_kb(route_id), self.scene_photo(state_data))
|
||||
return
|
||||
|
||||
if node_type == self.strings["type_opening"] or (node_type == self.strings["type_label"] and current_node.get("kind") == self.strings["type_opening"]):
|
||||
state_data["scene"] = {
|
||||
"raw_url": current_node.get("raw_url"),
|
||||
"location": current_node.get("location"),
|
||||
"action": current_node.get("action"),
|
||||
"kind": current_node.get("kind") or self.strings["type_opening"],
|
||||
"name": current_node.get("name") or self.strings["type_opening"],
|
||||
}
|
||||
state_data["pending"] = {"type": self.strings["type_opening"]}
|
||||
state_data["mode"] = self.strings["mode_play"]
|
||||
state_data["idx"] = node_index + 1
|
||||
self.state_set(state_data)
|
||||
await self.ui(target, self.strings["opening_title"], self.opening_kb(), self.scene_photo(state_data))
|
||||
return
|
||||
|
||||
state_data["idx"] = node_index + 1
|
||||
|
||||
async def menu(self, call: InlineCall):
|
||||
state = self.state_get()
|
||||
state["mode"] = self.strings["mode_menu"]
|
||||
state["pending"] = None
|
||||
self.state_set(state)
|
||||
await self.ui(call, self.strings["menu"], self.menu_kb(), self.menu_image)
|
||||
|
||||
async def disclaimer_msg(self, call: InlineCall):
|
||||
await self.ui(call, self.strings["disclaimer"], [[{"text": "Назад", "callback": self.menu}]], self.menu_image)
|
||||
|
||||
async def new_game(self, call: InlineCall):
|
||||
ok = await self.load_data(force=False)
|
||||
if not ok:
|
||||
await call.answer(self.strings["bad"], show_alert=True)
|
||||
return
|
||||
state = self.state_get()
|
||||
state.update(
|
||||
{
|
||||
"chapter": self.strings["chapter_prologue"],
|
||||
"idx": 0,
|
||||
"part": 0,
|
||||
"pending": None,
|
||||
"scene": {},
|
||||
"vars": {},
|
||||
"mode": self.strings["mode_play"],
|
||||
}
|
||||
)
|
||||
self.state_set(state)
|
||||
await self.go(call, state)
|
||||
|
||||
async def next_step(self, call: InlineCall):
|
||||
state = self.state_get()
|
||||
pending_node = state.get("pending") or {}
|
||||
if pending_node.get("type") == self.strings["type_opening"]:
|
||||
await self.menu(call)
|
||||
return
|
||||
if pending_node.get("type") in {self.strings["type_dialogue"], self.strings["type_narration"]}:
|
||||
if pending_node.get("part", 1) < len(pending_node.get("parts", [])):
|
||||
pending_node["part"] += 1
|
||||
state["pending"] = pending_node
|
||||
self.state_set(state)
|
||||
await self.ui(call, self.render_dialogs(state), self.start_kb(), self.scene_photo(state))
|
||||
return
|
||||
state["idx"] += 1
|
||||
state["pending"] = None
|
||||
self.state_set(state)
|
||||
await self.go(call, state)
|
||||
return
|
||||
await self.go(call, state)
|
||||
|
||||
async def opening_done(self, call: InlineCall):
|
||||
state = self.state_get()
|
||||
state["pending"] = None
|
||||
state["mode"] = self.strings["mode_menu"]
|
||||
self.state_set(state)
|
||||
await self.menu(call)
|
||||
|
||||
async def pick_option(self, call: InlineCall, route_id: str, option_index: int):
|
||||
state = self.state_get()
|
||||
option_items = list((self.routes_data.get(route_id, {}).get("options") or {}).items())
|
||||
if option_index < 0 or option_index >= len(option_items):
|
||||
return
|
||||
_, option_data = option_items[option_index]
|
||||
jump_label = option_data.get("jump")
|
||||
if jump_label and jump_label in self.label_index:
|
||||
state["chapter"], state["idx"] = self.label_index[jump_label]
|
||||
else:
|
||||
state["idx"] += 1
|
||||
state["pending"] = None
|
||||
self.state_set(state)
|
||||
await self.go(call, state)
|
||||
|
||||
async def save_menu(self, call: InlineCall, mode: str):
|
||||
state = self.state_get()
|
||||
state[self.strings["state_slots_from_menu"]] = state.get("mode") == self.strings["mode_menu"]
|
||||
state["mode"] = self.strings["mode_slots"]
|
||||
self.state_set(state)
|
||||
title_text = self.strings["save_header"] if mode == self.strings["save_action"] else self.strings["load_header"]
|
||||
await self.ui(call, title_text, self.save_kb(mode), self.save_image)
|
||||
|
||||
async def back_from_saves(self, call: InlineCall):
|
||||
state = self.state_get()
|
||||
if state.get(self.strings["state_slots_from_menu"]):
|
||||
state[self.strings["state_slots_from_menu"]] = False
|
||||
state["mode"] = self.strings["mode_menu"]
|
||||
state["pending"] = None
|
||||
self.state_set(state)
|
||||
await self.menu(call)
|
||||
return
|
||||
if state.get("chapter") and state.get("mode") != self.strings["mode_menu"]:
|
||||
state["mode"] = self.strings["mode_play"]
|
||||
self.state_set(state)
|
||||
pending_node = state.get("pending")
|
||||
if pending_node and pending_node.get("type") == self.strings["type_route"]:
|
||||
route_id = pending_node.get("id")
|
||||
question_text = self.routes_data.get(route_id, {}).get("question") or self.strings["default_route_question"]
|
||||
await self.ui(call, question_text, self.choice_kb(route_id), self.scene_photo(state))
|
||||
return
|
||||
await self.ui(call, self.render_dialogs(state) or self.strings["or_game"], self.start_kb(), self.scene_photo(state))
|
||||
return
|
||||
await self.menu(call)
|
||||
|
||||
async def save_action(self, call: InlineCall, mode: str, n: int):
|
||||
slots = self.slots_get()
|
||||
state = self.state_get()
|
||||
slot_key = str(n)
|
||||
if mode == self.strings["save_action"]:
|
||||
if slot_key in slots:
|
||||
state["mode"] = self.strings["mode_ask_rewrite"]
|
||||
self.state_set(state)
|
||||
kb = [
|
||||
[
|
||||
{"text": "Да", "callback": self.rewrite_true, "args": (n,)},
|
||||
{"text": "Нет", "callback": self.save_menu, "args": (self.strings["save_action"],)},
|
||||
],
|
||||
[{"text": "Назад", "callback": self.back_from_saves}],
|
||||
]
|
||||
await self.ui(call, self.strings["rewrite"].format(n), kb, self.save_image)
|
||||
return
|
||||
slots[slot_key] = self.state_preservation(state)
|
||||
self.slots_set(slots)
|
||||
await call.answer(self.strings["saved"].format(n), show_alert=True)
|
||||
await self.save_menu(call, self.strings["save_action"])
|
||||
return
|
||||
if slot_key not in slots:
|
||||
await call.answer(self.strings["empty"].format(n), show_alert=True)
|
||||
return
|
||||
loaded_state = copy.deepcopy(slots[slot_key])
|
||||
self.state_set(loaded_state)
|
||||
await call.answer(self.strings["loaded"].format(n), show_alert=True)
|
||||
await self.go(call, loaded_state)
|
||||
|
||||
async def rewrite_true(self, call: InlineCall, n: int):
|
||||
slots = self.slots_get()
|
||||
state = self.state_get()
|
||||
slots[str(n)] = self.state_preservation(state)
|
||||
self.slots_set(slots)
|
||||
await call.answer(self.strings["saved"].format(n), show_alert=True)
|
||||
await self.save_menu(call, self.strings["save_action"])
|
||||
|
||||
@loader.command()
|
||||
async def bl(self, message: Message):
|
||||
"""Запустить ваше бесконечное лето,нууу точнее пока что его пролог."""
|
||||
ok = await self.load_data()
|
||||
if not ok:
|
||||
await utils.answer(message, self.strings["bad"])
|
||||
return
|
||||
await self.ui(message, self.strings["menu"], self.menu_kb(), self.menu_image)
|
||||
@@ -1,9 +0,0 @@
|
||||
SpotiSaver
|
||||
SpotifyLyrics
|
||||
YandexLyrics
|
||||
HerokuTime
|
||||
Shazamio
|
||||
ForkCircles
|
||||
Mikuru
|
||||
pairavatars
|
||||
ASCII
|
||||
@@ -1,57 +0,0 @@
|
||||
# requires: https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz
|
||||
# meta banner: https://i.ibb.co/yFVJ6L5D/pairavs.webp
|
||||
# meta developer: @SunnexGB
|
||||
# я хочу красивый баннер,не осуждайте.
|
||||
# add version
|
||||
__version__ = (1, 0, 1)
|
||||
|
||||
import io
|
||||
from PIL import Image
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
|
||||
@loader.tds
|
||||
class pairavatars(loader.Module):
|
||||
"""Create pair avatars"""
|
||||
strings = {
|
||||
"name": "PairAvatars",
|
||||
"no_reply": "<tg-emoji emoji-id=5408830797513784663>🚫</tg-emoji> | <b>Reply to photo!</b>",
|
||||
"processing": "<tg-emoji emoji-id=5332695273762223342>💫</tg-emoji> | <b>Processing...</b>",
|
||||
"error": "<tg-emoji emoji-id=5409235172979672859>⚠️</tg-emoji> | <b>Error`</b>"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Создаёт парные авы",
|
||||
"no_reply": "<tg-emoji emoji-id=5408830797513784663>🚫</tg-emoji> | <b>Ответь на фото!</b>",
|
||||
"processing": "<tg-emoji emoji-id=5332695273762223342>💫</tg-emoji> | <b>Обработка...</b>",
|
||||
"error": "<tg-emoji emoji-id=5409235172979672859>⚠️</tg-emoji> | <b>Еррорь</b>"
|
||||
}
|
||||
|
||||
@loader.command(ru_doc="- Создать парные аватарки (команда работает ТОЛЬКО ответом на сообщение)", only_reply=True)
|
||||
async def pairavs(self, message: Message):
|
||||
"""- Create pair avatars (command work ONLY reply message)"""
|
||||
reply = await message.get_reply_message()
|
||||
processing_msg = await utils.answer(message, self.strings["processing"])
|
||||
try:
|
||||
tmp_data = await message.client.download_media(reply.photo, bytes)
|
||||
img = Image.open(io.BytesIO(tmp_data))
|
||||
w, h = img.size
|
||||
center = w // 2
|
||||
left_part = img.crop((0, 0, center, h))
|
||||
right_part = img.crop((center, 0, w, h))
|
||||
out_left, out_right = io.BytesIO(), io.BytesIO()
|
||||
left_part.save(out_left, "JPEG", quality=100)
|
||||
right_part.save(out_right, "JPEG", quality=100)
|
||||
out_left.name, out_right.name = "left.jpg", "right.jpg"
|
||||
out_left.seek(0)
|
||||
out_right.seek(0)
|
||||
|
||||
await message.client.send_file(
|
||||
message.chat_id,
|
||||
[out_left, out_right],
|
||||
reply_to=reply.id
|
||||
)
|
||||
|
||||
await processing_msg.delete()
|
||||
except Exception:
|
||||
await utils.answer(processing_msg, self.strings["error"])
|
||||
@@ -1,124 +0,0 @@
|
||||
# meta developer: @H_SunMods
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/7103b4ca-5fb1-4512-8a70-e720780c29c8.jpg
|
||||
# current ver
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
import logging
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class spotifyph(loader.Module):
|
||||
"""Progress bar current track in spotify"""
|
||||
|
||||
strings = {
|
||||
"name": "spotify_ph",
|
||||
"start_duration": "<tg-emoji emoji-id=5386793861483894694>🎶</tg-emoji><tg-emoji emoji-id=5384507693341906264>🎶</tg-emoji>",
|
||||
"start_full_duration": "<tg-emoji emoji-id=5386793861483894694>🎶</tg-emoji><tg-emoji emoji-id=5384401375721463792>🎶</tg-emoji>",
|
||||
"mid_duration": "<tg-emoji emoji-id=5386623003389888935>🎶</tg-emoji>",
|
||||
"empty_mid": "<tg-emoji emoji-id=5384267235302870604>🎶</tg-emoji>",
|
||||
"end_duration": "<tg-emoji emoji-id=5386826786703186322>🎶</tg-emoji>",
|
||||
"end_duration_full": "<tg-emoji emoji-id=5386623003389888935>🎶</tg-emoji>",
|
||||
"empty_end": "<tg-emoji emoji-id=5386826786703186322>🎶</tg-emoji>",
|
||||
"no_prem_start_duration": "ᵔᴥᵔ [---",
|
||||
"no_prem_start_full_duration": "ᵔᴥᵔ [~~~",
|
||||
"no_prem_mid_duration": "~~~",
|
||||
"no_prem_empty_mid": "---",
|
||||
"no_prem_end_duration_full": "~~~]",
|
||||
"no_prem_empty_end": "---]",
|
||||
"not_installed": "<b>SpotifyMod is not installed</b>",
|
||||
"nothing_plays": "<b>Nothing plays</b>",
|
||||
"sp_duration_desc": "Progress bar",
|
||||
"sp_track_desc": "Artist and song",
|
||||
"err": "<b>Error`</b>"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Прогресс бар играющего трека в спотифай",
|
||||
"not_installed": "<b>SpotifyMod не установлен</b>",
|
||||
"nothing_plays": "<b>Ничего не играет</b>",
|
||||
"sp_duration_desc": "Прогресс бар",
|
||||
"sp_track_desc": "Автор и песня",
|
||||
"err": "<b>Еррорь</b>"
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
utils.register_placeholder("sp_duration", self.sp_duration, self.strings("sp_duration_desc"))
|
||||
utils.register_placeholder("sp_track", self.get_sp_track, self.strings("sp_track_desc"))
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"show_text_time",
|
||||
True,
|
||||
"show text time",
|
||||
validator=loader.validators.Boolean(),
|
||||
)
|
||||
)
|
||||
|
||||
async def get_sp_track(self):
|
||||
try:
|
||||
s = self.lookup("SpotifyMod")
|
||||
if not s or not s.sp:
|
||||
return self.strings("not_installed")
|
||||
|
||||
p = s.sp.current_user_playing_track()
|
||||
if not (p and p.get('item')):
|
||||
return self.strings("nothing_plays")
|
||||
|
||||
artist = p['item']['artists'][0]['name']
|
||||
track_name = p['item']['name']
|
||||
return utils.escape_html(f"{artist} — {track_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in sp_track: {e}")
|
||||
return self.strings("err")
|
||||
|
||||
async def sp_duration(self):
|
||||
s = self.lookup("SpotifyMod")
|
||||
if not s or not s.sp:
|
||||
return self.strings("not_installed")
|
||||
|
||||
playback = s.sp.current_playback()
|
||||
if not playback or not playback.get("item"):
|
||||
return self.strings("nothing_plays")
|
||||
|
||||
prog_ms = playback.get("progress_ms", 0)
|
||||
dur_ms = playback["item"].get("duration_ms", 0)
|
||||
|
||||
if dur_ms == 0:
|
||||
return "00:00 / 00:00"
|
||||
|
||||
percent = (prog_ms / dur_ms) * 100
|
||||
filled_units = int(percent // 16.66)
|
||||
|
||||
# Логика для нищих
|
||||
user = getattr(self._client, "heroku_me", getattr(self._client, "me", None))
|
||||
is_premium = getattr(user, "premium", False)
|
||||
pref = "" if is_premium else "no_prem_"
|
||||
|
||||
if filled_units >= 1:
|
||||
start_pos = f"{pref}start_full_duration"
|
||||
else:
|
||||
start_pos = f"{pref}start_duration"
|
||||
bar = self.strings(start_pos)
|
||||
|
||||
for i in range(2, 6):
|
||||
if filled_units >= i:
|
||||
mid_pos = f"{pref}mid_duration"
|
||||
else:
|
||||
mid_pos = f"{pref}empty_mid"
|
||||
bar += self.strings(mid_pos)
|
||||
|
||||
if filled_units >= 6:
|
||||
end_key = f"{pref}end_duration_full"
|
||||
else:
|
||||
end_key = f"{pref}empty_end"
|
||||
bar += self.strings(end_key)
|
||||
|
||||
if self.config["show_text_time"]:
|
||||
prog_t = f"{prog_ms//60000:02}:{(prog_ms//1000)%60:02}"
|
||||
dur_t = f"{dur_ms//60000:02}:{(dur_ms//1000)%60:02}"
|
||||
return f"{bar} <code>{prog_t} / {dur_t}</code>"
|
||||
return bar
|
||||