REFACTOR: ITEM REVOLUTION #91

This commit is contained in:
Terry Geng
2020-03-05 16:28:08 +08:00
parent c32c6c5d9b
commit 6ab8a7958a
15 changed files with 1079 additions and 899 deletions

View File

@ -5,15 +5,16 @@ import pymumble.pymumble_py3 as pymumble
import re import re
import constants import constants
import media.file
import media.playlist
import media.radio
import media.system import media.system
import media.url
import util import util
import variables as var import variables as var
from librb import radiobrowser from librb import radiobrowser
from database import Database from database import Database
from media.playlist import PlaylistItemWrapper
from media.file import FileItem
from media.url_from_playlist import URLFromPlaylistItem, get_playlist_info
from media.url import URLItem
from media.radio import RadioItem
log = logging.getLogger("bot") log = logging.getLogger("bot")
@ -57,8 +58,8 @@ def register_all_commands(bot):
# Just for debug use # Just for debug use
bot.register_command('rtrms', cmd_real_time_rms) bot.register_command('rtrms', cmd_real_time_rms)
# bot.register_command('loop', cmd_loop_state) bot.register_command('loop', cmd_loop_state)
# bot.register_command('item', cmd_item) bot.register_command('item', cmd_item)
def send_multi_lines(bot, lines, text): def send_multi_lines(bot, lines, text):
global log global log
@ -138,16 +139,16 @@ def cmd_play(bot, user, text, command, parameter):
if var.playlist.length() > 0: if var.playlist.length() > 0:
if parameter: if parameter:
if parameter.isdigit() and int(parameter) > 0 and int(parameter) <= len(var.playlist): if parameter.isdigit() and 0 <= int(parameter) <= len(var.playlist):
var.playlist.point_to(int(parameter) - 1)
bot.interrupt_playing() bot.interrupt_playing()
bot.launch_music(int(parameter) - 1)
else: else:
bot.send_msg(constants.strings('invalid_index', index=parameter), text) bot.send_msg(constants.strings('invalid_index', index=parameter), text)
elif bot.is_pause: elif bot.is_pause:
bot.resume() bot.resume()
else: else:
bot.send_msg(util.format_current_playing(), text) bot.send_msg(var.playlist.current_item().format_current_playing(), text)
else: else:
bot.is_pause = False bot.is_pause = False
bot.send_msg(constants.strings('queue_empty'), text) bot.send_msg(constants.strings('queue_empty'), text)
@ -168,12 +169,11 @@ def cmd_play_file(bot, user, text, command, parameter):
files = util.get_recursive_file_list_sorted(var.music_folder) files = util.get_recursive_file_list_sorted(var.music_folder)
if int(parameter) < len(files): if int(parameter) < len(files):
filename = files[int(parameter)].replace(var.music_folder, '') filename = files[int(parameter)].replace(var.music_folder, '')
music = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(bot, filename), user)
'path': filename, var.playlist.append(music_wrapper)
'user': user} music = music_wrapper.item
music = var.playlist.append(music) log.info("cmd: add to playlist: " + music.format_debug_string())
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
# if parameter is {path} # if parameter is {path}
else: else:
@ -184,12 +184,11 @@ def cmd_play_file(bot, user, text, command, parameter):
return return
if os.path.isfile(path): if os.path.isfile(path):
music = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(bot, parameter), user)
'path': parameter, var.playlist.append(music_wrapper)
'user': user} music = music_wrapper.item
music = var.playlist.append(music) log.info("cmd: add to playlist: " + music.format_debug_string())
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
return return
# if parameter is {folder} # if parameter is {folder}
@ -211,12 +210,11 @@ def cmd_play_file(bot, user, text, command, parameter):
for file in files: for file in files:
count += 1 count += 1
music = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
'path': file, var.playlist.append(music_wrapper)
'user': user} music = music_wrapper.item
music = var.playlist.append(music) log.info("cmd: add to playlist: " + music.format_debug_string())
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) msgs.append("{} ({})".format(music.title, music.path))
msgs.append("{} ({})".format(music['title'], music['path']))
if count != 0: if count != 0:
send_multi_lines(bot, msgs, text) send_multi_lines(bot, msgs, text)
@ -230,12 +228,12 @@ def cmd_play_file(bot, user, text, command, parameter):
if len(matches) == 0: if len(matches) == 0:
bot.send_msg(constants.strings('no_file'), text) bot.send_msg(constants.strings('no_file'), text)
elif len(matches) == 1: elif len(matches) == 1:
music = {'type': 'file', file = matches[0][1]
'path': matches[0][1], music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
'user': user} var.playlist.append(music_wrapper)
music = var.playlist.append(music) music = music_wrapper.item
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) log.info("cmd: add to playlist: " + music.format_debug_string())
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text) bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
else: else:
msgs = [ constants.strings('multiple_matches')] msgs = [ constants.strings('multiple_matches')]
for match in matches: for match in matches:
@ -256,13 +254,11 @@ def cmd_play_file_match(bot, user, text, command, parameter):
match = re.search(parameter, file) match = re.search(parameter, file)
if match: if match:
count += 1 count += 1
music = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
'path': file, var.playlist.append(music_wrapper)
'user': user} music = music_wrapper.item
music = var.playlist.append(music) log.info("cmd: add to playlist: " + music.format_debug_string())
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) msgs.append("{} ({})".format(music.title, music.path))
msgs.append("{} ({})".format(music['title'], music['path']))
if count != 0: if count != 0:
send_multi_lines(bot, msgs, text) send_multi_lines(bot, msgs, text)
@ -279,22 +275,15 @@ def cmd_play_file_match(bot, user, text, command, parameter):
def cmd_play_url(bot, user, text, command, parameter): def cmd_play_url(bot, user, text, command, parameter):
global log global log
music = {'type': 'url', url = util.get_url_from_input(parameter)
# grab the real URL music_wrapper = PlaylistItemWrapper(URLItem(bot, url), user)
'url': util.get_url_from_input(parameter), var.playlist.append(music_wrapper)
'user': user,
'ready': 'validation'}
music = bot.validate_music(music) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
if music: bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
music = var.playlist.append(music)
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
if var.playlist.length() == 2: if var.playlist.length() == 2:
# If I am the second item on the playlist. (I am the next one!) # If I am the second item on the playlist. (I am the next one!)
bot.async_download_next() bot.async_download_next()
else:
bot.send_msg(constants.strings('unable_download'), text)
def cmd_play_playlist(bot, user, text, command, parameter): def cmd_play_playlist(bot, user, text, command, parameter):
@ -308,11 +297,11 @@ def cmd_play_playlist(bot, user, text, command, parameter):
url = util.get_url_from_input(parameter) url = util.get_url_from_input(parameter)
log.debug("cmd: fetching media info from playlist url %s" % url) log.debug("cmd: fetching media info from playlist url %s" % url)
items = media.playlist.get_playlist_info(url=url, start_index=offset, user=user) items = get_playlist_info(bot, url=url, start_index=offset, user=user)
if len(items) > 0: if len(items) > 0:
var.playlist.extend(items) var.playlist.extend(items)
for music in items: for music in items:
log.info("cmd: add to playlist: " + util.format_debug_song_string(music)) log.info("cmd: add to playlist: " + music.format_debug_string())
else: else:
bot.send_msg(constants.strings("playlist_fetching_failed"), text) bot.send_msg(constants.strings("playlist_fetching_failed"), text)
@ -335,16 +324,10 @@ def cmd_play_radio(bot, user, text, command, parameter):
parameter = parameter.split()[0] parameter = parameter.split()[0]
url = util.get_url_from_input(parameter) url = util.get_url_from_input(parameter)
if url: if url:
music = {'type': 'radio', music_wrapper = PlaylistItemWrapper(RadioItem(bot, url), user)
'url': url,
'user': user}
log.info("bot: fetching radio server description") var.playlist.append(music_wrapper)
music["name"] = media.radio.get_radio_server_description(url) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
var.playlist.append(music)
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
bot.async_download_next()
else: else:
bot.send_msg(constants.strings('bad_url')) bot.send_msg(constants.strings('bad_url'))
@ -440,13 +423,9 @@ def cmd_rb_play(bot, user, text, command, parameter):
url = radiobrowser.geturl_byid(parameter) url = radiobrowser.geturl_byid(parameter)
if url != "-1": if url != "-1":
log.info('cmd: Found url: ' + url) log.info('cmd: Found url: ' + url)
music = {'type': 'radio', music_wrapper = PlaylistItemWrapper(RadioItem(bot, url, stationname), user)
'name': stationname, var.playlist.append(music_wrapper)
'artist': homepage, log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
'url': url,
'user': user}
var.playlist.append(music)
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
bot.async_download_next() bot.async_download_next()
else: else:
log.info('cmd: No playable url found.') log.info('cmd: No playable url found.')
@ -637,7 +616,7 @@ def cmd_current_music(bot, user, text, command, parameter):
reply = "" reply = ""
if var.playlist.length() > 0: if var.playlist.length() > 0:
bot.send_msg(util.format_current_playing()) bot.send_msg(var.playlist.current_item().format_current_playing())
else: else:
reply = constants.strings('not_playing') reply = constants.strings('not_playing')
bot.send_msg(reply, text) bot.send_msg(reply, text)
@ -647,9 +626,7 @@ def cmd_skip(bot, user, text, command, parameter):
global log global log
if var.playlist.length() > 0: if var.playlist.length() > 0:
bot.stop() bot.interrupt_playing()
bot.launch_music()
bot.async_download_next()
else: else:
bot.send_msg(constants.strings('queue_empty'), text) bot.send_msg(constants.strings('queue_empty'), text)
@ -668,10 +645,6 @@ def cmd_last(bot, user, text, command, parameter):
def cmd_remove(bot, user, text, command, parameter): def cmd_remove(bot, user, text, command, parameter):
global log global log
if bot.download_in_progress:
bot.send_msg(constants.strings("cannot_change_when_download"))
return
# 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 int(parameter) > 0 \
and int(parameter) <= var.playlist.length(): and int(parameter) <= var.playlist.length():
@ -695,11 +668,10 @@ def cmd_remove(bot, user, text, command, parameter):
else: else:
removed = var.playlist.remove(index) removed = var.playlist.remove(index)
# the Title isn't here if the music wasn't downloaded
bot.send_msg(constants.strings('removing_item', bot.send_msg(constants.strings('removing_item',
item=removed['title'] if 'title' in removed else removed['url']), text) item=removed.format_song_string()), text)
log.info("cmd: delete from playlist: " + str(removed['path'] if 'path' in removed else removed['url'])) log.info("cmd: delete from playlist: " + removed.format_debug_string())
else: else:
bot.send_msg(constants.strings('bad_parameter', command=command)) bot.send_msg(constants.strings('bad_parameter', command=command))
@ -772,9 +744,9 @@ def cmd_repeat(bot, user, text, command, parameter):
var.playlist.current_index + 1, var.playlist.current_index + 1,
music music
) )
log.info("bot: add to playlist: " + util.format_debug_song_string(music)) log.info("bot: add to playlist: " + music.format_debug_string)
bot.send_msg(constants.strings("repeat", song=util.format_song_string(music), 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
@ -811,4 +783,4 @@ def cmd_loop_state(bot, user, text, command, parameter):
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()) print(var.playlist.current_item().item.to_dict())

View File

@ -182,15 +182,19 @@ multiple_matches = Track not found! Possible candidates:
queue_contents = Items on the playlist: queue_contents = Items on the playlist:
queue_empty = Playlist is empty! queue_empty = Playlist is empty!
invalid_index = Invalid index <i>{index}</i>. Use '!queue' to see your playlist. invalid_index = Invalid index <i>{index}</i>. Use '!queue' to see your playlist.
now_playing_radio = Now Playing Radio: <br /> <a href="{url}">{title}</a> <i>from</i> {name} <i>added by</i> {user} now_playing = Playing <br />{item}
now_playing_file = Now Playing File:<br /> {artist} - {title} <i>added by</i> {user} radio = Radio
now_playing_from_playlist = Now Playing URL:<br /> <a href="{url}">{title}</a> <i>from playlist</i> <a href="{playlist_url}">{playlist}</a> <i>added by</i> {user} file = File
now_playing_url = Now Playing URL: <br /> <a href="{url}">{title}</a> <i>added by</i> {user} url_from_playlist = URL
url = URL
radio_item = <a href="{url}">{title}</a> <i>from</i> {name} <i>added by</i> {user}
file_item = {artist} - {title} <i>added by</i> {user}
url_from_playlist_item = <a href="{url}">{title}</a> <i>from playlist</i> <a href="{playlist_url}">{playlist}</a> <i>added by</i> {user}
url_item = <a href="{url}">{title}</a> <i>added by</i> {user}
not_in_my_channel = You're not in my channel, command refused! not_in_my_channel = You're not in my channel, command refused!
pm_not_allowed = Private message aren't allowed. pm_not_allowed = Private message aren't allowed.
too_long = This music is too long, skip! too_long = This music is too long, skip!
download_in_progress = Download of {item} in progress... download_in_progress = Download of {item} in progress...
cannot_change_when_download = Downloading songs, please wait until the download completes.
removing_item = Removed entry {item} from playlist. removing_item = Removed entry {item} from playlist.
user_ban = You are banned, not allowed to do that! user_ban = You are banned, not allowed to do that!
url_ban = This url is banned! url_ban = This url is banned!

View File

@ -12,7 +12,11 @@ import random
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import errno import errno
import media import media
import media.radio from media.playlist import PlaylistItemWrapper
from media.file import FileItem
from media.url_from_playlist import URLFromPlaylistItem, get_playlist_info
from media.url import URLItem
from media.radio import RadioItem
import logging import logging
import time import time
import constants import constants
@ -58,6 +62,7 @@ class ReverseProxied(object):
web = Flask(__name__) web = Flask(__name__)
log = logging.getLogger("bot") log = logging.getLogger("bot")
user = 'Remote Control'
def init_proxy(): def init_proxy():
global web global web
@ -109,7 +114,7 @@ def index():
os=os, os=os,
playlist=var.playlist, playlist=var.playlist,
user=var.user, user=var.user,
paused=var.botamusique.is_pause paused=var.bot.is_pause
) )
@web.route("/playlist", methods=['GET']) @web.route("/playlist", methods=['GET'])
@ -124,10 +129,10 @@ def playlist():
items = [] items = []
for index, item 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,
m=item, m=item_wrapper.item,
playlist=var.playlist playlist=var.playlist
) )
) )
@ -138,7 +143,7 @@ def status():
if (var.playlist.length() > 0): if (var.playlist.length() > 0):
return jsonify({'ver': var.playlist.version, return jsonify({'ver': var.playlist.version,
'empty': False, 'empty': False,
'play': not var.botamusique.is_pause, 'play': not var.bot.is_pause,
'mode': var.playlist.mode}) 'mode': var.playlist.mode})
else: else:
return jsonify({'ver': var.playlist.version, return jsonify({'ver': var.playlist.version,
@ -159,25 +164,16 @@ def post():
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):
item = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(var.bot, request.form['add_file_bottom']), user)
'path' : request.form['add_file_bottom'], var.playlist.append(music_wrapper)
'title' : '', log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
'user' : 'Remote Control'}
item = var.playlist.append(util.attach_music_tag_info(item))
log.info('web: add to playlist(bottom): ' + util.format_debug_song_string(item))
elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']: elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
path = var.music_folder + request.form['add_file_next'] path = var.music_folder + request.form['add_file_next']
if os.path.isfile(path): if os.path.isfile(path):
item = {'type': 'file', music_wrapper = PlaylistItemWrapper(FileItem(var.bot, request.form['add_file_next']), user)
'path' : request.form['add_file_next'], var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
'title' : '', log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
'user' : 'Remote Control'}
item = var.playlist.insert(
var.playlist.current_index + 1,
item
)
log.info('web: add to playlist(next): ' + util.format_debug_song_string(item))
elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']): elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']):
try: try:
@ -202,42 +198,35 @@ def post():
else: else:
files = music_library.get_files(folder) files = music_library.get_files(folder)
files = list(map(lambda file: music_wrappers = list(map(
{'type':'file', lambda file: PlaylistItemWrapper(FileItem(var.bot, file), user),
'path': os.path.join(folder, file), files))
'user':'Remote Control'}, files))
files = var.playlist.extend(files) var.playlist.extend(files)
for file in files: for music_wrapper in music_wrappers:
log.info("web: add to playlist: %s" % util.format_debug_song_string(file)) log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
elif 'add_url' in request.form: elif 'add_url' in request.form:
music = {'type':'url', music_wrapper = PlaylistItemWrapper(URLItem(var.bot, request.form['add_url']), user)
'url': request.form['add_url'], var.playlist.append(music_wrapper)
'user': 'Remote Control',
'ready': 'validation'} log.info("web: add to playlist: " + music_wrapper.format_debug_string())
music = var.botamusique.validate_music(music)
if music:
var.playlist.append(music)
log.info("web: add to playlist: " + util.format_debug_song_string(music))
if var.playlist.length() == 2: if var.playlist.length() == 2:
# If I am the second item on the playlist. (I am the next one!) # If I am the second item on the playlist. (I am the next one!)
var.botamusique.async_download_next() var.bot.async_download_next()
elif 'add_radio' in request.form: elif 'add_radio' in request.form:
url = request.form['add_radio'] url = request.form['add_radio']
music = var.playlist.append({'type': 'radio', music_wrapper = PlaylistItemWrapper(RadioItem(var.bot, url), user)
'url': url, var.playlist.append(music_wrapper)
'user': "Remote Control"})
log.info("web: fetching radio server description") log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
music["name"] = media.radio.get_radio_server_description(url)
log.info("web: add to playlist: " + util.format_debug_song_string(music))
elif 'delete_music' in request.form: elif 'delete_music' in request.form:
music = var.playlist[int(request.form['delete_music'])] music_wrapper = var.playlist[int(request.form['delete_music'])]
log.info("web: delete from playlist: " + util.format_debug_song_string(music)) log.info("web: delete from playlist: " + music_wrapper.format_debug_string())
if var.playlist.length() >= int(request.form['delete_music']): if var.playlist.length() >= int(request.form['delete_music']):
index = int(request.form['delete_music']) index = int(request.form['delete_music'])
@ -246,26 +235,26 @@ def post():
var.playlist.remove(index) var.playlist.remove(index)
if index < len(var.playlist): if index < len(var.playlist):
if not var.botamusique.is_pause: if not var.bot.is_pause:
var.botamusique.interrupt_playing() var.bot.interrupt_playing()
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 var.botamusique.is_pause: if not var.bot.is_pause:
var.botamusique.interrupt_playing() var.bot.interrupt_playing()
else: else:
var.playlist.remove(index) var.playlist.remove(index)
elif 'play_music' in request.form: elif 'play_music' in request.form:
music = var.playlist[int(request.form['play_music'])] music_wrapper = var.playlist[int(request.form['play_music'])]
log.info("web: jump to: " + util.format_debug_song_string(music)) log.info("web: jump to: " + music_wrapper.format_debug_string())
if len(var.playlist) >= int(request.form['play_music']): if len(var.playlist) >= int(request.form['play_music']):
var.botamusique.interrupt_playing() var.playlist.point_to(int(request.form['play_music']) - 1)
var.botamusique.launch_music(int(request.form['play_music'])) var.bot.interrupt_playing()
elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']: elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']:
path = var.music_folder + request.form['delete_music_file'] path = var.music_folder + request.form['delete_music_file']
@ -283,7 +272,7 @@ def post():
elif 'action' in request.form: elif 'action' in request.form:
action = request.form['action'] action = request.form['action']
if action == "randomize": if action == "randomize":
var.botamusique.interrupt_playing() var.bot.interrupt_playing()
var.playlist.set_mode("random") var.playlist.set_mode("random")
var.db.set('playlist', 'playback_mode', "random") var.db.set('playlist', 'playback_mode', "random")
log.info("web: playback mode changed to random.") log.info("web: playback mode changed to random.")
@ -296,27 +285,27 @@ def post():
var.db.set('playlist', 'playback_mode', "repeat") var.db.set('playlist', 'playback_mode', "repeat")
log.info("web: playback mode changed to repeat.") log.info("web: playback mode changed to repeat.")
elif action == "stop": elif action == "stop":
var.botamusique.stop() var.bot.stop()
elif action == "pause": elif action == "pause":
var.botamusique.pause() var.bot.pause()
elif action == "resume": elif action == "resume":
var.botamusique.resume() var.bot.resume()
elif action == "clear": elif action == "clear":
var.botamusique.clear() var.bot.clear()
elif action == "volume_up": elif action == "volume_up":
if var.botamusique.volume_set + 0.03 < 1.0: if var.bot.volume_set + 0.03 < 1.0:
var.botamusique.volume_set = var.botamusique.volume_set + 0.03 var.bot.volume_set = var.bot.volume_set + 0.03
else: else:
var.botamusique.volume_set = 1.0 var.bot.volume_set = 1.0
var.db.set('bot', 'volume', str(var.botamusique.volume_set)) var.db.set('bot', 'volume', str(var.bot.volume_set))
log.info("web: volume up to %d" % (var.botamusique.volume_set * 100)) log.info("web: volume up to %d" % (var.bot.volume_set * 100))
elif action == "volume_down": elif action == "volume_down":
if var.botamusique.volume_set - 0.03 > 0: if var.bot.volume_set - 0.03 > 0:
var.botamusique.volume_set = var.botamusique.volume_set - 0.03 var.bot.volume_set = var.bot.volume_set - 0.03
else: else:
var.botamusique.volume_set = 0 var.bot.volume_set = 0
var.db.set('bot', 'volume', str(var.botamusique.volume_set)) var.db.set('bot', 'volume', str(var.bot.volume_set))
log.info("web: volume up to %d" % (var.botamusique.volume_set * 100)) log.info("web: volume up to %d" % (var.bot.volume_set * 100))
return status() return status()

