Add youtube playlist support
This commit is contained in:
parent
048351f22b
commit
c918cb0f97
7 changed files with 401 additions and 67 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
*.webm
|
||||
|
|
29
.vscode/launch.json
vendored
Normal file
29
.vscode/launch.json
vendored
Normal 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
2
Cargo.lock
generated
|
@ -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)",
|
||||
]
|
||||
|
|
|
@ -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"
|
104
src/main.rs
104
src/main.rs
|
@ -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
222
src/player.rs
Normal 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
105
src/youtube.rs
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue