Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ RUN apt update \
libmpv2 \
p7zip \
pulseaudio \
curl \
unzip \
&& apt autoclean \
&& apt clean \
&& rm -rf /var/lib/apt/list
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deno.land/x/install/install.sh | sh \
&& cp $HOME/.deno/bin/deno /usr/local/bin/deno
RUN useradd -ms /bin/bash ttbot
USER ttbot
WORKDIR /home/ttbot
COPY --chown=ttbot requirements.txt .
RUN pip install -r requirements.txt
COPY --chown=ttbot . .
RUN python tools/ttsdk_downloader.py && python tools/compile_locales.py
CMD pulseaudio --start && ./TTMediaBot.sh -c data/config.json --cache data/TTMediaBotCache.dat --log data/TTMediaBot.log
RUN chmod +x /home/ttbot/TTMediaBot.sh /home/ttbot/docker-entrypoint.sh
RUN mkdir -p /home/ttbot/data
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["-c", "data/config.json", "--cache", "data/TTMediaBotCache.dat", "--log", "data/TTMediaBot.log"]
2 changes: 1 addition & 1 deletion TTMediaBot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ PROGNAME=TTMediaBot.py
PROGDIR=$(dirname "$(readlink -f $0)")
LD_LIBRARY_PATH=$PROGDIR/TeamTalk_DLL:$PROGDIR:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
python3 "$PROGDIR/$PROGNAME" "$@"
exec python "$PROGDIR/$PROGNAME" "$@"
7 changes: 7 additions & 0 deletions bot/TeamTalk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def __init__(self, bot: Bot) -> None:
self.reconnect = False
self.reconnect_attempt = 0
self.user_account: UserAccount
self._joined_channel = False # Flag para saber se entrou no canal
self._ready_event = None # Will be set after joining channel

def initialize(self) -> None:
logging.debug("Initializing TeamTalk")
Expand Down Expand Up @@ -139,8 +141,11 @@ def join(self) -> None:
else:
channel_id = self.tt.getChannelIDFromPath(_str(self.config.channel))
if channel_id == 0:
logging.warning(f"Channel '{self.config.channel}' not found, falling back to channel 1")
channel_id = 1
logging.info(f"Joining channel ID: {channel_id}")
self.tt.doJoinChannelByID(channel_id, _str(self.config.channel_password))
self._joined_channel = False # Será definido como True no evento CMD_JOINED_CHANNEL

@property
def default_status(self) -> str:
Expand Down Expand Up @@ -192,6 +197,8 @@ def join_channel(self, channel: Union[str, int], password: str) -> int:
if channel_id == 0:
raise ValueError()
return self.tt.doJoinChannelByID(channel_id, _str(password))
def DoMoveUser(self, channel: int, usuarioid: int)->None:
self.tt.doMoveUser(channel, usuarioid)

def change_nickname(self, nickname: str) -> None:
self.tt.doChangeNickname(_str(nickname))
Expand Down
20 changes: 15 additions & 5 deletions bot/TeamTalk/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def run(self) -> None:
if self.config.event_handling.load_event_handlers:
self.event_handlers = self.import_event_handlers()
self._close = False
logging.info("TeamTalk thread started")
while not self._close:
event = self.ttclient.get_event(self.ttclient.tt.getMessage())
if event.event_type == EventType.NONE:
Expand All @@ -38,6 +39,7 @@ def run(self) -> None:
event.event_type == EventType.ERROR
and self.ttclient.state == State.CONNECTED
):
logging.warning(f"TeamTalk error: {event.error}")
self.ttclient.errors_queue.put(event.error)
elif (
event.event_type == EventType.SUCCESS
Expand Down Expand Up @@ -74,45 +76,50 @@ def run(self) -> None:
or self.config.reconnection_attempts < 0
):
self.ttclient.disconnect()
logging.info(f"Reconnecting in {self.config.reconnection_timeout} seconds...")
time.sleep(self.config.reconnection_timeout)
self.ttclient.connect()
self.ttclient.reconnect_attempt += 1
else:
logging.error("Connection error")
logging.error("Connection error - exiting")
sys.exit(1)
elif event.event_type == EventType.CON_SUCCESS:
self.ttclient.reconnect_attempt = 0
logging.info("Connection successful, logging in...")
self.ttclient.login()
elif event.event_type == EventType.ERROR:
if self.ttclient.flags & Flags.AUTHORIZED == Flags(0):
logging.warning("Login failed")
logging.warning(f"Login failed: {event.error}")
if (
self.ttclient.reconnect
and self.ttclient.reconnect_attempt
< self.config.reconnection_attempts
or self.config.reconnection_attempts < 0
):
logging.info(f"Retrying login in {self.config.reconnection_timeout} seconds...")
time.sleep(self.config.reconnection_timeout)
self.ttclient.login()
else:
logging.error("Login error")
logging.error("Login error - exiting")
sys.exit(1)
else:
logging.warning("Failed to join channel")
logging.warning(f"Failed to join channel: {event.error}")
if (
self.ttclient.reconnect
and self.ttclient.reconnect_attempt
< self.config.reconnection_attempts
or self.config.reconnection_attempts < 0
):
logging.info(f"Retrying to join channel in {self.config.reconnection_timeout} seconds...")
time.sleep(self.config.reconnection_timeout)
self.ttclient.join()
else:
logging.error("Error joining channel")
logging.error("Error joining channel - exiting")
sys.exit(1)
elif event.event_type == EventType.MYSELF_LOGGEDIN:
self.ttclient.user_account = event.user_account
self.ttclient.reconnect_attempt = 0
logging.info("Logged in successfully, joining channel...")
self.ttclient.join()
elif (
event.event_type == EventType.SUCCESS
Expand All @@ -121,7 +128,10 @@ def run(self) -> None:
self.ttclient.reconnect_attempt = 0
self.ttclient.reconnect = True
self.ttclient.state = State.CONNECTED
self.ttclient._joined_channel = True # Marcou como Connected = está no canal
self.ttclient.change_status_text(self.ttclient.status)
current_channel = self.ttclient.channel
logging.info(f"Connected to server and joined channel: {current_channel.name} (ID: {current_channel.id})")
if self.config.event_handling.load_event_handlers:
self.run_event_handler(event)

Expand Down
40 changes: 36 additions & 4 deletions bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
app_vars,
)