View File

@ -0,0 +1,158 @@
import logging
import os
import re
from io import BytesIO
import base64
import hashlib
import mutagen
from PIL import Image
import json
import util
import variables as var
from media.item import BaseItem
import constants
'''
type : file
id
path
title
artist
duration
thumbnail
user
'''
class FileItem(BaseItem):
def __init__(self, bot, path, from_dict=None):
if not from_dict:
super().__init__(bot)
self.path = path
self.title = ""
self.artist = "??"
self.thumbnail = None
if self.path:
self.id = hashlib.md5(path.encode()).hexdigest()
if os.path.exists(self.uri()):
self._get_info_from_tag()
self.ready = "yes"
else:
super().__init__(bot, from_dict)
self.path = from_dict['path']
self.title = from_dict['title']
self.artist = from_dict['artist']
self.thumbnail = from_dict['thumbnail']
if not self.validate():
self.ready = "failed"
self.type = "file"
def uri(self):
return var.music_folder + self.path
def is_ready(self):
return True
def validate(self):
if not os.path.exists(self.uri()):
self.log.info(
"file: music file missed for %s" % self.format_debug_string())
self.send_client_message(constants.strings('file_missed', file=self.path))
return False
self.ready = "yes"
return True
def _get_info_from_tag(self):
match = re.search("(.+)\.(.+)", self.uri())
assert match is not None
file_no_ext = match[1]
ext = match[2]
try:
im = None
path_thumbnail = file_no_ext + ".jpg"
if os.path.isfile(path_thumbnail):
im = Image.open(path_thumbnail)
if ext == "mp3":
# title: TIT2
# artist: TPE1, TPE2
# album: TALB
# cover artwork: APIC:
tags = mutagen.File(self.uri())
if 'TIT2' in tags:
self.title = tags['TIT2'].text[0]
if 'TPE1' in tags: # artist
self.artist = tags['TPE1'].text[0]
if im is None:
if "APIC:" in tags:
im = Image.open(BytesIO(tags["APIC:"].data))
elif ext == "m4a" or ext == "m4b" or ext == "mp4" or ext == "m4p":
# title: ©nam (\xa9nam)
# artist: ©ART
# album: ©alb
# cover artwork: covr
tags = mutagen.File(self.uri())
if '©nam' in tags:
self.title = tags['©nam'][0]
if '©ART' in tags: # artist
self.artist = tags['©ART'][0]
if im is None:
if "covr" in tags:
im = Image.open(BytesIO(tags["covr"][0]))
if im:
self.thumbnail = self._prepare_thumbnail(im)
except:
pass
if not self.title:
self.title = os.path.basename(file_no_ext)
def _prepare_thumbnail(self, im):
im.thumbnail((100, 100), Image.ANTIALIAS)
buffer = BytesIO()
im = im.convert('RGB')
im.save(buffer, format="JPEG")
return base64.b64encode(buffer.getvalue()).decode('utf-8')
def to_dict(self):
dict = super().to_dict()
dict['type'] = 'file'
dict['path'] = self.path
dict['title'] = self.title
dict['artist'] = self.artist
dict['thumbnail'] = self.thumbnail
return dict
def format_debug_string(self):
return "[file] {artist} - {title} ({path})".format(
title=self.title,
artist=self.artist,
path=self.path
)
def format_song_string(self, user):
return constants.strings("file_item",
title=self.title,
artist=self.artist,
user=user
)
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>'
display += "<br />" + thumbnail_html
return display
def display_type(self):
return constants.strings("file")

