Merge pull request #187 from azlux/translate

i18n support, with traduora
This commit is contained in:
Terry Geng 2020-07-12 18:22:50 +08:00 committed by GitHub
commit 0da652da10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 738 additions and 519 deletions

View File

@ -3,18 +3,22 @@ type: docker
name: default
steps:
- name: build
- name: build-web
image: node
commands:
- cd web
- npm install
- npm run build
- cd ..
- (cd web && npm install && npm run build)
- name: translate-web
image: python:3
commands:
- pip3 install jinja2
- (cd templates/ && ./translate.py)
- name: deploy-testing
image: debian
commands:
- apt-get -qq update && apt-get -qq install git > /dev/null
- apt-get -qq update && apt-get -qq install git python3-requests > /dev/null
- (cd lang && ./sync_translation.py --client $TRADUORA_W_CLIENT --secret $TRADUORA_W_SECRET --push)
- sed -i 's/target_version = git/target_version = testing/' configuration.default.ini
- version=$(git rev-parse HEAD)
- echo "current git commit is $version"
@ -53,7 +57,8 @@ steps:
- name: deploy-stable
image: debian
commands:
- apt-get -qq update && apt-get -qq install jq curl git pandoc > /dev/null
- apt-get -qq update && apt-get -qq install jq curl git pandoc python3-requests > /dev/null
- (cd lang && ./sync_translation.py --client $TRADUORA_W_CLIENT --secret $TRADUORA_W_SECRET --push)
- sed -i 's/target_version = git/target_version = stable/' configuration.default.ini
- git fetch --tags
- version=$(git describe --abbrev=0 --tags)
@ -85,6 +90,25 @@ steps:
when:
event:
- tag
- name: fetch-translate
image: debian
commands:
- apt update && apt install -y git python3-requests hub
- git branch bot-traduora && git checkout bot-traduora
- (cd lang/ && ./sync_translation.py --client $TRADUORA_R_CLIENT --secret $TRADUORA_R_SECRET --fetch)
- git add lang/* && git status
- git config --global user.email "github@azlux.fr" && git config --global user.name "azlux"
- "git commit -a -m 'Bot: update strings'"
- git remote set-url origin https://azlux:$GITHUB_API@github.com/azlux/botamusique/
- git push --set-upstream origin new-translations && git push
- "GITHUB_USER=\"azlux\" GITHUB_TOKEN=\"$GITHUB_API\" hub pull-request -m \"Bot: TRADUORA Update\""
when:
event:
- cron
cron:
- auto-fetch-lang
volumes:
- name: repo

2
.gitignore vendored
View File

@ -116,7 +116,7 @@ tmp/
*.db
templates/*
templates/*.html
# Pycharm
.idea/

View File

@ -91,9 +91,8 @@ cd botamusique
python3 -m venv venv
venv/bin/pip install wheel
venv/bin/pip install -r requirements.txt
cd web
npm install
npm run build
(cd web && npm install && npm run build)
(cd templates/ && ./translate.py)
```
</details>

View File

@ -6,7 +6,8 @@ import json
import re
import pymumble_py3 as pymumble
import constants
from constants import tr_cli as tr
from constants import commands
import interface
import media.system
import util
@ -22,56 +23,56 @@ log = logging.getLogger("bot")
def register_all_commands(bot):
bot.register_command(constants.commands('joinme'), cmd_joinme, access_outside_channel=True)
bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('play'), cmd_play)
bot.register_command(constants.commands('pause'), cmd_pause)
bot.register_command(constants.commands('play_file'), cmd_play_file)
bot.register_command(constants.commands('play_file_match'), cmd_play_file_match)
bot.register_command(constants.commands('play_url'), cmd_play_url)
bot.register_command(constants.commands('play_playlist'), cmd_play_playlist)
bot.register_command(constants.commands('play_radio'), cmd_play_radio)
bot.register_command(constants.commands('play_tag'), cmd_play_tags)
bot.register_command(constants.commands('rb_query'), cmd_rb_query)
bot.register_command(constants.commands('rb_play'), cmd_rb_play)
bot.register_command(constants.commands('yt_search'), cmd_yt_search)
bot.register_command(constants.commands('yt_play'), cmd_yt_play)
bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True)
bot.register_command(constants.commands('stop'), cmd_stop)
bot.register_command(constants.commands('clear'), cmd_clear)
bot.register_command(constants.commands('kill'), cmd_kill, admin=True)
bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True, admin=True)
bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
bot.register_command(constants.commands('volume'), cmd_volume)
bot.register_command(constants.commands('ducking'), cmd_ducking)
bot.register_command(constants.commands('ducking_threshold'), cmd_ducking_threshold)
bot.register_command(constants.commands('ducking_volume'), cmd_ducking_volume)
bot.register_command(constants.commands('current_music'), cmd_current_music)
bot.register_command(constants.commands('skip'), cmd_skip)
bot.register_command(constants.commands('last'), cmd_last)
bot.register_command(constants.commands('remove'), cmd_remove)
bot.register_command(constants.commands('list_file'), cmd_list_file)
bot.register_command(constants.commands('queue'), cmd_queue)
bot.register_command(constants.commands('random'), cmd_random)
bot.register_command(constants.commands('repeat'), cmd_repeat)
bot.register_command(constants.commands('mode'), cmd_mode)
bot.register_command(constants.commands('add_tag'), cmd_add_tag)
bot.register_command(constants.commands('remove_tag'), cmd_remove_tag)
bot.register_command(constants.commands('find_tagged'), cmd_find_tagged)
bot.register_command(constants.commands('search'), cmd_search_library)
bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist)
bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library)
bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True, admin=True)
bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True)
bot.register_command(constants.commands('requests_webinterface_access'), cmd_web_access)
bot.register_command(constants.commands('add_webinterface_user'), cmd_web_user_add, admin=True)
bot.register_command(constants.commands('remove_webinterface_user'), cmd_web_user_remove, admin=True)
bot.register_command(constants.commands('list_webinterface_user'), cmd_web_user_list, admin=True)
bot.register_command(constants.commands('change_user_password'), cmd_user_password, no_partial_match=True)
bot.register_command(commands('joinme'), cmd_joinme, access_outside_channel=True)
bot.register_command(commands('user_ban'), cmd_user_ban, no_partial_match=True, admin=True)
bot.register_command(commands('user_unban'), cmd_user_unban, no_partial_match=True, admin=True)
bot.register_command(commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True, admin=True)
bot.register_command(commands('url_ban'), cmd_url_ban, no_partial_match=True, admin=True)
bot.register_command(commands('url_unban'), cmd_url_unban, no_partial_match=True, admin=True)
bot.register_command(commands('play'), cmd_play)
bot.register_command(commands('pause'), cmd_pause)
bot.register_command(commands('play_file'), cmd_play_file)
bot.register_command(commands('play_file_match'), cmd_play_file_match)
bot.register_command(commands('play_url'), cmd_play_url)
bot.register_command(commands('play_playlist'), cmd_play_playlist)
bot.register_command(commands('play_radio'), cmd_play_radio)
bot.register_command(commands('play_tag'), cmd_play_tags)
bot.register_command(commands('rb_query'), cmd_rb_query)
bot.register_command(commands('rb_play'), cmd_rb_play)
bot.register_command(commands('yt_search'), cmd_yt_search)
bot.register_command(commands('yt_play'), cmd_yt_play)
bot.register_command(commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True)
bot.register_command(commands('stop'), cmd_stop)
bot.register_command(commands('clear'), cmd_clear)
bot.register_command(commands('kill'), cmd_kill, admin=True)
bot.register_command(commands('update'), cmd_update, no_partial_match=True, admin=True)
bot.register_command(commands('stop_and_getout'), cmd_stop_and_getout)
bot.register_command(commands('volume'), cmd_volume)
bot.register_command(commands('ducking'), cmd_ducking)
bot.register_command(commands('ducking_threshold'), cmd_ducking_threshold)
bot.register_command(commands('ducking_volume'), cmd_ducking_volume)
bot.register_command(commands('current_music'), cmd_current_music)
bot.register_command(commands('skip'), cmd_skip)
bot.register_command(commands('last'), cmd_last)
bot.register_command(commands('remove'), cmd_remove)
bot.register_command(commands('list_file'), cmd_list_file)
bot.register_command(commands('queue'), cmd_queue)
bot.register_command(commands('random'), cmd_random)
bot.register_command(commands('repeat'), cmd_repeat)
bot.register_command(commands('mode'), cmd_mode)
bot.register_command(commands('add_tag'), cmd_add_tag)
bot.register_command(commands('remove_tag'), cmd_remove_tag)
bot.register_command(commands('find_tagged'), cmd_find_tagged)
bot.register_command(commands('search'), cmd_search_library)
bot.register_command(commands('add_from_shortlist'), cmd_shortlist)
bot.register_command(commands('delete_from_library'), cmd_delete_from_library)
bot.register_command(commands('drop_database'), cmd_drop_database, no_partial_match=True, admin=True)
bot.register_command(commands('rescan'), cmd_refresh_cache, no_partial_match=True)
bot.register_command(commands('requests_webinterface_access'), cmd_web_access)
bot.register_command(commands('add_webinterface_user'), cmd_web_user_add, admin=True)
bot.register_command(commands('remove_webinterface_user'), cmd_web_user_remove, admin=True)
bot.register_command(commands('list_webinterface_user'), cmd_web_user_list, admin=True)
bot.register_command(commands('change_user_password'), cmd_user_password, no_partial_match=True)
# Just for debug use
bot.register_command('rtrms', cmd_real_time_rms, True)
# bot.register_command('loop', cmd_loop_state, True)
@ -160,7 +161,7 @@ def cmd_url_ban(bot, user, text, command, parameter):
var.cache.free_and_delete(item.id)
var.playlist.remove_by_id(item.id)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_url_ban_list(bot, user, text, command, parameter):
@ -184,14 +185,14 @@ def cmd_play(bot, user, text, command, parameter):
if params[0].isdigit() and 1 <= int(params[0]) <= len(var.playlist):
index = int(params[0])
else:
bot.send_msg(constants.strings('invalid_index', index=parameter), text)
bot.send_msg(tr('invalid_index', index=parameter), text)
return
if len(params) > 1:
try:
start_at = util.parse_time(params[1])
except ValueError:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
if len(var.playlist) > 0:
@ -204,14 +205,14 @@ def cmd_play(bot, user, text, command, parameter):
bot.send_msg(var.playlist.current_item().format_current_playing(), text)
else:
bot.is_pause = False
bot.send_msg(constants.strings('queue_empty'), text)
bot.send_msg(tr('queue_empty'), text)
def cmd_pause(bot, user, text, command, parameter):
global log
bot.pause()
bot.send_channel_msg(constants.strings('paused'))
bot.send_channel_msg(tr('paused'))
def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=False):
@ -222,7 +223,7 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
if music_wrappers:
var.playlist.append(music_wrappers[0])
log.info("cmd: add to playlist: " + music_wrappers[0].format_debug_string())
bot.send_msg(constants.strings('file_added', item=music_wrappers[0].format_song_string()), text)
bot.send_msg(tr('file_added', item=music_wrappers[0].format_song_string()), text)
return
# assume parameter is a folder
@ -230,7 +231,7 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
.and_equal('type', 'file')
.and_like('path', parameter + '%')), user)
if music_wrappers:
msgs = [constants.strings('multiple_file_added')]
msgs = [tr('multiple_file_added')]
for music_wrapper in music_wrappers:
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
@ -249,20 +250,20 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
music_wrapper = get_cached_wrapper_from_dict(matches[0], user)
var.playlist.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
bot.send_msg(tr('file_added', item=music_wrapper.format_song_string()), text)
return
elif len(matches) > 1:
song_shortlist = matches
msgs = [constants.strings('multiple_matches')]
msgs = [tr('multiple_matches')]
for index, match in enumerate(matches):
msgs.append("<b>{:d}</b> - <b>{:s}</b> ({:s})".format(
index + 1, match['title'], match['path']))
msgs.append(constants.strings("shortlist_instruction"))
msgs.append(tr("shortlist_instruction"))
send_multi_lines(bot, msgs, text)
return
if do_not_refresh_cache:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
else:
var.cache.build_dir_cache()
cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=True)
@ -273,7 +274,7 @@ def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cach
if parameter:
file_dicts = var.music_db.query_music(Condition().and_equal('type', 'file'))
msgs = [constants.strings('multiple_file_added') + "<ul>"]
msgs = [tr('multiple_file_added') + "<ul>"]
try:
count = 0
music_wrappers = []
@ -299,16 +300,16 @@ def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cach
send_multi_lines_in_channel(bot, msgs, "")
else:
if do_not_refresh_cache:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
else:
var.cache.build_dir_cache()
cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=True)
except re.error as e:
msg = constants.strings('wrong_pattern', error=str(e))
msg = tr('wrong_pattern', error=str(e))
bot.send_msg(msg, text)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_play_url(bot, user, text, command, parameter):
@ -320,12 +321,12 @@ def cmd_play_url(bot, user, text, command, parameter):
var.playlist.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
bot.send_msg(tr('file_added', item=music_wrapper.format_song_string()), text)
if len(var.playlist) == 2:
# If I am the second item on the playlist. (I am the next one!)
bot.async_download_next()
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_play_playlist(bot, user, text, command, parameter):
@ -345,7 +346,7 @@ def cmd_play_playlist(bot, user, text, command, parameter):
for music in items:
log.info("cmd: add to playlist: " + music.format_debug_string())
else:
bot.send_msg(constants.strings("playlist_fetching_failed"), text)
bot.send_msg(tr("playlist_fetching_failed"), text)
def cmd_play_radio(bot, user, text, command, parameter):
@ -353,7 +354,7 @@ def cmd_play_radio(bot, user, text, command, parameter):
if not parameter:
all_radio = var.config.items('radio')
msg = constants.strings('preconfigurated_radio')
msg = tr('preconfigurated_radio')
for i in all_radio:
comment = ""
if len(i[1].split(maxsplit=1)) == 2:
@ -370,9 +371,9 @@ def cmd_play_radio(bot, user, text, command, parameter):
var.playlist.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
bot.send_msg(tr('file_added', item=music_wrapper.format_song_string()), text)
else:
bot.send_msg(constants.strings('bad_url'), text)
bot.send_msg(tr('bad_url'), text)
def cmd_rb_query(bot, user, text, command, parameter):
@ -381,13 +382,13 @@ def cmd_rb_query(bot, user, text, command, parameter):
log.info('cmd: Querying radio stations')
if not parameter:
log.debug('rbquery without parameter')
msg = constants.strings('rb_query_empty')
msg = tr('rb_query_empty')
bot.send_msg(msg, text)
else:
log.debug('cmd: Found query parameter: ' + parameter)
rb = RadioBrowser()
rb_stations = rb.search(name=parameter, name_exact=False)
msg = constants.strings('rb_query_result')
msg = tr('rb_query_result')
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
if not rb_stations:
log.debug(f"cmd: No matches found for rbquery {parameter}")
@ -408,7 +409,7 @@ def cmd_rb_query(bot, user, text, command, parameter):
# Shorten message if message too long (stage I)
else:
log.debug('Result too long stage I')
msg = constants.strings('rb_query_result') + ' (shortened L1)'
msg = tr('rb_query_result') + ' (shortened L1)'
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th></tr>'
for s in rb_stations:
station_id = s['stationuuid']
@ -420,7 +421,7 @@ def cmd_rb_query(bot, user, text, command, parameter):
# Shorten message if message too long (stage II)
else:
log.debug('Result too long stage II')
msg = constants.strings('rb_query_result') + ' (shortened L2)'
msg = tr('rb_query_result') + ' (shortened L2)'
msg += '!rbplay ID - Station Name'
for s in rb_stations:
station_id = s['stationuuid']
@ -439,7 +440,7 @@ def cmd_rb_play(bot, user, text, command, parameter):
log.debug('cmd: Play a station by ID')
if not parameter:
log.debug('rbplay without parameter')
msg = constants.strings('rb_play_empty')
msg = tr('rb_play_empty')
bot.send_msg(msg, text)
else:
log.debug('cmd: Retreiving url for station ID ' + parameter)
@ -488,9 +489,9 @@ def cmd_yt_search(bot, user, text, command, parameter):
'title': result[1]
} for result in yt_last_result[yt_last_page * item_per_page: item_per_page]]
msg = _yt_format_result(yt_last_result, yt_last_page * item_per_page, item_per_page)
bot.send_msg(constants.strings('yt_result', result_table=msg), text)
bot.send_msg(tr('yt_result', result_table=msg), text)
else:
bot.send_msg(constants.strings('yt_no_more'), text)
bot.send_msg(tr('yt_no_more'), text)
# if query
else:
@ -501,11 +502,11 @@ def cmd_yt_search(bot, user, text, command, parameter):
song_shortlist = [{'type': 'url', 'url': "https://www.youtube.com/watch?v=" + result[0]}
for result in results[0: item_per_page]]
msg = _yt_format_result(results, 0, item_per_page)
bot.send_msg(constants.strings('yt_result', result_table=msg), text)
bot.send_msg(tr('yt_result', result_table=msg), text)
else:
bot.send_msg(constants.strings('yt_query_error'), text)
bot.send_msg(tr('yt_query_error'), text)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def _yt_format_result(results, start, count):
@ -529,16 +530,16 @@ def cmd_yt_play(bot, user, text, command, parameter):
url = "https://www.youtube.com/watch?v=" + yt_last_result[0][0]
cmd_play_url(bot, user, text, command, url)
else:
bot.send_msg(constants.strings('yt_query_error'), text)
bot.send_msg(tr('yt_query_error'), text)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_help(bot, user, text, command, parameter):
global log
bot.send_msg(constants.strings('help'), text)
bot.send_msg(tr('help'), text)
if bot.is_admin(user):
bot.send_msg(constants.strings('admin_help'), text)
bot.send_msg(tr('admin_help'), text)
def cmd_stop(bot, user, text, command, parameter):
@ -549,14 +550,14 @@ def cmd_stop(bot, user, text, command, parameter):
cmd_clear(bot, user, text, command, parameter)
else:
bot.stop()
bot.send_msg(constants.strings('stopped'), text)
bot.send_msg(tr('stopped'), text)
def cmd_clear(bot, user, text, command, parameter):
global log
bot.clear()
bot.send_msg(constants.strings('cleared'), text)
bot.send_msg(tr('cleared'), text)
def cmd_kill(bot, user, text, command, parameter):
@ -571,12 +572,12 @@ def cmd_update(bot, user, text, command, parameter):
if bot.is_admin(user):
bot.mumble.users[text.actor].send_text_message(
constants.strings('start_updating'))
tr('start_updating'))
msg = util.update(bot.version)
bot.mumble.users[text.actor].send_text_message(msg)
else:
bot.mumble.users[text.actor].send_text_message(
constants.strings('not_admin'))
tr('not_admin'))
def cmd_stop_and_getout(bot, user, text, command, parameter):
@ -595,11 +596,11 @@ def cmd_volume(bot, user, text, command, parameter):
# The volume is a percentage
if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.volume_helper.set_volume(float(parameter) / 100.0)
bot.send_msg(constants.strings('change_volume', volume=parameter, user=bot.mumble.users[text.actor]['name']), text)
bot.send_msg(tr('change_volume', volume=parameter, user=bot.mumble.users[text.actor]['name']), text)
var.db.set('bot', 'volume', str(float(parameter) / 100.0))
log.info(f'cmd: volume set to {float(parameter) / 100.0}')
else:
bot.send_msg(constants.strings('current_volume', volume=int(bot.volume_helper.plain_volume_set * 100)), text)
bot.send_msg(tr('current_volume', volume=int(bot.volume_helper.plain_volume_set * 100)), text)
def cmd_ducking(bot, user, text, command, parameter):
@ -608,7 +609,7 @@ def cmd_ducking(bot, user, text, command, parameter):
if parameter == "" or parameter == "on":
bot.is_ducking = True
var.db.set('bot', 'ducking', True)
bot.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED, bot.ducking_sound_received)
bot.mumble.callbacks.set_callback(pymumble.c.PYMUMBLE_CLBK_SOUNDRECEIVED, bot.ducking_sound_received)
bot.mumble.set_receive_sound(True)
log.info('cmd: ducking is on')
msg = "Ducking on."
@ -641,11 +642,11 @@ def cmd_ducking_volume(bot, user, text, command, parameter):
# The volume is a percentage
if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.volume_helper.set_ducking_volume(float(parameter) / 100.0)
bot.send_msg(constants.strings('change_ducking_volume', volume=parameter, user=bot.mumble.users[text.actor]['name']), text)
bot.send_msg(tr('change_ducking_volume', volume=parameter, user=bot.mumble.users[text.actor]['name']), text)
var.db.set('bot', 'ducking_volume', float(parameter) / 100.0)
log.info(f'cmd: volume on ducking set to {parameter}')
else:
bot.send_msg(constants.strings('current_ducking_volume', volume=int(bot.volume_helper.plain_ducking_volume_set * 100)), text)
bot.send_msg(tr('current_ducking_volume', volume=int(bot.volume_helper.plain_ducking_volume_set * 100)), text)
def cmd_current_music(bot, user, text, command, parameter):
@ -654,7 +655,7 @@ def cmd_current_music(bot, user, text, command, parameter):
if len(var.playlist) > 0:
bot.send_msg(var.playlist.current_item().format_current_playing(), text)
else:
bot.send_msg(constants.strings('not_playing'), text)
bot.send_msg(tr('not_playing'), text)
def cmd_skip(bot, user, text, command, parameter):
@ -667,7 +668,7 @@ def cmd_skip(bot, user, text, command, parameter):
bot.wait_for_ready = True
if len(var.playlist) == 0:
bot.send_msg(constants.strings('queue_empty'), text)
bot.send_msg(tr('queue_empty'), text)
def cmd_last(bot, user, text, command, parameter):
@ -677,7 +678,7 @@ def cmd_last(bot, user, text, command, parameter):
bot.interrupt()
var.playlist.point_to(len(var.playlist) - 1 - 1)
else:
bot.send_msg(constants.strings('queue_empty'), text)
bot.send_msg(tr('queue_empty'), text)
def cmd_remove(bot, user, text, command, parameter):
@ -690,8 +691,8 @@ def cmd_remove(bot, user, text, command, parameter):
if index == var.playlist.current_index:
removed = var.playlist[index]
bot.send_msg(constants.strings('removing_item',
item=removed.format_title()), text)
bot.send_msg(tr('removing_item',
item=removed.format_title()), text)
log.info("cmd: delete from playlist: " + removed.format_debug_string())
var.playlist.remove(index)
@ -710,7 +711,7 @@ def cmd_remove(bot, user, text, command, parameter):
var.playlist.remove(index)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_list_file(bot, user, text, command, parameter):
@ -722,7 +723,7 @@ def cmd_list_file(bot, user, text, command, parameter):
song_shortlist = files
msgs = [constants.strings("multiple_file_found") + "<ul>"]
msgs = [tr("multiple_file_found") + "<ul>"]
try:
count = 0
for index, file in enumerate(files):
@ -739,14 +740,14 @@ def cmd_list_file(bot, user, text, command, parameter):
if count != 0:
msgs.append("</ul>")
if count > ITEMS_PER_PAGE:
msgs.append(constants.strings("records_omitted"))
msgs.append(constants.strings("shortlist_instruction"))
msgs.append(tr("records_omitted"))
msgs.append(tr("shortlist_instruction"))
send_multi_lines(bot, msgs, text, "")
else:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
except re.error as e:
msg = constants.strings('wrong_pattern', error=str(e))
msg = tr('wrong_pattern', error=str(e))
bot.send_msg(msg, text)
@ -754,10 +755,10 @@ def cmd_queue(bot, user, text, command, parameter):
global log
if len(var.playlist) == 0:
msg = constants.strings('queue_empty')
msg = tr('queue_empty')
bot.send_msg(msg, text)
else:
msgs = [constants.strings('queue_contents')]
msgs = [tr('queue_contents')]
for i, music in enumerate(var.playlist):
tags = ''
if len(music.item().tags) > 0:
@ -797,25 +798,25 @@ def cmd_repeat(bot, user, text, command, parameter):
)
log.info("bot: add to playlist: " + music.format_debug_string())
bot.send_channel_msg(constants.strings("repeat", song=music.format_song_string(), n=str(repeat)))
bot.send_channel_msg(tr("repeat", song=music.format_song_string(), n=str(repeat)))
else:
bot.send_msg(constants.strings("queue_empty"), text)
bot.send_msg(tr("queue_empty"), text)
def cmd_mode(bot, user, text, command, parameter):
global log
if not parameter:
bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text)
bot.send_msg(tr("current_mode", mode=var.playlist.mode), text)
return
if parameter not in ["one-shot", "repeat", "random", "autoplay"]:
bot.send_msg(constants.strings('unknown_mode', mode=parameter), text)
bot.send_msg(tr('unknown_mode', mode=parameter), text)
else:
var.db.set('playlist', 'playback_mode', parameter)
var.playlist = media.playlist.get_playlist(parameter, var.playlist)
log.info(f"command: playback mode changed to {parameter}.")
bot.send_msg(constants.strings("change_mode", mode=var.playlist.mode,
user=bot.mumble.users[text.actor]['name']), text)
bot.send_msg(tr("change_mode", mode=var.playlist.mode,
user=bot.mumble.users[text.actor]['name']), text)
if parameter == "random":
bot.interrupt()
bot.launch_music()
@ -823,10 +824,10 @@ def cmd_mode(bot, user, text, command, parameter):
def cmd_play_tags(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
msgs = [constants.strings('multiple_file_added') + "<ul>"]
msgs = [tr('multiple_file_added') + "<ul>"]
count = 0
tags = parameter.split(",")
@ -842,7 +843,7 @@ def cmd_play_tags(bot, user, text, command, parameter):
var.playlist.extend(music_wrappers)
send_multi_lines_in_channel(bot, msgs, "")
else:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
def cmd_add_tag(bot, user, text, command, parameter):
@ -866,19 +867,19 @@ def cmd_add_tag(bot, user, text, command, parameter):
if index.isdigit() and 1 <= int(index) <= len(var.playlist):
var.playlist[int(index) - 1].add_tags(tags)
log.info(f"cmd: add tags {', '.join(tags)} to song {var.playlist[int(index) - 1].format_debug_string()}")
bot.send_msg(constants.strings("added_tags",
tags=", ".join(tags),
song=var.playlist[int(index) - 1].format_title()), text)
bot.send_msg(tr("added_tags",
tags=", ".join(tags),
song=var.playlist[int(index) - 1].format_title()), text)
return
elif index == "*":
for item in var.playlist:
item.add_tags(tags)
log.info(f"cmd: add tags {', '.join(tags)} to song {item.format_debug_string()}")
bot.send_msg(constants.strings("added_tags_to_all", tags=", ".join(tags)), text)
bot.send_msg(tr("added_tags_to_all", tags=", ".join(tags)), text)
return
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_remove_tag(bot, user, text, command, parameter):
@ -903,15 +904,15 @@ def cmd_remove_tag(bot, user, text, command, parameter):
if tags[0] != "*":
var.playlist[int(index) - 1].remove_tags(tags)
log.info(f"cmd: remove tags {', '.join(tags)} from song {var.playlist[int(index) - 1].format_debug_string()}")
bot.send_msg(constants.strings("removed_tags",
tags=", ".join(tags),
song=var.playlist[int(index) - 1].format_title()), text)
bot.send_msg(tr("removed_tags",
tags=", ".join(tags),
song=var.playlist[int(index) - 1].format_title()), text)
return
else:
var.playlist[int(index) - 1].clear_tags()
log.info(f"cmd: clear tags from song {var.playlist[int(index) - 1].format_debug_string()}")
bot.send_msg(constants.strings("cleared_tags",
song=var.playlist[int(index) - 1].format_title()), text)
bot.send_msg(tr("cleared_tags",
song=var.playlist[int(index) - 1].format_title()), text)
return
elif index == "*":
@ -919,26 +920,26 @@ def cmd_remove_tag(bot, user, text, command, parameter):
for item in var.playlist:
item.remove_tags(tags)
log.info(f"cmd: remove tags {', '.join(tags)} from song {item.format_debug_string()}")
bot.send_msg(constants.strings("removed_tags_from_all", tags=", ".join(tags)), text)
bot.send_msg(tr("removed_tags_from_all", tags=", ".join(tags)), text)
return
else:
for item in var.playlist:
item.clear_tags()
log.info(f"cmd: clear tags from song {item.format_debug_string()}")
bot.send_msg(constants.strings("cleared_tags_from_all"), text)
bot.send_msg(tr("cleared_tags_from_all"), text)
return
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_find_tagged(bot, user, text, command, parameter):
global song_shortlist
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
msgs = [constants.strings('multiple_file_found') + "<ul>"]
msgs = [tr('multiple_file_found') + "<ul>"]
count = 0
tags = parameter.split(",")
@ -957,20 +958,20 @@ def cmd_find_tagged(bot, user, text, command, parameter):
if count != 0:
msgs.append("</ul>")
if count > ITEMS_PER_PAGE:
msgs.append(constants.strings("records_omitted"))
msgs.append(constants.strings("shortlist_instruction"))
msgs.append(tr("records_omitted"))
msgs.append(tr("shortlist_instruction"))
send_multi_lines(bot, msgs, text, "")
else:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
def cmd_search_library(bot, user, text, command, parameter):
global song_shortlist
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
msgs = [constants.strings('multiple_file_found') + "<ul>"]
msgs = [tr('multiple_file_found') + "<ul>"]
count = 0
_keywords = parameter.split(" ")
@ -988,7 +989,7 @@ def cmd_search_library(bot, user, text, command, parameter):
music_wrapper = get_cached_wrapper(items[0], user)
var.playlist.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
bot.send_channel_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
bot.send_channel_msg(tr('file_added', item=music_wrapper.format_song_string()))
else:
for item in items:
count += 1
@ -1002,19 +1003,19 @@ def cmd_search_library(bot, user, text, command, parameter):
if count != 0:
msgs.append("</ul>")
if count > ITEMS_PER_PAGE:
msgs.append(constants.strings("records_omitted"))
msgs.append(constants.strings("shortlist_instruction"))
msgs.append(tr("records_omitted"))
msgs.append(tr("shortlist_instruction"))
send_multi_lines(bot, msgs, text, "")
else:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
else:
bot.send_msg(constants.strings("no_file"), text)
bot.send_msg(tr("no_file"), text)
def cmd_shortlist(bot, user, text, command, parameter):
global song_shortlist, log
if parameter.strip() == "*":
msgs = [constants.strings('multiple_file_added') + "<ul>"]
msgs = [tr('multiple_file_added') + "<ul>"]
music_wrappers = []
for kwargs in song_shortlist:
kwargs['user'] = user
@ -1032,11 +1033,11 @@ def cmd_shortlist(bot, user, text, command, parameter):
try:
indexes = [int(i) for i in parameter.split(" ")]
except ValueError:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
if len(indexes) > 1:
msgs = [constants.strings('multiple_file_added') + "<ul>"]
msgs = [tr('multiple_file_added') + "<ul>"]
music_wrappers = []
for index in indexes:
if 1 <= index <= len(song_shortlist):
@ -1048,7 +1049,7 @@ def cmd_shortlist(bot, user, text, command, parameter):
msgs.append("<li>[{}] <b>{}</b></li>".format(music_wrapper.item().type, music_wrapper.item().title))
else:
var.playlist.extend(music_wrappers)
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
var.playlist.extend(music_wrappers)
@ -1064,10 +1065,10 @@ def cmd_shortlist(bot, user, text, command, parameter):
music_wrapper = get_cached_wrapper_from_scrap(**kwargs)
var.playlist.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
bot.send_channel_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
bot.send_channel_msg(tr('file_added', item=music_wrapper.format_song_string()))
return
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_delete_from_library(bot, user, text, command, parameter):
@ -1075,11 +1076,11 @@ def cmd_delete_from_library(bot, user, text, command, parameter):
try:
indexes = [int(i) for i in parameter.split(" ")]
except ValueError:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
if len(indexes) > 1:
msgs = [constants.strings('multiple_file_added') + "<ul>"]
msgs = [tr('multiple_file_added') + "<ul>"]
count = 0
for index in indexes:
if 1 <= index <= len(song_shortlist):
@ -1092,11 +1093,11 @@ def cmd_delete_from_library(bot, user, text, command, parameter):
var.cache.free_and_delete(music_dict['id'])
count += 1
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
if count == 0:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
msgs.append("</ul>")
@ -1108,13 +1109,13 @@ def cmd_delete_from_library(bot, user, text, command, parameter):
music_dict = song_shortlist[index - 1]
if 'id' in music_dict:
music_wrapper = get_cached_wrapper_by_id(music_dict['id'], user)
bot.send_msg(constants.strings('file_deleted', item=music_wrapper.format_song_string()), text)
bot.send_msg(tr('file_deleted', item=music_wrapper.format_song_string()), text)
log.info("cmd: remove from library: " + music_wrapper.format_debug_string())
var.playlist.remove_by_id(music_dict['id'])
var.cache.free_and_delete(music_dict['id'])
return
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
def cmd_drop_database(bot, user, text, command, parameter):
@ -1126,9 +1127,9 @@ def cmd_drop_database(bot, user, text, command, parameter):
var.music_db.drop_table()
var.music_db = MusicDatabase(var.settings_db_path)
log.info("command: database dropped.")
bot.send_msg(constants.strings('database_dropped'), text)
bot.send_msg(tr('database_dropped'), text)
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
bot.mumble.users[text.actor].send_text_message(tr('not_admin'))
def cmd_refresh_cache(bot, user, text, command, parameter):
@ -1136,9 +1137,9 @@ def cmd_refresh_cache(bot, user, text, command, parameter):
if bot.is_admin(user):
var.cache.build_dir_cache()
log.info("command: Local file cache refreshed.")
bot.send_msg(constants.strings('cache_refreshed'), text)
bot.send_msg(tr('cache_refreshed'), text)
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
bot.mumble.users[text.actor].send_text_message(tr('not_admin'))
def cmd_web_access(bot, user, text, command, parameter):
@ -1164,12 +1165,12 @@ def cmd_web_access(bot, user, text, command, parameter):
else:
access_address = var.config.get("webinterface", "access_address")
bot.send_msg(constants.strings('webpage_address', address=access_address), text)
bot.send_msg(tr('webpage_address', address=access_address), text)
def cmd_user_password(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
user_info = var.db.get("user", user, fallback='{}')
@ -1178,12 +1179,12 @@ def cmd_user_password(bot, user, text, command, parameter):
var.db.set("user", user, json.dumps(user_dict))
bot.send_msg(constants.strings('user_password_set'), text)
bot.send_msg(tr('user_password_set'), text)
def cmd_web_user_add(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
@ -1193,14 +1194,14 @@ def cmd_web_user_add(bot, user, text, command, parameter):
if parameter not in web_users:
web_users.append(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ".join(web_users)), text)
bot.send_msg(tr('web_user_list', users=", ".join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
bot.send_msg(tr('command_disabled', command=command), text)
def cmd_web_user_remove(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
bot.send_msg(tr('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
@ -1210,9 +1211,9 @@ def cmd_web_user_remove(bot, user, text, command, parameter):
if parameter in web_users:
web_users.remove(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ".join(web_users)), text)
bot.send_msg(tr('web_user_list', users=", ".join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
bot.send_msg(tr('command_disabled', command=command), text)
def cmd_web_user_list(bot, user, text, command, parameter):
@ -1220,9 +1221,9 @@ def cmd_web_user_list(bot, user, text, command, parameter):
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
bot.send_msg(constants.strings('web_user_list', users=", ".join(web_users)), text)
bot.send_msg(tr('web_user_list', users=", ".join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
bot.send_msg(tr('command_disabled', command=command), text)
# Just for debug use

View File

@ -195,159 +195,3 @@ list_webinterface_user = webuserlist
add_webinterface_user = webuseradd
remove_webinterface_user = webuserdel
change_user_password = password
[strings]
current_volume = Current volume: {volume}.
current_ducking_volume = Volume on ducking: {volume}.
change_volume = Volume set to {volume} by {user}.
change_ducking_volume = Volume on ducking set to {volume} by {user}.
bad_command = {command}: command not found.
bad_parameter = {command}: invalid parameter.
error_executing_command = {command}: Command failed with error: {error}.
not_admin = You are not an admin!
not_playing = Nothing is playing right now.
no_file = File not found.
wrong_pattern = Invalid regex: {error}.
file_added = Added {item}.
file_deleted = Deleted {item} from the library.
multiple_file_added = Multiple items added:
multiple_file_deleted = Multiple items deleted from the library:
multiple_file_found = Found:
page_instruction = Page {current}/{total}. Use <i>!{command} {{page}}</i> to navigate.
records_omitted = ...
bad_url = Bad URL requested.
preconfigurated_radio = Preconfigurated Radio available:
unable_download = Unable to download <b>{item}</b>. Removed from the library.
unable_play = Unable to play <b>{item}</b>. Removed from the library.
which_command = Do you mean <br /> {commands}
multiple_matches = File not found! Possible candidates:
queue_contents = Items on the playlist:
queue_empty = Playlist is empty!
invalid_index = Invalid index <i>{index}</i>. Use '!queue' to see your playlist.
now_playing = Playing {item}
radio = Radio
file = File
url_from_playlist = URL
url = URL
radio_item = <a href="{url}"><b>{title}</b></a> <i>from</i> {name} <i>added by</i> {user}
file_item = <b>{artist} - {title}</b> <i>added by</i> {user}
url_from_playlist_item = <a href="{url}"><b>{title}</b></a> <i>from playlist</i> <a href="{playlist_url}">{playlist}</a> <i>added by</i> {user}
url_item = <a href="{url}"><b>{title}</b></a> <i>added by</i> {user}
not_in_my_channel = You're not in my channel, command refused!
pm_not_allowed = Private message aren't allowed.
too_long = <b>{song}</b> is too long, removed from playlist!
download_in_progress = Download of <b>{item}</b> in progress...
removing_item = Removed entry {item} from playlist.
user_ban = You are banned, not allowed to do that!
url_ban = This url is banned!
rb_query_result = This is the result of your query, send !rbplay 'ID' to play a station:
rb_play_empty = Please specify a radio station ID!
paused = Music paused.
stopped = Music stopped.
cleared = Playlist emptied.
database_dropped = Database dropped. All records have gone.
new_version_found = <h2>Update Available!</h2> Version {new_version} of botamusique is available! <hr />
<h3>Changelog</h3> {changelog} <hr /> Send <i>!update</i> to update!
update_successful = <h2>botamusique v{version} Installed!</h2><hr />
<h3>Changelog</h3> {changelog} <hr /> Visit <a href="https://github.com/azlux/botamusique">our github repo</a> for more details!
start_updating = Start updating...
file_missed = Music file '{file}' missed! This item has been removed from the playlist.
unknown_mode = Unknown playback mode '{mode}'. It should be one of <i>one-shot</i>, <i>repeat</i>, <i>random</i>.
current_mode = Current playback mode is <i>{mode}</i>.
change_mode = Playback mode set to <i>{mode}</i> by {user}.
repeat = Repeat {song} for {n} times.
yt_result = Youtube query result: {result_table} Use <i>!sl {{indexes}}</i> to play the item you want. <br />
<i>!ytquery -n</i> for the next page.
yt_no_more = No more results!
yt_query_error = Unable to query youtube!
playlist_fetching_failed = Unable to fetch the playlist!
cache_refreshed = Cache refreshed!
added_tags = Added tags <i>{tags}</i> to <b>{song}</b>.
added_tags_to_all = Added tags <i>{tags}</i> to songs on the playlist.
removed_tags = Removed tags <i>{tags}</i> from <b>{song}</b>.
removed_tags_from_all = Removed tags <i>{tags}</i> from songs on the playlist.
cleared_tags = Removed all tags from <b>{song}</b>.
cleared_tags_from_all = Removed all tags from songs on the playlist.
shortlist_instruction = Use <i>!sl {indexes}</i> to play the item you want.
auto_paused = Use <i>!play</i> to resume music!
webpage_address= Your own address to access the web interface is <a href="{address}">{address}</a>
web_user_list = Following users has the privilege to access the web interface: <br /> {users}
user_password_set = Your password has been updated.
command_disabled = {command}: command disabled!
help = <h3>Commands</h3>
<b>Control</b>
<ul>
<li> <b>!<u>w</u>eb</b> - get the URL of the web interface, if enabled. </li>
<li> <b>!play </b> (or <b>!p</b>) [{num}] [{start_from}] - resume from pausing / start to play (the num-th song is num if given) </li>
<li> <b>!<u>pa</u>use </b> - pause </li>
<li> <b>!<u>st</u>op </b> - stop playing </li>
<li> <b>!<u>sk</u>ip </b> - jump to the next song </li>
<li> <b>!<u>la</u>st </b> - jump to the last song </li>
<li> <b>!<u>v</u>olume </b> {volume} - get or change the volume (from 0 to 100) </li>
<li> <b>!<u>m</u>ode </b> [{mode}] - get or set the playback mode, {mode} should be one of <i>one-shot</i> (remove
item once played), <i>repeat</i> (looping through the playlist), <i>random</i> (randomize the playlist),
<i>autoplay</i> (randomly grab something from the music library).</li>
<li> <b>!duck </b> on/off - enable or disable ducking function </li>
<li> <b>!duckv </b> - set the volume of the bot when ducking is activated </li>
<li> <b>!<u>duckt</u>hres </b> - set the threshold of volume to activate ducking (3000 by default) </li>
<li> <b>!<u>o</u>ust </b> - stop playing and go to default channel </li>
</ul>
<b>Playlist</b>
<ul>
<li> <b>!<u>n</u>ow </b> (or <b>!np</b>) - display the current song </li>
<li> <b>!<u>q</u>ueue </b> - display items in the playlist </li>
<li> <b>!<u>t</u>ag </b> {tags} - add all items with tags {tags}, tags separated by ",". </li>
<li> <b>!file </b>(or <b>!f</b>) {path/folder/keyword} - add a single file to the playlist by its path or keyword in its path. </li>
<li> <b>!<u>filem</u>atch </b>(or <b>!fm</b>) {pattern} - add all files that match regex {pattern} </li>
<li> <b>!<u>ur</u>l </b> {url} - add Youtube or SoundCloud music </li>
<li> <b>!<u>playl</u>ist </b> {url} [{offset}] - add all items in a Youtube or SoundCloud playlist, and start with the {offset}-th item </li>
<li> <b>!<u>rad</u>io </b> {url} - append a radio {url} to the playlist </li>
<li> <b>!<u>rbq</u>uery </b> {keyword} - query http://www.radio-browser.info for a radio station </li>
<li> <b>!<u>rbp</u>lay </b> {id} - play a radio station with {id} (eg. !rbplay 96746) </li>
<li> <b>!<u>ys</u>earch </b> {keywords} - query youtube. Use <i>!ysearch -n</i> to turn the page. </li>
<li> <b>!<u>yp</u>lay </b> {keywords} - add the first search result of {keywords} into the playlist.</li>
<li> <b>!<u>sh</u>ortlist </b> (or <b>!sl</b>) {indexes/*} - add {indexes}-th item (or all items if * is given) on the shortlist. </li>
<li> <b>!rm </b> {num} - remove the num-th song on the playlist </li>
<li> <b>!<u>rep</u>eat </b> [{num}] - repeat current song {num} (1 by default) times.</li>
<li> <b>!<u>ran</u>dom </b> - randomize the playlist.</li>
</ul>
<b>Music Library</b>
<ul>
<li> <b>!<u>se</u>arch </b> {keywords} - find item with {keywords} in the music library, keywords separated by space.</li>
<li> <b>!<u>li</u>stfile </b> [{pattern}] - display list of available files (whose paths match the regex pattern if {pattern} is given) </li>
<li> <b>!<u>addt</u>ag </b> [{index}] {tags} - add {tags} to {index}-th(current song if {index} is omitted) item on the playlist, tags separated by ",". </li>
<li> <b>!<u>addt</u>ag </b> * {tags} - add {tags} to all items on the playlist. </li>
<li> <b>!<u>un</u>tag </b> [{index/*}] {tags}/* - remove {tags}/all tags from {index}-th(current song if {index} is omitted) item on the playlist. </li>
<li> <b>!<u>fin</u>dtagged </b> (or <b>!ft</b>) {tags} - find item with {tags} in the music library. </li>
<li> <b>!<u>del</u>ete </b> {index} - delete {index}-th item on the shortlist from the music library. </li>
</ul>
<b>Other</b>
<ul>
<li> <b>!<u>j</u>oinme {token} </b> - join your own channel with {token}.</li>
<li> <b>!<u>password</u> {password} </b> - change your password, used to access the web interface.</li>
</ul>
admin_help = <h3>Admin command</h3>
<b>Bot</b>
<ul>
<li><b>!<u>k</u>ill </b> - kill the bot</li>
<li><b>!update </b> - update the bot</li>
<li><b>!userban </b> {user} - ban a user</li>
<li><b>!userunban </b> {user} - unban a user</li>
<li><b>!urlbanlist </b> - list banned url</li>
<li><b>!urlban </b> [{url}] - ban {url} (or current item's url by default) and remove this url from the library.</li>
<li><b>!urlunban </b> {url} - unban {url}</li>
<li><b>!rescan </b> {url} - rebuild local music file cache</li>
<li><b>!dropdatabase</b> - clear the entire database, you will lose all settings and music library.</li>
</ul>
<b>Web Interface</b>
<ul>
<li><b>!<u>webuserlist</u></b> - list all users that have the permission of accessing the web interface, if auth mode is 'password'.</li>
<li><b>!<u>webuseradd</u> {nick name}</b> - grant the user with {nick name} the access to the web interface, if auth mode is 'password'.</li>
<li><b>!<u>webuserdel</u> {nick name}</b> - revoke the access to the web interface of {nick name}, if auth mode is 'password'.</li>
<li><b>!update </b> - update the bot</li>
<li><b>!userban </b> {user} - ban a user</li>
</ul>

View File

@ -177,18 +177,3 @@ port = 64738
# see configuration.default.ini. Copy options you want to edit into this file.
#play_file = file, f
#play_file_match = filematch, fm
# [strings] is used to compose what the bot says. You can customize them to fit in
# the style of your channel, or translate into your own language.
# For a full list of strings, please see configuration.default.ini.
# Copy options you want to edit into this file.
# Note: please keep those {placeholder} of each string in your new string.
[strings]
# Some examples are:
#current_volume = Current volume: {volume}
#current_volume = 当前音量为{volume}
#current_volume = よく聞いてね!今の音量は{volume}<br />ちゃんと覚える:大音量で耳が悪くなる!
#
#bad_command = {command}: command not found
#bad_command = {command}: 未知命令,键入'!help'以获取可用命令列表
#bad_command = {command}がなに?食べれる?おいしいでしか?

View File

@ -1,24 +1,54 @@
import json
import variables as var
default_lang_dict = {}
lang_dict = {}
def strings(option, *argv, **kwargs):
def load_lang(lang):
global lang_dict, default_lang_dict
with open("lang/en_US.json", "r") as f:
default_lang_dict = json.load(f)
with open(f"lang/{lang}.json", "r") as f:
lang_dict = json.load(f)
def tr_cli(option, *argv, **kwargs):
try:
string = var.config.get("strings", option)
if option in lang_dict['cli'] and lang_dict['cli'][option]:
string = lang_dict['cli'][option]
else:
string = default_lang_dict['cli'][option]
except KeyError:
raise KeyError("Missed strings in configuration file: '{string}'. ".format(string=option)
+ "Please restore you configuration file back to default if necessary.")
raise KeyError("Missed strings in language file: '{string}'. ".format(string=option))
return _tr(string, *argv, **kwargs)
def tr_web(option, *argv, **kwargs):
try:
if option in lang_dict['web'] and lang_dict['web'][option]:
string = lang_dict['web'][option]
else:
string = default_lang_dict['web'][option]
except KeyError:
raise KeyError("Missed strings in language file: '{string}'. ".format(string=option))
return _tr(string, *argv, **kwargs)
def _tr(string, *argv, **kwargs):
if argv or kwargs:
try:
formatted = string.format(*argv, **kwargs)
return formatted
except KeyError as e:
raise KeyError(
"Missed/Unexpected placeholder {{{placeholder}}} in string '{string}'. ".format(placeholder=str(e).strip("'"), string=option)
+ "Please restore you configuration file back to default if necessary.")
"Missed/Unexpected placeholder {{{placeholder}}} in string "
"'{string}'. ".format(placeholder=str(e).strip("'"),
string=string))
except TypeError:
raise KeyError(
"Missed placeholder in string '{string}'. ".format(string=option)
+ "Please restore you configuration file back to default if necessary.")
"Missed placeholder in string '{string}'. ".format(string=string))
else:
return string
@ -28,5 +58,4 @@ def commands(command):
string = var.config.get("commands", command)
return string
except KeyError:
raise KeyError("Missed command in configuration file: '{string}'. ".format(string=command)
+ "Please restore you configuration file back to default if necessary.")
raise KeyError("Missed command in configuration file: '{string}'. ".format(string=command))

View File

@ -169,7 +169,7 @@ def requires_auth(f):
bad_access_count[request.remote_addr] = 1
log.info(f"web: bad token from ip {request.remote_addr}.")
return render_template('need_token.html',
return render_template(f'need_token.{var.language}.html',
name=var.config.get('bot', 'username'),
command=f"{var.config.get('commands', 'command_symbol')[0]}{var.config.get('commands', 'requests_webinterface_access')}")
@ -225,7 +225,7 @@ def get_all_dirs():
@web.route("/", methods=['GET'])
@requires_auth
def index():
return open('templates/index.html', "r").read()
return open(f"templates/index.{var.language}.html", "r").read()
@web.route("/playlist", methods=['GET'])
@ -554,7 +554,6 @@ def library_info():
tags = var.music_db.query_all_tags()
max_upload_file_size = util.parse_file_size(var.config.get("webinterface", "max_upload_file_size", fallback="30MB"))
print(get_all_dirs())
return jsonify(dict(
dirs=get_all_dirs(),
upload_enabled=var.config.getboolean("webinterface", "upload_enabled", fallback=True),

157
lang/en_US.json Normal file
View File

@ -0,0 +1,157 @@
{
"cli": {
"added_tags": "Added tags <i>{tags}</i> to <b>{song}</b>.",
"added_tags_to_all": "Added tags <i>{tags}</i> to songs on the playlist.",
"admin_help": "<h3>Admin command</h3>\n<b>Bot</b>\n<ul>\n<li><b>!<u>k</u>ill </b> - kill the bot</li>\n<li><b>!update </b> - update the bot</li>\n<li><b>!userban </b> {user} - ban a user</li>\n<li><b>!userunban </b> {user} - unban a user</li>\n<li><b>!urlbanlist </b> - list banned url</li>\n<li><b>!urlban </b> [{url}] - ban {url} (or current item's url by default) and remove this url from the library.</li>\n<li><b>!urlunban </b> {url} - unban {url}</li>\n<li><b>!rescan </b> {url} - rebuild local music file cache</li>\n<li><b>!dropdatabase</b> - clear the entire database, you will lose all settings and music library.</li>\n</ul>\n<b>Web Interface</b>\n<ul>\n<li><b>!<u>webuserlist</u></b> - list all users that have the permission of accessing the web interface, if auth mode is 'password'.</li>\n<li><b>!<u>webuseradd</u> {nick name}</b> - grant the user with {nick name} the access to the web interface, if auth mode is 'password'.</li>\n<li><b>!<u>webuserdel</u> {nick name}</b> - revoke the access to the web interface of {nick name}, if auth mode is 'password'.</li>\n<li><b>!update </b> - update the bot</li>\n<li><b>!userban </b> {user} - ban a user</li>\n</ul>",
"auto_paused": "Use <i>!play</i> to resume music!",
"bad_command": "<i>{command}</i>: command not found.",
"bad_parameter": "<i>{command}</i>: invalid parameter.",
"bad_url": "Bad URL requested.",
"cache_refreshed": "Cache refreshed!",
"change_ducking_volume": "Volume on ducking set to {volume} by {user}.",
"change_mode": "Playback mode set to <i>{mode}</i> by {user}.",
"change_volume": "Volume set to {volume} by {user}.",
"cleared": "Playlist emptied.",
"cleared_tags": "Removed all tags from <b>{song}</b>.",
"cleared_tags_from_all": "Removed all tags from songs on the playlist.",
"command_disabled": "{command}: command disabled!",
"current_ducking_volume": "Volume on ducking: {volume}.",
"current_mode": "Current playback mode is <i>{mode}</i>.",
"current_volume": "Current volume: {volume}.",
"database_dropped": "Database dropped. All records have gone.",
"download_in_progress": "Download of <b>{item}</b> in progress...",
"error_executing_command": "{command}: Command failed with error: {error}.",
"file": "File",
"file_added": "Added {item}.",
"file_deleted": "Deleted {item} from the library.",
"file_item": "<b>{artist} - {title}</b> <i>added by</i> {user}",
"file_missed": "Music file '{file}' missed! This item has been removed from the playlist.",
"help": "<h3>Commands</h3>\n<b>Control</b>\n<ul>\n<li> <b>!<u>w</u>eb</b> - get the URL of the web interface, if enabled. </li>\n<li> <b>!play </b> (or <b>!p</b>) [{num}] [{start_from}] - resume from pausing / start to play (the num-th song is num if given) </li>\n<li> <b>!<u>pa</u>use </b> - pause </li>\n<li> <b>!<u>st</u>op </b> - stop playing </li>\n<li> <b>!<u>sk</u>ip </b> - jump to the next song </li>\n<li> <b>!<u>la</u>st </b> - jump to the last song </li>\n<li> <b>!<u>v</u>olume </b> {volume} - get or change the volume (from 0 to 100) </li>\n<li> <b>!<u>m</u>ode </b> [{mode}] - get or set the playback mode, {mode} should be one of <i>one-shot</i> (remove\nitem once played), <i>repeat</i> (looping through the playlist), <i>random</i> (randomize the playlist),\n<i>autoplay</i> (randomly grab something from the music library).</li>\n<li> <b>!duck </b> on/off - enable or disable ducking function </li>\n<li> <b>!duckv </b> - set the volume of the bot when ducking is activated </li>\n<li> <b>!<u>duckt</u>hres </b> - set the threshold of volume to activate ducking (3000 by default) </li>\n<li> <b>!<u>o</u>ust </b> - stop playing and go to default channel </li>\n</ul>\n<b>Playlist</b>\n<ul>\n<li> <b>!<u>n</u>ow </b> (or <b>!np</b>) - display the current song </li>\n<li> <b>!<u>q</u>ueue </b> - display items in the playlist </li>\n<li> <b>!<u>t</u>ag </b> {tags} - add all items with tags {tags}, tags separated by \",\". </li>\n<li> <b>!file </b>(or <b>!f</b>) {path/folder/keyword} - add a single file to the playlist by its path or keyword in its path. </li>\n<li> <b>!<u>filem</u>atch </b>(or <b>!fm</b>) {pattern} - add all files that match regex {pattern} </li>\n<li> <b>!<u>ur</u>l </b> {url} - add Youtube or SoundCloud music </li>\n<li> <b>!<u>playl</u>ist </b> {url} [{offset}] - add all items in a Youtube or SoundCloud playlist, and start with the {offset}-th item </li>\n<li> <b>!<u>rad</u>io </b> {url} - append a radio {url} to the playlist </li>\n<li> <b>!<u>rbq</u>uery </b> {keyword} - query http://www.radio-browser.info for a radio station </li>\n<li> <b>!<u>rbp</u>lay </b> {id} - play a radio station with {id} (eg. !rbplay 96746) </li>\n<li> <b>!<u>ys</u>earch </b> {keywords} - query youtube. Use <i>!ysearch -n</i> to turn the page. </li>\n<li> <b>!<u>yp</u>lay </b> {keywords} - add the first search result of {keywords} into the playlist.</li>\n<li> <b>!<u>sh</u>ortlist </b> (or <b>!sl</b>) {indexes/*} - add {indexes}-th item (or all items if * is given) on the shortlist. </li>\n<li> <b>!rm </b> {num} - remove the num-th song on the playlist </li>\n<li> <b>!<u>rep</u>eat </b> [{num}] - repeat current song {num} (1 by default) times.</li>\n<li> <b>!<u>ran</u>dom </b> - randomize the playlist.</li>\n</ul>\n<b>Music Library</b>\n<ul>\n<li> <b>!<u>se</u>arch </b> {keywords} - find item with {keywords} in the music library, keywords separated by space.</li>\n<li> <b>!<u>li</u>stfile </b> [{pattern}] - display list of available files (whose paths match the regex pattern if {pattern} is given) </li>\n<li> <b>!<u>addt</u>ag </b> [{index}] {tags} - add {tags} to {index}-th(current song if {index} is omitted) item on the playlist, tags separated by \",\". </li>\n<li> <b>!<u>addt</u>ag </b> * {tags} - add {tags} to all items on the playlist. </li>\n<li> <b>!<u>un</u>tag </b> [{index/*}] {tags}/* - remove {tags}/all tags from {index}-th(current song if {index} is omitted) item on the playlist. </li>\n<li> <b>!<u>fin</u>dtagged </b> (or <b>!ft</b>) {tags} - find item with {tags} in the music library. </li>\n<li> <b>!<u>del</u>ete </b> {index} - delete {index}-th item on the shortlist from the music library. </li>\n</ul>\n<b>Other</b>\n<ul>\n<li> <b>!<u>j</u>oinme {token} </b> - join your own channel with {token}.</li>\n<li> <b>!<u>password</u> {password} </b> - change your password, used to access the web interface.</li>\n</ul>",
"invalid_index": "Invalid index <i>{index}</i>. Use '!queue' to see the playlist.",
"multiple_file_added": "Multiple items added:",
"multiple_file_deleted": "Multiple items deleted from the library:",
"multiple_file_found": "Found:",
"multiple_matches": "File not found! Possible candidates:",
"new_version_found": "<h2>Update Available!</h2> Version {new_version} of botamusique is available! <hr />\n<h3>Changelog</h3> {changelog} <hr /> Send <i>!update</i> to update!",
"no_file": "File not found.",
"not_admin": "You are not an admin!",
"not_in_my_channel": "You're not in my channel!",
"not_playing": "Nothing is playing right now.",
"now_playing": "Playing {item}",
"page_instruction": "Page {current}/{total}. Use <i>!{command} {{page}}</i> to navigate.",
"paused": "Music paused.",
"playlist_fetching_failed": "Unable to fetch the playlist!",
"pm_not_allowed": "Private message aren't allowed.",
"preconfigurated_radio": "Preconfigurated Radio available:",
"queue_contents": "Items on the playlist:",
"queue_empty": "Playlist is empty!",
"radio": "Radio",
"radio_item": "<a href=\"{url}\"><b>{title}</b></a> <i>from</i> {name} <i>added by</i> {user}",
"rb_play_empty": "Please specify a radio station ID!",
"rb_query_result": "This is the result of your query, send <i> !rbplay {ID} </i> to play a station:",
"records_omitted": "...",
"removed_tags": "Removed tags <i>{tags}</i> from <b>{song}</b>.",
"removed_tags_from_all": "Removed tags <i>{tags}</i> from songs on the playlist.",
"removing_item": "Removed entry {item} from playlist.",
"repeat": "Repeat {song} for {n} times.",
"shortlist_instruction": "Use <i>!sl {indexes}</i> to play the item you want.",
"start_updating": "Start updating...",
"stopped": "Music stopped.",
"too_long": "<b>{song}</b> is too long, removed from playlist!",
"unable_download": "Unable to download <b>{item}</b>. Removed from the library.",
"unable_play": "Unable to play <b>{item}</b>. Removed from the library.",
"unknown_mode": "Unknown playback mode '{mode}'. It should be one of <i>one-shot</i>, <i>repeat</i>, <i>random</i>.",
"update_successful": "<h2>botamusique v{version} Installed!</h2><hr />\n<h3>Changelog</h3> {changelog} <hr /> Visit <a href=\"https://github.com/azlux/botamusique\">our github repo</a> for more details!",
"url": "URL",
"url_ban": "This URL is banned!",
"url_from_playlist": "URL",
"url_from_playlist_item": "<a href=\"{url}\"><b>{title}</b></a> <i>from playlist</i> <a href=\"{playlist_url}\">{playlist}</a> <i>added by</i> {user}",
"url_item": "<a href=\"{url}\"><b>{title}</b></a> <i>added by</i> {user}",
"user_ban": "You are banned, not allowed to do that!",
"user_password_set": "Your password has been updated.",
"web_user_list": "Following users has the privilege to access the web interface: <br /> {users}",
"webpage_address": "Your own address to access the web interface is <a href=\"{address}\">{address}</a>",
"which_command": "Do you mean <br /> {commands}",
"wrong_pattern": "Invalid regex: {error}.",
"yt_no_more": "No more results!",
"yt_query_error": "Unable to query youtube!",
"yt_result": "Youtube query result: {result_table} Use <i>!sl {{indexes}}</i> to play the item you want. <br />\n<i>!ytquery -n</i> for the next page."
},
"web": {
"action": "Action",
"add": "Add",
"add_all": "Add All",
"add_radio": "Add Radio",
"add_radio_url": "Add Radio URL",
"add_to_bottom": "Add to bottom",
"add_to_bottom_of_current_playlist": "Add to bottom of current playlist",
"add_to_playlist_next": "Add to playlist right after current song",
"add_url": "Add URL",
"add_youtube_or_soundcloud_url": "Add Youtube or Soundcloud URL",
"are_you_really_sure": "Are you really sure?",
"aria_botamusique_logo": "Botamusique Logo: a person with two headphones, enjoying the music",
"aria_default_cover": "A black square with two eight notes beamed together.",
"aria_empty_box": "A drawing of an empty box.",
"aria_remove_this_song": "Remove this song from the current playlist",
"aria_skip_current_song": "Skip current song and play this song right now",
"aria_skip_to_next_track": "Skip to next track",
"aria_spinner": "A loading spinner",
"aria_warning_of_deletion": "Warning about deletion of files.",
"autoplay": "Autoplay",
"browse_music_file": "Browse Music file",
"cancel": "Cancel",
"cancel_upload_warning": "<strong>Are you really sure?</strong> <br /> Click again to abort uploading.",
"change_playback_mode": "Change Playback Mode",
"choose_file": "Choose file",
"clear_playlist": "Clear Playlist",
"close": "Close",
"delete_all": "Delete All",
"delete_all_files": "Delete All Listed Files",
"delete_file_warning": "All files listed here, include files on other pages, will be deleted from your hard-drive.\n Is that what you want?",
"directory": "Directory",
"download_all": "Download All",
"download_song_from_library": "Download song from library",
"edit_submit": "Edit!",
"edit_tags_for": "Edit tags for",
"expand_playlist": "See item <span\n class=\"playlist-expand-item-range\"></span> on the playlist.",
"file": "File",
"filters": "Filters",
"index": "#",
"keywords": "Keywords",
"keywords_placeholder": "Keywords...",
"mini_player_title": "Now Playing...",
"music_library": "Music Library",
"next_to_play": "Next to play",
"no_tag": "No tag",
"oneshot": "One-shot",
"open_volume_controls": "Open Volume Controls",
"page_title": "botamusique Web Interface",
"pause": "Pause",
"play": "Play",
"playlist_controls": "Playlist controls",
"radio": "Radio",
"radio_url_placeholder": "Radio URL...",
"random": "Random",
"remove_song_from_library": "Remove song from library",
"repeat": "Repeat",
"rescan_files": "Rescan Files",
"skip_track": "Skip Track",
"submit": "Submit",
"tags": "Tags",
"tags_to_add": "Tags to add",
"title": "Title",
"token": "Token",
"token_required": "Token Required",
"token_required_message": "You are accessing the web interface of {{ name }}.\nA token is needed to grant you access.<br />\nPlease send \"{{ command }}\" to the bot in mumble to acquire one.",
"type": "Type",
"upload_file": "Upload File",
"upload_submit": "Upload!",
"upload_to": "Upload To",
"uploaded_finished": "Uploaded finished!",
"uploading_files": "Uploading files...",
"url": "URL",
"url_path": "Url/Path",
"url_placeholder": "URL...",
"volume_slider": "Volume Slider"
}
}

95
lang/sync_translation.py Executable file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
import os
import re
import argparse
import requests
base_url = "https://translate.azlux.fr/api/v1"
project_id = "4aafb197-3282-47b3-a197-0ca870cf6ab2"
def get_access_header(client, secret):
data = {"grant_type": "client_credentials",
"client_id": client,
"client_secret": secret}
r = requests.post(f"{base_url}/auth/token", json=data)
if r.status_code != 200:
print("Access denied! Please check your client ID or secret.")
exit(1)
token = r.json()["access_token"]
headers = {"Authorization": "Bearer " + token,
"Accept": "application/json, text/plain, */*"}
return headers
def fetch_translation(r_client, r_secret):
headers = get_access_header(r_client, r_secret)
r = requests.get(f"{base_url}/projects/{project_id}/translations", headers=headers)
translations = r.json()['data']
for translation in translations:
lang_code = translation['locale']['code']
print(f" - Fetching {lang_code}")
params = {'locale': lang_code,
'format': 'jsonnested'}
r = requests.get(f"{base_url}/projects/{project_id}/exports", params=params, headers=headers)
with open(lang_code + ".json", "wb") as f:
f.write(r.content)
def push_strings(w_client, w_secret):
print("Pushing local translation files into the remote host...")
headers = get_access_header(w_client, w_secret)
lang_files = os.listdir('.')
lang_list = []
for lang_file in lang_files:
match = re.search("([a-z]{2}_[A-Z]{2})\.json", lang_file)
if match:
lang_list.append(match[1])
for lang in lang_list:
print(f" - Pushing {lang}")
params = {'locale': lang,
'format': 'jsonnested'}
files = {'file': open(lang + ".json", 'r')}
r = requests.post(f"{base_url}/projects/{project_id}/imports", params=params, headers=headers, files=files)
assert r.status_code == 200, f"Unable to push {lang} into remote host. {r.status_code}"
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Sync translation files with azlux's traduora server.")
parser.add_argument("--client", dest="client",
type=str, help="Client ID used to access the server.")
parser.add_argument("--secret", dest="secret",
type=str, help="Secret used to access the server.")
parser.add_argument("--fetch", dest='fetch', action="store_true",
help='Fetch translation files from the server.')
parser.add_argument("--push", dest='push', action="store_true",
help='Push local translation files into the server.')
args = parser.parse_args()
if not args.client or not args.secret:
print("Client ID and secret need to be provided!")
exit(1)
if args.push:
push_strings(args.client, args.secret)
if args.fetch:
fetch_translation(args.client, args.secret)
print("Done.")

