Compare commits
10 commits
ed7bd5efc8
...
17b426be66
| Author | SHA1 | Date | |
|---|---|---|---|
| 17b426be66 | |||
| 7bfca6f035 | |||
| cfd8429188 | |||
| 0448b3eced | |||
| b278a6aa52 | |||
| 959f954d9d | |||
| d6b2b54d13 | |||
| 285384be22 | |||
| 9a924c8c0d | |||
| aa04c6ef5b |
8 changed files with 133 additions and 59 deletions
|
|
@ -12,11 +12,11 @@ impl<T, R> Requester<T, R> {
|
|||
pub fn send(&self, data: T) -> Result<Response<R>, SendError<T>> {
|
||||
let (response_tx, response_rx) = mpsc::channel();
|
||||
match self.tx.send(Request {
|
||||
data,
|
||||
data: Some(data),
|
||||
tx: response_tx,
|
||||
}) {
|
||||
Ok(_) => Ok(Response { rx: response_rx }),
|
||||
Err(e) => Err(SendError(e.0.data)),
|
||||
Err(e) => Err(SendError(e.0.data.unwrap())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,14 +40,14 @@ impl<T, R> Responder<T, R> {
|
|||
|
||||
pub fn channel<T, R>() -> (Requester<T, R>, Responder<T, R>) {
|
||||
let (requester_tx, responder_rx) = mpsc::channel();
|
||||
return (
|
||||
(
|
||||
Requester { tx: requester_tx },
|
||||
Responder { rx: responder_rx },
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Request<T, R> {
|
||||
pub data: T,
|
||||
pub data: Option<T>,
|
||||
tx: Sender<R>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,33 @@
|
|||
use std::{path::Path, process::Command};
|
||||
|
||||
use crate::track::TrackInfo;
|
||||
use crate::{file::FileHandle, track::{Track, TrackInfo}};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DownloadError;
|
||||
|
||||
pub fn download_from_youtube(url: &str) -> Result<TrackInfo, DownloadError> {
|
||||
pub fn download_from_youtube(url: &str) -> Result<Track, DownloadError> {
|
||||
let output = Command::new("yt-dlp")
|
||||
.args([
|
||||
"--print",
|
||||
"%(id)s %(duration)i",
|
||||
"--no-playlist",
|
||||
"--no-warnings",
|
||||
"--",
|
||||
url,
|
||||
]).output().unwrap();
|
||||
|
||||
let items: Vec<String> = std::str::from_utf8(&output.stdout).unwrap().split(' ').map(|s| s.replace("\n", "")).collect();
|
||||
|
||||
let filename = items[0].clone() + ".mp3";
|
||||
let duration: u32 = items[1].parse().unwrap();
|
||||
|
||||
println!("{}", filename);
|
||||
println!("{}", duration);
|
||||
|
||||
if duration > 900 {
|
||||
return Err(DownloadError);
|
||||
}
|
||||
|
||||
let output = Command::new("yt-dlp")
|
||||
.args([
|
||||
"-o",
|
||||
|
|
@ -13,22 +35,22 @@ pub fn download_from_youtube(url: &str) -> Result<TrackInfo, DownloadError> {
|
|||
"--extract-audio",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--print",
|
||||
"%(id)s",
|
||||
"--no-simulate",
|
||||
"--no-playlist",
|
||||
"--no-warnings",
|
||||
"--",
|
||||
url,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
if !output.stderr.is_empty() || output.stdout.is_empty() {
|
||||
println!("{}", std::str::from_utf8(output.stderr.as_slice()).unwrap());
|
||||
println!("{}", std::str::from_utf8(output.stdout.as_slice()).unwrap());
|
||||
return Err(DownloadError);
|
||||
}
|
||||
|
||||
let filename = std::str::from_utf8(output.stdout.as_slice())
|
||||
.unwrap()
|
||||
.replace('\n', "")
|
||||
+ ".mp3";
|
||||
let file_handle = FileHandle::new(Path::new(filename.as_str()));
|
||||
let info = TrackInfo::new(&filename);
|
||||
|
||||
Ok(TrackInfo::new(&Path::new(filename.as_str())))
|
||||
Ok(Track::new(info, file_handle))
|
||||
}
|
||||
|
|
|
|||
25
src/file.rs
Normal file
25
src/file.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileHandle {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileHandle {
|
||||
pub fn new(path: &Path) -> FileHandle {
|
||||
FileHandle {
|
||||
path: PathBuf::from(path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileHandle {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(self.path.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
pub mod channel;
|
||||
pub mod download;
|
||||
pub mod file;
|
||||
pub mod player;
|
||||
pub mod telegram;
|
||||
mod track;
|
||||
|
||||
pub use track::TrackInfo;
|
||||
pub use track::Track;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use music_bot::{player::MusicPlayer, telegram};
|
||||
|
||||
fn main() {
|
||||
let player = MusicPlayer::new();
|
||||
let player = MusicPlayer::build();
|
||||
let mut bot = telegram::TelegramBot::build(player);
|
||||
bot.telegram_main();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::{collections::VecDeque, fs::File, io::BufReader, thread, thread::JoinHandle};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
|
||||
use std::{collections::VecDeque, fs::File, io::BufReader, thread, thread::JoinHandle};
|
||||
|
||||
use crate::channel::{self, Requester, Responder, TryRecvError};
|
||||
use crate::track::TrackInfo;
|
||||
use crate::track::{Track, TrackInfo};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
enum WorkerRequest {
|
||||
AddTrack(TrackInfo),
|
||||
AddTrack(Track),
|
||||
Pause,
|
||||
Play,
|
||||
Stop,
|
||||
|
|
@ -14,7 +14,7 @@ enum WorkerRequest {
|
|||
ListTracks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
enum WorkerResponse {
|
||||
TrackList(VecDeque<TrackInfo>),
|
||||
None,
|
||||
|
|
@ -25,19 +25,20 @@ use WorkerResponse::*;
|
|||
|
||||
pub struct MusicPlayer {
|
||||
requester: Requester<WorkerRequest, WorkerResponse>,
|
||||
_worker: JoinHandle<()>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
_stream: OutputStream,
|
||||
_handle: OutputStreamHandle,
|
||||
sink: Sink,
|
||||
queue: VecDeque<TrackInfo>,
|
||||
queue: VecDeque<Track>,
|
||||
current: Option<Track>,
|
||||
responder: Responder<WorkerRequest, WorkerResponse>,
|
||||
}
|
||||
|
||||
fn get_source(track: TrackInfo) -> Decoder<BufReader<File>> {
|
||||
let file = BufReader::new(File::open(&track.path).unwrap());
|
||||
fn get_source(track: &Track) -> Decoder<BufReader<File>> {
|
||||
let file = BufReader::new(File::open(track.file.get_path()).unwrap());
|
||||
Decoder::new(file).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -59,28 +60,34 @@ impl Worker {
|
|||
Stop => {
|
||||
self.sink.stop();
|
||||
self.queue.clear();
|
||||
self.current.take();
|
||||
None
|
||||
}
|
||||
SkipOne => {
|
||||
self.sink.skip_one();
|
||||
self.current.take();
|
||||
None
|
||||
}
|
||||
ListTracks => TrackList(self.queue.clone()),
|
||||
ListTracks => TrackList(self.queue.iter().map(|track| track.info.clone()).collect::<VecDeque<TrackInfo>>().clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(&mut self) {
|
||||
loop {
|
||||
match self.responder.try_recv() {
|
||||
Ok(req) => {
|
||||
let _ = req.respond(self.match_request(req.data.clone()));
|
||||
Ok(mut req) => {
|
||||
let data = req.data.take().unwrap();
|
||||
let _ = req.respond(self.match_request(data));
|
||||
}
|
||||
Err(TryRecvError::Empty) => {}
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
};
|
||||
if self.sink.empty() && !self.queue.is_empty() {
|
||||
let next_track = self.queue.pop_front().unwrap();
|
||||
self.sink.append(get_source(next_track));
|
||||
self.sink.append(get_source(&next_track));
|
||||
self.current = Some(next_track);
|
||||
} else if self.sink.empty() {
|
||||
self.current.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,25 +95,26 @@ impl Worker {
|
|||
pub fn build(responder: Responder<WorkerRequest, WorkerResponse>) -> Worker {
|
||||
let (_stream, _handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&_handle).unwrap();
|
||||
let queue: VecDeque<TrackInfo> = VecDeque::new();
|
||||
let queue: VecDeque<Track> = VecDeque::new();
|
||||
Worker {
|
||||
_stream,
|
||||
_handle,
|
||||
sink,
|
||||
queue,
|
||||
current: Option::None,
|
||||
responder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayer {
|
||||
pub fn new() -> MusicPlayer {
|
||||
pub fn build() -> MusicPlayer {
|
||||
let (requester, responder) = channel::channel();
|
||||
let _worker = thread::spawn(move || Worker::build(responder).main());
|
||||
MusicPlayer { requester, _worker }
|
||||
let worker = thread::spawn(move || Worker::build(responder).main());
|
||||
MusicPlayer { requester, worker: Some(worker) }
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, track: TrackInfo) {
|
||||
pub fn enqueue(&mut self, track: Track) {
|
||||
self.requester.send(AddTrack(track)).unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -134,3 +142,10 @@ impl MusicPlayer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MusicPlayer {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
self.worker.take().unwrap().join().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
|
||||
use crate::{player::MusicPlayer, download};
|
||||
use crate::{download, player::MusicPlayer};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(
|
||||
|
|
@ -41,17 +41,15 @@ impl TelegramBot {
|
|||
cmd: Command,
|
||||
) -> Result<(), teloxide::RequestError> {
|
||||
match cmd {
|
||||
Command::Play(url) => {
|
||||
match download::download_from_youtube(&url) {
|
||||
Command::Play(url) => match download::download_from_youtube(&url) {
|
||||
Ok(track_info) => {
|
||||
player.lock().unwrap().enqueue(track_info);
|
||||
bot.send_message(msg.chat.id, "Added to the queue.").await?;
|
||||
},
|
||||
}
|
||||
Err(_) => {
|
||||
bot.send_message(msg.chat.id, format!("Failed to download.")).await?;
|
||||
}
|
||||
}
|
||||
bot.send_message(msg.chat.id, "Failed to download.").await?;
|
||||
}
|
||||
},
|
||||
Command::Stop => {
|
||||
player.lock().unwrap().stop();
|
||||
bot.send_message(msg.chat.id, "Stopped.").await?;
|
||||
|
|
@ -70,15 +68,17 @@ impl TelegramBot {
|
|||
}
|
||||
Command::List => {
|
||||
let tracks = player.lock().unwrap().list_tracks();
|
||||
bot.send_message(
|
||||
msg.chat.id,
|
||||
tracks
|
||||
let message: String;
|
||||
if tracks.is_empty() {
|
||||
message = String::from("The queue is empty.");
|
||||
} else {
|
||||
message = tracks
|
||||
.iter()
|
||||
.map(|t| t.path.to_str().unwrap())
|
||||
.map(|t| t.name.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n"),
|
||||
)
|
||||
.await?;
|
||||
.join("\n");
|
||||
}
|
||||
bot.send_message(msg.chat.id, message).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
|
|
|
|||
23
src/track.rs
23
src/track.rs
|
|
@ -1,13 +1,24 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use crate::file::FileHandle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackInfo {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl TrackInfo {
|
||||
pub fn new(path: &Path) -> TrackInfo {
|
||||
TrackInfo {
|
||||
path: PathBuf::from(path),
|
||||
}
|
||||
pub fn new(name: &str) -> TrackInfo {
|
||||
TrackInfo { name: String::from(name) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Track {
|
||||
pub info: TrackInfo,
|
||||
pub file: FileHandle,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub fn new(info: TrackInfo, file: FileHandle) -> Track {
|
||||
Track { info, file }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue