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")
# 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
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)
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:
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}
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)
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:
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)
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):
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)
parameter = parameter.split()[0]
url = util.get_url_from_input(parameter)
if url:
- music = {'type': 'radio',
- 'url': url,
- 'user': user}
-
- log.info("bot: fetching radio server description")
- music["name"] = media.radio.get_radio_server_description(url)
+ music_wrapper = PlaylistItemWrapper(RadioItem(bot, url), user)
- 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'))
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.')
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)
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)
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():
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))
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
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())
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!
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
web = Flask(__name__)
log = logging.getLogger("bot")
+user = 'Remote Control'
def init_proxy():
global web
os=os,
playlist=var.playlist,
user=var.user,
- paused=var.botamusique.is_pause
+ paused=var.bot.is_pause
)
@web.route("/playlist", methods=['GET'])
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
)
)
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,
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:
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'])
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']
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.")
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()
+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")
--- /dev/null
+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}
+
+
-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
+
+
+class PlaylistItemWrapper:
+ def __init__(self, item, user):
+ self.item = item
+ self.user = user
+
+ def to_dict(self):
+ dict = self.item.to_dict()
+ dict['user'] = self.user
+ return dict
+
+ def format_current_playing(self):
+ return self.item.format_current_playing(self.user)
+
+ 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)
-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
-
- 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']
-
- 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
-
- return items
+ self.log.debug("playlist: validating finished.")
+ self.validating_thread_lock.release()
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)
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'])
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")
+
+
+
+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")
+
+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"
-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:
+ 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
- except KeyError:
- return music
+ 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:
- return music
- return False
+ 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")
--- /dev/null
+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")
import time
import sys
import math
-import re
import signal
import configparser
import audioop
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:
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 = []
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
# 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)
-
- 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
+ music_wrapper = var.playlist.current_item()
+ uri = music_wrapper.item.uri()
- # 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"
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
# 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:
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
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'):
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', '-')
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.
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):
var.playlist.set_mode(playback_mode)
# Start the main loop.
- var.botamusique.loop()
+ var.bot.loop()
+++ /dev/null
-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 ====")
\ No newline at end of file
+++ /dev/null
-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
-
<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>
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)
-botamusique = None
+bot = None
playlist = None
user = ""