View File

@ -9,7 +9,7 @@ from PIL import Image
import util
import variables as var
from media.item import BaseItem, item_builders, item_loaders, item_id_generators, ValidationFailedError
import constants
from constants import tr_cli as tr
'''
type : file
@ -75,7 +75,7 @@ class FileItem(BaseItem):
if not os.path.exists(self.uri()):
self.log.info(
"file: music file missed for %s" % self.format_debug_string())
raise ValidationFailedError(constants.strings('file_missed', file=self.path))
raise ValidationFailedError(tr('file_missed', file=self.path))
if self.duration == 0:
self.duration = util.get_media_duration(self.uri())
@ -185,14 +185,14 @@ class FileItem(BaseItem):
)
def format_song_string(self, user):
return constants.strings("file_item",
title=self.title,
artist=self.artist if self.artist else '??',
user=user
)
return tr("file_item",
title=self.title,
artist=self.artist if self.artist else '??',
user=user
)
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
display = tr("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>'
@ -208,4 +208,4 @@ class FileItem(BaseItem):
return title
def display_type(self):
return constants.strings("file")
return tr("file")

View File

@ -7,7 +7,7 @@ import hashlib
from media.item import BaseItem
from media.item import item_builders, item_loaders, item_id_generators
import constants
from constants import tr_cli as tr
log = logging.getLogger("bot")
@ -147,18 +147,18 @@ class RadioItem(BaseItem):
)
def format_song_string(self, user):
return constants.strings("radio_item",
url=self.url,
title=get_radio_title(self.url), # the title of current song
name=self.title, # the title of radio station
user=user
)
return tr("radio_item",
url=self.url,
title=get_radio_title(self.url), # the title of current song
name=self.title, # the title of radio station
user=user
)
def format_current_playing(self, user):
return constants.strings("now_playing", item=self.format_song_string(user))
return tr("now_playing", item=self.format_song_string(user))
def format_title(self):
return self.title if self.title else self.url
def display_type(self):
return constants.strings("radio")
return tr("radio")

View File

@ -9,7 +9,7 @@ import glob
from io import BytesIO
import base64
import constants
from constants import tr_cli as tr
import media
import variables as var
from media.item import BaseItem, item_builders, item_loaders, item_id_generators, ValidationFailedError, \
@ -99,7 +99,7 @@ class URLItem(BaseItem):
# Check the length, useful in case of playlist, it wasn't checked before)
log.info(
"url: " + self.url + " has a duration of " + str(self.duration / 60) + " min -- too long")
raise ValidationFailedError(constants.strings('too_long', song=self.title))
raise ValidationFailedError(tr('too_long', song=self.title))
else:
self.ready = "validated"
self.version += 1 # notify wrapper to save me
@ -138,7 +138,7 @@ class URLItem(BaseItem):
if not succeed:
self.ready = 'failed'
self.log.error("url: error while fetching info from the URL")
raise ValidationFailedError(constants.strings('unable_download', item=self.format_title()))
raise ValidationFailedError(tr('unable_download', item=self.format_title()))
def _download(self):
media.system.clear_tmp_folder(var.tmp_folder, var.config.getint('bot', 'tmp_folder_max_size'))
@ -193,7 +193,7 @@ class URLItem(BaseItem):
os.remove(f)
self.ready = "failed"
self.downloading = False
raise PreparationFailedError(constants.strings('unable_download', item=self.format_title()))
raise PreparationFailedError(tr('unable_download', item=self.format_title()))
def _read_thumbnail_from_file(self, path_thumbnail):
if os.path.isfile(path_thumbnail):
@ -226,14 +226,14 @@ class URLItem(BaseItem):
def format_song_string(self, user):
if self.ready in ['validated', 'yes']:
return constants.strings("url_item",
title=self.title if self.title else "??",
url=self.url,
user=user)
return tr("url_item",
title=self.title if self.title else "??",
url=self.url,
user=user)
return self.url
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
display = tr("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
@ -246,4 +246,4 @@ class URLItem(BaseItem):
return self.title if self.title.strip() else self.url
def display_type(self):
return constants.strings("url")
return tr("url")

View File

@ -1,5 +1,5 @@
import youtube_dl
import constants
from constants import tr_cli as tr
import variables as var
from media.item import item_builders, item_loaders, item_id_generators
from media.url import URLItem, url_item_id_generator
@ -96,15 +96,15 @@ class PlaylistURLItem(URLItem):
)
def format_song_string(self, user):
return constants.strings("url_from_playlist_item",
title=self.title,
url=self.url,
playlist_url=self.playlist_url,
playlist=self.playlist_title,
user=user)
return tr("url_from_playlist_item",
title=self.title,
url=self.url,
playlist_url=self.playlist_url,
playlist=self.playlist_title,
user=user)
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
display = tr("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
@ -114,4 +114,4 @@ class PlaylistURLItem(URLItem):
return display
def display_type(self):
return constants.strings("url_from_playlist")
return tr("url_from_playlist")

View File

@ -24,6 +24,7 @@ from packaging import version
import util
import command
import constants
from constants import tr_cli as tr
from database import SettingsDatabase, MusicDatabase, DatabaseMigration
import media.system
from media.item import ValidationFailedError, PreparationFailedError
@ -118,7 +119,7 @@ class MumbleBot:
self.mumble.start() # start the mumble thread
self.mumble.is_ready() # wait for the connection
if self.mumble.connected >= pymumble_py3.constants.PYMUMBLE_CONN_STATE_FAILED:
if self.mumble.connected >= pymumble.constants.PYMUMBLE_CONN_STATE_FAILED:
exit()
self.set_comment()
@ -179,7 +180,7 @@ class MumbleBot:
if not last_startup_version or version.parse(last_startup_version) < version.parse(self.version):
var.db.set("bot", "version", self.version)
changelog = util.fetch_changelog()
self.send_channel_msg(constants.strings("update_successful", version=self.version, changelog=changelog))
self.send_channel_msg(tr("update_successful", version=self.version, changelog=changelog))
# Set the CTRL+C shortcut
def ctrl_caught(self, signal, frame):
@ -206,7 +207,7 @@ class MumbleBot:
self.log.info(f"update: new version {new_version} found, current installed version {self.version}.")
self.log.info(f"update: changelog: {changelog}")
changelog = changelog.replace("\n", "<br>")
self.send_channel_msg(constants.strings('new_version_found', new_version=new_version, changelog=changelog))
self.send_channel_msg(tr('new_version_found', new_version=new_version, changelog=changelog))
else:
self.log.debug("update: no new version found.")
@ -263,13 +264,13 @@ class MumbleBot:
# Anti stupid guy function
if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_private_message') and text.session:
self.mumble.users[text.actor].send_text_message(
constants.strings('pm_not_allowed'))
tr('pm_not_allowed'))
return
for i in var.db.items("user_ban"):
if user.lower() == i[0]:
self.mumble.users[text.actor].send_text_message(
constants.strings('user_ban'))
tr('user_ban'))
return
if not self.is_admin(user) and parameter:
@ -278,7 +279,7 @@ class MumbleBot:
for i in var.db.items("url_ban"):
if input_url == i[0]:
self.mumble.users[text.actor].send_text_message(
constants.strings('url_ban'))
tr('url_ban'))
return
command_exc = ""
@ -299,15 +300,15 @@ class MumbleBot:
elif len(matches) > 1:
self.mumble.users[text.actor].send_text_message(
constants.strings('which_command', commands="<br>".join(matches)))
tr('which_command', commands="<br>".join(matches)))
return
else:
self.mumble.users[text.actor].send_text_message(
constants.strings('bad_command', command=command))
tr('bad_command', command=command))
return
if self.cmd_handle[command_exc]['admin'] and not self.is_admin(user):
self.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
self.mumble.users[text.actor].send_text_message(tr('not_admin'))
return
if not self.cmd_handle[command_exc]['access_outside_channel'] \
@ -315,7 +316,7 @@ class MumbleBot:
and not var.config.getboolean('bot', 'allow_other_channel_message') \
and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']:
self.mumble.users[text.actor].send_text_message(
constants.strings('not_in_my_channel'))
tr('not_in_my_channel'))
return
self.cmd_handle[command_exc]['handle'](self, user, text, command_exc, parameter)
@ -323,7 +324,7 @@ class MumbleBot:
error_traceback = traceback.format_exc()
error = error_traceback.rstrip().split("\n")[-1]
self.log.error(f"bot: command {command_exc} failed with error: {error_traceback}\n")
self.send_msg(constants.strings('error_executing_command', command=command_exc, error=error), text)
self.send_msg(tr('error_executing_command', command=command_exc, error=error), text)
def send_msg(self, msg, text):
msg = msg.encode('utf-8', 'ignore').decode('utf-8')
@ -355,7 +356,7 @@ class MumbleBot:
if var.config.get("bot", "when_nobody_in_channel") == "pause_resume":
self.resume()
elif var.config.get("bot", "when_nobody_in_channel") == "pause":
self.send_channel_msg(constants.strings("auto_paused"))
self.send_channel_msg(tr("auto_paused"))
elif len(own_channel.get_users()) == 1:
# if the bot is the only user left in the channel
@ -436,7 +437,7 @@ class MumbleBot:
self.log.info("bot: current music isn't ready, start downloading.")
self.async_download(item)
self.send_channel_msg(
constants.strings('download_in_progress', item=item.format_title()))
tr('download_in_progress', item=item.format_title()))
def _download(self, item):
ver = item.version
@ -514,7 +515,7 @@ class MumbleBot:
self.log.error("bot: with ffmpeg error: %s", self.last_ffmpeg_err)
self.last_ffmpeg_err = ""
self.send_channel_msg(constants.strings('unable_play', item=current.format_title()))
self.send_channel_msg(tr('unable_play', item=current.format_title()))
var.playlist.remove_by_id(current.id)
var.cache.free_and_delete(current.id)
@ -713,6 +714,8 @@ def start_web_interface(addr, port):
if __name__ == '__main__':
supported_languages = util.get_supported_language()
parser = argparse.ArgumentParser(
description='Bot for playing music on Mumble')
@ -720,9 +723,11 @@ if __name__ == '__main__':
parser.add_argument("--config", dest='config', type=str, default='configuration.ini',
help='Load configuration from this file. Default: configuration.ini')
parser.add_argument("--db", dest='db', type=str,
default=None, help='settings database file. Default: settings-{username_of_the_bot}.db')
default=None, help='Settings database file')
parser.add_argument("--music-db", dest='music_db', type=str,
default=None, help='music library database file. Default: music.db')
default=None, help='Music library database file')
parser.add_argument("--lang", dest='lang', type=str, default=None,
help='Preferred language. Support ' + ", ".join(supported_languages))
parser.add_argument("-q", "--quiet", dest="quiet",
action="store_true", help="Only Error logs")
@ -814,6 +819,25 @@ if __name__ == '__main__':
var.music_folder = util.solve_filepath(var.config.get('bot', 'music_folder'))
var.tmp_folder = util.solve_filepath(var.config.get('bot', 'tmp_folder'))
# ======================
# Translation
# ======================
lang = ""
if args.lang:
lang = args.lang
else:
lang = var.config.get('bot', 'language', fallback='en_US')
if lang not in supported_languages:
raise KeyError(f"Unsupported language {lang}")
var.language = lang
constants.load_lang(lang)
# ======================
# Prepare Cache
# ======================
var.cache = MusicCache(var.music_db)
if var.config.getboolean("bot", "refresh_cache_on_startup", fallback=True):

58
templates/translate.py Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
import os
import json
import re
import jinja2
default_lang_dict = {}
lang_dict = {}
def load_lang(lang):
with open(f"../lang/{lang}.json", "r") as f:
return json.load(f)
def tr(option):
try:
if option in lang_dict['web'] and lang_dict['web'][option]:
string = lang_dict['web'][option]
else:
string = default_lang_dict['web'][option]
return string
except KeyError:
raise KeyError("Missed strings in language file: '{string}'. ".format(string=option))
if __name__ == "__main__":
html_files = os.listdir('.')
for html_file in html_files:
match = re.search("(.+)\.template\.html", html_file)
if match is None:
continue
print(f"Populating {html_file} with translations...")
basename = match[1]
with open(html_file, "r") as f:
html = f.read()
lang_files = os.listdir('../lang')
lang_list = []
default_lang_dict = load_lang("en_US")
for lang_file in lang_files:
match = re.search("([a-z]{2}_[A-Z]{2})\.json", lang_file)
if match:
lang_list.append(match[1])
template = jinja2.Template(html)
for lang in lang_list:
print(f" - Populating {lang}...")
lang_dict = load_lang(lang)
with open(f"{basename}.{lang}.html", "w") as f:
f.write(template.render(tr=tr))
print("Done.")

12
util.py
View File

@ -8,7 +8,6 @@ import io
import sys
import variables as var
import zipfile
import requests
import re
import subprocess as sp
import logging
@ -416,6 +415,17 @@ def verify_password(password, salted_hash, salt):
return False
def get_supported_language():
lang_files = os.listdir('lang')
lang_list = []
for lang_file in lang_files:
match = re.search("([a-z]{2}_[A-Z]{2})\.json", lang_file)
if match:
lang_list.append(match[1])
return lang_list
class LoggerIOWrapper(io.TextIOWrapper):
def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer):
super().__init__(fallback_io_buffer, write_through=True)

View File

@ -23,3 +23,5 @@ bot_logger = None
music_folder = ""
tmp_folder = ""
language = ""

View File

@ -816,7 +816,7 @@ const modal_tag = $('.modal-tag');
const modal_tag_text = $('.modal-tag-text');
function addTagModalShow(_id, _title, _tag_tuples) {
add_tag_modal_title.html('Edit tags for ' + _title);
add_tag_modal_title.html(_title);
add_tag_modal_item_id.val(_id);
add_tag_modal_tags.empty();
_tag_tuples.forEach(function (tag_tuple) {

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8">
<title>botamusique web interface</title>
<title>{{ tr('page_title') }}</title>
<link rel="icon" href="static/image/favicon.ico" />
@ -18,46 +18,46 @@
<div class="row">
<div class="col-auto">
<img src="static/image/logo.png" height="200px"
alt="Botamusique Logo: a person with two headphones, enjoying the music">
alt="{{ tr('aria_botamusique_logo') }}">
</div>
<div class="col my-auto">
<h1>botamusique Web Interface</h1>
<h1>{{ tr('page_title') }}</h1>
</div>
</div>
</header>
<main id="playlist" class="container mb-5">
<div class="btn-toolbar mb-2" role="toolbar" aria-label="Playlist controls">
<button type="button" id="play-pause-btn" class="btn btn-info mb-2 btn-space" aria-label="Play">
<div class="btn-toolbar mb-2" role="toolbar" aria-label="{{ tr('playlist_controls') }}">
<button type="button" id="play-pause-btn" class="btn btn-info mb-2 btn-space" aria-label="{{ tr('play') }}">
<i class="fas fa-play"></i>
</button>
<button type="button" id="fast-forward-btn" class="btn btn-info mb-2" aria-label="Skip Track">
<button type="button" id="fast-forward-btn" class="btn btn-info mb-2" aria-label="{{ tr('skip_track') }}">
<i class="fas fa-fast-forward"></i>
</button>
<div class="ml-auto">
<div class="dropdown mr-2">
<button class="btn btn-secondary dropdown-toggle" type="button" id="play-mode"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-label="Change Playback Mode">
aria-label="{{ tr('change_playback_mode') }}">
<i class="fas fa-tasks mr-2" aria-hidden="true" id="modeIndicator"></i>
</button>
<div class="dropdown-menu" aria-labelledby="play-mode">
<a class="dropdown-item" href="#" id="one-shot-mode-btn">
<i class="fas fa-tasks mr-2" aria-hidden="true"></i>One-shot
<i class="fas fa-tasks mr-2" aria-hidden="true"></i> {{ tr('oneshot') }}
</a>
<a class="dropdown-item" href="#" id="random-mode-btn">
<i class="fas fa-random mr-2" aria-hidden="true"></i>Random
<i class="fas fa-random mr-2" aria-hidden="true"></i> {{ tr('random') }}
</a>
<a class="dropdown-item" href="#" id="repeat-mode-btn">
<i class="fas fa-redo mr-2" aria-hidden="true"></i>Repeat
<i class="fas fa-redo mr-2" aria-hidden="true"></i> {{ tr('repeat') }}
</a>
<a class="dropdown-item" href="#" id="autoplay-mode-btn">
<i class="fas fa-robot mr-2" aria-hidden="true"></i>Autoplay
<i class="fas fa-robot mr-2" aria-hidden="true"></i> {{ tr('autoplay') }}
</a>
</div>
</div>
<button type="button" id="volume-popover-btn" class="btn btn-warning ml-1"
aria-label="Open Volume Controls">
aria-label="{{ tr('open_volume_controls') }}">
<i class="fa fa-volume-up" aria-hidden="true"></i>
</button>
@ -67,7 +67,7 @@
</a>
<input type="range" class="custom-range ml-1" id="volume-slider" min="0" max="1" step="0.01"
value="0.5" aria-label="Volume Slider" />
value="0.5" aria-label="{{ tr('volume_slider') }}" />
<a id="volume-up-btn">
<i class="fa fa-volume-up" aria-hidden="true"></i>
@ -81,29 +81,28 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col" class="d-none d-md-table-cell">#</th>
<th scope="col" class="w-50">Title</th>
<th scope="col" class="d-none d-md-table-cell">Url/Path</th>
<th scope="col">Action</th>
<th scope="col" class="d-none d-md-table-cell">{{ tr('index') }}</th>
<th scope="col" class="w-50">{{ tr('title') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ tr('url_path') }}</th>
<th scope="col">{{ tr('action') }}</th>
</tr>
</thead>
<tbody id="playlist-table" class="playlist-table">
<tr id="playlist-loading">
<td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg"
alt="A loading spinner" />
alt="{{ tr('aria_spinner') }}" />
</td>
</tr>
<tr id="playlist-empty" class="d-none">
<td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg"
alt="A drawing of an empty box." />
alt="{{ tr('aria_empty_box') }}" />
</td>
</tr>
<tr class="playlist-expand table-dark d-none">
<td colspan="4" class="text-center">
<a class="text-muted" href="javascript:">See item <span
class="playlist-expand-item-range"></span> on the playlist.</a>
<a class="text-muted" href="javascript:">{{ tr('expand_playlist') }}</a>
</td>
</tr>
<tr class="playlist-item-template d-none" aria-hidden="true">
@ -112,7 +111,7 @@
<input hidden type="hidden" class="playlist-item-id" value="" />
<div class="float-left">
<img width="80" class="playlist-item-thumbnail" src="static/image/unknown-album.png"
alt="A black square with two eight notes beamed together." />
alt="{{ tr('aria_default_cover') }}" />
</div>
<div class="playlist-artwork">
<b class="playlist-item-title"></b>
@ -134,11 +133,11 @@
<td>
<div class="btn-group">
<button type="button" class="playlist-item-play btn btn-info btn-sm"
aria-label="Skip current song and play this song right now">
aria-label="{{ tr('aria_skip_current_song') }}">
<i class="fas fa-play" aria-hidden="true"></i>
</button>
<button type="button" class="playlist-item-trash btn btn-danger btn-sm ml-1"
aria-label="Remove this song from the current playlist">
aria-label="{{ tr('aria_remove_this_song') }}">
<i class="fas fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
@ -150,52 +149,51 @@
<div class="btn-group">
<button type="button" id="clear-playlist-btn" class="btn btn-danger mr-1">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist
</button>
<i class="fas fa-trash-alt" aria-hidden="true"></i> {{ tr('clear_playlist') }}</button>
</div>
</main>
<div class="container mb-3">
<h2 id="forms">Music Library</h2>
<h2 id="forms">{{ tr('music_library') }}</h2>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title">Filters</h3>
<h3 class="card-title">{{ tr('filters') }}</h3>
<hr>
<div class="row">
<div class="col">
<fieldset id="filter-type" class="mb-2">
<legend>Type</legend>
<legend>{{ tr('type') }}</legend>
<div class="btn-group btn-group-sm btn-group-toggle">
<label id="filter-type-file" class="btn btn-secondary">
<input type="checkbox" name="options"> File
<input type="checkbox" name="options">{{ tr('file') }}
</label>
<label id="filter-type-url" class="btn btn-secondary">
<input type="checkbox" name="options"> URL
<input type="checkbox" name="options">{{ tr('url') }}
</label>
<label id="filter-type-radio" class="btn btn-secondary">
<input type="checkbox" name="options"> Radio
<input type="checkbox" name="options">{{ tr('radio') }}
</label>
</div>
</fieldset>
<label for="filter-dir">Directory</label>
<label for="filter-dir">{{ tr('directory') }}</label>
<div id="filter-path" class="input-group mb-2">
<select class="form-control form-control-sm" id="filter-dir" disabled>
<option value="">.</option>
</select>
</div>
<label for="filter-keywords">Keywords</label>
<label for="filter-keywords">{{ tr('keywords') }}</label>
<div id="filter-path" class="input-group mb-2">
<input class="form-control form-control-sm" id="filter-keywords" name="keywords"
placeholder="Keywords..." style="margin-top:5px;" />
placeholder="{{ tr('keywords_placeholder') }}" style="margin-top:5px;" />
</div>
</div>
<div class="col">
<fieldset id="filter-tags">
<legend>Tags</legend>
<legend>{{ tr('tags') }}</legend>
<span class="filter-tag tag-unclicked tag-click badge"></span>
</fieldset>
</div>
@ -205,11 +203,11 @@
<div id="library-group" class="list-group library-group" style="overflow: auto;">
<div id="library-item-loading" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg"
alt="A loading spinner" />
alt="{{ tr('aria_spinner') }}" />
</div>
<div id="library-item-empty" style="display: none" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg"
alt="A drawing of an empty box." />
alt="{{ tr('aria_empty_box') }}" />
</div>
<div id="library-item" style="display: none;" class="list-group-item library-item">
<input hidden type="hidden" class="library-item-id" value="" />
@ -217,10 +215,10 @@
<div class="library-thumb-col">
<div class="library-thumb-img">
<img class="library-item-thumb library-thumb-img" src="static/image/unknown-album.png"
alt="A black square with two eight notes beamed together." />
alt="{{ tr('aria_default_cover') }}" />
</div>
<div class="btn-group-vertical library-thumb-grp">
<div class="library-item-play btn btn-secondary library-thumb-btn-up" title="Play">
<div class="library-item-play btn btn-secondary library-thumb-btn-up" title="{{ tr('play') }}">
<i class="fas fa-play" aria-hidden="true"></i>
</div>
</div>
@ -239,14 +237,14 @@
<div class="library-item-tags">
<a class="tag-space tag-click library-item-edit"><i class="fas fa-edit"
style="color: #AAAAAA"></i></a>
<span class="library-item-notag badge badge-light text-muted font-italic">No tag</span>
<span class="library-item-notag badge badge-light text-muted font-italic">{{ tr('no_tag') }}</span>
<span class="library-item-tag tag-space badge">Tag</span>
</div>
</div>
<div class="btn-group library-action">
<button class="library-item-add-next btn btn-info btn-sm btn-space" type="button"
title="Next to play" aria-label="Add to playlist right after current song">
title="{{ tr('next_to_play') }}" aria-label="{{ tr('add_to_playlist_next') }}">
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;"
viewBox="5 5 17 17">
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z">
@ -257,7 +255,7 @@
</svg>
</button>
<button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space"
type="button" title="Add to bottom" aria-label="Add to bottom of current playlist">
type="button" title="{{ tr('add_to_bottom') }}" aria-label="{{ tr('add_to_bottom_of_current_playlist') }}">
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;"
viewBox="2 2 20 20">
<path
@ -266,11 +264,11 @@
</svg>
</button>
<button class="library-item-download btn btn-primary btn-sm btn-space" type="button"
aria-label="Download song from library">
aria-label="{{ tr('download_song_from_library') }}">
<i class="fas fa-download" aria-hidden="true"></i>
</button>
<button class="library-item-trash btn btn-danger btn-sm btn-space" type="button"
aria-label="Remove song from library">
aria-label="{{ tr('remove_song_from_library') }}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
@ -290,40 +288,36 @@
<div class="btn-group mb-2" role="group">
<button id="add-to-playlist-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-plus" aria-hidden="true"></i> Add All
<i class="fas fa-plus" aria-hidden="true"></i>{{ tr('add_all') }}
</button>
<button id="library-rescan-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
<i class="fas fa-sync-alt" aria-hidden="true"></i>{{ tr('rescan_files') }}
</button>
<button id="library-download-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-download" aria-hidden="true"></i> Download All
<i class="fas fa-download" aria-hidden="true"></i>{{ tr('download_all') }}
</button>
<button type="button" class="btn btn-danger mr-1" data-toggle="modal"
data-target="#deleteWarningModal">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All
<i class="fas fa-trash-alt" aria-hidden="true"></i>{{ tr('delete_all') }}
</button>
</div>
<!-- QUESTION: should this div have aria-hidden as true?? -->
<div class="modal fade" id="deleteWarningModal" tabindex="-1" role="dialog"
aria-label="Modal Window for warning about deletion of files." aria-hidden="true">
aria-label="{{ tr('aria_warning_of_deletion') }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="deleteWarningModalLabel">Are you really sure?</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<h3 class="modal-title" id="deleteWarningModalLabel">{{ tr('are_you_really_sure') }}</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr('close') }}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
All files listed here, include files on other pages, will be deleted from your
hard-drive.
Is that what you want?
</div>
{{ tr('delete_file_warning') }}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ tr('close') }}</button>
<button id="library-delete-btn" type="button" class="btn btn-danger"
data-dismiss="modal">Delete All Listed Files</button>
data-dismiss="modal">{{ tr('delete_all_files') }}</button>
</div>
</div>
</div>
@ -336,7 +330,7 @@
<input type="hidden" id="uploadDisabled" value="false" />
<div class="card">
<div class="card-header">
<h3 class="card-title">Upload File</h3>
<h3 class="card-title">{{ tr('upload_file') }}</h3>
</div>
<div class="card-body">
<form action="./upload" method="post" enctype="multipart/form-data">
@ -345,8 +339,8 @@
<div class="input-group mb-3">
<div id="uploadField" style="display: flex; width: 100%">
<div class="custom-file">
<input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile" aria-describedby="uploadSubmit" value="Browse Music file" multiple />
<label class="custom-file-label" for="uploadSelectFile">Choose file</label>
<input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile" aria-describedby="uploadSubmit" value="{{ tr('browse_music_file') }}" multiple />
<label class="custom-file-label" for="uploadSelectFile">{{ tr('choose_file') }}</label>
</div>
</div>
</div>
@ -356,7 +350,7 @@
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label for="uploadTargetDir" class="input-group-text">Upload To</label>
<label for="uploadTargetDir" class="input-group-text">{{ tr('upload_to') }}</label>
</div>
<input class="form-control" list="upload-target-dirs" id="uploadTargetDir" name="upload-target-dirs" placeholder="uploads" />
<datalist id="upload-target-dirs">
@ -364,7 +358,7 @@
</div>
</div>
<div class="col-auto">
<button class="btn btn-primary" type="button" id="uploadSubmit"><i class="fas fa-upload mr-1"></i>Upload!</button>
<button class="btn btn-primary" type="button" id="uploadSubmit"><i class="fas fa-upload mr-1"></i>{{ tr('upload_submit') }}</button>
</div>
</div>
</div>
@ -378,29 +372,29 @@
<div class="card-deck">
<div id="add-music-url" class="card">
<div class="card-header">
<h3 class="card-title">Add URL</h3>
<h3 class="card-title">{{ tr('add_url') }}</h3>
</div>
<div class="card-body">
<label for="music-url-input">Add Youtube or Soundcloud URL</label>
<label for="music-url-input">{{ tr('add_youtube_or_soundcloud_url') }}</label>
<div class="input-group mb-2">
<input class="form-control" type="text" id="music-url-input" placeholder="URL...">
<input class="form-control" type="text" id="music-url-input" placeholder="{{ tr('url_placeholder') }}">
</div>
<button type="submit" class="btn btn-primary">
Add URL
{{ tr('add_url') }}
</button>
</div>
</div>
<div id="add-radio-url" class="card">
<div class="card-header">
<h3 class="card-title">Add Radio</h3>
<h3 class="card-title">{{ tr('add_radio') }}</h3>
</div>
<div class="card-body">
<label for="radio-url-input">Add Radio URL</label>
<label for="radio-url-input">{{ tr('add_radio_url') }}</label>
<div class="input-group mb-2">
<input id="radio-url-input" class="form-control" type="text" placeholder="Radio Address...">
<input id="radio-url-input" class="form-control" type="text" placeholder="{{ tr('radio_url_placeholder') }}">
</div>
<button type="submit" class="btn btn-primary">
Add Radio
{{ tr('add_radio') }}
</button>
</div>
</div>
@ -418,26 +412,26 @@
<div id="playerToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<i class="fas fa-play-circle mr-2 text-primary"></i>
<strong class="mr-auto">Now Playing...</strong>
<strong class="mr-auto">{{ tr('mini_player_title') }}</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body" id="playerContainer">
<img id="playerArtworkIdle" src="static/image/empty_box.svg" alt="A drawing of an empty box." />
<img id="playerArtworkIdle" src="static/image/empty_box.svg" alt="{{ tr('aria_empty_box') }}" />
<img id="playerArtwork" src="static/image/unknown-album.png" style="display: none;"
alt="A black square with two eight notes beamed together." />
alt="{{ tr('aria_default_cover') }}" />
<div id="playerInfo">
<div id="playerActionBox">
<button id="playerPlayBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
aria-label="Play">
aria-label="{{ tr('play') }}">
<i class="fas fa-play"></i>
</button>
<button id="playerPauseBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
aria-label="Pause">
aria-label="{{ tr('pause') }}">
<i class="fas fa-pause"></i>
</button>
<button id="playerSkipBtn" class="btn btn-primary btn-sm" aria-label="Skip to next track">
<button id="playerSkipBtn" class="btn btn-primary btn-sm" aria-label="{{ tr('aria_skip_to_next_track') }}">
<i class="fas fa-fast-forward"></i>
</button>
</div>
@ -470,7 +464,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="addTagModalTitle">Edit tags for ?</h3>
<h3 class="modal-title">{{ tr('edit_tags_for') }} <span id="addTagModalTitle">?</span></h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@ -486,17 +480,17 @@
</div>
<div class="input-group">
<input class="form-control form-control-sm btn-space" type="text" id="addTagModalInput"
placeholder="tag1,tag2,..." aria-label="Tags to add">
placeholder="tag1,tag2,..." aria-label="{{ tr('tags_to_add') }}">
<button id="addTagModalAddBtn" type="button" class="btn btn-primary btn-sm">
<i class="fas fa-plus" aria-hidden="true"></i>
Add
{{ tr('add') }}
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ tr('close') }}</button>
<button id="addTagModalSubmit" type="button" class="btn btn-success"
data-dismiss="modal">Edit!</button>
data-dismiss="modal">{{ tr('edit_submit') }}</button>
</div>
</div>
</div>
@ -507,12 +501,12 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>Uploading files...</h3>
<h3 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>{{ tr('uploading_files') }}</h3>
</div>
<div id="uploadModalBody" class="modal-body">
<div id="uploadSuccessAlert" class="alert alert-success" role="alert" style="display: none">
<i class="fas fa-check mr-1"></i>
Uploaded finished!
{{ tr('uploaded_finished') }}
</div>
<div id="uploadModalList" style="margin-left: 5px; margin-bottom: 10px;">
<div class="uploadItem" style="display: none; width: 100%; padding-bottom: 8px;">
@ -529,11 +523,11 @@
</div>
<div class="modal-footer">
<button type="button" id="uploadClose" class="btn btn-success" data-dismiss="modal">
<i class="fas fa-times mr-1"></i> Close</button>
<i class="fas fa-times mr-1"></i> {{ tr('close') }}</button>
<button type="button" id="uploadCancel" class="btn btn-danger" data-toggle="tooltip"
data-html="true"
title="<strong>Are you really sure?</strong> <br /> Click again to abort uploading.">
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Cancel</button>
title="{{ tr('cancel_upload_warning') }}">
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> {{ tr('cancel') }}</button>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8">
<title>botamusique web interface</title>
<title>{{ tr('page_title') }}</title>
<link rel="icon" href="static/image/favicon.ico" />
@ -16,19 +16,17 @@
<div class="col-8" style="margin: auto; padding-top: 50px;">
<div class="card">
<div class="card-header">
Token Required
{{ tr('token_required') }}
</div>
<div class="card-body">
<h3>Token Required!</h3>
You are accessing the web interface of {{ name }}.
A token is needed to grant you access.<br />
Please send "{{ command }}" to the bot in mumble to acquire one.
<h3>{{ tr('token_required') }}</h3>
{{ tr('token_required_message') }}
<form action="." method="get">
<div class="form-group mt-3">
<label for="token_input">Token</label>
<label for="token_input">{{ tr('token') }}</label>
<div class="input-group">
<input type="password" class="form-control btn-space" id="token_input" name="token" placeholder="xxxxxxx">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-primary">{{ tr('submit') }}</button>
</div>
</div>
</form>

View File

@ -29,13 +29,13 @@ module.exports = {
filename: 'static/css/[name].css',
}),
new HtmlWebpackPlugin({
filename: 'templates/index.html',
template: './templates/index.html',
filename: 'templates/index.template.html',
template: './templates/index.template.html',
inject: false,
}),
new HtmlWebpackPlugin({
filename: 'templates/need_token.html',
template: './templates/need_token.html',
filename: 'templates/need_token.template.html',
template: './templates/need_token.template.html',
inject: false,
}),
],