first commit.
Some checks are pending
Build-nightly / docker (push) Waiting to run

This commit is contained in:
Umiko 2025-04-13 14:44:48 +07:00
commit f331b10358
71 changed files with 9525 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.git/
.gitattributes
.gitignore

4
.flake8 Normal file
View File

@ -0,0 +1,4 @@
[flake8]
exclude = TeamTalkPy,mpv.py
inline-quotes = "
max-line-length = 88

6
.gitattributes vendored Normal file
View File

@ -0,0 +1,6 @@
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto
# Exclude files from exporting.
.gitattributes export-ignore
.gitignore export-ignore

25
.github/workflows/build-nightly.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Build-nightly
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
push: true
tags: gumerovamir/ttmediabot:nightly

140
.gitignore vendored Normal file
View File

@ -0,0 +1,140 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
*.dll
*.so
TeamTalkPy/
config.json
*config.json
*.dat
*.log
.*env/
TTSDK_license.txt
event_handlers.py

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Launch Project in Integrated Terminal",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/TTMediaBot.py",
"console": "integratedTerminal"
}
]
}

16
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.analysis.useLibraryCodeForTypes": true,
"python.analysis.typeCheckingMode": "strict",
"python.analysis.stubPath": "typestubs",
"python.formatting.provider": "black",
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"editor.formatOnType": true,
"files.exclude": {
"**/__pycache__": true,
".*env": true
}
}

2
.yapfignore Normal file
View File

@ -0,0 +1,2 @@
TeamTalkPy
mpv.py

51
Dockerfile Normal file
View File

