Merge pull request #100 from Lartza/pep8

Cleanup
This commit is contained in:
azlux 2020-03-10 23:30:35 +01:00 committed by GitHub
commit 1a97d1c588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 194 additions and 208 deletions

View File

@ -1,6 +1,5 @@
# coding=utf-8 # coding=utf-8
import logging import logging
import os.path
import pymumble.pymumble_py3 as pymumble import pymumble.pymumble_py3 as pymumble
import re import re
@ -10,15 +9,13 @@ import util
import variables as var import variables as var
from librb import radiobrowser from librb import radiobrowser
from database import SettingsDatabase, MusicDatabase from database import SettingsDatabase, MusicDatabase
from media.item import item_builders, item_loaders, item_id_generators, dict_to_item, dicts_to_items from media.item import item_id_generators, dict_to_item, dicts_to_items
from media.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags from media.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags
from media.file import FileItem from media.url_from_playlist import get_playlist_info
from media.url_from_playlist import PlaylistURLItem, get_playlist_info
from media.url import URLItem
from media.radio import RadioItem
log = logging.getLogger("bot") log = logging.getLogger("bot")
def register_all_commands(bot): def register_all_commands(bot):
bot.register_command(constants.commands('joinme'), cmd_joinme, no_partial_match=False, access_outside_channel=True) bot.register_command(constants.commands('joinme'), cmd_joinme, no_partial_match=False, access_outside_channel=True)
bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True) bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True)
@ -71,6 +68,7 @@ def register_all_commands(bot):
bot.register_command('loop', cmd_loop_state, True) bot.register_command('loop', cmd_loop_state, True)
bot.register_command('item', cmd_item, True) bot.register_command('item', cmd_item, True)
def send_multi_lines(bot, lines, text, linebreak="<br />"): def send_multi_lines(bot, lines, text, linebreak="<br />"):
global log global log
@ -79,19 +77,20 @@ def send_multi_lines(bot, lines, text, linebreak="<br />"):
for newline in lines: for newline in lines:
msg += br msg += br
br = linebreak br = linebreak
if (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4) != 0: # 4 == len("<br>") if (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4) != 0: # 4 == len("<br>")
bot.send_msg(msg, text) bot.send_msg(msg, text)
msg = "" msg = ""
msg += newline msg += newline
bot.send_msg(msg, text) bot.send_msg(msg, text)
# ---------------- Variables ----------------- # ---------------- Variables -----------------
song_shortlist = [] song_shortlist = []
# ---------------- Commands ------------------
# ---------------- Commands ------------------
def cmd_joinme(bot, user, text, command, parameter): def cmd_joinme(bot, user, text, command, parameter):
global log global log
@ -146,6 +145,7 @@ def cmd_url_ban(bot, user, text, command, parameter):
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return return
def cmd_url_ban_list(bot, user, text, command, parameter): def cmd_url_ban_list(bot, user, text, command, parameter):
if bot.is_admin(user): if bot.is_admin(user):
bot.mumble.users[text.actor].send_text_message(util.get_url_ban()) bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
@ -153,6 +153,7 @@ def cmd_url_ban_list(bot, user, text, command, parameter):
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return return
def cmd_url_unban(bot, user, text, command, parameter): def cmd_url_unban(bot, user, text, command, parameter):
global log global log
@ -170,9 +171,11 @@ def cmd_play(bot, user, text, command, parameter):
if len(var.playlist) > 0: if len(var.playlist) > 0:
if parameter: if parameter:
if parameter.isdigit() and 1 <= int(parameter) <= len(var.playlist): if parameter.isdigit() and 1 <= int(parameter) <= len(var.playlist):
var.playlist.point_to(int(parameter) - 1 - 1) # First "-1" transfer 12345 to 01234, second "-1" # First "-1" transfer 12345 to 01234, second "-1"
# point to the previous item. the loop will next to # point to the previous item. the loop will next to
# the one you want # the one you want
var.playlist.point_to(int(parameter) - 1 - 1)
bot.interrupt() bot.interrupt()
else: else:
bot.send_msg(constants.strings('invalid_index', index=parameter), text) bot.send_msg(constants.strings('invalid_index', index=parameter), text)
@ -241,7 +244,7 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
else: else:
# try to do a partial match # try to do a partial match
files = var.cache.files files = var.cache.files
matches = [ file for file in files if parameter.lower() in file.lower()] matches = [file for file in files if parameter.lower() in file.lower()]
if len(matches) == 1: if len(matches) == 1:
file = matches[0] file = matches[0]
music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user) music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
@ -250,7 +253,7 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string())) bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
return return
elif len(matches) > 1: elif len(matches) > 1:
msgs = [ constants.strings('multiple_matches') ] msgs = [constants.strings('multiple_matches')]
song_shortlist = [] song_shortlist = []
for index, match in enumerate(matches): for index, match in enumerate(matches):
id = var.cache.file_id_lookup[match] id = var.cache.file_id_lookup[match]
@ -274,10 +277,9 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=False): def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=False):
global log global log
music_folder = var.music_folder
if parameter: if parameter:
files = var.cache.files files = var.cache.files
msgs = [ constants.strings('multiple_file_added') + "<ul>"] msgs = [constants.strings('multiple_file_added') + "<ul>"]
count = 0 count = 0
try: try:
music_wrappers = [] music_wrappers = []
@ -289,12 +291,12 @@ def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cach
music_wrappers.append(music_wrapper) music_wrappers.append(music_wrapper)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string()) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
msgs.append("<li><b>{}</b> ({})</li>".format(music_wrapper.item().title, msgs.append("<li><b>{}</b> ({})</li>".format(music_wrapper.item().title,
file[:match.span()[0]] file[:match.span()[0]]
+ "<b style='color:pink'>" + "<b style='color:pink'>"
+ file[match.span()[0]: match.span()[1]] + file[match.span()[0]: match.span()[1]]
+ "</b>" + "</b>"
+ file[match.span()[1]:] + file[match.span()[1]:]
)) ))
if count != 0: if count != 0:
msgs.append("</ul>") msgs.append("</ul>")
@ -331,7 +333,6 @@ def cmd_play_url(bot, user, text, command, parameter):
bot.send_msg(constants.strings('bad_parameter', command=command)) bot.send_msg(constants.strings('bad_parameter', command=command))
def cmd_play_playlist(bot, user, text, command, parameter): def cmd_play_playlist(bot, user, text, command, parameter):
global log global log
@ -480,8 +481,10 @@ def cmd_rb_play(bot, user, text, command, parameter):
msg += "No playable url found for this station, please try another station." msg += "No playable url found for this station, please try another station."
bot.send_msg(msg, text) bot.send_msg(msg, text)
yt_last_result = [] yt_last_result = []
yt_last_page = 0 # TODO: if we keep adding global variables, we need to consider sealing all commands up into classes. yt_last_page = 0 # TODO: if we keep adding global variables, we need to consider sealing all commands up into classes.
def cmd_yt_search(bot, user, text, command, parameter): def cmd_yt_search(bot, user, text, command, parameter):
global log, yt_last_result, yt_last_page, song_shortlist global log, yt_last_result, yt_last_page, song_shortlist
@ -516,6 +519,7 @@ def cmd_yt_search(bot, user, text, command, parameter):
else: else:
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
def _yt_format_result(results, start, count): def _yt_format_result(results, start, count):
msg = '<table><tr><th width="10%">Index</th><th>Title</th><th width="20%">Uploader</th></tr>' msg = '<table><tr><th width="10%">Index</th><th>Title</th><th width="20%">Uploader</th></tr>'
for index, item in enumerate(results[start:start+count]): for index, item in enumerate(results[start:start+count]):
@ -602,7 +606,7 @@ def cmd_volume(bot, user, text, command, parameter):
if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100: if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.volume_set = float(float(parameter) / 100) bot.volume_set = float(float(parameter) / 100)
bot.send_msg(constants.strings('change_volume', bot.send_msg(constants.strings('change_volume',
volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name'])) volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']))
var.db.set('bot', 'volume', str(bot.volume_set)) var.db.set('bot', 'volume', str(bot.volume_set))
log.info('cmd: volume set to %d' % (bot.volume_set * 100)) log.info('cmd: volume set to %d' % (bot.volume_set * 100))
else: else:
@ -652,7 +656,7 @@ def cmd_ducking_volume(bot, user, text, command, parameter):
if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100: if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.ducking_volume = float(float(parameter) / 100) bot.ducking_volume = float(float(parameter) / 100)
bot.send_msg(constants.strings('change_ducking_volume', bot.send_msg(constants.strings('change_ducking_volume',
volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text) volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
# var.db.set('bot', 'volume', str(bot.volume_set)) # var.db.set('bot', 'volume', str(bot.volume_set))
var.db.set('bot', 'ducking_volume', str(bot.ducking_volume)) var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
log.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100)) log.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
@ -692,12 +696,10 @@ def cmd_remove(bot, user, text, command, parameter):
global log global log
# Allow to remove specific music into the queue with a number # Allow to remove specific music into the queue with a number
if parameter and parameter.isdigit() and int(parameter) > 0 \ if parameter and parameter.isdigit() and 0 < int(parameter) <= len(var.playlist):
and int(parameter) <= len(var.playlist):
index = int(parameter) - 1 index = int(parameter) - 1
removed = None
if index == var.playlist.current_index: if index == var.playlist.current_index:
removed = var.playlist.remove(index) removed = var.playlist.remove(index)
@ -707,7 +709,7 @@ def cmd_remove(bot, user, text, command, parameter):
var.playlist.current_index -= 1 var.playlist.current_index -= 1
# then the bot will move to next item # then the bot will move to next item
else: # if item deleted is the last item of the queue else: # if item deleted is the last item of the queue
var.playlist.current_index -= 1 var.playlist.current_index -= 1
if not bot.is_pause: if not bot.is_pause:
bot.interrupt() bot.interrupt()
@ -715,7 +717,7 @@ def cmd_remove(bot, user, text, command, parameter):
removed = var.playlist.remove(index) removed = var.playlist.remove(index)
bot.send_msg(constants.strings('removing_item', bot.send_msg(constants.strings('removing_item',
item=removed.format_short_string()), text) item=removed.format_short_string()), text)
log.info("cmd: delete from playlist: " + removed.format_debug_string()) log.info("cmd: delete from playlist: " + removed.format_debug_string())
else: else:
@ -726,7 +728,7 @@ def cmd_list_file(bot, user, text, command, parameter):
global log global log
files = var.cache.files files = var.cache.files
msgs = [ constants.strings("multiple_file_found") ] msgs = [constants.strings("multiple_file_found")]
try: try:
count = 0 count = 0
for index, file in enumerate(files): for index, file in enumerate(files):
@ -755,29 +757,30 @@ def cmd_queue(bot, user, text, command, parameter):
msg = constants.strings('queue_empty') msg = constants.strings('queue_empty')
bot.send_msg(msg, text) bot.send_msg(msg, text)
else: else:
msgs = [ constants.strings('queue_contents')] msgs = [constants.strings('queue_contents')]
for i, music in enumerate(var.playlist): for i, music in enumerate(var.playlist):
newline = ''
tags = '' tags = ''
if len(music.item().tags) > 0: if len(music.item().tags) > 0:
tags = "<sup>{}</sup>".format(", ".join(music.item().tags)) tags = "<sup>{}</sup>".format(", ".join(music.item().tags))
if i == var.playlist.current_index: if i == var.playlist.current_index:
newline = "<b style='color:orange'>{} ({}) {} </b> {}".format(i + 1, music.display_type(), newline = "<b style='color:orange'>{} ({}) {} </b> {}".format(i + 1, music.display_type(),
music.format_short_string(), tags) music.format_short_string(), tags)
else: else:
newline = '<b>{}</b> ({}) {} {}'.format(i + 1, music.display_type(), newline = '<b>{}</b> ({}) {} {}'.format(i + 1, music.display_type(),
music.format_short_string(), tags) music.format_short_string(), tags)
msgs.append(newline) msgs.append(newline)
send_multi_lines(bot, msgs, text) send_multi_lines(bot, msgs, text)
def cmd_random(bot, user, text, command, parameter): def cmd_random(bot, user, text, command, parameter):
global log global log
bot.interrupt() bot.interrupt()
var.playlist.randomize() var.playlist.randomize()
def cmd_repeat(bot, user, text, command, parameter): def cmd_repeat(bot, user, text, command, parameter):
global log global log
@ -795,13 +798,14 @@ def cmd_repeat(bot, user, text, command, parameter):
bot.send_msg(constants.strings("repeat", song=music.format_song_string(), n=str(repeat)), text) bot.send_msg(constants.strings("repeat", song=music.format_song_string(), n=str(repeat)), text)
def cmd_mode(bot, user, text, command, parameter): def cmd_mode(bot, user, text, command, parameter):
global log global log
if not parameter: if not parameter:
bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text) bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text)
return return
if not parameter in ["one-shot", "repeat", "random", "autoplay"]: if parameter not in ["one-shot", "repeat", "random", "autoplay"]:
bot.send_msg(constants.strings('unknown_mode', mode=parameter), text) bot.send_msg(constants.strings('unknown_mode', mode=parameter), text)
else: else:
var.db.set('playlist', 'playback_mode', parameter) var.db.set('playlist', 'playback_mode', parameter)
@ -813,6 +817,7 @@ def cmd_mode(bot, user, text, command, parameter):
bot.interrupt() bot.interrupt()
bot.launch_music() bot.launch_music()
def cmd_play_tags(bot, user, text, command, parameter): def cmd_play_tags(bot, user, text, command, parameter):
if not parameter: if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
@ -829,7 +834,6 @@ def cmd_play_tags(bot, user, text, command, parameter):
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string()) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
msgs.append("<li><b>{}</b> (<i>{}</i>)</li>".format(music_wrapper.item().title, ", ".join(music_wrapper.item().tags))) msgs.append("<li><b>{}</b> (<i>{}</i>)</li>".format(music_wrapper.item().title, ", ".join(music_wrapper.item().tags)))
if count != 0: if count != 0:
msgs.append("</ul>") msgs.append("</ul>")
var.playlist.extend(music_wrappers) var.playlist.extend(music_wrappers)
@ -842,8 +846,6 @@ def cmd_add_tag(bot, user, text, command, parameter):
global log global log
params = parameter.split() params = parameter.split()
index = ""
tags = []
if len(params) == 2: if len(params) == 2:
index = params[0] index = params[0]
tags = list(map(lambda t: t.strip(), params[1].split(","))) tags = list(map(lambda t: t.strip(), params[1].split(",")))
@ -880,8 +882,6 @@ def cmd_remove_tag(bot, user, text, command, parameter):
params = parameter.split() params = parameter.split()
index = ""
tags = []
if len(params) == 2: if len(params) == 2:
index = params[0] index = params[0]
tags = list(map(lambda t: t.strip(), params[1].split(","))) tags = list(map(lambda t: t.strip(), params[1].split(",")))
@ -897,7 +897,7 @@ def cmd_remove_tag(bot, user, text, command, parameter):
if tags[0] != "*": if tags[0] != "*":
var.playlist[int(index) - 1].remove_tags(tags) var.playlist[int(index) - 1].remove_tags(tags)
log.info("cmd: remove tags %s from song %s" % (", ".join(tags), log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
var.playlist[int(index) - 1].format_debug_string())) var.playlist[int(index) - 1].format_debug_string()))
bot.send_msg(constants.strings("removed_tags", bot.send_msg(constants.strings("removed_tags",
tags=", ".join(tags), tags=", ".join(tags),
song=var.playlist[int(index) - 1].format_short_string()), text) song=var.playlist[int(index) - 1].format_short_string()), text)
@ -914,7 +914,7 @@ def cmd_remove_tag(bot, user, text, command, parameter):
for item in var.playlist: for item in var.playlist:
item.remove_tags(tags) item.remove_tags(tags)
log.info("cmd: remove tags %s from song %s" % (", ".join(tags), log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
item.format_debug_string())) item.format_debug_string()))
bot.send_msg(constants.strings("removed_tags_from_all", tags=", ".join(tags)), text) bot.send_msg(constants.strings("removed_tags_from_all", tags=", ".join(tags)), text)
return return
else: else:
@ -926,6 +926,7 @@ def cmd_remove_tag(bot, user, text, command, parameter):
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
def cmd_find_tagged(bot, user, text, command, parameter): def cmd_find_tagged(bot, user, text, command, parameter):
global song_shortlist global song_shortlist
@ -954,6 +955,7 @@ def cmd_find_tagged(bot, user, text, command, parameter):
else: else:
bot.send_msg(constants.strings("no_file"), text) bot.send_msg(constants.strings("no_file"), text)
def cmd_search_library(bot, user, text, command, parameter): def cmd_search_library(bot, user, text, command, parameter):
global song_shortlist global song_shortlist
if not parameter: if not parameter:
@ -993,9 +995,8 @@ def cmd_search_library(bot, user, text, command, parameter):
def cmd_shortlist(bot, user, text, command, parameter): def cmd_shortlist(bot, user, text, command, parameter):
global song_shortlist, log global song_shortlist, log
indexes = []
try: try:
indexes = [ int(i) for i in parameter.split(" ") ] indexes = [int(i) for i in parameter.split(" ")]
except ValueError: except ValueError:
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
return return
@ -1033,9 +1034,8 @@ def cmd_shortlist(bot, user, text, command, parameter):
def cmd_delete_from_library(bot, user, text, command, parameter): def cmd_delete_from_library(bot, user, text, command, parameter):
global song_shortlist, log global song_shortlist, log
indexes = []
try: try:
indexes = [ int(i) for i in parameter.split(" ") ] indexes = [int(i) for i in parameter.split(" ")]
except ValueError: except ValueError:
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
return return
@ -1049,7 +1049,7 @@ def cmd_delete_from_library(bot, user, text, command, parameter):
if 'id' in music_dict: if 'id' in music_dict:
music_wrapper = get_cached_wrapper_by_id(bot, music_dict['id'], user) music_wrapper = get_cached_wrapper_by_id(bot, music_dict['id'], user)
log.info("cmd: remove from library: " + music_wrapper.format_debug_string()) log.info("cmd: remove from library: " + music_wrapper.format_debug_string())
msgs.append("<li>[{}] <b>{}</b></li>".format(music_wrapper.item().type ,music_wrapper.item().title)) msgs.append("<li>[{}] <b>{}</b></li>".format(music_wrapper.item().type, music_wrapper.item().title))
var.playlist.remove_by_id(music_dict['id']) var.playlist.remove_by_id(music_dict['id'])
var.cache.free_and_delete(music_dict['id']) var.cache.free_and_delete(music_dict['id'])
count += 1 count += 1
@ -1078,6 +1078,7 @@ def cmd_delete_from_library(bot, user, text, command, parameter):
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
def cmd_drop_database(bot, user, text, command, parameter): def cmd_drop_database(bot, user, text, command, parameter):
global log global log
@ -1091,6 +1092,7 @@ def cmd_drop_database(bot, user, text, command, parameter):
else: else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
def cmd_refresh_cache(bot, user, text, command, parameter): def cmd_refresh_cache(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): if bot.is_admin(user):
@ -1100,13 +1102,16 @@ def cmd_refresh_cache(bot, user, text, command, parameter):
else: else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
# Just for debug use # Just for debug use
def cmd_real_time_rms(bot, user, text, command, parameter): def cmd_real_time_rms(bot, user, text, command, parameter):
bot._display_rms = not bot._display_rms bot._display_rms = not bot._display_rms
def cmd_loop_state(bot, user, text, command, parameter): def cmd_loop_state(bot, user, text, command, parameter):
print(bot._loop_status) print(bot._loop_status)
def cmd_item(bot, user, text, command, parameter): def cmd_item(bot, user, text, command, parameter):
print(bot.wait_for_downloading) print(bot.wait_for_downloading)
print(var.playlist.current_item().to_dict()) print(var.playlist.current_item().to_dict())

