diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7c59f61 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +priv/uploads +deps/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b9b569 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,98 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of +# Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20210902-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.12.3-erlang-24.1.4-debian-bullseye-20210902-slim +# +ARG BUILDER_IMAGE="hexpm/elixir:1.12.3-erlang-24.1.4-debian-bullseye-20210902-slim" +ARG RUNNER_IMAGE="debian:bullseye-20210902-slim" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +# note: if your project uses a tool like https://purgecss.com/, +# which customizes asset compilation based on what it finds in +# your Elixir templates, you will need to move the asset compilation +# step down so that `lib` is available. +COPY assets assets + +# For Phoenix 1.6 and later, compile assets using esbuild +RUN mix assets.deploy + +# For Phoenix versions earlier than 1.6, compile assets npm +# RUN cd assets && yarn install && yarn run webpack --mode production +# RUN mix phx.digest + +# Compile the release +COPY lib lib + +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/prod/rel ./ + +USER nobody + +# Create a symlink to the application directory by extracting the directory name. This is required +# since the release directory will be named after the application, and we don't know that name. +RUN set -eux; \ + ln -nfs /app/$(basename *)/bin/$(basename *) /app/entry + +CMD /app/entry start \ No newline at end of file diff --git a/README.md b/README.md index 427ad23..edbd77b 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,12 @@ Play music together with Phoenix LiveView! Visit [todo]() to try it out, or run locally: * Create a [Github OAuth app](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app) - * Export your GitHub client ID and secret: + - Set the app homepage to `http://localhost:4000` and `Authorization callback URL` to `http://localhost:4000/oauth/callbacks/github` + - After completing the form, click "Generate a new client secret" to obtain your API secret + * Export your GitHub Client ID and secret: - export LIVE_BEATS_GITHUB_CLIENT_ID="..." - export LIVE_BEATS_GITHUB_CLIENT_SECRET="..." + export LIVE_BEATS_GITHUB_CLIENT_ID="..." + export LIVE_BEATS_GITHUB_CLIENT_SECRET="..." * Install dependencies with `mix deps.get` * Create and migrate your database with `mix ecto.setup` diff --git a/assets/js/app.js b/assets/js/app.js index e696062..872b831 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,7 +1,7 @@ import "phoenix_html" import {Socket} from "phoenix" -import {LiveSocket} from "./phoenix_live_view" -// import {LiveSocket} from "phoenix_live_view" +// import {LiveSocket} from "./phoenix_live_view" +import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" let nowSeconds = () => Math.round(Date.now() / 1000) @@ -33,8 +33,10 @@ Hooks.AudioPlayer = { let enableAudio = () => { if(this.player.src){ document.removeEventListener("click", enableAudio) - this.player.play().catch(error => null) - this.player.pause() + if(this.player.readyState === 0){ + this.player.play().catch(error => null) + this.player.pause() + } } } document.addEventListener("click", enableAudio) diff --git a/config/runtime.exs b/config/runtime.exs index 18490b6..76f2616 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -14,9 +14,11 @@ if config_env() == :prod do For example: ecto://USER:PASS@HOST/DATABASE """ + ipv6? = !!System.get_env("IPV6") + config :live_beats, LiveBeats.Repo, # ssl: true, - # socket_options: [:inet6], + socket_options: if(ipv6?, do: [:inet6], else: []), url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") @@ -27,7 +29,11 @@ if config_env() == :prod do You can generate one by calling: mix phx.gen.secret """ + app_name = System.fetch_env!("FLY_APP_NAME") + host = System.get_env("URL_HOST") || "example.com" + config :live_beats, LiveBeatsWeb.Endpoint, + url: [host: host, port: 80], http: [ # Enable IPv6 and bind on all interfaces. # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. @@ -36,33 +42,18 @@ if config_env() == :prod do ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000") ], - secret_key_base: secret_key_base + check_origin: ["//#{host}"], + secret_key_base: secret_key_base, + server: true - # ## Using releases - # - # If you are doing OTP releases, you need to instruct Phoenix - # to start each relevant endpoint: - # - # config :live_beats, LiveBeatsWeb.Endpoint, server: true - # - # Then you can assemble a release by calling `mix release`. - # See `mix help release` for more information. + config :live_beats, :file_host, %{ + scheme: "http", + host: host, + port: 80 + } - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Also, you may need to configure the Swoosh API client of your choice if you - # are not using SMTP. Here is an example of the configuration: - # - # config :live_beats, LiveBeats.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # For this example you need include a HTTP client required by Swoosh API client. - # Swoosh supports Hackney and Finch out of the box: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Hackney - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. + config :live_beats, :github, %{ + client_id: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"), + client_secret: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_SECRET"), + } end diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..5f07bf5 --- /dev/null +++ b/fly.toml @@ -0,0 +1,42 @@ +app = "livebeats" + +kill_signal = "SIGTERM" +kill_timeout = 5 +processes = [] + +[deploy] + release_command = "/app/entry eval LiveBeats.Release.migrate" + +[env] + IPV6 = 1 + URL_HOST = "livebeats.fly.dev" + +[experimental] + allowed_public_ports = [] + auto_rollback = true + +[[services]] + http_checks = [] + internal_port = 4000 + processes = ["app"] + protocol = "tcp" + script_checks = [] + + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "30s" # allow some time for startup + interval = "15s" + restart_limit = 0 + timeout = "2s" \ No newline at end of file diff --git a/lib/live_beats/accounts/user.ex b/lib/live_beats/accounts/user.ex index b61caa9..e6c007f 100644 --- a/lib/live_beats/accounts/user.ex +++ b/lib/live_beats/accounts/user.ex @@ -12,6 +12,8 @@ defmodule LiveBeats.Accounts.User do field :role, :string, default: "subscriber" field :profile_tagline, :string field :active_profile_user_id, :id + field :avatar_url, :string + field :external_homepage_url, :string has_many :identities, Identity @@ -22,7 +24,7 @@ defmodule LiveBeats.Accounts.User do A user changeset for github registration. """ def github_registration_changeset(info, primary_email, emails, token) do - %{"login" => username} = info + %{"login" => username, "avatar_url" => avatar_url, "html_url" => external_homepage_url} = info identity_changeset = Identity.github_registration_changeset(info, primary_email, emails, token) @@ -31,11 +33,13 @@ defmodule LiveBeats.Accounts.User do params = %{ "username" => username, "email" => primary_email, - "name" => get_change(identity_changeset, :provider_name) + "name" => get_change(identity_changeset, :provider_name), + "avatar_url" => avatar_url, + "external_homepage_url" => external_homepage_url } %User{} - |> cast(params, [:email, :name, :username]) + |> cast(params, [:email, :name, :username, :avatar_url, :external_homepage_url]) |> validate_required([:email, :name, :username]) |> validate_username() |> validate_email() diff --git a/lib/live_beats/media_library.ex b/lib/live_beats/media_library.ex index 8d227e2..46fbace 100644 --- a/lib/live_beats/media_library.ex +++ b/lib/live_beats/media_library.ex @@ -203,7 +203,7 @@ defmodule LiveBeats.MediaLibrary do where: s.status in [:playing], limit: ^Keyword.fetch!(opts, :limit), order_by: [desc: s.updated_at], - select: struct(u, [:id, :username, :profile_tagline]) + select: struct(u, [:id, :username, :profile_tagline, :avatar_url, :external_homepage_url]) ) |> Repo.all() |> Enum.map(&get_profile!/1) @@ -214,7 +214,13 @@ defmodule LiveBeats.MediaLibrary do end def get_profile!(%Accounts.User{} = user) do - %Profile{user_id: user.id, username: user.username, tagline: user.profile_tagline} + %Profile{ + user_id: user.id, + username: user.username, + tagline: user.profile_tagline, + avatar_url: user.avatar_url, + external_homepage_url: user.external_homepage_url + } end def owns_profile?(%Accounts.User{} = user, %Profile{} = profile) do diff --git a/lib/live_beats/media_library/profile.ex b/lib/live_beats/media_library/profile.ex index a8a1026..815b2d6 100644 --- a/lib/live_beats/media_library/profile.ex +++ b/lib/live_beats/media_library/profile.ex @@ -1,3 +1,3 @@ defmodule LiveBeats.MediaLibrary.Profile do - defstruct user_id: nil, username: nil, tagline: nil + defstruct user_id: nil, username: nil, tagline: nil, avatar_url: nil, external_homepage_url: nil end diff --git a/lib/live_beats/release.ex b/lib/live_beats/release.ex new file mode 100644 index 0000000..a39eb54 --- /dev/null +++ b/lib/live_beats/release.ex @@ -0,0 +1,28 @@ +defmodule LiveBeats.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :live_beats + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/lib/live_beats_web/channels/presence.ex b/lib/live_beats_web/channels/presence.ex new file mode 100644 index 0000000..299464d --- /dev/null +++ b/lib/live_beats_web/channels/presence.ex @@ -0,0 +1,34 @@ +defmodule LiveBeatsWeb.Presence do + @moduledoc """ + Provides presence tracking to channels and processes. + + See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) + docs for more details. + """ + use Phoenix.Presence, otp_app: :live_beats, + pubsub_server: LiveBeats.PubSub + + import Phoenix.LiveView.Helpers + import LiveBeatsWeb.LiveHelpers + + def listening_now(assigns) do + ~H""" + +
Or - start your 14-day free trial + listen now without signing in (TODO)
diff --git a/lib/live_beats_web/live/song_live/index.ex b/lib/live_beats_web/live/song_live/index.ex index d6c59f6..43d8879 100644 --- a/lib/live_beats_web/live/song_live/index.ex +++ b/lib/live_beats_web/live/song_live/index.ex @@ -2,13 +2,20 @@ defmodule LiveBeatsWeb.SongLive.Index do use LiveBeatsWeb, :live_view alias LiveBeats.{Accounts, MediaLibrary, MP3Stat} - alias LiveBeatsWeb.LayoutComponent + alias LiveBeatsWeb.{LayoutComponent, Presence} alias LiveBeatsWeb.SongLive.{SongRowComponent, UploadFormComponent} def render(assigns) do ~H""" <.title_bar> - <%= @profile.tagline %> <%= if @owns_profile? do %>(you)<% end %> +