REFACTOR: ITEM REVOLUTION #91
This commit is contained in:
146
command.py
146
command.py
@ -5,15 +5,16 @@ import pymumble.pymumble_py3 as pymumble
|
||||
import re
|
||||
|
||||
import constants
|
||||
import media.file
|
||||
import media.playlist
|
||||
import media.radio
|
||||
import media.system
|
||||
import media.url
|
||||
import util
|
||||
import variables as var
|
||||
from librb import radiobrowser
|
||||
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")
|
||||
|
||||
@ -57,8 +58,8 @@ def register_all_commands(bot):
|
||||
|
||||
# Just for debug use
|
||||
bot.register_command('rtrms', cmd_real_time_rms)
|
||||
# bot.register_command('loop', cmd_loop_state)
|
||||
# bot.register_command('item', cmd_item)
|
||||
bot.register_command('loop', cmd_loop_state)
|
||||
bot.register_command('item', cmd_item)
|
||||
|
||||
def send_multi_lines(bot, lines, text):
|
||||
global log
|
||||
@ -138,16 +139,16 @@ def cmd_play(bot, user, text, command, parameter):
|
||||
|
||||
if var.playlist.length() > 0:
|
||||
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.launch_music(int(parameter) - 1)
|
||||
else:
|
||||
bot.send_msg(constants.strings('invalid_index', index=parameter), text)
|
||||
|
||||
elif bot.is_pause:
|
||||
bot.resume()
|
||||
else:
|
||||
bot.send_msg(util.format_current_playing(), text)
|
||||
bot.send_msg(var.playlist.current_item().format_current_playing(), text)
|
||||
else:
|
||||
bot.is_pause = False
|
||||
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)
|
||||
if int(parameter) < len(files):
|
||||
filename = files[int(parameter)].replace(var.music_folder, '')
|
||||
music = {'type': 'file',
|
||||
'path': filename,
|
||||
'user': user}
|
||||
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)
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(bot, filename), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
music = music_wrapper.item
|
||||
log.info("cmd: add to playlist: " + music.format_debug_string())
|
||||
bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
|
||||
|
||||
# if parameter is {path}
|
||||
else:
|
||||
@ -184,12 +184,11 @@ def cmd_play_file(bot, user, text, command, parameter):
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
music = {'type': 'file',
|
||||
'path': parameter,
|
||||
'user': user}
|
||||
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)
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(bot, parameter), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
music = music_wrapper.item
|
||||
log.info("cmd: add to playlist: " + music.format_debug_string())
|
||||
bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
|
||||
return
|
||||
|
||||
# if parameter is {folder}
|
||||
@ -211,12 +210,11 @@ def cmd_play_file(bot, user, text, command, parameter):
|
||||
|
||||
for file in files:
|
||||
count += 1
|
||||
music = {'type': 'file',
|
||||
'path': file,
|
||||
'user': user}
|
||||
music = var.playlist.append(music)
|
||||
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
|
||||
msgs.append("{} ({})".format(music['title'], music['path']))
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
music = music_wrapper.item
|
||||
log.info("cmd: add to playlist: " + music.format_debug_string())
|
||||
msgs.append("{} ({})".format(music.title, music.path))
|
||||
|
||||
if count != 0:
|
||||
send_multi_lines(bot, msgs, text)
|
||||
@ -230,12 +228,12 @@ def cmd_play_file(bot, user, text, command, parameter):
|
||||
if len(matches) == 0:
|
||||
bot.send_msg(constants.strings('no_file'), text)
|
||||
elif len(matches) == 1:
|
||||
music = {'type': 'file',
|
||||
'path': matches[0][1],
|
||||
'user': user}
|
||||
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)
|
||||
file = matches[0][1]
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
music = music_wrapper.item
|
||||
log.info("cmd: add to playlist: " + music.format_debug_string())
|
||||
bot.send_msg(constants.strings('file_added', item=music.format_song_string(user)), text)
|
||||
else:
|
||||
msgs = [ constants.strings('multiple_matches')]
|
||||
for match in matches:
|
||||
@ -256,13 +254,11 @@ def cmd_play_file_match(bot, user, text, command, parameter):
|
||||
match = re.search(parameter, file)
|
||||
if match:
|
||||
count += 1
|
||||
music = {'type': 'file',
|
||||
'path': file,
|
||||
'user': user}
|
||||
music = var.playlist.append(music)
|
||||
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
|
||||
|
||||
msgs.append("{} ({})".format(music['title'], music['path']))
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(bot, file), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
music = music_wrapper.item
|
||||
log.info("cmd: add to playlist: " + music.format_debug_string())
|
||||
msgs.append("{} ({})".format(music.title, music.path))
|
||||
|
||||
if count != 0:
|
||||
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):
|
||||
global log
|
||||
|
||||
music = {'type': 'url',
|
||||
# grab the real URL
|
||||
'url': util.get_url_from_input(parameter),
|
||||
'user': user,
|
||||
'ready': 'validation'}
|
||||
url = util.get_url_from_input(parameter)
|
||||
music_wrapper = PlaylistItemWrapper(URLItem(bot, url), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
|
||||
music = bot.validate_music(music)
|
||||
if music:
|
||||
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 I am the second item on the playlist. (I am the next one!)
|
||||
bot.async_download_next()
|
||||
else:
|
||||
bot.send_msg(constants.strings('unable_download'), text)
|
||||
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
|
||||
bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
|
||||
if var.playlist.length() == 2:
|
||||
# If I am the second item on the playlist. (I am the next one!)
|
||||
bot.async_download_next()
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
var.playlist.extend(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:
|
||||
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]
|
||||
url = util.get_url_from_input(parameter)
|
||||
if url:
|
||||
music = {'type': 'radio',
|
||||
'url': url,
|
||||
'user': user}
|
||||
music_wrapper = PlaylistItemWrapper(RadioItem(bot, url), user)
|
||||
|
||||
log.info("bot: fetching radio server description")
|
||||
music["name"] = media.radio.get_radio_server_description(url)
|
||||
|
||||
var.playlist.append(music)
|
||||
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
|
||||
bot.async_download_next()
|
||||
var.playlist.append(music_wrapper)
|
||||
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
|
||||
else:
|
||||
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)
|
||||
if url != "-1":
|
||||
log.info('cmd: Found url: ' + url)
|
||||
music = {'type': 'radio',
|
||||
'name': stationname,
|
||||
'artist': homepage,
|
||||
'url': url,
|
||||
'user': user}
|
||||
var.playlist.append(music)
|
||||
log.info("cmd: add to playlist: " + util.format_debug_song_string(music))
|
||||
music_wrapper = PlaylistItemWrapper(RadioItem(bot, url, stationname), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
|
||||
bot.async_download_next()
|
||||
else:
|
||||
log.info('cmd: No playable url found.')
|
||||
@ -637,7 +616,7 @@ def cmd_current_music(bot, user, text, command, parameter):
|
||||
|
||||
reply = ""
|
||||
if var.playlist.length() > 0:
|
||||
bot.send_msg(util.format_current_playing())
|
||||
bot.send_msg(var.playlist.current_item().format_current_playing())
|
||||
else:
|
||||
reply = constants.strings('not_playing')
|
||||
bot.send_msg(reply, text)
|
||||
@ -647,9 +626,7 @@ def cmd_skip(bot, user, text, command, parameter):
|
||||
global log
|
||||
|
||||
if var.playlist.length() > 0:
|
||||
bot.stop()
|
||||
bot.launch_music()
|
||||
bot.async_download_next()
|
||||
bot.interrupt_playing()
|
||||
else:
|
||||
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):
|
||||
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
|
||||
if parameter and parameter.isdigit() and int(parameter) > 0 \
|
||||
and int(parameter) <= var.playlist.length():
|
||||
@ -695,11 +668,10 @@ def cmd_remove(bot, user, text, command, parameter):
|
||||
else:
|
||||
removed = var.playlist.remove(index)
|
||||
|
||||
# the Title isn't here if the music wasn't downloaded
|
||||
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:
|
||||
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,
|
||||
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):
|
||||
global log
|
||||
@ -811,4 +783,4 @@ def cmd_loop_state(bot, user, text, command, parameter):
|
||||
|
||||
def cmd_item(bot, user, text, command, parameter):
|
||||
print(bot.wait_for_downloading)
|
||||
print(var.playlist.current_item())
|
||||
print(var.playlist.current_item().item.to_dict())
|
||||
|
@ -182,15 +182,19 @@ multiple_matches = Track not found! Possible candidates:
|
||||
queue_contents = Items on the playlist:
|
||||
queue_empty = Playlist is empty!
|
||||
invalid_index = Invalid index <i>{index}</i>. Use '!queue' to see your playlist.
|
||||
now_playing_radio = Now Playing Radio: <br /> <a href="{url}">{title}</a> <i>from</i> {name} <i>added by</i> {user}
|
||||
now_playing_file = Now Playing File:<br /> {artist} - {title} <i>added by</i> {user}
|
||||
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}
|
||||
now_playing_url = Now Playing URL: <br /> <a href="{url}">{title}</a> <i>added by</i> {user}
|
||||
now_playing = Playing <br />{item}
|
||||
radio = Radio
|
||||
file = File
|
||||
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!
|
||||
pm_not_allowed = Private message aren't allowed.
|
||||
too_long = This music is too long, skip!
|
||||
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.
|
||||
user_ban = You are banned, not allowed to do that!
|
||||
url_ban = This url is banned!
|
||||
|
127
interface.py
127
interface.py
@ -12,7 +12,11 @@ import random
|
||||
from werkzeug.utils import secure_filename
|
||||
import errno
|
||||
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 time
|
||||
import constants
|
||||
@ -58,6 +62,7 @@ class ReverseProxied(object):
|
||||
|
||||
web = Flask(__name__)
|
||||
log = logging.getLogger("bot")
|
||||
user = 'Remote Control'
|
||||
|
||||
def init_proxy():
|
||||
global web
|
||||
@ -109,7 +114,7 @@ def index():
|
||||
os=os,
|
||||
playlist=var.playlist,
|
||||
user=var.user,
|
||||
paused=var.botamusique.is_pause
|
||||
paused=var.bot.is_pause
|
||||
)
|
||||
|
||||
@web.route("/playlist", methods=['GET'])
|
||||
@ -124,10 +129,10 @@ def playlist():
|
||||
|
||||
items = []
|
||||
|
||||
for index, item in enumerate(var.playlist):
|
||||
for index, item_wrapper in enumerate(var.playlist):
|
||||
items.append(render_template('playlist.html',
|
||||
index=index,
|
||||
m=item,
|
||||
m=item_wrapper.item,
|
||||
playlist=var.playlist
|
||||
)
|
||||
)
|
||||
@ -138,7 +143,7 @@ def status():
|
||||
if (var.playlist.length() > 0):
|
||||
return jsonify({'ver': var.playlist.version,
|
||||
'empty': False,
|
||||
'play': not var.botamusique.is_pause,
|
||||
'play': not var.bot.is_pause,
|
||||
'mode': var.playlist.mode})
|
||||
else:
|
||||
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']:
|
||||
path = var.music_folder + request.form['add_file_bottom']
|
||||
if os.path.isfile(path):
|
||||
item = {'type': 'file',
|
||||
'path' : request.form['add_file_bottom'],
|
||||
'title' : '',
|
||||
'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))
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(var.bot, request.form['add_file_bottom']), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
|
||||
|
||||
elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
|
||||
path = var.music_folder + request.form['add_file_next']
|
||||
if os.path.isfile(path):
|
||||
item = {'type': 'file',
|
||||
'path' : request.form['add_file_next'],
|
||||
'title' : '',
|
||||
'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))
|
||||
music_wrapper = PlaylistItemWrapper(FileItem(var.bot, request.form['add_file_next']), user)
|
||||
var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
|
||||
log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
|
||||
|
||||
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:
|
||||
@ -202,42 +198,35 @@ def post():
|
||||
else:
|
||||
files = music_library.get_files(folder)
|
||||
|
||||
files = list(map(lambda file:
|
||||
{'type':'file',
|
||||
'path': os.path.join(folder, file),
|
||||
'user':'Remote Control'}, files))
|
||||
music_wrappers = list(map(
|
||||
lambda file: PlaylistItemWrapper(FileItem(var.bot, file), user),
|
||||
files))
|
||||
|
||||
files = var.playlist.extend(files)
|
||||
var.playlist.extend(files)
|
||||
|
||||
for file in files:
|
||||
log.info("web: add to playlist: %s" % util.format_debug_song_string(file))
|
||||
for music_wrapper in music_wrappers:
|
||||
log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
|
||||
|
||||
|
||||
elif 'add_url' in request.form:
|
||||
music = {'type':'url',
|
||||
'url': request.form['add_url'],
|
||||
'user': 'Remote Control',
|
||||
'ready': 'validation'}
|
||||
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 I am the second item on the playlist. (I am the next one!)
|
||||
var.botamusique.async_download_next()
|
||||
music_wrapper = PlaylistItemWrapper(URLItem(var.bot, request.form['add_url']), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
|
||||
log.info("web: add to playlist: " + music_wrapper.format_debug_string())
|
||||
if var.playlist.length() == 2:
|
||||
# If I am the second item on the playlist. (I am the next one!)
|
||||
var.bot.async_download_next()
|
||||
|
||||
elif 'add_radio' in request.form:
|
||||
url = request.form['add_radio']
|
||||
music = var.playlist.append({'type': 'radio',
|
||||
'url': url,
|
||||
'user': "Remote Control"})
|
||||
log.info("web: fetching radio server description")
|
||||
music["name"] = media.radio.get_radio_server_description(url)
|
||||
log.info("web: add to playlist: " + util.format_debug_song_string(music))
|
||||
music_wrapper = PlaylistItemWrapper(RadioItem(var.bot, url), user)
|
||||
var.playlist.append(music_wrapper)
|
||||
|
||||
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
|
||||
|
||||
elif 'delete_music' in request.form:
|
||||
music = var.playlist[int(request.form['delete_music'])]
|
||||
log.info("web: delete from playlist: " + util.format_debug_song_string(music))
|
||||
music_wrapper = var.playlist[int(request.form['delete_music'])]
|
||||
log.info("web: delete from playlist: " + music_wrapper.format_debug_string())
|
||||
|
||||
if var.playlist.length() >= int(request.form['delete_music']):
|
||||
index = int(request.form['delete_music'])
|
||||
@ -246,26 +235,26 @@ def post():
|
||||
var.playlist.remove(index)
|
||||
|
||||
if index < len(var.playlist):
|
||||
if not var.botamusique.is_pause:
|
||||
var.botamusique.interrupt_playing()
|
||||
if not var.bot.is_pause:
|
||||
var.bot.interrupt_playing()
|
||||
var.playlist.current_index -= 1
|
||||
# then the bot will move to next item
|
||||
|
||||
else: # if item deleted is the last item of the queue
|
||||
var.playlist.current_index -= 1
|
||||
if not var.botamusique.is_pause:
|
||||
var.botamusique.interrupt_playing()
|
||||
if not var.bot.is_pause:
|
||||
var.bot.interrupt_playing()
|
||||
else:
|
||||
var.playlist.remove(index)
|
||||
|
||||
|
||||
elif 'play_music' in request.form:
|
||||
music = var.playlist[int(request.form['play_music'])]
|
||||
log.info("web: jump to: " + util.format_debug_song_string(music))
|
||||
music_wrapper = var.playlist[int(request.form['play_music'])]
|
||||
log.info("web: jump to: " + music_wrapper.format_debug_string())
|
||||
|
||||
if len(var.playlist) >= int(request.form['play_music']):
|
||||
var.botamusique.interrupt_playing()
|
||||
var.botamusique.launch_music(int(request.form['play_music']))
|
||||
var.playlist.point_to(int(request.form['play_music']) - 1)
|
||||
var.bot.interrupt_playing()
|
||||
|
||||
elif 'delete_music_file' in request.form and ".." not in 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:
|
||||
action = request.form['action']
|
||||
if action == "randomize":
|
||||
var.botamusique.interrupt_playing()
|
||||
var.bot.interrupt_playing()
|
||||
var.playlist.set_mode("random")
|
||||
var.db.set('playlist', 'playback_mode', "random")
|
||||
log.info("web: playback mode changed to random.")
|
||||
@ -296,27 +285,27 @@ def post():
|
||||
var.db.set('playlist', 'playback_mode', "repeat")
|
||||
log.info("web: playback mode changed to repeat.")
|
||||
elif action == "stop":
|
||||
var.botamusique.stop()
|
||||
var.bot.stop()
|
||||
elif action == "pause":
|
||||
var.botamusique.pause()
|
||||
var.bot.pause()
|
||||
elif action == "resume":
|
||||
var.botamusique.resume()
|
||||
var.bot.resume()
|
||||
elif action == "clear":
|
||||
var.botamusique.clear()
|
||||
var.bot.clear()
|
||||
elif action == "volume_up":
|
||||
if var.botamusique.volume_set + 0.03 < 1.0:
|
||||
var.botamusique.volume_set = var.botamusique.volume_set + 0.03
|
||||
if var.bot.volume_set + 0.03 < 1.0:
|
||||
var.bot.volume_set = var.bot.volume_set + 0.03
|
||||
else:
|
||||
var.botamusique.volume_set = 1.0
|
||||
var.db.set('bot', 'volume', str(var.botamusique.volume_set))
|
||||
log.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
|
||||
var.bot.volume_set = 1.0
|
||||
var.db.set('bot', 'volume', str(var.bot.volume_set))
|
||||
log.info("web: volume up to %d" % (var.bot.volume_set * 100))
|
||||
elif action == "volume_down":
|
||||
if var.botamusique.volume_set - 0.03 > 0:
|
||||
var.botamusique.volume_set = var.botamusique.volume_set - 0.03
|
||||
if var.bot.volume_set - 0.03 > 0:
|
||||
var.bot.volume_set = var.bot.volume_set - 0.03
|
||||
else:
|
||||
var.botamusique.volume_set = 0
|
||||
var.db.set('bot', 'volume', str(var.botamusique.volume_set))
|
||||
log.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
|
||||
var.bot.volume_set = 0
|
||||
var.db.set('bot', 'volume', str(var.bot.volume_set))
|
||||
log.info("web: volume up to %d" % (var.bot.volume_set * 100))
|
||||
|
||||
return status()
|
||||
|
||||
|
158
media/file.py
158
media/file.py
@ -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
98
media/item.py
Normal 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}
|
||||
|
||||
|
@ -1,44 +1,271 @@
|
||||
import youtube_dl
|
||||
import json
|
||||
import random
|
||||
import hashlib
|
||||
import threading
|
||||
import logging
|
||||
|
||||
import util
|
||||
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=""):
|
||||
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
|
||||
class PlaylistItemWrapper:
|
||||
def __init__(self, item, user):
|
||||
self.item = item
|
||||
self.user = user
|
||||
|
||||
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
|
||||
url = info['entries'][j]['url'] if info['entries'][j]['url'][0:4] == 'http' else "https://www.youtube.com/watch?v=" + info['entries'][j]['url']
|
||||
def to_dict(self):
|
||||
dict = self.item.to_dict()
|
||||
dict['user'] = self.user
|
||||
return dict
|
||||
|
||||
music = {'type': 'url',
|
||||
'title': title,
|
||||
'url': url,
|
||||
'user': user,
|
||||
'from_playlist': True,
|
||||
'playlist_title': playlist_title,
|
||||
'playlist_url': url,
|
||||
'ready': 'validation'}
|
||||
items.append(music)
|
||||
except:
|
||||
pass
|
||||
def format_current_playing(self):
|
||||
return self.item.format_current_playing(self.user)
|
||||
|
||||
return items
|
||||
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
|
||||
|
||||
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()
|
||||
|
@ -1,16 +1,19 @@
|
||||
import re
|
||||
import logging
|
||||
import json
|
||||
import http.client
|
||||
import struct
|
||||
import requests
|
||||
import traceback
|
||||
import hashlib
|
||||
|
||||
from media.item import BaseItem
|
||||
import constants
|
||||
|
||||
log = logging.getLogger("bot")
|
||||
|
||||
def get_radio_server_description(url):
|
||||
global log
|
||||
|
||||
log.debug("radio: fetching radio server description")
|
||||
p = re.compile('(https?\:\/\/[^\/]*)', re.IGNORECASE)
|
||||
res = re.search(p, url)
|
||||
base_url = res.group(1)
|
||||
@ -50,6 +53,9 @@ def get_radio_server_description(url):
|
||||
|
||||
|
||||
def get_radio_title(url):
|
||||
global log
|
||||
|
||||
log.debug("radio: fetching radio server description")
|
||||
try:
|
||||
r = requests.get(url, headers={'Icy-MetaData': '1'}, stream=True, timeout=5)
|
||||
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:
|
||||
pass
|
||||
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")
|
||||
|
||||
|
||||
|
||||
|
227
media/url.py
227
media/url.py
@ -1,22 +1,215 @@
|
||||
import threading
|
||||
import logging
|
||||
import os
|
||||
import hashlib
|
||||
import traceback
|
||||
from PIL import Image
|
||||
import youtube_dl
|
||||
import glob
|
||||
|
||||
import constants
|
||||
import media
|
||||
import variables as var
|
||||
from media.file import FileItem
|
||||
import media.system
|
||||
|
||||
log = logging.getLogger("bot")
|
||||
|
||||
|
||||
def get_url_info(music):
|
||||
ydl_opts = {
|
||||
'noplaylist': True
|
||||
}
|
||||
music['duration'] = 0
|
||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||
for i in range(2):
|
||||
try:
|
||||
info = ydl.extract_info(music['url'], download=False)
|
||||
music['duration'] = info['duration'] / 60
|
||||
music['title'] = info['title']
|
||||
except youtube_dl.utils.DownloadError:
|
||||
pass
|
||||
except KeyError:
|
||||
return 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:
|
||||
return music
|
||||
return False
|
||||
# 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 = {
|
||||
'noplaylist': True
|
||||
}
|
||||
succeed = False
|
||||
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(self.url, download=False)
|
||||
self.duration = info['duration'] / 60
|
||||
self.title = info['title']
|
||||
succeed = True
|
||||
return True
|
||||
except youtube_dl.utils.DownloadError:
|
||||
pass
|
||||
|
||||
if not succeed:
|
||||
self.ready = 'failed'
|
||||
self.log.error("url: error while fetching info from the URL")
|
||||
self.send_client_message(constants.strings('unable_download'))
|
||||
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")
|
||||
|
99
media/url_from_playlist.py
Normal file
99
media/url_from_playlist.py
Normal 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")
|
269
mumbleBot.py
269
mumbleBot.py
@ -5,7 +5,6 @@ import threading
|
||||
import time
|
||||
import sys
|
||||
import math
|
||||
import re
|
||||
import signal
|
||||
import configparser
|
||||
import audioop
|
||||
@ -28,11 +27,9 @@ import constants
|
||||
from database import Database
|
||||
import media.url
|
||||
import media.file
|
||||
import media.playlist
|
||||
import media.radio
|
||||
import media.system
|
||||
from librb import radiobrowser
|
||||
from playlist import PlayList
|
||||
from media.playlist import PlayList
|
||||
|
||||
|
||||
class MumbleBot:
|
||||
@ -71,6 +68,7 @@ class MumbleBot:
|
||||
self.thread = None
|
||||
self.thread_stderr = None
|
||||
self.is_pause = False
|
||||
self.pause_at_id = ""
|
||||
self.playhead = -1
|
||||
self.song_start_at = -1
|
||||
#self.download_threads = []
|
||||
@ -221,7 +219,8 @@ class MumbleBot:
|
||||
self.log.info('bot: received command ' + command + ' - ' + parameter + ' by ' + user)
|
||||
|
||||
# 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(
|
||||
constants.strings('not_in_my_channel'))
|
||||
return
|
||||
@ -294,56 +293,18 @@ class MumbleBot:
|
||||
# Launch and Download
|
||||
# =======================
|
||||
|
||||
def launch_music(self, index=-1):
|
||||
uri = ""
|
||||
music = None
|
||||
def launch_music(self):
|
||||
if var.playlist.is_empty():
|
||||
return
|
||||
assert self.wait_for_downloading == False
|
||||
|
||||
if index == -1:
|
||||
music = var.playlist.current_item()
|
||||
else:
|
||||
music = var.playlist.jump(index)
|
||||
music_wrapper = var.playlist.current_item()
|
||||
uri = music_wrapper.item.uri()
|
||||
|
||||
self.wait_for_downloading = False
|
||||
|
||||
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
|
||||
self.log.info("bot: play music " + music_wrapper.item.format_debug_string())
|
||||
|
||||
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'):
|
||||
ffmpeg_debug = "debug"
|
||||
@ -365,172 +326,22 @@ class MumbleBot:
|
||||
self.playhead = 0
|
||||
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):
|
||||
# Function start if the next music isn't ready
|
||||
# Do nothing in case the next music is already downloaded
|
||||
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.
|
||||
# however, for performance consideration, youtube playlist won't be validate when added.
|
||||
# the validation has to be done here.
|
||||
while var.playlist.next_item() and var.playlist.next_item()['ready'] == "validation":
|
||||
music = self.validate_music(var.playlist.next_item())
|
||||
if music:
|
||||
var.playlist.update(music, music['id'])
|
||||
break
|
||||
else:
|
||||
var.playlist.remove(var.playlist.next_index())
|
||||
next = var.playlist.next_item().item
|
||||
if next.validate():
|
||||
if not next.is_ready():
|
||||
next.async_prepare()
|
||||
break
|
||||
else:
|
||||
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
|
||||
@ -577,17 +388,30 @@ class MumbleBot:
|
||||
# ffmpeg thread has gone. indicate that last song has finished. move to the next song.
|
||||
if not self.wait_for_downloading:
|
||||
if var.playlist.next():
|
||||
# if downloading in the other thread
|
||||
self.launch_music()
|
||||
self.async_download_next()
|
||||
current = var.playlist.current_item().item
|
||||
if current.validate():
|
||||
print("validate")
|
||||
if current.is_ready():
|
||||
print("ready")
|
||||
self.launch_music()
|
||||
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:
|
||||
self._loop_status = 'Empty queue'
|
||||
else:
|
||||
if var.playlist.current_item():
|
||||
if var.playlist.current_item()["ready"] != "downloading":
|
||||
current = var.playlist.current_item().item
|
||||
if current:
|
||||
if current.is_ready():
|
||||
self.wait_for_downloading = False
|
||||
self.launch_music()
|
||||
self.async_download_next()
|
||||
elif current.is_failed():
|
||||
var.playlist.remove_by_id(current.id)
|
||||
else:
|
||||
self._loop_status = 'Wait for downloading'
|
||||
else:
|
||||
@ -666,6 +490,7 @@ class MumbleBot:
|
||||
def pause(self):
|
||||
# Kill the ffmpeg thread
|
||||
if self.thread:
|
||||
self.pause_at_id = var.playlist.current_item().item.id
|
||||
self.thread.kill()
|
||||
self.thread = None
|
||||
self.is_pause = True
|
||||
@ -678,10 +503,10 @@ class MumbleBot:
|
||||
if var.playlist.current_index == -1:
|
||||
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():
|
||||
self.launch_music()
|
||||
if not music_wrapper or not music_wrapper.item.id == self.pause_at_id or not music_wrapper.item.is_ready():
|
||||
self.playhead = 0
|
||||
return
|
||||
|
||||
if var.config.getboolean('debug', 'ffmpeg'):
|
||||
@ -691,12 +516,7 @@ class MumbleBot:
|
||||
|
||||
self.log.info("bot: resume music at %.2f seconds" % self.playhead)
|
||||
|
||||
uri = ""
|
||||
if music["type"] == "url":
|
||||
uri = music['path']
|
||||
|
||||
elif music["type"] == "file":
|
||||
uri = var.music_folder + var.playlist.current_item()["path"]
|
||||
uri = music_wrapper.item.uri()
|
||||
|
||||
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-ss', "%f" % self.playhead, '-i',
|
||||
uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
|
||||
@ -713,6 +533,7 @@ class MumbleBot:
|
||||
self.thread_stderr = os.fdopen(pipe_rd)
|
||||
self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
|
||||
self.last_volume_cycle_time = time.time()
|
||||
self.pause_at_id = ""
|
||||
|
||||
|
||||
# TODO: this is a temporary workaround for issue #44 of pymumble.
|
||||
@ -808,8 +629,8 @@ if __name__ == '__main__':
|
||||
var.bot_logger = bot_logger
|
||||
|
||||
var.playlist = PlayList() # playlist should be initialized after the database
|
||||
var.botamusique = MumbleBot(args)
|
||||
command.register_all_commands(var.botamusique)
|
||||
var.bot = MumbleBot(args)
|
||||
command.register_all_commands(var.bot)
|
||||
|
||||
# load playlist
|
||||
if var.config.getboolean('bot', 'save_playlist', fallback=True):
|
||||
@ -827,4 +648,4 @@ if __name__ == '__main__':
|
||||
var.playlist.set_mode(playback_mode)
|
||||
|
||||
# Start the main loop.
|
||||
var.botamusique.loop()
|
||||
var.bot.loop()
|
||||
|
244
playlist.py
244
playlist.py
@ -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 ====")
|
24
playlist.txt
24
playlist.txt
@ -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
|
||||
|
@ -11,32 +11,34 @@
|
||||
<th scope="row">{{ index + 1 }}</th>
|
||||
<td>
|
||||
<div class="playlist-title">
|
||||
{% if 'thumbnail' in m %}
|
||||
<img width="80" src="data:image/PNG;base64,{{ m['thumbnail'] }}"/>
|
||||
{% if m.type != 'radio' and m.thumbnail %}
|
||||
<img width="80" src="data:image/PNG;base64,{{ m.thumbnail }}"/>
|
||||
{% else %}
|
||||
<img width="80" src="static/image/unknown-album.png"/>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="playlist-artwork">
|
||||
{% if 'title' in m and m['title'].strip() %}
|
||||
<b>{{ m['title']|truncate(45) }}</b>
|
||||
{% elif 'url' in m %}
|
||||
<b>{{ m['url']|truncate(45) }}</b>
|
||||
{% if m.title.strip() %}
|
||||
<b>{{ m.title|truncate(45) }}</b>
|
||||
{% elif m.url %}
|
||||
<b>{{ m.url|truncate(45) }}</b>
|
||||
{% endif %}
|
||||
<span class="badge badge-secondary">{{ m['type'].capitalize() }}</span>
|
||||
<span class="badge badge-secondary">{{ m.display_type() }}</span>
|
||||
<br>
|
||||
{% if 'artist' in m %}
|
||||
{{ m['artist'] }}
|
||||
{% if m.type == 'file' %}
|
||||
{{ m.artist }}
|
||||
{% elif m.type == 'url_from_playlist' %}
|
||||
<a href="{{ m.playlist_url }}"><i>{{ m.playlist_title|truncate(50) }}</i></a>
|
||||
{% else %}
|
||||
Unknown Artist
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if 'url' in m %}
|
||||
<small><a href="{{ m['url'] }}"><i>{{ m['url']|truncate(50) }}</i></a></small>
|
||||
{% elif 'path' in m %}
|
||||
<small>{{ m['path']|truncate(50) }}</small>
|
||||
{% 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>
|
||||
{% elif m.type == 'file' %}
|
||||
<small>{{ m.path|truncate(50) }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
175
util.py
175
util.py
@ -62,181 +62,6 @@ def get_recursive_file_list_sorted(path):
|
||||
filelist.sort()
|
||||
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)
|
||||
# - returns the absolute path of the created zip file
|
||||
# - zip file will be in the applications tmp folder (according to configuration)
|
||||
|
@ -1,4 +1,4 @@
|
||||
botamusique = None
|
||||
bot = None
|
||||
playlist = None
|
||||
|
||||
user = ""
|
||||
|
Reference in New Issue
Block a user