Add youtube playlist support

This commit is contained in:
hodasemi 2018-08-14 18:42:53 +02:00
parent 048351f22b
commit c918cb0f97
7 changed files with 401 additions and 67 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
/target
**/*.rs.bk
*.webm

29
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug RMusicBot",
"cargo": {
"args": [
"build",
],
"filter": {
"name": "RMusicBot",
"kind": "bin"
}
},
"linux": {
"env": {
"RUST_BACKTRACE": "1"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

2
Cargo.lock generated
View file

@ -3,6 +3,8 @@ name = "RMusicBot"
version = "0.1.0"
dependencies = [
"framework 0.1.0 (git+ssh://git@141.30.224.77:23/hodasemi/framework.git?branch=Animations)",
"parking_lot 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
"serenity 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
"typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]

View file

@ -5,6 +5,7 @@ authors = ["hodasemi <michaelh.95@t-online.de>"]
[dependencies]
typemap = "~0.3"
serde_json = "*"
[dependencies.serenity]
default-features = false
@ -26,3 +27,6 @@ git = "ssh://git@141.30.224.77:23/hodasemi/framework.git"
branch = "Animations"
default-features = false
features = [ "helper" ]
[dependencies.parking_lot]
version = "^0.5"

View file

@ -1,5 +1,8 @@
#[macro_use] extern crate serenity;
#[macro_use]
extern crate serenity;
extern crate parking_lot;
extern crate serde_json;
extern crate typemap;
#[macro_use]
@ -9,7 +12,7 @@ extern crate framework;
// feature, it's not as ergonomic to work with as it could be. The client
// provides a clean bridged integration with voice.
use serenity::client::bridge::voice::ClientVoiceManager;
use serenity::client::{CACHE, Client, Context, EventHandler};
use serenity::client::{Client, Context, EventHandler, CACHE};
use serenity::framework::StandardFramework;
use serenity::model::channel::Message;
use serenity::model::gateway::Ready;
@ -21,14 +24,24 @@ use serenity::model::misc::Mentionable;
//
// <https://github.com/Amanieu/parking_lot#features>
use serenity::prelude::Mutex;
use serenity::voice;
use serenity::Result as SerenityResult;
use std::sync::Arc;
use typemap::Key;
use framework::prelude::*;
struct VoiceManager;
mod player;
mod youtube;
use player::*;
/*
const fn empty_vec<T>() -> Vec<T> {
Vec::new()
}
*/
pub struct VoiceManager;
impl Key for VoiceManager {
type Value = Arc<Mutex<ClientVoiceManager>>;
@ -45,7 +58,7 @@ impl EventHandler for Handler {
fn main() {
// read config file
let config = check_result_return!(read_config("bot.conf"));
let token = match config.get("Meta") {
Some(info) => match info.get("token") {
Some(token_pair) => {
@ -63,7 +76,7 @@ fn main() {
return;
}
};
let mut client = Client::new(&token, Handler).expect("Err creating client");
// Obtain a lock to the data owned by the client, and insert the client's
@ -74,20 +87,25 @@ fn main() {
data.insert::<VoiceManager>(Arc::clone(&client.voice_manager));
}
client.with_framework(StandardFramework::new()
.configure(|c| c
.prefix("~")
.on_mention(true))
.cmd("deafen", deafen)
.cmd("join", join)
.cmd("leave", leave)
.cmd("mute", mute)
.cmd("play", play)
.cmd("ping", ping)
.cmd("undeafen", undeafen)
.cmd("unmute", unmute));
let media_data = Arc::new(MediaData::default());
let _ = client.start().map_err(|why| println!("Client ended: {:?}", why));
client.with_framework(
StandardFramework::new()
.configure(|c| c.prefix("~").on_mention(true))
.cmd("deafen", deafen)
.cmd("join", join)
.cmd("leave", leave)
.cmd("mute", mute)
.cmd("play", Play::new(media_data.clone()))
.cmd("pause", Pause::new(media_data.clone()))
.cmd("ping", ping)
.cmd("undeafen", undeafen)
.cmd("unmute", unmute),
);
let _ = client
.start()
.map_err(|why| println!("Client ended: {:?}", why));
}
command!(deafen(ctx, msg) {
@ -216,54 +234,6 @@ command!(ping(_context, msg) {
check_msg(msg.channel_id.say("Pong!"));
});
command!(play(ctx, msg, args) {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
check_msg(msg.channel_id.say("Must provide a URL to a video or audio"));
return Ok(());
},
};
if !url.starts_with("http") {
check_msg(msg.channel_id.say("Must provide a valid URL"));
return Ok(());
}
let guild_id = match CACHE.read().guild_channel(msg.channel_id) {
Some(channel) => channel.read().guild_id,
None => {
check_msg(msg.channel_id.say("Error finding channel info"));
return Ok(());
},
};
let mut manager_lock = ctx.data.lock().get::<VoiceManager>().cloned().unwrap();
let mut manager = manager_lock.lock();
if let Some(handler) = manager.get_mut(guild_id) {
let source = match voice::ytdl(&url) {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
check_msg(msg.channel_id.say("Error sourcing ffmpeg"));
return Ok(());
},
};
handler.play(source);
check_msg(msg.channel_id.say("Playing song"));
} else {
check_msg(msg.channel_id.say("Not in a voice channel to play in"));
}
});
command!(undeafen(ctx, msg) {
let guild_id = match CACHE.read().guild_channel(msg.channel_id) {
Some(channel) => channel.read().guild_id,

222
src/player.rs Normal file
View file

@ -0,0 +1,222 @@
use parking_lot;
use serenity;
use serenity::client::CACHE;
use serenity::model::id::{ChannelId, GuildId};
use serenity::voice::{AudioSource, Handler, LockedAudio};
use std::cell::RefCell;
use std::ops::DerefMut;
use std::sync::Arc;
use std::thread;
use std::time;
macro_rules! check_option {
($v:expr) => {
match $v {
Some(t) => t,
None => return (),
}
};
}
pub struct Song {
pub source: Box<AudioSource>,
pub name: String,
}
pub struct MediaData {
playlist: RefCell<Vec<Song>>,
current_song: RefCell<Option<LockedAudio>>,
}
fn guild_id(channel_id: ChannelId) -> Option<GuildId> {
match CACHE.read().guild_channel(channel_id) {
Some(channel) => Some(channel.read().guild_id),
None => {
super::check_msg(channel_id.say("Error finding channel info"));
None
}
}
}
fn handler<'a>(
guild_id: GuildId,
manager: &'a mut parking_lot::MutexGuard<
'_,
serenity::client::bridge::voice::ClientVoiceManager,
>,
) -> Option<&'a mut Handler> {
manager.get_mut(guild_id)
}
impl MediaData {
fn start_thread(
media: &Arc<MediaData>,
channel_id: ChannelId,
manager_lock: &Arc<
serenity::prelude::Mutex<serenity::client::bridge::voice::ClientVoiceManager>,
>,
) {
let media_clone = media.clone();
let manager_clone = manager_lock.clone();
thread::spawn(move || loop {
let mut borrow = media_clone.current_song.borrow_mut();
if let Some(ref mut song) = borrow.deref_mut() {
let song_lock = song.clone();
let audio = song_lock.lock();
if audio.finished {
Self::next_song(
song,
media_clone.playlist.borrow_mut().deref_mut(),
channel_id,
&manager_clone,
)
}
continue;
}
let mut playlist = media_clone.playlist.borrow_mut();
if !playlist.is_empty() {
let mut manager = manager_clone.lock();
if let Some(handler) = handler(check_option!(guild_id(channel_id)), &mut manager) {
let first = playlist.remove(0);
*borrow = Some(handler.play_returning(first.source));
super::check_msg(channel_id.say(format!(
"Playing song: {}",
super::youtube::convert_file_name(first.name)
)));
}
}
let two_sec = time::Duration::new(2, 0);
thread::sleep(two_sec);
});
}
fn next_song(
song: &mut LockedAudio,
playlist: &mut Vec<Song>,
channel_id: ChannelId,
manager_lock: &Arc<
serenity::prelude::Mutex<serenity::client::bridge::voice::ClientVoiceManager>,
>,
) {
let mut manager = manager_lock.lock();
if let Some(handler) = handler(check_option!(guild_id(channel_id)), &mut manager) {
let first = playlist.remove(0);
*song = handler.play_returning(first.source);
super::check_msg(channel_id.say(format!(
"Playing song: {}",
super::youtube::convert_file_name(first.name)
)));
}
}
}
impl Default for MediaData {
fn default() -> MediaData {
MediaData {
playlist: RefCell::new(Vec::new()),
current_song: RefCell::new(None),
}
}
}
unsafe impl Send for MediaData {}
unsafe impl Sync for MediaData {}
pub struct Play {
media: Arc<MediaData>,
}
impl Play {
pub fn new(media_data: Arc<MediaData>) -> Play {
Play { media: media_data }
}
}
impl serenity::framework::standard::Command for Play {
#[allow(unreachable_code, unused_mut)]
fn execute(
&self,
mut ctx: &mut serenity::client::Context,
msg: &serenity::model::channel::Message,
mut args: serenity::framework::standard::Args,
) -> ::std::result::Result<(), serenity::framework::standard::CommandError> {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
super::check_msg(msg.channel_id.say("Must provide a URL to a video or audio"));
return Ok(());
}
};
if !url.starts_with("http") {
super::check_msg(msg.channel_id.say("Must provide a valid URL"));
return Ok(());
}
let mut manager_lock = ctx.data
.lock()
.get::<super::VoiceManager>()
.cloned()
.unwrap();
let mut source = match super::youtube::youtube_dl(&url) {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
super::check_msg(msg.channel_id.say("Error sourcing ffmpeg"));
return Ok(());
}
};
self.media.playlist.borrow_mut().append(&mut source);
MediaData::start_thread(&self.media, msg.channel_id, &manager_lock);
Ok(())
}
}
pub struct Pause {
media: Arc<MediaData>,
}
impl Pause {
pub fn new(media_data: Arc<MediaData>) -> Pause {
Pause { media: media_data }
}
}
impl serenity::framework::standard::Command for Pause {
#[allow(unreachable_code, unused_mut)]
fn execute(
&self,
_: &mut serenity::client::Context,
_: &serenity::model::channel::Message,
_: serenity::framework::standard::Args,
) -> ::std::result::Result<(), serenity::framework::standard::CommandError> {
if let Some(song) = self.media.current_song.borrow_mut().deref_mut() {
//song.pause();
}
Ok(())
}
}

105
src/youtube.rs Normal file
View file

@ -0,0 +1,105 @@
use serenity::voice::ffmpeg;
use std::process::{Command, Output, Stdio};
use std::str::from_utf8;
use player::Song;
const FIRST_LOAD_PREFIX: &str = "[download] Destination:";
const RELOAD_SUFFIX: &str = " has already been downloaded";
const PREFIX: &str = "[download] ";
pub fn convert_file_name(file: String) -> String {
let mut file = file.to_string();
let file_name_len = file.len();
let mut file_without_suffix = if file.ends_with(".webm") {
file.split_off(file_name_len - 5);
file
} else {
file
};
let file_without_id = match file_without_suffix.rfind("-") {
Some(minus_pos) => {
file_without_suffix.split_off(minus_pos);
file_without_suffix
}
None => file_without_suffix,
};
file_without_id
}
fn convert_output(out: &Output) -> Result<Vec<String>, String> {
match from_utf8(out.stdout.as_slice()) {
Ok(string) => {
let lines = string.split("\n");
let mut files = Vec::new();
for line in lines {
if !line.is_empty() {
let mut line = line.to_string();
let line_len = line.len();
if line.starts_with(FIRST_LOAD_PREFIX) {
let file_name = line.split_off(FIRST_LOAD_PREFIX.len());
files.push(file_name.trim().to_string());
} else if line.ends_with(RELOAD_SUFFIX) {
line.split_off(line_len - RELOAD_SUFFIX.len());
let file_name = line.split_off(PREFIX.len());
files.push(file_name.trim().to_string());
}
}
}
Ok(files)
}
Err(_) => Err("error converting output".to_string()),
}
}
fn print_output(out: &Output) {
match from_utf8(out.stdout.as_slice()) {
Ok(string) => println!("value: {}", string),
Err(_) => println!("error converting output"),
};
}
pub fn youtube_dl(uri: &str) -> Result<Vec<Song>, String> {
let args = ["-f", "webm[abr>0]/bestaudio/best", uri];
let out = match Command::new("youtube-dl")
.args(&args)
.stdin(Stdio::null())
.output()
{
Ok(out) => out,
Err(_) => return Err("youtube-dl error".to_string()),
};
if !out.status.success() {
return Err("shell error".to_string());
}
let files = check_result!(convert_output(&out));
for file in &files {
println!("file: {}", file);
}
let mut ffmpegs = Vec::new();
for file in files {
match ffmpeg(&file) {
Ok(mpeg) => ffmpegs.push(Song {
source: mpeg,
name: file,
}),
Err(_) => return Err("ffmpeg error".to_string()),
}
}
Ok(ffmpegs)
}