This commit is contained in:
commit
f331b10358
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.git/
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
|
4
.flake8
Normal file
4
.flake8
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
exclude = TeamTalkPy,mpv.py
|
||||||
|
inline-quotes = "
|
||||||
|
max-line-length = 88
|
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal 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
25
.github/workflows/build-nightly.yml
vendored
Normal 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
140
.gitignore
vendored
Normal 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
15
.vscode/launch.json
vendored
Normal 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
16
.vscode/settings.json
vendored
Normal 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
2
.yapfignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
TeamTalkPy
|
||||||
|
mpv.py
|
51
Dockerfile
Normal file
51
Dockerfile
Normal 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
21
LICENSE
Normal 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
76
README.md
Normal 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
64
TTMediaBot.py
Normal 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
7
TTMediaBot.sh
Normal 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
386
bot/TeamTalk/__init__.py
Normal 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
336
bot/TeamTalk/structs.py
Normal 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
214
bot/TeamTalk/thread.py
Normal 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
150
bot/__init__.py
Normal 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
54
bot/app_vars.py
Normal 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
74
bot/cache.py
Normal 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
267
bot/commands/__init__.py
Normal 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,
|
||||||
|
)
|
889
bot/commands/admin_commands.py
Normal file
889
bot/commands/admin_commands.py
Normal 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
32
bot/commands/command.py
Normal 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))
|
30
bot/commands/task_processor.py
Normal file
30
bot/commands/task_processor.py
Normal 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)
|
629
bot/commands/user_commands.py
Normal file
629
bot/commands/user_commands.py
Normal 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
65
bot/config/__init__.py
Normal 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
127
bot/config/models.py
Normal 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()
|
1
bot/connectors/__init__.py
Normal file
1
bot/connectors/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .tt_player_connector import TTPlayerConnector, MinimalTTPlayerConnector
|
110
bot/connectors/tt_player_connector.py
Normal file
110
bot/connectors/tt_player_connector.py
Normal 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
78
bot/errors.py
Normal 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
59
bot/logger.py
Normal 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)
|
1
bot/migrators/__init__.py
Normal file
1
bot/migrators/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from bot.migrators import cache_migrator, config_migrator
|
42
bot/migrators/cache_migrator.py
Normal file
42
bot/migrators/cache_migrator.py
Normal 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
|
37
bot/migrators/config_migrator.py
Normal file
37
bot/migrators/config_migrator.py
Normal 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
23
bot/modules/__init__.py
Normal 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
24
bot/modules/shortener.py
Normal 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
74
bot/modules/streamer.py
Normal 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("")
|
32
bot/modules/task_scheduler.py
Normal file
32
bot/modules/task_scheduler.py
Normal 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
82
bot/modules/uploader.py
Normal 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
283
bot/player/__init__.py
Normal 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
23
bot/player/enums.py
Normal 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
111
bot/player/track.py
Normal 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
85
bot/services/__init__.py
Normal 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
150
bot/services/vk.py
Normal 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
150
bot/services/yam.py
Normal 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
191
bot/services/yt.py
Normal 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
51
bot/sound_devices.py
Normal 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
32
bot/translator.py
Normal 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
28
bot/utils.py
Normal 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
159
changelog.txt
Normal 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
80
config_default.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
8
development-requirements.txt
Normal file
8
development-requirements.txt
Normal 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
11
downloader.py
Normal 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}")
|
984
locale/id/LC_MESSAGES/TTMediaBot.po
Normal file
984
locale/id/LC_MESSAGES/TTMediaBot.po
Normal 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"
|
||||||
|
|
13
requirements.txt
Normal file
13
requirements.txt
Normal 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
7
srb
Normal 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}
|
||||||
|
|
12
systemd/user/TTMediaBot.service
Normal file
12
systemd/user/TTMediaBot.service
Normal 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
|
13
systemd/user/pulseaudio.service
Normal file
13
systemd/user/pulseaudio.service
Normal 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
|
10
systemd/user/pulseaudio.socket
Normal file
10
systemd/user/pulseaudio.socket
Normal 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
49
tools/compile_locales.py
Normal 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()
|
95
tools/libmpv_win_downloader.py
Normal file
95
tools/libmpv_win_downloader.py
Normal 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
117
tools/ttsdk_downloader.py
Normal 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
134
tools/vk_auth.py
Normal 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
76
tools/yam_auth.py
Normal 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
65
typestubs/mpv.pyi
Normal 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
9
typestubs/patoolib.pyi
Normal 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: ...
|
35
typestubs/version/__init__.pyi
Normal file
35
typestubs/version/__init__.pyi
Normal 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:
|
||||||
|
...
|
||||||
|
|
45
typestubs/vk_api/__init__.py
Normal file
45
typestubs/vk_api/__init__.py
Normal 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:
|
||||||
|
...
|
6
typestubs/vk_api/exceptions.py
Normal file
6
typestubs/vk_api/exceptions.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class ApiHttpError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(Exception):
|
||||||
|
pass
|
7
typestubs/youtubesearchpython.pyi
Normal file
7
typestubs/youtubesearchpython.pyi
Normal 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
24
typestubs/yt_dlp.pyi
Normal 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]: ...
|
Loading…
x
Reference in New Issue
Block a user