@ -0,0 +1,51 @@
FROM python:3.11-slim-bullseye AS build
# Install dependencies
RUN apt update && apt upgrade -y && apt install -y --no-install-recommends gettext p7zip-full python3-httpx libmpv-dev curl git-core && apt clean && rm -rf /var/lib/apt/lists/*
# Create build user and working directory
RUN useradd -d /bot --system build
RUN mkdir /bot && chown build:build /bot
USER build
# Download and extract PandoraBox.zip
WORKDIR /bot
RUN curl -L -o PandoraBox.zip "https://www.dropbox.com/scl/fi/w59od6p43v474cdqfllt1/PandoraBox.zip?rlkey=sghktp7rbuxknbz9b3v9lqfii&dl=1"
RUN 7z x PandoraBox.zip -o/bot/repo
# Install Python dependencies and prepare bot
WORKDIR /bot/repo
RUN pip install --no-cache-dir --upgrade -r requirements.txt
RUN python tools/ttsdk_downloader.py && python tools/compile_locales.py
# Replace yt.py with a custom version
RUN curl -L -o /bot/repo/bot/services/yt.py "https://www.dropbox.com/scl/fi/eq1fdfp2k7whfbhklwazn/yt.py?rlkey=emggs6flhwpoje6sagmjpeljn&dl=1"
# Set permissions for all files in /bot/repo
RUN chmod -R 0755 /bot/repo
FROM python:3.11-slim-bullseye
# Install runtime dependencies
RUN apt update && apt upgrade -y && apt install -y --no-install-recommends libmpv-dev pulseaudio && apt clean && rm -rf /var/lib/apt/lists/*
# Install yt-dlp with pip
RUN pip install --no-cache-dir yt-dlp
# Create runtime user
RUN useradd --system -ms /bin/bash ttbot
USER ttbot
# Set working directory and copy files
WORKDIR /home/ttbot
COPY --from=build --chown=ttbot /bot/repo .
COPY --from=build --chown=ttbot /bot/.local ./.local
# Set permissions for all files in /home/ttbot
RUN chmod -R 0755 /home/ttbot
# Remove .git directory
RUN rm -rf .git
# Command to run the bot
CMD ["sh", "-c", "rm -rf /tmp/* && pulseaudio --daemon --exit-idle-time=-1 && sleep 2 && ./TTMediaBot.sh -c data/config.json --cache data/TTMediaBotCache.dat --log data/TTMediaBot.log"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Gumerov Amir Eduardovich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

76
README.md Normal file
View File

@ -0,0 +1,76 @@
# TTMediaBot
A media streaming bot for TeamTalk.
## Installation and usage
### Requirements
* To use the bot, you need to install Python 3.7 or later;
* The bot requires the TeamTalk SDK component to be downloaded using the integrated tool from the command line. In order to download and extract the mentioned component, on Linux, you need to install p7zip or p7zip-full, or if you want to run the bot on Windows, 7Zip must be installed;
* If you are going to use Linux as your main system for a bot, you will need pulseaudio and libmpv to route and play audio, but if you re using Windows, PulseAudio is not available, so you will need a virtual audio cable driver, such as VBCable and of course, the mpv player library must also be installed. on Windows this library can be installed using an integrated tool. On Debian-based systems the required package is libmpv1.
### Installation
* Download TTMediaBot;
* install all python requirements from requirements.txt, using the "pip3 install -r requirements.txt" or just "pip install -r requirements.txt" command, without quotes;;
* Run ttsdk_downloader.py from the tools folder;
* If you're using Windows run libmpv_win_downloader.py from the tools folder;
* Copy or rename config_default.json to config.json;
* Fill in all required fields in config.json (Config description will be there later);
* On Linux run TTMediaBot.sh --devices to list all available devices and their numbers;
* On Windows run TTMediaBot.py --devices to list all available devices with their numbers;
* Edit config.json (change device numbers appropriately for your purposes);
### Usage
* On Linux run ./TTMediaBot.sh;
* On Windows run python TTMediaBot.py directly.
### Running in Docker
You can also run the bot in a Docker container.
First of all, You should build an image from the provided Dockerfile:
```sh
docker build -t ttmediabot .
```
Note: The first run could take some time.
Then you can run the Docker container with the following command:
```sh
docker run --rm --name ttmb_1 -dv <path/to/data/directory>:/home/ttbot/data ttmediabot
```
<path/to/data/directory> here means a directory where config.json file will be stored. It should not contain any other unrelated data.
The cache and log files will be stored in the specified directory.
## Startup options
* --devices - Shows the list of all available input and output audio devices;
* -c PATH - Sets the path to the configuration file.
## Config file options
* language - Sets the bot's interface language. Warning! to select a language you need an appropriate language folder inside the "locale" folder;
* sound devices - Here you have to enter audio device numbers. Devices should be connected to each other (like Virtual audio cable or pulseaudio);
* player - This section sets the configuration for the player such as default volume, maximum volume, etc;
* teamtalk - here are main options for bot to connect and login to your TeamTalk server;
* Services - Here you should configure available services for music search and playback;
* logger - Here you can configure various logging related options.
## Pulse audio or VB cable settings
### Linux variant
* Install pulseaudio.
* type $pulseaudio --start
* Next command creates null sink and this sink can be monitored by default pulse input device.
$pacmd load-module module-null-sink
* then run ./TTMediaBot.sh --devices and check its numbers.
output should be null audio output, input should be pulse.
* put this numbers to your config.json.
### windows variant
* install VB-cable, run "TTMediaBot.py --devices" and check numbers of VB-cable devices
* put this numbers to your config.json.
## Some notes about the Windows variant
* When listing input and output devices in the Windows variant of TTMediaBot, please note, that the input device will be doubled, i.e., if the output device is line 1 with number 3, the input device for line 1 will be listed twice, at number 5 and, for example, at number 7.
* The correct number will be the last one as input, that is, if we selected the output as line 1 with the number 3, the input device would be line 1 with number 7 of the two options, number 5 and number 7.
* The same method applies to all numbers and all Input / Outputs.
# support us
* yoomoney: https://yoomoney.ru/to/4100117354062028
# contacts
* telegram channel: https://t.me/TTMediaBot_chat
* E-mail: TTMediaBot@gmail.com

64
TTMediaBot.py Normal file
View File

@ -0,0 +1,64 @@
from typing import Optional
from os import path
from argparse import ArgumentParser
from bot import Bot, app_vars
from bot.config import save_default_file
from bot.sound_devices import SoundDeviceManager
parser = ArgumentParser()
parser.add_argument(
"-c",
"--config",
help="Path to the configuration file",
default=path.join(app_vars.directory, "config.json"),
)
parser.add_argument("-C", "--cache", help="Path to the cache file", default=None)
parser.add_argument("-l", "--log", help="Path to the log file", default=None)
parser.add_argument(
"--devices", help="Show available devices and exit", action="store_true"
)
parser.add_argument(
"--default-config",
help='Save default config to "config_default.json" and exit',
action="store_true",
)
args = parser.parse_args()
def main(
config: str = args.config,
cache: Optional[str] = args.cache,
log: Optional[str] = args.log,
devices: bool = args.devices,
default_config: bool = args.default_config,
) -> None:
if devices:
bot = Bot(None, None, None)
echo_sound_devices(bot.sound_device_manager)
elif default_config:
save_default_file()
print("Successfully dumped to config_default.json")
else:
bot = Bot(config, cache, log)
bot.initialize()
try:
bot.run()
except KeyboardInterrupt:
bot.close()
def echo_sound_devices(sound_device_manager: SoundDeviceManager):
print("Output devices:")
for i, device in enumerate(sound_device_manager.output_devices):
print("\t{index}: {name}".format(index=i, name=device.name))
print()
print("Input devices:")
for i, device in enumerate(sound_device_manager.input_devices):
print("\t{index}: {name}".format(index=i, name=device.name))
if __name__ == "__main__":
main()

7
TTMediaBot.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
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" "$@"

386
bot/TeamTalk/__init__.py Normal file
View File

@ -0,0 +1,386 @@
from __future__ import annotations
import logging
import os
import re
import sys
from typing import AnyStr, List, TYPE_CHECKING, Optional, Union
from queue import Queue
from bot import app_vars
from bot.sound_devices import SoundDevice, SoundDeviceType
if sys.platform == "win32":
if sys.version_info.major == 3 and sys.version_info.minor >= 8:
os.add_dll_directory(app_vars.directory)
else:
os.chdir(app_vars.directory)
from bot.TeamTalk.thread import TeamTalkThread
from bot.TeamTalk.structs import *
import TeamTalkPy
re_line_endings = re.compile("[\\r\\n]")
if TYPE_CHECKING:
from bot import Bot
def _str(data: AnyStr) -> AnyStr:
if isinstance(data, str):
if os.supports_bytes_environ:
return bytes(data, "utf-8")
else:
return data
else:
return str(data, "utf-8")
def split(text: str, max_length: int = app_vars.max_message_length) -> List[str]:
if len(text) <= max_length:
lines = [text]
else:
lines = [""]
for line in text.split("\n"):
if len(line) <= max_length:
if len(lines[-1]) > 0 and len(lines[-1]) + len(line) + 1 <= max_length:
lines[-1] += "\n" + line
elif len(lines) == 1 and len(lines[0]) == 0:
lines[0] = line
else:
lines.append(line)
else:
words = [""]
for word in line.split(" "):
if len(word) <= max_length:
if (
len(words[-1]) > 0
and len(words[-1]) + len(word) + 1 <= max_length
):
words[-1] += " " + word
elif len(words) == 1 and len(words[0]) == 0:
words[0] == word
else:
words.append(word)
else:
chunk = word
for _ in range(0, int(len(chunk) / max_length) + 1):
words.append(chunk[0:max_length])
chunk = chunk[max_length::]
lines += words
return lines
class TeamTalk:
def __init__(self, bot: Bot) -> None:
self.config = bot.config.teamtalk
self.translator = bot.translator
TeamTalkPy.setLicense(
_str(self.config.license_name), _str(self.config.license_key)
)
self.tt = TeamTalkPy.TeamTalk()
self.state = State.NOT_CONNECTED
self.is_voice_transmission_enabled = False
self.nickname = self.config.nickname
self.gender = UserStatusMode.__members__[self.config.gender.upper()]
self.status = self.default_status
self.errors_queue: Queue[Error] = Queue()
self.event_success_queue: Queue[Event] = Queue()
self.message_queue: Queue[Message] = Queue()
self.myself_event_queue: Queue[Event] = Queue()
self.uploaded_files_queue: Queue[File] = Queue()
self.thread = TeamTalkThread(bot, self)
self.reconnect = False
self.reconnect_attempt = 0
self.user_account: UserAccount
def initialize(self) -> None:
logging.debug("Initializing TeamTalk")
self.thread.start()
self.connect()
logging.debug("TeamTalk initialized")
def close(self) -> None:
logging.debug("Closing teamtalk")
self.thread.close()
self.disconnect()
self.state = State.NOT_CONNECTED
self.tt.closeTeamTalk()
logging.debug("Teamtalk closed")
def connect(self) -> None:
self.state = State.CONNECTING
self.tt.connect(
_str(self.config.hostname),
self.config.tcp_port,
self.config.udp_port,
0,
0,
self.config.encrypted,
)
def disconnect(self) -> None:
self.tt.disconnect()
self.state = State.NOT_CONNECTED
def login(self) -> None:
self.tt.doLogin(
_str(self.config.nickname),
_str(self.config.username),
_str(self.config.password),
_str(app_vars.client_name),
)
def join(self) -> None:
if isinstance(self.config.channel, int):
channel_id = int(self.config.channel)
else:
channel_id = self.tt.getChannelIDFromPath(_str(self.config.channel))
if channel_id == 0:
channel_id = 1
self.tt.doJoinChannelByID(channel_id, _str(self.config.channel_password))
@property
def default_status(self) -> str:
if self.config.status:
return self.config.status
else:
return self.translator.translate('PM "start" to get short help!')
def send_message(
self, text: str, user: Optional[User] = None, type: int = 1
) -> None:
for string in split(text):
message = TeamTalkPy.TextMessage()
message.nFromUserID = self.tt.getMyUserID()
message.nMsgType = type
message.szMessage = _str(string)
if type == 1:
if isinstance(user, int):
message.nToUserID = user
else:
message.nToUserID = user.id
elif type == 2:
message.nChannelID = self.tt.getMyChannelID()
self.tt.doTextMessage(message)
def send_file(self, channel: Union[int, str], file_path: str):
if isinstance(channel, int):
channel_id = channel
else:
channel_id = self.tt.getChannelIDFromPath(_str(channel))
if channel_id == 0:
raise ValueError()
return self.tt.doSendFile(channel_id, _str(file_path))
def delete_file(self, channel: Union[int, str], file_id: int) -> int:
if isinstance(channel, int):
channel_id = channel
else:
channel_id = self.tt.getChannelIDFromPath(_str(channel))
if channel_id == 0 or file_id == 0:
raise ValueError()
return self.tt.doDeleteFile(channel_id, file_id)
def join_channel(self, channel: Union[str, int], password: str) -> int:
if isinstance(channel, int):
channel_id = channel
else:
channel_id = self.tt.getChannelIDFromPath(_str(channel))
if channel_id == 0:
raise ValueError()
return self.tt.doJoinChannelByID(channel_id, _str(password))
def change_nickname(self, nickname: str) -> None:
self.tt.doChangeNickname(_str(nickname))
def change_status_text(self, text: str) -> None:
if text:
self.status = split(text)[0]
else:
self.status = split(self.default_status)[0]
self.tt.doChangeStatus(self.gender.value, _str(self.status))
def change_gender(self, gender: str) -> None:
self.gender = UserStatusMode.__members__[gender.upper()]
self.tt.doChangeStatus(self.gender.value, _str(self.status))
def get_channel(self, channel_id: int) -> Channel:
channel = self.tt.getChannel(channel_id)
return self.get_channel_from_obj(channel)
def get_channel_from_obj(self, obj: TeamTalkPy.Channel) -> Channel:
try:
return Channel(
obj.nChannelID,
obj.szName,
obj.szTopic,
obj.nMaxUsers,
ChannelType(obj.uChannelType),
)
except ValueError:
return Channel(0, "", "", 0, ChannelType.Default)
@property
def flags(self) -> Flags:
return Flags(self.tt.getFlags())
def get_error(self, error_no: int, cmdid: int) -> Error:
try:
error_type = ErrorType(error_no)
except ValueError:
error_type = ErrorType(0)
return Error(_str(self.tt.getErrorMessage(error_no)), error_type, cmdid)
def get_message(self, msg: TeamTalkPy.TextMessage) -> Message:
try:
return Message(
re.sub(re_line_endings, "", _str(msg.szMessage)),
self.get_user(msg.nFromUserID),
self.get_channel(msg.nChannelID),
MessageType(msg.nMsgType),
)
except ValueError:
return Message("", self.get_user(1), self.get_channel(1), MessageType.User)
def get_file(self, file: TeamTalkPy.RemoteFile) -> File:
return File(
file.nFileID,
_str(file.szFileName),
self.get_channel(file.nChannelID),
file.nFileSize,
_str(file.szUsername),
)
@property
def user(self) -> User:
user = self.get_user(self.tt.getMyUserID())
user.user_account = self.user_account
return user
@property
def channel(self) -> Channel:
return self.get_channel(self.tt.getMyChannelID())
def get_user(self, id: int) -> User:
user = self.tt.getUser(id)
gender = UserStatusMode(user.nStatusMode)
return User(
user.nUserID,
_str(user.szNickname),
_str(user.szUsername),
_str(user.szStatusMsg),
gender,
UserState(user.uUserState),
self.get_channel(user.nChannelID),
_str(user.szClientName),
user.uVersion,
self.get_user_account(_str(user.szUsername)),
UserType(user.uUserType),
True
if _str(user.szUsername) in self.config.users.admins or user.uUserType == 2
else False,
_str(user.szUsername) in self.config.users.banned_users,
)
def get_user_account(self, username: str) -> UserAccount:
return UserAccount(username, "", "", UserType.Null, UserRight.Null, "/")
def get_user_account_by_tt_obj(self, obj: TeamTalkPy.UserAccount) -> UserAccount:
return UserAccount(
_str(obj.szUsername),
_str(obj.szPassword),
_str(obj.szNote),
UserType(obj.uUserType),
UserRight(obj.uUserRights),
_str(obj.szInitChannel),
)
def get_event(self, obj: TeamTalkPy.TTMessage) -> Event:
try:
channel = self.get_channel_from_obj(obj.channel)
except (UnicodeDecodeError, ValueError):
channel = Channel(1, "", "", 0, ChannelType.Default)
try:
error = self.get_error(obj.clienterrormsg.nErrorNo, obj.nSource)
except (UnicodeDecodeError, ValueError):
error = Error("", ErrorType.Success, 1)
try:
file = self.get_file(obj.remotefile)
except (UnicodeDecodeError, ValueError):
file = File(1, "", channel, 0, "")
try:
user_account = self.get_user_account_by_tt_obj(obj.useraccount)
except (UnicodeDecodeError, ValueError):
user_account = UserAccount("", "", "", UserType.Null, UserRight.Null, "")
try:
user = self.get_user(obj.user.nUserID)
except (UnicodeDecodeError, ValueError):
user = User(
1,
"",
"",
"",
UserStatusMode.M,
UserState.Null,
channel,
"",
1,
user_account,
UserType.Null,
False,
False,
)
try:
message = self.get_message(obj.textmessage)
except (UnicodeDecodeError, ValueError):
message = Message("", user, channel, MessageType.NONE)
return Event(
EventType(obj.nClientEvent),
obj.nSource,
channel,
error,
file,
message,
user,
user_account,
)
def get_input_devices(self) -> List[SoundDevice]:
devices: List[SoundDevice] = []
device_list = [i for i in self.tt.getSoundDevices()]
for device in device_list:
if sys.platform == "win32":
if (
device.nSoundSystem == TeamTalkPy.SoundSystem.SOUNDSYSTEM_WASAPI
and device.nMaxOutputChannels == 0
):
devices.append(
SoundDevice(
_str(device.szDeviceName),
device.nDeviceID,
SoundDeviceType.Input,
)
)
else:
devices.append(
SoundDevice(
_str(device.szDeviceName),
device.nDeviceID,
SoundDeviceType.Input,
)
)
return devices
def set_input_device(self, id: int) -> None:
self.tt.initSoundInputDevice(id)
def enable_voice_transmission(self) -> None:
self.tt.enableVoiceTransmission(True)
self.is_voice_transmission_enabled = True
def disable_voice_transmission(self) -> None:
self.tt.enableVoiceTransmission(False)
self.is_voice_transmission_enabled = False

336
bot/TeamTalk/structs.py Normal file
View File

@ -0,0 +1,336 @@
from enum import Enum, Flag
import TeamTalkPy
class State(Enum):
NOT_CONNECTED = 0
CONNECTING = 1
RECONNECTING = 2
CONNECTED = 3
class Flags(Flag):
CLOSED = TeamTalkPy.ClientFlags.CLIENT_CLOSED
SND_INPUT_READY = TeamTalkPy.ClientFlags.CLIENT_SNDINPUT_READY
SND_OUTPUT_READY = TeamTalkPy.ClientFlags.CLIENT_SNDOUTPUT_READY
SND_INOUTPUT_DUPLEX = TeamTalkPy.ClientFlags.CLIENT_SNDINOUTPUT_DUPLEX
SND_INPUT_VOICE_ACTIVATED = TeamTalkPy.ClientFlags.CLIENT_SNDINPUT_VOICEACTIVATED
SND_INPUT_VOICE_ACTIVE = TeamTalkPy.ClientFlags.CLIENT_SNDINPUT_VOICEACTIVE
SND_OUTPUT_MUTE = TeamTalkPy.ClientFlags.CLIENT_SNDOUTPUT_MUTE
SND_OUTPUT_AUTO_3D_POSITION = TeamTalkPy.ClientFlags.CLIENT_SNDOUTPUT_AUTO3DPOSITION
VIDEO_CAPTURE_READY = TeamTalkPy.ClientFlags.CLIENT_VIDEOCAPTURE_READY
TX_VOICE = TeamTalkPy.ClientFlags.CLIENT_TX_VOICE
TX_VIDEO_CAPTURE = TeamTalkPy.ClientFlags.CLIENT_TX_VIDEOCAPTURE
TX_DESKTOP = TeamTalkPy.ClientFlags.CLIENT_TX_DESKTOP
DESKTOP_ACTIVE = TeamTalkPy.ClientFlags.CLIENT_DESKTOP_ACTIVE
MUX_AUDIO_FILE = TeamTalkPy.ClientFlags.CLIENT_MUX_AUDIOFILE
CONNECTING = TeamTalkPy.ClientFlags.CLIENT_CONNECTING
CONNECTED = TeamTalkPy.ClientFlags.CLIENT_CONNECTED
CONNECTION = TeamTalkPy.ClientFlags.CLIENT_CONNECTION = (
TeamTalkPy.ClientFlags.CLIENT_CONNECTING
or TeamTalkPy.ClientFlags.CLIENT_CONNECTED
)
AUTHORIZED = TeamTalkPy.ClientFlags.CLIENT_AUTHORIZED
STREAM_AUDIO = TeamTalkPy.ClientFlags.CLIENT_STREAM_AUDIO
STREAM_VIDEO = TeamTalkPy.ClientFlags.CLIENT_STREAM_VIDEO
class ChannelType(Flag):
ClassRoom = TeamTalkPy.ChannelType.CHANNEL_CLASSROOM
Default = TeamTalkPy.ChannelType.CHANNEL_DEFAULT
Hidden = TeamTalkPy.ChannelType.CHANNEL_HIDDEN
NoRecording = TeamTalkPy.ChannelType.CHANNEL_NO_RECORDING
NoVoiceActivation = TeamTalkPy.ChannelType.CHANNEL_NO_VOICEACTIVATION
OperatorRecnvOnly = TeamTalkPy.ChannelType.CHANNEL_OPERATOR_RECVONLY
Permanent = TeamTalkPy.ChannelType.CHANNEL_PERMANENT
SoloTransmit = TeamTalkPy.ChannelType.CHANNEL_SOLO_TRANSMIT
class Channel:
def __init__(
self, id: int, name: str, topic: str, max_users: int, type: ChannelType
) -> None:
self.id = id
self.name = name
self.topic = topic
self.max_users = max_users
self.type = type
class ErrorType(Enum):
Success = TeamTalkPy.ClientError.CMDERR_SUCCESS
SyntaxError = TeamTalkPy.ClientError.CMDERR_SYNTAX_ERROR
UnknownCommand = TeamTalkPy.ClientError.CMDERR_UNKNOWN_COMMAND
MissingParameter = TeamTalkPy.ClientError.CMDERR_MISSING_PARAMETER
IncompatibleProtocols = TeamTalkPy.ClientError.CMDERR_INCOMPATIBLE_PROTOCOLS
UnknownAudioCodec = TeamTalkPy.ClientError.CMDERR_UNKNOWN_AUDIOCODEC
InvalidUsername = TeamTalkPy.ClientError.CMDERR_INVALID_USERNAME
IncorrectChannelPassword = TeamTalkPy.ClientError.CMDERR_INCORRECT_CHANNEL_PASSWORD
InvalidAccount = TeamTalkPy.ClientError.CMDERR_INVALID_ACCOUNT
MaxServerUsersExceeded = TeamTalkPy.ClientError.CMDERR_MAX_SERVER_USERS_EXCEEDED
MaxChannelUsersExceeded = TeamTalkPy.ClientError.CMDERR_MAX_CHANNEL_USERS_EXCEEDED
ServerBanned = TeamTalkPy.ClientError.CMDERR_SERVER_BANNED
NotAuthorised = TeamTalkPy.ClientError.CMDERR_NOT_AUTHORIZED
MaxDiskusageExceeded = TeamTalkPy.ClientError.CMDERR_MAX_DISKUSAGE_EXCEEDED
IncorrectOperatorPassword = TeamTalkPy.ClientError.CMDERR_INCORRECT_OP_PASSWORD
AudioCodecBitrateLimitExceeded = (
TeamTalkPy.ClientError.CMDERR_AUDIOCODEC_BITRATE_LIMIT_EXCEEDED
)
MaxLoginsPerIpAddressExceeded = (
TeamTalkPy.ClientError.CMDERR_MAX_LOGINS_PER_IPADDRESS_EXCEEDED
)
MaxChannelsExceeded = TeamTalkPy.ClientError.CMDERR_MAX_CHANNELS_EXCEEDED
CommandFlood = TeamTalkPy.ClientError.CMDERR_COMMAND_FLOOD
ChannelBanned = TeamTalkPy.ClientError.CMDERR_CHANNEL_BANNED
NotLoggedin = TeamTalkPy.ClientError.CMDERR_NOT_LOGGEDIN
AlreadyLoggedin = TeamTalkPy.ClientError.CMDERR_ALREADY_LOGGEDIN
NotInChannel = TeamTalkPy.ClientError.CMDERR_NOT_IN_CHANNEL
AlreadyInChannel = TeamTalkPy.ClientError.CMDERR_ALREADY_IN_CHANNEL
ChannelAlreadyExists = TeamTalkPy.ClientError.CMDERR_CHANNEL_ALREADY_EXISTS
ChannelNotFound = TeamTalkPy.ClientError.CMDERR_CHANNEL_NOT_FOUND
UserNotFound = TeamTalkPy.ClientError.CMDERR_USER_NOT_FOUND
BanNotFound = TeamTalkPy.ClientError.CMDERR_BAN_NOT_FOUND
FileTransferNotFound = TeamTalkPy.ClientError.CMDERR_FILETRANSFER_NOT_FOUND
OpenFileFailed = TeamTalkPy.ClientError.CMDERR_OPENFILE_FAILED
AccountNotFound = TeamTalkPy.ClientError.CMDERR_ACCOUNT_NOT_FOUND
FileNotFound = TeamTalkPy.ClientError.CMDERR_FILE_NOT_FOUND
FileAlreadyExists = TeamTalkPy.ClientError.CMDERR_FILE_ALREADY_EXISTS
FileSharingDisabled = TeamTalkPy.ClientError.CMDERR_FILESHARING_DISABLED
ChannelHasUsers = TeamTalkPy.ClientError.CMDERR_CHANNEL_HAS_USERS
LoginServiceUnavailable = TeamTalkPy.ClientError.CMDERR_LOGINSERVICE_UNAVAILABLE
ChannelCannotBeHidden = TeamTalkPy.ClientError.CMDERR_CHANNEL_CANNOT_BE_HIDDEN
SndInputFailure = TeamTalkPy.ClientError.INTERR_SNDINPUT_FAILURE
SndOutputFailure = TeamTalkPy.ClientError.INTERR_SNDOUTPUT_FAILURE
AudioCodecInitFailed = TeamTalkPy.ClientError.INTERR_AUDIOCODEC_INIT_FAILED
SpeexDSPInitFailed = TeamTalkPy.ClientError.INTERR_SPEEXDSP_INIT_FAILED
TTMesageQueueOverflow = TeamTalkPy.ClientError.INTERR_TTMESSAGE_QUEUE_OVERFLOW
SndEffectFailure = TeamTalkPy.ClientError.INTERR_SNDEFFECT_FAILURE
class Error:
def __init__(self, message: str, type: ErrorType, command_id: int) -> None:
self.message = message
self.type = type
self.command_id = command_id
class UserType(Enum):
Null = 0
Default = 1
Admin = 2
class UserState(Flag):
Null = TeamTalkPy.UserState.USERSTATE_NONE
Voice = TeamTalkPy.UserState.USERSTATE_VOICE
MuteVoice = TeamTalkPy.UserState.USERSTATE_MUTE_VOICE
MuteMediaFile = TeamTalkPy.UserState.USERSTATE_MUTE_MEDIAFILE
Desktop = TeamTalkPy.UserState.USERSTATE_DESKTOP
VideoCapture = TeamTalkPy.UserState.USERSTATE_VIDEOCAPTURE
AudioFile = TeamTalkPy.UserState.USERSTATE_MEDIAFILE_AUDIO
VideoFile = TeamTalkPy.UserState.USERSTATE_MEDIAFILE_VIDEO
MediaFile = TeamTalkPy.UserState.USERSTATE_MEDIAFILE
class UserStatusMode(Flag):
Available = 0
Away = 1
Question = 2
VideoTx = 512
Desktop = 1024
StreamMediaFile = 2048
M = Available
F = 256
N = 4096
class UserRight(Flag):
Null = TeamTalkPy.UserRight.USERRIGHT_NONE
MultiLogin = TeamTalkPy.UserRight.USERRIGHT_MULTI_LOGIN
ViewAllUsers = TeamTalkPy.UserRight.USERRIGHT_VIEW_ALL_USERS
CreateTemporaryChannel = TeamTalkPy.UserRight.USERRIGHT_CREATE_TEMPORARY_CHANNEL
ModifyChannels = TeamTalkPy.UserRight.USERRIGHT_MODIFY_CHANNELS
BroadcastTextMessage = TeamTalkPy.UserRight.USERRIGHT_TEXTMESSAGE_BROADCAST
KickUsers = TeamTalkPy.UserRight.USERRIGHT_KICK_USERS
BanUsers = TeamTalkPy.UserRight.USERRIGHT_BAN_USERS
MoveUsers = TeamTalkPy.UserRight.USERRIGHT_MOVE_USERS
OperatorEnable = TeamTalkPy.UserRight.USERRIGHT_OPERATOR_ENABLE
UploadFiles = TeamTalkPy.UserRight.USERRIGHT_UPLOAD_FILES
DownloadFiles = TeamTalkPy.UserRight.USERRIGHT_DOWNLOAD_FILES
UpdateServerProperties = TeamTalkPy.UserRight.USERRIGHT_UPDATE_SERVERPROPERTIES
TransmitVoice = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_VOICE
TransmitVideoCapture = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_VIDEOCAPTURE
TransmitDesktop = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_DESKTOP
TransmitDesktopInput = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_DESKTOPINPUT
TransmitMediaFileAudio = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_MEDIAFILE_AUDIO
TransmitMediaFileVideo = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_MEDIAFILE_VIDEO
TransmitMediaFile = TeamTalkPy.UserRight.USERRIGHT_TRANSMIT_MEDIAFILE
LockedNickname = TeamTalkPy.UserRight.USERRIGHT_LOCKED_NICKNAME
LockedStatus = TeamTalkPy.UserRight.USERRIGHT_LOCKED_STATUS
RecordVoice = TeamTalkPy.UserRight.USERRIGHT_RECORD_VOICE
ViewHiddenChannels = TeamTalkPy.UserRight.USERRIGHT_VIEW_HIDDEN_CHANNELS
class UserAccount:
def __init__(
self,
username: str,
password: str,
note: str,
type: UserType,
rights: UserRight,
init_channel: str,
) -> None:
self.username = username
self.password = password
self.note = note
self.type = type
self.rights = rights
self.init_channel = init_channel
class User:
def __init__(
self,
id: int,
nickname: str,
username: str,
status: str,
gender: UserStatusMode,
state: UserState,
channel: Channel,
client_name: str,
version: int,
user_account: UserAccount,
type: UserType,
is_admin: bool,
is_banned: bool,
) -> None:
self.id = id
self.nickname = nickname
self.username = username
self.channel = channel
self.status = status
self.gender = gender
self.state = state
self.client_name = client_name
self.version = version
self.user_account = user_account
self.type = type
self.is_admin = is_admin
self.is_banned = is_banned
class MessageType(Enum):
NONE = 0
User = TeamTalkPy.TextMsgType.MSGTYPE_USER
Channel = TeamTalkPy.TextMsgType.MSGTYPE_CHANNEL
Broadcast = TeamTalkPy.TextMsgType.MSGTYPE_BROADCAST
Custom = TeamTalkPy.TextMsgType.MSGTYPE_CUSTOM
class Message:
def __init__(
self, text: str, user: User, channel: Channel, type: MessageType
) -> None:
self.text = text
self.channel = channel
self.user = user
self.type = type
class File:
def __init__(
self, id: int, name: str, channel: Channel, size: int, username: str
) -> None:
self.id = id
self.name = name
self.channel = channel
self.size = size
self.username = username
class EventType(Enum):
NONE = TeamTalkPy.ClientEvent.CLIENTEVENT_NONE
CON_SUCCESS = TeamTalkPy.ClientEvent.CLIENTEVENT_CON_SUCCESS
CON_FAILED = TeamTalkPy.ClientEvent.CLIENTEVENT_CON_FAILED
CON_LOST = TeamTalkPy.ClientEvent.CLIENTEVENT_CON_LOST
CON_MAX_PAYLOAD_UPDATED = TeamTalkPy.ClientEvent.CLIENTEVENT_CON_MAX_PAYLOAD_UPDATED
PROCESSING = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_PROCESSING
ERROR = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_ERROR
SUCCESS = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_SUCCESS
MYSELF_LOGGEDIN = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_MYSELF_LOGGEDIN
MYSELF_LOGGEDOUT = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_MYSELF_LOGGEDOUT
MYSELF_KICKED = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_MYSELF_KICKED
USER_LOGGEDIN = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_LOGGEDIN
USER_LOGGEDOUT = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_LOGGEDOUT
USER_UPDATE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_UPDATE
USER_JOINED = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_JOINED
USER_LEFT = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_LEFT
USER_TEXT_MESSAGE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USER_TEXTMSG
CHANNEL_NEW = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_CHANNEL_NEW
CHANNEL_UPDATE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_CHANNEL_UPDATE
CHANNEL_REMOVE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_CHANNEL_REMOVE
SERVER_UPDATE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_SERVER_UPDATE
SERVER_STATISTICS = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_SERVERSTATISTICS
FILE_NEW = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_FILE_NEW
FILE_REMOVE = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_FILE_REMOVE
USER_ACCOUNT = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_USERACCOUNT
BANNED_USER = TeamTalkPy.ClientEvent.CLIENTEVENT_CMD_BANNEDUSER
STATE_CHANGE = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_STATECHANGE
USER_VIDEO_CAPTURE = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_VIDEOCAPTURE
USER_MEDIAFILE_VIDEO = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_MEDIAFILE_VIDEO
USER_DESKTOP_WINDOW = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_DESKTOPWINDOW
USER_DESKTOP_CURSOR = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_DESKTOPCURSOR
USER_DESKTOP_INPUT = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_DESKTOPINPUT
USER_RECORD_MEDIAFILE = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_RECORD_MEDIAFILE
USER_AUDIO_BLOCK = TeamTalkPy.ClientEvent.CLIENTEVENT_USER_AUDIOBLOCK
INTERNAL_ERROR = TeamTalkPy.ClientEvent.CLIENTEVENT_INTERNAL_ERROR
VOICE_ACTIVATION = TeamTalkPy.ClientEvent.CLIENTEVENT_VOICE_ACTIVATION
HOTKEY = TeamTalkPy.ClientEvent.CLIENTEVENT_HOTKEY
HOTKEY_TEST = TeamTalkPy.ClientEvent.CLIENTEVENT_HOTKEY_TEST
FILE_TRANSFER = TeamTalkPy.ClientEvent.CLIENTEVENT_FILETRANSFER
DESKTOP_WINDOW_TRANSFER = TeamTalkPy.ClientEvent.CLIENTEVENT_DESKTOPWINDOW_TRANSFER
STREAM_MEDIAFILE = TeamTalkPy.ClientEvent.CLIENTEVENT_STREAM_MEDIAFILE
LOCAL_MEDIAFILE = TeamTalkPy.ClientEvent.CLIENTEVENT_LOCAL_MEDIAFILE
AUDIO_INPUT = TeamTalkPy.ClientEvent.CLIENTEVENT_AUDIOINPUT
USER_FIRST_STREAM_VOICE_PACKET = (
TeamTalkPy.ClientEvent.CLIENTEVENT_USER_FIRSTVOICESTREAMPACKET
)
class Event:
def __init__(
self,
event_type: EventType,
source: int,
channel: Channel,
error: Error,
file: File,
message: Message,
user: User,
user_account: UserAccount,
):
self.event_type = event_type
self.source = source
# ("ttType", INT32),
# ("uReserved", UINT32),
self.channel = channel
self.error = error
# desktop_input
# ("filetransfer", FileTransfer),
# ("mediafileinfo", MediaFileInfo),
self.file = file
# ("serverproperties", ServerProperties),
# ("serverstatistics", ServerStatistics),
self.message: Message = message
self.user = user
self.user_account = user_account
# ("banneduser", BannedUser),
# ("bActive", BOOL),
# ("nBytesRemain", INT32),
# ("nStreamID", INT32),
# ("nPayloadSize", INT32),
# ("nStreamType", INT32),
# ("audioinputprogress", AudioInputProgress),

214
bot/TeamTalk/thread.py Normal file
View File

@ -0,0 +1,214 @@
from __future__ import annotations
from importlib.machinery import SourceFileLoader
import logging
import os
from threading import Thread
import time
from typing import Any, Callable, Optional, Tuple, TYPE_CHECKING
from types import ModuleType
import types
import sys
from bot.TeamTalk.structs import *
if TYPE_CHECKING:
from bot import Bot
from bot.TeamTalk import TeamTalk
class TeamTalkThread(Thread):
def __init__(self, bot: Bot, ttclient: TeamTalk):
Thread.__init__(self, daemon=True)
self.name = "TeamTalkThread"
self.bot = bot
self.config = ttclient.config
self.ttclient = ttclient
def run(self) -> None:
if self.config.event_handling.load_event_handlers:
self.event_handlers = self.import_event_handlers()
self._close = False
while not self._close:
event = self.ttclient.get_event(self.ttclient.tt.getMessage())
if event.event_type == EventType.NONE:
continue
elif (
event.event_type == EventType.ERROR
and self.ttclient.state == State.CONNECTED
):
self.ttclient.errors_queue.put(event.error)
elif (
event.event_type == EventType.SUCCESS
and self.ttclient.state == State.CONNECTED
):
self.ttclient.event_success_queue.put(event)
elif (
event.event_type == EventType.USER_TEXT_MESSAGE
and event.message.type == MessageType.User
):
self.ttclient.message_queue.put(event.message)
elif (
event.event_type == EventType.FILE_NEW
and event.file.username == self.config.username
and event.file.channel.id == self.ttclient.channel.id
):
self.ttclient.uploaded_files_queue.put(event.file)
elif (
event.event_type == EventType.CON_FAILED
or event.event_type == EventType.CON_LOST
or event.event_type == EventType.MYSELF_KICKED
):
if event.event_type == EventType.CON_FAILED:
logging.warning("Connection failed")
elif event.event_type == EventType.CON_LOST:
logging.warning("Server lost")
else:
logging.warning("Kicked")
self.ttclient.disconnect()
if (
self.ttclient.reconnect
and self.ttclient.reconnect_attempt
< self.config.reconnection_attempts
or self.config.reconnection_attempts < 0
):
self.ttclient.disconnect()
time.sleep(self.config.reconnection_timeout)
self.ttclient.connect()
self.ttclient.reconnect_attempt += 1
else:
logging.error("Connection error")
sys.exit(1)
elif event.event_type == EventType.CON_SUCCESS:
self.ttclient.reconnect_attempt = 0
self.ttclient.login()
elif event.event_type == EventType.ERROR:
if self.ttclient.flags & Flags.AUTHORIZED == Flags(0):
logging.warning("Login failed")
if (
self.ttclient.reconnect
and self.ttclient.reconnect_attempt
< self.config.reconnection_attempts
or self.config.reconnection_attempts < 0
):
time.sleep(self.config.reconnection_timeout)
self.ttclient.login()
else:
logging.error("Login error")
sys.exit(1)
else:
logging.warning("Failed to join channel")
if (
self.ttclient.reconnect
and self.ttclient.reconnect_attempt
< self.config.reconnection_attempts
or self.config.reconnection_attempts < 0
):
time.sleep(self.config.reconnection_timeout)
self.ttclient.join()
else:
logging.error("Error joining channel")
sys.exit(1)
elif event.event_type == EventType.MYSELF_LOGGEDIN:
self.ttclient.user_account = event.user_account
self.ttclient.reconnect_attempt = 0
self.ttclient.join()
elif (
event.event_type == EventType.SUCCESS
and self.ttclient.state == State.CONNECTING
):
self.ttclient.reconnect_attempt = 0
self.ttclient.reconnect = True
self.ttclient.state = State.CONNECTED
self.ttclient.change_status_text(self.ttclient.status)
if self.config.event_handling.load_event_handlers:
self.run_event_handler(event)
def close(self) -> None:
self._close = True
def get_function_name_by_event_type(self, event_type: EventType) -> str:
return f"on_{event_type.name.lower()}"
def import_event_handlers(self) -> ModuleType:
try:
if (
os.path.isfile(self.config.event_handling.event_handlers_file_name)
and os.path.splitext(
self.config.event_handling.event_handlers_file_name
)[1]
== ".py"
):
module = SourceFileLoader(
os.path.splitext(
self.config.event_handling.event_handlers_file_name
)[0],
self.config.event_handling.event_handlers_file_name,
).load_module()
elif os.path.isdir(
self.config.event_handling.event_handlers_file_name
) and "__init__.py" in os.listdir(
self.config.event_handling.event_handlers_file_name
):
module = SourceFileLoader(
self.config.event_handling.event_handlers_file_name,
self.config.event_handling.event_handlers_file_name
+ "/__init__.py",
).load_module()
else:
logging.error(
"Incorrect path to event handlers. An empty module will be used"
)
module = types.ModuleType("event_handlers")
except Exception as e:
logging.error(
"Can't load specified event handlers. Error: {}. An empty module will be used.".format(
e
)
)
module = types.ModuleType("event_handlers")
return module
def parse_event(self, event: Event) -> Tuple[Any, ...]:
if event.event_type in (
EventType.USER_UPDATE,
EventType.USER_JOINED,
EventType.USER_LOGGEDIN,
EventType.USER_LOGGEDOUT,
):
return (event.user,)
elif event.event_type == EventType.USER_LEFT:
return (event.source, event.user)
elif event.event_type == EventType.USER_TEXT_MESSAGE:
return (event.message,)
elif event.event_type in (
EventType.CHANNEL_NEW,
EventType.CHANNEL_UPDATE,
EventType.CHANNEL_REMOVE,
):
return (event.channel,)
elif event.event_type in (
EventType.FILE_NEW,
EventType.FILE_REMOVE,
):
return (event.file,)
else:
return (1, 2)
def run_event_handler(self, event: Event) -> None:
try:
event_handler: Optional[Callable[..., None]] = getattr(
self.event_handlers,
self.get_function_name_by_event_type(event.event_type),
None,
)
if not event_handler:
return
try:
event_handler(*self.parse_event(event), self.bot)
except Exception as e:
print("Error in event handling {}".format(e))
except AttributeError:
self.event_handlers = self.import_event_handlers()
self.run_event_handler(event)

150
bot/__init__.py Normal file
View File

@ -0,0 +1,150 @@
import os
import logging
import queue
import sys
import time
from typing import Optional, List, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from bot.config.models import CronEntryModel
from pydantic import ValidationError
from crontab import CronTab
from bot import (
TeamTalk,
cache,
commands,
config,
connectors,
logger,
modules,
player,
services,
sound_devices,
translator,
app_vars,
)
from bot.utils import sort_cron_tasks
class Bot:
def __init__(
self,
config_file_name: Optional[str],
cache_file_name: Optional[str] = None,
log_file_name: Optional[str] = None,
) -> None:
try:
self.config_manager = config.ConfigManager(config_file_name)
except ValidationError as e:
for error in e.errors():
print(
"Error in config:",
".".join([str(i) for i in error["loc"]]),
error["msg"],
)
sys.exit(1)
except PermissionError:
sys.exit(
"The configuration file cannot be accessed due to a permission error or is already used by another instance of the bot"
)
self.config = self.config_manager.config
self.translator = translator.Translator(self.config.general.language)
try:
if cache_file_name:
self.cache_manager = cache.CacheManager(cache_file_name)
else:
cache_file_name = self.config.general.cache_file_name
if not os.path.isdir(
os.path.join(*os.path.split(cache_file_name)[0:-1])
):
cache_file_name = os.path.join(
self.config_manager.config_dir, cache_file_name
)
self.cache_manager = cache.CacheManager(cache_file_name)
except PermissionError:
sys.exit(
"The cache file cannot be accessed due to a permission error or is already used by another instance of the bot"
)
self.cache = self.cache_manager.cache
self.log_file_name = log_file_name
self.player = player.Player(self)
self.periodic_player = player.Player(self)
self.ttclient = TeamTalk.TeamTalk(self)
self.tt_player_connector = connectors.TTPlayerConnector(self)
self.periodic_tt_player_connector = connectors.MinimalTTPlayerConnector(self)
self.sound_device_manager = sound_devices.SoundDeviceManager(self)
self.service_manager = services.ServiceManager(self)
self.module_manager = modules.ModuleManager(self)
self.command_processor = commands.CommandProcessor(self)
self.scheduled_command_processor = commands.ScheduledCommandProcessor(self)
self.cron_patterns: List[Tuple[CronTab, CronEntryModel]] = []
def initialize(self):
if self.config.logger.log:
logger.initialize_logger(self)
logging.debug("Initializing")
self.sound_device_manager.initialize()
self.ttclient.initialize()
self.player.initialize()
self.periodic_player.initialize()
self.service_manager.initialize()
if self.config.schedule.enabled:
# parse all cron patterns into CronTab instances and store
for entry in self.config.schedule.patterns:
logging.debug(
f"Parsing cron pattern '{entry.pattern}' and appending to list"
)
e = CronTab(entry.pattern)
self.cron_patterns.append((e, entry))
logging.debug("Initialized")
def run(self):
logging.debug("Starting")
counter = 0
counter_total = int(1 / app_vars.loop_timeout)
self.player.run()
self.periodic_player.run()
self.tt_player_connector.start()
self.periodic_tt_player_connector.start()
self.command_processor.run()
self.scheduled_command_processor.run()
logging.info("Started")
self._close = False
while not self._close:
if self.config.schedule.enabled and counter == counter_total:
tasks = sort_cron_tasks(self.cron_patterns)
for ct, entry in tasks:
if ct.next() <= 1:
# run the associated command here
logging.debug(
f"Running command '{entry.command}' for cron pattern '{entry.pattern}"
)
# call the scheduled command processor with the cron entry from config
self.scheduled_command_processor(entry)
try:
message = self.ttclient.message_queue.get_nowait()
logging.info(
"New message {text} from {username}".format(
text=message.text, username=message.user.username
)
)
self.command_processor(message)
except queue.Empty:
pass
if counter > counter_total:
counter = 0
counter += 1
time.sleep(app_vars.loop_timeout)
def close(self) -> None:
logging.debug("Closing bot")
self.player.close()
self.periodic_player.close()
self.ttclient.close()
self.tt_player_connector.close()
self.periodic_tt_player_connector.close()
self.config_manager.close()
self.cache_manager.close()
self._close = True
logging.info("Bot closed")

54
bot/app_vars.py Normal file
View File

@ -0,0 +1,54 @@
from __future__ import annotations
import os
from typing import Callable, TYPE_CHECKING
if TYPE_CHECKING:
from bot.translator import Translator
app_name = "PandoraBox"
app_version = "2.3.5"
client_name = app_name + "-V" + app_version
about_text: Callable[[Translator], str] = lambda translator: translator.translate(
"""\
A media streaming bot for TeamTalk.
Authors: Amir Gumerov, Vladislav Kopylov, Beqa Gozalishvili, Kirill Belousov.
Home page: https://github.com/gumerov-amir/TTMediaBot
License: MIT License\
"""
)
start_bottt: Callable[[Translator], str] = lambda translator: translator.translate(
"""\
Hello there!
I'm PandoraBox, your go-to companion for discovering amazing songs and audio through YouTube, Yandex Music, and VK.
Hosted on Pandora's server, I'm all set to bring audio magic to your TeamTalk experience.
To get started, simply send me a private message with a specific command.
Here's how you can interact with me:
- help: Receive a handy guide to all the commands you can use, delivered straight to your private message.
- contacts: Get contact information sent directly to your private message.
- log: Check out the change log file.
If you encounter any issues or want to chat with us, feel free to reach out.
Thank you for choosing our service, and have a fantastic day!\
"""
)
contacts_bot: Callable[[Translator], str] = lambda translator: translator.translate(
"""\
If you encounter any issues with this bot, please reach out to our dedicated technicians:
- Muhammad:
- WhatsApp: https://api.whatsapp.com/send?phone=6282156978782
- Telegram: https://t.me/muha_aku
- Rexya:
- WhatsApp: https://api.whatsapp.com/send?phone=6288222553434
- Email: rexya@infiartt.com
Join the TTMediaBot Official Group on Telegram: https://t.me/TTMediaBot_chat\
"""
)
fallback_service = "yt"
loop_timeout = 0.01
max_message_length = 512
recents_max_lenth = 64
tt_event_timeout = 2
directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

74
bot/cache.py Normal file
View File

@ -0,0 +1,74 @@
from __future__ import annotations
import pickle
from collections import deque
from typing import Any, Dict, List, TYPE_CHECKING
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]
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
self.recents: deque[Track] = (
cache_data["recents"]
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 {}
)
@property
def data(self):
return {"cache_version": self.cache_version, "recents": self.recents, "favorites": self.favorites}
class CacheManager:
version = 1
def __init__(self, file_name: str) -> None:
self.file_name = file_name
try:
self.data = cache_migrator.migrate(self, self._load())
self.cache = Cache(self.data)
except FileNotFoundError:
self.cache = Cache({})
self._dump(self.cache.data)
self._lock()
def _dump(self, data: cache_data_type):
with open(self.file_name, "wb") as f:
pickle.dump(data, f)
def _load(self) -> cache_data_type:
with open(self.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 close(self):
self.file_locker.release()
def save(self):
self.file_locker.release()
self._dump(self.cache.data)
self.file_locker.acquire()

267
bot/commands/__init__.py Normal file
View File

@ -0,0 +1,267 @@
from __future__ import annotations
import logging
import re
from threading import Thread
from typing import Any, List, TYPE_CHECKING, Tuple
from bot import app_vars, errors
from bot.TeamTalk.structs import Message, User, UserType
from bot.commands import admin_commands, user_commands
from bot.commands.task_processor import TaskProcessor
re_command = re.compile("[a-z]+")
re_arg_split = re.compile(r"(?<!\\)\|")
if TYPE_CHECKING:
from bot import Bot
from bot.config.models import CronEntryModel
class CommandProcessor:
def __init__(self, bot: Bot):
self.task_processor = TaskProcessor(self)
self.bot = bot
self.config = bot.config
self.config_manager = bot.config_manager
self.cache = bot.cache
self.cache_manager = bot.cache_manager
self.module_manager = bot.module_manager
self.player = bot.player
self.periodic_player = bot.periodic_player
self.service_manager = bot.service_manager
self.ttclient = bot.ttclient
self.translator = bot.translator
self.locked = False
self.current_command_id = 0
self.commands_dict = {
"start": user_commands.StartBottt,
"about": user_commands.AboutCommand,
"contacts": user_commands.ContactsBot,
"help": user_commands.HelpCommand,
"p": user_commands.PlayPauseCommand,
"u": user_commands.PlayUrlCommand,
"sv": user_commands.ServiceCommand,
"s": user_commands.StopCommand,
"b": user_commands.PreviousTrackCommand,
"n": user_commands.NextTrackCommand,
"c": user_commands.SelectTrackCommand,
"sb": user_commands.SeekBackCommand,
"sf": user_commands.SeekForwardCommand,
"sbf": user_commands.DefaultSeekStepCommand,
"v": user_commands.VolumeCommand,
"sp": user_commands.SpeedCommand,
"f": user_commands.FavoritesCommand,
"m": user_commands.ModeCommand,
"gl": user_commands.GetLinkCommand,
"dl": user_commands.DownloadCommand,
"r": user_commands.RecentsCommand,
"log":user_commands.ChangeLogCommand,
}
self.admin_commands_dict = {
"bc": admin_commands.BlockCommandCommand,
"bmsg": admin_commands.SendBroadcastMessages,
"cc": admin_commands.ClearCacheCommand,
"sds": admin_commands.ShowDefaultServerCommand,
"cds": admin_commands.DefaultServerCommand,
"cdt": admin_commands.DefaultTCPPortCommand,
"cdd": admin_commands.DefaultUDPPortCommand,
"cdu": admin_commands.DefaultUsernameCommand,
"cdup": admin_commands.DefaultPasswordCommand,
"cdc": admin_commands.DefaultChannelCommand,
"cdcp": admin_commands.DefaultChannelPasswordCommand,
"cdid": admin_commands.GetChannelIDCommand,
"cg": admin_commands.ChangeGenderCommand,
"cl": admin_commands.ChangeLanguageCommand,
"cmsg": admin_commands.SendChannelMessages,
"cn": admin_commands.ChangeNicknameCommand,
"cs": admin_commands.ChangeStatusCommand,
"cr": admin_commands.SchedulerCommand,
"jc": admin_commands.JoinChannelCommand,
# "jcj": admin_commands.CreateChannelCommand,
# "ts": TaskSchedulerCommand,
"lll": admin_commands.LockCommand,
"ua": admin_commands.AdminUsersCommand,
"ub": admin_commands.BannedUsersCommand,
"eh": admin_commands.EventHandlingCommand,
"sc": admin_commands.SaveConfigCommand,
#"va": admin_commands.VoiceTransmissionCommand,
"rsrs": admin_commands.RestartCommand,
"qqq": admin_commands.QuitCommand,
}
def run(self):
self.task_processor.start()
def __call__(self, message: Message) -> None:
command_thread = Thread(target=self._run, args=(message,))
command_thread.start()
def _run(self, message: Message) -> None:
try:
command_name, arg = self.parse_command(message.text)
if self.check_access(message.user, command_name):
command_class = self.get_command(command_name, message.user)
command = command_class(self)
self.current_command_id = id(command)
result = command(arg, message.user)
if result:
self.ttclient.send_message(
result,
message.user,
) # here was command.ttclient later
except errors.InvalidArgumentError:
self.ttclient.send_message(
self.help(command_name, message.user),
message.user,
)
except errors.AccessDeniedError as e:
self.ttclient.send_message(str(e), message.user)
except (errors.ParseCommandError, errors.UnknownCommandError):
self.ttclient.send_message(
self.translator.translate('Unknown command. Send "start" for help.'),
message.user,
)
except Exception as e:
logging.error("", exc_info=True)
self.ttclient.send_message(
self.translator.translate("Error: {}").format(str(e)),
message.user,
)
def check_access(self, user: User, command: str) -> bool:
if (
not user.is_admin and user.type != UserType.Admin
) or app_vars.app_name in user.client_name:
if app_vars.app_name in user.client_name:
raise errors.AccessDeniedError("")
elif user.is_banned:
raise errors.AccessDeniedError(
self.translator.translate("You are banned"),
)
elif user.channel.id != self.ttclient.channel.id:
raise errors.AccessDeniedError(
self.translator.translate("You are not in bot's channel"),
)
elif self.locked:
raise errors.AccessDeniedError(
self.translator.translate("Bot is locked"),
)
elif command in self.config.general.blocked_commands:
raise errors.AccessDeniedError(
self.translator.translate("This command is blocked"),
)
else:
return True
else:
return True
def get_command(self, command: str, user: Optional[User]) -> Any:
if command in self.commands_dict:
return self.commands_dict[command]
elif (
(user is not None)
and (user.is_admin or user.type == UserType.Admin)
and command in self.admin_commands_dict
):
return self.admin_commands_dict[command]
else:
raise errors.UnknownCommandError()
def help(self, arg: str, user: User) -> str:
if arg:
if arg in self.commands_dict:
return "{} {}".format(arg, self.commands_dict[arg](self).help)
elif user.is_admin and arg in self.admin_commands_dict:
return "{} {}".format(arg, self.admin_commands_dict[arg](self).help)
else:
return self.translator.translate("Unknown command")
else:
help_strings: List[str] = []
for i in list(self.commands_dict):
help_strings.append(self.help(i, user))
if user.is_admin:
for i in list(self.admin_commands_dict):
help_strings.append(self.help(i, user))
return "\n".join(help_strings)
def parse_command(self, text: str) -> Tuple[str, str]:
text = text.strip()
try:
command = re.findall(re_command, text.split(" ")[0].lower())[0]
except IndexError:
raise errors.ParseCommandError()
arg = " ".join(text.split(" ")[1::])
return command, arg
def split_arg(self, arg: str) -> List[str]:
args = re.split(re_arg_split, arg)
for i, arg in enumerate(args):
args[i] = args[i].strip().replace("\\|", "|")
return args
class ScheduledCommandProcessor(CommandProcessor):
"""Command processor, specifically tailored for scheduled tasks. Takes a CronEntry instead of a message."""
def __init__(self, bot: Bot):
super().__init__(bot)
def __call__(self, task: CronEntryModel) -> None:
command_thread = Thread(target=self._run, args=(task,))
command_thread.start()
def _run(self, task: CronEntryModel) -> None:
"""Takes a cron task and runs it. Doesn't provide user to the command, doesn't check access (as there is no user to check). Logs errors and reports them to the channel also if that is enabled. Cron tasks can run admin commands."""
try:
command_name, arg = self.parse_command(task.command)
if (
command_name in self.commands_dict
or command_name in self.admin_commands_dict
):
command_class = self.get_command(command_name, None)
command = command_class(self)
self.current_command_id = id(command)
result = command(arg, None)
if result:
logger.info(
f"Successfully ran cron command '{task.command}; result: {result}"
)
self.ttclient.send_message(
result,
message.user,
)
except errors.InvalidArgumentError:
log.error(
f"Invalid argument for scheduled command '{task.command}'; cron pattern: {task.pattern}"
)
if self.config.general.send_channel_messages:
self.ttclient.send_message(
self.translator.translate(
f"Scheduled command '{task.command}' failed: invalid argument"
),
type=2,
)
except errors.AccessDeniedError as e:
log.error(
f"Got access denied while running scheduled task '{task.command}'; cron pattern: {task.pattern}"
)
except (errors.ParseCommandError, errors.UnknownCommandError):
if self.config.general.send_channel_messages:
self.ttclient.send_message(
self.translator.translate(
f"Unknown scheduled command: '{task.command}'."
),
type=2,
)
except Exception as e:
logging.error(
f"Error running scheduled command '{task.command}", exc_info=True
)
if self.config.general.send_channel_messages:
self.ttclient.send_message(
self.translator.translate(
f"Error running scheduled command '{task.command}: {e}"
),
type=2,
)

View File

@ -0,0 +1,889 @@
from __future__ import annotations
import logging
import os
import subprocess
import sys
import time
from typing import Optional, TYPE_CHECKING
from queue import Empty
import humanize
from crontab import CronTab
from bot.commands.command import Command
from bot.config.models import CronEntryModel
from bot.player.enums import State
from bot import app_vars, errors
if TYPE_CHECKING:
from bot.TeamTalk.structs import User
class BlockCommandCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"+/-COMMAND Block or unblock command. +COMMAND add command to the block list. -COMMAND remove from it. Without a command shows the block list."
)
def __call__(self, arg: str, user: User) -> Optional[str]:
arg = arg.lower()
if len(arg) >= 1 and arg[1:] not in self.command_processor.commands_dict:
raise errors.InvalidArgumentError()
if not arg:
return (
", ".join(self.config.general.blocked_commands)
if self.config.general.blocked_commands
else self.translator.translate("the list is empty")
)
if arg[0] == "+":
if arg[1::] not in self.config.general.blocked_commands:
self.config.general.blocked_commands.append(arg[1::])
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"command \"{command}\" was blocked."
).format(command=arg[1::]),
user,
)
self.config_manager.save()
else:
self.translator.translate("This command is already added")
elif arg[0] == "-":
if arg[1::] in self.config.general.blocked_commands:
del self.config.general.blocked_commands[
self.config.general.blocked_commands.index(arg[1::])
]
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"command \"{command}\" was unblocked."
).format(command=arg[1::]),
user,
)
self.config_manager.save()
else:
return self.translator.translate("This command is not blocked")
else:
raise errors.InvalidArgumentError()
class ChangeGenderCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"GENDER Changes bot's gender. n neutral, m male, f female"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
try:
self.ttclient.change_gender(arg)
self.config.teamtalk.gender = arg
except KeyError:
raise errors.InvalidArgumentError()
self.config_manager.save()
class ChangeLanguageCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("LANGUAGE change player language")
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
self.translator.set_locale(arg)
self.config.general.language = arg
self.ttclient.change_status_text("")
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"the language has been changed to {language}."
).format(language=arg),
user,
)
self.config_manager.save()
except errors.LocaleNotFoundError:
return self.translator.translate("Incorrect language")
else:
return self.translator.translate(
"Current language: {current_language}. Available languages: {available_languages}."
).format(
current_language=self.translator.get_locale(),
available_languages=", ".join(self.translator.get_locales()),
)
class ChangeNicknameCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("NICKNAME Changes bot's nickname")
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.ttclient.change_nickname(arg)
self.config.teamtalk.nickname = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"nickname change to {newname}."
).format(newname=arg),
user,
)
else:
self.ttclient.change_nickname(app_vars.client_name)
self.config.teamtalk.nickname = app_vars.client_name
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"nickname set to default client ({newname})."
).format(newname=app_vars.client_name),
user,
)
self.config_manager.save()
class ClearCacheCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"r/f Clears bot's cache. r clears recents, f clears favorites, without an option clears the entire cache"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if not arg:
self.cache.recents.clear()
self.cache.favorites.clear()
self.cache_manager.save()
return self.translator.translate("Cache cleared")
elif arg == "r":
self.cache.recents.clear()
self.cache_manager.save()
return self.translator.translate("Recents cleared")
elif arg == "f":
self.cache.favorites.clear()
self.cache_manager.save()
return self.translator.translate("Favorites cleared")
class ShowDefaultServerCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Read config file and show default Server, Port, Username, Password, Channel, Channel password.")
def __call__(self, arg: str, user: User) -> Optional[str]:
return self.translator.translate(
"Server: {hostname}\n"
"TCP Port: {tcpport}\n"
"UDP Port: {udpport}\n"
"Username: {username}\n"
"Password: {password}\n"
"Channel: {channel}\n"
"Channel password: {channel_password}"
).format(
hostname=self.config.teamtalk.hostname,
tcpport=self.config.teamtalk.tcp_port,
udpport=self.config.teamtalk.udp_port,
username=self.config.teamtalk.username,
password=self.config.teamtalk.password,
channel=self.config.teamtalk.channel,
channel_password=self.config.teamtalk.channel_password,
)
class DefaultServerCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Server information.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.config.teamtalk.hostname = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Server change to {newhostname}."
).format(newhostname=arg),
user,
)
self.config_manager.save()
else:
return self.translator.translate("Default server can not be blank, Please specify default Server information!")
class DefaultTCPPortCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default TCP port information.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
tcpport = int(arg)
self.config.teamtalk.tcp_port = int(arg)
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default TCP port change to {newtcp_port}."
).format(newtcp_port=int(arg)),
user,
)
self.config_manager.save()
except ValueError:
raise errors.InvalidArgumentError
else:
return self.translator.translate("Default TCP port can not be blank, Please specify default TCP port!")
class DefaultUDPPortCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default UDP port information.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
udpport = int(arg)
self.config.teamtalk.udp_port = int(arg)
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default TCP port change to {newudp_port}."
).format(newudp_port=int(arg)),
user,
)
self.config_manager.save()
except ValueError:
raise errors.InvalidArgumentError
else:
return self.translator.translate("Default UDP port can not be blank, Please specify default UDP port!")
class DefaultUsernameCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Username information. Without any arguments, set default Username to blank.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.config.teamtalk.username = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Username change to {newusername}."
).format(newusername=arg),
user,
)
self.config_manager.save()
else:
self.config.teamtalk.username = ""
self.config_manager.save()
return self.translator.translate("Default username set to blank.")
class DefaultPasswordCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Password information. Without any arguments, set default password to blank.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.config.teamtalk.password = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Password change to {newpassword}."
).format(newpassword=arg),
user,
)
self.config_manager.save()
else:
self.config.teamtalk.password = ""
self.config_manager.save()
return self.translator.translate("Default Password set to blank.")
class DefaultChannelCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Channel information. Without any arguments, set default Channel to current channel.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.config.teamtalk.channel = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Channel change to {newchannel}, if your channel has a password, please set it manually!"
).format(newchannel=arg),
user,
)
self.config_manager.save()
else:
channel_id = str(self.ttclient.channel.id)
self.config.teamtalk.channel = channel_id
self.config_manager.save()
return self.translator.translate("Default Channel set to current channel, if your channel has a password, please set it manually!")
class DefaultChannelPasswordCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Channel password information. Without any arguments, set default Channel password to blank..'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.config.teamtalk.channel_password = arg
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Channel password change to {newchannel_password}."
).format(newchannel_password=arg),
user,
)
self.config_manager.save()
else:
self.config.teamtalk.channel_password = ""
self.config_manager.save()
return self.translator.translate("Default Channel password set to blank.")
class GetChannelIDCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Get current channel ID, will be useful for several things.")
def __call__(self, arg: str, user: User) -> Optional[str]:
return str(self.ttclient.channel.id)
class JoinChannelCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
'Join channel. first argument is channel name or id, second argument is password, split argument " | ", if password is undefined, don\'t type second argument'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
args = self.command_processor.split_arg(arg)
if not arg:
channel = self.config.teamtalk.channel
password = self.config.teamtalk.channel_password
elif len(args) == 2:
channel = args[0]
password = args[1]
else:
channel = arg
password = ""
if isinstance(channel, str) and channel.isdigit():
channel = int(channel)
try:
cmd = self.ttclient.join_channel(channel, password)
except ValueError:
return self.translator.translate("This channel does not exist")
while True:
try:
event = self.ttclient.event_success_queue.get_nowait()
if event.source == cmd:
break
else:
self.ttclient.event_success_queue.put(event)
except Empty:
pass
try:
error = self.ttclient.errors_queue.get_nowait()
if error.command_id == cmd:
return self.translator.translate(
"Error joining channel: {error}".format(error=error.message)
)
else:
self.ttclient.errors_queue.put(error)
except Empty:
pass
time.sleep(app_vars.loop_timeout)
""" class CreateChannelCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
'Create channel. first argument is channel name, second argument is parent channel ID, third argument is the channel topic, fourth argument is the channel password. Split arguments " | ", if password or topic is undefined, don\'t type fourth or third argument.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
args = self.command_processor.split_arg(arg)
if len(args) == 4:
channel_name = args[0]
parent_channel_id = int(args[1])
channel_topic = args[2]
channel_password = args[3]
elif len(args) == 3:
channel_name = args[0]
parent_channel_id = int(args[1])
channel_topic = args[2]
channel_password = ""
elif len(args) == 2:
channel_name = args[0]
parent_channel_id = int(args[1])
channel_topic = ""
channel_password = ""
else:
return self.help
"""
""" class TaskSchedulerCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Task scheduler")
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg[0] == "+":
self._add(arg[1::])
def _add(self, arg: str) -> None:
args = arg.split("|")
timestamp = self._get_timestamp(args[0])
task = []
for arg in args[1::]:
try:
command, arg = self.parse_command(message.text)
if self.check_access(message.user, command):
command = self.get_command(command, message.user)
task.append((command, arg))
except errors.AccessDeniedError as e:
return e
except (errors.ParseCommandError, errors.UnknownCommandError):
return self.translator.translate("Unknown command. Send \"start\" for help.")
except errors.InvalidArgumentError:
return self.help(command, message.user)
if timestamp in self.module_manager.task_scheduler.tasks:
self.module_manager.task_scheduler[timestamp].append(task)
else:
self.module_manager.task_scheduler.tasks[timestamp] = [task,]
def _get_timestamp(self, t):
return int(datetime.combine(datetime.today(), datetime.strptime(t, self.config["general"]["time_format"]).time()).timestamp())
"""
class VoiceTransmissionCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Enables or disables voice transmission")
def __call__(self, arg: str, user: User) -> Optional[str]:
if not self.ttclient.is_voice_transmission_enabled:
self.ttclient.enable_voice_transmission()
if self.player.state == State.Stopped:
self.ttclient.change_status_text(
self.translator.translate("Voice transmission enabled")
)
return self.translator.translate("Voice transmission enabled")
else:
self.ttclient.disable_voice_transmission()
if self.player.state == State.Stopped:
self.ttclient.change_status_text("")
return self.translator.translate("Voice transmission disabled")
class LockCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Locks or unlocks the bot")
def __call__(self, arg: str, user: User) -> Optional[str]:
self.command_processor.locked = not self.command_processor.locked
if self.command_processor.locked:
self.config.general.bot_lock = True
self.run_async(
self.ttclient.send_message,
self.translator.translate("Bot is now locked by administrator."),
user,
)
else:
self.config.general.bot_lock = False
self.run_async(
self.ttclient.send_message,
self.translator.translate("Bot is now unlocked by administrator."),
user,
)
self.config_manager.save()
return None
def on_startup(self) -> None:
self.command_processor.locked = self.config.general.bot_lock
class ChangeStatusCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("STATUS Changes bot's status")
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.ttclient.change_status_text(arg)
self.config.teamtalk.status = self.ttclient.status
self.run_async(
self.ttclient.send_message,
self.translator.translate("Status changed."),
user,
)
else:
self.config.teamtalk.status = ""
self.ttclient.change_status_text("")
self.run_async(
self.ttclient.send_message,
self.translator.translate("Status change to default."),
user,
)
self.config_manager.save()
class EventHandlingCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Enables or disables event handling")
def __call__(self, arg: str, user: User) -> Optional[str]:
self.config.teamtalk.event_handling.load_event_handlers = (
not self.config.teamtalk.event_handling.load_event_handlers
)
return (
self.translator.translate("Event handling is enabled")
if self.config.teamtalk.event_handling.load_event_handlers
else self.translator.translate("Event handling is disabled")
)
class SendChannelMessages(Command):
@property
def help(self) -> str:
return self.translator.translate(
"Send your message to current channel. Without any argument, turn or off Channel messages."
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
if self.config.general.send_channel_messages:
self.ttclient.send_message(
self .translator.translate("{message}.\nSender: {nickname}.").format(
message=arg, nickname=user.nickname),
type=2
)
elif not self.config.general.send_channel_messages:
return self.translator.translate(
"Please enable channel messages first"
)
else:
self.config.general.send_channel_messages = (not self.config.general.send_channel_messages)
return (self.translator.translate("Channel messages enabled")
if self.config.general.send_channel_messages
else self.translator.translate("Channel messages disabled")
)
self.config_manager.save()
class SendBroadcastMessages(Command):
@property
def help(self) -> str:
return self.translator.translate(
"Send broadcast message to all users. Without any argument, turn on or off Broadcast messages"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
if self.config.general.send_broadcast_messages:
self.ttclient.send_message(
self .translator.translate("{message}.\nSender: {nickname}.").format(
message=arg, nickname=user.nickname),
type=3
)
elif not self.config.general.send_broadcast_messages:
return self.translator.translate(
"Please enable broadcast messages first"
)
else:
self.config.general.send_broadcast_messages = (not self.config.general.send_broadcast_messages)
return (self.translator.translate("Broadcast messages enabled")
if self.config.general.send_broadcast_messages
else self.translator.translate("Broadcast messages disabled")
)
self.config_manager.save()
class SaveConfigCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Saves bot's configuration")
def __call__(self, arg: str, user: User) -> Optional[str]:
self.config_manager.save()
return self.translator.translate("Configuration saved")
class AdminUsersCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"+/-USERNAME Manage the list of administrators. +USERNAME add a user. -USERNAME remove it. Without an option show the list."
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
if arg[0] == "+":
self.config.teamtalk.users.admins.append(arg[1::])
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{username} is now admin in this player."
).format(username=arg[1::]),
user,
)
self.config_manager.save()
elif arg[0] == "-":
try:
del self.config.teamtalk.users.admins[
self.config.teamtalk.users.admins.index(arg[1::])
]
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{username} no longer admin in this player."
).format(username=arg[1::]),
user,
)
self.config_manager.save()
except ValueError:
return self.translator.translate(
"this user is not in the admin list"
)
else:
admins = self.config.teamtalk.users.admins.copy()
if len(admins) > 0:
if "" in admins:
admins[admins.index("")] = "<Anonymous>"
return ", ".join(admins)
else:
return self.translator.translate("the list is empty")
class BannedUsersCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"+/-USERNAME Manage the list of banned users. +USERNAME add a user. -USERNAME remove it. Without an option show the list."
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
if arg[0] == "+":
self.config.teamtalk.users.banned_users.append(arg[1::])
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{username} now banned"
).format(username=arg[1::], nickname=user.nickname),
user,
)
self.config_manager.save()
elif arg[0] == "-":
try:
del self.config.teamtalk.users.banned_users[
self.config.teamtalk.users.banned_users.index(arg[1::])
]
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{username} now unbanned."
).format(username=arg[1::]),
user,
)
self.config_manager.save()
except ValueError:
return self.translator.translate("this user is not banned")
else:
banned_users = self.config.teamtalk.users.banned_users.copy()
if len(banned_users) > 0:
if "" in banned_users:
banned_users[banned_users.index("")] = "<Anonymous>"
return ", ".join(banned_users)
else:
return self.translator.translate("the list is empty")
class QuitCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Quits the bot")
def __call__(self, arg: str, user: User) -> Optional[str]:
self.config_manager.save()
if self.player.state != State.Stopped:
volume = int(0)
if 0 <= volume <= self.config.player.max_volume:
self.player.set_volume(int(0))
self._bot.close()
class RestartCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Restarts the bot")
def __call__(self, arg: str, user: User) -> Optional[str]:
self.config_manager.save()
if self.player.state != State.Stopped:
volume = int(0)
if 0 <= volume <= self.config.player.max_volume:
self.player.set_volume(int(0))
self._bot.close()
args = sys.argv
if sys.platform == "win32":
subprocess.run([sys.executable] + args)
else:
args.insert(0, sys.executable)
os.execv(sys.executable, args)
class SchedulerCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"Controls the cron scheduler, allowing admins to toggle it and add / remove scheduled commands"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg == "toggle" or arg == "t":
return self.toggle()
elif arg.startswith("add "):
# pass in the string after our command:
add_argstr = arg[4:]
return self.add(add_argstr)
elif arg.startswith("rm "):
return self.remove(arg[3:])
elif arg.strip() == "list" or arg.strip() == "ls":
return self.list_tasks()
elif arg.strip() in ["help", "?", "h"]:
return """Unknown cr subcommand. Options:
toggle (shorten to t) - turn the scheduler on or off.
add - add a cron task.
Format is cron expression|command with arguments.
Remove - remove a task.
list or ls - list all scheduled tasks."""
else:
s: str = (
self.translator.translate(
"Scheduled tasks are enabled (disable with 'cr toggle')"
)
if self.config.schedule.enabled
else self.translator.translate(
"Scheduled tasks are disabled; enable with 'cr toggle'"
)
)
s += "\n" + self.list_tasks()
s += self.translator.translate(
"Commands: \ncr add cron expression|command\ncr rm #\ncr ls\ncr toggle"
)
return s
def toggle(self):
self.config.schedule.enabled = not self.config.schedule.enabled
if self.config.schedule.enabled:
self.reparse_patterns()
return (
self.translator.translate("Scheduler enabled.")
if self.config.schedule.enabled
else self.translator.translate("Scheduler disabled")
)
def reparse_patterns(self):
self._bot.cron_patterns = []
for entry in self.config.schedule.patterns:
logging.debug(
f"Parsing cron pattern '{entry.pattern}' and appending to list"
)
e = CronTab(entry.pattern)
self._bot.cron_patterns.append((e, entry))
def add(self, argstr: str) -> str:
# Our arg should be a cron expression, | and the command.
help_text = self.translator.translate(
"Incorrect format. Cron expression | command you want to run with arguments after"
)
if "|" not in argstr:
return help_text
args = argstr.split("|")
if len(args) != 2:
return help_text
cronexpr = args[0].strip()
cmd = args[1].strip()
try:
ct: CronTab = CronTab(cronexpr)
entry = CronEntryModel(pattern=cronexpr, command=cmd)
self.config.schedule.patterns.append(entry)
self.reparse_patterns()
return self.translator.translate("Task scheduled.")
except ValueError:
return self.translator.translate(
"Not a valid cron expression. Pleaes use cr expression-help for more details."
)
def remove(self, arg: str) -> str:
if arg == "":
return self.list_tasks()
try:
task_index = int(arg)
task_index -= 1
if task_index > len(self.config.schedule.patterns) or task_index < 0:
return self.translator.translate(
"Task number out of range - should be 1 to {}".format(len(l))
)
task = self.config.schedule.patterns.pop(task_index)
self.reparse_patterns()
return self.translator.translate(
f"Removed task #{task_index+1}, {task.pattern}: {task.command}"
)
except ValueError:
return self.translator.translate("Invalid task number")
def list_tasks(self) -> str:
if len(self.config.schedule.patterns) == 0:
return self.translator.translate("There are no scheduled tasks")
lines: list[str] = []
for idx, task in enumerate(self.config.schedule.patterns):
lines.append("Task #{}".format(idx + 1))
lines.append(" Cron pattern: {}".format(task.pattern))
lines.append(" command: {}".format(task.command))
c = CronTab(task.pattern)
lines.append(
"Pattern ran: {} ago;".format(
humanize.precisedelta(c.previous(default_utc=False))
)
)
lines.append(
"Next run in {}.".format(
humanize.precisedelta(c.next(default_utc=False))
)
)
s: str = ""
for l in lines:
s += l + "\n"
return s
return lines

32
bot/commands/command.py Normal file
View File

@ -0,0 +1,32 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING, Callable
from bot.commands.task_processor import Task
if TYPE_CHECKING:
from bot.commands import CommandProcessor
class Command:
def __init__(self, command_processor: CommandProcessor):
self._bot = command_processor.bot
self.cache = command_processor.cache
self.cache_manager = command_processor.cache_manager
self.command_processor = command_processor
self.config = command_processor.config
self.config_manager = command_processor.config_manager
self.module_manager = command_processor.module_manager
self.player = command_processor.player
self.periodic_player = command_processor.periodic_player
self.service_manager = command_processor.service_manager
self._task_processor = command_processor.task_processor
self.ttclient = command_processor.ttclient
self.translator = command_processor.translator
@property
def help(self) -> str:
return self.translator.translate("help text not found")
def run_async(self, func: Callable[..., None], *args: Any, **kwargs: Any) -> None:
self._task_processor.task_queue.put(Task(id(self), func, args, kwargs))

View File

@ -0,0 +1,30 @@
from __future__ import annotations
from threading import Thread
from queue import Queue
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from bot.commands import CommandProcessor
class Task:
def __init__(
self, command_id: int, function: Callable[..., None], args: Any, kwargs: Any
) -> None:
self.command_id = command_id
self.function = function
self.args = args
self.kwargs = kwargs
class TaskProcessor(Thread):
def __init__(self, command_processor: CommandProcessor) -> None:
super().__init__(daemon=True)
self.command_processor = command_processor
self.task_queue: Queue[Task] = Queue()
def run(self) -> None:
while True:
task = self.task_queue.get()
if task.command_id == self.command_processor.current_command_id:
task.function(*task.args, **task.kwargs)

View File

@ -0,0 +1,629 @@
from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING
import os
from bot.commands.command import Command
from bot.player.enums import Mode, State, TrackType
from bot.TeamTalk.structs import User, UserRight
from bot import errors, app_vars
if TYPE_CHECKING:
from bot.TeamTalk.structs import User
class HelpCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Shows command help")
def __call__(self, arg: str, user: User) -> Optional[str]:
return self.command_processor.help(arg, user)
class AboutCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Shows information about the bot")
def __call__(self, arg: str, user: User) -> Optional[str]:
return app_vars.client_name + "\n" + app_vars.about_text(self.translator)
class StartBottt(Command):
@property
def help(self) -> str:
return self.translator.translate("Shows greetings")
def __call__(self, arg: str, user: User) -> Optional[str]:
return app_vars.start_bottt(self.translator)
class ContactsBot(Command):
@property
def help(self) -> str:
return self.translator.translate("Shows contact information")
def __call__(self, arg: str, user: User) -> Optional[str]:
return app_vars.contacts_bot(self.translator)
class PlayPauseCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"QUERY Plays tracks found for the query. If no query is given, plays or pauses current track"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
self.run_async(
self.ttclient.send_message,
self.translator.translate("Searching..."),
user,
)
try:
track_list = self.service_manager.service.search(arg)
if self.config.general.send_channel_messages:
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{nickname} requested {request}"
).format(nickname=user.nickname, request=arg),
type=2,
)
self.run_async(self.player.play, track_list)
return self.translator.translate("Playing {}").format(
track_list[0].name
)
except errors.NothingFoundError:
return self.translator.translate("Nothing is found for your query")
except errors.ServiceError:
return self.translator.translate(
"The selected service is currently unavailable"
)
else:
if self.player.state == State.Playing:
self.run_async(self.player.pause)
elif self.player.state == State.Paused:
self.run_async(self.player.play)
class PlayUrlCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("URL Plays a stream from a given URL")
def __call__(self, arg: str, user: Optional[User]) -> Optional[str]:
if arg:
try:
tracks = self.module_manager.streamer.get(arg, user.is_admin if user is not None else True)
if user is not None and self.config.general.send_channel_messages:
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"{nickname} requested playing from a URL"
).format(nickname=user.nickname),
type=2,
)
self.run_async(self.player.play, tracks)
except errors.IncorrectProtocolError:
return self.translator.translate("Incorrect protocol")
except errors.ServiceError:
return self.translator.translate("Cannot process stream URL")
except errors.PathNotFoundError:
return self.translator.translate("The path cannot be found")
else:
raise errors.InvalidArgumentError
class StopCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Stops playback")
def __call__(self, arg: str, user: User) -> Optional[str]:
if self.player.state != State.Stopped:
volume = int(0)
if 0 <= volume <= self.config.player.max_volume:
self.player.set_volume(int(0))
self.player.stop()
self.player.set_volume(self.config.player.default_volume)
if self.config.general.send_channel_messages:
self.ttclient.send_message(
self.translator.translate("{nickname} stopped playback").format(
nickname=user.nickname
),
type=2,
)
else:
return self.translator.translate("Nothing is playing")
class VolumeCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"VALUE Set the volume between 0 and {max_volume}. If no value is specified, show the current volume level."
).format(max_volume=self.config.player.max_volume)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
volume = int(arg)
if 0 <= volume <= self.config.player.max_volume:
self.player.set_volume(int(arg))
self.config.player.default_volume = int(arg)
self.config_manager.save()
if self.config.general.send_channel_messages:
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"volume set to {volume}% by {nickname}"
).format(volume=int(arg), nickname=user.nickname),
type=2,
)
else:
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"volume is now {volume}%"
).format(volume=int(arg)),
user,
)
else:
raise ValueError
except ValueError:
raise errors.InvalidArgumentError
else:
return self.translator.translate("current volume is {volume}%").format(volume=self.player.volume)
class SeekBackCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"STEP Seeks current track backward. the default step is {seek_step} seconds"
).format(seek_step=self.config.player.seek_step)
def __call__(self, arg: str, user: User) -> Optional[str]:
if self.player.state == State.Stopped:
return self.translator.translate("Nothing is playing")
if arg:
try:
self.player.seek_back(float(arg))
except ValueError:
raise errors.InvalidArgumentError
else:
self.player.seek_back()
class SeekForwardCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"STEP Seeks current track forward. the default step is {seek_step} seconds"
).format(seek_step=self.config.player.seek_step)
def __call__(self, arg: str, user: User) -> Optional[str]:
if self.player.state == State.Stopped:
return self.translator.translate("Nothing is playing")
if arg:
try:
self.player.seek_forward(float(arg))
except ValueError:
raise errors.InvalidArgumentError
else:
self.player.seek_forward()
class NextTrackCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Plays next track")
def __call__(self, arg: str, user: User) -> Optional[str]:
try:
self.player.next()
return self.translator.translate("Playing {}").format(
self.player.track.name
)
except errors.NoNextTrackError:
return self.translator.translate("No next track")
except errors.NothingIsPlayingError:
return self.translator.translate("Nothing is playing")
class PreviousTrackCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Plays previous track")
def __call__(self, arg: str, user: User) -> Optional[str]:
try:
self.player.previous()
return self.translator.translate("Playing {}").format(
self.player.track.name
)
except errors.NoPreviousTrackError:
return self.translator.translate("No previous track")
except errors.NothingIsPlayingError:
return self.translator.translate("Nothing is playing")
class ModeCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"MODE Sets the playback mode. If no mode is specified, the current mode and a list of modes are displayed"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
self.mode_names = {
Mode.SingleTrack: self.translator.translate("Single Track"),
Mode.RepeatTrack: self.translator.translate("Repeat Track"),
Mode.TrackList: self.translator.translate("Track list"),
Mode.RepeatTrackList: self.translator.translate("Repeat track list"),
Mode.Random: self.translator.translate("Random"),
}
mode_help = self.translator.translate(
"Current mode: {current_mode}\n{modes}"
).format(
current_mode=self.mode_names[self.player.mode],
modes="\n".join(
[
"{value} {name}".format(name=self.mode_names[i], value=i.value)
for i in Mode.__members__.values()
]
),
)
if arg:
try:
mode = Mode(arg.lower())
if mode == Mode.Random:
self.player.shuffle(True)
if self.player.mode == Mode.Random and mode != Mode.Random:
self.player.shuffle(False)
self.player.mode = Mode(mode)
return self.translator.translate("Current mode: {mode}").format(
mode=self.mode_names[self.player.mode]
)
except ValueError:
return "Incorrect mode\n" + mode_help
else:
return mode_help
class ServiceCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"SERVICE Selects the service to play from, sv SERVICE h returns additional help. If no service is specified, the current service and a list of available services are displayed"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
args = arg.split(" ")
if args[0]:
service_name = args[0].lower()
if service_name not in self.service_manager.services:
return self.translator.translate("Unknown service.\n{}").format(
self.service_help
)
service = self.service_manager.services[service_name]
if len(args) == 1:
if not service.hidden and service.is_enabled:
self.service_manager.service = service
if service.warning_message:
return self.translator.translate(
"Current service: {}\nWarning: {}"
).format(service.name, service.warning_message)
return self.translator.translate("Current service: {}").format(
service.name
)
elif not service.is_enabled:
if service.error_message:
return self.translator.translate(
"Error: {error}\n{service} is disabled".format(
error=service.error_message,
service=service.name,
)
)
else:
return self.translator.translate(
"{service} is disabled".format(service=service.name)
)
elif len(args) >= 1:
if service.help:
return service.help
else:
return self.translator.translate(
"This service has no additional help"
)
else:
return self.service_help
@property
def service_help(self):
services: List[str] = []
for i in self.service_manager.services:
service = self.service_manager.services[i]
if not service.is_enabled:
if service.error_message:
services.append(
"{} (Error: {})".format(service.name, service.error_message)
)
else:
services.append("{} (Error)".format(service.name))
elif service.warning_message:
services.append(
self.translator.translate("{} (Warning: {})").format(
service.name, service.warning_message
)
)
else:
services.append(service.name)
help = self.translator.translate(
"Current service: {current_service}\nAvailable:\n{available_services}\nsend sv SERVICE h for additional help"
).format(
current_service=self.service_manager.service.name,
available_services="\n".join(services),
)
return help
class SelectTrackCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"NUMBER Selects track by number from the list of current results"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
number = int(arg)
if number > 0:
index = number - 1
elif number < 0:
index = number
else:
return self.translator.translate("Incorrect number")
self.player.play_by_index(index)
return self.translator.translate("Playing {} {}").format(
arg, self.player.track.name
)
except errors.IncorrectTrackIndexError:
return self.translator.translate("Out of list")
except errors.NothingIsPlayingError:
return self.translator.translate("Nothing is playing")
except ValueError:
raise errors.InvalidArgumentError
else:
if self.player.state != State.Stopped:
return self.translator.translate("Playing {} {}").format(
self.player.track_index + 1, self.player.track.name
)
else:
return self.translator.translate("Nothing is playing")
class SpeedCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"SPEED Sets playback speed from 0.25 to 4. If no speed is given, shows current speed"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if not arg:
return self.translator.translate("Current rate: {}").format(
str(self.player.get_speed())
)
else:
try:
self.player.set_speed(float(arg))
except ValueError:
raise errors.InvalidArgumentError()
class FavoritesCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"+/-NUMBER Manages favorite tracks. + adds the current track to favorites. - removes a track requested from favorites. If a number is specified after +/-, adds/removes a track with that number"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if user.username == "":
return self.translator.translate(
"This command is not available for guest users"
)
if arg:
if arg[0] == "+":
return self._add(user)
elif arg[0] == "-":
return self._del(arg, user)
else:
return self._play(arg, user)
else:
return self._list(user)
def _add(self, user: User) -> str:
if self.player.state != State.Stopped:
if user.username in self.cache.favorites:
self.cache.favorites[user.username].append(self.player.track.get_raw())
else:
self.cache.favorites[user.username] = [self.player.track.get_raw()]
self.cache_manager.save()
return self.translator.translate("Added")
else:
return self.translator.translate("Nothing is playing")
def _del(self, arg: str, user: User) -> str:
if (self.player.state != State.Stopped and len(arg) == 1) or len(arg) > 1:
try:
if len(arg) == 1:
self.cache.favorites[user.username].remove(self.player.track)
else:
del self.cache.favorites[user.username][int(arg[1::]) - 1]
self.cache_manager.save()
return self.translator.translate("Deleted")
except IndexError:
return self.translator.translate("Out of list")
except ValueError:
if not arg[1::].isdigit:
return self.help
return self.translator.translate("This track is not in favorites")
else:
return self.translator.translate("Nothing is playing")
def _list(self, user: User) -> str:
track_names: List[str] = []
try:
for number, track in enumerate(self.cache.favorites[user.username]):
track_names.append(
"{number}: {track_name}".format(
number=number + 1,
track_name=track.name if track.name else track.url,
)
)
except KeyError:
pass
if len(track_names) > 0:
return "\n".join(track_names)
else:
return self.translator.translate("The list is empty")
def _play(self, arg: str, user: User) -> Optional[str]:
try:
self.player.play(
self.cache.favorites[user.username], start_track_index=int(arg) - 1
)
except ValueError:
raise errors.InvalidArgumentError()
except IndexError:
return self.translator.translate("Out of list")
except KeyError:
return self.translator.translate("The list is empty")
class GetLinkCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Gets a direct link to the current track")
def __call__(self, arg: str, user: User) -> Optional[str]:
if self.player.state != State.Stopped:
url = self.player.track.original_url
if url:
shortener = self.module_manager.shortener
return shortener.get(url) if shortener else url
else:
return self.translator.translate("URL is not available")
else:
return self.translator.translate("Nothing is playing")
class RecentsCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"NUMBER Plays a track with the given number from a list of recent tracks. Without a number shows recent tracks"
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
self.player.play(
list(reversed(list(self.cache.recents))),
start_track_index=int(arg) - 1,
)
except ValueError:
raise errors.InvalidArgumentError()
except IndexError:
return self.translator.translate("Out of list")
else:
track_names: List[str] = []
for number, track in enumerate(reversed(self.cache.recents)):
if track.name:
track_names.append(f"{number + 1}: {track.name}")
else:
track_names.append(f"{number + 1}: {track.url}")
return (
"\n".join(track_names)
if track_names
else self.translator.translate("The list is empty")
)
class DownloadCommand(Command):
@property
def help(self) -> str:
return self.translator.translate(
"Downloads the current track and uploads it to the channel."
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if not (
self.ttclient.user.user_account.rights & UserRight.UploadFiles
== UserRight.UploadFiles
):
raise PermissionError(
self.translator.translate("Cannot upload file to channel")
)
if self.player.state != State.Stopped:
track = self.player.track
if track.url and (
track.type == TrackType.Default or track.type == TrackType.Local
):
self.module_manager.uploader(self.player.track, user)
return self.translator.translate("Downloading...")
else:
return self.translator.translate("Live streams cannot be downloaded")
else:
return self.translator.translate("Nothing is playing")
class ChangeLogCommand(Command):
@property
def help(self) -> str:
return self.translator.translate("Show the change log.")
def __call__(self, arg: str, user: User) -> Optional[str]:
filename = "changelog.txt"
if os.path.exists(filename):
if os.path.getsize(filename ) == 0:
return self.translator.translate("Change log is not available. Contact the administrator.")
else:
f = open(filename, "r")
f = f.read()
for line in f:
return self.translator.translate(f)
f.close()
else:
return self.translator.translate("Change log is not available. Contact the administrator.")
class DefaultSeekStepCommand(Command):
@property
def help(self):
return self.translator.translate(
'Change default Seek step for player.'
)
def __call__(self, arg: str, user: User) -> Optional[str]:
if arg:
try:
seekstep = float(arg)
self.config.player.seek_step = float(arg)
self.run_async(
self.ttclient.send_message,
self.translator.translate(
"Default Seek step change to {newseek_step} second."
).format(newseek_step=float(arg)),
user,
)
self.config_manager.save()
except ValueError:
raise errors.InvalidArgumentError
else:
return self.translator.translate("Default Seek step can not be blank, Please specify default Seek step!")

65
bot/config/__init__.py Normal file
View File

@ -0,0 +1,65 @@
import json
import os
import sys
from typing import Any, Dict, Optional
from bot import app_vars, utils
from bot.config.models import ConfigModel
from bot.migrators import config_migrator
import portalocker
config_data_type = Dict[str, Any]
def save_default_file() -> None:
with open(utils.get_abs_path("config_default.json"), "w") as f:
json.dump(ConfigModel().dict(), f, indent=4, ensure_ascii=False)
class ConfigManager:
version = 1
def __init__(self, file_name: Optional[str]) -> None:
if file_name:
if os.path.isfile(file_name):
self.file_name = os.path.abspath(file_name)
config_dict = config_migrator.migrate(self, self._load())
self.config_dir = os.path.dirname(self.file_name)
self._lock()
else:
sys.exit("Incorrect configuration file path")
else:
self.config_dir = app_vars.directory
config_dict = {}
self.config: ConfigModel = ConfigModel(**config_dict)
def _dump(self, data: config_data_type):
with open(self.file_name, "w", encoding="UTF-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def _load(self):
with open(self.file_name, "r", encoding="UTF-8") as f:
try:
return json.load(f)
except json.decoder.JSONDecodeError as e:
sys.exit("Syntax error in configuration file: " + str(e))
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 close(self):
self.file_locker.release()
def save(self):
self.file_locker.release()
self._dump(self.config.dict())
self.file_locker.acquire()

127
bot/config/models.py Normal file
View File

@ -0,0 +1,127 @@
from typing import Any, Dict, List, Union, Optional
from pydantic import BaseModel, validator
from crontab import CronTab
class GeneralModel(BaseModel):
language: str = "en"
send_channel_messages: bool = True
send_broadcast_messages: bool = False
bot_lock: bool = False
cache_file_name: str = "TTMediaBotCache.dat"
blocked_commands: List[str] = []
delete_uploaded_files_after: int = 300
time_format: str = r"%H:%M"
class SoundDevicesModel(BaseModel):
output_device: int = 0
input_device: int = 0
class PlayerModel(BaseModel):
default_volume: int = 50
max_volume: int = 100
volume_fading: bool = True
volume_fading_interval: float = 0.025
seek_step: int = 5
player_options: Dict[str, Any] = {}
class TeamTalkUserModel(BaseModel):
admins: List[str] = ["admin"]
banned_users: List[str] = []
class EventHandlingModel(BaseModel):
load_event_handlers: bool = False
event_handlers_file_name: str = "event_handlers.py"
class TeamTalkModel(BaseModel):
hostname: str = "localhost"
tcp_port: int = 10333
udp_port: int = 10333
encrypted: bool = False
nickname: str = "TTMediaBot"
status: str = ""
gender: str = "n"
username: str = ""
password: str = ""
channel: Union[int, str] = ""
channel_password: str = ""
license_name: str = ""
license_key: str = ""
reconnection_attempts: int = -1
reconnection_timeout: int = 10
users: TeamTalkUserModel = TeamTalkUserModel()
event_handling: EventHandlingModel = EventHandlingModel()
class VkModel(BaseModel):
enabled: bool = True
token: str = ""
class YtModel(BaseModel):
enabled: bool = True
class YamModel(BaseModel):
enabled: bool = True
token: str = ""
class ServicesModel(BaseModel):
default_service: str = "vk"
vk: VkModel = VkModel()
yam: YamModel = YamModel()
yt: YtModel = YtModel()
class LoggerModel(BaseModel):
log: bool = True
level: str = "INFO"
format: str = "%(levelname)s [%(asctime)s]: %(message)s in %(threadName)s file: %(filename)s line %(lineno)d function %(funcName)s"
mode: Union[int, str] = "FILE"
file_name: str = "TTMediaBot.log"
max_file_size: int = 0
backup_count: int = 0
class ShorteningModel(BaseModel):
shorten_links: bool = False
service: str = "clckru"
service_params: Dict[str, Any] = {}
class CronEntryModel(BaseModel, arbitrary_types_allowed=True):
# Text cron pattern; will be parsed with the crontab library and a configuration error raised if a pattern fails to parse
pattern: str = ""
# What to run when this cron entry matches
command: str = ""
@validator('pattern')
def cron_pattern_must_be_valid(cls, pattern):
if pattern != "":
# Add a try here later, with a message about which cron entry is invalid (pattern and command in output if I don't add name)
_entry = CronTab(pattern)
return pattern
class SchedulesModel(BaseModel):
enabled: bool = True
patterns: List[CronEntryModel] = []
class ConfigModel(BaseModel):
config_version: int = 0
general: GeneralModel = GeneralModel()
sound_devices: SoundDevicesModel = SoundDevicesModel()
player: PlayerModel = PlayerModel()
teamtalk: TeamTalkModel = TeamTalkModel()
services: ServicesModel = ServicesModel()
schedule: SchedulesModel = SchedulesModel()
logger: LoggerModel = LoggerModel()
shortening: ShorteningModel = ShorteningModel()

View File

@ -0,0 +1 @@
from .tt_player_connector import TTPlayerConnector, MinimalTTPlayerConnector

View File

@ -0,0 +1,110 @@
from __future__ import annotations
import logging
from threading import Thread
import time
from typing import TYPE_CHECKING
from bot.player import State
from bot import app_vars
if TYPE_CHECKING:
from bot import Bot
class TTPlayerConnector(Thread):
def __init__(self, bot: Bot):
super().__init__(daemon=True)
self.name = "TTPlayerConnector"
self.player = bot.player
self.ttclient = bot.ttclient
self.translator = bot.translator
def run(self):
last_player_state = State.Stopped
last_track_meta = {"name": None, "url": None}
self._close = False
while not self._close:
try:
if self.player.state != last_player_state:
last_player_state = self.player.state
if self.player.state == State.Playing:
self.ttclient.enable_voice_transmission()
last_track_meta = self.player.track.get_meta()
if self.player.track.name:
self.ttclient.change_status_text(
self.translator.translate(
"Playing: {track_name}"
).format(track_name=self.player.track.name)
)
else:
self.ttclient.change_status_text(
self.translator.translate(
"Playing: {stream_url}"
).format(stream_url=self.player.track.url)
)
elif self.player.state == State.Stopped:
self.ttclient.disable_voice_transmission()
self.ttclient.change_status_text("")
elif self.player.state == State.Paused:
self.ttclient.disable_voice_transmission()
if self.player.track.name:
self.ttclient.change_status_text(
self.translator.translate(
"Paused: {track_name}"
).format(track_name=self.player.track.name)
)
else:
self.ttclient.change_status_text(
self.translator.translate(
"Paused: {stream_url}"
).format(stream_url=self.player.track.url)
)
if (
self.player.track.get_meta() != last_track_meta
and last_player_state != State.Stopped
):
last_track_meta = self.player.track.get_meta()
self.ttclient.change_status_text(
"{state}: {name}".format(
state=self.ttclient.status.split(":")[0],
name=self.player.track.name,
)
)
except Exception:
logging.error("", exc_info=True)
time.sleep(app_vars.loop_timeout)
def close(self):
self._close = True
class MinimalTTPlayerConnector(Thread):
"""TT player connector, controls setting status and toggling audio transmission. This minimal varient doesn't change status text (as its for periodic / other announcements.
One other consideration is that this connector checks to see if the regular player is playing before stopping transmission when it's starting or stopping playback, and it doesn't keep track of (ha) track metadata.
"""
def __init__(self, bot: Bot):
super().__init__(daemon=True)
self.name = "MinimalTTPlayerConnector"
self.player = bot.player
self.periodic_player = bot.periodic_player
self.ttclient = bot.ttclient
self.translator = bot.translator
def run(self):
last_player_state = State.Stopped
self._close = False
while not self._close:
try:
if self.periodic_player.state != last_player_state:
last_player_state = self.periodic_player.state
if self.periodic_player.state == State.Playing and self.player.state != State.Playing:
self.ttclient.enable_voice_transmission()
elif self.periodic_player.state != State.Playing and self.player.state != State.Playing:
self.ttclient.disable_voice_transmission()
except Exception:
logging.error("", exc_info=True)
time.sleep(app_vars.loop_timeout)
def close(self):
self._close = True

78
bot/errors.py Normal file
View File

@ -0,0 +1,78 @@
class ParseCommandError(Exception):
pass
class AccessDeniedError(Exception):
pass
class UnknownCommandError(Exception):
pass
class InvalidArgumentError(Exception):
pass
class ServiceNotFoundError(Exception):
pass
class ServiceIsDisabledError(Exception):
pass
class ServiceError(Exception):
pass
class NothingFoundError(Exception):
pass
class NoNextTrackError(Exception):
pass
class NoPreviousTrackError(Exception):
pass
class IncorrectProtocolError(Exception):
pass
class PathNotFoundError(Exception):
pass
class IncorrectTrackIndexError(Exception):
pass
class NothingIsPlayingError(Exception):
pass
class IncorrectPositionError(Exception):
pass
class TTEventError(Exception):
pass
class ConnectionError(Exception):
pass
class LoginError(Exception):
pass
class LocaleNotFoundError(Exception):
pass
class JoinChannelError(Exception):
pass

59
bot/logger.py Normal file
View File

@ -0,0 +1,59 @@
from __future__ import annotations
from enum import Flag
import logging
from logging.handlers import RotatingFileHandler
import os
import sys
from typing import TYPE_CHECKING, Any, List
from bot import app_vars
if TYPE_CHECKING:
from bot import Bot
class Mode(Flag):
STDOUT = 1
FILE = 2
STDOUT_AND_FILE = STDOUT | FILE
def initialize_logger(bot: Bot) -> None:
config = bot.config.logger
logging.addLevelName(5, "PLAYER_DEBUG")
level = logging.getLevelName(config.level)
formatter = logging.Formatter(config.format)
handlers: List[Any] = []
try:
mode = (
Mode(config.mode)
if isinstance(config.mode, int)
else Mode.__members__[config.mode]
)
except KeyError:
sys.exit("Invalid log mode name")
if mode & Mode.FILE == Mode.FILE:
if bot.log_file_name:
file_name = bot.log_file_name
else:
file_name = config.file_name
if os.path.isdir(os.path.join(*os.path.split(file_name)[0:-1])):
file = file_name
else:
file = os.path.join(bot.config_manager.config_dir, file_name)
rotating_file_handler = RotatingFileHandler(
filename=file,
mode="a",
maxBytes=config.max_file_size * 1024,
backupCount=config.backup_count,
encoding="UTF-8",
)
rotating_file_handler.setFormatter(formatter)
rotating_file_handler.setLevel(level)
handlers.append(rotating_file_handler)
if mode & Mode.STDOUT == Mode.STDOUT:
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(formatter)
stream_handler.setLevel(level)
handlers.append(stream_handler)
logging.basicConfig(level=level, format=config.format, handlers=handlers)

View File

@ -0,0 +1 @@
from bot.migrators import cache_migrator, config_migrator

View File

@ -0,0 +1,42 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bot.cache import CacheManager, cache_data_type
def to_v1(cache_data: cache_data_type) -> cache_data_type:
return update_version(cache_data, 1)
migrate_functs = {1: to_v1}
def migrate(
cache_manager: CacheManager,
cache_data: cache_data_type,
) -> cache_data_type:
if "cache_version" not in cache_data:
cache_data = update_version(cache_data, 0)
elif (
not isinstance(cache_data["cache_version"], int)
or cache_data["cache_version"] > cache_manager.version
):
sys.exit("Error: invalid cache_version value")
elif cache_data["cache_version"] == cache_manager.version:
return cache_data
else:
for ver in migrate_functs:
if ver > cache_data["cache_version"]:
cache_data = migrate_functs[ver](cache_data)
cache_manager._dump(cache_data)
return cache_data
def update_version(cache_data: cache_data_type, version: int) -> cache_data_type:
_cache_data = {"cache_version": version}
_cache_data.update(cache_data)
return _cache_data

View File

@ -0,0 +1,37 @@
import sys
from bot.config import ConfigManager, config_data_type
def to_v1(config_data: config_data_type) -> config_data_type:
return update_version(config_data, 1)
migrate_functs = {1: to_v1}
def migrate(
config_manager: ConfigManager,
config_data: config_data_type,
) -> config_data_type:
if "config_version" not in config_data:
update_version(config_data, 0)
elif (
not isinstance(config_data["config_version"], int)
or config_data["config_version"] > config_manager.version
):
sys.exit("Error: invalid config_version value")
elif config_data["config_version"] == config_manager.version:
return config_data
else:
for ver in migrate_functs:
if ver > config_data["config_version"]:
config_data = migrate_functs[ver](config_data)
config_manager._dump(config_data)
return config_data
def update_version(config_data: config_data_type, version: int) -> config_data_type:
_config_data = {"config_version": version}
_config_data.update(config_data)
return _config_data

23
bot/modules/__init__.py Normal file
View File

@ -0,0 +1,23 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from bot.modules.uploader import Uploader
from bot.modules.shortener import Shortener
from bot.modules.streamer import Streamer
# from bot.modules.task_scheduler import TaskScheduler
if TYPE_CHECKING:
from bot import Bot
class ModuleManager:
def __init__(self, bot: Bot):
self.shortener = (
Shortener(bot.config.shortening)
if bot.config.shortening.shorten_links
else None
)
self.streamer = Streamer(bot)
# self.task_scheduler = TaskScheduler(bot)
self.uploader = Uploader(bot)

24
bot/modules/shortener.py Normal file
View File

@ -0,0 +1,24 @@
import logging
import pyshorteners
from bot.config.models import ShorteningModel
class Shortener:
def __init__(self, config: ShorteningModel) -> None:
self.shorten_links = config.shorten_links
self.shortener = pyshorteners.Shortener(**config.service_params)
if config.service not in self.shortener.available_shorteners:
logging.error("Unknown shortener service, this feature will be disabled")
self.shorten_links = False
self.shorten_service = getattr(self.shortener, config.service, None)
def get(self, url: str) -> str:
try:
if self.shorten_links:
return self.shorten_service.short(url)
except Exception:
logging.error("", exc_info=True)
self.shorten_links = False
return url

74
bot/modules/streamer.py Normal file
View File

@ -0,0 +1,74 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING, List
from urllib.parse import urlparse
from bot import errors
from bot.player.enums import TrackType
from bot.player.track import Track
if TYPE_CHECKING:
from bot import Bot
class Streamer:
def __init__(self, bot: Bot):
self.allowed_schemes: List[str] = ["http", "https", "rtmp", "rtsp"]
self.config = bot.config
self.service_manager = bot.service_manager
def get(self, url: str, is_admin: bool) -> List[Track]:
parsed_url = urlparse(url)
if parsed_url.scheme in self.allowed_schemes:
track = Track(url=url, type=TrackType.Direct)
fetched_data = [track]
for service in self.service_manager.services.values():
try:
if (
parsed_url.hostname in service.hostnames
or service.name == self.service_manager.fallback_service
):
fetched_data = service.get(url)
break
except errors.ServiceError:
continue
except Exception:
if service.name == self.service_manager.fallback_service:
return [
track,
]
if len(fetched_data) == 1 and fetched_data[0].url.startswith(
str(track.url)
):
return [
track,
]
else:
return fetched_data
elif is_admin:
if os.path.isfile(url):
track = Track(
url=url,
name=os.path.split(url)[-1],
format=os.path.splitext(url)[1],
type=TrackType.Local,
)
return [
track,
]
elif os.path.isdir(url):
tracks: List[Track] = []
for path, _, files in os.walk(url):
for file in sorted(files):
url = os.path.join(path, file)
name = os.path.split(url)[-1]
format = os.path.splitext(url)[1]
track = Track(
url=url, name=name, format=format, type=TrackType.Local
)
tracks.append(track)
return tracks
else:
raise errors.PathNotFoundError("")
else:
raise errors.IncorrectProtocolError("")

View File

@ -0,0 +1,32 @@
"""
from __future__ import annotations
import threading
import time
from typing import TYPE_CHECKING
from bot import app_vars
if TYPE_CHECKING:
from bot import Bot
class TaskScheduler(threading.Thread):
def __init__(self, bot: Bot):
super().__init__(daemon=True)
self.name = "SchedulerThread"
self.tasks = {}
# self.user = User(0, "", "", 0, 0, "", is_admin=True, is_banned=False)
def run(self):
while True:
for t in self.tasks:
if self.get_time() >= t:
task = self.tasks[t]
task[0](task[1], self.user)
time.sleep(app_vars.loop_timeout)
def get_time(self):
return int(round(time.time()))
"""

82
bot/modules/uploader.py Normal file
View File

@ -0,0 +1,82 @@
from __future__ import annotations
import threading
import time
import os
import tempfile
from typing import TYPE_CHECKING
from queue import Empty
from bot.player.track import Track
from bot.player.enums import TrackType
from bot.TeamTalk.structs import ErrorType, User
from bot import app_vars
if TYPE_CHECKING:
from bot import Bot
class Uploader:
def __init__(self, bot: Bot):
self.config = bot.config
self.ttclient = bot.ttclient
self.translator = bot.translator
def __call__(self, track: Track, user: User) -> None:
thread = threading.Thread(
target=self.run,
daemon=True,
args=(
track,
user,
),
)
thread.start()
def run(self, track: Track, user: User) -> None:
error_exit = False
if track.type == TrackType.Default:
temp_dir = tempfile.TemporaryDirectory()
file_path = track.download(temp_dir.name)
else:
file_path = track.url
command_id = self.ttclient.send_file(self.ttclient.channel.id, file_path)
file_name = os.path.basename(file_path)
while True:
try:
file = self.ttclient.uploaded_files_queue.get_nowait()
if file.name == file_name:
break
else:
self.ttclient.uploaded_files_queue.put(file)
except Empty:
pass
try:
error = self.ttclient.errors_queue.get_nowait()
if (
error.command_id == command_id
and error.type == ErrorType.MaxDiskusageExceeded
):
self.ttclient.send_message(
self.translator.translate("Error: {}").format(
"Max diskusage exceeded"
),
user,
)
error_exit = True
else:
self.ttclient.errors_queue.put(error)
except Empty:
pass
time.sleep(app_vars.loop_timeout)
time.sleep(app_vars.loop_timeout)
if track.type == TrackType.Default:
temp_dir.cleanup()
if error_exit:
return
if self.config.general.delete_uploaded_files_after > 0:
timeout = self.config.general.delete_uploaded_files_after
else:
return
time.sleep(timeout)
self.ttclient.delete_file(file.channel.id, file.id)

283
bot/player/__init__.py Normal file
View File

@ -0,0 +1,283 @@
from __future__ import annotations
import html
import logging
import time
from typing import Any, Dict, Callable, List, Optional, TYPE_CHECKING
import random
import mpv
from bot import errors
from bot.player.enums import Mode, State, TrackType
from bot.player.track import Track
from bot.sound_devices import SoundDevice, SoundDeviceType
if TYPE_CHECKING:
from bot import Bot
class Player:
def __init__(self, bot: Bot):
self.config = bot.config.player
self.cache = bot.cache
self.cache_manager = bot.cache_manager
mpv_options = {
"demuxer_lavf_o": "http_persistent=false",
"demuxer_max_back_bytes": 1048576,
"demuxer_max_bytes": 2097152,
"video": False,
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36",
"ytdl": False,
}
mpv_options.update(self.config.player_options)
try:
self._player = mpv.MPV(**mpv_options, log_handler=self.log_handler)
except AttributeError:
del mpv_options["demuxer_max_back_bytes"]
self._player = mpv.MPV(**mpv_options, log_handler=self.log_handler)
self._log_level = 5
self.track_list: List[Track] = []
self.track: Track = Track()
self.track_index: int = -1
self.state = State.Stopped
self.mode = Mode.TrackList
self.volume = self.config.default_volume
def initialize(self) -> None:
logging.debug("Initializing player")
logging.debug("Player initialized")
def run(self) -> None:
logging.debug("Registering player callbacks")
self.register_event_callback("end-file", self.on_end_file)
self._player.observe_property("metadata", self.on_metadata_update)
self._player.observe_property("media-title", self.on_metadata_update)
logging.debug("Player callbacks registered")
def close(self) -> None:
logging.debug("Closing player")
if self.state != State.Stopped:
self.stop()
self._player.terminate()
logging.debug("Player closed")
def play(
self,
tracks: Optional[List[Track]] = None,
start_track_index: Optional[int] = None,
) -> None:
if tracks != None:
self.track_list = tracks
if not start_track_index and self.mode == Mode.Random:
self.shuffle(True)
self.track_index = self._index_list[0]
self.track = self.track_list[self.track_index]
else:
self.track_index = start_track_index if start_track_index else 0
self.track = tracks[self.track_index]
self._play(self.track.url)
else:
self._player.pause = False
self._player.volume = self.volume
self.state = State.Playing
def pause(self) -> None:
self.state = State.Paused
self._player.pause = True
def stop(self) -> None:
self.state = State.Stopped
self._player.stop()
self.track_list = []
self.track = Track()
self.track_index = -1
def _play(self, arg: str, save_to_recents: bool = True) -> None:
if save_to_recents:
try:
if self.cache.recents[-1] != self.track_list[self.track_index]:
self.cache.recents.append(
self.track_list[self.track_index].get_raw()
)
except:
self.cache.recents.append(self.track_list[self.track_index].get_raw())
self.cache_manager.save()
self._player.pause = False
self._player.play(arg)
def next(self) -> None:
track_index = self.track_index
if len(self.track_list) > 0:
if self.mode == Mode.Random:
try:
track_index = self._index_list[
self._index_list.index(self.track_index) + 1
]
except IndexError:
track_index = 0
else:
track_index += 1
else:
track_index = 0
try:
self.play_by_index(track_index)
except errors.IncorrectTrackIndexError:
if self.mode == Mode.RepeatTrackList:
self.play_by_index(0)
else:
raise errors.NoNextTrackError()
def previous(self) -> None:
track_index = self.track_index
if len(self.track_list) > 0:
if self.mode == Mode.Random:
try:
track_index = self._index_list[
self._index_list.index(self.track_index) - 1
]
except IndexError:
track_index = len(self.track_list) - 1
else:
if track_index == 0 and self.mode != Mode.RepeatTrackList:
raise errors.NoPreviousTrackError
else:
track_index -= 1
else:
track_index = 0
try:
self.play_by_index(track_index)
except errors.IncorrectTrackIndexError:
if self.mode == Mode.RepeatTrackList:
self.play_by_index(len(self.track_list) - 1)
else:
raise errors.NoPreviousTrackError
def play_by_index(self, index: int) -> None:
if index < len(self.track_list) and index >= (0 - len(self.track_list)):
self.track = self.track_list[index]
self.track_index = self.track_list.index(self.track)
self._play(self.track.url)
self.state = State.Playing
else:
raise errors.IncorrectTrackIndexError()
def set_volume(self, volume: int) -> None:
volume = volume if volume <= self.config.max_volume else self.config.max_volume
self.volume = volume
if self.config.volume_fading:
n = 1 if self._player.volume < volume else -1
for i in range(int(self._player.volume), volume, n):
self._player.volume = i
time.sleep(self.config.volume_fading_interval)
else:
self._player.volume = volume
def get_speed(self) -> float:
return self._player.speed
def set_speed(self, arg: float) -> None:
if arg < 0.25 or arg > 4:
raise ValueError()
self._player.speed = arg
def seek_back(self, step: Optional[float] = None) -> None:
step = step if step else self.config.seek_step
if step <= 0:
raise ValueError()
try:
self._player.seek(-step, reference="relative")
except SystemError:
self.stop()
def seek_forward(self, step: Optional[float] = None) -> None:
step = step if step else self.config.seek_step
if step <= 0:
raise ValueError()
try:
self._player.seek(step, reference="relative")
except SystemError:
self.stop()
def get_duration(self) -> float:
return self._player.duration
"""def get_position(self) -> float:
return self._player.time_pos
def set_position(self, arg: float) -> None:
if arg < 0:
raise errors.IncorrectPositionError()
self._player.seek(arg, reference="absolute")"""
def get_output_devices(self) -> List[SoundDevice]:
devices: List[SoundDevice] = []
for device in self._player.audio_device_list:
devices.append(
SoundDevice(
device["description"], device["name"], SoundDeviceType.Output
)
)
return devices
def set_output_device(self, id: str) -> None:
self._player.audio_device = id
def shuffle(self, enable: bool) -> None:
if enable:
self._index_list = [i for i in range(0, len(self.track_list))]
random.shuffle(self._index_list)
else:
del self._index_list
def register_event_callback(
self, callback_name: str, callback_func: Callable[[mpv.MpvEvent], None]
) -> None:
self._player.event_callback(callback_name)(callback_func)
def log_handler(self, level: str, component: str, message: str) -> None:
logging.log(self._log_level, "{}: {}: {}".format(level, component, message))
def _parse_metadata(self, metadata: Dict[str, Any]) -> str:
stream_names = ["icy-name"]
stream_name = None
title = None
artist = None
for i in metadata:
if i in stream_names:
stream_name = html.unescape(metadata[i])
if "title" in i:
title = html.unescape(metadata[i])
if "artist" in i:
artist = html.unescape(metadata[i])
chunks: List[str] = []
chunks.append(artist) if artist else ...
chunks.append(title) if title else ...
chunks.append(stream_name) if stream_name else ...
return " - ".join(chunks)
def on_end_file(self, event: mpv.MpvEvent) -> None:
if self.state == State.Playing and self._player.idle_active:
if self.mode == Mode.SingleTrack or self.track.type == TrackType.Direct:
self.stop()
elif self.mode == Mode.RepeatTrack:
self.play_by_index(self.track_index)
else:
try:
self.next()
except errors.NoNextTrackError:
self.stop()
def on_metadata_update(self, name: str, value: Any) -> None:
if self.state == State.Playing and (
self.track.type == TrackType.Direct or self.track.type == TrackType.Local
):
metadata = self._player.metadata
try:
new_name = self._parse_metadata(metadata)
if not new_name:
new_name = html.unescape(self._player.media_title)
except TypeError:
new_name = html.unescape(self._player.media_title)
if self.track.name != new_name and new_name:
self.track.name = new_name

23
bot/player/enums.py Normal file
View File

@ -0,0 +1,23 @@
from enum import Enum
class State(Enum):
Stopped = "Stopped"
Playing = "Playing"
Paused = "Paused"
class Mode(Enum):
SingleTrack = "st"
RepeatTrack = "rt"
TrackList = "tl"
RepeatTrackList = "rtl"
Random = "rnd"
class TrackType(Enum):
Default = 0
Live = 1
Local = 2
Direct = 3
Dynamic = 4

111
bot/player/track.py Normal file
View File

@ -0,0 +1,111 @@
from __future__ import annotations
import copy
import os
from threading import Lock
from typing import Any, Dict, Optional, TYPE_CHECKING
from bot.player.enums import TrackType
from bot import utils
if TYPE_CHECKING:
from bot.services import Service
class Track:
format: str
type: TrackType
def __init__(
self,
service: str = "",
url: str = "",
name: str = "",
format: str = "",
extra_info: Optional[Dict[str, Any]] = None,
type: TrackType = TrackType.Default,
) -> None:
self.service = service
self.url = url
self.name = name
self.format = format
self.extra_info = extra_info
self.type = type
self._lock = Lock()
self._is_fetched = False
def download(self, directory: str) -> str:
service: Service = get_service_by_name(self.service)
file_name = self.name + "." + self.format
file_name = utils.clean_file_name(file_name)
file_path = os.path.join(directory, file_name)
service.download(self, file_path)
return file_path
def _fetch_stream_data(self):
if self.type != TrackType.Dynamic or self._is_fetched:
return
self._original_track = copy.deepcopy(self)
service: Service = get_service_by_name(self.service)
track = service.get(self._url, extra_info=self.extra_info, process=True)[0]
self.url = track.url
self.name = track.name
self._original_track.name = track.name
self.format = track.format
self.type = track.type
self.extra_info = track.extra_info
self._is_fetched = True
@property
def url(self) -> str:
with self._lock:
self._fetch_stream_data()
return self._url
@url.setter
def url(self, value: str) -> None:
self._url = value
@property
def original_url(self) -> str:
if self.extra_info and "webpage_url" in self.extra_info:
return self.extra_info["webpage_url"]
return self._url # fallback ke URL biasa
@property
def name(self) -> str:
with self._lock:
if not self._name:
self._fetch_stream_data()
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
def get_meta(self) -> Dict[str, Any]:
try:
return {"name": self.name, "url": self.url}
except:
return {"name": None, "url": ""}
def get_raw(self) -> Track:
raw = copy.deepcopy(self._original_track) if hasattr(self, "_original_track") else copy.deepcopy(self)
if raw.extra_info and "webpage_url" in raw.extra_info:
raw.url = raw.extra_info["webpage_url"]
return raw
def __bool__(self):
if self.service or self.url:
return True
else:
return False
def __getstate__(self) -> Dict[str, Any]:
state: Dict[str, Any] = self.__dict__.copy()
del state["_lock"]
return state
def __setstate__(self, state: Dict[str, Any]):
self.__dict__.update(state)
self._lock = Lock()

85
bot/services/__init__.py Normal file
View File

@ -0,0 +1,85 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import logging
from typing import Any, Dict, List, Optional, TYPE_CHECKING
import downloader
from bot import app_vars, errors
if TYPE_CHECKING:
from bot import Bot
from bot.player.track import Track
class Service(ABC):
name: str
is_enabled: bool
hidden: bool
hostnames: List[str]
error_message: str
warning_message: str
help: str
def download(self, track: Track, file_path: str) -> None:
downloader.download_file(track.url, file_path)
@abstractmethod
def get(
self,
url: str,
extra_info: Optional[Dict[str, Any]] = None,
process: bool = False,
) -> List[Track]:
...
@abstractmethod
def initialize(self) -> None:
...
@abstractmethod
def search(self, query: str) -> List[Track]:
...
from bot.services.vk import VkService
from bot.services.yam import YamService
from bot.services.yt import YtService
class ServiceManager:
def __init__(self, bot: Bot) -> None:
self.config = bot.config.services
self.services: Dict[str, Service] = {
"vk": VkService(bot, self.config.vk),
"yam": YamService(bot, self.config.yam),
"yt": YtService(bot, self.config.yt),
}
self.service: Service = self.services[self.config.default_service]
self.fallback_service = app_vars.fallback_service
import builtins
builtins.__dict__["get_service_by_name"] = self.get_service_by_name
def initialize(self) -> None:
logging.debug("Initializing services")
for service in self.services.values():
if not service.is_enabled:
continue
try:
service.initialize()
except errors.ServiceError as e:
service.is_enabled = False
service.error_message = str(e)
if self.service == service:
self.service = self.services[self.fallback_service]
logging.debug("Services initialized")
def get_service_by_name(self, name: str) -> Service:
try:
service = self.services[name]
if not service.is_enabled:
raise errors.ServiceIsDisabledError(service.error_message)
return service
except KeyError as e:
raise errors.ServiceNotFoundError(str(e))

150
bot/services/vk.py Normal file
View File

@ -0,0 +1,150 @@
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from urllib.parse import urlparse
if TYPE_CHECKING:
from bot import Bot
import mpv
import requests
import vk_api
from bot.config.models import VkModel
from bot.player.track import Track
from bot.services import Service as _Service
from bot import errors
class VkService(_Service):
def __init__(self, bot: Bot, config: VkModel) -> None:
self.bot = bot
self.config = config
self.name = "vk"
self.hostnames = [
"vk.com",
"www.vk.com",
"vkontakte.ru",
"www.vkontakte.ru",
"m.vk.com",
"m.vkontakte.ru",
]
self.is_enabled = config.enabled
self.error_message = ""
self.warning_message = ""
self.help = ""
self.format = "mp3"
self.hidden = False
def download(self, track: Track, file_path: str) -> None:
if ".m3u8" not in track.url:
super().download(track, file_path)
return
_mpv = mpv.MPV(
**{
"demuxer_lavf_o": "http_persistent=false",
"ao": "null",
"ao_null_untimed": True,
}
)
_mpv.play(track.url)
_mpv.record_file = file_path
while not _mpv.idle_active:
pass
_mpv.terminate()
def initialize(self) -> None:
http = requests.Session()
http.headers.update(
{
"User-agent": "VKAndroidApp/6.2-5091 (Android 9; SDK 28; samsungexynos7870; samsung j6lte; 720x1450)"
}
)
self._session = vk_api.VkApi(
token=self.config.token, session=http, api_version="5.89"
)
self.api = self._session.get_api()
try:
self.api.account.getInfo()
except (
vk_api.exceptions.ApiHttpError,
vk_api.exceptions.ApiError,
requests.exceptions.ConnectionError,
) as e:
logging.error(e)
raise errors.ServiceError(e)
def get(
self,
url: str,
extra_info: Optional[Dict[str, Any]] = None,
process: bool = False,
) -> List[Track]:
parsed_url = urlparse(url)
path = parsed_url.path[1::]
if path.startswith("video-"):
raise errors.ServiceError()
try:
if "music/" in path:
id = path.split("/")[-1]
ids = id.split("_")
o_id = ids[0]
p_id = ids[1]
audios = self.api.audio.get(owner_id=int(o_id), album_id=int(p_id))
elif "audio" in path:
audios = {
"count": 1,
"items": self.api.audio.getById(audios=[path[5::]]),
}
else:
object_info = self.api.utils.resolveScreenName(screen_name=path)
if object_info["type"] == "group":
id = -object_info["object_id"]
else:
id = object_info["object_id"]
audios = self.api.audio.get(owner_id=id, count=6000)
if "count" in audios and audios["count"] > 0:
tracks: List[Track] = []
for audio in audios["items"]:
if "url" not in audio or not audio["url"]:
continue
track = Track(
service=self.name,
url=audio["url"],
name="{} - {}".format(audio["artist"], audio["title"]),
format=self.format,
extra_info={"webpage_url": f"https://vk.com/audio{audio['owner_id']}_{audio['id']}"}
)
tracks.append(track)
if tracks:
return tracks
else:
raise errors.NothingFoundError()
else:
raise errors.NothingFoundError
except NotImplementedError as e:
print("vk get error")
print(e)
raise NotImplementedError()
def search(self, query: str) -> List[Track]:
results = self.api.audio.search(q=query, count=300, sort=0)
if "count" in results and results["count"] > 0:
tracks: List[Track] = []
for track in results["items"]:
if "url" not in track or not track["url"]:
continue
track = Track(
service=self.name,
url=track["url"],
name="{} - {}".format(track["artist"], track["title"]),
format=self.format,
extra_info={"webpage_url": f"https://vk.com/audio{track['owner_id']}_{track['id']}"}
)
tracks.append(track)
if tracks:
return tracks
else:
raise errors.NothingFoundError()
else:
raise errors.NothingFoundError()

150
bot/services/yam.py Normal file
View File

@ -0,0 +1,150 @@
from __future__ import annotations
import logging
import time
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from urllib.parse import urlparse
if TYPE_CHECKING:
from bot import Bot
from yandex_music import Client
from yandex_music.exceptions import UnauthorizedError, NetworkError
from bot.config.models import YamModel
from bot.player.enums import TrackType
from bot.player.track import Track
from bot.services import Service
from bot import errors
class YamService(Service):
def __init__(self, bot: Bot, config: YamModel):
self.bot = bot
self.config = config
self.name = "yam"
self.hostnames = ["music.yandex.ru"]
self.is_enabled = self.config.enabled
self.error_message = ""
self.warning_message = ""
self.help = ""
self.hidden = False
self.format = ".mp3"
def initialize(self):
self.api = Client(token=self.config.token)
try:
self.api.init()
except (UnauthorizedError, NetworkError) as e:
logging.error(e)
raise errors.ServiceError(e)
if not self.api.account_status().account.uid:
self.warning_message = self.bot.translator.translate(
"Token is not provided"
)
elif not self.api.account_status().plus["has_plus"]:
self.warning_message = self.bot.translator.translate(
"You don't have Yandex Plus"
)
def get(
self,
url: str,
extra_info: Optional[Dict[str, Any]] = None,
process: bool = False,
) -> List[Track]:
if not process:
parsed_data = urlparse(url)
path = parsed_data.path
if "/album/" in path and "/track/" in path:
split_path = path.split("/")
real_id = split_path[4] + ":" + split_path[2]
return self.get(None, extra_info={"track_id": real_id}, process=True)
elif "/album/" in path:
tracks = []
album = self.api.albums_with_tracks(path.split("/")[2])
if len(album.volumes) == 0 or len(album.volumes[0]) == 0:
raise errors.ServiceError()
for volume in album.volumes:
for track in volume:
tracks.append(
Track(
service=self.name,
extra_info={"track_id": track.track_id},
type=TrackType.Dynamic,
)
)
return tracks
if "/artist/" in path:
tracks = []
artist_tracks = self.api.artists_tracks(path.split("/")[2]).tracks
if len(artist_tracks) == 0:
raise errors.ServiceError()
for track in artist_tracks:
tracks.append(
Track(
service=self.name,
extra_info={"track_id": track.track_id},
type=TrackType.Dynamic,
)
)
return tracks
elif "users" in path and "playlist" in path:
tracks = []
split_path = path.split("/")
user_id = split_path[2]
kind = split_path[4]
playlist = self.api.users_playlists(kind=kind, user_id=user_id)
if playlist.track_count == 0:
raise errors.ServiceError()
for track in playlist.tracks:
tracks.append(
Track(
service=self.name,
extra_info={"track_id": track.track_id},
type=TrackType.Dynamic,
)
)
return tracks
else:
track = self.api.tracks(extra_info["track_id"])[0]
return [
Track(
service=self.name,
name="{} - {}".format(
" & ".join(track.artists_name()), track.title
),
url=track.get_download_info(get_direct_links=True)[0].direct_link,
type=TrackType.Default,
format=self.format,
)
]
def search(self, query: str) -> List[Track]:
tracks: List[Track] = []
found_tracks = self.api.search(text=query, nocorrect=True, type_="all").tracks
if found_tracks:
for track in found_tracks.results:
tracks.append(
Track(
service=self.name,
type=TrackType.Dynamic,
extra_info={"track_id": track.track_id},
)
)
found_podcast_episodes = self.api.search(
text=query, nocorrect=True, type_="podcast_episode"
).podcast_episodes
if found_podcast_episodes:
for podcast_episode in found_podcast_episodes.results:
tracks.append(
Track(
service=self.name,
type=TrackType.Dynamic,
extra_info={"track_id": podcast_episode.track_id},
)
)
if tracks:
return tracks
else:
raise errors.NothingFoundError("")

191
bot/services/yt.py Normal file
View File

@ -0,0 +1,191 @@
from __future__ import annotations
import logging
import time
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
if TYPE_CHECKING:
from bot import Bot
from yt_dlp import YoutubeDL
from yt_dlp.downloader import get_suitable_downloader
from bot.config.models import YtModel
from bot.player.enums import TrackType
from bot.player.track import Track
from bot.services import Service as _Service
from bot import errors
class YtService(_Service):
def __init__(self, bot: Bot, config: YtModel):
self.bot = bot
self.config = config
self.name = "yt"
self.hostnames = []
self.is_enabled = self.config.enabled
self.error_message = ""
self.warning_message = ""
self.help = ""
self.hidden = False
self._last_search = time.time()
self._search_interval = 1.0
self._search_results_cache = {}
self._track_cache = {}
self._cache_timeout = 300
API_KEY = 'AIzaSyAnXGFy067AjtuuySsldXi17ysOEQW_ssw'
try:
self.youtube_api = build("youtube", "v3", developerKey=API_KEY)
except Exception as e:
logging.error(f"Failed to initialize YouTube API: {str(e)}")
self.is_enabled = False
self.error_message = "YouTube API initialization failed"
def initialize(self):
self._ydl_config = {
"skip_download": True,
"format": "m4a/bestaudio/best[protocol!=m3u8_native]/best",
"socket_timeout": 5,
"logger": logging.getLogger("root"),
"cookiefile": "/home/ttbot/data/cookies.txt"
}
def download(self, track: Track, file_path: str) -> None:
try:
info = track.extra_info
if not info:
super().download(track, file_path)
return
with YoutubeDL(self._ydl_config) as ydl:
dl = get_suitable_downloader(info)(ydl, self._ydl_config)
dl.download(file_path, info)
except Exception as e:
logging.error(f"Download error: {str(e)}", exc_info=True)
raise errors.ServiceError("Download failed")
def get(
self,
url: str,
extra_info: Optional[Dict[str, Any]] = None,
process: bool = False,
) -> List[Track]:
if not (url or extra_info):
raise errors.InvalidArgumentError()
cache_key = url or extra_info.get('webpage_url', '') if extra_info else ''
if cache_key in self._track_cache and not process:
cache_time, track = self._track_cache[cache_key]
if time.time() - cache_time < self._cache_timeout:
return [track]
try:
with YoutubeDL(self._ydl_config) as ydl:
if process and extra_info:
info = ydl.extract_info(extra_info.get('webpage_url', url), process=False)
elif not extra_info:
info = ydl.extract_info(url, process=False)
else:
info = extra_info
info_type = info.get("_type")
if info_type == "url" and not info.get("ie_key"):
return self.get(info["url"], process=False)
elif info_type == "playlist":
tracks: List[Track] = []
for entry in info["entries"]:
data = self.get("", extra_info=entry, process=False)
tracks.extend(data)
return tracks
if not process:
track = Track(
service=self.name,
url=info.get('webpage_url', url) or url,
name=info.get('title', ''),
extra_info=info,
format=info.get('ext', ''),
type=TrackType.Dynamic
)
self._track_cache[cache_key] = (time.time(), track)
return [track]
stream = ydl.process_ie_result(info, download=False)
if "url" not in stream:
raise errors.ServiceError("No URL found in stream info")
title = stream.get("title", "")
if stream.get("uploader"):
title += f" - {stream['uploader']}"
track_type = TrackType.Live if stream.get("is_live") else TrackType.Default
track = Track(
service=self.name,
url=stream["url"],
name=title,
format=stream.get("ext", ""),
type=track_type,
extra_info=stream
)
self._track_cache[cache_key] = (time.time(), track)
return [track]
except Exception as e:
logging.error(f"Error getting track info: {str(e)}", exc_info=True)
raise errors.ServiceError(f"Failed to get track info: {str(e)}")
def search(self, query: str) -> List[Track]:
current_time = time.time()
if query in self._search_results_cache:
cache_time, results = self._search_results_cache[query]
if current_time - cache_time < self._cache_timeout:
return results.copy()
time_since_last = current_time - self._last_search
if time_since_last < self._search_interval:
time.sleep(self._search_interval - time_since_last)
try:
search_response = self.youtube_api.search().list(
q=query,
part="snippet",
maxResults=25,
type="video"
).execute()
tracks = []
for search_result in search_response.get("items", []):
if 'videoId' in search_result.get('id', {}):
video_url = f"https://www.youtube.com/watch?v={search_result['id']['videoId']}"
track = Track(
service=self.name,
url=video_url,
name=search_result["snippet"]["title"],
type=TrackType.Dynamic,
extra_info={"webpage_url": video_url, "title": search_result["snippet"]["title"]}
)
self._track_cache[video_url] = (current_time, track.get_raw())
tracks.append(track)
self._search_results_cache[query] = (current_time, tracks.copy())
self._last_search = current_time
if tracks:
return tracks
raise errors.NothingFoundError("No videos found")
except HttpError as e:
logging.error(f"YouTube API error: {str(e)}", exc_info=True)
raise errors.NothingFoundError(f"YouTube search failed: {str(e)}")
except Exception as e:
logging.error(f"Unexpected error in search: {str(e)}", exc_info=True)
raise errors.ServiceError(f"Search failed: {str(e)}")

51
bot/sound_devices.py Normal file
View File

@ -0,0 +1,51 @@
from __future__ import annotations
from enum import Enum
import logging
import sys
from typing import TYPE_CHECKING, Union
if TYPE_CHECKING:
from bot import Bot
class SoundDevice:
def __init__(self, name: str, id: Union[int, str], type: SoundDeviceType) -> None:
self.name = name
self.id = id
self.type = type
class SoundDeviceType(Enum):
Output = 0
Input = 1
class SoundDeviceManager:
def __init__(self, bot: Bot) -> None:
self.config = bot.config
self.output_device_index = self.config.sound_devices.output_device
self.input_device_index = self.config.sound_devices.input_device
self.player = bot.player
self.ttclient = bot.ttclient
self.output_devices = self.player.get_output_devices()
self.input_devices = self.ttclient.get_input_devices()
def initialize(self) -> None:
logging.debug("Initializing sound devices")
try:
self.player.set_output_device(
str(self.output_devices[self.output_device_index].id)
)
except IndexError:
error = "Incorrect output device index: " + str(self.output_device_index)
logging.error(error)
sys.exit(error)
try:
self.ttclient.set_input_device(
int(self.input_devices[self.input_device_index].id)
)
except IndexError:
error = "Incorrect input device index: " + str(self.input_device_index)
logging.error(error)
sys.exit(error)
logging.debug("Sound devices initialized")

32
bot/translator.py Normal file
View File

@ -0,0 +1,32 @@
import gettext
import os
from typing import List
from bot import app_vars, errors
class Translator:
def __init__(self, language: str) -> None:
self._locale = "en"
self.set_locale(language)
def get_locale(self) -> str:
return self._locale
def get_locales(self) -> List[str]:
return ["en"] + os.listdir(os.path.join(app_vars.directory, "locale"))
def set_locale(self, locale: str) -> None:
if locale in self.get_locales() or locale == "en":
self._locale = locale
self.translation = gettext.translation(
"TTMediaBot",
os.path.join(app_vars.directory, "locale"),
languages=[locale],
fallback=True,
)
else:
raise errors.LocaleNotFoundError()
def translate(self, message: str) -> str:
return self.translation.gettext(message)

28
bot/utils.py Normal file
View File

@ -0,0 +1,28 @@
import os
from typing import List, Tuple
from crontab import CronTab
from bot import app_vars
from bot.config.models import CronEntryModel
def clean_file_name(file_name: str) -> str:
for char in ["\\", "/", "%", "*", "?", ":", '"', "|"] + [
chr(i) for i in range(1, 32)
]:
file_name = file_name.replace(char, "_")
file_name = file_name.strip()
return file_name
def get_abs_path(file_name: str) -> str:
return os.path.join(app_vars.directory, file_name)
def sort_cron_tasks(
tasks: List[Tuple[CronTab, CronEntryModel]]
) -> List[CronEntryModel]:
"""Given a list of CronTask instances, return the same list, sorted by ascending next run time."""
# sort by item[0].next(), a function on CronTab instance
sorted_tasks = sorted(tasks, key=lambda t: t[0].next(default_utc=False))
return sorted_tasks

159
changelog.txt Normal file
View File

@ -0,0 +1,159 @@
This change log is written to find out the changes that have been made by Pandora, and the source code still refers to TTMediaBot.
07-04-2025
1. Some of the default TTMediabot languages were deleted. Things that are irrelevant because Pandorabox has changed a lot of things to be done. Currently there are only English and Indonesian languages.
2. Improve Indonesian Language, in reality, it must be re -brought up. 177 items? How wasteful is!
3. The default dockerfile from TTMediaBot must be kicked and replaced with a more stable one, instead of letting Pulseaudio jammed. Thank you to friends in the TTMediaBot Telegram Group, you are amazing!
4. Improfify YouTube Search and Youtube DLP.
05-04-2025
1. Get link command (gl) is now fix. Stop the temporary url that makes your screen full of insignificant spamming URLs, kick out!
2. Recent and Favorite Command (R) (F) should be able to store well. Don't worry about ASMR Audio or other pleasure audio, now hugged tightly.
3. Don't forget to buy us coffee and snacks, Starbucks!
29-12-2024
1. Fix pydantic error, fix mvp for stabilization and for windows.
16-06-2023
1. add the Scheduler Command to perform certain tasks using crontab. Thanks to someone who made it, I can't remember, but one of the members of the TTMediaBot group on telegram.
2. Fixed on connector players. Enjoy!
3. bump Version to 2.3.3.
30-05-2023
1. align the code with TTMediaBot.
2. add new command (cdid) to get channel ID, this will be useful to get channel ID instead of channel path to apply to config file.
3. The translate files still broke, i'm lazy :P. If someone wants to help me, catch me on contact page!
4. The download file from youtube it's should be faster now!
19-04-2022
1. Fix Indonesian translation for some strings not work.
2. now possible to start stop enable or disable services without logging in to the current user. Just copy file srb to /usr/sbin and type chnmod +x /usr/sbin/srb. if you want to restart service TTMediaBot with user root for example, you can type srb restart then answer all question.
15-04-2022
1. Indonesian translate should work fine now.
2. changed the bot version, adapted to the TTMediaBot source code.
3. Improve server security from SSH attacks.
Merge source code from TTMediaBot;
1. migrator functionality.
2. services strings translation fix.
3. localisation fixes.
4. fixed compile_locales.py.
5. trying to fix localisation system.
6. Update russian translation has been finished.
7. migrate improve, TTSDK version 5.8 is latest, some fixes.
8. yandex music playlist support. (Still can't be use because stuck on authorization).
9. str fix.
10. Yam: Gracefully handle network errors..
11. Remove useless typestubs..
06-04-2022
1. When stop player, restart bot and quit bot, player volume must be returned to 0. This will take effect while player is playing, this will depend on volume_fading in the config file.
02-04-2022
1. Fixing on the help section, on send message to channel.
2. added "sbf" command to change default seek step for player.
3. fixing typo month on the log.
01-04-2022
1. Removed the "bm" command. Combine enable/disable broadcast messages with the "bmsg" command.
2. Removed the "cm" command. Combine enable/disable channel messages with the "cmsg" command.
3. Removed "dcs" command, "dss" command, "dcc" command and "dsc" command.
4. Added "sds" command for showing server information like server host, TCP/UDP port, username, password, channel and channel password.
5. Added "cds" command to change default server host.
6. Added "cdt" and "cdd" commands to change default TCP and UDP port.
7. Added "cdu" and "cdup" commands to change default username and password.
8. Added "cdc" "cdcp" command to change default channel and channel password.
9. Now all Pandora users can view the change log with the "log" command. It is hoped that it will make it easier for users to know the changes.
31-03-2022
Merge source code from TTMediaBot;
1. changed authors list.
2. libmpv_win_downloader: Fix architecture selection..
3. fixed displaying input devices list.
4. requirements fix.
5. improved compile_locales.py, cleaned up development-requirements.txt.
6. Added yandex music service, but it can not play playlists, improved service_manager. (In pandora this service can't be use, because error can't login to yandex music).
15-02-2022
1. Auto save configuration has been added for some commands.
2. If the user restarts or quits, the bot will save the configuration automatically to the config file. Don't worry about losing your configuration!
3. Added "dcs" command. Change default server host, TCP/UDP port, username and password with one command.
4. Added "dcc" command. Change default channel and channel password with one command.
5. added "dss" and "dsc" command. shows default server information and default channel information.
6. If you set the volume, it will now be automatically saved to the config file. This is so that the bot restarts or lost connection, the bot will not return to the default volume configuration.
7. Modify the "cn" command. Now if the user sends the "cn" command without any arguments, the bot nickname will be returned to the client bot name.
8. Modify the "cs" command. If the user sends the "cs" command without any arguments, the bot will return to the default status.
18-01-2022
Merge source code from TTMediaBot;
1. Fix reconnection logic.
2. Fix status loss after reconnecting.
3. Support newer Windows builds of libmpv.
4. mpv.py: fix NameError while exiting with newer mpv versions.
5. Fix broken metadata updating on libmpv-2. We need to use observed properties, because the metadata-update event has been removed in mpv API v2.
6. Replace legacy urlretrieve with a custom requests-based download function. This should solve all certificate-related problems. Also, make a few cosmetic code changes.
02-12-2021
Merge source code from TTMediaBot;
1. Vk_auth: Fix wording.
26-11-2021
Merge source code from TTMediaBot;
1. Shortener module now will be loded if corresponding option is enabled.
2. update readme.
3. vk.com/audio links suport, changed vk_auth vk_api version.
14-11-2021
Merge source code from TTMediaBot;
1. vk tracks downloading fix.
2. trying fix vk.
3. Some fixes.
29-10-2021
Merge source code from TTMediaBot;
1. Changed the most importent function
26-09-2021
1. Added the "bm" command. allows bots to enable/disable broadcast messages.
2. Added "bmsg" command. Allows broadcast messages to be sent via bots, useful for iPhone and android users. (will working with admin rights)
3. Added "cmsg" command. Allows users to send messages to channels using bots. (will working with admin rights)
11-09-2021
1. Added the "start" command. To make this bot user friendly, add a greeting in the status and at the beginning of the command, instead of the "h" command.
2. Added the "contacts" command. Make it easy for all users to contact the administrator.
3. Change the "h" command. Now the "help" command will bring up all the commands that can be used in this bot.
4. Instead of "a" command to see About this bot, change it to "about" command.
5. Fix typo Mit license to MIT license in the section about this bot, also fix the placement of "\" in the code section about this bot.

80
config_default.json Normal file
View File

@ -0,0 +1,80 @@
{
"config_version": 0,
"general": {
"language": "en",
"send_channel_messages": false,
"send_broadcast_messages": true,
"bot_lock": false,
"cache_file_name": "TTMediaBotCache.dat",
"blocked_commands": [],
"delete_uploaded_files_after": 300,
"time_format": "%H:%M"
},
"sound_devices": {
"output_device": 1,
"input_device": 0
},
"player": {
"default_volume": 50,
"max_volume": 100,
"volume_fading": true,
"volume_fading_interval": 0.08,
"seek_step": 5,
"player_options": {}
},
"teamtalk": {
"hostname": "tt.infiartt.com",
"tcp_port": 1945,
"udp_port": 1945,
"encrypted": false,
"nickname": "🎶 player",
"status": "",
"gender": "n",
"username": "",
"password": "",
"channel": "",
"channel_password": "",
"license_name": "",
"license_key": "",
"reconnection_attempts": -1,
"reconnection_timeout": 10,
"users": {
"admins": [
"admin"
],
"banned_users": []
},
"event_handling": {
"load_event_handlers": false,
"event_handlers_file_name": "event_handlers.py"
}
},
"services": {
"default_service": "yt",
"vk": {
"enabled": true,
"token": "30638cf75e342a89fc8edca415f22e584015c7991abea763447d958d5533ef75ffa812cd2d21209df4d04"
},
"yam": {
"enabled": false,
"token": ""
},
"yt": {
"enabled": true
}
},
"logger": {
"log": true,
"level": "INFO",
"format": "%(levelname)s [%(asctime)s]: %(message)s in %(threadName)s file: %(filename)s line %(lineno)d function %(funcName)s",
"mode": "FILE",
"file_name": "TTMediaBot.log",
"max_file_size": 0,
"backup_count": 0
},
"shortening": {
"shorten_links": false,
"service": "clckru",
"service_params": {}
}
}

View File

@ -0,0 +1,8 @@
black
flake8
flake8-black
flake8-bandit
flake8-commas
flake8-eradicate
flake8-import-order
flake8-quotes

11
downloader.py Normal file
View File

@ -0,0 +1,11 @@
import requests
import shutil
def download_file(url: str, file_path: str) -> None:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"}
with requests.get(url, headers=headers, stream=True) as r:
try:
with open(file_path, "wb") as f:
shutil.copyfileobj(r.raw, f)
except Exception as e:
print(f"An error occurred while downloading the file: {e}")

View File

@ -0,0 +1,984 @@
# Indonesian (Indonesia) translations for TTMediaBot.
# Copyright (C) 2025 TTMediaBot-team
# This file is distributed under the same license as the TTMediaBot project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: TTMediaBot VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-07 19:17+0700\n"
"PO-Revision-Date: 2025-04-07 19:13+0700\n"
"Last-Translator: \n"
"Language: id_ID\n"
"Language-Team: Pandora's Team\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: D:/tanah/PandoraBox/bot/app_vars.py:11
msgid ""
"A media streaming bot for TeamTalk.\n"
"Authors: Amir Gumerov, Vladislav Kopylov, Beqa Gozalishvili, Kirill "
"Belousov.\n"
"Home page: https://github.com/gumerov-amir/TTMediaBot\n"
"License: MIT License"
msgstr ""
"Bot media streaming untuk TeamTalk.\n"
"Penulis: Amir Gumerov, Vladislav Kopylov, Beqa Gozalishvili, Kirill "
"Belousov.\n"
"Home page: https://github.com/gumerov-amir/TTMediaBot\n"
"License: MIT License"
#: D:/tanah/PandoraBox/bot/app_vars.py:19
msgid ""
"Hello there!\n"
"I'm PandoraBox, your go-to companion for discovering amazing songs and "
"audio through YouTube, Yandex Music, and VK.\n"
"Hosted on Pandora's server, I'm all set to bring audio magic to your "
"TeamTalk experience.\n"
"To get started, simply send me a private message with a specific command."
"\n"
"Here's how you can interact with me:\n"
"- help: Receive a handy guide to all the commands you can use, delivered "
"straight to your private message.\n"
"- contacts: Get contact information sent directly to your private "
"message.\n"
"- log: Check out the change log file.\n"
"If you encounter any issues or want to chat with us, feel free to reach "
"out.\n"
"Thank you for choosing our service, and have a fantastic day!"
msgstr ""
"Halo!\n"
"Saya PandoraBox, teman Anda untuk menemukan lagu dan audio yang luar "
"biasa melalui YouTube, Yandex Music, dan VK.\n"
"Di -host di server Pandora, saya siap untuk membawa audio keren ke "
"TeamTalk Anda.\n"
"Untuk memulai, cukup kirimkan pesan pribadi dengan perintah tertentu.\n"
"Inilah cara Anda dapat berinteraksi dengan saya:\n"
"- help: Menerima panduan praktis untuk semua perintah yang dapat Anda "
"gunakan, dikirim langsung ke pesan pribadi Anda.\n"
"- contacts: Dapatkan informasi kontak yang dikirim langsung ke pesan "
"pribadi Anda.\n"
"- log: Lihat file log perubahan.\n"
"Jika Anda menghadapi masalah atau ingin mengobrol dengan kami, jangan "
"ragu untuk menghubungi kami.\n"
"Terima kasih telah memilih layanan kami, dan semoga harimu fantastis!"
#: D:/tanah/PandoraBox/bot/app_vars.py:33
msgid ""
"If you encounter any issues with this bot, please reach out to our "
"dedicated technicians:\n"
"\n"
"- Muhammad:\n"
" - WhatsApp: https://api.whatsapp.com/send?phone=6282156978782\n"
" - Telegram: https://t.me/muha_aku\n"
"\n"
"- Rexya:\n"
" - WhatsApp: https://api.whatsapp.com/send?phone=6288222553434\n"
" - Email: rexya@infiartt.com\n"
"\n"
"Join the TTMediaBot Official Group on Telegram: "
"https://t.me/TTMediaBot_chat"
msgstr ""
"Jika Anda menghadapi masalah apa pun dengan bot ini, silakan hubungi "
"teknisi kami:\n"
"\n"
"- Muhammad:\n"
" - WhatsApp: https://api.whatsapp.com/send?phone=6282156978782\n"
" - Telegram: https://t.me/muha_aku\n"
"\n"
"- Rexya:\n"
" - WhatsApp: https://api.whatsapp.com/send?phone=6288222553434\n"
" - Email: rexya@infiartt.com\n"
"\n"
"Gabung dengan Grup Resmi TTMediaBot di Telegram: "
"https://t.me/TTMediaBot_chat"
#: D:/tanah/PandoraBox/bot/TeamTalk/__init__.py:150
msgid "PM \"start\" to get short help!"
msgstr "PM \"start\" untuk mendapatkan bantuan singkat!"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:122
msgid "Unknown command. Send \"start\" for help."
msgstr "Perintah tidak diketahui. Kirim \"Start\" untuk bantuan."
#: D:/tanah/PandoraBox/bot/commands/__init__.py:128
#: D:/tanah/PandoraBox/bot/modules/uploader.py:61
msgid "Error: {}"
msgstr "Kesalahan: {}"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:140
msgid "You are banned"
msgstr "Anda dilarang"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:144
msgid "You are not in bot's channel"
msgstr "Anda tidak berada di saluran bot"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:148
msgid "Bot is locked"
msgstr "Bot terkunci"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:152
msgid "This command is blocked"
msgstr "Perintah ini diblokir"
#: D:/tanah/PandoraBox/bot/commands/__init__.py:178
msgid "Unknown command"
msgstr "Perintah tidak diketahui"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:24
msgid ""
"+/-COMMAND Block or unblock command. +COMMAND add command to the block "
"list. -COMMAND remove from it. Without a command shows the block list."
msgstr ""
"+/- lakukan blokir atau unblokir perintah. +Perintah Tambahkan perintah "
"ke daftar blokir. -perintah hapus perintah dari daftar blokir. Tanpa "
"tanda apapun tunjukan daftar blokir."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:36
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:683
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:727
msgid "the list is empty"
msgstr "Daftar kosong"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:43
msgid "command \"{command}\" was blocked."
msgstr "Perintah \"{command}\" diblokir."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:50
msgid "This command is already added"
msgstr "Perintah ini sudah ditambahkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:58
msgid "command \"{command}\" was unblocked."
msgstr "Perintah \"{command}\" tidak diblokir."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:65
msgid "This command is not blocked"
msgstr "Perintah ini tidak diblokir"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:73
msgid "GENDER Changes bot's gender. n neutral, m male, f female"
msgstr "Rubah Jenis Kelamin Bot. n netral, m laki-laki, f perempuan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:89
msgid "LANGUAGE change player language"
msgstr "Ubah bahasa pemain"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:99
msgid "the language has been changed to {language}."
msgstr "Bahasa telah di ubah ke {language}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:106
msgid "Incorrect language"
msgstr "Bahasa tidak tersedia"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:108
msgid ""
"Current language: {current_language}. Available languages: "
"{available_languages}."
msgstr ""
"Bahasa saat ini: {current_language}. Bahasa yang tersedia: "
"{available_languages}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:119
msgid "NICKNAME Changes bot's nickname"
msgstr "Ubah nama panggilan bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:127
msgid "nickname change to {newname}."
msgstr "Nama panggilan telah di ubah ke {newname}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:137
msgid "nickname set to default client ({newname})."
msgstr "nama panggilan diatur ke klien default ({newname})."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:148
msgid ""
"r/f Clears bot's cache. r clears recents, f clears favorites, without an "
"option clears the entire cache"
msgstr ""
"R/F bersihkan cache terbaru dan favorit Bot. r membersihkan terbaru, f "
"membersihkan favorit, tanpa tanda apapun hapus seluruh cache "
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:157
msgid "Cache cleared"
msgstr "Cache dihapus"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:161
msgid "Recents cleared"
msgstr "Terbaru dibersihkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:165
msgid "Favorites cleared"
msgstr "Favorit dibersihkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:171
msgid ""
"Read config file and show default Server, Port, Username, Password, "
"Channel, Channel password."
msgstr ""
"Baca file konfigurasi dan tampilkan server default, port, nama pengguna, "
"kata sandi, saluran, kata sandi saluran."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:174
msgid ""
"Server: {hostname}\n"
"TCP Port: {tcpport}\n"
"UDP Port: {udpport}\n"
"Username: {username}\n"
"Password: {password}\n"
"Channel: {channel}\n"
"Channel password: {channel_password}"
msgstr ""
"Server: {hostname}\n"
"TCP Port: {tcpport}\n"
"UDP Port: {udpport}\n"
"Nama pengguna: {username}\n"
"Kata sandi: {password}\n"
"Saluran: {channel}\n"
"Kata sandi saluran: {channel_password}"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:196
msgid "Change default Server information."
msgstr "Ubah Informasi Server Default."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:205
msgid "Default Server change to {newhostname}."
msgstr "Server di ubah ke {newhostname}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:212
msgid ""
"Default server can not be blank, Please specify default Server "
"information!"
msgstr ""
"Server default tidak dapat kosong, harap tentukan informasi server "
"default!"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:218
msgid "Change default TCP port information."
msgstr "Ubah informasi TCP port default."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:229
msgid "Default TCP port change to {newtcp_port}."
msgstr "TCP Port di ubah ke {newtcp_port}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:238
msgid "Default TCP port can not be blank, Please specify default TCP port!"
msgstr "Port TCP default tidak dapat kosong, tentukan port TCP default!"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:244
msgid "Change default UDP port information."
msgstr "Ubah informasi UDP port default."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:255
msgid "Default TCP port change to {newudp_port}."
msgstr "UDP Port di ubah ke {newudp_port}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:264
msgid "Default UDP port can not be blank, Please specify default UDP port!"
msgstr "Port UDP default tidak dapat kosong, tentukan port UDP default!"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:270
msgid ""
"Change default Username information. Without any arguments, set default "
"Username to blank."
msgstr ""
"Ubah informasi nama pengguna. Tanpa argumen apa pun, atur nama pengguna "
"default menjadi kosong."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:279
msgid "Default Username change to {newusername}."
msgstr "Nama pengguna di ubah ke {newusername}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:288
msgid "Default username set to blank."
msgstr "Nama pengguna diatur ke kosong."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:294
msgid ""
"Change default Password information. Without any arguments, set default "
"password to blank."
msgstr ""
"Ubah informasi kata sandi. Tanpa argumen apa pun, atur kata sandi default"
" ke kosong."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:303
msgid "Default Password change to {newpassword}."
msgstr "Kata sandi di ubah ke {newpassword}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:312
msgid "Default Password set to blank."
msgstr "Kata sandi diatur ke kosong."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:318
msgid ""
"Change default Channel information. Without any arguments, set default "
"Channel to current channel."
msgstr ""
"Ubah informasi saluran. Tanpa argumen apa pun, atur saluran default ke "
"saluran saat ini."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:327
msgid ""
"Default Channel change to {newchannel}, if your channel has a password, "
"please set it manually!"
msgstr ""
"Saluran di ubah ke {newchannel}, Jika saluran anda memiliki kata sandi, "
"silakan atur secara manual!"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:337
msgid ""
"Default Channel set to current channel, if your channel has a password, "
"please set it manually!"
msgstr ""
"Saluran diatur ke saluran saat ini, jika saluran Anda memiliki kata "
"sandi, silakan atur secara manual!"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:343
msgid ""
"Change default Channel password information. Without any arguments, set "
"default Channel password to blank.."
msgstr ""
"Ubah informasi kata sandi saluran. Tanpa argumen apa pun, atur kata sandi"
" saluran default menjadi kosong.."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:352
msgid "Default Channel password change to {newchannel_password}."
msgstr "Kata sandi saluran di ubah ke {newchannel_password}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:361
msgid "Default Channel password set to blank."
msgstr "Kata sandi saluran diatur ke kosong."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:367
msgid "Get current channel ID, will be useful for several things."
msgstr "Dapatkan ID saluran saat ini, akan berguna untuk beberapa hal."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:376
msgid ""
"Join channel. first argument is channel name or id, second argument is "
"password, split argument \" | \", if password is undefined, don't type "
"second argument"
msgstr ""
"Gabung dengan saluran. Argumen pertama adalah nama atau ID saluran, "
"argumen kedua adalah kata sandi. Setiap argumen dipisahkan dengan tanda "
"\"|\", jika kata sandi tidak ditentukan, jangan ketik argumen kedua"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:396
msgid "This channel does not exist"
msgstr "Saluran ini tidak ada"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:410
msgid "Error joining channel: {error}"
msgstr "Kesalahan bergabung dengan saluran: {error}"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:487
msgid "Enables or disables voice transmission"
msgstr "Aktifkan atau nonaktifkan transmisi suara"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:494
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:496
msgid "Voice transmission enabled"
msgstr "Transmisi suara diaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:501
msgid "Voice transmission disabled"
msgstr "Transmisi suara dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:507
msgid "Locks or unlocks the bot"
msgstr "Kunci atau buka kunci bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:515
msgid "Bot is now locked by administrator."
msgstr "Bot dikunci oleh administrator."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:522
msgid "Bot is now unlocked by administrator."
msgstr "Bot dibuka oleh administrator."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:536
msgid "STATUS Changes bot's status"
msgstr "ubah status bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:544
msgid "Status changed."
msgstr "Status diubah."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:552
msgid "Status change to default."
msgstr "Status di ubah ke default."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:561
msgid "Enables or disables event handling"
msgstr "Aktifkan atau nonaktifkan penanganan acara"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:568
msgid "Event handling is enabled"
msgstr "Penanganan acara diaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:570
msgid "Event handling is disabled"
msgstr "Penanganan acara dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:577
msgid ""
"Send your message to current channel. Without any argument, turn or off "
"Channel messages."
msgstr ""
"Kirim pesan Anda ke saluran saat ini. Tanpa argumen apapun, aktifkan atau"
" nonaktifkan pesan saluran."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:585
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:613
msgid ""
"{message}.\n"
"Sender: {nickname}."
msgstr ""
"{message}.\n"
"pengirim: {nickname}."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:590
msgid "Please enable channel messages first"
msgstr "silahkan aktifkan pesan saluran terlebih dahulu"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:595
msgid "Channel messages enabled"
msgstr "Pesan saluran diaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:597
msgid "Channel messages disabled"
msgstr "Pesan saluran dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:605
msgid ""
"Send broadcast message to all users. Without any argument, turn on or off"
" Broadcast messages"
msgstr ""
"Kirim pesan siaran ke semua pengguna. Tanpa argumen apa pun, aktifkan "
"atau nonaktifkan pesan siaran"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:618
msgid "Please enable broadcast messages first"
msgstr "Silahkan aktifkan pesan siaran terlebih dahulu"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:623
msgid "Broadcast messages enabled"
msgstr "Pesan siaran diaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:625
msgid "Broadcast messages disabled"
msgstr "Pesan siaran dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:633
msgid "Saves bot's configuration"
msgstr "Simpan konfigurasi bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:637
msgid "Configuration saved"
msgstr "Konfigurasi disimpan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:643
msgid ""
"+/-USERNAME Manage the list of administrators. +USERNAME add a user. "
"-USERNAME remove it. Without an option show the list."
msgstr ""
"+/- Nama pengguna mengelola daftar administrator. +Nama pengguna "
"Tambahkan pengguna. -Nama pengguna hapus nama pengguna. Tanpa argumen "
"apapun, tunjukkan daftar administrator."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:653
msgid "{username} is now admin in this player."
msgstr "{username} sekarang admin di player ini."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:666
msgid "{username} no longer admin in this player."
msgstr "{username} tidak lagi admin di player ini."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:673
msgid "this user is not in the admin list"
msgstr "Pengguna ini tidak ada dalam daftar admin"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:689
msgid ""
"+/-USERNAME Manage the list of banned users. +USERNAME add a user. "
"-USERNAME remove it. Without an option show the list."
msgstr ""
"+/- Nama pengguna mengelola daftar pengguna yang dilarang. +Nama pengguna"
" Tambahkan pengguna. -Nama pengguna hapus pengguna. Tanpa argumen apapun,"
" tunjukkan daftar pengguna yang dilarang."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:699
msgid "{username} now banned"
msgstr "{username} sekarang dilarang"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:712
msgid "{username} now unbanned."
msgstr "{username} sekarang tidak dilarang."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:719
msgid "this user is not banned"
msgstr "Pengguna ini tidak dilarang"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:733
msgid "Quits the bot"
msgstr "Tutup bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:747
msgid "Restarts the bot"
msgstr "Mulai ulang bot"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:767
msgid ""
"Controls the cron scheduler, allowing admins to toggle it and add / "
"remove scheduled commands"
msgstr ""
"Mengontrol cron scheduler, memungkinkan admin untuk mengubahnya dan "
"menambahkan / menghapus perintah yang dijadwalkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:791
msgid "Scheduled tasks are enabled (disable with 'cr toggle')"
msgstr "Tugas Terjadwal diaktifkan (Nonaktifkan dengan 'CR Toggle')"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:795
msgid "Scheduled tasks are disabled; enable with 'cr toggle'"
msgstr "Tugas Terjadwal dinonaktifkan (aktifkan dengan 'CR Toggle')"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:800
msgid ""
"Commands: \n"
"cr add cron expression|command\n"
"cr rm #\n"
"cr ls\n"
"cr toggle"
msgstr ""
"Perintah: \n"
"cr tambahkan cron expression|Perintah\n"
"cr rm #\n"
"cr ls\n"
"cr toggle"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:810
msgid "Scheduler enabled."
msgstr "Penjadwal diaktifkan."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:812
msgid "Scheduler disabled"
msgstr "Penjadwal dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:826
msgid ""
"Incorrect format. Cron expression | command you want to run with "
"arguments after"
msgstr ""
"Format yang salah. Ekspresi cron | Perintah yang ingin Anda jalankan "
"dengan argumen setelahnya"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:841
msgid "Task scheduled."
msgstr "Tugas dijadwalkan."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:843
msgid ""
"Not a valid cron expression. Pleaes use cr expression-help for more "
"details."
msgstr ""
"Bukan ekspresi cron yang valid. Pleaes menggunakan CR Expression-Help "
"untuk detail lebih lanjut."
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:855
msgid "Task number out of range - should be 1 to {}"
msgstr "Nomor tugas di luar jangkauan - harus 1 hingga {}"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:863
msgid "Invalid task number"
msgstr "Nomor tugas tidak valid"
#: D:/tanah/PandoraBox/bot/commands/admin_commands.py:867
msgid "There are no scheduled tasks"
msgstr "Tidak ada tugas yang dijadwalkan"
#: D:/tanah/PandoraBox/bot/commands/command.py:29
msgid "help text not found"
msgstr "Bantuan teks tidak ditemukan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:17
msgid "Shows command help"
msgstr "Tunjukan daftar Perintah"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:26
msgid "Shows information about the bot"
msgstr "Tunjukan informasi tentang bot"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:35
msgid "Shows greetings"
msgstr "salam"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:44
msgid "Shows contact information"
msgstr "Tunjukan informasi kontak"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:53
msgid ""
"QUERY Plays tracks found for the query. If no query is given, plays or "
"pauses current track"
msgstr ""
"Putar trek yang ditemukan dari daftar pencarian. Jika tidak ada daftar "
"pencarian, putar atau jeda trek saat ini"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:61
msgid "Searching..."
msgstr "Mencari ..."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:69
msgid "{nickname} requested {request}"
msgstr "{nickname} Meminta memutarkan {request}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:75
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:227
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:244
msgid "Playing {}"
msgstr "Memutar {}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:79
msgid "Nothing is found for your query"
msgstr "Tidak ada yang ditemukan dari pencarian Anda"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:81
msgid "The selected service is currently unavailable"
msgstr "Layanan yang dipilih saat ini tidak tersedia"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:94
msgid "URL Plays a stream from a given URL"
msgstr "URL putar streaming dari URL yang diberikan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:103
msgid "{nickname} requested playing from a URL"
msgstr "{nickname} memutar streaming dari URL"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:110
msgid "Incorrect protocol"
msgstr "kesalahan Protokol"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:112
msgid "Cannot process stream URL"
msgstr "Tidak dapat memproses URL streaming"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:114
msgid "The path cannot be found"
msgstr "jalur tidak dapat ditemukan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:122
msgid "Stops playback"
msgstr "Hentikan pemutaran"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:133
msgid "{nickname} stopped playback"
msgstr "{nickname} menghentikan pemutaran"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:139
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:190
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:209
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:233
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:250
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:397
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:406
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:459
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:477
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:523
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:583
msgid "Nothing is playing"
msgstr "Tidak ada yang diputar"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:145
msgid ""
"VALUE Set the volume between 0 and {max_volume}. If no value is "
"specified, show the current volume level."
msgstr ""
"Atur volume antara 0 dan {max_volume}. Jika tidak ada nilai yang "
"ditentukan, tunjukkan level volume saat ini."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:160
msgid "volume set to {volume}% by {nickname}"
msgstr "Volume diatur ke {volume}% oleh {nickname}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:168
msgid "volume is now {volume}%"
msgstr "Volume {volume}%"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:178
msgid "current volume is {volume}%"
msgstr "Volume saat ini {volume}%"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:184
msgid "STEP Seeks current track backward. the default step is {seek_step} seconds"
msgstr "Mundur {seek_step} detik"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:203
msgid "STEP Seeks current track forward. the default step is {seek_step} seconds"
msgstr "maju {seek_step} detik"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:222
msgid "Plays next track"
msgstr "putar trek berikutnya"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:231
msgid "No next track"
msgstr "Tidak ada trek berikutnya"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:239
msgid "Plays previous track"
msgstr "putar trek sebelumnya"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:248
msgid "No previous track"
msgstr "Tidak ada trek sebelumnya"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:256
msgid ""
"MODE Sets the playback mode. If no mode is specified, the current mode "
"and a list of modes are displayed"
msgstr ""
"Atur Mode Playback. Jika tidak ada mode yang ditentukan, mode saat ini "
"dan daftar mode ditampilkan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:262
msgid "Single Track"
msgstr "Trek tunggal"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:263
msgid "Repeat Track"
msgstr "Ulangi trek"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:264
msgid "Track list"
msgstr "daftar trek"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:265
msgid "Repeat track list"
msgstr "Ulangi daftar trek"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:266
msgid "Random"
msgstr "Acak"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:268
msgid ""
"Current mode: {current_mode}\n"
"{modes}"
msgstr ""
"Mode saat ini: {current_mode}\n"
"{modes}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:287
msgid "Current mode: {mode}"
msgstr "Mode saat ini: {mode}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:299
msgid ""
"SERVICE Selects the service to play from, sv SERVICE h returns additional"
" help. If no service is specified, the current service and a list of "
"available services are displayed"
msgstr ""
"pilih layanan yang akan dimainkan, sv SERVICE h akan menampilkan bantuan "
"tambahan. Jika tidak ada layanan yang ditentukan, layanan saat ini dan "
"daftar layanan yang tersedia ditampilkan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:308
msgid ""
"Unknown service.\n"
"{}"
msgstr ""
"Layanan tidak diketahui.\n"
"{}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:316
msgid ""
"Current service: {}\n"
"Warning: {}"
msgstr ""
"Layanan saat ini: {}\n"
"Peringatan: {}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:319
msgid "Current service: {}"
msgstr "Layanan saat ini: {}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:325
msgid ""
"Error: {error}\n"
"{service} is disabled"
msgstr ""
"Kesalahan: {error}\n"
"{service} dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:332
msgid "{service} is disabled"
msgstr "{service} dinonaktifkan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:338
msgid "This service has no additional help"
msgstr "Layanan ini tidak memiliki bantuan tambahan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:358
msgid "{} (Warning: {})"
msgstr "{} (Peringatan: {})"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:364
msgid ""
"Current service: {current_service}\n"
"Available:\n"
"{available_services}\n"
"send sv SERVICE h for additional help"
msgstr ""
"Layanan saat ini: {current_service}\n"
"Tersedia:\n"
"{available_services}\n"
"kirim sv SERVICE h untuk bantuan tambahan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:376
msgid "NUMBER Selects track by number from the list of current results"
msgstr "Pilih trek dengan angka dari daftar hasil saat ini"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:389
msgid "Incorrect number"
msgstr "Angka salah"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:391
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:402
msgid "Playing {} {}"
msgstr "Memutar {} {}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:395
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:471
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:504
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:543
msgid "Out of list"
msgstr "Di luar daftar"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:412
msgid ""
"SPEED Sets playback speed from 0.25 to 4. If no speed is given, shows "
"current speed"
msgstr ""
"Atur kecepatan pemutaran dari 0.25 ke 4. Jika tidak ada kecepatan yang "
"diberikan, Tunjukan kecepatan saat ini"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:418
msgid "Current rate: {}"
msgstr "Tingkat saat ini: {}"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:431
msgid ""
"+/-NUMBER Manages favorite tracks. + adds the current track to favorites."
" - removes a track requested from favorites. If a number is specified "
"after +/-, adds/removes a track with that number"
msgstr ""
"+/- Nomor mengelola trek favorit. + Menambahkan trek saat ini ke favorit."
" - Menghapus trek yang diminta dari favorit. Jika angka ditentukan "
"setelah +/-, tambahkan/hapus trek dengan angka yang sudah ditentukan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:437
msgid "This command is not available for guest users"
msgstr "Perintah ini tidak tersedia untuk pengguna tamu"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:457
msgid "Added"
msgstr "Ditambahkan"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:469
msgid "Deleted"
msgstr "Dihapus"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:475
msgid "This track is not in favorites"
msgstr "Trek ini tidak ada dalam favorit"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:494
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:506
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:554
msgid "The list is empty"
msgstr "Daftar kosong"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:512
msgid "Gets a direct link to the current track"
msgstr "Dapatkan tautan langsung ke trek saat ini"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:521
msgid "URL is not available"
msgstr "URL tidak tersedia"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:529
msgid ""
"NUMBER Plays a track with the given number from a list of recent tracks."
" Without a number shows recent tracks"
msgstr ""
"Nomor putar trek dengan nomor yang sesuai dengan daftar trek terbaru. "
"Tanpa nomor tunjukan trek terbaru"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:561
msgid "Downloads the current track and uploads it to the channel."
msgstr "Unduh trek saat ini dan unggah ke saluran."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:571
msgid "Cannot upload file to channel"
msgstr "Tidak dapat mengunggah file ke saluran"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:579
msgid "Downloading..."
msgstr "Mengunduh ..."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:581
msgid "Live streams cannot be downloaded"
msgstr "Live streams tidak dapat diunduh"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:588
msgid "Show the change log."
msgstr "Tunjukkan log perubahan."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:594
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:602
msgid "Change log is not available. Contact the administrator."
msgstr "Log perubahan tidak tersedia. hubungi administrator."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:608
msgid "Change default Seek step for player."
msgstr "Ubah aturan detik maju/mundur pada player"
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:619
msgid "Default Seek step change to {newseek_step} second."
msgstr "Maju/mundur di ubah ke {newseek_step} detik."
#: D:/tanah/PandoraBox/bot/commands/user_commands.py:628
msgid "Default Seek step can not be blank, Please specify default Seek step!"
msgstr "Maju/mundur tidak dapat kosong, tentukan nilainya!"
#: D:/tanah/PandoraBox/bot/connectors/tt_player_connector.py:35
msgid "Playing: {track_name}"
msgstr "Memutar: {track_name}"
#: D:/tanah/PandoraBox/bot/connectors/tt_player_connector.py:41
msgid "Playing: {stream_url}"
msgstr "Memutar: {stream_url}"
#: D:/tanah/PandoraBox/bot/connectors/tt_player_connector.py:52
msgid "Paused: {track_name}"
msgstr "digeda: {track_name}"
#: D:/tanah/PandoraBox/bot/connectors/tt_player_connector.py:58
msgid "Paused: {stream_url}"
msgstr "dijeda: {stream_url}"
#: D:/tanah/PandoraBox/bot/services/yam.py:42
msgid "Token is not provided"
msgstr "Token tidak disediakan"
#: D:/tanah/PandoraBox/bot/services/yam.py:46
msgid "You don't have Yandex Plus"
msgstr "Anda tidak memiliki Yandex Plus"

2215
mpv.py Normal file

File diff suppressed because it is too large Load Diff

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
babel
beautifulsoup4
patool
portalocker
pydantic
pyshorteners
requests
vk-api
yandex_music
google-api-python-client
yt-dlp
crontab
humanize == 4.6

7
srb Normal file
View File

@ -0,0 +1,7 @@
echo "Enter the username for the service that you want to $1."
read user
echo "Enter the service name that will perform the above action."
read service
sudo -u ${user} XDG_RUNTIME_DIR="/run/user/$(id -u ${user})" systemctl --user $1 ${service}

View File

@ -0,0 +1,12 @@
[Unit]
Description=TTMediaBot
Requires=pulseaudio.service
[Service]
Type=simple
WorkingDirectory=/home/ttInfiArtt/TTMediaBot
ExecStart=/home/ttInfiArtt/TTMediaBot/TTMediaBot.sh -c /home/ttInfiArtt/TTMediaBot/config.json
Restart=on-failure
[Install]
WantedBy=default.target

View File

@ -0,0 +1,13 @@
[Unit]
Description=Pulseaudio Sound Service
Requires=pulseaudio.socket
[Service]
Type=notify
ExecStart=/usr/bin/pulseaudio --verbose --daemonize=no
ExecStartPost=/usr/bin/pactl load-module module-null-sink
Restart=on-failure
[Install]
Also=pulseaudio.socket
WantedBy=default.target

View File

@ -0,0 +1,10 @@
[Unit]
Description=Pulseaudio Sound System
[Socket]
Priority=6
Backlog=5
ListenStream=%t/pulse/native
[Install]
WantedBy=sockets.target

49
tools/compile_locales.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
cd = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
locale_path = os.path.join(cd, "locale")
pot_file_path = os.path.join(locale_path, "TTMediaBot.pot")
source_paths = [os.path.join(cd, "bot"), os.path.join(cd, "TTMediaBot.py")]
babel_prefix = "{} -m babel.messages.frontend".format(sys.executable)
locale_domain = "TTMediaBot"
def extract():
code = subprocess.call(
f"{babel_prefix} extract {' '.join(source_paths)} -o {pot_file_path} --keywords=translate -c translators: --copyright-holder=TTMediaBot-team --project=TTMediaBot",
shell=True,
)
if code:
sys.exit("Bable is not installed. please install all the requirements")
def update():
code = subprocess.call(
f"{babel_prefix} update -i {pot_file_path} -d {locale_path} -D {locale_domain} --update-header-comment --previous",
shell=True,
)
if code:
sys.exit(code)
def compile():
code = subprocess.call(
f"{babel_prefix} compile -d {locale_path} -D {locale_domain}", shell=True
)
if code:
sys.exit(code)
def main():
extract()
update()
compile()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
import bs4
import patoolib
import requests
import os
import platform
import re
import shutil
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.dirname(path)
sys.path.append(path)
import downloader
url = "https://sourceforge.net/projects/mpv-player-windows/files/libmpv/"
def download():
try:
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
r = requests.get(url, headers=headers)
r.raise_for_status() # raise an error if there was a problem with the request
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
return
page = bs4.BeautifulSoup(r.text, features="html.parser")
table = page.find("table")
if platform.architecture()[0][0:2] == "64":
l_ver = table.find("a", href=True, title=re.compile("x86_64")).get("title")
else:
l_ver = table.find("a", href=True, title=re.compile("i686")).get("title")
download_url = l_ver.replace("Click to download ", "https://excellmedia.dl.sourceforge.net/project/mpv-player-windows/libmpv/")
try:
downloader.download_file(download_url, os.path.join(os.getcwd(), "libmpv.7z"))
except Exception as e:
print(f"Error downloading file: {e}")
def extract():
try:
os.mkdir(os.path.join(os.getcwd(), "libmpv"))
except FileExistsError:
shutil.rmtree(os.path.join(os.getcwd(), "libmpv"))
os.mkdir(os.path.join(os.getcwd(), "libmpv"))
try:
patoolib.extract_archive(
os.path.join(os.getcwd(), "libmpv.7z"),
outdir=os.path.join(os.getcwd(), "libmpv"),
)
except Exception as e:
print(f"Error extracting file: {e}")
return
def move_file():
try:
source = os.path.join(os.getcwd(), "libmpv", "libmpv-2.dll")
dest = os.path.join(os.getcwd(), os.pardir) if os.path.basename(os.getcwd()) == "tools" else os.getcwd()
if not os.path.exists(source):
raise FileNotFoundError("The file libmpv-2.dll does not exist")
elif os.path.exists(os.path.join(dest, "libmpv-2.dll")):
os.remove(os.path.join(dest, "libmpv-2.dll"))
shutil.move(source, os.path.join(dest, "libmpv-2.dll"))
except (FileNotFoundError, FileExistsError, Exception) as e:
print(f"Error moving file: {e}")
def clean():
os.remove(os.path.join(os.getcwd(), "libmpv.7z"))
shutil.rmtree(os.path.join(os.getcwd(), "libmpv"))
def install():
if sys.platform != "win32":
sys.exit("This script should be run only on Windows")
print("Installing libmpv for Windows...")
print("Downloading latest libmpv version...")
download()
print("Downloaded")
print("extracting...")
extract()
print("extracted")
print("moving...")
move_file()
print("moved")
print("cleaning...")
clean()
print("cleaned.")
print("Installed, exiting.")
if __name__ == "__main__":
install()

117
tools/ttsdk_downloader.py Normal file
View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
import bs4
import patoolib
import requests
import os
import platform
import shutil
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.dirname(path)
sys.path.append(path)
import downloader
url = "https://bearware.dk/teamtalksdk"
def get_url_suffix_from_platform() -> str:
machine = platform.machine()
if sys.platform == "win32":
architecture = platform.architecture()
if machine == "AMD64" or machine == "x86":
if architecture[0] == "64bit":
return "win64"
else:
return "win32"
else:
sys.exit("Native Windows on ARM is not supported")
elif sys.platform == "darwin":
sys.exit("Darwin is not supported")
else:
if machine == "AMD64" or machine == "x86_64":
return "ubuntu18_x86_64"
elif "arm" in machine:
return "raspbian_armhf"
else:
sys.exit("Your architecture is not supported")
def download() -> None:
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
r = requests.get(url, headers=headers)
page = bs4.BeautifulSoup(r.text, features="html.parser")
# The last tested version series is v5.8x
versions = page.find_all("li")
version = [i for i in versions if "5.8" in i.text][-1].a.get("href")[0:-1]
download_url = (
url
+ "/"
+ version
+ "/"
+ "tt5sdk_{v}_{p}.7z".format(v=version, p=get_url_suffix_from_platform())
)
print("Downloading from " + download_url)
downloader.download_file(download_url, os.path.join(os.getcwd(), "ttsdk.7z"))
def extract() -> None:
try:
os.mkdir(os.path.join(os.getcwd(), "ttsdk"))
except FileExistsError:
shutil.rmtree(os.path.join(os.getcwd(), "ttsdk"))
os.mkdir(os.path.join(os.getcwd(), "ttsdk"))
patoolib.extract_archive(
os.path.join(os.getcwd(), "ttsdk.7z"), outdir=os.path.join(os.getcwd(), "ttsdk")
)
def move() -> None:
path = os.path.join(os.getcwd(), "ttsdk", os.listdir(os.path.join(os.getcwd(), "ttsdk"))[0])
libraries = ["TeamTalk_DLL", "TeamTalkPy"]
dest_dir = os.path.join(os.getcwd(), os.pardir) if os.path.basename(os.getcwd()) == "tools" else os.getcwd()
for library in libraries:
try:
os.rename(
os.path.join(path, "Library", library), os.path.join(dest_dir, library)
)
except OSError:
shutil.rmtree(os.path.join(dest_dir, library))
os.rename(
os.path.join(path, "Library", library), os.path.join(dest_dir, library)
)
try:
os.rename(
os.path.join(path, "License.txt"), os.path.join(dest_dir, "TTSDK_license.txt")
)
except FileExistsError:
os.remove(os.path.join(dest_dir, "TTSDK_license.txt"))
os.rename(
os.path.join(path, "License.txt"), os.path.join(dest_dir, "TTSDK_license.txt")
)
def clean() -> None:
os.remove(os.path.join(os.getcwd(), "ttsdk.7z"))
shutil.rmtree(os.path.join(os.getcwd(), "ttsdk"))
def install() -> None:
print("Installing TeamTalk sdk components")
print("Downloading latest sdk version")
download()
print("Downloaded. extracting")
extract()
print("Extracted. moving")
move()
print("moved. cleaning")
clean()
print("cleaned.")
print("Installed")
if __name__ == "__main__":
install()

134
tools/vk_auth.py Normal file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env python3
import requests
from getpass import getpass
import json
import os
class AuthenticationError(Exception):
pass
class PhoneValidationError(Exception):
pass
class TokenValidationError(Exception):
pass
client_id = "2274003"
client_secret = "hHbZxrka2uZ6jB1inYsH"
api_ver = "5.89"
scope = "all"
user_agent = "VKAndroidApp/6.2-5091 (Android 9; SDK 28; samsungexynos7870; samsung j6lte; 720x1450)"
api_url = "https://api.vk.com/method/"
receipt = "fkdoOMX_yqQ:APA91bHbLn41RMJmAbuFjqLg5K-QW7si9KajBGCDJxcpzbuvEcPIk9rwx5HWa1yo1pTzpaKL50mXiWvtqApBzymO2sRKlyRiWqqzjMTXUyA5HnRJZyXWWGPX8GkFxQQ4bLrDCcnb93pn"
def request_auth(login: str, password: str, scope: str = "", code: str = "") -> str:
if not (login or password):
raise ValueError
url = (
"https://oauth.vk.com/token?grant_type=password&client_id="
+ client_id
+ "&client_secret="
+ client_secret
+ "&username="
+ login
+ "&password="
+ password
+ "&v="
+ api_ver
+ "&2fa_supported=1&force_sms=1"
)
if scope:
url += "&scope=" + scope
if code:
url += "&code=" + code
headers = {"User-Agent": user_agent}
r = requests.get(url, headers=headers)
if r.status_code == 200 and "access_token" in r.text:
res = r.json()
access_token = res["access_token"]
return access_token
elif "need_validation" in r.text:
res = r.json()
sid = res["validation_sid"]
code = handle_2fa(sid)
access_token = request_auth(login, password, scope=scope, code=code)
return access_token
else:
raise AuthenticationError(r.text)
def handle_2fa(sid: str) -> str:
if not sid:
raise ValueError("No sid is given")
url = api_url + "auth.validatePhone?sid=" + sid + "&v=" + api_ver
headers = {"User-Agent": user_agent}
r = requests.get(url, headers=headers)
if r.status_code == 200:
print("Two factor authentication is required")
code = ""
while not code:
code = input("SMS code: ")
if len(code) != 6 or not code.isdigit():
print("SMS code must be a string of 6 digits")
continue
return code
else:
raise PhoneValidationError(r.text)
def validate_token(token: str) -> str:
if not (token):
raise ValueError("Required argument is missing")
url = api_url + "auth.refreshToken?access_token=" + token + "&receipt=" + receipt + "&v=" + api_ver
headers = {"User-Agent": user_agent}
r = requests.get(url, headers=headers)
if r.status_code == 200 and "token" in r.text:
res = r.json()
received_token = res["response"]["token"]
if not received_token:
raise TokenValidationError(r.text)
else:
return received_token
else:
raise TokenValidationError(r.text)
def main():
login = ""
password = ""
try:
print("VK Authentication Helper for TTMediaBot")
print()
print("Enter your VK credentials to continue")
while not login:
login = input("Phone, email or login: ")
while not password:
password = getpass("Password: ")
token = request_auth(login, password, scope=scope)
validated_token = validate_token(token)
y_or_n = input("Do you want to save the token to the configuration file? y/n")
if y_or_n == "y":
config_file = input("Configuration file path: ")
with open(config_file, "r") as f:
data = json.load(f)
data["services"]["vk"]["token"] = validated_token
with open(config_file, "w") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print("Your token has been successfully saved to the configuration file")
else:
print("Your VK token:")
print(validated_token)
except Exception as e:
print(e)
input("Press enter to continue")
if __name__ == "__main__":
main()

76
tools/yam_auth.py Normal file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
from getpass import getpass
import json
import os
import requests
CLIENT_ID = "23cabbbdc6cd418abb4b39c32c41195d"
CLIENT_SECRET = "53bc75238f0c4d08a118e51fe9203300"
USER_AGENT = "Yandex-Music-API"
HEADERS = {
"X-Yandex-Music-Client": "YandexMusicAndroid/23020251",
"USER_AGENT": USER_AGENT,
}
url = "https://oauth.yandex.ru/token"
def get_token(
username, password, grant_type="password", x_captcha_answer=None, x_captcha_key=None
):
data = {
"grant_type": grant_type,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"username": username,
"password": password,
}
if x_captcha_answer and x_captcha_key:
data.update(
{"x_captcha_answer": x_captcha_answer, "x_captcha_key": x_captcha_key}
)
try:
resp = requests.request("post", url, data=data, headers=HEADERS)
except requests.RequestException as e:
raise NetworkError(e)
if not (200 <= resp.status_code <= 299):
raise SystemError("Error")
json_data = json.loads(resp.content.decode("utf-8"))
return json_data["access_token"]
def main():
login = ""
password = ""
try:
print("Yandex music Authentication Helper for TTMediaBot")
print()
print("Enter your Yandex credentials to continue")
while not login:
login = input("email or login: ")
while not password:
password = getpass("Password: ")
token = get_token(login, password)
y_or_n = input("Do you want to save the token to the configuration file? y/n")
if y_or_n == "y":
config_file = input("Configuration file path: ")
with open(config_file, "r") as f:
data = json.load(f)
try:
data["services"]["yam"]["token"] = token
except KeyError:
data["services"]["yam"] = {"enabled": True, "token": token}
with open(config_file, "w") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print("Your token has been successfully saved to the configuration file")
else:
print("Your yandex music token:")
print(token)
except Exception as e:
print(e)
input("Press enter to continue")
if __name__ == "__main__":
main()

65
typestubs/mpv.pyi Normal file
View File

@ -0,0 +1,65 @@
from ctypes import *
from typing import Any, Callable, Dict, List, Optional
class MpvEventID(c_int):
NONE: int
SHUTDOWN: int
LOG_MESSAGE: int
GET_PROPERTY_REPLY: int
SET_PROPERTY_REPLY: int
COMMAND_REPLY: int
START_FILE: int
END_FILE: int
FILE_LOADED: int
TRACKS_CHANGED: int
TRACK_SWITCHED: int
IDLE: int
PAUSE: int
UNPAUSE: int
TICK: int
SCRIPT_INPUT_DISPATCH: int
CLIENT_MESSAGE: int
VIDEO_RECONFIG: int
AUDIO_RECONFIG: int
METADATA_UPDATE: int
SEEK: int
PLAYBACK_RESTART: int
PROPERTY_CHANGE: int
CHAPTER_CHANGE: int
class MpvEvent(Structure):
_fields_ = [
("event_id", MpvEventID),
("error", c_int),
("reply_userdata", c_ulonglong),
("data", c_void_p),
]
class MPV:
def __init__(
self,
*extra_mpv_flags: Any,
log_handler: Optional[Callable[[str, str, str], None]] = ...,
start_event_thread: bool = ...,
loglevel: Optional[str] = ...,
**extra_mpv_opts: Any
) -> None: ...
audio_device: str
audio_device_list: List[Dict[str, Any]]
idle_active: bool
duration: float
def event_callback(
self, *event_types: str
) -> Callable[[Callable[[MpvEvent], None]], None]: ...
media_title: str
metadata: Dict[str, Any]
pause: bool
def seek(
self, amount: float, reference: str = ..., precision: str = ...
) -> None: ...
speed: float
def stop(self, keep_playlist: bool = ...) -> None: ...
stream_record: str
def play(self, filename: str) -> None: ...
def terminate(self) -> None: ...
volume: int

9
typestubs/patoolib.pyi Normal file
View File

@ -0,0 +1,9 @@
from typing import Optional
def extract_archive(
archive: str,
verbosity: int = ...,
outdir: Optional[str] = ...,
program: Optional[str] = ...,
interactive: bool = ...,
) -> None: ...

View File

@ -0,0 +1,35 @@
"""
This type stub file was generated by pyright.
"""
class _Comparable:
"""Implements rich comparison if __lt__ and __eq__ are provided."""
def __gt__(self, other: _Comparable: _Comparable) -> bool:
...
def __le__(self, other: _Comparable) -> bool:
...
def __ne__(self, other: _Comparable) -> bool:
...
def __ge__(self, other: _Comparable) -> bool:
...
class Version(_Comparable):
def __init__(self, version: str) -> None:
...
def __str__(self) -> str:
...
def __repr__(self): # -> str:
...
def __lt__(self, other: Version) -> bool:
...
def __eq__(self, other: Version) -> bool:
...

View File

@ -0,0 +1,45 @@
from typing import Any, Dict
import requests
class Audio:
def get(
self,
owner_id: int = 1,
album_id: int = 1,
count: int = ...,
) -> Dict[str, Any]:
...
def search(self, q: str = "", count: int = 100, sort: int = 0) -> Dict[str, Any]:
...
class Account:
def getInfo(self) -> Dict[str, Any]:
...
class Utils:
def resolveScreenName(self, screen_name: str = "") -> Dict[str, Any]:
...
class Api:
account: Account
audio: Audio
utils: Utils
class VkApi:
def __init__(
self,
token: str = "",
session: requests.Session = ...,
api_version: str = "",
) -> None:
...
def get_api(self) -> Api:
...

View File

@ -0,0 +1,6 @@
class ApiHttpError(Exception):
pass
class ApiError(Exception):
pass

View File

@ -0,0 +1,7 @@
from typing import Any, Dict
class VideosSearch:
def __init__(
self, query: str, limit: int = ..., language: str = ..., region: str = ...
) -> None: ...
def result(self, mode: int = ...) -> Dict[str, Any]: ...

24
typestubs/yt_dlp.pyi Normal file
View File

@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Any, Dict, Optional
class YoutubeDL:
def __init__(
self, params: Optional[Dict[str, Any]] = ..., auto_init: bool = ...
) -> None: ...
def __enter__(self) -> YoutubeDL: ...
def __exit__(self) -> None: ...
def extract_info(
self,
url: Optional[str],
download: bool = ...,
ie_key: Optional[str] = ...,
extra_info: Optional[Dict[str, Any]] = ...,
process: bool = ...,
force_generic_extractor: bool = ...,
) -> Dict[str, Any]: ...
def process_ie_result(
self,
ie_result: Dict[str, Any],
download: bool = ...,
extra_info: Optional[Dict[str, Any]] = ...,
) -> Dict[str, Any]: ...