98
media/item.py Normal file
View File

@ -0,0 +1,98 @@
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
"""
FORMAT OF A MUSIC INTO THE PLAYLIST
type : url
id
url
title
path
duration
artist
thumbnail
user
ready (validation, no, downloading, yes, failed)
from_playlist (yes,no)
playlist_title
playlist_url
type : radio
id
url
name
current_title
user
"""
class BaseItem:
def __init__(self, bot, from_dict=None):
self.bot = bot
self.log = logging.getLogger("bot")
self.type = "base"
if from_dict is None:
self.id = ""
self.ready = "pending" # pending - is_valid() -> validated - prepare() -> yes, failed
else:
self.id = from_dict['id']
self.ready = from_dict['ready']
def is_ready(self):
return True if self.ready == "yes" else False
def is_failed(self):
return True if self.ready == "failed" else False
def validate(self):
return False
def uri(self):
raise
def async_prepare(self):
th = threading.Thread(
target=self.prepare, name="Prepare-" + self.id[:7])
self.log.info(
"%s: start preparing item in thread: " % self.type + self.format_debug_string())
th.daemon = True
th.start()
#self.download_threads.append(th)
return th
def prepare(self):
return True
def play(self):
pass
def format_song_string(self, user):
return self.id
def format_current_playing(self, user):
return self.id
def format_debug_string(self):
return self.id
def display_type(self):
return ""
def send_client_message(self, msg):
self.bot.send_msg(msg)
def to_dict(self):
return {"type" : "base", "id": self.id, "ready": self.ready}

