Current section
Files
Jump to
Current section
Files
lib/nervoku.ex
defmodule Nervoku do
import SweetXml
@moduledoc """
Documentation for Nervoku.
"""
@keymap %{
home: "Home",
rev: "Rev",
fwd: "Fwd",
play: "Play",
select: "Select",
left: "Left",
right: "Right",
down: "Down",
up: "Up",
back: "Back",
instant_replay: "InstantReplay",
info: "Info",
backspace: "Backspace",
search: "Search",
enter: "Enter",
volume_down: "VolumeDown",
volume_mute: "VolumeMute",
volume_up: "VolumeUp",
power_off: "PowerOff",
channel_up: "ChannelUp",
channel_down: "ChannelDown",
input_tuner: "InputTuner",
input_hdmi1: "InputHDMI1",
input_hdmi2: "InputHDMI2",
input_hdmi3: "InputHDMI3",
input_hdmi4: "InputHDMI4",
input_av1: "InputAV1"
}
@available_keys Map.keys(@keymap)
def key_map(), do: @keymap
@doc """
Lists apps for given roku device
## Examples
iex> Nervoku.list_apps()
[
%{
id: '31012',
name: 'FandangoNOW Movies & TV',
sub_type: nil,
type: 'menu',
version: '1.9.31'
},
%{
id: 'tvinput.hdmi1',
name: [72, 68, 77, 73, 160, 49],
sub_type: nil,
type: 'tvin',
version: '1.0.0'
}
]
"""
def ip(), do: "192.168.86.28"
@doc ~S"""
Lists all available apps on given device.
## Examples
iex> 192.168.86.28 |> Nervoku.list_apps()
[
%{
id: "13535",
name: "Plex",
sub_type: "rsga",
type: "appl",
version: "6.3.13"
},
%{
id: "151908",
name: "The Roku Channel",
sub_type: "rsga",
type: "appl",
version: "2.5.19"
}
]
"""
def list_apps(ip) do
get("#{ip}:8060/query/apps")
|> SweetXml.xpath(
~x"//app"l,
name: ~x"./text()"S,
id: ~x"./@id"S,
type: ~x"./@type"S,
sub_type: ~x"./@subtype"S,
version: ~x"./@version"S
)
end
@doc ~S"""
Tells what device is currently in use.
## Examples
iex> 192.168.86.28 |> Nervoku.active_app()
%{id: "13535", name: "Plex", sub_type: "rsga", type: "appl", version: "6.3.13"}
"""
def active_app(ip) do
get("#{ip}:8060/query/active-app")
|> SweetXml.xpath(
~x"//active-app/app",
id: ~x"./@id"S,
name: ~x"./text()"S,
sub_type: ~x"./@subtype"S,
type: ~x"./@type"S,
version: ~x"./@version"S
)
end
@doc ~S"""
Gets device info for your roku
## Examples
iex> 192.168.86.28 |> Nervoku.device_info()
%{
software_build: "4129",
default_name: "TCL•Roku TV - UHOAIUFHSAF",
time_zone_offset: -240,
ethernet_mac: "23:23:23:41:34:cd",
tuner_type: "ATSC",
network_name: "Network",
screen_size: 49,
network_type: "wifi",
supports_find_remote?: true,
software_version: "9.1.0",
power_mode: "PowerOn",
country: "US",
udn: "818afcbc-8181-81cf-81dd-1919ccff99cc",
language: "en",
wifi_driver: "realtek",
time_zone_name: "United States/Eastern",
serial_number: "UAYA9292SS99",
is_stick?: false,
advertising_id: "818afcbc-8181-81cf-81dd-1919ccff99cc",
panel_id: 32,
time_zone_auto?: true,
headphones_connected?: false,
build_number: "309.10E04129A",
model_region: "US",
friendly_model_name: "TCL•Roku TV",
supports_ethernet?: true,
uptime: 1409333,
vendor_name: "TCL",
is_tv?: true,
clock_format: "12-hour",
user_device_name: "49\" TCL Roku TV",
model_number: "7117X",
time_zone_tz: "America/New_York",
model_name: "49S517",
find_remote_is_possible?: false,
wifi_mac: "1b:7a:6a:41:f1:dc",
friendly_name: "49\" TCL Roku TV",
supports_suspend?: true,
time_zone: "US/Eastern",
device_id: "FSU8C8992112",
secure_device?: true,
locale: "en_US"
}
"""
def device_info(ip) do
get("#{ip}:8060/query/device-info")
|> SweetXml.xpath(
~x"//device-info",
udn: ~x"./udn/text()"S,
serial_number: ~x"./serial-number/text()"S,
device_id: ~x"./device-id/text()"S,
advertising_id: ~x"./advertising-id/text()"S,
vendor_name: ~x"./vendor-name/text()"S,
model_number: ~x"./model-number/text()"S,
model_name: ~x"./model-name/text()"S,
model_region: ~x"./model-region/text()"S,
is_tv?: ~x"./is-tv/text()"S,
is_stick?: ~x"./is-stick/text()"S,
screen_size: ~x"./screen-size/text()"I,
panel_id: ~x"./panel-id/text()"I,
tuner_type: ~x"./tuner-type/text()"S,
supports_ethernet?: ~x"./supports-ethernet/text()"S,
wifi_mac: ~x"./wifi-mac/text()"S,
wifi_driver: ~x"./wifi-driver/text()"S,
ethernet_mac: ~x"./ethernet-mac/text()"S,
network_type: ~x"./network-type/text()"S,
network_name: ~x"./network-name/text()"S,
friendly_name: ~x"./friendly-device-name/text()"S,
friendly_model_name: ~x"./friendly-model-name/text()"S,
default_name: ~x"./default-device-name/text()"S,
user_device_name: ~x"./user-device-name/text()"S,
build_number: ~x"./build-number/text()"S,
software_version: ~x"./software-version/text()"S,
software_build: ~x"./software-build/text()"S,
secure_device?: ~x"./secure-device/text()"S,
language: ~x"./language/text()"S,
country: ~x"./country/text()"S,
locale: ~x"./locale/text()"S,
time_zone_auto?: ~x"./time-zone-auto/text()"S,
time_zone: ~x"./time-zone/text()"S,
time_zone_name: ~x"./time-zone-name/text()"S,
time_zone_tz: ~x"./time-zone-tz/text()"S,
time_zone_offset: ~x"./time-zone-offset/text()"I,
clock_format: ~x"./clock-format/text()"S,
power_mode: ~x"./power-mode/text()"S,
uptime: ~x"./uptime/text()"I,
supports_suspend?: ~x"./supports-suspend/text()"S,
supports_find_remote?: ~x"./supports-find-remote/text()"S,
find_remote_is_possible?: ~x"./find-remote-is-possible/text()"S,
headphones_connected?: ~x"./headphones-connected/text()"S
)
|> Map.update!(
:is_tv?,
&parse_bool/1
)
|> Map.update!(
:is_stick?,
&parse_bool/1
)
|> Map.update!(
:supports_ethernet?,
&parse_bool/1
)
|> Map.update!(
:secure_device?,
&parse_bool/1
)
|> Map.update!(
:time_zone_auto?,
&parse_bool/1
)
|> Map.update!(
:supports_suspend?,
&parse_bool/1
)
|> Map.update!(
:supports_find_remote?,
&parse_bool/1
)
|> Map.update!(
:find_remote_is_possible?,
&parse_bool/1
)
|> Map.update!(
:headphones_connected?,
&parse_bool/1
)
end
@doc ~S"""
Gets available TV channels (if using roku TV)
## Examples
iex> 192.168.86.28 |> Nervoku.tv_channels()
[%{
name: "UNC-EX",
number: 58.3,
physical_channel: 44,
physical_frequency: 653000,
type: "air-digital"
},
%{
name: "NCCHL",
number: 58.4,
physical_channel: 44,
physical_frequency: 653000,
type: "air-digital"
}]
"""
def tv_channels(ip) do
get("#{ip}:8060/query/tv-channels")
|> SweetXml.xpath(
~x"//tv-channels/channel"l,
number: ~x"./number/text()"F,
name: ~x"./name/text()"S,
type: ~x"./type/text()"S,
physical_channel: ~x"./physical-channel/text()"I,
physical_frequency: ~x"./physical-frequency/text()"I
)
end
@doc ~S"""
Presses one of the available keys.
## Examples
iex> 192.168.86.28 |> Nervoku.press_key(:DIN)
{:error, :invalid_key}
iex> 192.168.86.28 |> Nervoku.press_key(:down)
:ok
"""
def press_key(ip, key) when key in @available_keys do
post("#{ip}:8060/keypress/#{@keymap[key]}")
|> case do
"" -> :ok
_ -> {:error, :request_failed}
end
end
def press_key(_ip, _key), do: {:error, :invalid_key}
@doc ~S"""
Launches an app my its app ID
## Examples
iex> 192.168.86.28 |> Nervoku.launch_app("13535")
:ok
iex> 192.168.86.28 |> Nervoku.launch_app("tvinput.dtv")
:ok
"""
def launch_app(ip, app_id) do
post("#{ip}:8060/launch/#{app_id}")
|> case do
"" -> :ok
_ -> :error
end
end
defp parse_bool("true"), do: true
defp parse_bool("false"), do: false
defp get(url) do
{:ok, response} = Mojito.request(:get, "http://#{url}")
response.body
end
defp post(url) do
{:ok, response} = Mojito.request(:post, "http://#{url}")
response.body
end
@doc ~S"""
Checks the network for any roku devices. Will attempt SSDP and if none are found does a manual ping.
If you have a Roku TV it has to do the manual ping because they aren't setup for SSDP.
## Examples
iex> Nervoku.find_rokus()
{:ok, %{location: "192.168.86.28"}}
"""
def find_rokus(base_ip \\ "192.168.86") do
case SSDP.discover(target: SSDP.roku()) do
%{} = m when map_size(m) == 0 ->
batch_find(base_ip)
device ->
device
end
end
defp batch_find(ip) do
Enum.map(1..255, fn i ->
"http://#{ip}.#{i}:8060/query/device-info"
end)
|> Task.async_stream(
fn url ->
case Mojito.request(:get, url) do
{:ok, _response} ->
%{location: URI.parse(url).host}
_response ->
nil
end
end,
max_concurrency: 100,
timeout: 15_000
)
|> Enum.filter(fn {:ok, r} -> not is_nil(r) end)
|> List.first()
end
end