View File

@ -1,32 +1,32 @@
import variables as var import variables as var
def strings(option, *argv, **kwargs): def strings(option, *argv, **kwargs):
string = ""
try: try:
string = var.config.get("strings", option) string = var.config.get("strings", option)
except KeyError as e: except KeyError:
raise KeyError("Missed strings in configuration file: '{string}'. ".format(string=option) + raise KeyError("Missed strings in configuration file: '{string}'. ".format(string=option)
"Please restore you configuration file back to default if necessary.") + "Please restore you configuration file back to default if necessary.")
if argv or kwargs: if argv or kwargs:
try: try:
formatted = string.format(*argv, **kwargs) formatted = string.format(*argv, **kwargs)
return formatted return formatted
except KeyError as e: except KeyError as e:
raise KeyError( raise KeyError(
"Missed/Unexpected placeholder {{{placeholder}}} in string '{string}'. ".format(placeholder=str(e).strip("'"), string=option) + "Missed/Unexpected placeholder {{{placeholder}}} in string '{string}'. ".format(placeholder=str(e).strip("'"), string=option)
"Please restore you configuration file back to default if necessary.") + "Please restore you configuration file back to default if necessary.")
except TypeError as e: except TypeError:
raise KeyError( raise KeyError(
"Missed placeholder in string '{string}'. ".format(string=option) + "Missed placeholder in string '{string}'. ".format(string=option)
"Please restore you configuration file back to default if necessary.") + "Please restore you configuration file back to default if necessary.")
else: else:
return string return string
def commands(command): def commands(command):
string = ""
try: try:
string = var.config.get("commands", command) string = var.config.get("commands", command)
return string return string
except KeyError as e: except KeyError:
raise KeyError("Missed command in configuration file: '{string}'. ".format(string=command) + raise KeyError("Missed command in configuration file: '{string}'. ".format(string=command)
"Please restore you configuration file back to default if necessary.") + "Please restore you configuration file back to default if necessary.")

