diff --git a/gleam.toml b/gleam.toml index fcaabef..70b914e 100644 --- a/gleam.toml +++ b/gleam.toml @@ -17,6 +17,7 @@ gleam_stdlib = ">= 0.44.0 and < 2.0.0" gleam_otp = ">= 1.2.0 and < 2.0.0" gleam_erlang = ">= 1.3.0 and < 2.0.0" simplifile = ">= 2.3.1 and < 3.0.0" +gleam_json = ">= 3.1.0 and < 4.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index c3d61b4..a94ff87 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,6 +4,7 @@ packages = [ { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, @@ -12,6 +13,7 @@ packages = [ [requirements] gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } +gleam_json = { version = ">= 3.1.0 and < 4.0.0" } gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/mpv/control.gleam b/src/mpv/control.gleam new file mode 100644 index 0000000..ad5b38d --- /dev/null +++ b/src/mpv/control.gleam @@ -0,0 +1,73 @@ +import gleam/json +import gleam/result +import gleam/string + +import mpv/internal/control as internal_control +import mpv/key.{type Key, Char} +import tcp/reason.{type Reason} +import tcp/tcp.{type Socket} + +pub type Control { + TogglePlayPause + Exit +} + +pub type ControlError { + ControlError(details: String) +} + +pub fn from_key(key: Key) -> Result(Control, Nil) { + case key { + Char(char) -> char_control(char) + _ -> Error(Nil) + } +} + +fn char_control(char: String) -> Result(Control, Nil) { + case char { + " " -> Ok(TogglePlayPause) + "q" -> Ok(Exit) + _ -> Error(Nil) + } +} + +pub fn toggle_play_pause(socket: Socket) -> Result(Nil, ControlError) { + let command = + json.object([#("command", json.array(["cycle", "pause"], of: json.string))]) + + case send_command(socket, command) { + Error(r) -> Error(ControlError(reason.to_string(r))) + Ok(_) -> Ok(Nil) + } +} + +// https://mpv.io/manual/master/#command-interface-playback-time +pub type PlaybackTime { + PlaybackTime(data: Float) +} + +pub fn get_playback_time(socket: Socket) -> Result(PlaybackTime, ControlError) { + let command = + json.object([ + #( + "command", + json.array(["get_property_string", "playback-time"], of: json.string), + ), + ]) + + case send_command(socket, command) { + Error(r) -> Error(ControlError(reason.to_string(r))) + Ok(json_string) -> + case internal_control.parse_playback_time(json_string) { + Error(e) -> Error(ControlError(string.inspect(e))) + Ok(data) -> Ok(PlaybackTime(data)) + } + } +} + +fn send_command(socket: Socket, command: json.Json) -> Result(String, Reason) { + result.try(tcp.send(socket, json.to_string(command) <> "\n"), fn(_) { + let timeout_ms = 10_000 + tcp.receive(socket, timeout_ms) + }) +} diff --git a/src/mpv/internal/control.gleam b/src/mpv/internal/control.gleam new file mode 100644 index 0000000..597a695 --- /dev/null +++ b/src/mpv/internal/control.gleam @@ -0,0 +1,25 @@ +import gleam/dynamic/decode +import gleam/float +import gleam/json +import gleam/string + +pub fn parse_playback_time( + json_string: String, +) -> Result(Float, json.DecodeError) { + let decoder = { + let float_dececoder = fn(data_string) { + case float.parse(data_string) { + Error(_) -> decode.failure(0.0, "data") + Ok(float_value) -> decode.success(float_value) + } + } + use data <- decode.field( + "data", + decode.then(decode.string, float_dececoder), + ) + + decode.success(data) + } + + json.parse(from: string.trim(json_string), using: decoder) +} diff --git a/src/mpv/key.gleam b/src/mpv/key.gleam index 1f40752..9ef4a1b 100644 --- a/src/mpv/key.gleam +++ b/src/mpv/key.gleam @@ -43,7 +43,6 @@ pub fn start_raw_shell() { internal_key.shell_start_interactive(#(no_shell, raw)) } -// TODO map key to something like Control, to not leak `Continue` etc. pub fn read_input_until_key(l: List(String)) -> Key { let l = internal_key.read_input() |> list.wrap |> list.append(l, _) diff --git a/src/mpv/mpv.gleam b/src/mpv/mpv.gleam index cb78784..e433703 100644 --- a/src/mpv/mpv.gleam +++ b/src/mpv/mpv.gleam @@ -1,15 +1,14 @@ import gleam/erlang/process.{type Subject} +import gleam/float import gleam/otp/actor +import gleam/result import gleam/string -import mpv/key.{type Key, Char} -import tcp/reason.{type Reason} +import mpv/control.{type Control} +import mpv/key +import tcp/reason import tcp/tcp.{type Socket} -pub type Message { - KeyPress(Key) -} - type State(socket, exit) { State(socket: Socket, exit: Subject(Nil)) } @@ -39,28 +38,41 @@ pub fn new(exit: Subject(Nil)) -> Result(Nil, String) { } } -pub fn toggle_play_pause(socket: Socket) -> Result(Nil, Reason) { - tcp.send(socket, "{\"command\":[\"cycle\",\"pause\"]}\n") -} - fn handle_message( state: State(socket, exit), - message: Message, -) -> actor.Next(State(socket, exit), Message) { - case message { - KeyPress(Char("q")) -> { + control: Control, +) -> actor.Next(State(socket, exit), Control) { + case control { + control.TogglePlayPause -> { + echo "toggling play/pause" + + let _ = + result.map_error(control.toggle_play_pause(state.socket), fn(err) { + echo "Could not toggle play/pause: " <> err.details + }) + + let _ = + result.map(control.get_playback_time(state.socket), fn(playback) { + echo "playback: " <> float.to_string(playback.data) + }) + + actor.continue(state) + } + control.Exit -> { process.send(state.exit, Nil) actor.stop() } - KeyPress(key) -> { - echo "key: " <> string.inspect(key) - actor.continue(state) - } } } -fn read_input(subject: Subject(Message)) -> Nil { - key.read_input_until_key([]) |> KeyPress |> process.send(subject, _) +fn read_input(subject: Subject(Control)) -> Nil { + case + key.read_input_until_key([]) + |> control.from_key + { + Error(_) -> Nil + Ok(control) -> process.send(subject, control) + } read_input(subject) } diff --git a/test/mpv/control_test.gleam b/test/mpv/control_test.gleam new file mode 100644 index 0000000..66cb559 --- /dev/null +++ b/test/mpv/control_test.gleam @@ -0,0 +1,33 @@ +import gleam/list +import gleeunit + +import mpv/control.{type Control} +import mpv/internal/control as control_internal +import mpv/key.{type Key, Char} + +pub fn main() -> Nil { + gleeunit.main() +} + +type TestCase { + TestCase(key: Key, expected: Result(Control, Nil)) +} + +pub fn control_from_key_test() { + let test_cases = [ + TestCase(Char(" "), Ok(control.TogglePlayPause)), + TestCase(Char("q"), Ok(control.Exit)), + ] + + list.each(test_cases, fn(tc) { + assert tc.expected == control.from_key(tc.key) + }) +} + +pub fn parse_playback_time_test() { + let json_string = + "{\"data\":\"123.456789\",\"request_id\":0,\"error\":\"success\"}\n" + + let assert Ok(data) = control_internal.parse_playback_time(json_string) + assert data == 123.456789 +}