View File

@ -1,44 +1,271 @@
import youtube_dl import json
import random
import hashlib
import threading
import logging
import util
import variables as var import variables as var
from media.item import BaseItem
from media.file import FileItem
from media.url import URLItem
def get_playlist_info(url, start_index=0, user=""): class PlaylistItemWrapper:
items = [] def __init__(self, item, user):
ydl_opts = { self.item = item
'extract_flat': 'in_playlist' self.user = user
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
attempts = var.config.getint('bot', 'download_attempts', fallback=2)
for i in range(attempts):
try:
info = ydl.extract_info(url, download=False)
# # if url is not a playlist but a video
# if 'entries' not in info and 'webpage_url' in info:
# music = {'type': 'url',
# 'title': info['title'],
# 'url': info['webpage_url'],
# 'user': user,
# 'ready': 'validation'}
# items.append(music)
# return items
playlist_title = info['title'] def to_dict(self):
for j in range(start_index, min(len(info['entries']), start_index + var.config.getint('bot', 'max_track_playlist'))): dict = self.item.to_dict()
# Unknow String if No title into the json dict['user'] = self.user
title = info['entries'][j]['title'] if 'title' in info['entries'][j] else "Unknown Title" return dict
# Add youtube url if the url in the json isn't a full url
url = info['entries'][j]['url'] if info['entries'][j]['url'][0:4] == 'http' else "https://www.youtube.com/watch?v=" + info['entries'][j]['url']
music = {'type': 'url', def format_current_playing(self):
'title': title, return self.item.format_current_playing(self.user)
'url': url,
'user': user,
'from_playlist': True,
'playlist_title': playlist_title,
'playlist_url': url,
'ready': 'validation'}
items.append(music)
except:
pass
def format_song_string(self):
return self.item.format_song_string(self.user)
def format_debug_string(self):
return self.item.format_debug_string()
def dict_to_item(dict):
if dict['type'] == 'file':
return PlaylistItemWrapper(FileItem(var.bot, "", dict), dict['user'])
elif dict['type'] == 'url':
return PlaylistItemWrapper(URLItem(var.bot, "", dict), dict['user'])
class PlayList(list):
def __init__(self, *args):
super().__init__(*args)
self.current_index = -1
self.version = 0 # increase by one after each change
self.mode = "one-shot" # "repeat", "random"
self.pending_items = []
self.log = logging.getLogger("bot")
self.validating_thread_lock = threading.Lock()
def is_empty(self):
return True if len(self) == 0 else False
def set_mode(self, mode):
# modes are "one-shot", "repeat", "random"
self.mode = mode
if mode == "random":
self.randomize()
elif mode == "one-shot" and self.current_index > 0:
# remove items before current item
self.version += 1
for i in range(self.current_index):
super().__delitem__(0)
self.current_index = 0
def append(self, item: PlaylistItemWrapper):
self.version += 1
super().append(item)
self.pending_items.append(item)
self.start_async_validating()
return item
def insert(self, index, item):
self.version += 1
if index == -1:
index = self.current_index
item = util.attach_music_tag_info(item)
super().insert(index, item)
if index <= self.current_index:
self.current_index += 1
self.pending_items.append(item)
self.start_async_validating()
return item
def length(self):
return len(self)
def extend(self, items):
self.version += 1
items = list(map(
lambda item: item,
items))
super().extend(items)
self.pending_items.extend(items)
self.start_async_validating()
return items return items
def next(self):
if len(self) == 0:
return False
self.version += 1
#logging.debug("playlist: Next into the queue")
if self.current_index < len(self) - 1:
if self.mode == "one-shot" and self.current_index != -1:
super().__delitem__(self.current_index)
else:
self.current_index += 1
return self[self.current_index]
else:
self.current_index = 0
if self.mode == "one-shot":
self.clear()
return False
elif self.mode == "repeat":
return self[0]
elif self.mode == "random":
self.randomize()
return self[0]
else:
raise TypeError("Unknown playlist mode '%s'." % self.mode)
def point_to(self, index):
if -1 <= index < len(self):
self.current_index = index
def find(self, id):
for index, wrapper in enumerate(self):
if wrapper.item.id == id:
return index
return None
def update(self, item, id):
self.version += 1
index = self.find(id)
if index:
self[index] = item
return True
return False
def __delitem__(self, key):
return self.remove(key)
def remove(self, index=-1):
self.version += 1
if index > len(self) - 1:
return False
if index == -1:
index = self.current_index
removed = self[index]
super().__delitem__(index)
if self.current_index > index:
self.current_index -= 1
return removed
def remove_by_id(self, id):
to_be_removed = []
for index, item in enumerate(self):
if item.id == id:
to_be_removed.append(index)
for index in to_be_removed:
self.remove(index)
def current_item(self):
if len(self) == 0:
return False
return self[self.current_index]
def next_index(self):
if len(self) == 0 or (len(self) == 1 and self.mode == 'one_shot'):
return False
if self.current_index < len(self) - 1:
return self.current_index + 1
else:
return 0
def next_item(self):
if len(self) == 0 or (len(self) == 1 and self.mode == 'one_shot'):
return False
return self[self.next_index()]
def jump(self, index):
if self.mode == "one-shot":
for i in range(index):
super().__delitem__(0)
self.current_index = 0
else:
self.current_index = index
self.version += 1
return self[self.current_index]
def randomize(self):
# current_index will lose track after shuffling, thus we take current music out before shuffling
#current = self.current_item()
#del self[self.current_index]
random.shuffle(self)
#self.insert(0, current)
self.current_index = -1
self.version += 1
def clear(self):
self.version += 1
self.current_index = -1
super().clear()
def save(self):
var.db.remove_section("playlist_item")
var.db.set("playlist", "current_index", self.current_index)
for index, music in enumerate(self):
var.db.set("playlist_item", str(index), json.dumps(music.to_dict()))
def load(self):
current_index = var.db.getint("playlist", "current_index", fallback=-1)
if current_index == -1:
return
items = list(var.db.items("playlist_item"))
items.sort(key=lambda v: int(v[0]))
self.extend(list(map(lambda v: dict_to_item(json.loads(v[1])), items)))
self.current_index = current_index
def _debug_print(self):
print("===== Playlist(%d)=====" % self.current_index)
for index, item_wrapper in enumerate(self):
if index == self.current_index:
print("-> %d %s" % (index, item_wrapper.item.title))
else:
print("%d %s" % (index, item_wrapper.item.title))
print("===== End =====")
def start_async_validating(self):
if not self.validating_thread_lock.locked():
th = threading.Thread(target=self._check_valid, name="Validating")
th.daemon = True
th.start()
def _check_valid(self):
self.log.debug("playlist: start validating...")
self.validating_thread_lock.acquire()
while len(self.pending_items) > 0:
item = self.pending_items.pop().item
self.log.debug("playlist: validating %s" % item.format_debug_string())
if not item.validate() or item.ready == 'failed':
# TODO: logging
self.remove_by_id(item.id)
self.log.debug("playlist: validating finished.")
self.validating_thread_lock.release()

View File

@ -1,16 +1,19 @@
import re import re
import logging import logging
import json
import http.client
import struct import struct
import requests import requests
import traceback import traceback
import hashlib
from media.item import BaseItem
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")
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)
@ -50,6 +53,9 @@ def get_radio_server_description(url):
def get_radio_title(url): def get_radio_title(url):
global log
log.debug("radio: fetching radio server description")
try: try:
r = requests.get(url, headers={'Icy-MetaData': '1'}, stream=True, timeout=5) r = requests.get(url, headers={'Icy-MetaData': '1'}, stream=True, timeout=5)
icy_metaint_header = int(r.headers['icy-metaint']) icy_metaint_header = int(r.headers['icy-metaint'])
@ -67,3 +73,57 @@ def get_radio_title(url):
except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as e: except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as e:
pass pass
return url return url
class RadioItem(BaseItem):
def __init__(self, bot, url, name="", from_dict=None):
if from_dict is None:
super().__init__(bot)
self.url = url
if not name:
self.title = get_radio_server_description(self.url) # The title of the radio station
else:
self.title = name
self.id = hashlib.md5(url.encode()).hexdigest()
else:
super().__init__(bot, from_dict)
self.url = from_dict['url']
self.title = from_dict['title']
self.type = "radio"
def validate(self):
return True
def is_ready(self):
return True
def uri(self):
return self.url
def to_dict(self):
dict = super().to_dict()
dict['url'] = self.url
dict['title'] = self.title
def format_debug_string(self):
return "[radio] {name} ({url})".format(
name=self.title,
url=self.url
)
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
)
def format_current_playing(self, user):
return constants.strings("now_playing", item=self.format_song_string(user))
def display_type(self):
return constants.strings("radio")