View File

@ -2,17 +2,19 @@ import sqlite3
import json import json
import datetime import datetime
class DatabaseError(Exception): class DatabaseError(Exception):
pass pass
class SettingsDatabase: class SettingsDatabase:
version = 1 version = 1
def __init__(self, db_path): def __init__(self, db_path):
self.db_path = db_path self.db_path = db_path
# connect # connect
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
self.db_version_check_and_create() self.db_version_check_and_create()
@ -57,9 +59,9 @@ class SettingsDatabase:
"value TEXT, " "value TEXT, "
"UNIQUE(section, option))") "UNIQUE(section, option))")
cursor.execute("INSERT INTO botamusique (section, option, value) " cursor.execute("INSERT INTO botamusique (section, option, value) "
"VALUES (?, ?, ?)" , ("bot", "db_version", "1")) "VALUES (?, ?, ?)", ("bot", "db_version", "1"))
cursor.execute("INSERT INTO botamusique (section, option, value) " cursor.execute("INSERT INTO botamusique (section, option, value) "
"VALUES (?, ?, ?)" , ("bot", "music_db_version", "0")) "VALUES (?, ?, ?)", ("bot", "music_db_version", "0"))
conn.commit() conn.commit()
conn.close() conn.close()
@ -90,7 +92,7 @@ class SettingsDatabase:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT OR REPLACE INTO botamusique (section, option, value) " cursor.execute("INSERT OR REPLACE INTO botamusique (section, option, value) "
"VALUES (?, ?, ?)" , (section, option, value)) "VALUES (?, ?, ?)", (section, option, value))
conn.commit() conn.commit()
conn.close() conn.close()
@ -214,7 +216,7 @@ class MusicDatabase:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
results = cursor.execute("SELECT id, type, title, metadata, tags FROM music " results = cursor.execute("SELECT id, type, title, metadata, tags FROM music "
"WHERE %s" % condition_str, filler).fetchall() "WHERE %s" % condition_str, filler).fetchall()
conn.close() conn.close()
return self._result_to_dict(results) return self._result_to_dict(results)
@ -227,7 +229,6 @@ class MusicDatabase:
condition.append('LOWER(title) LIKE ?') condition.append('LOWER(title) LIKE ?')
filler.append("%{:s}%".format(keyword.lower())) filler.append("%{:s}%".format(keyword.lower()))
condition_str = " AND ".join(condition) condition_str = " AND ".join(condition)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
@ -246,7 +247,6 @@ class MusicDatabase:
condition.append('LOWER(tags) LIKE ?') condition.append('LOWER(tags) LIKE ?')
filler.append("%{:s},%".format(tag.lower())) filler.append("%{:s},%".format(tag.lower()))
condition_str = " AND ".join(condition) condition_str = " AND ".join(condition)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
@ -284,7 +284,6 @@ class MusicDatabase:
return self._result_to_dict(results) return self._result_to_dict(results)
def _result_to_dict(self, results): def _result_to_dict(self, results):
if len(results) > 0: if len(results) > 0:
music_dicts = [] music_dicts = []
@ -320,11 +319,10 @@ class MusicDatabase:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM music " cursor.execute("DELETE FROM music "
"WHERE %s" % condition_str, filler) "WHERE %s" % condition_str, filler)
conn.commit() conn.commit()
conn.close() conn.close()
def drop_table(self): def drop_table(self):
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()

