diff --git a/README.md b/README.md index a3caf23..fd9b357 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ config :mopidy, api_url: System.get_env("MOPIDY_API_URL") ``` +If you want to be able to receive events via websockets you will also need to set `websocket_api_url` + ## Usage The online [documentation][doc] for the Mopidy HTTP API will give you a general @@ -46,6 +48,24 @@ iex> search_results.artists uri: "spotify:artist:4Z8W4fKeB5YxbusRsdQVPb"}] ``` +To receive events: +```elixir +iex> Mopidy.Events.create_stream |> Enum.each(&IO.inspect/1) +{:ok, + %{"event" => "playback_state_changed", "new_state" => "playing", + "old_state" => "paused"}} +{:ok, + %{event: "track_playback_resumed", time_position: 1819, + tl_track: %Mopidy.TlTrack{__model__: "TlTrack", tlid: 658, + track: %Mopidy.Track{__model__: "Track", + album: %Mopidy.Album{__model__: "Album", + name: "It'll End In Tears (Remastered)", + uri: "spotify:album:6D6C7jGsJdzJpcEaMcxswR"}, + artists: [%Mopidy.Artist{__model__: "Artist", name: "This Mortal Coil", + uri: "spotify:artist:5OK8j1JnhoBlivN32G7yOO"}], + name: "Song To The Siren (Remastered)", + uri: "spotify:track:0BPTTsnnfz44XmZn3EE0oo"}}}} +``` ## License MIT License, see [LICENSE](LICENSE) for details. diff --git a/lib/mopidy.ex b/lib/mopidy.ex index 2413c5f..d96d1cd 100644 --- a/lib/mopidy.ex +++ b/lib/mopidy.ex @@ -83,6 +83,49 @@ defmodule Mopidy.Playlist do } end + +defmodule Mopidy.Websocket do + @moduledoc """ + A Websocket connection to Mopidy + """ + defstruct connection: nil + + @type t :: %__MODULE__{ + connection: Socket.Web.t + } + + def new do + %Mopidy.Websocket{connection: connect()} + end + + def receive_next_event(%Mopidy.Websocket{connection: connection} = socket) when not is_nil(connection) do + case Socket.Web.recv(connection) do + {:ok, {:text, data}} -> {[parse_event(data)], socket} + _ -> {:halt, socket} + end + end + + def receive_next_event(%Mopidy.Websocket{connection: nil} = socket) do + {:halt, socket} + end + + def parse_event(data) do + Mopidy.Events.parse_event(Poison.decode!(data)) + end + + defp connect do + %URI{host: host, path: path, port: port} = URI.parse(mopidy_websocket_api_url()) + case Socket.Web.connect {host, port || 80}, path: path || "" do + {:ok, conn} -> conn + _ -> nil + end + end + + defp mopidy_websocket_api_url do + Application.get_env(:mopidy, :websocket_api_url) + end +end + defmodule Mopidy do @moduledoc """ An HTTP client for Mopidy @@ -96,7 +139,7 @@ defmodule Mopidy do @request_timeout 5_000 def start(_type, _args) do - start + start() import Supervisor.Spec, warn: false @@ -111,7 +154,7 @@ defmodule Mopidy do Returns string """ def process_url(endpoint) do - mopidy_api_url <> endpoint + mopidy_api_url() <> endpoint end def process_request_body(body) do @@ -126,8 +169,8 @@ defmodule Mopidy do Set our request headers for every request. """ def process_request_headers(headers) do - Dict.put headers, :"User-Agent", "Mopidy/v1 mopidy-elixir/0.2.0" - Dict.put headers, :"Content-Type", "application/json" + Keyword.put headers, :"User-Agent", "Mopidy/v1 mopidy-elixir/0.2.0" + Keyword.put headers, :"Content-Type", "application/json" end @doc """ @@ -141,7 +184,7 @@ defmodule Mopidy do def api_request(data \\ %{}) do body = Map.merge(%{id: "1", jsonrpc: "2.0"}, data) - with %HTTPotion.Response{body: body} <- Mopidy.post(nil, [body: body, timeout: mopidy_request_timeout]) do + with %HTTPotion.Response{body: body} <- Mopidy.post(nil, [body: body, timeout: mopidy_request_timeout()]) do {:ok, body} else %HTTPotion.ErrorResponse{message: message} -> {:error, message} @@ -172,7 +215,7 @@ defmodule Mopidy do def parse_data(:value, body), do: {:ok, body["value"]} def parse_data(:result, body), do: {:ok, body["result"]} def parse_data(:uri, body), do: {:ok, parse_data(:uri, body["result"], %{})} - def parse_data(data_type, body), do: {:ok, parse_data(data_type, body["result"], [])} + def parse_data(data_type, body), do: {:ok, parse_data(data_type, body["result"], [])} # List parsing def parse_data(_data_type, nil, _accumulator), do: nil diff --git a/lib/mopidy/events.ex b/lib/mopidy/events.ex new file mode 100644 index 0000000..a377124 --- /dev/null +++ b/lib/mopidy/events.ex @@ -0,0 +1,47 @@ +defmodule Mopidy.Events do + @moduledoc """ + Get a stream of Mopidy events + """ + alias Mopidy.{TlTrack, Playlist} + + def create_stream do + Stream.resource( + fn -> Mopidy.Websocket.new end, + fn (socket) -> + Mopidy.Websocket.receive_next_event(socket) + end, + fn _ -> IO.puts("stream ended") end + ) + end + + # Entry point + def parse_event(%{"event" => event} = body), do: {:ok, parse_event(event, body)} + + # Event parsing + def parse_event(event, datum_data) when event in ~w(track_playback_resumed track_playback_paused track_playback_ended) do + %{ + event: event, + time_position: datum_data["time_position"], + tl_track: Mopidy.parse_data(%TlTrack{}, datum_data["tl_track"], %{}) + } + end + + def parse_event("track_playback_started" = event, datum_data) do + %{ + event: event, + tl_track: Mopidy.parse_data(%TlTrack{}, datum_data["tl_track"], %{}) + } + end + + def parse_event("playlist_changed" = event, datum_data) do + %{ + event: event, + playlist: Mopidy.parse_data(%Playlist{}, datum_data["playlist"], %{}) + } + end + + def parse_event(_, datum_data) do + datum_data + end + +end \ No newline at end of file diff --git a/mix.exs b/mix.exs index 52db6f4..2f7098e 100644 --- a/mix.exs +++ b/mix.exs @@ -6,9 +6,9 @@ defmodule Mopidy.Mixfile do app: :mopidy, version: "0.3.0", elixir: ">= 1.3.0", - deps: deps, - description: description, - package: package, + deps: deps(), + description: description(), + package: package(), build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, test_coverage: [tool: ExCoveralls], @@ -34,13 +34,13 @@ defmodule Mopidy.Mixfile do [ {:credo, "~> 0.4", only: [:dev, :test]}, {:dogma, "~> 0.1", only: :dev}, - {:earmark, "~> 0.1", only: :dev}, + {:earmark, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.11", only: :dev}, {:excoveralls, "~> 0.4", only: :test}, - {:httpotion, "~> 3.0.0"}, - {:inch_ex, "~> 0.4", only: :docs}, + {:httpotion, "~> 3.0"}, {:mix_test_watch, "~> 0.2", only: :test}, - {:poison, "~> 2.1"} + {:poison, "~> 3.1"}, + {:socket, "~> 0.3"} ] end diff --git a/mix.lock b/mix.lock index e7b02d9..9abf891 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,23 @@ -%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, - "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, - "credo": {:hex, :credo, "0.4.5", "5c5daaf50a2a96068c0f21b6fbd382d206702efa8836a946eeab0b8ac25f5f22", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, - "dogma": {:hex, :dogma, "0.1.7", "927f76a89a809db96e0983b922fc899f601352690aefa123529b8aa0c45123b2", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, optional: false]}]}, - "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}, - "excoveralls": {:hex, :excoveralls, "0.5.5", "d97b6fc7aa59c5f04f2fa7ec40fc0b7555ceea2a5f7e7c442aad98ddd7f79002", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]}, - "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, - "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, - "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, - "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, - "httpotion": {:hex, :httpotion, "3.0.0", "4ce0af28f4254bdd0457d0fe065a83787751e0a5b5f5d27030948253be38df5c", [:mix], [{:ibrowse, "~> 4.2", [hex: :ibrowse, optional: false]}]}, - "ibrowse": {:hex, :ibrowse, "4.2.2", "b32b5bafcc77b7277eff030ed32e1acc3f610c64e9f6aea19822abcadf681b4b", [:rebar3], []}, - "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, - "inch_ex": {:hex, :inch_ex, "0.5.3", "39f11e96181ab7edc9c508a836b33b5d9a8ec0859f56886852db3d5708889ae7", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, - "jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}, - "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, + "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpotion": {:hex, :httpotion, "3.0.3", "17096ea1a7c0b2df74509e9c15a82b670d66fc4d66e6ef584189f63a9759428d", [:mix], [{:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: false]}], "hexpm"}, + "ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [:rebar3], [], "hexpm"}, + "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "mix_test_watch": {:hex, :mix_test_watch, "0.5.0", "2c322d119a4795c3431380fca2bca5afa4dc07324bd3c0b9f6b2efbdd99f5ed3", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "socket": {:hex, :socket, "0.3.12", "4a6543815136503fee67eff0932da1742fad83f84c49130c854114153cc549a6", [:mix], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, +}