View File

@ -1,22 +1,215 @@
import threading
import logging
import os
import hashlib
import traceback
from PIL import Image
import youtube_dl import youtube_dl
import glob
import constants
import media
import variables as var import variables as var
from media.file import FileItem
import media.system
log = logging.getLogger("bot")
def get_url_info(music): class URLItem(FileItem):
def __init__(self, bot, url, from_dict=None):
self.validating_lock = threading.Lock()
if from_dict is None:
self.url = url
self.title = ''
self.duration = 0
self.ready = 'pending'
super().__init__(bot, "")
self.id = hashlib.md5(url.encode()).hexdigest()
path = var.tmp_folder + self.id + ".mp3"
if os.path.isfile(path):
self.log.info("url: file existed for url %s " % self.url)
self.ready = 'yes'
self.path = path
self._get_info_from_tag()
else:
# self._get_info_from_url()
pass
else:
super().__init__(bot, "", from_dict)
self.url = from_dict['url']
self.duration = from_dict['duration']
self.downloading = False
self.type = "url"
def uri(self):
return self.path
def is_ready(self):
if self.downloading or self.ready != 'yes':
return False
if self.ready == 'yes' and not os.path.exists(self.path):
self.log.info(
"url: music file missed for %s" % self.format_debug_string())
self.ready = 'validated'
return False
return True
def validate(self):
if self.ready in ['yes', 'validated']:
return True
if os.path.exists(self.path):
self.ready = "yes"
return True
# avoid multiple process validating in the meantime
self.validating_lock.acquire()
info = self._get_info_from_url()
self.validating_lock.release()
if self.duration == 0 and not info:
return False
if self.duration > var.config.getint('bot', 'max_track_duration') != 0:
# 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) + " min -- too long")
self.send_client_message(constants.strings('too_long'))
return False
else:
self.ready = "validated"
return True
# Run in a other thread
def prepare(self):
if not self.downloading:
assert self.ready == 'validated'
return self._download()
else:
assert self.ready == 'yes'
return True
def _get_info_from_url(self):
self.log.info("url: fetching metadata of url %s " % self.url)
ydl_opts = { ydl_opts = {
'noplaylist': True 'noplaylist': True
} }
music['duration'] = 0 succeed = False
with youtube_dl.YoutubeDL(ydl_opts) as ydl: with youtube_dl.YoutubeDL(ydl_opts) as ydl:
for i in range(2): attempts = var.config.getint('bot', 'download_attempts', fallback=2)
for i in range(attempts):
try: try:
info = ydl.extract_info(music['url'], download=False) info = ydl.extract_info(self.url, download=False)
music['duration'] = info['duration'] / 60 self.duration = info['duration'] / 60
music['title'] = info['title'] self.title = info['title']
succeed = True
return True
except youtube_dl.utils.DownloadError: except youtube_dl.utils.DownloadError:
pass pass
except KeyError:
return music if not succeed:
else: self.ready = 'failed'
return music self.log.error("url: error while fetching info from the URL")
self.send_client_message(constants.strings('unable_download'))
return False return False
def _download(self):
media.system.clear_tmp_folder(var.tmp_folder, var.config.getint('bot', 'tmp_folder_max_size'))
self.downloading = True
base_path = var.tmp_folder + self.id
save_path = base_path + ".%(ext)s"
mp3_path = base_path + ".mp3"
# Download only if music is not existed
self.ready = "preparing"
self.log.info("bot: downloading url (%s) %s " % (self.title, self.url))
ydl_opts = ""
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': save_path,
'noplaylist': True,
'writethumbnail': True,
'updatetime': False,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192'},
{'key': 'FFmpegMetadata'}]
}
# TODO
self.send_client_message(constants.strings('download_in_progress', item=self.url))
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
attempts = var.config.getint('bot', 'download_attempts', fallback=2)
download_succeed = False
for i in range(attempts):
self.log.info("bot: download attempts %d / %d" % (i+1, attempts))
try:
info = ydl.extract_info(self.url)
download_succeed = True
break
except:
error_traceback = traceback.format_exc().split("During")[0]
error = error_traceback.rstrip().split("\n")[-1]
self.log.error("bot: download failed with error:\n %s" % error)
if download_succeed:
self.path = mp3_path
self.ready = "yes"
self.log.info(
"bot: finished downloading url (%s) %s, saved to %s." % (self.title, self.url, self.path))
self.downloading = False
return True
else:
for f in glob.glob(base_path + "*"):
os.remove(f)
self.send_client_message(constants.strings('unable_download'))
self.ready = "failed"
self.downloading = False
return False
def _read_thumbnail_from_file(self, path_thumbnail):
if os.path.isfile(path_thumbnail):
im = Image.open(path_thumbnail)
self.thumbnail = self._prepare_thumbnail(im)
def to_dict(self):
dict = super().to_dict()
dict['type'] = 'url'
dict['url'] = self.url
dict['duration'] = self.duration
return dict
def format_debug_string(self):
return "[url] {title} ({url})".format(
title=self.title,
url=self.url
)
def format_song_string(self, user):
return constants.strings("url_item",
title=self.title,
url=self.url,
user=user)
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>'
display += "<br />" + thumbnail_html
return display
def display_type(self):
return constants.strings("url")

View File

