require Logger alias Nostrum.Struct alias Thulani.Audio.Util defmodule Thulani.Audio.Server do use GenServer @type which :: String.t() @type audio_ref :: Nostrum.Voice.play_input() def start_link(guild_id) when is_integer(guild_id) do GenServer.start_link(__MODULE__, guild_id, name: {:via, Registry, {Thulani.Audio.Registry, guild_id}} ) end @spec play_now(Struct.Guild.id(), audio_ref, keyword()) :: which def play_now(guild_id, channel, audio_ref, opts \\ []) do dispatch(guild_id, {:now, {channel, audio_ref, opts}}) end @spec enqueue(Struct.Guild.id(), audio_ref, keyword()) :: which def enqueue(guild_id, channel, audio_ref, opts \\ []) do dispatch(guild_id, {:enqueue, {channel, audio_ref, opts}}) end @spec cancel(Struct.Guild.id(), which) :: GenServer.call() def cancel(guild_id, which) do dispatch(guild_id, {:cancel, which}) end @spec stop(Struct.Guild.id()) :: GenServer.call() def stop(guild_id) do dispatch(guild_id, :stop) end #### @impl true def init(guild_id) do Nostrum.Voice.leave_channel(guild_id) schedule_tick() guild = Nostrum.Cache.GuildCache.get!(guild_id) Logger.info("starting voice for guild '#{guild.name}' (#{guild_id})") {:ok, {guild_id, :queue.new()}} end @impl true def handle_call({:now, {channel, ref, opts}}, _from, {guild, q}) do guild_ = Nostrum.Cache.GuildCache.get!(guild) channel_ = guild_.channels[channel] {result, q} = with :ok <- play(guild, channel, ref, opts) do id = UUID.uuid4() q = :queue.in_r({id, channel, ref, opts}, q) Logger.info( "started playback #{id} on '#{guild_.name}'/##{channel_.name}", guild: guild, channel: channel, playback: id ) {{:ok, id}, q} else e -> {e, q} end {:reply, result, {guild, q}} end @impl true def handle_call({:enqueue, {channel, audio_ref, opts}}, from, {guild, q}) do if :queue.is_empty(q) do # start playback immediately if queue is empty handle_call({:now, {channel, audio_ref, opts}}, from, {guild, q}) else id = UUID.uuid4() q = :queue.in({id, channel, audio_ref, opts}, q) guild_ = Nostrum.Cache.GuildCache.get!(guild) channel_ = guild_.channels[channel] Logger.info( "queued playback #{id} for '#{guild_.name}'/##{channel_.name}", guild: guild, channel: channel, playback: id ) {:reply, {:ok, id}, {guild, q}} end end @impl true def handle_call({:cancel, _audio_ref}, _from, {guild, q}) do q = :queue.delete_with(fn _elem -> false end, q) {:reply, :ok, {guild, q}} end @impl true def handle_call(:stop, _from, {guild, _q}) do result = Nostrum.Voice.stop(guild) {:reply, result, {guild, :queue.new()}} end @impl true def handle_info(:tick, {guild, q} = state) do state = if Nostrum.Voice.playing?(guild) do state else q = if not :queue.is_empty(q) do {{:value, {id, _channel, ref, opts}}, q} = :queue.out(q) Logger.info("completed playback #{id}", id: id, ref: ref, opts: opts) q else q end if :queue.is_empty(q) do if not Util.disconnected?(guild) do Logger.debug("no audio in queue, disconnecting from voice") Util.disconnect(guild) Logger.debug("waiting for disconnect") if Util.wait_disconnected(guild) do Logger.debug("disconnected") else Logger.debug("disconnect timed out") end end else {id, channel, ref, opts} = :queue.head(q) Logger.debug("starting queued playback #{id}") play(guild, channel, ref, opts) end {guild, q} end schedule_tick() {:noreply, state} end ### defp tick_interval, do: 100 defp schedule_tick(interval \\ tick_interval()) when is_integer(interval) do Process.send_after(self(), :tick, tick_interval()) end defp play(guild, channel, ref, opts) do {:ok, args} = play_args(ref, opts) if Nostrum.Voice.playing?(guild) do Nostrum.Voice.stop(guild) end :ok = Nostrum.Voice.join_channel(guild, channel, false, true) if Util.wait_ready(guild) do Logger.debug("connected") apply(Nostrum.Voice, :play, [guild | args]) Logger.debug("waiting for playing back") if not Util.wait_playing(guild) do {:error, :playback_timed_out} else Logger.debug("playback started") :ok end else {:error, :voice_unready} end end defp play_args(ref, opts) do with {:ok, type} <- Util.stream_type(ref) do {:ok, [ref, type, opts]} else {:error, _} = err -> err otherwise -> dbg(otherwise) {:error, :unknown} end end defp dispatch(guild_id, args) do ensure(guild_id) call(guild_id, args) end defp call(guild_id, args), do: GenServer.call(address(guild_id), args) defp address(guild_id), do: {:via, Registry, {Thulani.Audio.Registry, guild_id}} defp ensure(guild_id) do case Registry.lookup(Thulani.Audio.Registry, guild_id) do [{pid, _}] when is_pid(pid) -> nil [] -> {:ok, _} = Thulani.Audio.Supervisor.start_child(guild_id) otherwise -> dbg(otherwise) end end end