diff --git a/src/discordbot/discordbot.py b/src/discordbot/discordbot.py index 10633a4..f5ddfa9 100644 --- a/src/discordbot/discordbot.py +++ b/src/discordbot/discordbot.py @@ -40,7 +40,6 @@ logger_discord.setLevel(log_level) database = {} rec_path = "/recordings" -rechead = {} # Bot functions @bot.event @@ -53,12 +52,12 @@ async def on_ready(): @has_role(worshipper_role_name) async def hello(ctx): author_name = ctx.author.name - await ctx.channel.send(f'hi, {author_name} :blush:') + await ctx.channel.send(f'Hi, {author_name} :blush:') @hello.error async def hello_error(ctx, error): if isinstance(error, CheckFailure): - await ctx.channel.send('do I know you?') + await ctx.channel.send('Do I know you?') @bot.command(name='time', help='Show current time in UTC') async def time(ctx): @@ -85,20 +84,45 @@ async def epg(ctx): @bot.command(name='now', help='Displays whats playing right now') async def now(ctx): - head = await query_playhead() + playhead = await query_playhead() + stream_name = playhead['name'] + stream_prio = playhead['prio'] + return f'Now playing {stream_name} (prio={stream_prio})' await ctx.channel.send(head) @bot.command(name='rec', help='Start the recorder') @has_role(boss_role_name) async def rec(ctx): - await ctx.channel.send('soon...') + playhead = await query_playhead() + stream_name = playhead['name'] + stream_prio = playhead['prio'] + # Check if the recorder job already exists + if scheduler.get_job('recorder') is None: + await ctx.channel.send(f'Recording from {stream_name} (prio={stream_prio})') + scheduler.add_job(func=recorder, id='recorder', args=(playhead)) + else: + await ctx.channel.send(f'Recorder is busy!') @rec.error async def rec_error(ctx, error): if isinstance(error, CheckFailure): - await ctx.channel.send('access denied') + await ctx.channel.send('Access denied!') + +@bot.command(name='stop', help='Stop the recorder') +@has_role(boss_role_name) +async def stop(ctx): + # Check if the recorder job already exists + if scheduler.get_job('recorder') is not None: + await ctx.channel.send(f'Shutting down recorder') + scheduler.remove_job('recorder') + else: + await ctx.channel.send(f'Recorder is stopped.') + +@stop.error +async def stop_error(ctx, error): + if isinstance(error, CheckFailure): + await ctx.channel.send('Access denied!') -# Helper functions async def query_playhead(): head_url = f'https://{scheduler_hostname}/playhead' if requests.get(head_url).status_code == 200: @@ -107,13 +131,10 @@ async def query_playhead(): playhead = response.json() else: logger_discord.error('Cannot connect to the playhead!') - head_name = playhead['name'] - head_prio = playhead['prio'] - return f'now playing {head_name}' + return playhead async def query_database(): global database - global rechead db_url = f'https://{scheduler_hostname}/database' try: if requests.get(db_url).status_code == 200: @@ -133,29 +154,18 @@ async def query_database(): logger_discord.error('Database is empty!') return - # Search for live streams + # Search for live streams and announce them for key, value in database.items(): stream_name = value['name'] stream_start_at = value['start_at'] stream_meta = value['meta'] if stream_start_at == 'now': - # Check if the job already exists + # Check if the announement job already exists if scheduler.get_job('announce_live_channel') is None: # Job doesn't exist, so add it logger_discord.info(f'{stream_name} live stream detected!') scheduler.add_job(func=announce_live_channel, trigger='interval', minutes=int(live_channel_update), id='announce_live_channel', args=(stream_name, stream_meta)) - - # Manually execute the job immediately scheduler.get_job('announce_live_channel').modify(next_run_time=datetime.now()) - - # Set global rechead - rec_url = f'https://{scheduler_hostname}/rechead' - if requests.get(rec_url).status_code == 200: - response = requests.get(rec_url) - response.raise_for_status() - rechead = response.json() - - # Exit since we found one return else: # Exit since we already have a live announcement job @@ -166,122 +176,87 @@ async def query_database(): scheduler.remove_job('announce_live_channel') if live_channel_id != 0: live_channel = bot.get_channel(int(live_channel_id)) - if rechead != {}: - rec_stream_name = rechead['name'] - video_filename = rechead['video'] - thumb_filename = rechead['thumb'] - # Reset the rechead - rechead = {} - - # Creating an embed - img_url = f'https://{scheduler_hostname}/static/images' - thumb_url = f'https://{scheduler_hostname}/thumb/{thumb_filename}' - video_download_url = f'https://{scheduler_hostname}/video/download/{video_filename}' - video_filename_no_extension = video_filename.split('.')[0] - video_watch_url = f'https://{scheduler_hostname}/video/watch/{video_filename_no_extension}' - embed = discord.Embed(title=f'VOD: {video_filename_no_extension}', - url=f'{video_watch_url}', - description=f'{rec_stream_name}', - colour=0x00b0f4, - timestamp=datetime.now()) - embed.add_field(name="Download", - value=f'[mp4 file]({video_download_url})', - inline=True) - embed.add_field(name="Watch", - value=f'[plyr.js player]({video_watch_url}) :]', - inline=True) - embed.set_image(url=thumb_url) - #embed.set_thumbnail(url=f'{img_url}/logo-96.png') - embed.set_footer(text="DeflaxTV", - icon_url=f'{img_url}/logo-96.png') - # Sending the embed to the channel - await live_channel.send(embed=embed) - logger_discord.info(f'{rec_stream_name} is offline. VOD: {video_filename_no_extension}') - else: - await live_channel.send('Stream is offline.') - logger_discord.info('Stream is offline.') + await live_channel.send(f'{stream_name} is offline.') + logger_discord.info(f'{stream_name} is offline.') async def announce_live_channel(stream_name, stream_meta): - logger_discord.info(f'{stream_name} is live! {stream_meta}') if live_channel_id != 0: live_channel = bot.get_channel(int(live_channel_id)) await live_channel.send(f'{stream_name} is live! :satellite_orbital: {stream_meta}') + logger_discord.info(f'{stream_name} is live! {stream_meta}') # Execute recorder -async def exec_recorder(stream_id, stream_name, stream_hls_url): - global rechead - current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S-%f") +async def exec_recorder(playhead): + stream_id = playhead['id'] + stream_name = playhead['name'] + stream_hls_url = playhead['head'] + current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") video_file = current_datetime + ".mp4" thumb_file = current_datetime + ".png" - if rechead != {}: - logger_job.error('Recorder is already started. Refusing to start another job.') + video_output = f'{rec_path}/live/{video_file}' + thumb_output = f'{rec_path}/live/{thumb_file}' + + try: + logger_discord.info(f'Recording video {video_file}') + # Record a mp4 file + ffmpeg = ( + FFmpeg() + .option("y") + .input(stream_hls_url) + .output(video_output, + {"codec:v": "copy", "codec:a": "copy", "bsf:a": "aac_adtstoasc"}, + )) + @ffmpeg.on("progress") + def on_progress(progress: Progress): + print(progress) + ffmpeg.execute() + logger_discord.info(f'Recording of {video_file} finished.') + + except Exception as joberror: + logger_discord.error(f'Recording of {video_file} failed!') + logger_discord.error(joberror) + else: - logger_job.warning(f'Recording {video_file} started.') - rechead = { 'id': stream_id, - 'name': stream_name, - 'video': video_file, - 'thumb': thumb_file } - video_output = f'{rec_path}/live/{video_file}' - thumb_output = f'{rec_path}/live/{thumb_file}' + # Show Metadata + ffmpeg_metadata = ( + FFmpeg(executable="ffprobe") + .input(video_output, + print_format="json", + show_streams=None,) + ) + media = json.loads(ffmpeg_metadata.execute()) + logger_discord.info(f"# Video") + logger_discord.info(f"- Codec: {media['streams'][0]['codec_name']}") + logger_discord.info(f"- Resolution: {media['streams'][0]['width']} X {media['streams'][0]['height']}") + logger_discord.info(f"- Duration: {media['streams'][0]['duration']}") + logger_discord.info(f"# Audio") + logger_discord.info(f"- Codec: {media['streams'][1]['codec_name']}") + logger_discord.info(f"- Sample Rate: {media['streams'][1]['sample_rate']}") + logger_discord.info(f"- Duration: {media['streams'][1]['duration']}") - try: - # Record a mp4 file - ffmpeg = ( - FFmpeg() - .option("y") - .input(stream_hls_url) - .output(video_output, - {"codec:v": "copy", "codec:a": "copy", "bsf:a": "aac_adtstoasc"}, - )) - @ffmpeg.on("progress") - def on_progress(progress: Progress): - print(progress) - ffmpeg.execute() - logger_job.warning(f'Recording of {video_file} finished.') - - except Exception as joberror: - logger_job.error(f'Recording of {video_file} failed!') - logger_job.error(joberror) - - else: - # Show Metadata - ffmpeg_metadata = ( - FFmpeg(executable="ffprobe") - .input(video_output, - print_format="json", - show_streams=None,) - ) - media = json.loads(ffmpeg_metadata.execute()) - logger_job.warning(f"# Video") - logger_job.warning(f"- Codec: {media['streams'][0]['codec_name']}") - logger_job.warning(f"- Resolution: {media['streams'][0]['width']} X {media['streams'][0]['height']}") - logger_job.warning(f"- Duration: {media['streams'][0]['duration']}") - logger_job.warning(f"# Audio") - logger_job.warning(f"- Codec: {media['streams'][1]['codec_name']}") - logger_job.warning(f"- Sample Rate: {media['streams'][1]['sample_rate']}") - logger_job.warning(f"- Duration: {media['streams'][1]['duration']}") - - thumb_skip_time = float(media['streams'][0]['duration']) // 2 - thumb_width = media['streams'][0]['width'] + thumb_skip_time = float(media['streams'][0]['duration']) // 2 + thumb_width = media['streams'][0]['width'] - # Generate thumbnail image from the recorded mp4 file - ffmpeg_thumb = ( - FFmpeg() - .input(video_output, ss=thumb_skip_time) - .output(thumb_output, vf='scale={}:{}'.format(thumb_width, -1), vframes=1) - ) - ffmpeg_thumb.execute() - logger_job.warning(f'Thumbnail {thumb_file} created.') - - # When ready, move the recorded from the live dir to the archives and reset the rec head - os.rename(f'{video_output}', f'{rec_path}/vod/{video_file}') - os.rename(f'{thumb_output}', f'{rec_path}/thumb/{thumb_file}') + try: + logger_discord.info(f'Generating thumb {thumb_file}') + # Generate thumbnail image from the recorded mp4 file + ffmpeg_thumb = ( + FFmpeg() + .input(video_output, ss=thumb_skip_time) + .output(thumb_output, vf='scale={}:{}'.format(thumb_width, -1), vframes=1) + ) + ffmpeg_thumb.execute() + logger_discord.info(f'Thumbnail {thumb_file} created.') + + except Exception as joberror: + logger_discord.error(f'Generating thumb {thumb_file} failed!') + logger_discord.error(joberror) - finally: - # Reset the rechead - time.sleep(5) - rechead = {} - logger_job.warning(f'Rechead reset.') + # When ready, move the recorded from the live dir to the archives and reset the rec head + os.rename(f'{video_output}', f'{rec_path}/vod/{video_file}') + os.rename(f'{thumb_output}', f'{rec_path}/thumb/{thumb_file}') + await create_embed(stream_name, video_output, thumb_output) + logger_discord.info('Recording job done in {rec_job_time} minutes') # HLS Converter async def hls_converter(): @@ -297,10 +272,33 @@ async def hls_converter(): if entry.lower().endswith('.mp4'): input_file = file_path break - #logger_job.warning(f'{input_file} found. Converting to HLS...') - + #logger_discord.info(f'{input_file} found. Converting to HLS...') except Exception as e: - logger_job.error(e) + logger_discord.error(e) + +async def create_embed(stream_name, video_filename, thumb_filename): + # Creating an embed + img_url = f'https://{scheduler_hostname}/static/images' + thumb_url = f'https://{scheduler_hostname}/thumb/{thumb_filename}' + video_download_url = f'https://{scheduler_hostname}/video/download/{video_filename}' + video_filename_no_extension = video_filename.split('.')[0] + video_watch_url = f'https://{scheduler_hostname}/video/watch/{video_filename_no_extension}' + embed = discord.Embed(title=f'VOD: {video_filename_no_extension}', + url=f'{video_watch_url}', + description=f'{stream_name}', + colour=0x00b0f4, + timestamp=datetime.now()) + embed.add_field(name="Download", + value=f'[mp4 file]({video_download_url})', + inline=True) + embed.add_field(name="Watch", + value=f'[plyr.js player]({video_watch_url}) :]', + inline=True) + embed.set_image(url=thumb_url) + #embed.set_thumbnail(url=f'{img_url}/logo-96.png') + embed.set_footer(text="DeflaxTV", + icon_url=f'{img_url}/logo-96.png') + await live_channel.send(embed=embed) # Run the bot with your token -asyncio.run(bot.run(bot_token)) +asyncio.run(bot.run(bot_token)) \ No newline at end of file