@ -0,0 +1,99 @@
import youtube_dl
import constants
import media
import variables as var
from media.url import URLItem
from media.playlist import PlaylistItemWrapper
def get_playlist_info(bot, url, start_index=0, user=""):
items = []
ydl_opts = {
'extract_flat': 'in_playlist'
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
attempts = var.config.getint('bot', 'download_attempts', fallback=2)
for i in range(attempts):
try:
info = ydl.extract_info(url, download=False)
# # if url is not a playlist but a video
# if 'entries' not in info and 'webpage_url' in info:
# music = {'type': 'url',
# 'title': info['title'],
# 'url': info['webpage_url'],
# 'user': user,
# 'ready': 'validation'}
# items.append(music)
# return items
playlist_title = info['title']
for j in range(start_index, min(len(info['entries']),
start_index + var.config.getint('bot', 'max_track_playlist'))):
# Unknow String if No title into the json
title = info['entries'][j]['title'] if 'title' in info['entries'][j] else "Unknown Title"
# Add youtube url if the url in the json isn't a full url
item_url = info['entries'][j]['url'] if info['entries'][j]['url'][0:4] == 'http' \
else "https://www.youtube.com/watch?v=" + info['entries'][j]['url']
music = PlaylistItemWrapper(
URLFromPlaylistItem(
bot,
item_url,
title,
url,
playlist_title
), user)
items.append(music)
except:
pass
return items
class URLFromPlaylistItem(URLItem):
def __init__(self, bot, url, title, playlist_url, playlist_title, from_dict=None):
if from_dict is None:
super().__init__(bot, url)
self.title = title
self.playlist_url = playlist_url
self.playlist_title = playlist_title
else:
super().__init__(bot, "", from_dict)
self.playlist_title = from_dict['playlist_title']
self.playlist_url = from_dict['playlist_url']
self.type = "url_from_playlist"
def to_dict(self):
dict = super().to_dict()
dict['playlist_url'] = self.playlist_url
dict['playlist_title'] = self.playlist_title
return dict
def format_debug_string(self):
return "[url] {title} ({url}) from playlist {playlist}".format(
title=self.title,
url=self.url,
playlist=self.playlist_title
)
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)
def format_current_playing(self, user):
display = constants.strings("now_playing", item=self.format_song_string(user))
if self.thumbnail:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
self.thumbnail + '"/>'
display += "<br />" + thumbnail_html
return display
def display_type(self):
return constants.strings("url_from_playlist")

View File

@ -5,7 +5,6 @@ import threading
import time import time
import sys import sys
import math import math
import re
import signal import signal
import configparser import configparser
import audioop import audioop
@ -28,11 +27,9 @@ import constants
from database import Database from database import Database
import media.url import media.url
import media.file import media.file
import media.playlist
import media.radio import media.radio
import media.system import media.system
from librb import radiobrowser from media.playlist import PlayList
from playlist import PlayList
class MumbleBot: class MumbleBot:
@ -71,6 +68,7 @@ class MumbleBot:
self.thread = None self.thread = None
self.thread_stderr = None self.thread_stderr = None
self.is_pause = False self.is_pause = False
self.pause_at_id = ""
self.playhead = -1 self.playhead = -1
self.song_start_at = -1 self.song_start_at = -1
#self.download_threads = [] #self.download_threads = []
@ -221,7 +219,8 @@ class MumbleBot:
self.log.info('bot: received command ' + command + ' - ' + parameter + ' by ' + user) self.log.info('bot: received command ' + command + ' - ' + parameter + ' by ' + user)
# Anti stupid guy function # Anti stupid guy function
if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_other_channel_message') and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']: if not self.is_admin(user) 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( self.mumble.users[text.actor].send_text_message(
constants.strings('not_in_my_channel')) constants.strings('not_in_my_channel'))
return return
@ -294,56 +293,18 @@ class MumbleBot:
# Launch and Download # Launch and Download
# ======================= # =======================
def launch_music(self, index=-1): def launch_music(self):
uri = ""
music = None
if var.playlist.is_empty(): if var.playlist.is_empty():
return return
assert self.wait_for_downloading == False
if index == -1: music_wrapper = var.playlist.current_item()
music = var.playlist.current_item() uri = music_wrapper.item.uri()
else:
music = var.playlist.jump(index)
self.wait_for_downloading = False self.log.info("bot: play music " + music_wrapper.item.format_debug_string())
self.log.info("bot: play music " + util.format_debug_song_string(music))
if music["type"] == "url":
# Delete older music is the tmp folder is too big
media.system.clear_tmp_folder(var.tmp_folder, var.config.getint('bot', 'tmp_folder_max_size'))
if music['ready'] == 'downloading':
self.wait_for_downloading = True
self.log.info("bot: current music isn't ready, other thread is downloading.")
return
# Check if the music is ready to be played
if music["ready"] != "yes" or not os.path.exists(music['path']):
self.wait_for_downloading = True
self.log.info("bot: current music isn't ready, start downloading.")
self.async_download(index)
return
if music['ready'] == 'failed':
self.log.info("bot: removing music from the playlist: %s" % util.format_debug_song_string(music))
var.playlist.remove(index)
return
uri = music['path']
elif music["type"] == "file":
if not self.check_item_path_or_remove():
return
uri = var.music_folder + var.playlist.current_item()["path"]
elif music["type"] == "radio":
uri = music["url"]
if 'name' not in music:
self.log.info("bot: fetching radio server description")
title = media.radio.get_radio_server_description(uri)
music["name"] = title
if var.config.getboolean('bot', 'announce_current_music'): if var.config.getboolean('bot', 'announce_current_music'):
self.send_msg(util.format_current_playing()) self.send_msg(music_wrapper.format_current_playing())
if var.config.getboolean('debug', 'ffmpeg'): if var.config.getboolean('debug', 'ffmpeg'):
ffmpeg_debug = "debug" ffmpeg_debug = "debug"
@ -365,172 +326,22 @@ class MumbleBot:
self.playhead = 0 self.playhead = 0
self.last_volume_cycle_time = time.time() self.last_volume_cycle_time = time.time()
def validate_music(self, music):
url = music['url']
url_hash = hashlib.md5(url.encode()).hexdigest()
path = var.tmp_folder + url_hash + ".%(ext)s"
mp3 = path.replace(".%(ext)s", ".mp3")
music['path'] = mp3
# Download only if music is not existed
if os.path.isfile(mp3):
self.log.info("bot: file existed for url %s " % music['url'])
music['ready'] = 'yes'
return music
music = media.url.get_url_info(music)
self.log.info("bot: verifying the duration of url %s " % music['url'])
if music:
if music['duration'] > var.config.getint('bot', 'max_track_duration'):
# Check the length, useful in case of playlist, it wasn't checked before)
self.log.info(
"the music " + music["url"] + " has a duration of " + str(music['duration']) + "s -- too long")
self.send_msg(constants.strings('too_long'))
return False
else:
music['ready'] = "no"
return music
else:
self.log.error("bot: error while fetching info from the URL")
self.send_msg(constants.strings('unable_download'))
return False
def download_music(self, index=-1):
if index == -1:
index = var.playlist.current_index
music = var.playlist[index]
if music['type'] != 'url':
# then no need to download
return music
self.download_in_progress = True
url = music['url']
url_hash = hashlib.md5(url.encode()).hexdigest()
path = var.tmp_folder + url_hash + ".%(ext)s"
mp3 = path.replace(".%(ext)s", ".mp3")
music['path'] = mp3
# Download only if music is not existed
if not os.path.isfile(mp3):
# download the music
music['ready'] = "downloading"
var.playlist.update(music, music['id'])
self.log.info("bot: downloading url (%s) %s " % (music['title'], url))
ydl_opts = ""
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': path,
'noplaylist': True,
'writethumbnail': True,
'updatetime': False,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192'},
{'key': 'FFmpegMetadata'}]
}
self.send_msg(constants.strings('download_in_progress', item=music['title']))
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
attempts = var.config.getint('bot', 'download_attempts', fallback=2)
download_succeed = False
for i in range(attempts):
self.log.info("bot: download attempts %d / %d" % (i+1, attempts))
try:
ydl.extract_info(url)
download_succeed = True
break
except:
error_traceback = traceback.format_exc().split("During")[0]
error = error_traceback.rstrip().split("\n")[-1]
self.log.error("bot: download failed with error:\n %s" % error)
if download_succeed:
music['ready'] = "yes"
self.log.info(
"bot: finished downloading url (%s) %s, saved to %s." % (music['title'], url, music['path']))
else:
for f in [mp3, path.replace(".%(ext)s", ".jpg"), path.replace(".%(ext)s", ".m4a")]:
if os.path.exists(f):
os.remove(f)
self.send_msg(constants.strings('unable_download'))
music['ready'] = "failed"
else:
self.log.info("bot: music file existed, skip downloading " + mp3)
music['ready'] = "yes"
music = util.attach_music_tag_info(music)
var.playlist.update(music, music['id'])
self.download_in_progress = False
return music
def async_download_next(self): def async_download_next(self):
# Function start if the next music isn't ready # Function start if the next music isn't ready
# Do nothing in case the next music is already downloaded # Do nothing in case the next music is already downloaded
self.log.debug("bot: Async download next asked ") self.log.debug("bot: Async download next asked ")
if var.playlist.next_item() and var.playlist.next_item()['type'] == 'url': while var.playlist.next_item() and var.playlist.next_item().item.type == 'url':
# usually, all validation will be done when adding to the list. # usually, all validation will be done when adding to the list.
# however, for performance consideration, youtube playlist won't be validate when added. # however, for performance consideration, youtube playlist won't be validate when added.
# the validation has to be done here. # the validation has to be done here.
while var.playlist.next_item() and var.playlist.next_item()['ready'] == "validation": next = var.playlist.next_item().item
music = self.validate_music(var.playlist.next_item()) if next.validate():
if music: if not next.is_ready():
var.playlist.update(music, music['id']) next.async_prepare()
break break
else: else:
var.playlist.remove(var.playlist.next_index()) var.playlist.remove_by_id(next.id)
if var.playlist.next_item() and var.playlist.next_item()['ready'] == "no":
self.async_download(var.playlist.next_index())
def async_download(self, index):
th = threading.Thread(
target=self.download_music, name="DownloadThread-" + var.playlist[index]['id'][:5], args=(index,))
self.log.info(
"bot: start downloading item in thread: " + util.format_debug_song_string(var.playlist[index]))
th.daemon = True
th.start()
#self.download_threads.append(th)
return th
def check_item_path_or_remove(self, index = -1):
if index == -1:
index = var.playlist.current_index
music = var.playlist[index]
if music['type'] == 'radio':
return True
if not 'path' in music:
return False
else:
if music["type"] == "url":
uri = music['path']
if not os.path.exists(uri):
music['ready'] = 'validation'
return False
elif music["type"] == "file":
uri = var.music_folder + music["path"]
if not os.path.exists(uri):
self.log.info("bot: music file missed. removing music from the playlist: %s" % util.format_debug_song_string(music))
self.send_msg(constants.strings('file_missed', file=music["path"]))
var.playlist.remove(index)
return False
return True
# ======================= # =======================
# Loop # Loop
@ -577,17 +388,30 @@ class MumbleBot:
# ffmpeg thread has gone. indicate that last song has finished. move to the next song. # ffmpeg thread has gone. indicate that last song has finished. move to the next song.
if not self.wait_for_downloading: if not self.wait_for_downloading:
if var.playlist.next(): if var.playlist.next():
# if downloading in the other thread current = var.playlist.current_item().item
if current.validate():
print("validate")
if current.is_ready():
print("ready")
self.launch_music() self.launch_music()
self.async_download_next() self.async_download_next()
else:
self.log.info("bot: current music isn't ready, start downloading.")
self.wait_for_downloading = True
current.async_prepare()
else:
var.playlist.remove_by_id(current.id)
else: else:
self._loop_status = 'Empty queue' self._loop_status = 'Empty queue'
else: else:
if var.playlist.current_item(): current = var.playlist.current_item().item
if var.playlist.current_item()["ready"] != "downloading": if current:
if current.is_ready():
self.wait_for_downloading = False self.wait_for_downloading = False
self.launch_music() self.launch_music()
self.async_download_next() self.async_download_next()
elif current.is_failed():
var.playlist.remove_by_id(current.id)
else: else:
self._loop_status = 'Wait for downloading' self._loop_status = 'Wait for downloading'
else: else:
@ -666,6 +490,7 @@ class MumbleBot:
def pause(self): def pause(self):
# Kill the ffmpeg thread # Kill the ffmpeg thread
if self.thread: if self.thread:
self.pause_at_id = var.playlist.current_item().item.id
self.thread.kill() self.thread.kill()
self.thread = None self.thread = None
self.is_pause = True self.is_pause = True
@ -678,10 +503,10 @@ class MumbleBot:
if var.playlist.current_index == -1: if var.playlist.current_index == -1:
var.playlist.next() var.playlist.next()
music = var.playlist.current_item() music_wrapper = var.playlist.current_item()
if music['type'] == 'radio' or self.playhead == 0 or not self.check_item_path_or_remove(): if not music_wrapper or not music_wrapper.item.id == self.pause_at_id or not music_wrapper.item.is_ready():
self.launch_music() self.playhead = 0
return return
if var.config.getboolean('debug', 'ffmpeg'): if var.config.getboolean('debug', 'ffmpeg'):
@ -691,12 +516,7 @@ class MumbleBot:
self.log.info("bot: resume music at %.2f seconds" % self.playhead) self.log.info("bot: resume music at %.2f seconds" % self.playhead)
uri = "" uri = music_wrapper.item.uri()
if music["type"] == "url":
uri = music['path']
elif music["type"] == "file":
uri = var.music_folder + var.playlist.current_item()["path"]
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', '-')
@ -713,6 +533,7 @@ class MumbleBot:
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 = ""
# TODO: this is a temporary workaround for issue #44 of pymumble. # TODO: this is a temporary workaround for issue #44 of pymumble.
@ -808,8 +629,8 @@ if __name__ == '__main__':
var.bot_logger = bot_logger var.bot_logger = bot_logger
var.playlist = PlayList() # playlist should be initialized after the database var.playlist = PlayList() # playlist should be initialized after the database
var.botamusique = MumbleBot(args) var.bot = MumbleBot(args)
command.register_all_commands(var.botamusique) command.register_all_commands(var.bot)
# load playlist # load playlist
if var.config.getboolean('bot', 'save_playlist', fallback=True): if var.config.getboolean('bot', 'save_playlist', fallback=True):
@ -827,4 +648,4 @@ if __name__ == '__main__':
var.playlist.set_mode(playback_mode) var.playlist.set_mode(playback_mode)
# Start the main loop. # Start the main loop.
var.botamusique.loop() var.bot.loop()

View File

@ -1,244 +0,0 @@
import json
import random
import hashlib
import util
import variables as var
"""
FORMAT OF A MUSIC INTO THE PLAYLIST
type : url
id
url
title
path
duration
artist
thumbnail
user
ready (validation, no, downloading, yes, failed)
from_playlist (yes,no)
playlist_title
playlist_url
type : radio
id
url
name
current_title
user
type : file
id
path
title
artist
duration
thumbnail
user
"""
class PlayList(list):
current_index = -1
version = 0 # increase by one after each change
mode = "one-shot" # "repeat", "random"
def __init__(self, *args):
super().__init__(*args)
def is_empty(self):
return True if len(self) == 0 else False
def set_mode(self, mode):
# modes are "one-shot", "repeat", "random"
self.mode = mode
if mode == "random":
self.randomize()
elif mode == "one-shot" and self.current_index > 0:
# remove items before current item
self.version += 1
for i in range(self.current_index):
super().__delitem__(0)
self.current_index = 0
def append(self, item):
self.version += 1
item = util.attach_music_tag_info(item)
super().append(item)
return item
def insert(self, index, item):
self.version += 1
if index == -1:
index = self.current_index
item = util.attach_music_tag_info(item)
super().insert(index, item)
if index <= self.current_index:
self.current_index += 1
return item
def length(self):
return len(self)
def extend(self, items):
self.version += 1
items = list(map(
lambda item: util.attach_music_tag_info(item),
items))
super().extend(items)
return items
def next(self):
if len(self) == 0:
return False
self.version += 1
#logging.debug("playlist: Next into the queue")
if self.current_index < len(self) - 1:
if self.mode == "one-shot" and self.current_index != -1:
super().__delitem__(self.current_index)
else:
self.current_index += 1
return self[self.current_index]
else:
self.current_index = 0
if self.mode == "one-shot":
self.clear()
return False
elif self.mode == "repeat":
return self[0]
elif self.mode == "random":
self.randomize()
return self[0]
else:
raise TypeError("Unknown playlist mode '%s'." % self.mode)
def find(self, id):
for index, item in enumerate(self):
if item['id'] == id:
return index
return None
def update(self, item, id):
self.version += 1
index = self.find(id)
if index:
self[index] = item
return True
return False
def __delitem__(self, key):
return self.remove(key)
def remove(self, index=-1):
self.version += 1
if index > len(self) - 1:
return False
if index == -1:
index = self.current_index
removed = self[index]
super().__delitem__(index)
if self.current_index > index:
self.current_index -= 1
return removed
def current_item(self):
if len(self) == 0:
return False
return self[self.current_index]
def current_item_downloading(self):
if len(self) == 0:
return False
if self[self.current_index]['type'] == 'url' and self[self.current_index]['ready'] == 'downloading':
return True
return False
def next_index(self):
if len(self) == 0 or (len(self) == 1 and self.mode == 'one_shot'):
return False
if self.current_index < len(self) - 1:
return self.current_index + 1
else:
return 0
def next_item(self):
if len(self) == 0 or (len(self) == 1 and self.mode == 'one_shot'):
return False
return self[self.next_index()]
def jump(self, index):
if self.mode == "one-shot":
for i in range(index):
super().__delitem__(0)
self.current_index = 0
else:
self.current_index = index
self.version += 1
return self[self.current_index]
def randomize(self):
# current_index will lose track after shuffling, thus we take current music out before shuffling
#current = self.current_item()
#del self[self.current_index]
random.shuffle(self)
#self.insert(0, current)
self.current_index = -1
self.version += 1
def clear(self):
self.version += 1
self.current_index = -1
super().clear()
def save(self):
var.db.remove_section("playlist_item")
var.db.set("playlist", "current_index", self.current_index)
for index, music in enumerate(self):
if music['type'] == 'url' and music['ready'] == 'downloading':
music['ready'] = 'no'
var.db.set("playlist_item", str(index), json.dumps(music))
def load(self):
current_index = var.db.getint("playlist", "current_index", fallback=-1)
if current_index == -1:
return
items = list(var.db.items("playlist_item"))
items.sort(key=lambda v: int(v[0]))
self.extend(list(map(lambda v: json.loads(v[1]), items)))
self.current_index = current_index
def _debug_print(self):
print("===== Playlist(%d) ====" % self.current_index)
for index, item in enumerate(self):
if index == self.current_index:
print("-> %d %s" % (index, item['title']))
else:
print("%d %s" % (index, item['title']))
print("===== End ====")