View File

@ -16,7 +16,7 @@ import time
class ReverseProxied(object): class ReverseProxied(object):
'''Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is this to a URL other than / and to an HTTP scheme that is
different than what is used locally. different than what is used locally.
@ -31,7 +31,7 @@ class ReverseProxied(object):
} }
:param app: the WSGI application :param app: the WSGI application
''' """
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
@ -57,6 +57,7 @@ web = Flask(__name__)
log = logging.getLogger("bot") log = logging.getLogger("bot")
user = 'Remote Control' user = 'Remote Control'
def init_proxy(): def init_proxy():
global web global web
if var.is_proxified: if var.is_proxified:
@ -64,19 +65,21 @@ def init_proxy():
# https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app # https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app
def check_auth(username, password): def check_auth(username, password):
"""This function is called to check if a username / """This function is called to check if a username /
password combination is valid. password combination is valid.
""" """
return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password") return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password")
def authenticate(): def authenticate():
"""Sends a 401 response that enables basic auth""" """Sends a 401 response that enables basic auth"""
global log global log
return Response( return Response('Could not verify your access level for that URL.\n'
'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401,
'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f): def requires_auth(f):
@wraps(f) @wraps(f)
@ -90,6 +93,7 @@ def requires_auth(f):
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
def tag_color(tag): def tag_color(tag):
num = hash(tag) % 8 num = hash(tag) % 8
if num == 0: if num == 0:
@ -109,14 +113,15 @@ def tag_color(tag):
elif num == 7: elif num == 7:
return "dark" return "dark"
def build_tags_color_lookup(): def build_tags_color_lookup():
color_lookup = {} color_lookup = {}
for tag in var.music_db.query_all_tags(): for tag in var.music_db.query_all_tags():
color_lookup[tag] = tag_color(tag) color_lookup[tag] = tag_color(tag)
return color_lookup return color_lookup
def build_path_tags_lookup(): def build_path_tags_lookup():
path_tags_lookup = {} path_tags_lookup = {}
ids = list(var.cache.file_id_lookup.values()) ids = list(var.cache.file_id_lookup.values())
@ -128,11 +133,13 @@ def build_path_tags_lookup():
return path_tags_lookup return path_tags_lookup
def recur_dir(dirobj): def recur_dir(dirobj):
for name, dir in dirobj.get_subdirs().items(): for name, dir in dirobj.get_subdirs().items():
print(dirobj.fullpath + "/" + name) print(dirobj.fullpath + "/" + name)
recur_dir(dir) recur_dir(dir)
@web.route("/", methods=['GET']) @web.route("/", methods=['GET'])
@requires_auth @requires_auth
def index(): def index():
@ -153,21 +160,22 @@ def index():
paused=var.bot.is_pause, paused=var.bot.is_pause,
) )
@web.route("/playlist", methods=['GET']) @web.route("/playlist", methods=['GET'])
@requires_auth @requires_auth
def playlist(): def playlist():
if len(var.playlist) == 0: if len(var.playlist) == 0:
return jsonify({'items': [render_template('playlist.html', return jsonify({'items': [render_template('playlist.html',
m=False, m=False,
index=-1 index=-1
)] )]
}) })
tags_color_lookup = build_tags_color_lookup() tags_color_lookup = build_tags_color_lookup()
items = [] items = []
for index, item_wrapper in enumerate(var.playlist): for index, item_wrapper in enumerate(var.playlist):
items.append(render_template('playlist.html', items.append(render_template('playlist.html',
index=index, index=index,
tags_color_lookup=tags_color_lookup, tags_color_lookup=tags_color_lookup,
m=item_wrapper.item(), m=item_wrapper.item(),
@ -175,7 +183,8 @@ def playlist():
) )
) )
return jsonify({ 'items': items }) return jsonify({'items': items})
def status(): def status():
if len(var.playlist) > 0: if len(var.playlist) > 0:
@ -197,7 +206,7 @@ def post():
if request.method == 'POST': if request.method == 'POST':
if request.form: if request.form:
log.debug("web: Post request from %s: %s" % ( request.remote_addr, str(request.form))) log.debug("web: Post request from %s: %s" % (request.remote_addr, str(request.form)))
if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']: if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
path = var.music_folder + request.form['add_file_bottom'] path = var.music_folder + request.form['add_file_bottom']
if os.path.isfile(path): if os.path.isfile(path):
@ -231,15 +240,13 @@ def post():
music_wrappers = list(map( music_wrappers = list(map(
lambda file: lambda file:
get_cached_wrapper_by_id(var.bot, var.cache.file_id_lookup[folder + file], user), get_cached_wrapper_by_id(var.bot, var.cache.file_id_lookup[folder + file], user), files))
files))
var.playlist.extend(music_wrappers) var.playlist.extend(music_wrappers)
for music_wrapper in music_wrappers: for music_wrapper in music_wrappers:
log.info('web: add to playlist: ' + music_wrapper.format_debug_string()) log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
elif 'add_url' in request.form: elif 'add_url' in request.form:
music_wrapper = get_cached_wrapper_from_scrap(var.bot, type='url', url=request.form['add_url'], user=user) music_wrapper = get_cached_wrapper_from_scrap(var.bot, type='url', url=request.form['add_url'], user=user)
var.playlist.append(music_wrapper) var.playlist.append(music_wrapper)
@ -279,7 +286,6 @@ def post():
else: else:
var.playlist.remove(index) var.playlist.remove(index)
elif 'play_music' in request.form: elif 'play_music' in request.form:
music_wrapper = var.playlist[int(request.form['play_music'])] music_wrapper = var.playlist[int(request.form['play_music'])]
log.info("web: jump to: " + music_wrapper.format_debug_string()) log.info("web: jump to: " + music_wrapper.format_debug_string())
@ -358,6 +364,7 @@ def post():
return status() return status()
@web.route('/upload', methods=["POST"]) @web.route('/upload', methods=["POST"])
def upload(): def upload():
global log global log
@ -366,7 +373,7 @@ def upload():
if not files: if not files:
return redirect("./", code=406) return redirect("./", code=406)
#filename = secure_filename(file.filename).strip() # filename = secure_filename(file.filename).strip()
for file in files: for file in files:
filename = file.filename filename = file.filename
if filename == '': if filename == '':
@ -385,7 +392,7 @@ def upload():
if "audio" in file.mimetype: if "audio" in file.mimetype:
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir)) storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
print('storagepath:',storagepath) print('storagepath:', storagepath)
if not storagepath.startswith(os.path.abspath(var.music_folder)): if not storagepath.startswith(os.path.abspath(var.music_folder)):
return redirect("./", code=406) return redirect("./", code=406)

View File

@ -2,28 +2,32 @@ from librb.rbRadios import RadioBrowser
rb = RadioBrowser() rb = RadioBrowser()
def getstations_byname(query): def getstations_byname(query):
results = rb.stations_byname(query) results = rb.stations_byname(query)
stations = [] stations = []
for st in results: for st in results:
try: try:
station = {'stationname': st['name'], 'id':st['id'], 'codec':st['codec'], 'bitrate':st['bitrate'], 'country':st['country'], 'homepage':st['homepage'], 'genre':st['tags']} station = {'stationname': st['name'], 'id': st['id'], 'codec': st['codec'], 'bitrate': st['bitrate'], 'country': st['country'], 'homepage': st['homepage'], 'genre': st['tags']}
stations.append(station) stations.append(station)
except: except:
pass pass
return stations return stations
def geturl_byid(id): def geturl_byid(id):
url = rb.playable_station(id)['url'] url = rb.playable_station(id)['url']
if url != None: if url is not None:
return url return url
else: else:
return "-1" return "-1"
def getstationname_byid(id): def getstationname_byid(id):
return rb.stations_byid(id) return rb.stations_byid(id)
if __name__ == "__main__": if __name__ == "__main__":
r = getstations_byname('r.sh') r = getstations_byname('r.sh')
stationinfo = getstationname_byid(96748) stationinfo = getstationname_byid(96748)
pass pass

View File

@ -1,8 +1,5 @@
import requests import requests
from xml.etree import ElementTree
from urllib.parse import urljoin
from librb.rbConstants import endpoints, BASE_URL from librb.rbConstants import endpoints, BASE_URL
@ -179,5 +176,4 @@ class RadioBrowser:
assert isinstance(params, dict), "params is not a dict" assert isinstance(params, dict), "params is not a dict"
kwargs["params"] = params kwargs["params"] = params
endpoint = self.builder.produce_endpoint(endpoint="station_search") endpoint = self.builder.produce_endpoint(endpoint="station_search")
return request(endpoint, **kwargs) return request(endpoint, **kwargs)

View File

@ -1,11 +1,10 @@
import logging import logging
import os import os
from database import MusicDatabase
import json import json
import threading import threading
from media.item import item_builders, item_loaders, item_id_generators, dict_to_item, dicts_to_items from media.item import item_builders, item_id_generators, dict_to_item
from database import MusicDatabase from database import MusicDatabase
import variables as var import variables as var
import util import util
@ -18,9 +17,10 @@ class MusicCache(dict):
self.log = logging.getLogger("bot") self.log = logging.getLogger("bot")
self.dir = None self.dir = None
self.files = [] self.files = []
self.file_id_lookup = {}
self.dir_lock = threading.Lock() self.dir_lock = threading.Lock()
def get_item_by_id(self, bot, id): # Why all these functions need a bot? Because it need the bot to send message! def get_item_by_id(self, bot, id): # Why all these functions need a bot? Because it need the bot to send message!
if id in self: if id in self:
return self[id] return self[id]
@ -32,9 +32,8 @@ class MusicCache(dict):
return item return item
else: else:
return None return None
#print(id) # print(id)
#raise KeyError("Unable to fetch item from the database! Please try to refresh the cache by !recache.") # raise KeyError("Unable to fetch item from the database! Please try to refresh the cache by !recache.")
def get_item(self, bot, **kwargs): def get_item(self, bot, **kwargs):
# kwargs should provide type and id, and parameters to build the item if not existed in the library. # kwargs should provide type and id, and parameters to build the item if not existed in the library.
@ -55,7 +54,7 @@ class MusicCache(dict):
return item return item
# if not in the database, build one # if not in the database, build one
self[id] = item_builders[kwargs['type']](bot, **kwargs) # newly built item will not be saved immediately self[id] = item_builders[kwargs['type']](bot, **kwargs) # newly built item will not be saved immediately
return self[id] return self[id]
def get_items_by_tags(self, bot, tags): def get_items_by_tags(self, bot, tags):
@ -112,7 +111,6 @@ class MusicCache(dict):
self.dir_lock.acquire() self.dir_lock.acquire()
self.log.info("library: rebuild directory cache") self.log.info("library: rebuild directory cache")
self.files = [] self.files = []
self.file_id_lookup = {}
files = util.get_recursive_file_list_sorted(var.music_folder) files = util.get_recursive_file_list_sorted(var.music_folder)
self.dir = util.Dir(var.music_folder) self.dir = util.Dir(var.music_folder)
for file in files: for file in files:
@ -254,4 +252,4 @@ def get_cached_wrappers_by_tags(bot, tags, user):
ret = [] ret = []
for item in items: for item in items:
ret.append(CachedItemWrapper(var.cache, item.id, item.type, user)) ret.append(CachedItemWrapper(var.cache, item.id, item.type, user))
return ret return ret

View File

@ -1,4 +1,3 @@
import logging
import os import os
import re import re
from io import BytesIO from io import BytesIO
@ -6,9 +5,7 @@ import base64
import hashlib import hashlib
import mutagen import mutagen
from PIL import Image from PIL import Image
import json
import util
import variables as var import variables as var
from media.item import BaseItem, item_builders, item_loaders, item_id_generators from media.item import BaseItem, item_builders, item_loaders, item_id_generators
import constants import constants
@ -24,15 +21,19 @@ type : file
user user
''' '''
def file_item_builder(bot, **kwargs): def file_item_builder(bot, **kwargs):
return FileItem(bot, kwargs['path']) return FileItem(bot, kwargs['path'])
def file_item_loader(bot, _dict): def file_item_loader(bot, _dict):
return FileItem(bot, "", _dict) return FileItem(bot, "", _dict)
def file_item_id_generator(**kwargs): def file_item_id_generator(**kwargs):
return hashlib.md5(kwargs['path'].encode()).hexdigest() return hashlib.md5(kwargs['path'].encode()).hexdigest()
item_builders['file'] = file_item_builder item_builders['file'] = file_item_builder
item_loaders['file'] = file_item_loader item_loaders['file'] = file_item_loader
item_id_generators['file'] = file_item_id_generator item_id_generators['file'] = file_item_id_generator
@ -74,12 +75,12 @@ class FileItem(BaseItem):
self.send_client_message(constants.strings('file_missed', file=self.path)) self.send_client_message(constants.strings('file_missed', file=self.path))
return False return False
#self.version += 1 # 0 -> 1, notify the wrapper to save me when validate() is visited the first time # self.version += 1 # 0 -> 1, notify the wrapper to save me when validate() is visited the first time
self.ready = "yes" self.ready = "yes"
return True return True
def _get_info_from_tag(self): def _get_info_from_tag(self):
match = re.search("(.+)\.(.+)", self.uri()) match = re.search(r"(.+)\.(.+)", self.uri())
assert match is not None assert match is not None
file_no_ext = match[1] file_no_ext = match[1]
@ -153,17 +154,17 @@ class FileItem(BaseItem):
def format_song_string(self, user): def format_song_string(self, user):
return constants.strings("file_item", return constants.strings("file_item",
title=self.title, title=self.title,
artist=self.artist if self.artist else '??', artist=self.artist if self.artist else '??',
user=user user=user
) )
def format_current_playing(self, user): def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user)) display = constants.strings("now_playing", item=self.format_song_string(user))
if self.thumbnail: if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \ thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>' self.thumbnail + '"/>'
display += "<br />" + thumbnail_html display += "<br />" + thumbnail_html
return display return display

View File

@ -1,33 +1,27 @@
import logging import logging
import threading
import os
import re
from io import BytesIO
import base64
import hashlib
import mutagen
from PIL import Image
import util
import variables as var
item_builders = {} item_builders = {}
item_loaders = {} item_loaders = {}
item_id_generators = {} item_id_generators = {}
def example_builder(bot, **kwargs): def example_builder(bot, **kwargs):
return BaseItem(bot) return BaseItem(bot)
def example_loader(bot, _dict): def example_loader(bot, _dict):
return BaseItem(bot, from_dict=_dict) return BaseItem(bot, from_dict=_dict)
def example_id_generator(**kwargs): def example_id_generator(**kwargs):
return "" return ""
item_builders['base'] = example_builder item_builders['base'] = example_builder
item_loaders['base'] = example_loader item_loaders['base'] = example_loader
item_id_generators['base'] = example_id_generator item_id_generators['base'] = example_id_generator
def dicts_to_items(bot, music_dicts): def dicts_to_items(bot, music_dicts):
items = [] items = []
for music_dict in music_dicts: for music_dict in music_dicts:
@ -35,6 +29,7 @@ def dicts_to_items(bot, music_dicts):
items.append(item_loaders[type](bot, music_dict)) items.append(item_loaders[type](bot, music_dict))
return items return items
def dict_to_item(bot, music_dict): def dict_to_item(bot, music_dict):
type = music_dict['type'] type = music_dict['type']
return item_loaders[type](bot, music_dict) return item_loaders[type](bot, music_dict)
@ -48,11 +43,11 @@ class BaseItem:
self.title = "" self.title = ""
self.path = "" self.path = ""
self.tags = [] self.tags = []
self.version = 0 # if version increase, wrapper will re-save this item self.version = 0 # if version increase, wrapper will re-save this item
if from_dict is None: if from_dict is None:
self.id = "" self.id = ""
self.ready = "pending" # pending - is_valid() -> validated - prepare() -> yes, failed self.ready = "pending" # pending - is_valid() -> validated - prepare() -> yes, failed
else: else:
self.id = from_dict['id'] self.id = from_dict['id']
self.ready = from_dict['ready'] self.ready = from_dict['ready']
@ -110,6 +105,4 @@ class BaseItem:
self.bot.send_msg(msg) self.bot.send_msg(msg)
def to_dict(self): def to_dict(self):
return {"type" : self.type, "id": self.id, "ready": self.ready, "path": self.path, "tags": self.tags} return {"type": self.type, "id": self.id, "ready": self.ready, "path": self.path, "tags": self.tags}

View File

@ -162,12 +162,12 @@ class BasePlaylist(list):
def randomize(self): def randomize(self):
# current_index will lose track after shuffling, thus we take current music out before shuffling # current_index will lose track after shuffling, thus we take current music out before shuffling
#current = self.current_item() # current = self.current_item()
#del self[self.current_index] # del self[self.current_index]
random.shuffle(self) random.shuffle(self)
#self.insert(0, current) # self.insert(0, current)
self.current_index = -1 self.current_index = -1
self.version += 1 self.version += 1
@ -183,7 +183,7 @@ class BasePlaylist(list):
var.db.set("playlist", "current_index", self.current_index) var.db.set("playlist", "current_index", self.current_index)
for index, music in enumerate(self): for index, music in enumerate(self):
var.db.set("playlist_item", str(index), json.dumps({'id': music.id, 'user': music.user })) var.db.set("playlist_item", str(index), json.dumps({'id': music.id, 'user': music.user}))
def load(self): def load(self):
current_index = var.db.getint("playlist", "current_index", fallback=-1) current_index = var.db.getint("playlist", "current_index", fallback=-1)
@ -212,7 +212,7 @@ class BasePlaylist(list):
def start_async_validating(self): def start_async_validating(self):
if not self.validating_thread_lock.locked(): if not self.validating_thread_lock.locked():
time.sleep(0.1) # Just avoid validation finishes too fast and delete songs while something is reading it. time.sleep(0.1) # Just avoid validation finishes too fast and delete songs while something is reading it.
th = threading.Thread(target=self._check_valid, name="Validating") th = threading.Thread(target=self._check_valid, name="Validating")
th.daemon = True th.daemon = True
th.start() th.start()
@ -359,4 +359,3 @@ class AutoPlaylist(OneshotPlaylist):
if len(self) == 0: if len(self) == 0:
self.refresh() self.refresh()
return super().next() return super().next()

View File

@ -11,16 +11,16 @@ import constants
log = logging.getLogger("bot") log = logging.getLogger("bot")
def get_radio_server_description(url): def get_radio_server_description(url):
global log global log
log.debug("radio: fetching radio server description") log.debug("radio: fetching radio server description")
p = re.compile('(https?\:\/\/[^\/]*)', re.IGNORECASE) p = re.compile('(https?://[^/]*)', re.IGNORECASE)
res = re.search(p, url) res = re.search(p, url)
base_url = res.group(1) base_url = res.group(1)
url_icecast = base_url + '/status-json.xsl' url_icecast = base_url + '/status-json.xsl'
url_shoutcast = base_url + '/stats?json=1' url_shoutcast = base_url + '/stats?json=1'
title_server = None
try: try:
r = requests.get(url_shoutcast, timeout=10) r = requests.get(url_shoutcast, timeout=10)
data = r.json() data = r.json()
@ -30,7 +30,7 @@ def get_radio_server_description(url):
except (requests.exceptions.ConnectionError, except (requests.exceptions.ConnectionError,
requests.exceptions.HTTPError, requests.exceptions.HTTPError,
requests.exceptions.ReadTimeout, requests.exceptions.ReadTimeout,
requests.exceptions.Timeout) as e: requests.exceptions.Timeout):
error_traceback = traceback.format_exc() error_traceback = traceback.format_exc()
error = error_traceback.rstrip().split("\n")[-1] error = error_traceback.rstrip().split("\n")[-1]
log.debug("radio: unsuccessful attempts on fetching radio description (shoutcast): " + error) log.debug("radio: unsuccessful attempts on fetching radio description (shoutcast): " + error)
@ -51,7 +51,7 @@ def get_radio_server_description(url):
except (requests.exceptions.ConnectionError, except (requests.exceptions.ConnectionError,
requests.exceptions.HTTPError, requests.exceptions.HTTPError,
requests.exceptions.ReadTimeout, requests.exceptions.ReadTimeout,
requests.exceptions.Timeout) as e: requests.exceptions.Timeout):
error_traceback = traceback.format_exc() error_traceback = traceback.format_exc()
error = error_traceback.rstrip().split("\n")[-1] error = error_traceback.rstrip().split("\n")[-1]
log.debug("radio: unsuccessful attempts on fetching radio description (icecast): " + error) log.debug("radio: unsuccessful attempts on fetching radio description (icecast): " + error)
@ -81,7 +81,7 @@ def get_radio_title(url):
requests.exceptions.HTTPError, requests.exceptions.HTTPError,
requests.exceptions.ReadTimeout, requests.exceptions.ReadTimeout,
requests.exceptions.Timeout, requests.exceptions.Timeout,
KeyError) as e: KeyError):
log.debug("radio: unsuccessful attempts on fetching radio title (icy)") log.debug("radio: unsuccessful attempts on fetching radio title (icy)")
return url return url
@ -92,12 +92,15 @@ def radio_item_builder(bot, **kwargs):
else: else:
return RadioItem(bot, kwargs['url'], '') return RadioItem(bot, kwargs['url'], '')
def radio_item_loader(bot, _dict): def radio_item_loader(bot, _dict):
return RadioItem(bot, "", "", _dict) return RadioItem(bot, "", "", _dict)
def radio_item_id_generator(**kwargs): def radio_item_id_generator(**kwargs):
return hashlib.md5(kwargs['url'].encode()).hexdigest() return hashlib.md5(kwargs['url'].encode()).hexdigest()
item_builders['radio'] = radio_item_builder item_builders['radio'] = radio_item_builder
item_loaders['radio'] = radio_item_loader item_loaders['radio'] = radio_item_loader
item_id_generators['radio'] = radio_item_id_generator item_id_generators['radio'] = radio_item_id_generator
@ -109,7 +112,7 @@ class RadioItem(BaseItem):
super().__init__(bot) super().__init__(bot)
self.url = url self.url = url
if not name: if not name:
self.title = get_radio_server_description(self.url) # The title of the radio station self.title = get_radio_server_description(self.url) # The title of the radio station
else: else:
self.title = name self.title = name
self.id = hashlib.md5(url.encode()).hexdigest() self.id = hashlib.md5(url.encode()).hexdigest()
@ -121,7 +124,7 @@ class RadioItem(BaseItem):
self.type = "radio" self.type = "radio"
def validate(self): def validate(self):
self.version += 1 # 0 -> 1, notify the wrapper to save me when validate() is visited the first time self.version += 1 # 0 -> 1, notify the wrapper to save me when validate() is visited the first time
return True return True
def is_ready(self): def is_ready(self):
@ -146,8 +149,8 @@ class RadioItem(BaseItem):
def format_song_string(self, user): def format_song_string(self, user):
return constants.strings("radio_item", return constants.strings("radio_item",
url=self.url, url=self.url,
title=get_radio_title(self.url), # the title of current song title=get_radio_title(self.url), # the title of current song
name=self.title, # the title of radio station name=self.title, # the title of radio station
user=user user=user
) )
@ -159,6 +162,3 @@ class RadioItem(BaseItem):
def display_type(self): def display_type(self):
return constants.strings("radio") return constants.strings("radio")

View File

@ -17,15 +17,19 @@ import media.system
log = logging.getLogger("bot") log = logging.getLogger("bot")
def url_item_builder(bot, **kwargs): def url_item_builder(bot, **kwargs):
return URLItem(bot, kwargs['url']) return URLItem(bot, kwargs['url'])
def url_item_loader(bot, _dict): def url_item_loader(bot, _dict):
return URLItem(bot, "", _dict) return URLItem(bot, "", _dict)
def url_item_id_generator(**kwargs): def url_item_id_generator(**kwargs):
return hashlib.md5(kwargs['url'].encode()).hexdigest() return hashlib.md5(kwargs['url'].encode()).hexdigest()
item_builders['url'] = url_item_builder item_builders['url'] = url_item_builder
item_loaders['url'] = url_item_loader item_loaders['url'] = url_item_loader
item_id_generators['url'] = url_item_id_generator item_id_generators['url'] = url_item_id_generator
@ -97,7 +101,7 @@ class URLItem(BaseItem):
return False return False
else: else:
self.ready = "validated" self.ready = "validated"
self.version += 1 # notify wrapper to save me self.version += 1 # notify wrapper to save me
return True return True
# Run in a other thread # Run in a other thread
@ -145,8 +149,6 @@ class URLItem(BaseItem):
self.ready = "preparing" self.ready = "preparing"
self.log.info("bot: downloading url (%s) %s " % (self.title, self.url)) self.log.info("bot: downloading url (%s) %s " % (self.title, self.url))
ydl_opts = ""
ydl_opts = { ydl_opts = {
'format': 'bestaudio/best', 'format': 'bestaudio/best',
'outtmpl': save_path, 'outtmpl': save_path,
@ -166,7 +168,7 @@ class URLItem(BaseItem):
for i in range(attempts): for i in range(attempts):
self.log.info("bot: download attempts %d / %d" % (i+1, attempts)) self.log.info("bot: download attempts %d / %d" % (i+1, attempts))
try: try:
info = ydl.extract_info(self.url) ydl.extract_info(self.url)
download_succeed = True download_succeed = True
break break
except: except:
@ -181,7 +183,7 @@ class URLItem(BaseItem):
"bot: finished downloading url (%s) %s, saved to %s." % (self.title, self.url, self.path)) "bot: finished downloading url (%s) %s, saved to %s." % (self.title, self.url, self.path))
self.downloading = False self.downloading = False
self._read_thumbnail_from_file(base_path + ".jpg") self._read_thumbnail_from_file(base_path + ".jpg")
self.version += 1 # notify wrapper to save me self.version += 1 # notify wrapper to save me
return True return True
else: else:
for f in glob.glob(base_path + "*"): for f in glob.glob(base_path + "*"):
@ -214,7 +216,6 @@ class URLItem(BaseItem):
return dict return dict
def format_debug_string(self): def format_debug_string(self):
return "[url] {title} ({url})".format( return "[url] {title} ({url})".format(
title=self.title, title=self.title,
@ -224,9 +225,9 @@ class URLItem(BaseItem):
def format_song_string(self, user): def format_song_string(self, user):
if self.ready in ['validated', 'yes']: if self.ready in ['validated', 'yes']:
return constants.strings("url_item", return constants.strings("url_item",
title=self.title if self.title else "??", title=self.title if self.title else "??",
url=self.url, url=self.url,
user=user) user=user)
return self.url return self.url
def format_current_playing(self, user): def format_current_playing(self, user):
@ -235,7 +236,7 @@ class URLItem(BaseItem):
if self.thumbnail: if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \ thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>' self.thumbnail + '"/>'
display += "<br />" + thumbnail_html display += "<br />" + thumbnail_html
return display return display

View File

@ -1,11 +1,10 @@
import youtube_dl import youtube_dl
import constants import constants
import media
import variables as var import variables as var
import hashlib
from media.item import item_builders, item_loaders, item_id_generators from media.item import item_builders, item_loaders, item_id_generators
from media.url import URLItem, url_item_id_generator from media.url import URLItem, url_item_id_generator
def get_playlist_info(url, start_index=0, user=""): def get_playlist_info(url, start_index=0, user=""):
items = [] items = []
ydl_opts = { ydl_opts = {
@ -63,6 +62,7 @@ def playlist_url_item_builder(bot, **kwargs):
def playlist_url_item_loader(bot, _dict): def playlist_url_item_loader(bot, _dict):
return PlaylistURLItem(bot, "", "", "", "", _dict) return PlaylistURLItem(bot, "", "", "", "", _dict)
item_builders['url_from_playlist'] = playlist_url_item_builder item_builders['url_from_playlist'] = playlist_url_item_builder
item_loaders['url_from_playlist'] = playlist_url_item_loader item_loaders['url_from_playlist'] = playlist_url_item_loader
item_id_generators['url_from_playlist'] = url_item_id_generator item_id_generators['url_from_playlist'] = url_item_id_generator
@ -98,11 +98,11 @@ class PlaylistURLItem(URLItem):
def format_song_string(self, user): def format_song_string(self, user):
return constants.strings("url_from_playlist_item", return constants.strings("url_from_playlist_item",
title=self.title, title=self.title,
url=self.url, url=self.url,
playlist_url=self.playlist_url, playlist_url=self.playlist_url,
playlist=self.playlist_title, playlist=self.playlist_title,
user=user) user=user)
def format_current_playing(self, user): def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user)) display = constants.strings("now_playing", item=self.format_song_string(user))
@ -110,7 +110,7 @@ class PlaylistURLItem(URLItem):
if self.thumbnail: if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \ thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>' self.thumbnail + '"/>'
display += "<br />" + thumbnail_html display += "<br />" + thumbnail_html
return display return display

View File

@ -14,8 +14,6 @@ import os
import os.path import os.path
import pymumble.pymumble_py3 as pymumble import pymumble.pymumble_py3 as pymumble
import variables as var import variables as var
import hashlib
import youtube_dl
import logging import logging
import logging.handlers import logging.handlers
import traceback import traceback
@ -25,9 +23,6 @@ import util
import command import command
import constants import constants
from database import SettingsDatabase, MusicDatabase from database import SettingsDatabase, MusicDatabase
import media.url
import media.file
import media.radio
import media.system import media.system
from media.playlist import BasePlaylist from media.playlist import BasePlaylist
from media.cache import MusicCache from media.cache import MusicCache
@ -74,8 +69,8 @@ class MumbleBot:
self.song_start_at = -1 self.song_start_at = -1
self.last_ffmpeg_err = "" self.last_ffmpeg_err = ""
self.read_pcm_size = 0 self.read_pcm_size = 0
#self.download_threads = [] # self.download_threads = []
self.wait_for_downloading = False # flag for the loop are waiting for download to complete in the other thread self.wait_for_downloading = False # flag for the loop are waiting for download to complete in the other thread
if var.config.getboolean("webinterface", "enabled"): if var.config.getboolean("webinterface", "enabled"):
wi_addr = var.config.get("webinterface", "listening_addr") wi_addr = var.config.get("webinterface", "listening_addr")
@ -151,7 +146,7 @@ class MumbleBot:
self.ducking_volume = var.db.getfloat("bot", "ducking_volume", fallback=self.ducking_volume) self.ducking_volume = var.db.getfloat("bot", "ducking_volume", fallback=self.ducking_volume)
self.ducking_threshold = var.config.getfloat("bot", "ducking_threshold", fallback=5000) self.ducking_threshold = var.config.getfloat("bot", "ducking_threshold", fallback=5000)
self.ducking_threshold = var.db.getfloat("bot", "ducking_threshold", fallback=self.ducking_threshold) self.ducking_threshold = var.db.getfloat("bot", "ducking_threshold", fallback=self.ducking_threshold)
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED, \ self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
self.ducking_sound_received) self.ducking_sound_received)
self.mumble.set_receive_sound(True) self.mumble.set_receive_sound(True)
@ -173,7 +168,7 @@ class MumbleBot:
self.nb_exit += 1 self.nb_exit += 1
if var.config.getboolean('bot', 'save_playlist', fallback=True) \ if var.config.getboolean('bot', 'save_playlist', fallback=True) \
and var.config.get("bot", "save_music_library", fallback=True): and var.config.get("bot", "save_music_library", fallback=True):
self.log.info("bot: save playlist into database") self.log.info("bot: save playlist into database")
var.playlist.save() var.playlist.save()
@ -191,9 +186,9 @@ class MumbleBot:
for command in cmds: for command in cmds:
command = command.strip() command = command.strip()
if command: if command:
self.cmd_handle[command] = { 'handle': handle, self.cmd_handle[command] = {'handle': handle,
'partial_match': not no_partial_match, 'partial_match': not no_partial_match,
'access_outside_channel': access_outside_channel} 'access_outside_channel': access_outside_channel}
self.log.debug("bot: command added: " + command) self.log.debug("bot: command added: " + command)
def set_comment(self): def set_comment(self):
@ -247,7 +242,6 @@ class MumbleBot:
constants.strings('url_ban')) constants.strings('url_ban'))
return return
command_exc = "" command_exc = ""
try: try:
if command in self.cmd_handle: if command in self.cmd_handle:
@ -278,7 +272,7 @@ class MumbleBot:
and not self.is_admin(user) \ and not self.is_admin(user) \
and not var.config.getboolean('bot', 'allow_other_channel_message') \ and not var.config.getboolean('bot', 'allow_other_channel_message') \
and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself[ and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself[
'channel_id']: 'channel_id']:
self.mumble.users[text.actor].send_text_message( self.mumble.users[text.actor].send_text_message(
constants.strings('not_in_my_channel')) constants.strings('not_in_my_channel'))
return return
@ -319,7 +313,7 @@ class MumbleBot:
def launch_music(self): def launch_music(self):
if var.playlist.is_empty(): if var.playlist.is_empty():
return return
assert self.wait_for_downloading == False assert self.wait_for_downloading is False
music_wrapper = var.playlist.current_item() music_wrapper = var.playlist.current_item()
uri = music_wrapper.uri() uri = music_wrapper.uri()
@ -341,7 +335,7 @@ class MumbleBot:
# The ffmpeg process is a thread # The ffmpeg process is a thread
# prepare pipe for catching stderr of ffmpeg # prepare pipe for catching stderr of ffmpeg
pipe_rd, pipe_wd = os.pipe() pipe_rd, pipe_wd = os.pipe()
util.pipe_no_wait(pipe_rd) # Let the pipe work in non-blocking mode util.pipe_no_wait(pipe_rd) # Let the pipe work in non-blocking mode
self.thread_stderr = os.fdopen(pipe_rd) self.thread_stderr = os.fdopen(pipe_rd)
self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480) self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
self.is_pause = False self.is_pause = False
@ -367,7 +361,6 @@ class MumbleBot:
var.playlist.remove_by_id(next.id) var.playlist.remove_by_id(next.id)
var.cache.free_and_delete(next.id) var.cache.free_and_delete(next.id)
# ======================= # =======================
# Loop # Loop
# ======================= # =======================
@ -490,14 +483,14 @@ class MumbleBot:
if rms < self.ducking_threshold: if rms < self.ducking_threshold:
print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(rms/200), end='\r') print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(rms/200), end='\r')
else: else:
print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(self.ducking_threshold/200) \ print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(self.ducking_threshold/200)
+ '+'*int((rms - self.ducking_threshold)/200), end='\r') + '+'*int((rms - self.ducking_threshold)/200), end='\r')
if rms > self.ducking_threshold: if rms > self.ducking_threshold:
if self.on_ducking is False: if self.on_ducking is False:
self.log.debug("bot: ducking triggered") self.log.debug("bot: ducking triggered")
self.on_ducking = True self.on_ducking = True
self.ducking_release = time.time() + 1 # ducking release after 1s self.ducking_release = time.time() + 1 # ducking release after 1s
# ======================= # =======================
# Play Control # Play Control
@ -558,7 +551,6 @@ class MumbleBot:
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-ss', "%f" % self.playhead, '-i', command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-ss', "%f" % self.playhead, '-i',
uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-') uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
if var.config.getboolean('bot', 'announce_current_music'): if var.config.getboolean('bot', 'announce_current_music'):
self.send_msg(var.playlist.current_item().format_current_playing()) self.send_msg(var.playlist.current_item().format_current_playing())
@ -566,13 +558,12 @@ class MumbleBot:
# The ffmpeg process is a thread # The ffmpeg process is a thread
# prepare pipe for catching stderr of ffmpeg # prepare pipe for catching stderr of ffmpeg
pipe_rd, pipe_wd = os.pipe() pipe_rd, pipe_wd = os.pipe()
util.pipe_no_wait(pipe_rd) # Let the pipe work in non-blocking mode util.pipe_no_wait(pipe_rd) # Let the pipe work in non-blocking mode
self.thread_stderr = os.fdopen(pipe_rd) self.thread_stderr = os.fdopen(pipe_rd)
self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480) self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
self.last_volume_cycle_time = time.time() self.last_volume_cycle_time = time.time()
self.pause_at_id = "" self.pause_at_id = ""
# TODO: this is a temporary workaround for issue #44 of pymumble. # TODO: this is a temporary workaround for issue #44 of pymumble.
def _clear_pymumble_soundqueue(self): def _clear_pymumble_soundqueue(self):
for id, user in self.mumble.users.items(): for id, user in self.mumble.users.items():
@ -582,7 +573,6 @@ class MumbleBot:
self.log.debug("bot: pymumble soundqueue cleared.") self.log.debug("bot: pymumble soundqueue cleared.")
def start_web_interface(addr, port): def start_web_interface(addr, port):
global formatter global formatter
import interface import interface
@ -590,9 +580,8 @@ def start_web_interface(addr, port):
# setup logger # setup logger
werkzeug_logger = logging.getLogger('werkzeug') werkzeug_logger = logging.getLogger('werkzeug')
logfile = util.solve_filepath(var.config.get('webinterface', 'web_logfile')) logfile = util.solve_filepath(var.config.get('webinterface', 'web_logfile'))
handler = None
if logfile: if logfile:
handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240) # Rotate after 10KB handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240) # Rotate after 10KB
else: else:
handler = logging.StreamHandler() handler = logging.StreamHandler()
@ -657,7 +646,7 @@ if __name__ == '__main__':
logfile = util.solve_filepath(var.config.get('bot', 'logfile')) logfile = util.solve_filepath(var.config.get('bot', 'logfile'))
handler = None handler = None
if logfile: if logfile:
handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240) # Rotate after 10KB handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240) # Rotate after 10KB
else: else:
handler = logging.StreamHandler() handler = logging.StreamHandler()

19
util.py
View File

@ -6,23 +6,16 @@ import magic
import os import os
import sys import sys
import variables as var import variables as var
import constants
import zipfile import zipfile
import requests import requests
import mutagen
import re import re
import subprocess as sp import subprocess as sp
import logging import logging
import youtube_dl import youtube_dl
from importlib import reload from importlib import reload
from PIL import Image
from io import BytesIO
from sys import platform from sys import platform
import traceback import traceback
import urllib.parse, urllib.request, urllib.error import urllib.request
import base64
import media
import media.radio
from packaging import version from packaging import version
log = logging.getLogger("bot") log = logging.getLogger("bot")
@ -65,6 +58,7 @@ def get_recursive_file_list_sorted(path):
filelist.sort() filelist.sort()
return filelist return filelist
# - zips all files of the given zippath (must be a directory) # - zips all files of the given zippath (must be a directory)
# - returns the absolute path of the created zip file # - returns the absolute path of the created zip file
# - zip file will be in the applications tmp folder (according to configuration) # - zip file will be in the applications tmp folder (according to configuration)
@ -172,7 +166,7 @@ def url_unban(url):
def pipe_no_wait(pipefd): def pipe_no_wait(pipefd):
''' Used to fetch the STDERR of ffmpeg. pipefd is the file descriptor returned from os.pipe()''' """ Used to fetch the STDERR of ffmpeg. pipefd is the file descriptor returned from os.pipe()"""
if platform == "linux" or platform == "linux2" or platform == "darwin": if platform == "linux" or platform == "linux2" or platform == "darwin":
import fcntl import fcntl
import os import os
@ -309,18 +303,19 @@ def get_url_from_input(string):
else: else:
return False return False
def youtube_search(query): def youtube_search(query):
global log global log
try: try:
r = requests.get("https://www.youtube.com/results", params={'search_query': query}, timeout=5) r = requests.get("https://www.youtube.com/results", params={'search_query': query}, timeout=5)
results = re.findall("watch\?v=(.*?)\".*?title=\"(.*?)\".*?" results = re.findall(r"watch\?v=(.*?)\".*?title=\"(.*?)\".*?"
"(?:user|channel).*?>(.*?)<", r.text) # (id, title, uploader) "(?:user|channel).*?>(.*?)<", r.text) # (id, title, uploader)
if len(results) > 0: if len(results) > 0:
return results return results
except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError, requests.exceptions.Timeout) as e: except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError, requests.exceptions.Timeout):
error_traceback = traceback.format_exc().split("During")[0] error_traceback = traceback.format_exc().split("During")[0]
log.error("util: youtube query failed with error:\n %s" % error_traceback) log.error("util: youtube query failed with error:\n %s" % error_traceback)
return False return False