from bot.player.enums import State
from bot.player.enums import Mode

class Bot:
def __init__(
Expand Down Expand Up @@ -90,19 +92,49 @@ def run(self):
self.tt_player_connector.start()
self.command_processor.run()
logging.info("Started")

# Esperar estar conectado E no canal antes de processar comandos de startup
logging.info("Waiting to join channel...")
max_wait = 30 # segundos máximos de espera
waited = 0
while not self.ttclient._joined_channel and waited < max_wait:
time.sleep(1)
waited += 1
if waited % 5 == 0:
logging.debug(f"Still waiting to join channel... ({waited}/{max_wait}s)")

if not self.ttclient._joined_channel:
logging.error("Timed out waiting to join channel!")
else:
logging.info(f"Successfully joined channel after {waited} seconds")

logging.info(f"Processing {len(self.config.general.start_commands)} startup command(s)...")
startup_context_user = User(
id=-1, nickname="Startup", username="",
channel=self.ttclient.channel,
type=UserType.Admin, is_admin=True,
status="", gender=UserStatusMode.N, state=UserState.Null,
id=-1, nickname="Startup", username="",
channel=self.ttclient.channel,
type=UserType.Admin, is_admin=True,
status="", gender=UserStatusMode.N, state=UserState.Null,
client_name="", version=0, user_account=None, is_banned=False
)
for command in self.config.general.start_commands:
message = Message(text=command, user=startup_context_user, channel=self.ttclient.channel, type=MessageType.User)
self.command_processor(message)
if (
self.player.mode == Mode.Queue
and self.cache.queue
and self.player.state == State.Stopped
):
try:
self.player.play_queue()
except (errors.NothingIsPlayingError, errors.NoNextTrackError):
pass
self._close = False
while not self._close:
root_channel_id = self.config.general.root_channel_id
if self.config.general.back_to_root_channel and len(self.ttclient.tt.getChannelUsers(self.ttclient.channel.id))==1 and self.ttclient.channel.id != root_channel_id:
if self.player.state != State.Stopped:
self.player.stop()
self.ttclient.DoMoveUser(self.ttclient.user.id, root_channel_id)
try:
message = self.ttclient.message_queue.get_nowait()
logging.info(
Expand Down
2 changes: 1 addition & 1 deletion bot/app_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""
)
fallback_service = "yt"
loop_timeout = 0.01
loop_timeout = 0.1
max_message_length = 256
recents_max_lenth = 32
tt_event_timeout = 2
Expand Down
118 changes: 92 additions & 26 deletions bot/cache.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,140 @@
from __future__ import annotations

import json
import os
import pickle
from collections import deque
from typing import Any, Dict, List, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, List

from bot import app_vars
from bot.migrators import cache_migrator

import portalocker

if TYPE_CHECKING:
from bot.player.track import Track


cache_data_type = Dict[str, Any]
MAX_QUEUE_SIZE = 1000


class Cache:
def __init__(self, cache_data: cache_data_type):
self.cache_version = cache_data["cache_version"] if "cache_version" in cache_data else CacheManager.version
# Corrigir bug: garantir que recents seja sempre deque com maxlen
self.recents: deque[Track] = (
cache_data["recents"]
deque(cache_data["recents"], maxlen=app_vars.recents_max_lenth)
if "recents" in cache_data
else deque(maxlen=app_vars.recents_max_lenth)
)
self.favorites: Dict[str, List[Track]] = (
cache_data["favorites"] if "favorites" in cache_data else {}
)
# Limitar queue ao carregar
self.queue: List[Track] = (
cache_data["queue"][:MAX_QUEUE_SIZE] if "queue" in cache_data else []
)

def add_to_queue(self, track: "Track") -> None:
"""Adiciona track à queue respeitando o limite MAX_QUEUE_SIZE."""
self.queue.append(track)
if len(self.queue) > MAX_QUEUE_SIZE:
self.queue.pop(0) # Remove o mais antigo (FIFO)

def extend_queue(self, tracks: List["Track"]) -> None:
"""Adiciona múltiplos tracks à queue respeitando o limite."""
self.queue.extend(tracks)
if len(self.queue) > MAX_QUEUE_SIZE:
self.queue = self.queue[-MAX_QUEUE_SIZE:] # Mantém apenas os mais recentes

@property
def data(self):
return {"cache_version": self.cache_version, "recents": self.recents, "favorites": self.favorites}
return {
"cache_version": self.cache_version,
"recents": self.recents,
"favorites": self.favorites,
"queue": self.queue,
}


class CacheManager:
version = 1
version = 4

def __init__(self, file_name: str) -> None:
self.file_name = file_name
self.original_file_name = os.path.abspath(file_name)
self._prepare_paths(file_name)
self._ensure_cache_dir()
try:
self.data = cache_migrator.migrate(self, self._load())
self.cache = Cache(self.data)
data = cache_migrator.migrate(self, self._load())
self.cache = Cache(data)
except FileNotFoundError:
self.cache = Cache({})
self._dump(self.cache.data)
self._lock()
else:
self._dump(self.cache.data)

def _prepare_paths(self, file_name: str) -> None:
abs_path = os.path.abspath(file_name)
if os.path.isdir(abs_path):
self.cache_dir = abs_path
else:
base_dir = os.path.splitext(abs_path)[0]
self.cache_dir = base_dir
self.recents_file = os.path.join(self.cache_dir, "recents.dat")
self.favorites_file = os.path.join(self.cache_dir, "favorites.dat")
self.queue_file = os.path.join(self.cache_dir, "queue.dat")
self.meta_file = os.path.join(self.cache_dir, "meta.json")

def _dump(self, data: cache_data_type):
with open(self.file_name, "wb") as f:
pickle.dump(data, f)
os.makedirs(self.cache_dir, exist_ok=True)
with open(self.recents_file, "wb") as f:
pickle.dump(data.get("recents", deque(maxlen=app_vars.recents_max_lenth)), f)
with open(self.favorites_file, "wb") as f:
pickle.dump(data.get("favorites", {}), f)
with open(self.queue_file, "wb") as f:
pickle.dump(data.get("queue", []), f)
with open(self.meta_file, "w", encoding="utf-8") as f:
json.dump(
{
"cache_version": data.get("cache_version", self.version),
},
f,
)

def _load(self) -> cache_data_type:
with open(self.file_name, "rb") as f:
try:
return self._load_split()
except FileNotFoundError:
return self._load_single()

def _load_split(self) -> cache_data_type:
with open(self.meta_file, "r", encoding="utf-8") as f:
try:
meta = json.load(f)
except Exception:
f.seek(0)
meta = pickle.load(f)
with open(self.recents_file, "rb") as f:
recents = pickle.load(f)
with open(self.favorites_file, "rb") as f:
favorites = pickle.load(f)
with open(self.queue_file, "rb") as f:
queue = pickle.load(f)
return {
"cache_version": meta.get("cache_version", self.version),
"recents": recents,
"favorites": favorites,
"queue": queue,
}

def _load_single(self) -> cache_data_type:
with open(self.original_file_name, "rb") as f:
return pickle.load(f)

def _lock(self):
self.file_locker = portalocker.Lock(
self.file_name,
timeout=0,
flags=portalocker.LOCK_EX | portalocker.LOCK_NB,
)
try:
self.file_locker.acquire()
except portalocker.exceptions.LockException:
raise PermissionError()
def _ensure_cache_dir(self):
os.makedirs(self.cache_dir, exist_ok=True)

def close(self):
self.file_locker.release()
pass

def save(self):
self.file_locker.release()
self._dump(self.cache.data)
self.file_locker.acquire()
Loading