View File

@ -1,24 +0,0 @@
type : url
url
title
path
duration
thundnail
user
ready (validation, no, downloading, yes)
from_playlist (yes,no)
playlist_title
playlist_url
type : radio
url
name
current_title
user
type : file
path
title
duration
user

View File

@ -11,32 +11,34 @@
<th scope="row">{{ index + 1 }}</th> <th scope="row">{{ index + 1 }}</th>
<td> <td>
<div class="playlist-title"> <div class="playlist-title">
{% if 'thumbnail' in m %} {% if m.type != 'radio' and m.thumbnail %}
<img width="80" src="data:image/PNG;base64,{{ m['thumbnail'] }}"/> <img width="80" src="data:image/PNG;base64,{{ m.thumbnail }}"/>
{% else %} {% else %}
<img width="80" src="static/image/unknown-album.png"/> <img width="80" src="static/image/unknown-album.png"/>
{% endif %} {% endif %}
</div> </div>
<div class="playlist-artwork"> <div class="playlist-artwork">
{% if 'title' in m and m['title'].strip() %} {% if m.title.strip() %}
<b>{{ m['title']|truncate(45) }}</b> <b>{{ m.title|truncate(45) }}</b>
{% elif 'url' in m %} {% elif m.url %}
<b>{{ m['url']|truncate(45) }}</b> <b>{{ m.url|truncate(45) }}</b>
{% endif %} {% endif %}
<span class="badge badge-secondary">{{ m['type'].capitalize() }}</span> <span class="badge badge-secondary">{{ m.display_type() }}</span>
<br> <br>
{% if 'artist' in m %} {% if m.type == 'file' %}
{{ m['artist'] }} {{ m.artist }}
{% elif m.type == 'url_from_playlist' %}
<a href="{{ m.playlist_url }}"><i>{{ m.playlist_title|truncate(50) }}</i></a>
{% else %} {% else %}
Unknown Artist Unknown Artist
{% endif %} {% endif %}
</div> </div>
</td> </td>
<td> <td>
{% if 'url' in m %} {% if m.type == 'url' or m.type == 'radio' or m.type == 'url_from_playlist' %}
<small><a href="{{ m['url'] }}"><i>{{ m['url']|truncate(50) }}</i></a></small> <small><a href="{{ m.url }}"><i>{{ m.url|truncate(50) }}</i></a></small>
{% elif 'path' in m %} {% elif m.type == 'file' %}
<small>{{ m['path']|truncate(50) }}</small> <small>{{ m.path|truncate(50) }}</small>
{% endif %} {% endif %}
</td> </td>
<td> <td>

175
util.py
View File

@ -62,181 +62,6 @@ def get_recursive_file_list_sorted(path):
filelist.sort() filelist.sort()
return filelist return filelist
def get_music_path(music):
uri = ''
if music["type"] == "url":
uri = music['path']
elif music["type"] == "file":
uri = var.music_folder + music["path"]
elif music["type"] == "radio":
uri = music['url']
return uri
def attach_item_id(item):
if item['type'] == 'url':
item['id'] = hashlib.md5(item['url'].encode()).hexdigest()
elif item['type'] == 'file':
item['id'] = hashlib.md5(item['path'].encode()).hexdigest()
elif item['type'] == 'radio':
item['id'] = hashlib.md5(item['url'].encode()).hexdigest()
return item
def attach_music_tag_info(music):
music = attach_item_id(music)
if "path" in music:
uri = get_music_path(music)
if os.path.isfile(uri):
match = re.search("(.+)\.(.+)", uri)
if match is None:
return music
file_no_ext = match[1]
ext = match[2]
try:
im = None
path_thumbnail = file_no_ext + ".jpg"
if os.path.isfile(path_thumbnail):
im = Image.open(path_thumbnail)
if ext == "mp3":
# title: TIT2
# artist: TPE1, TPE2
# album: TALB
# cover artwork: APIC:
tags = mutagen.File(uri)
if 'TIT2' in tags:
music['title'] = tags['TIT2'].text[0]
if 'TPE1' in tags: # artist
music['artist'] = tags['TPE1'].text[0]
if im is None:
if "APIC:" in tags:
im = Image.open(BytesIO(tags["APIC:"].data))
elif ext == "m4a" or ext == "m4b" or ext == "mp4" or ext == "m4p":
# title: ©nam (\xa9nam)
# artist: ©ART
# album: ©alb
# cover artwork: covr
tags = mutagen.File(uri)
if '©nam' in tags:
music['title'] = tags['©nam'][0]
if '©ART' in tags: # artist
music['artist'] = tags['©ART'][0]
if im is None:
if "covr" in tags:
im = Image.open(BytesIO(tags["covr"][0]))
if im:
im.thumbnail((100, 100), Image.ANTIALIAS)
buffer = BytesIO()
im = im.convert('RGB')
im.save(buffer, format="JPEG")
music['thumbnail'] = base64.b64encode(buffer.getvalue()).decode('utf-8')
except:
pass
else:
uri = music['url']
# if nothing found
if 'title' not in music:
match = re.search("([^\.]+)\.?.*", os.path.basename(uri))
music['title'] = match[1]
return music
def format_song_string(music):
display = ''
source = music["type"]
title = music["title"] if "title" in music else "Unknown title"
artist = music["artist"] if "artist" in music else "Unknown artist"
if source == "radio":
display = constants.strings("now_playing_radio",
url=music["url"],
title=media.radio.get_radio_title(music["url"]),
name=music["name"],
user=music["user"]
)
elif source == "url" and 'from_playlist' in music:
display = constants.strings("now_playing_from_playlist",
title=title,
url=music['url'],
playlist_url=music["playlist_url"],
playlist=music["playlist_title"],
user=music["user"]
)
elif source == "url":
display = constants.strings("now_playing_url",
title=title,
url=music["url"],
user=music["user"]
)
elif source == "file":
display = constants.strings("now_playing_file",
title=title,
artist=artist,
user=music["user"]
)
return display
def format_debug_song_string(music):
display = ''
source = music["type"]
title = music["title"] if "title" in music else "??"
artist = music["artist"] if "artist" in music else "??"
if source == "radio":
display = "[radio] {name} ({url}) by {user}".format(
name=music["name"],
url=music["url"],
user=music["user"]
)
elif source == "url" and 'from_playlist' in music:
display = "[url] {title} ({url}) from playlist {playlist} by {user}".format(
title=title,
url=music["url"],
playlist=music["playlist_title"],
user=music["user"]
)
elif source == "url":
display = "[url] {title} ({url}) by {user}".format(
title=title,
url=music["url"],
user=music["user"]
)
elif source == "file":
display = "[file] {artist} - {title} ({path}) by {user}".format(
title=title,
artist=artist,
path=music["path"],
user=music["user"]
)
return display
def format_current_playing():
music = var.playlist.current_item()
display = format_song_string(music)
if 'thumbnail' in music:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
music['thumbnail'] + '"/>'
return display + "<br />" + thumbnail_html
return display
# - 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)

View File

@ -1,4 +1,4 @@
botamusique = None bot = None
playlist = None playlist = None
user = "" user = ""