mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 14:34:17 +02:00
Added and updated repositories 2026-03-23 01:30:41
This commit is contained in:
@@ -24,3 +24,4 @@ deviceinfo
|
||||
mpi
|
||||
aigenuser
|
||||
github
|
||||
stream
|
||||
222
fiksofficial/python-modules/libs/pyupdater.py
Normal file
222
fiksofficial/python-modules/libs/pyupdater.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# ______ ___ ___ _ _
|
||||
# ____ | ___ \ | \/ | | | | |
|
||||
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
|
||||
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
|
||||
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
|
||||
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
|
||||
# \____/ __/ |
|
||||
# |___/
|
||||
|
||||
# На модуль распространяется лицензия "GNU General Public License v3.0"
|
||||
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
|
||||
|
||||
# meta developer: @pymodule
|
||||
# requires: aiohttp
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from .. import loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BLOB_RE = re.compile(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)")
|
||||
RAW_RE = re.compile(r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)")
|
||||
|
||||
API_BASE = "https://37a5bcc11453.vps.myjino.ru"
|
||||
BOT_USERNAME = "pyupdater_bot"
|
||||
GITHUB_TOKEN = ""
|
||||
|
||||
TOKEN_DB_KEY = "user_token"
|
||||
TOKEN_PREFIX = "pyut_"
|
||||
|
||||
|
||||
def _parse(url: str):
|
||||
for pat in (BLOB_RE, RAW_RE):
|
||||
m = pat.match(url.strip())
|
||||
if m:
|
||||
return m.groups()
|
||||
return None
|
||||
|
||||
|
||||
def _gh_headers() -> dict:
|
||||
h = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
|
||||
if GITHUB_TOKEN:
|
||||
h["Authorization"] = f"Bearer {GITHUB_TOKEN}"
|
||||
return h
|
||||
|
||||
|
||||
def _sign(secret_key: str, api_key: str) -> str:
|
||||
bucket = (int(time.time()) // 60) * 60
|
||||
msg = f"{api_key}:{bucket}".encode()
|
||||
return hmac.new(secret_key.encode(), msg, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
class PyUpdaterLib(loader.Library):
|
||||
developer = "@pyupdater"
|
||||
version = (1, 0, 0)
|
||||
|
||||
async def init(self):
|
||||
await self._refresh_token()
|
||||
|
||||
async def _refresh_token(self):
|
||||
"""Каждый раз при загрузке библиотеки запрашивает свежий токен у бота."""
|
||||
try:
|
||||
sent = await self.client.send_message(BOT_USERNAME, "/token")
|
||||
|
||||
response = None
|
||||
for _ in range(20):
|
||||
await asyncio.sleep(0.5)
|
||||
msgs = await self.client.get_messages(BOT_USERNAME, limit=1)
|
||||
if (
|
||||
msgs
|
||||
and msgs[0].id != sent.id
|
||||
and msgs[0].text
|
||||
and msgs[0].text.startswith(TOKEN_PREFIX)
|
||||
):
|
||||
response = msgs[0]
|
||||
break
|
||||
|
||||
if response is None:
|
||||
logger.warning("PyUpdater: не получили токен от бота")
|
||||
return
|
||||
|
||||
self._lib_set(TOKEN_DB_KEY, response.text.strip())
|
||||
logger.info("PyUpdater: токен обновлён")
|
||||
|
||||
async def _cleanup():
|
||||
await asyncio.sleep(3)
|
||||
try:
|
||||
await sent.delete()
|
||||
await response.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
asyncio.create_task(_cleanup())
|
||||
|
||||
except Exception:
|
||||
logger.exception("PyUpdater: ошибка при получении токена")
|
||||
|
||||
async def check(
|
||||
self,
|
||||
module: loader.Module,
|
||||
github_url: str,
|
||||
api_key: str,
|
||||
secret_key: str,
|
||||
) -> None:
|
||||
parts = _parse(github_url)
|
||||
if parts is None:
|
||||
logger.warning("PyUpdater: cannot parse URL %r", github_url)
|
||||
return
|
||||
|
||||
owner, repo, branch, path = parts
|
||||
module_name = module.strings["name"]
|
||||
|
||||
asyncio.create_task(self._ping(api_key, secret_key, module.tg_id, module_name))
|
||||
asyncio.create_task(self._maybe_confirm_test(api_key, module.tg_id))
|
||||
|
||||
commits_url = (
|
||||
f"https://api.github.com/repos/{owner}/{repo}/commits"
|
||||
f"?path={path}&sha={branch}&per_page=1"
|
||||
)
|
||||
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}"
|
||||
db_key = f"pyu_sha_{owner}_{repo}_{path.replace('/', '_')}"
|
||||
saved_sha: str = module._db.get(module_name, db_key, "")
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
commits_url,
|
||||
headers=_gh_headers(),
|
||||
timeout=aiohttp.ClientTimeout(total=10),
|
||||
) as resp:
|
||||
if resp.status in (403, 429):
|
||||
logger.warning("PyUpdater: GitHub rate limit for %s", module_name)
|
||||
return
|
||||
resp.raise_for_status()
|
||||
commits = await resp.json()
|
||||
|
||||
if not commits:
|
||||
return
|
||||
|
||||
latest_sha: str = commits[0]["sha"]
|
||||
if latest_sha == saved_sha:
|
||||
return
|
||||
|
||||
async with session.get(
|
||||
raw_url,
|
||||
headers=_gh_headers(),
|
||||
timeout=aiohttp.ClientTimeout(total=20),
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
new_code = await resp.text()
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("PyUpdater: timeout for %s", module_name)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("PyUpdater: error checking %s", module_name)
|
||||
return
|
||||
|
||||
module._db.set(module_name, db_key, latest_sha)
|
||||
logger.info("PyUpdater: reloading %s (commit %s)", module_name, latest_sha[:7])
|
||||
|
||||
loader_mod = next(
|
||||
(m for m in module.allmodules.modules if m.__class__.__name__ == "LoaderMod"),
|
||||
None,
|
||||
)
|
||||
if loader_mod is None:
|
||||
logger.error("PyUpdater: LoaderMod not found")
|
||||
return
|
||||
|
||||
asyncio.create_task(loader_mod.load_module(new_code, None, save_fs=True))
|
||||
|
||||
async def _maybe_confirm_test(self, api_key: str, user_tg_id: int) -> None:
|
||||
try:
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"{API_BASE}/test/pending",
|
||||
params={"api_key": api_key},
|
||||
timeout=aiohttp.ClientTimeout(total=8),
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return
|
||||
data = await resp.json()
|
||||
if not data.get("pending"):
|
||||
return
|
||||
|
||||
await session.post(
|
||||
f"{API_BASE}/test/confirm",
|
||||
json={"api_key": api_key, "user_tg_id": user_tg_id},
|
||||
timeout=aiohttp.ClientTimeout(total=8),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _ping(self, api_key: str, secret_key: str, user_tg_id: int, module_name: str) -> None:
|
||||
user_token: str = self._lib_get(TOKEN_DB_KEY, "")
|
||||
payload = {
|
||||
"api_key": api_key,
|
||||
"signature": _sign(secret_key, api_key),
|
||||
"user_tg_id": user_tg_id,
|
||||
"module_name": module_name,
|
||||
}
|
||||
if user_token:
|
||||
payload["user_token"] = user_token
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(
|
||||
f"{API_BASE}/ping",
|
||||
json=payload,
|
||||
timeout=aiohttp.ClientTimeout(total=8),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
435
fiksofficial/python-modules/stream.py
Normal file
435
fiksofficial/python-modules/stream.py
Normal file
@@ -0,0 +1,435 @@
|
||||
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from .. import loader, utils
|
||||
from ..inline.types import InlineCall
|
||||
|
||||
def detect_type(path: str) -> str:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
if not mime:
|
||||
return "video"
|
||||
if mime.startswith("video"):
|
||||
return "video"
|
||||
if mime.startswith("audio"):
|
||||
return "audio"
|
||||
if mime.startswith("image"):
|
||||
return "image"
|
||||
return "video"
|
||||
|
||||
TYPE_ICON = {"video": "🎬", "audio": "🎵", "image": "🖼️"}
|
||||
PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"]
|
||||
TUNES = ["zerolatency", "film", "animation", "grain", "stillimage", "fastdecode"]
|
||||
SCALES = ["off", "426x240", "640x360", "854x480", "1280x720", "1920x1080", "2560x1440"]
|
||||
FPS_OPT = [24, 25, 30, 48, 60]
|
||||
|
||||
def build_cmd(file_path: str, rtmp_url: str, cfg: dict) -> list:
|
||||
preset = cfg.get("preset", "veryfast")
|
||||
tune = cfg.get("tune", "zerolatency")
|
||||
vbr = cfg.get("vbitrate", "2000k")
|
||||
abr = cfg.get("abitrate", "128k")
|
||||
fps = str(cfg.get("fps", 30))
|
||||
res = cfg.get("resolution", None)
|
||||
threads = str(cfg.get("threads", 0))
|
||||
gop = str(int(fps) * 2)
|
||||
bufsize = str(int(vbr.replace("k", "")) * 2) + "k"
|
||||
ftype = detect_type(file_path)
|
||||
|
||||
base = ["ffmpeg", "-re", "-stream_loop", "-1", "-threads", threads]
|
||||
vf_scale = f",scale={res}" if res else ""
|
||||
common_v = [
|
||||
"-c:v", "libx264", "-preset", preset, "-tune", tune,
|
||||
"-pix_fmt", "yuv420p", "-profile:v", "baseline",
|
||||
"-r", fps, "-g", gop, "-keyint_min", gop, "-sc_threshold", "0",
|
||||
"-b:v", vbr, "-maxrate", vbr, "-bufsize", bufsize,
|
||||
]
|
||||
common_a = ["-c:a", "aac", "-b:a", abr, "-ar", "44100"]
|
||||
out = ["-f", "flv", rtmp_url]
|
||||
|
||||
if ftype == "video":
|
||||
vf = ["-vf", f"scale=trunc(iw/2)*2:trunc(ih/2)*2{vf_scale}"] if res else []
|
||||
return base + ["-i", file_path] + common_v + vf + common_a + out
|
||||
if ftype == "audio":
|
||||
size = res or "1280x720"
|
||||
return (
|
||||
base
|
||||
+ ["-i", file_path, "-f", "lavfi", "-i", f"color=c=black:s={size}:r={fps}"]
|
||||
+ ["-shortest"] + common_v + common_a
|
||||
+ ["-map", "1:v:0", "-map", "0:a:0"] + out
|
||||
)
|
||||
if ftype == "image":
|
||||
scale_vf = f"scale=trunc(iw/2)*2:trunc(ih/2)*2{vf_scale}"
|
||||
return (
|
||||
base
|
||||
+ ["-loop", "1", "-i", file_path, "-f", "lavfi", "-i", "anullsrc=r=44100:cl=stereo"]
|
||||
+ ["-vf", scale_vf] + common_v
|
||||
+ ["-shortest"] + common_a
|
||||
+ ["-map", "0:v:0", "-map", "1:a:0"] + out
|
||||
)
|
||||
raise ValueError(f"Unsupported: {ftype}")
|
||||
|
||||
@loader.tds
|
||||
class StreamMod(loader.Module):
|
||||
"""📡 RTMP media streaming"""
|
||||
strings = {
|
||||
"name": "Stream",
|
||||
"status_active": "▶️ <b>Stream is live</b>\n\n{icon} <code>{file}</code>\n⏱ Time: <b>{elapsed}</b>\n🔢 PID: <code>{pid}</code>\n📡 <code>{rtmp}</code>\n🎥 <b>{vbr}</b> | <b>{fps}fps</b> | <b>{preset}</b>\n🔊 <b>{abr}</b>\n📋 Queue: <b>{queue}</b>",
|
||||
"status_idle": "⏸ <b>Stream is not active</b>",
|
||||
"status_queue": "\n📋 Queue: <b>{n}</b>",
|
||||
"stopped": "⏹ <b>Stream stopped.</b>",
|
||||
"no_rtmp": "❌ <b>RTMP not configured!</b>\nTap a button to set it up.",
|
||||
"downloading": "⏳ Downloading…",
|
||||
"dl_failed": "❌ Failed to download file.",
|
||||
"queued": "📋 Added to queue ({n})\n{icon} <code>{file}</code>",
|
||||
"not_running": "Not running",
|
||||
"queue_empty": "Queue is empty",
|
||||
"queue_header": "📋 Queue:\n",
|
||||
"settings_title": "⚙️ <b>Stream settings</b>",
|
||||
"btn_stop": "⏹ Stop",
|
||||
"btn_queue": "📋 Queue",
|
||||
"btn_refresh": "🔄 Refresh",
|
||||
"btn_settings": "⚙️ Settings",
|
||||
"btn_status": "📊 Status",
|
||||
"btn_back": "🔙 Back",
|
||||
"btn_preset": "🎞 Preset: {v}",
|
||||
"btn_tune": "🎭 Tune: {v}",
|
||||
"btn_vbr": "🎥 Video: {v}",
|
||||
"btn_abr": "🔊 Audio: {v}",
|
||||
"btn_fps": "📐 FPS: {v}",
|
||||
"btn_res": "🖥 Res: {v}",
|
||||
"btn_threads": "🧵 Threads: {v}",
|
||||
"btn_rtmps": "📡 RTMP URL",
|
||||
"btn_key": "🔑 Stream key",
|
||||
"btn_set_rtmps": "📡 Set RTMP URL",
|
||||
"btn_set_key": "🔑 Set stream key",
|
||||
"ph_vbr": "Video bitrate, e.g. 2000k",
|
||||
"ph_abr": "Audio bitrate, e.g. 128k",
|
||||
"ph_threads": "Thread count (0 = auto)",
|
||||
"ph_rtmps": "rtmp://a.rtmp.youtube.com/live2",
|
||||
"ph_key": "Stream key...",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "📡 RTMP стриминг медиафайлов",
|
||||
"status_active": "▶️ <b>Трансляция идёт</b>\n\n{icon} <code>{file}</code>\n⏱ Время: <b>{elapsed}</b>\n🔢 PID: <code>{pid}</code>\n📡 <code>{rtmp}</code>\n🎥 <b>{vbr}</b> | <b>{fps}fps</b> | <b>{preset}</b>\n🔊 <b>{abr}</b>\n📋 В очереди: <b>{queue}</b>",
|
||||
"status_idle": "⏸ <b>Трансляция не активна</b>",
|
||||
"status_queue": "\n📋 В очереди: <b>{n}</b>",
|
||||
"stopped": "⏹ <b>Трансляция остановлена.</b>",
|
||||
"no_rtmp": "❌ <b>RTMP не настроен!</b>\nНажми кнопку чтобы задать прямо сейчас.",
|
||||
"downloading": "⏳ Скачиваю…",
|
||||
"dl_failed": "❌ Не удалось скачать файл.",
|
||||
"queued": "📋 Добавлено в очередь ({n} шт.)\n{icon} <code>{file}</code>",
|
||||
"not_running": "Не запущено",
|
||||
"queue_empty": "Очередь пуста",
|
||||
"queue_header": "📋 Очередь:\n",
|
||||
"settings_title": "⚙️ <b>Настройки трансляции</b>",
|
||||
"btn_stop": "⏹ Стоп",
|
||||
"btn_queue": "📋 Очередь",
|
||||
"btn_refresh": "🔄 Обновить",
|
||||
"btn_settings": "⚙️ Настройки",
|
||||
"btn_status": "📊 Статус",
|
||||
"btn_back": "🔙 Назад",
|
||||
"btn_preset": "🎞 Пресет: {v}",
|
||||
"btn_tune": "🎭 Tune: {v}",
|
||||
"btn_vbr": "🎥 Видео: {v}",
|
||||
"btn_abr": "🔊 Аудио: {v}",
|
||||
"btn_fps": "📐 FPS: {v}",
|
||||
"btn_res": "🖥 Разр: {v}",
|
||||
"btn_threads": "🧵 Треды: {v}",
|
||||
"btn_rtmps": "📡 RTMP URL",
|
||||
"btn_key": "🔑 Ключ",
|
||||
"btn_set_rtmps": "📡 Задать RTMP URL",
|
||||
"btn_set_key": "🔑 Задать ключ",
|
||||
"ph_vbr": "Битрейт видео, напр. 2000k",
|
||||
"ph_abr": "Битрейт аудио, напр. 128k",
|
||||
"ph_threads": "Потоков (0 = авто)",
|
||||
"ph_rtmps": "rtmp://a.rtmp.youtube.com/live2",
|
||||
"ph_key": "Ключ трансляции...",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._proc: subprocess.Popen | None = None
|
||||
self._file: str | None = None
|
||||
self._started: float | None = None
|
||||
self._queue: list[str] = []
|
||||
self._qtask: asyncio.Task | None = None
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue("rtmps", "", "Base RTMP URL (rtmp://...)"),
|
||||
loader.ConfigValue("key", "", "Stream key"),
|
||||
loader.ConfigValue("preset", "veryfast", "x264 preset",
|
||||
validator=loader.validators.Choice(PRESETS)),
|
||||
loader.ConfigValue("tune", "zerolatency","x264 tune",
|
||||
validator=loader.validators.Choice(TUNES)),
|
||||
loader.ConfigValue("vbitrate", "2000k", "Video bitrate (e.g. 1500k, 3000k)"),
|
||||
loader.ConfigValue("abitrate", "128k", "Audio bitrate (e.g. 64k, 192k)"),
|
||||
loader.ConfigValue("fps", 30, "Frames per second",
|
||||
validator=loader.validators.Integer(minimum=1, maximum=120)),
|
||||
loader.ConfigValue("resolution", "", "Output resolution (e.g. 1280x720, empty = no scaling)"),
|
||||
loader.ConfigValue("threads", 0, "FFmpeg thread count (0 = auto)",
|
||||
validator=loader.validators.Integer(minimum=0, maximum=64)),
|
||||
loader.ConfigValue("loop", True, "Loop the file indefinitely",
|
||||
validator=loader.validators.Boolean()),
|
||||
loader.ConfigValue("reconnect", True, "Auto-restart on stream disconnect",
|
||||
validator=loader.validators.Boolean()),
|
||||
)
|
||||
|
||||
def _s(self, key: str, **kw) -> str:
|
||||
return self.strings[key].format(**kw) if kw else self.strings[key]
|
||||
|
||||
def _running(self) -> bool:
|
||||
return self._proc is not None and self._proc.poll() is None
|
||||
|
||||
def _stop(self):
|
||||
if self._proc:
|
||||
try:
|
||||
self._proc.terminate()
|
||||
self._proc.wait(timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
self._proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self._proc = None
|
||||
if self._file and os.path.exists(self._file):
|
||||
try:
|
||||
os.remove(self._file)
|
||||
except Exception:
|
||||
pass
|
||||
self._file = None
|
||||
self._started = None
|
||||
|
||||
def _launch(self, path: str):
|
||||
cfg = {k: self.config[k] for k in ("preset", "tune", "vbitrate", "abitrate", "fps", "threads")}
|
||||
cfg["resolution"] = self.config["resolution"] or None
|
||||
rtmp = f"{self.config['rtmps'].rstrip('/')}/{self.config['key']}"
|
||||
self._proc = subprocess.Popen(build_cmd(path, rtmp, cfg), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self._file = path
|
||||
self._started = time.time()
|
||||
|
||||
def _elapsed(self) -> str:
|
||||
if not self._started:
|
||||
return "00:00:00"
|
||||
e = int(time.time() - self._started)
|
||||
return f"{e//3600:02d}:{(e%3600)//60:02d}:{e%60:02d}"
|
||||
|
||||
def _status_text(self) -> str:
|
||||
if not self._running():
|
||||
txt = self._s("status_idle")
|
||||
if self._queue:
|
||||
txt += self._s("status_queue", n=len(self._queue))
|
||||
return txt
|
||||
ftype = detect_type(self._file or "")
|
||||
rtmp = f"{self.config['rtmps'].rstrip('/')}/{self.config['key'][:4]}***"
|
||||
return self._s(
|
||||
"status_active",
|
||||
icon=TYPE_ICON.get(ftype, "📄"),
|
||||
file=os.path.basename(self._file or "?"),
|
||||
elapsed=self._elapsed(),
|
||||
pid=self._proc.pid if self._proc else "—",
|
||||
rtmp=rtmp,
|
||||
vbr=self.config["vbitrate"],
|
||||
fps=self.config["fps"],
|
||||
preset=self.config["preset"],
|
||||
abr=self.config["abitrate"],
|
||||
queue=len(self._queue),
|
||||
)
|
||||
|
||||
def _res_label(self) -> str:
|
||||
r = self.config["resolution"]
|
||||
return r if r else "auto"
|
||||
|
||||
def _thr_label(self) -> str:
|
||||
t = self.config["threads"]
|
||||
return str(t) if t else "auto"
|
||||
|
||||
def _main_markup(self) -> list:
|
||||
running = self._running()
|
||||
return [
|
||||
[
|
||||
{"text": self._s("btn_stop"), "callback": self._cb_stop} if running
|
||||
else {"text": self._s("btn_queue"), "callback": self._cb_queue},
|
||||
{"text": self._s("btn_refresh"), "callback": self._cb_refresh},
|
||||
],
|
||||
[
|
||||
{"text": self._s("btn_settings"), "callback": self._cb_settings},
|
||||
{"text": self._s("btn_status"), "callback": self._cb_status},
|
||||
],
|
||||
]
|
||||
|
||||
def _settings_markup(self) -> list:
|
||||
return [
|
||||
[
|
||||
{"text": self._s("btn_preset", v=self.config["preset"]), "callback": self._cb_set_preset},
|
||||
{"text": self._s("btn_tune", v=self.config["tune"]), "callback": self._cb_set_tune},
|
||||
],
|
||||
[
|
||||
{"text": self._s("btn_vbr", v=self.config["vbitrate"]),
|
||||
"input": self._s("ph_vbr"), "handler": self._ih_vbr},
|
||||
{"text": self._s("btn_abr", v=self.config["abitrate"]),
|
||||
"input": self._s("ph_abr"), "handler": self._ih_abr},
|
||||
],
|
||||
[
|
||||
{"text": self._s("btn_fps", v=self.config["fps"]), "callback": self._cb_set_fps},
|
||||
{"text": self._s("btn_res", v=self._res_label()), "callback": self._cb_set_res},
|
||||
],
|
||||
[
|
||||
{"text": self._s("btn_threads", v=self._thr_label()),
|
||||
"input": self._s("ph_threads"), "handler": self._ih_threads},
|
||||
],
|
||||
[
|
||||
{"text": self._s("btn_rtmps"),
|
||||
"input": self._s("ph_rtmps"), "handler": self._ih_rtmps},
|
||||
{"text": self._s("btn_key"),
|
||||
"input": self._s("ph_key"), "handler": self._ih_key},
|
||||
],
|
||||
[{"text": self._s("btn_back"), "callback": self._cb_back}],
|
||||
]
|
||||
|
||||
async def _ih_vbr(self, call: InlineCall, query: str):
|
||||
q = query.strip()
|
||||
if q.endswith("k") and q[:-1].isdigit():
|
||||
self.config["vbitrate"] = q
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _ih_abr(self, call: InlineCall, query: str):
|
||||
q = query.strip()
|
||||
if q.endswith("k") and q[:-1].isdigit():
|
||||
self.config["abitrate"] = q
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _ih_threads(self, call: InlineCall, query: str):
|
||||
q = query.strip()
|
||||
if q.isdigit():
|
||||
self.config["threads"] = int(q)
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _ih_rtmps(self, call: InlineCall, query: str):
|
||||
q = query.strip()
|
||||
if q.startswith("rtmp"):
|
||||
self.config["rtmps"] = q.rstrip("/")
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _ih_key(self, call: InlineCall, query: str):
|
||||
q = query.strip()
|
||||
if q:
|
||||
self.config["key"] = q
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _cb_refresh(self, call: InlineCall):
|
||||
await call.edit(self._status_text(), reply_markup=self._main_markup())
|
||||
|
||||
async def _cb_status(self, call: InlineCall):
|
||||
await call.answer(self._elapsed() if self._running() else self._s("not_running"))
|
||||
|
||||
async def _cb_stop(self, call: InlineCall):
|
||||
self._queue.clear()
|
||||
if self._qtask:
|
||||
self._qtask.cancel()
|
||||
self._qtask = None
|
||||
self._stop()
|
||||
await call.edit(self._s("stopped"), reply_markup=self._main_markup())
|
||||
|
||||
async def _cb_queue(self, call: InlineCall):
|
||||
if not self._queue:
|
||||
await call.answer(self._s("queue_empty"), show_alert=True)
|
||||
return
|
||||
lines = [f"{i}. {TYPE_ICON.get(detect_type(f), '📄')} {os.path.basename(f)}"
|
||||
for i, f in enumerate(self._queue, 1)]
|
||||
await call.answer(self._s("queue_header") + "\n".join(lines), show_alert=True)
|
||||
|
||||
async def _cb_back(self, call: InlineCall):
|
||||
await call.edit(self._status_text(), reply_markup=self._main_markup())
|
||||
|
||||
async def _cb_settings(self, call: InlineCall):
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _cb_set_preset(self, call: InlineCall):
|
||||
cur = self.config["preset"]
|
||||
self.config["preset"] = PRESETS[(PRESETS.index(cur) + 1) % len(PRESETS)]
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _cb_set_tune(self, call: InlineCall):
|
||||
cur = self.config["tune"]
|
||||
self.config["tune"] = TUNES[(TUNES.index(cur) + 1) % len(TUNES)]
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _cb_set_fps(self, call: InlineCall):
|
||||
cur = self.config["fps"]
|
||||
self.config["fps"] = FPS_OPT[(FPS_OPT.index(cur) + 1) % len(FPS_OPT)] if cur in FPS_OPT else 30
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
async def _cb_set_res(self, call: InlineCall):
|
||||
cur = self.config["resolution"] or "off"
|
||||
idx = SCALES.index(cur) if cur in SCALES else 0
|
||||
nxt = SCALES[(idx + 1) % len(SCALES)]
|
||||
self.config["resolution"] = "" if nxt == "off" else nxt
|
||||
await call.edit(self._s("settings_title"), reply_markup=self._settings_markup())
|
||||
|
||||
@loader.command(ru_doc="[ответ на медиа] – запустить трансляцию")
|
||||
async def stream(self, message):
|
||||
"""[reply to media] — start stream or add to queue"""
|
||||
if not self.config["rtmps"] or not self.config["key"]:
|
||||
await self.inline.form(
|
||||
self._s("no_rtmp"),
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[{"text": self._s("btn_set_rtmps"), "input": self._s("ph_rtmps"), "handler": self._ih_rtmps}],
|
||||
[{"text": self._s("btn_set_key"), "input": self._s("ph_key"), "handler": self._ih_key}],
|
||||
],
|
||||
)
|
||||
return
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.media:
|
||||
await self.inline.form(
|
||||
self._status_text(),
|
||||
message=message,
|
||||
reply_markup=self._main_markup(),
|
||||
)
|
||||
return
|
||||
|
||||
status = await utils.answer(message, self._s("downloading"))
|
||||
path = await reply.download_media(file=f"/tmp/stream_{int(time.time())}")
|
||||
if not path:
|
||||
await status.edit(self._s("dl_failed"))
|
||||
return
|
||||
await status.delete()
|
||||
|
||||
if self._running():
|
||||
self._queue.append(path)
|
||||
await self.inline.form(
|
||||
self._s("queued", n=len(self._queue), icon=TYPE_ICON.get(detect_type(path), "📄"), file=os.path.basename(path)),
|
||||
message=message,
|
||||
reply_markup=self._main_markup(),
|
||||
)
|
||||
return
|
||||
|
||||
self._stop()
|
||||
self._launch(path)
|
||||
await self.inline.form(
|
||||
self._status_text(),
|
||||
message=message,
|
||||
reply_markup=self._main_markup(),
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="– панель управления трансляцией")
|
||||
async def streamctl(self, message):
|
||||
"""– open stream control panel"""
|
||||
await self.inline.form(
|
||||
self._status_text(),
|
||||
message=message,
|
||||
reply_markup=self._main_markup(),
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="– остановить трансляцию и очистить очередь")
|
||||
async def streamstop(self, message):
|
||||
"""– stop stream and clear queue"""
|
||||
self._queue.clear()
|
||||
if self._qtask:
|
||||
self._qtask.cancel()
|
||||
self._qtask = None
|
||||
self._stop()
|
||||
await utils.answer(message, self._s("stopped"))
|
||||
@@ -88,6 +88,12 @@ class SenderGifts(loader.Module):
|
||||
"gifts": [
|
||||
{"id": 5866352046986232958, "emoji": "🧸", "name": "8 Марта мишка", "price": 50},
|
||||
]
|
||||
},
|
||||
"saint_patricks_day ": {
|
||||
"name": "💰 День святого патрика",
|
||||
"gifts": [
|
||||
{"id": 5893356958802511476, "emoji": "🧸", "name": "Лепрекон мишка", "price": 50},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Banners:
|
||||
font_bytes = requests.get(self.font_url).content
|
||||
title_font = self._get_font(55, font_bytes)
|
||||
artist_font = self._get_font(45, font_bytes)
|
||||
lfm_font = self._get_font(35, font_bytes)
|
||||
lfm_font = self._get_font(55, font_bytes)
|
||||
|
||||
img = self._prepare_background(W, H)
|
||||
draw = ImageDraw.Draw(img)
|
||||
@@ -88,8 +88,8 @@ class Banners:
|
||||
draw.text((text_x, text_y_start), display_title, font=title_font, fill="white")
|
||||
draw.text((text_x, text_y_start + 70), display_artist, font=artist_font, fill="#B3B3B3")
|
||||
|
||||
bar_y = 480
|
||||
draw.text((text_x, bar_y), "last.fm", font=lfm_font, fill="white")
|
||||
text_y = 430
|
||||
draw.text((text_x, text_y), "last.fm", font=lfm_font, fill="white")
|
||||
|
||||
by = io.BytesIO()
|
||||
img.save(by, format="PNG")
|
||||
@@ -98,24 +98,24 @@ class Banners:
|
||||
return by
|
||||
|
||||
def vertical(self):
|
||||
W, H = 1000, 1500
|
||||
padding = 80
|
||||
W, H = 1000, 1300
|
||||
padding = 60
|
||||
cover_size = 800
|
||||
|
||||
font_bytes = requests.get(self.font_url).content
|
||||
title_font = self._get_font(60, font_bytes)
|
||||
artist_font = self._get_font(45, font_bytes)
|
||||
lfm_font = self._get_font(35, font_bytes)
|
||||
lfm_font = self._get_font(60, font_bytes)
|
||||
|
||||
img = self._prepare_background(W, H)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
cover = self._prepare_cover(cover_size, 40)
|
||||
cover_x = (W - cover_size) // 2
|
||||
cover_y = 120
|
||||
cover_y = 100
|
||||
img.paste(cover, (cover_x, cover_y), cover)
|
||||
|
||||
text_area_y = cover_y + cover_size + 120
|
||||
text_area_y = cover_y + cover_size + 60
|
||||
text_width_limit = W - (padding * 2)
|
||||
|
||||
display_title = self.title
|
||||
@@ -134,10 +134,10 @@ class Banners:
|
||||
artist_w = artist_font.getlength(display_artist)
|
||||
draw.text(((W - artist_w) / 2, text_area_y + 75), display_artist, font=artist_font, fill="#B3B3B3")
|
||||
|
||||
bar_y = text_area_y + 260
|
||||
text_y = text_area_y + 180
|
||||
|
||||
lfm_w = lfm_font.getlength("last.fm")
|
||||
draw.text(((W - lfm_w) / 2, bar_y), "last.fm", font=lfm_font, fill="white")
|
||||
draw.text(((W - lfm_w) / 2, text_y), "last.fm", font=lfm_font, fill="white")
|
||||
|
||||
by = io.BytesIO()
|
||||
img.save(by, format="PNG")
|
||||
@@ -165,14 +165,6 @@ class lastfmmod(loader.Module):
|
||||
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Укажите ваш никнейм с last.fm</b>",
|
||||
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера...</i>",
|
||||
}
|
||||
strings_jp = {
|
||||
"name": "LastFm",
|
||||
"no_track": "<emoji document_id=5465665476971471368>❌</emoji> <b>現在再生中のトラックはありません</b>",
|
||||
"_doc_text": "ファイルの横に表示されるテキスト",
|
||||
"_doc_username": "Last.fmのユーザー名",
|
||||
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Last.fmのニックネームを入力してください</b>",
|
||||
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>バナーをアップロード中...</i>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
@@ -180,6 +172,7 @@ class lastfmmod(loader.Module):
|
||||
loader.ConfigValue("custom_text", "<emoji document_id=5413612466208799435>🤩</emoji> <b>{song_name}</b> — <b>{song_artist}</b>", lambda: self.strings["_doc_text"]),
|
||||
loader.ConfigValue("font", "https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf", "Custom font URL (ttf)"),
|
||||
loader.ConfigValue("banner_version", "horizontal", lambda: "Banner version", validator=loader.validators.Choice(["horizontal", "vertical"])),
|
||||
loader.ConfigValue("fallback_cover", "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", "Fallback cover URL if track has no image"),
|
||||
)
|
||||
|
||||
@loader.command(alias="np")
|
||||
@@ -191,20 +184,25 @@ class lastfmmod(loader.Module):
|
||||
return await utils.answer(message, self.strings["nick_error"])
|
||||
|
||||
try:
|
||||
msg = await utils.answer(message, self.strings["uploading"])
|
||||
|
||||
url = f'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&nowplaying=true&user={user}&api_key=460cda35be2fbf4f28e8ea7a38580730&format=json'
|
||||
data = requests.get(url).json()
|
||||
track = next((t for t in data.get('recenttracks', {}).get('track', []) if t.get('@attr', {}).get('nowplaying')), None)
|
||||
if not track:
|
||||
return await utils.answer(message, self.strings["no_track"])
|
||||
return await utils.answer(msg, self.strings["no_track"])
|
||||
name = track.get('name', 'Unknown')
|
||||
artist = track.get('artist', {}).get('#text', 'Unknown')
|
||||
caption = self.config["custom_text"].format(song_artist=artist, song_name=name)
|
||||
imgs = track.get('image', [])
|
||||
cov_url = next((i['#text'] for i in imgs if i['size'] == 'extralarge'), imgs[-1]['#text'] if imgs else None)
|
||||
|
||||
if not cov_url:
|
||||
return await utils.answer(message, caption)
|
||||
msg = await utils.answer(message, self.strings["uploading"])
|
||||
if not cov_url or not str(cov_url).strip():
|
||||
cov_url = self.config["fallback_cover"]
|
||||
|
||||
if not cov_url or not str(cov_url).strip():
|
||||
return await utils.answer(msg, caption)
|
||||
|
||||
cov_bytes = await utils.run_sync(requests.get, cov_url)
|
||||
banners = Banners(name, artist, cov_bytes.content, self.config["font"])
|
||||
file = await utils.run_sync(getattr(banners, self.config["banner_version"]))
|
||||
|
||||
Reference in New Issue
Block a user