Skip to content

Commit

Permalink
Upgrade to v2.0.0 (#6)
Browse files Browse the repository at this point in the history
* Add dependencies

* Add tests

* Add options

* Format

* Add gitignore to .idea

* Add function for parse end_time

* Add parse duration function

* Add function for set mpv args

* Upgrade version in Cargo.toml
  • Loading branch information
satler-git authored May 26, 2024
1 parent dbe012f commit 3bdfb77
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Cargo.lock
# Added by cargo

/target
.idea
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tt"
version = "1.0.1"
version = "2.0.0"
edition = "2021"
license = "MIT"

Expand All @@ -16,3 +16,4 @@ percent-encoding = "2.3.1"
serde_json = "1.0.115"
directories = "5.0.1"
rand = "0.8.5"
clap = { version = "4.5.4", features = ["derive"] }
208 changes: 196 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
//! * `api_key`
//! gasのデプロイID
use chrono::{DateTime, FixedOffset, NaiveTime, Utc};
use chrono::{DateTime, FixedOffset, NaiveTime, Timelike, Utc};
use clap::Parser;
use directories::ProjectDirs;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use rand::seq::SliceRandom;
use rusqlite::{params, Connection, Result};
use serde_derive::{Deserialize, Serialize};
use std::process::{Command, Stdio};
use std::{
process::{Command, Stdio},
u32,
};

#[derive(Serialize, Deserialize)]
struct MyConfig {
Expand All @@ -19,7 +23,7 @@ struct MyConfig {
}

/// `MyConfig` implements `Default`
impl ::std::default::Default for MyConfig {
impl Default for MyConfig {
fn default() -> Self {
Self {
api_key: "".into(),
Expand Down Expand Up @@ -106,13 +110,19 @@ struct Request {

/// 与えられた単語のリストからvideo_idを取得してmpvで再生します
/// * `search_word_list` - 検索する単語のリスト
async fn play_music(search_word_list: [&String; 2]) {
async fn play_music(search_word_list: [&String; 2], mpv_arsg: &Option<Vec<String>>) {
println!("Playing {} {}", search_word_list[0], search_word_list[1]);

let video_id = search_youtube(search_word_list).await;

let mut mpv_options: Vec<String> = vec![];
if let Some(mo) = mpv_arsg {
mpv_options = mo.clone();
}
// デフォルトのオプションを追加
mpv_options.push("-fs".into());
mpv_options.push(video_id.into());
match Command::new("mpv")
.args(["-fs", /* "--volume=50", */ &video_id])
.args(mpv_options)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.output()
Expand All @@ -125,8 +135,8 @@ async fn play_music(search_word_list: [&String; 2]) {

impl Request {
/// `play_music()`で再生します
async fn play(&self) {
play_music([&self.song_name, &self.artist_name]).await;
async fn play(&self, mpv_arsg: &Option<Vec<String>>) {
play_music([&self.song_name, &self.artist_name], mpv_arsg).await;
}

fn set_as_played(&self, conn: &Connection) {
Expand Down Expand Up @@ -231,7 +241,8 @@ fn comp_time(cfg: &MyConfig) -> bool {

/// SQLiteから次の流すべきリクエストを判断し`play_song()`で再生
/// * `conn` SQLiteのコネクション
async fn play_next(conn: &Connection) {
/// * `mpv_arsg` MPVへのオプションのオプション
async fn play_next(conn: &Connection, mpv_arsg: &Option<Vec<String>>) {
// playedがfalseかつ、arrangeが最小(!unique)
let mut stmt = conn
.prepare("select id, song_name, artist_name from requests where played = 0 and arrange = (select MIN(arrange) from requests where played = 0)")
Expand All @@ -258,20 +269,193 @@ async fn play_next(conn: &Connection) {

let next = requests.choose(&mut rand::thread_rng()).unwrap();

next.play().await;
next.play(mpv_arsg).await;
next.set_as_played(&conn);
}

/// ClI引数用のストラクト
#[derive(Parser, Debug)]
#[command(version)]
#[command(about = "A CLI tool for playing music automatically.", long_about = None)]
struct Args {
// "12:30"のように24hで指定する
#[arg(
short,
long,
help = "Specify the end time in 24-hour notation, separated by \":\", for example, \"12:30\" minutes. \nCan't be specified at the same time as the duration."
)]
#[arg(conflicts_with = "duration")]
end_time: Option<String>,
// "1h2m3s"的な感じで指定する
#[arg(
short,
long,
help = "Specify by integer, separated by h, m, and s, as in \"1h2m3s\". Each can be omitted."
)]
duration: Option<String>,
#[arg(last = true, help = "Arguments passed directly to mpv.")]
mpv_arsg: Option<Vec<String>>,
}

/// `12:30`の様に与えられたのを`[12, 30, 0]`の様にパースする
/// `26:40`の様に与えられた場合はパニック
fn parse_end_time(end_time: String) -> [u32; 3] {
let str_words: Vec<&str> = end_time.split(":").collect();

let u32_words = str_words
.iter()
.map(|s| s.parse::<u32>().unwrap())
.collect::<Vec<u32>>();

// 正しい数じゃないか確認
if (u32_words[0] >= 24) | (u32_words[1] >= 60) | (str_words.len() != 2) {
panic!("The end_time option was invalid.");
}

[u32_words[0], u32_words[1], 0]
}

/// `[12, 70, 0]`を`[13, 10, 0]`にmodする関数
fn mod_time(time: [u32; 3]) -> [u32; 3] {
[
(time[0] + time[1] / 60 + time[2] / 3600) % 24,
(time[1] + time[2] / 60) % 60,
time[2] % 60,
]
}

/// `1h2m30s`の様な入力を`[1, 2, 30]`の様に返す
/// それぞれ省略可能で例えば`1h30m`が`[1, 30, 0]`
/// 24時間以上はpanic
fn parse_duration_diff(duration: String) -> [u32; 3] {
let mut h_point = 0;
let mut m_point = 0;
let mut h = 0;
let mut m = 0;
let mut s = 0;
if duration.contains("h") {
h_point = duration.find("h").unwrap();
h = String::from(&duration[0..h_point])
.parse()
.expect("The duration option was invalid.");
h_point += 1;
}
if duration.contains("m") {
m_point = duration.find("m").unwrap();
m = String::from(&duration[h_point..m_point])
.parse()
.expect("The duration option was invalid.");
m_point += 1;
} else if duration.contains("h") {
m_point = h_point.clone();
}
if duration.contains("s") {
s = String::from(&duration[m_point..duration.find("s").unwrap()])
.parse()
.expect("The duration option was invalid.");
}
mod_time([h, m, s])
}

/// `parse_duration_diff()`を使用して現在時刻と足しあわせる関数
fn parse_duration(duration: String) -> [u32; 3] {
let diff = parse_duration_diff(duration);
let now = Utc::now()
.with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap())
.time();
mod_time([
diff[0] + now.hour(),
diff[1] + now.minute(),
diff[2] + now.second(),
])
}

#[tokio::main]
async fn main() -> Result<(), confy::ConfyError> {
let cfg: MyConfig = confy::load("tt", "tt")?;
let mut cfg: MyConfig = confy::load("tt", "tt")?;
let args = Args::parse();
// argsをcfgに反映
if let Some(end_time) = args.end_time {
cfg.end_time = parse_end_time(end_time);
} else if let Some(duration) = args.duration {
cfg.end_time = parse_duration(duration);
}
let conn = init_sqlite().unwrap();
sync_backend(&cfg, &conn).await.unwrap();

while comp_time(&cfg) {
play_next(&conn).await;
play_next(&conn, &args.mpv_arsg).await;
println!("Comp to time: {}", comp_time(&cfg));
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

/// 正常な入力をパース出来るか
#[test]
fn check_parse_end_time() {
assert_eq!([12, 30, 0], parse_end_time("12:30".into()));
assert_eq!([6, 6, 0], parse_end_time("6:6".into()));
assert_eq!([13, 5, 0], parse_end_time("13:05".into()));
}

#[test]
#[should_panic]
fn check_parse_end_time_invaild_h() {
parse_end_time("100:0".into());
}
#[test]
#[should_panic]
fn check_parse_end_time_invaild_m() {
parse_end_time("0:100".into());
}
#[test]
#[should_panic]
fn check_parse_end_time_invaild_length() {
parse_end_time("0:0:0".into());
}
#[test]
#[should_panic]
fn check_parse_end_time_invaild_h_minus() {
parse_end_time("-1:0".into());
}

#[test]
fn check_mod_time() {
assert_eq!([2, 0, 0], mod_time([1, 60, 0]));
assert_eq!([1, 0, 0], mod_time([0, 0, 3600]));
}
#[test]
fn check_parse_duration() {
assert_eq!([1, 2, 3], parse_duration_diff("1h2m3s".into()));
assert_eq!([1, 2, 0], parse_duration_diff("1h2m".into()));
assert_eq!([0, 2, 3], parse_duration_diff("2m3s".into()));
assert_eq!([2, 2, 0], parse_duration_diff("1h62m".into()));
assert_eq!([1, 0, 3], parse_duration_diff("1h3s".into()));
}
/// テスト用のインメモリSQLiteのコネクションを作成
fn create_sqlite_conn() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute(
"
CREATE TABLE requests(
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
song_name TEXT NOT NULL,
artist_name TEXT NOT NULL,
played INTEGER NOT NULL,
uuid TEXT NOT NULL,
arrange INTEGER NOT NULL,
UNIQUE(uuid)
);
",
params![],
)
.unwrap();
conn
}
}

0 comments on commit 3bdfb77

Please sign in to comment.