ffmpeg hls playlist implementation

This commit is contained in:
CJ_Clippy 2025-01-28 23:15:42 -08:00
parent 2e4887a5a1
commit 8aa8f231ed
91 changed files with 1388 additions and 2781 deletions

View File

@ -8,7 +8,10 @@
"python310Packages.pip@latest",
"hcloud@latest",
"lazydocker@latest",
"ruby@latest"
"ruby@latest",
"chisel@latest",
"bento4@latest",
"shaka-packager@latest"
],
"env": {
"DEVBOX_COREPACK_ENABLED": "true",
@ -26,6 +29,7 @@
"test": [
"echo \"Error: no test specified\" && exit 1"
],
"tunnel": "dotenvx run -f ./.kamal/secrets.development -- chisel client bright.fp.sbtp.xyz:9090 R:4000",
"backup": "docker exec -t postgres_db pg_dumpall -c -U postgres > ./backups/dev_`date +%Y-%m-%d_%H_%M_%S`.sql"
}
}

View File

@ -1,6 +1,102 @@
{
"lockfile_version": "1",
"packages": {
"bento4@latest": {
"last_modified": "2025-01-25T23:17:58Z",
"resolved": "github:NixOS/nixpkgs/b582bb5b0d7af253b05d58314b85ab8ec46b8d19#bento4",
"source": "devbox-search",
"version": "1.6.0-641",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/c88fmklr5716ksfd30103l5ga96jqydc-bento4-1.6.0-641",
"default": true
}
],
"store_path": "/nix/store/c88fmklr5716ksfd30103l5ga96jqydc-bento4-1.6.0-641"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/dzv9rzqawf9nd529lx0sb6zk6k30bllq-bento4-1.6.0-641",
"default": true
}
],
"store_path": "/nix/store/dzv9rzqawf9nd529lx0sb6zk6k30bllq-bento4-1.6.0-641"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/3w09k9fp9d76a3vh6zmifbssv83ngv5q-bento4-1.6.0-641",
"default": true
}
],
"store_path": "/nix/store/3w09k9fp9d76a3vh6zmifbssv83ngv5q-bento4-1.6.0-641"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/nnflb6279al7r7ad0qrraln3w5brpba0-bento4-1.6.0-641",
"default": true
}
],
"store_path": "/nix/store/nnflb6279al7r7ad0qrraln3w5brpba0-bento4-1.6.0-641"
}
}
},
"chisel@latest": {
"last_modified": "2024-12-23T21:10:33Z",
"resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#chisel",
"source": "devbox-search",
"version": "1.10.1",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/2rls3b9lq2i3g53zpr09d6ph43mgfxwz-chisel-1.10.1",
"default": true
}
],
"store_path": "/nix/store/2rls3b9lq2i3g53zpr09d6ph43mgfxwz-chisel-1.10.1"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/7ff1z4mr0ia2ifdgggpqkbc2j795ccy4-chisel-1.10.1",
"default": true
}
],
"store_path": "/nix/store/7ff1z4mr0ia2ifdgggpqkbc2j795ccy4-chisel-1.10.1"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/2cb5sa449vpah2g4q4prvqfz1dcf1rdw-chisel-1.10.1",
"default": true
}
],
"store_path": "/nix/store/2cb5sa449vpah2g4q4prvqfz1dcf1rdw-chisel-1.10.1"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/pphc5mnhx6mb08ak6mb3rnh061427xbj-chisel-1.10.1",
"default": true
}
],
"store_path": "/nix/store/pphc5mnhx6mb08ak6mb3rnh061427xbj-chisel-1.10.1"
}
}
},
"ffmpeg@latest": {
"last_modified": "2025-01-07T09:15:50Z",
"resolved": "github:NixOS/nixpkgs/8c9fd3e564728e90829ee7dbac6edc972971cd0f#ffmpeg",
@ -517,6 +613,54 @@
}
}
},
"shaka-packager@latest": {
"last_modified": "2025-01-25T23:17:58Z",
"resolved": "github:NixOS/nixpkgs/b582bb5b0d7af253b05d58314b85ab8ec46b8d19#shaka-packager",
"source": "devbox-search",
"version": "3.4.2",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/l0srzffgawm37rnii66r3vbxhh699f7w-shaka-packager-3.4.2",
"default": true
}
],
"store_path": "/nix/store/l0srzffgawm37rnii66r3vbxhh699f7w-shaka-packager-3.4.2"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/40bpzld1ccq4kwjfrrncdj9xqpmrk537-shaka-packager-3.4.2",
"default": true
}
],
"store_path": "/nix/store/40bpzld1ccq4kwjfrrncdj9xqpmrk537-shaka-packager-3.4.2"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/npgp484fhvi2xpfi1f7bpcnxc7a0krq2-shaka-packager-3.4.2",
"default": true
}
],
"store_path": "/nix/store/npgp484fhvi2xpfi1f7bpcnxc7a0krq2-shaka-packager-3.4.2"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/z0k69qksnh4sk4fagmmv7pcwy0sv7kby-shaka-packager-3.4.2",
"default": true
}
],
"store_path": "/nix/store/z0k69qksnh4sk4fagmmv7pcwy0sv7kby-shaka-packager-3.4.2"
}
}
},
"yt-dlp@latest": {
"last_modified": "2025-01-03T14:51:55Z",
"resolved": "github:NixOS/nixpkgs/a27871180d30ebee8aa6b11bf7fef8a52f024733#yt-dlp",

View File

@ -1,58 +1,12 @@
services:
# This service is just here for env var re-use between all the superstreamer-* services.
# IDK if there is a way to do this without an image so we just run alpine which quits right away.
superstreamer:
image: alpine
environment:
- PUBLIC_API_ENDPOINT=http://localhost:52001
- PUBLIC_STITCHER_ENDPOINT=http://localhost:52002
- REDIS_HOST=redis
- REDIS_PORT=6379
- DATABASE_URI=postgres://postgres:password@db:5432/superstreamer
env_file: .kamal/secrets.development
superstreamer-app:
extends: superstreamer
image: "superstreamerapp/app:alpha"
opentracker:
image: anthonyzou/opentracker:latest
ports:
- 52000:52000
superstreamer-api:
extends: superstreamer
image: "superstreamerapp/api:alpha"
restart: always
ports:
- 52001:52001
depends_on:
- db
- redis
superstreamer-stitcher:
extends: superstreamer
image: "superstreamerapp/stitcher:alpha"
restart: always
ports:
- 52002:52002
depends_on:
- redis
superstreamer-artisan:
extends: superstreamer
image: "superstreamerapp/artisan:alpha"
restart: always
depends_on:
- redis
redis:
image: redis/redis-stack-server:7.2.0-v6
ports:
- 127.0.0.1:6379:6379
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
- "6969:6969/tcp"
- "6969:6969/udp"
volumes:
- redis_data:/data
- ./packages/opentracker/opentracker.conf:/etc/opentracker.conf:ro
bright:
container_name: bright

View File

@ -22,7 +22,7 @@ ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
FROM ${BUILDER_IMAGE} AS builder
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git inotify-tools \
RUN apt-get update -y && apt-get install -y build-essential git inotify-tools ffmpeg \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# prepare build dir
@ -43,6 +43,7 @@ RUN mix deps.get --only $MIX_ENV
RUN mkdir config
RUN mkdir contrib
# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
@ -79,6 +80,7 @@ RUN mix release
FROM builder AS dev
COPY ./services/bright/config/test.exs config/test.exs
RUN ls -la ./contrib/
RUN mkdir -p ~/.cache/futureporn
CMD [ "mix", "phx.server" ]
@ -115,4 +117,5 @@ USER nobody
# above and adding an entrypoint. See https://github.com/krallin/tini for details
# ENTRYPOINT ["/tini", "--"]
RUN mkdir -p ~/.config/futureporn
CMD ["/app/bin/server"]

View File

@ -27,6 +27,7 @@ config :bright, BrightWeb.Endpoint,
config :bright, Oban,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.PG,
queues: [default: 10],
repo: Bright.Repo,
plugins: [

View File

@ -31,6 +31,8 @@ config :bright,
public_s3_endpoint: System.get_env("PUBLIC_S3_ENDPOINT"),
s3_cdn_endpoint: System.get_env("PUBLIC_S3_ENDPOINT")
config :bright, :buckets,
media: System.get_env("AWS_BUCKET")
# @see https://elixirforum.com/t/backblaze-and-ex-aws-ex-aws-s3-2-4-3-presign-url-issue/56805
config :ex_aws,

View File

@ -6,4 +6,18 @@ defmodule Bright do
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
@doc """
Looks up `Application` config or raises if keyspace is not configured.
"""
def config([main_key | rest] = keyspace) when is_list(keyspace) do
main = Application.fetch_env!(:bright, main_key)
Enum.reduce(rest, main, fn next_key, current ->
case Keyword.fetch(current, next_key) do
{:ok, val} -> val
:error -> raise ArgumentError, "no config found under #{inspect(keyspace)}"
end
end)
end
end

View File

@ -1,104 +0,0 @@
defmodule Bright.Blog do
@moduledoc """
The Blog context.
"""
import Ecto.Query, warn: false
alias Bright.Repo
alias Bright.Blog.Post
@doc """
Returns the list of posts.
## Examples
iex> list_posts()
[%Post{}, ...]
"""
def list_posts do
Repo.all(Post)
end
@doc """
Gets a single post.
Raises `Ecto.NoResultsError` if the Post does not exist.
## Examples
iex> get_post!(123)
%Post{}
iex> get_post!(456)
** (Ecto.NoResultsError)
"""
def get_post!(id), do: Repo.get!(Post, id)
@doc """
Creates a post.
## Examples
iex> create_post(%{field: value})
{:ok, %Post{}}
iex> create_post(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a post.
## Examples
iex> update_post(post, %{field: new_value})
{:ok, %Post{}}
iex> update_post(post, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a post.
## Examples
iex> delete_post(post)
{:ok, %Post{}}
iex> delete_post(post)
{:error, %Ecto.Changeset{}}
"""
def delete_post(%Post{} = post) do
Repo.delete(post)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking post changes.
## Examples
iex> change_post(post)
%Ecto.Changeset{data: %Post{}}
"""
def change_post(%Post{} = post, attrs \\ %{}) do
Post.changeset(post, attrs)
end
end

View File

@ -1,18 +0,0 @@
defmodule Bright.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :body, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end

View File

@ -10,6 +10,10 @@ defmodule Bright.Cache do
require Logger
def cache_dir do
@cache_dir
end
def generate_basename(input) do
prefix = :crypto.strong_rand_bytes(6) |> Base.encode64(padding: false) |> String.replace(~r/[^a-zA-Z0-9]/, "")
base = Path.basename(input)

View File

@ -1,214 +0,0 @@
defmodule Bright.Catalog do
@moduledoc """
The Catalog context.
"""
import Ecto.Query, warn: false
alias Bright.Repo
alias Bright.Catalog.Product
alias Bright.Catalog.Category
@doc """
Returns the list of products.
## Examples
iex> list_products()
[%Product{}, ...]
"""
def list_products do
Repo.all(Product)
end
@doc """
Gets a single product.
Raises `Ecto.NoResultsError` if the Product does not exist.
## Examples
iex> get_product!(123)
%Product{}
iex> get_product!(456)
** (Ecto.NoResultsError)
"""
def get_product!(id) do
Product
|> Repo.get!(id)
|> Repo.preload(:categories)
end
@doc """
Creates a product.
## Examples
iex> create_product(%{field: value})
{:ok, %Product{}}
iex> create_product(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_product(attrs \\ %{}) do
%Product{}
|> change_product(attrs)
|> Repo.insert()
end
@doc """
Updates a product.
## Examples
iex> update_product(product, %{field: new_value})
{:ok, %Product{}}
iex> update_product(product, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_product(%Product{} = product, attrs) do
product
|> change_product(attrs)
|> Repo.update()
end
@doc """
Deletes a product.
## Examples
iex> delete_product(product)
{:ok, %Product{}}
iex> delete_product(product)
{:error, %Ecto.Changeset{}}
"""
def delete_product(%Product{} = product) do
Repo.delete(product)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking product changes.
## Examples
iex> change_product(product)
%Ecto.Changeset{data: %Product{}}
"""
def change_product(%Product{} = product, attrs \\ %{}) do
categories = list_categories_by_id(attrs["category_ids"])
product
|> Repo.preload(:categories)
|> Product.changeset(attrs)
|> Ecto.Changeset.put_assoc(:categories, categories)
end
@doc """
Returns the list of categories.
## Examples
iex> list_categories()
[%Category{}, ...]
"""
def list_categories do
Repo.all(Category)
end
@doc """
Gets a single category.
Raises `Ecto.NoResultsError` if the Category does not exist.
## Examples
iex> get_category!(123)
%Category{}
iex> get_category!(456)
** (Ecto.NoResultsError)
"""
def get_category!(id), do: Repo.get!(Category, id)
@doc """
Creates a category.
## Examples
iex> create_category(%{field: value})
{:ok, %Category{}}
iex> create_category(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_category(attrs \\ %{}) do
%Category{}
|> Category.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a category.
## Examples
iex> update_category(category, %{field: new_value})
{:ok, %Category{}}
iex> update_category(category, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_category(%Category{} = category, attrs) do
category
|> Category.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a category.
## Examples
iex> delete_category(category)
{:ok, %Category{}}
iex> delete_category(category)
{:error, %Ecto.Changeset{}}
"""
def delete_category(%Category{} = category) do
Repo.delete(category)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking category changes.
## Examples
iex> change_category(category)
%Ecto.Changeset{data: %Category{}}
"""
def change_category(%Category{} = category, attrs \\ %{}) do
Category.changeset(category, attrs)
end
def list_categories_by_id(nil), do: []
def list_categories_by_id(category_ids) do
Repo.all(from c in Category, where: c.id in ^category_ids)
end
end

View File

@ -1,18 +0,0 @@
defmodule Bright.Catalog.Category do
use Ecto.Schema
import Ecto.Changeset
schema "categories" do
field :title, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(category, attrs) do
category
|> cast(attrs, [:title])
|> validate_required([:title])
|> unique_constraint(:title)
end
end

View File

@ -1,23 +0,0 @@
defmodule Bright.Catalog.Product do
use Ecto.Schema
import Ecto.Changeset
alias Bright.Catalog.Category
schema "products" do
field :description, :string
field :title, :string
field :price, :decimal
field :views, :integer
many_to_many :categories, Category, join_through: "product_categories", on_replace: :delete
timestamps(type: :utc_datetime)
end
@doc false
def changeset(product, attrs) do
product
|> cast(attrs, [:title, :description, :price, :views])
|> validate_required([:title, :description, :price])
end
end

View File

@ -5,8 +5,8 @@ defmodule Bright.Downloader do
def get(url) do
filename = Bright.Cache.generate_filename(url)
IO.puts("Downloader getting url=#{url}")
IO.puts("Downloader downloading to filename=#{filename}")
try do
{download!(url, filename), filename}
@ -17,7 +17,8 @@ defmodule Bright.Downloader do
end
# greets https://elixirforum.com/t/how-to-download-big-files/9173/4
defp download!(file_url, filename) do
def download!(file_url, filename) do
IO.puts("Downloader downloading file_url=#{file_url} to filename=#{filename}")
file =
if File.exists?(filename) do
File.open!(filename, [:append])

View File

@ -0,0 +1,23 @@
defmodule Bright.Events do
defmodule ThumbnailsGenerated do
defstruct vod: nil
end
defmodule ProcessingQueued do
defstruct vod: nil
end
defmodule ProcessingProgressed do
defstruct vod: nil, stage: nil, pct: nil
end
defmodule ProcessingCompleted do
defstruct vod: nil, action: nil, url: nil
end
defmodule ProcessingFailed do
defstruct vod: nil, attempt: nil, max_attempts: nil
end
end

View File

@ -30,7 +30,54 @@ defmodule Bright.Images do
end
end
@doc """
get the number of frames in a video.
this is a fallback if get_video_framecount/1 fails
This code is copied from ffmpex, making a slight change to cmd_args because we need to set `-count_frames`.
in ffmpex we aren't able to set cmd_args ourselves
"""
def get_video_framecount_slow(file_path) do
cmd_args = ["-print_format", "json", "-show_streams", "-count_frames", file_path]
{:ok, streams} = case Rambo.run(ffprobe_path(), cmd_args, log: false) do
{:ok, %{out: result}} ->
streams =
result
|> Jason.decode!()
|> Map.get("streams", [])
{:ok, streams}
{:error, %{err: result}} ->
file_error(file_path, result)
end
streams
|> Enum.find(fn stream -> stream["codec_type"] == "video" end)
|> case do
nil -> {:error, "No video stream found"}
video_stream ->
nb_read_frames =
video_stream
|> Map.get("nb_read_frames", %{})
case nb_read_frames do
nil -> {:error, "nb_read_frames not found. (nil)"}
%{} -> {:error, "nb_read_frames not found. (empty map.)"}
nb_read_frames ->
case Integer.parse(nb_read_frames) do
{number, _} -> {:ok, number}
end
end
end
end
def get_video_framecount(file_path) do
IO.puts "get_video_framecount using file_path=#{file_path}"
case FFprobe.streams(file_path) do
{:ok, streams} ->
streams
@ -43,8 +90,8 @@ defmodule Bright.Images do
|> Map.get("nb_frames", %{})
case nb_frames do
nil -> {:error, "nb_frames not found"}
%{} -> {:error, "nb_frames not found. (empty map)"}
nil -> {:error, "nb_frames not found. (nil)"}
%{} -> get_video_framecount_slow(file_path)
nb_frames ->
case Integer.parse(nb_frames) do
{number, _} -> {:ok, number}
@ -56,6 +103,8 @@ defmodule Bright.Images do
end
end
defp gen_thumb(input_file, output_file) do
case get_video_framecount(input_file) do
{:error, reason} -> {:error, reason}
@ -100,7 +149,33 @@ defmodule Bright.Images do
gen_thumb(input_file, output_file)
end
## copied from ffmpex
defp file_error(file_path, error_text) do
cond do
File.exists?(file_path) -> {:error, :invalid_file}
String.contains?(error_text, "Invalid data found when processing input") -> {:error, :invalid_file}
String.contains?(error_text, "404 Not Found") -> {:error, :no_such_file}
true -> {:error, :no_such_file}
end
end
# Read ffprobe path from config. If unspecified, check if `ffprobe` is in env $PATH.
# If it is not, then raise a error.
defp ffprobe_path do
case Application.get_env(:ffmpex, :ffprobe_path, nil) do
nil ->
case System.find_executable("ffprobe") do
nil ->
raise "FFmpeg not installed"
path ->
path
end
path ->
path
end
end

View File

@ -1,3 +0,0 @@
defmodule Bright.Mailer do
use Swoosh.Mailer, otp_app: :bright
end

View File

@ -1,239 +1,70 @@
defmodule Bright.ObanWorkers.CreateHlsPlaylist do
use Oban.Worker, queue: :default, max_attempts: 6
use Oban.Worker, queue: :default, max_attempts: 3
alias Bright.Repo
alias Bright.Streams
alias Bright.Streams.Vod
alias Bright.{
Repo,
Downloader,
B2,
Images,
Cache
}
require Logger
@auth_token Application.get_env(:bright, :superstreamer_auth_token)
@superstreamer_url System.get_env("SUPERSTREAMER_URL")
@public_s3_endpoint Application.get_env(:bright, :s3_cdn_endpoint)
# args: %{"vod_id" => 10, "input_url" => "http://38.242.193.246:8081/fixtures/2024-12-19T03-10-30Z.ts"}
import Ecto.Query, warn: false
@impl Oban.Worker
def perform(%Oban.Job{args: %{"vod_id" => vod_id}}) do
Logger.info(">>>> create_hls_playlist is performing. vod_id=#{vod_id}")
vod = Repo.get!(Vod, vod_id)
def perform(%Oban.Job{args: %{"vod_id" => vod_id}} = job) do
vod = Streams.get_vod!(vod_id)
build_transmuxer(job, vod)
payload = build_payload(vod.origin_temp_input_url)
Logger.info("Starting transcoding for VOD ID #{vod_id}")
with {:ok, transcode_job_id} <- start_transcode(payload),
{:ok, asset_id} <- poll_job_completion(transcode_job_id),
{:ok, package_job_id} <- start_package(asset_id),
{:ok, asset_id} <- poll_job_completion(package_job_id) do
update_vod_with_playlist_url(vod, asset_id)
Logger.info("HLS playlist created and updated for VOD ID #{vod_id}")
else
{:error, reason} ->
Logger.error("Failed to create HLS playlist for VOD ID #{vod_id}: #{inspect(reason)}")
{:error, reason}
end
# IDK how to use liveview, pubsub, etc. so I disabled this.
# ** (ArgumentError) unknown registry: nil. Either the registry name is invalid or the registry is not running, possibly because its application isn't started
# (elixir 1.17.3) lib/registry.ex:1086: Registry.meta/2
# (phoenix_pubsub 2.1.3) lib/phoenix/pubsub.ex:148: Phoenix.PubSub.broadcast/4
# (phoenix_pubsub 2.1.3) lib/phoenix/pubsub.ex:241: Phoenix.PubSub.broadcast!/4
# (bright 0.1.0) lib/bright/oban_workers/create_hls_playlist.ex:45: Bright.ObanWorkers.CreateHlsPlaylist.await_transmuxer/3
# (oban 2.19.0) lib/oban/queue/executor.ex:145: Oban.Queue.Executor.perform/1
# (oban 2.19.0) lib/oban/queue/executor.ex:77: Oban.Queue.Executor.call/1
# (elixir 1.17.3) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
# (elixir 1.17.3) lib/task/supervised.ex:36: Task.Supervised.reply/4
await_transmuxer(vod)
end
defp build_payload(input_url) do
%{
"inputs" => [
%{"type" => "audio", "path" => input_url, "language" => "eng"},
%{"type" => "video", "path" => input_url}
],
"streams" => [
%{"type" => "video", "codec" => "h264", "height" => 1080},
# %{"type" => "video", "codec" => "h264", "height" => 720}, # when I enabled this, I see a superstreamer error? -- "header 'content-length' is listed in signed headers, but is not present "
%{"type" => "video", "codec" => "h264", "height" => 144},
%{"type" => "audio", "codec" => "aac"}
],
"tag" => "create_hls_playlist"
}
end
defp build_transmuxer(job, %Vod{} = vod) do
job_pid = self()
Task.async(fn ->
try do
hls_video =
Streams.transmux_to_hls(vod, fn progress ->
send(job_pid, {:progress, progress})
end)
defp start_transcode(payload) do
Logger.info("Starting transcode with payload: #{inspect(payload)}")
IO.puts "Starting transcode with payload: #{inspect(payload)}"
headers = auth_headers()
Logger.info("auth headers as follows")
Logger.info(inspect(headers))
Logger.info("@superstreamer_url=#{@superstreamer_url}")
if is_nil(@superstreamer_url) do
Logger.error("The @superstreamer_url is nil. This must be set before proceeding.")
raise "The @superstreamer_url is not configured."
end
Logger.info("now we will POST /transcode to superstreamer_url=#{@superstreamer_url}")
data = case HTTPoison.post("#{@superstreamer_url}/transcode", Jason.encode!(payload), headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
[] ->
{:error, "We got an empty response from Superstreamer"}
failed ->
Logger.error("Failed to POST /transcode: #{inspect(failed)}")
{:error, :failed}
end
Logger.info("we got some data as follows. #{inspect(data)}")
formatted = case data do
{:ok, %{"jobId" => transcode_job_id}} ->
{:ok, transcode_job_id}
end
Logger.info("start_transcode has finished it's duties and is returning the following formatted data.")
Logger.info(inspect(formatted))
formatted
end
defp start_package(asset_id) do
payload = %{
"assetId" => asset_id,
"concurrency" => 1,
"public" => false
}
Logger.info("Starting packaging for asset ID #{asset_id}")
headers = auth_headers()
Logger.info("auth headers as follows")
Logger.info(inspect(headers))
Logger.info("now we will POST /package to superstreamer_url=#{@superstreamer_url}")
data = case HTTPoison.post("#{@superstreamer_url}/package", Jason.encode!(payload), headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
failed ->
Logger.error("Failed to POST /package: #{inspect(failed)}")
{:error, :failed}
end
Logger.info("we got some data as follows. #{inspect(data)}")
formatted = case data do
{:ok, %{"jobId" => package_job_id}} ->
{:ok, package_job_id}
end
Logger.info("start_package has finished it's duties and is returning the following formatted data.")
Logger.info(inspect(formatted))
formatted
end
defp poll_job_completion(job_id) do
Logger.info("Polling job completion for Job ID #{job_id}")
poll_interval = 5_000
max_retries = 999
Enum.reduce_while(1..max_retries, :ok, fn _, acc ->
case get_job_status(job_id) do
{:ok, "completed", data} ->
Logger.info("Job ID #{job_id} completed successfully")
Logger.info("here we need to return {:ok, asset_id}")
Logger.info(inspect(data))
formatted = case data do
{:ok, %{"outputData" => outputData}} ->
case Jason.decode(outputData) do
{:ok, decoded} -> decoded
{:error, reason} ->
Logger.error("Failed to decode outputData: #{inspect(reason)}")
%{}
end
end
Logger.info(">>>> formatted=#{inspect(formatted)}")
{:halt, {:ok, formatted["assetId"]}}
{:ok, "failed", _data} ->
{:halt, {:error, "superstreamer reports that the job failed."}}
{:ok, state, data} ->
Logger.info("Job ID #{job_id} #{state}. Re-polling in #{poll_interval}.")
:timer.sleep(poll_interval)
{:cont, acc}
{:error, reason} ->
Logger.error("Error polling job ID #{job_id}: #{inspect(reason)}")
{:halt, {:error, reason}}
send(job_pid, {:complete, hls_video})
rescue
e ->
send(job_pid, {:error, e, job})
reraise e, __STACKTRACE__
end
end)
end
defp get_job_status(job_id) do
headers = auth_headers()
defp await_transmuxer(vod, stage \\ :retrieving, done \\ 0) do
receive do
{:progress, %{stage: stage_now, done: done_now, total: total}} ->
Streams.broadcast_processing_progressed!(stage, vod, min(1, done / total))
done_total = if(stage == stage_now, do: done, else: 0)
await_transmuxer(vod, stage_now, done_total + done_now)
data = case HTTPoison.get("#{@superstreamer_url}/jobs/#{job_id}", headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:complete, vod} ->
Streams.broadcast_processing_progressed!(stage, vod, 1)
Streams.broadcast_processing_completed!(:upload, vod, vod.url)
{:ok, vod.url}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
failed ->
Logger.error("Failed to GET /jobs/<job_id>: #{inspect(failed)}")
{:error, :failed}
{:error, e, %Oban.Job{attempt: attempt, max_attempts: max_attempts}} ->
Streams.broadcast_processing_failed!(vod, attempt, max_attempts)
{:error, e}
end
status = case data do
{:ok, %{"state" => state}} ->
{:ok, state, data}
end
Logger.info("job #{job_id} status=#{inspect(status)}")
status
end
defp update_vod_with_playlist_url(vod, asset_id) do
playlist_url = generate_playlist_url(asset_id)
Logger.info("playlist_url=#{playlist_url}")
vod
|> Ecto.Changeset.change(playlist_url: playlist_url)
|> Repo.update!()
end
defp generate_playlist_url(asset_id), do: "#{@public_s3_endpoint}/package/#{asset_id}/hls/master.m3u8"
defp auth_headers do
[
{"authorization", "Bearer #{@auth_token}"},
{"content-type", "application/json"}
]
end
end

View File

@ -0,0 +1,238 @@
defmodule Bright.ObanWorkers.CreateHlsPlaylist do
use Oban.Worker, queue: :default, max_attempts: 6
alias Bright.Repo
alias Bright.Streams.Vod
require Logger
# args: %{"vod_id" => 10, "input_url" => "http://38.242.193.246:8081/fixtures/2024-12-19T03-10-30Z.ts"}
@impl Oban.Worker
def perform(%Oban.Job{args: %{"vod_id" => vod_id}}) do
Application.get_env(:bright, :superstreamer_url) || raise("superstreamer_url missing from app config")
Logger.info(">>>> create_hls_playlist is performing. vod_id=#{vod_id}")
vod = Repo.get!(Vod, vod_id)
payload = build_payload(vod.origin_temp_input_url)
Logger.info("Starting transcoding for VOD ID #{vod_id}")
with {:ok, transcode_job_id} <- start_transcode(payload),
{:ok, asset_id} <- poll_job_completion(transcode_job_id),
{:ok, package_job_id} <- start_package(asset_id),
{:ok, asset_id} <- poll_job_completion(package_job_id) do
update_vod_with_playlist_url(vod, asset_id)
Logger.info("HLS playlist created and updated for VOD ID #{vod_id}")
else
{:error, reason} ->
Logger.error("Failed to create HLS playlist for VOD ID #{vod_id}: #{inspect(reason)}")
{:error, reason}
end
end
defp build_payload(input_url) do
%{
"inputs" => [
%{"type" => "audio", "path" => input_url, "language" => "eng"},
%{"type" => "video", "path" => input_url}
],
"streams" => [
%{"type" => "video", "codec" => "h264", "height" => 1080},
# %{"type" => "video", "codec" => "h264", "height" => 720}, # when I enabled this, I see a superstreamer error? -- "header 'content-length' is listed in signed headers, but is not present "
%{"type" => "video", "codec" => "h264", "height" => 144},
%{"type" => "audio", "codec" => "aac"}
],
"tag" => "create_hls_playlist"
}
end
defp start_transcode(payload) do
Logger.info("Starting transcode with payload: #{inspect(payload)}")
IO.puts "Starting transcode with payload: #{inspect(payload)}"
headers = auth_headers()
Logger.info("auth headers as follows")
Logger.info(inspect(headers))
superstreamer_url = Application.get_env(:bright, :superstreamer_url)
Logger.info("now we will POST /transcode to superstreamer_url=#{superstreamer_url}")
data = case HTTPoison.post("#{superstreamer_url}/transcode", Jason.encode!(payload), headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
[] ->
{:error, "We got an empty response from Superstreamer"}
failed ->
Logger.error("Failed to POST /transcode: #{inspect(failed)}")
{:error, :failed}
end
Logger.info("we got some data as follows. #{inspect(data)}")
formatted = case data do
{:ok, %{"jobId" => transcode_job_id}} ->
{:ok, transcode_job_id}
end
Logger.info("start_transcode has finished it's duties and is returning the following formatted data.")
Logger.info(inspect(formatted))
formatted
end
defp start_package(asset_id) do
superstreamer_url = Application.get_env(:bright, :superstreamer_url)
payload = %{
"assetId" => asset_id,
"concurrency" => 1,
"public" => false
}
Logger.info("Starting packaging for asset ID #{asset_id}")
headers = auth_headers()
Logger.info("auth headers as follows")
Logger.info(inspect(headers))
Logger.info("now we will POST /package to superstreamer_url=#{superstreamer_url}")
data = case HTTPoison.post("#{superstreamer_url}/package", Jason.encode!(payload), headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
failed ->
Logger.error("Failed to POST /package: #{inspect(failed)}")
{:error, :failed}
end
Logger.info("we got some data as follows. #{inspect(data)}")
formatted = case data do
{:ok, %{"jobId" => package_job_id}} ->
{:ok, package_job_id}
end
Logger.info("start_package has finished it's duties and is returning the following formatted data.")
Logger.info(inspect(formatted))
formatted
end
defp poll_job_completion(job_id) do
Logger.info("Polling job completion for Job ID #{job_id}")
poll_interval = 5_000
max_retries = 999
Enum.reduce_while(1..max_retries, :ok, fn _, acc ->
case get_job_status(job_id) do
{:ok, "completed", data} ->
Logger.info("Job ID #{job_id} completed successfully")
Logger.info("here we need to return {:ok, asset_id}")
Logger.info(inspect(data))
formatted = case data do
{:ok, %{"outputData" => outputData}} ->
case Jason.decode(outputData) do
{:ok, decoded} -> decoded
{:error, reason} ->
Logger.error("Failed to decode outputData: #{inspect(reason)}")
%{}
end
end
Logger.info(">>>> formatted=#{inspect(formatted)}")
{:halt, {:ok, formatted["assetId"]}}
{:ok, "failed", _data} ->
{:halt, {:error, "superstreamer reports that the job failed."}}
{:ok, state, data} ->
Logger.info("Job ID #{job_id} #{state}. Re-polling in #{poll_interval}.")
:timer.sleep(poll_interval)
{:cont, acc}
{:error, reason} ->
Logger.error("Error polling job ID #{job_id}: #{inspect(reason)}")
{:halt, {:error, reason}}
end
end)
end
defp get_job_status(job_id) do
headers = auth_headers()
data = case HTTPoison.get("#{Application.get_env(:bright, :superstreamer_url)}/jobs/#{job_id}", headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
{:error, %{status: status, body: body}}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
failed ->
Logger.error("Failed to GET /jobs/<job_id>: #{inspect(failed)}")
{:error, :failed}
end
status = case data do
{:ok, %{"state" => state}} ->
{:ok, state, data}
end
Logger.info("job #{job_id} status=#{inspect(status)}")
status
end
defp update_vod_with_playlist_url(vod, asset_id) do
playlist_url = generate_playlist_url(asset_id)
Logger.info("playlist_url=#{playlist_url}")
vod
|> Ecto.Changeset.change(playlist_url: playlist_url)
|> Repo.update!()
end
defp generate_playlist_url(asset_id) do
public_s3_endpoint = Application.get_env(:bright, :public_s3_endpoint) || raise("public_s3_endpoint was nil")
"#{public_s3_endpoint}/package/#{asset_id}/hls/master.m3u8"
end
defp auth_headers do
superstreamer_auth_token = Application.get_env(:bright, :superstreamer_auth_token) || raise("superstreamer_auth_token was nil")
[
{"authorization", "Bearer #{superstreamer_auth_token}"},
{"content-type", "application/json"}
]
end
end

View File

@ -28,7 +28,6 @@ defmodule Bright.ObanWorkers.CreateThumbnail do
{:ok, %{output: output, filename: output_file}} <- Images.create_thumbnail(local_filename),
{:ok, s3Asset} <- B2.put(output_file)
do
IO.puts("updating vod ...")
update_vod_with_thumbnail_url(vod, s3Asset.cdn_url)
else
{:error, reason} ->
@ -41,16 +40,8 @@ defmodule Bright.ObanWorkers.CreateThumbnail do
defp generate_thumbnail_url(basename), do: "#{@public_s3_endpoint}/#{basename}"
# defp update_vod_with_thumbnail_url(vod, thumbnail_url) do
# IO.puts "thumbnail_url=#{thumbnail_url}"
# vod
# |> Ecto.Changeset.change(thumbnail_url: thumbnail_url)
# |> Repo.update!()
# end
defp update_vod_with_thumbnail_url(vod, thumbnail_url) do
IO.puts "thumbnail_url=#{thumbnail_url}"
case Repo.update(vod |> Ecto.Changeset.change(thumbnail_url: thumbnail_url)) do
{:ok, updated_vod} -> {:ok, updated_vod}
{:error, changeset} -> {:error, changeset}

View File

@ -1,231 +0,0 @@
defmodule Bright.Orders do
@moduledoc """
The Orders context.
"""
import Ecto.Query, warn: false
alias Bright.Repo
alias Bright.Orders.{Order,LineItem}
alias Bright.ShoppingCart
@doc """
Returns the list of orders.
## Examples
iex> list_orders()
[%Order{}, ...]
"""
def list_orders do
Repo.all(Order)
end
@doc """
Gets a single order.
Raises `Ecto.NoResultsError` if the Order does not exist.
## Examples
iex> get_order!(123)
%Order{}
iex> get_order!(456)
** (Ecto.NoResultsError)
"""
# def get_order!(id), do: Repo.get!(Order, id)
def get_order!(user_uuid, id) do
Order
|> Repo.get_by!(id: id, user_uuid: user_uuid)
|> Repo.preload([line_items: [:product]])
end
@doc """
Creates a order.
## Examples
iex> create_order(%{field: value})
{:ok, %Order{}}
iex> create_order(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_order(attrs \\ %{}) do
%Order{}
|> Order.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a order.
## Examples
iex> update_order(order, %{field: new_value})
{:ok, %Order{}}
iex> update_order(order, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_order(%Order{} = order, attrs) do
order
|> Order.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a order.
## Examples
iex> delete_order(order)
{:ok, %Order{}}
iex> delete_order(order)
{:error, %Ecto.Changeset{}}
"""
def delete_order(%Order{} = order) do
Repo.delete(order)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking order changes.
## Examples
iex> change_order(order)
%Ecto.Changeset{data: %Order{}}
"""
def change_order(%Order{} = order, attrs \\ %{}) do
Order.changeset(order, attrs)
end
alias Bright.Orders.LineItem
@doc """
Returns the list of order_line_items.
## Examples
iex> list_order_line_items()
[%LineItem{}, ...]
"""
def list_order_line_items do
Repo.all(LineItem)
end
@doc """
Gets a single line_item.
Raises `Ecto.NoResultsError` if the Line item does not exist.
## Examples
iex> get_line_item!(123)
%LineItem{}
iex> get_line_item!(456)
** (Ecto.NoResultsError)
"""
def get_line_item!(id), do: Repo.get!(LineItem, id)
@doc """
Creates a line_item.
## Examples
iex> create_line_item(%{field: value})
{:ok, %LineItem{}}
iex> create_line_item(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_line_item(attrs \\ %{}) do
%LineItem{}
|> LineItem.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a line_item.
## Examples
iex> update_line_item(line_item, %{field: new_value})
{:ok, %LineItem{}}
iex> update_line_item(line_item, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_line_item(%LineItem{} = line_item, attrs) do
line_item
|> LineItem.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a line_item.
## Examples
iex> delete_line_item(line_item)
{:ok, %LineItem{}}
iex> delete_line_item(line_item)
{:error, %Ecto.Changeset{}}
"""
def delete_line_item(%LineItem{} = line_item) do
Repo.delete(line_item)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking line_item changes.
## Examples
iex> change_line_item(line_item)
%Ecto.Changeset{data: %LineItem{}}
"""
def change_line_item(%LineItem{} = line_item, attrs \\ %{}) do
LineItem.changeset(line_item, attrs)
end
def complete_order(%ShoppingCart.Cart{} = cart) do
line_items =
Enum.map(cart.items, fn item ->
%{product_id: item.product_id, price: item.product.price, quantity: item.quantity}
end)
order =
Ecto.Changeset.change(%Order{},
user_uuid: cart.user_uuid,
total_price: ShoppingCart.total_cart_price(cart),
line_items: line_items
)
Ecto.Multi.new()
|> Ecto.Multi.insert(:order, order)
|> Ecto.Multi.run(:prune_cart, fn _repo, _changes ->
ShoppingCart.prune_cart_items(cart)
end)
|> Repo.transaction()
|> case do
{:ok, %{order: order}} -> {:ok, order}
{:error, name, value, _changes_so_far} -> {:error, {name, value}}
end
end
end

View File

@ -1,21 +0,0 @@
defmodule Bright.Orders.LineItem do
use Ecto.Schema
import Ecto.Changeset
schema "order_line_items" do
field :price, :decimal
field :quantity, :integer
belongs_to :order, Bright.Orders.Order
belongs_to :product, Bright.Catalog.Product
timestamps(type: :utc_datetime)
end
@doc false
def changeset(line_item, attrs) do
line_item
|> cast(attrs, [:price, :quantity])
|> validate_required([:price, :quantity])
end
end

View File

@ -1,21 +0,0 @@
defmodule Bright.Orders.Order do
use Ecto.Schema
import Ecto.Changeset
schema "orders" do
field :user_uuid, Ecto.UUID
field :total_price, :decimal
has_many :line_items, Bright.Orders.LineItem
has_many :products, through: [:line_items, :product]
timestamps(type: :utc_datetime)
end
@doc false
def changeset(order, attrs) do
order
|> cast(attrs, [:user_uuid, :total_price])
|> validate_required([:user_uuid, :total_price])
end
end

View File

@ -1,272 +0,0 @@
defmodule Bright.ShoppingCart do
@moduledoc """
The ShoppingCart context.
"""
import Ecto.Query, warn: false
alias Bright.Repo
alias Bright.Catalog
alias Bright.ShoppingCart.{Cart, CartItem}
@doc """
Returns the list of carts.
## Examples
iex> list_carts()
[%Cart{}, ...]
"""
def list_carts do
Repo.all(Cart)
end
@doc """
Gets a single cart.
Raises `Ecto.NoResultsError` if the Cart does not exist.
## Examples
iex> get_cart!(123)
%Cart{}
iex> get_cart!(456)
** (Ecto.NoResultsError)
"""
def get_cart!(id), do: Repo.get!(Cart, id)
@doc """
Creates a cart.
## Examples
iex> create_cart(%{field: value})
{:ok, %Cart{}}
iex> create_cart(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_cart(user_uuid) do
%Cart{user_uuid: user_uuid}
|> Cart.changeset(%{})
|> Repo.insert()
|> case do
{:ok, cart} -> {:ok, reload_cart(cart)}
{:error, changeset} -> {:error, changeset}
end
end
def reload_cart(%Cart{} = cart), do: get_cart_by_user_uuid(cart.user_uuid)
def add_item_to_cart(%Cart{} = cart, product_id) do
product = Catalog.get_product!(product_id)
%CartItem{quantity: 1, price_when_carted: product.price}
|> CartItem.changeset(%{})
|> Ecto.Changeset.put_assoc(:cart, cart)
|> Ecto.Changeset.put_assoc(:product, product)
|> Repo.insert(
on_conflict: [inc: [quantity: 1]],
conflict_target: [:cart_id, :product_id]
)
end
def remove_item_from_cart(%Cart{} = cart, product_id) do
{1, _} =
Repo.delete_all(
from(i in CartItem,
where: i.cart_id == ^cart.id,
where: i.product_id == ^product_id
)
)
{:ok, reload_cart(cart)}
end
@doc """
Updates a cart.
## Examples
iex> update_cart(cart, %{field: new_value})
{:ok, %Cart{}}
iex> update_cart(cart, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_cart(%Cart{} = cart, attrs) do
changeset =
cart
|> Cart.changeset(attrs)
|> Ecto.Changeset.cast_assoc(:items, with: &CartItem.changeset/2)
Ecto.Multi.new()
|> Ecto.Multi.update(:cart, changeset)
|> Ecto.Multi.delete_all(:discarded_items, fn %{cart: cart} ->
from(i in CartItem, where: i.cart_id == ^cart.id and i.quantity == 0)
end)
|> Repo.transaction()
|> case do
{:ok, %{cart: cart}} -> {:ok, cart}
{:error, :cart, changeset, _changes_so_far} -> {:error, changeset}
end
end
@doc """
Deletes a cart.
## Examples
iex> delete_cart(cart)
{:ok, %Cart{}}
iex> delete_cart(cart)
{:error, %Ecto.Changeset{}}
"""
def delete_cart(%Cart{} = cart) do
Repo.delete(cart)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking cart changes.
## Examples
iex> change_cart(cart)
%Ecto.Changeset{data: %Cart{}}
"""
def change_cart(%Cart{} = cart, attrs \\ %{}) do
Cart.changeset(cart, attrs)
end
alias Bright.ShoppingCart.CartItem
@doc """
Returns the list of cart_items.
## Examples
iex> list_cart_items()
[%CartItem{}, ...]
"""
def list_cart_items do
Repo.all(CartItem)
end
@doc """
Gets a single cart_item.
Raises `Ecto.NoResultsError` if the Cart item does not exist.
## Examples
iex> get_cart_item!(123)
%CartItem{}
iex> get_cart_item!(456)
** (Ecto.NoResultsError)
"""
def get_cart_item!(id), do: Repo.get!(CartItem, id)
@doc """
Creates a cart_item.
## Examples
iex> create_cart_item(%{field: value})
{:ok, %CartItem{}}
iex> create_cart_item(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_cart_item(attrs \\ %{}) do
%CartItem{}
|> CartItem.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a cart_item.
## Examples
iex> update_cart_item(cart_item, %{field: new_value})
{:ok, %CartItem{}}
iex> update_cart_item(cart_item, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_cart_item(%CartItem{} = cart_item, attrs) do
cart_item
|> CartItem.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a cart_item.
## Examples
iex> delete_cart_item(cart_item)
{:ok, %CartItem{}}
iex> delete_cart_item(cart_item)
{:error, %Ecto.Changeset{}}
"""
def delete_cart_item(%CartItem{} = cart_item) do
Repo.delete(cart_item)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking cart_item changes.
## Examples
iex> change_cart_item(cart_item)
%Ecto.Changeset{data: %CartItem{}}
"""
def change_cart_item(%CartItem{} = cart_item, attrs \\ %{}) do
CartItem.changeset(cart_item, attrs)
end
def get_cart_by_user_uuid(user_uuid) do
Repo.one(
from(c in Cart,
where: c.user_uuid == ^user_uuid,
left_join: i in assoc(c, :items),
left_join: p in assoc(i, :product),
order_by: [asc: i.inserted_at],
preload: [items: {i, product: p}]
)
)
end
def total_item_price(%CartItem{} = item) do
Decimal.mult(item.product.price, item.quantity)
end
def total_cart_price(%Cart{} = cart) do
Enum.reduce(cart.items, 0, fn item, acc ->
item
|> total_item_price()
|> Decimal.add(acc)
end)
end
def prune_cart_items(%Cart{} = cart) do
{_, _} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id))
{:ok, reload_cart(cart)}
end
end

View File

@ -0,0 +1,82 @@
defmodule Bright.Storage do
def endpoint_url do
%{scheme: scheme, host: host} = Application.fetch_env!(:ex_aws, :s3) |> Enum.into(%{})
"#{scheme}#{host}"
end
def bucket(), do: Bright.config([:buckets, :media])
def to_absolute(type, uuid, uri) do
if URI.parse(uri).scheme do
uri
else
to_absolute_uri(type, uuid, uri)
end
end
defp to_absolute_uri(:video, uuid, uri),
do: "#{endpoint_url()}/#{bucket()}/#{uuid}/#{uri}"
defp to_absolute_uri(:clip, uuid, uri),
do: "#{endpoint_url()}/#{bucket()}/clips/#{uuid}/#{uri}"
def upload_to_bucket(contents, remote_path, bucket, opts \\ []) do
op = Bright.config([:buckets, bucket]) |> ExAws.S3.put_object(remote_path, contents, opts)
ExAws.request(op, [])
end
def upload_from_filename_to_bucket(
local_path,
remote_path,
bucket,
cb \\ fn _ -> nil end,
opts \\ []
) do
%{size: size} = File.stat!(local_path)
chunk_size = 5 * 1024 * 1024
ExAws.S3.Upload.stream_file(local_path, [{:chunk_size, chunk_size}])
|> Stream.map(fn chunk ->
cb.(%{stage: :persisting, done: chunk_size, total: size})
chunk
end)
|> ExAws.S3.upload(Bright.config([:buckets, bucket]), remote_path, opts)
|> ExAws.request([])
end
def upload(contents, remote_path, opts \\ []) do
upload_to_bucket(contents, remote_path, :media, opts)
end
def upload_from_filename(local_path, remote_path, cb \\ fn _ -> nil end, opts \\ []) do
upload_from_filename_to_bucket(
local_path,
remote_path,
:media,
cb,
opts
)
end
def update_object!(bucket, object, opts) do
bucket = Bright.config([:buckets, bucket])
with {:ok, %{body: body}} <- ExAws.S3.get_object(bucket, object) |> ExAws.request(),
{:ok, res} <- ExAws.S3.put_object(bucket, object, body, opts) |> ExAws.request() do
res
else
err -> err
end
end
def remove(remote_path, opts \\ []) do
remove_from_bucket(remote_path, :media, opts)
end
def remove_from_bucket(remote_path, bucket, opts) do
ExAws.S3.delete_object(Bright.config([:buckets, bucket]), remote_path, opts)
|> ExAws.request([])
end
end

View File

@ -11,6 +11,13 @@ defmodule Bright.Streams do
alias Bright.Vtubers.Vtuber
alias Bright.Tags.Tag
alias Bright.Platforms.Platform
alias Bright.{
Cache,
Events,
Downloader,
Storage,
}
@doc """
Returns the list of streams.
@ -271,4 +278,175 @@ defmodule Bright.Streams do
def change_vod(%Vod{} = vod, attrs \\ %{}) do
Vod.changeset(vod, attrs)
end
def transmux_to_hls(%Vod{} = vod, cb) do
if !vod.origin_temp_input_url, do: raise("vod was missing origin_temp_input_url")
local_path = Cache.generate_filename(vod.origin_temp_input_url)
Downloader.download!(vod.origin_temp_input_url, local_path)
IO.puts "transmuxing to hls using origin_temp_input_url=#{vod.origin_temp_input_url}, local_path=#{local_path}"
master_pl_name = "master.m3u8"
dir_name = "vod-#{vod.id}"
dir = Path.join(Bright.Cache.cache_dir, dir_name)
File.mkdir_p!(dir)
cb.(%{stage: :transmuxing, done: 1, total: 1})
# @see https://www.mux.com/articles/how-to-convert-mp4-to-hls-format-with-ffmpeg-a-step-by-step-guide#when-to-use-hls-over-mp4-formats-whats-the-difference
# ffmpeg -i input_video.mp4 \
# -filter_complex \
# "[0:v]split=3[v1][v2][v3]; \
# [v1]scale=w=1920:h=1080[v1out]; \
# [v2]scale=w=1280:h=720[v2out]; \
# [v3]scale=w=854:h=480[v3out]" \
# -map "[v1out]" -c:v:0 libx264 -b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
# -map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k \
# -map "[v3out]" -c:v:2 libx264 -b:v:2 1400k -maxrate:v:2 1498k -bufsize:v:2 2100k \
# -map a:0 -c:a aac -b:a:0 192k -ac 2 \
# -map a:0 -c:a aac -b:a:1 128k -ac 2 \
# -map a:0 -c:a aac -b:a:2 96k -ac 2 \
# -f hls \
# -hls_time 10 \
# -hls_playlist_type vod \
# -hls_flags independent_segments \
# -hls_segment_type mpegts \
# -hls_segment_filename stream_%v/data%03d.ts \
# -master_pl_name master.m3u8 \
# -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
# stream_%v/playlist.m3u8
System.cmd("ffmpeg", [
"-i",
local_path,
"-filter_complex",
"[0:v]split=5[v1][v2][v3][v4][v5];" <>
"[v1]scale=w=1920:h=1080[v1out];" <>
"[v2]scale=w=1280:h=720[v2out];" <>
"[v3]scale=w=854:h=480[v3out];" <>
"[v4]scale=w=640:h=360[v4out];" <>
"[v5]scale=w=284:h=160[v5out]",
# Video streams
"-map", "[v1out]", "-c:v:0", "libx264", "-b:v:0", "5000k", "-maxrate:v:0", "5350k", "-bufsize:v:0", "7500k",
"-map", "[v2out]", "-c:v:1", "libx264", "-b:v:1", "2800k", "-maxrate:v:1", "2996k", "-bufsize:v:1", "4200k",
"-map", "[v3out]", "-c:v:2", "libx264", "-b:v:2", "1400k", "-maxrate:v:2", "1498k", "-bufsize:v:2", "2100k",
"-map", "[v4out]", "-c:v:3", "libx264", "-b:v:3", "800k", "-maxrate:v:3", "856k", "-bufsize:v:3", "1200k",
"-map", "[v5out]", "-c:v:4", "libx264", "-b:v:4", "300k", "-maxrate:v:4", "300k", "-bufsize:v:4", "480k",
# Audio streams
"-map", "a:0", "-c:a:0", "aac", "-b:a:0", "192k", "-ac:a:0", "2",
"-map", "a:0", "-c:a:1", "aac", "-b:a:1", "192k", "-ac:a:1", "2",
"-map", "a:0", "-c:a:2", "aac", "-b:a:2", "192k", "-ac:a:2", "2",
"-map", "a:0", "-c:a:3", "aac", "-b:a:3", "164k", "-ac:a:3", "2",
"-map", "a:0", "-c:a:4", "aac", "-b:a:4", "164k", "-ac:a:4", "2",
"-f", "hls",
"-hls_time", "2",
"-hls_playlist_type", "vod",
"-hls_flags", "independent_segments",
"-hls_segment_type", "mpegts",
"-start_number", "0",
"-hls_list_size", "0",
"-hls_segment_filename", "#{dir}/stream_%v_segment_%d.ts",
"-master_pl_name", master_pl_name,
"-var_stream_map", "v:0,a:0 v:1,a:1 v:2,a:2 v:3,a:3 v:4,a:4",
"#{dir}/stream_%v.m3u8"
])
files = Path.wildcard("#{dir}/*")
files
|> Elixir.Stream.map(fn hls_local_path ->
cb.(%{stage: :persisting, done: 1, total: length(files)})
hls_local_path
end)
|> Enum.each(fn hls_local_path ->
Storage.upload_from_filename(
hls_local_path,
"package/vod-#{vod.id}/#{Path.basename(hls_local_path)}",
cb,
content_type:
if(String.ends_with?(hls_local_path, ".m3u8"),
do: "application/x-mpegURL",
else: "video/mp4"
)
)
end)
playlist_url = "#{Bright.config([:s3_cdn_endpoint])}/package/vod-#{vod.id}/master.m3u8"
IO.puts "playlist_url=#{playlist_url} local_path=#{local_path}"
hls_vod = update_vod(vod, %{
playlist_url: playlist_url,
local_path: local_path
})
IO.puts inspect(hls_vod)
cb.(%{stage: :generating_thumbnail, done: 1, total: 1})
# {:ok, hls_vod} = store_thumbnail_from_file(hls_vod, vod.local_path)
# @TODO should probably keep the file cached locally for awhile for any additional processing
# File.rm!(hls_vod.local_path)
hls_vod
end
defp thumbnail_filename(%Vod{} = vod) do
"vod-#{vod.id}-index.jpeg"
end
def store_thumbnail_from_file(%Vod{} = vod, src_path, marker \\ %{minutes: 0}, opts \\ []) do
with {:ok, thumbnail} <- create_thumbnail_from_file(vod, src_path, marker, opts),
{:ok, %{key: key, cdn_url: cdn_url}} <- B2.put(thumbnail, thumbnail_filename(vod)) do
{:ok, vod_thumbnail} =
Vod
|> change_vod(%{
thumbnail_url: thumbnail_filename(vod)
})
|> Repo.insert(on_conflict: :nothing)
end
end
defp create_thumbnail_from_file(%Vod{} = vod, src_path, marker, opts \\ []) do
dst_path = Path.join(System.tmp_dir!(), "#{vod.id}-#{marker.minutes}.jpeg")
if not File.exists?(dst_path) do
:ok = Thumbnex.create_thumbnail(src_path, dst_path, opts)
end
File.read(dst_path)
end
defp broadcast!(topic, msg) do
Phoenix.PubSub.broadcast!(@pubsub, topic, {__MODULE__, msg})
end
def broadcast_processing_progressed!(stage, vod, pct) do
broadcast!("backend", %Events.ProcessingProgressed{vod: vod, stage: stage, pct: pct})
end
def broadcast_processing_completed!(action, vod, url) do
broadcast!("backend", %Events.ProcessingCompleted{action: action, vod: vod, url: url})
end
def broadcast_processing_failed!(vod, attempt, max_attempts) do
broadcast!("backend", %Events.ProcessingFailed{
vod: vod,
attempt: attempt,
max_attempts: max_attempts
})
end
end

View File

@ -12,8 +12,10 @@ defmodule Bright.Streams.Vod do
field :torrent, :string
field :notes, :string
field :thumbnail_url, :string
field :local_path, :string
belongs_to :stream, Bright.Streams.Stream
# belongs_to :uploader, Bright.Accounts.User, foreign_key: :uploaded_by_id # Metadata for uploader
timestamps(type: :utc_datetime)
end
@ -21,7 +23,7 @@ defmodule Bright.Streams.Vod do
@doc false
def changeset(vod, attrs) do
vod
|> cast(attrs, [:s3_cdn_url, :mux_asset_id, :mux_playback_id, :ipfs_cid, :torrent, :stream_id, :origin_temp_input_url, :playlist_url, :thumbnail_url])
|> cast(attrs, [:local_path, :s3_cdn_url, :mux_asset_id, :mux_playback_id, :ipfs_cid, :torrent, :stream_id, :origin_temp_input_url, :playlist_url, :thumbnail_url])
|> validate_required([:stream_id])
end

View File

@ -1,145 +0,0 @@
defmodule Bright.User do
use Ecto.Schema
import Ecto.Changeset
alias Bright.{Repo, Regexp}
schema "users" do
field :name, :string
field :is_admin, :boolean
field :auth_token, :string
field :auth_token_expires_at, :utc_datetime
field :signed_in_at, :utc_datetime
field :joined_at, :utc_datetime
field :patreon_handle, :string
field :github_handle, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :patreon_handle, :github_handle, :is_admin])
|> validate_required([:name, :patreon_handle])
end
defp changeset_with_allowed_attrs(user, attrs, allowed) do
user
|> cast(attrs, allowed)
|> validate_required([:name])
|> validate_format(:name, Regexp.name(), message: Regexp.name_message())
|> validate_length(:name, max: 40, message: "max 40 chars")
|> validate_format(:github_handle, Regexp.social(), message: Regexp.social_message())
|> validate_format(:patreon_handle, Regexp.social(), message: Regexp.social_message())
|> unique_constraint(:github_handle)
|> unique_constraint(:patreon_handle)
end
def auth_changeset(user, attrs \\ %{}),
do: cast(user, attrs, ~w(auth_token auth_token_expires_at)a)
def update_changeset(user, attrs \\ %{}) do
user
|> insert_changeset(attrs)
end
def refresh_auth_token(user, expires_in \\ 60 * 24) do
auth_token = Base.encode16(:crypto.strong_rand_bytes(8))
expires_at = Timex.add(Timex.now(), Timex.Duration.from_minutes(expires_in))
changeset =
auth_changeset(user, %{auth_token: auth_token, auth_token_expires_at: expires_at})
{:ok, user} = Repo.update(changeset)
user
end
def insert_changeset(user, attrs \\ %{}) do
allowed = ~w(name github_handle patreon_handle)a
changeset_with_allowed_attrs(user, attrs, allowed)
end
# def join(conn = %{method: "POST"}, params = %{"user" => user_params}) do
# changeset = User.insert_changeset(%User{}, user_params)
# case Repo.insert(changeset) do
# {:ok, user} ->
# welcome_community(conn, user)
# {:error, changeset} ->
# conn
# |> put_flash(:error, "Something went wrong. 😭")
# |> render(:join, changeset: changeset, user: nil)
# end
# end
# def create_from_ueberauth(%{provider: :github, info: %{nickname: handle}}) do
# changeset = User.insert_changeset(%User{}, %{github_handle: handle, patreon_handle: nil, name: handle})
# case Repo.insert(changeset) do
# {:ok, user} -> {:ok, user}
# {:error, changeset} -> {:error, changeset}
# end
# end
def get!(id) do
User
|> Repo.get(id)
end
def get_by_ueberauth(%{provider: :github, info: %{nickname: handle}}) do
Repo.get_by(__MODULE__, github_handle: handle)
end
def get_by_ueberauth(%{provider: :patreon, info: %{id: patreon_id}}) do
Repo.get_by(__MODULE__, patreon_handle: patreon_id)
end
def get_by_ueberauth(_), do: nil
def sign_in_changes(user) do
change(user, %{
auth_token: nil,
auth_token_expires_at: nil,
signed_in_at: now_in_seconds(),
joined_at: user.joined_at || now_in_seconds()
})
end
defp now_in_seconds, do: Timex.now() |> DateTime.truncate(:second)
def vod_count(user) do
user
|> Vod.authored_by()
|> Vod.published()
|> Repo.count()
end
def tag_count(user) do
user
|> Tag.authored_by()
|> Tag.published()
|> Repo.count()
end
def timestamp_count(user) do
user
|> Timestamp.authored_by()
|> Timestamp.published()
|> Repo.count()
end
def stream_count(user) do
user
|> Stream.authored_by()
|> Stream.published()
|> Repo.count()
end
end

View File

@ -1,70 +0,0 @@
defmodule Bright.UserFromAuth do
@moduledoc """
Retrieve the user information from an auth request
"""
require Logger
require Jason
alias Ueberauth.Auth
def find_or_create(%Auth{provider: :identity} = auth) do
case validate_pass(auth.credentials) do
:ok ->
{:ok, basic_info(auth)}
{:error, reason} ->
{:error, reason}
end
end
def find_or_create(%Auth{} = auth) do
{:ok, basic_info(auth)}
end
# github does it this way
defp avatar_from_auth(%{info: %{urls: %{avatar_url: image}}}), do: image
# facebook does it this way
defp avatar_from_auth(%{info: %{image: image}}), do: image
# default case if nothing matches
defp avatar_from_auth(auth) do
Logger.warning("#{auth.provider} needs to find an avatar URL!")
Logger.debug(Jason.encode!(auth))
nil
end
defp basic_info(auth) do
%{id: auth.uid, name: name_from_auth(auth), avatar: avatar_from_auth(auth)}
end
defp name_from_auth(auth) do
if auth.info.name do
auth.info.name
else
name =
[auth.info.first_name, auth.info.last_name]
|> Enum.filter(&(&1 != nil and &1 != ""))
if Enum.empty?(name) do
auth.info.nickname
else
Enum.join(name, " ")
end
end
end
defp validate_pass(%{other: %{password: nil}}) do
{:error, "Password required"}
end
defp validate_pass(%{other: %{password: pw, password_confirmation: pw}}) do
:ok
end
defp validate_pass(%{other: %{password: _}}) do
{:error, "Passwords do not match"}
end
defp validate_pass(_), do: {:error, "Password Required"}
end

View File

@ -102,9 +102,9 @@ defmodule Bright.Vtubers do
Vtuber.changeset(vtuber, attrs)
end
defimpl String.Chars, for: Bright.User do
def to_string(%Bright.User{name: name}) when not is_nil(name), do: name
def to_string(%Bright.User{}), do: "Anonymous"
end
# defimpl String.Chars, for: Bright.Auth.User do
# def to_string(%Bright.Auth.User{name: name}) when not is_nil(name), do: name
# def to_string(%Bright.Auth.User{}), do: "Anonymous"
# end
end

View File

@ -118,11 +118,13 @@
</.link>
<% else %>
<.link
href={~p"/users/register"}
href={~p"/auth/github"}
method="get"
class="navbar-item"
>
Register
Sign in via GH
</.link>
<%# <p>hello</p> %>
<%# <.link
href={~p"/auth/github"}
class="navbar-item"
@ -139,6 +141,49 @@
</div>
</div>
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 text-zinc-900">
<figure class="image is-32x32">
<img alt={@current_user.name} class="is-rounded" src={@current_user.avatar} />
</figure>
</li>
<li>
<.link
href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Settings
</.link>
</li>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/users/register"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Register
</.link>
</li>
<li>
<.link
href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
</.link>
</li>
<% end %>
</ul>
</nav>
<%= @inner_content %>
</body>

View File

@ -1,7 +0,0 @@
defmodule BrightWeb.AuthHTML do
use BrightWeb, :html
embed_templates "auth_html/*"
end

View File

@ -1 +0,0 @@
<p>hello this is request.html</p>

View File

@ -1,32 +0,0 @@
defmodule BrightWeb.CartController do
use BrightWeb, :controller
alias Bright.ShoppingCart
def show(conn, _params) do
render(conn, :show, changeset: ShoppingCart.change_cart(conn.assigns.cart))
# render(conn, :show)
end
def update(conn, %{"cart" => cart_params}) do
case ShoppingCart.update_cart(conn.assigns.cart, cart_params) do
{:ok, _cart} ->
redirect(conn, to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:error, "There was an error updating your cart")
|> redirect(to: ~p"/cart")
end
end
end
# def show(conn, %{"id" => id}) do
# product = Catalog.get_product!(id)
# render(conn, :show, product: product)
# end
# def show(conn, %{"id" => id}) do
# stream =
# id
# |> Streams.get_stream!()
# |> Streams.inc_page_views()
# render(conn, :show, stream: stream)
# end

View File

@ -1,10 +0,0 @@
defmodule BrightWeb.CartHTML do
use BrightWeb, :html
# this alias is for the html.heex templates
alias Bright.ShoppingCart
embed_templates "cart_html/*"
def currency_to_str(%Decimal{} = val), do: "$#{Decimal.round(val, 2)}"
end

View File

@ -1,26 +0,0 @@
<.header>
My Cart
<:subtitle :if={@cart.items == []}>Your cart is empty</:subtitle>
<:actions>
<.link href={~p"/orders"} method="post">
<.button>Complete order</.button>
</.link>
</:actions>
</.header>
<div :if={@cart.items !== []}>
<.simple_form :let={f} for={@changeset} action={~p"/cart"}>
<.inputs_for :let={%{data: item} = item_form} field={f[:items]}>
<.input field={item_form[:quantity]} type="number" label={item.product.title} />
{currency_to_str(ShoppingCart.total_item_price(item))}
</.inputs_for>
<:actions>
<.button>Update cart</.button>
</:actions>
</.simple_form>
<b>Total</b>: {currency_to_str(ShoppingCart.total_cart_price(@cart))}
</div>
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -1,22 +0,0 @@
defmodule BrightWeb.CartItemController do
use BrightWeb, :controller
alias Bright.ShoppingCart
def create(conn, %{"product_id" => product_id}) do
case ShoppingCart.add_item_to_cart(conn.assigns.cart, product_id) do
{:ok, _item} ->
conn
|> put_flash(:info, "Item added to your cart")
|> redirect(to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:eerror, "There was an error adding the item to your cart")
|> redirect(to: ~p"/cart")
end
end
def delete(conn, %{"id" => product_id}) do
{:ok, _cart} = ShoppingCart.remove_item_from_cart(conn.assigns.cart, product_id)
redirect(conn, to: ~p"/cart")
end
end

View File

@ -1,17 +0,0 @@
defmodule BrightWeb.HelloController do
use BrightWeb, :controller
# plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
def index(conn, _params) do
render(conn, :index)
end
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render(:show)
end
end

View File

@ -1,13 +0,0 @@
defmodule BrightWeb.HelloHTML do
use BrightWeb, :html
embed_templates "hello_html/*"
attr :messenger, :string, required: true
def greet(assigns) do
~H"""
<h2>Hello World, from {@messenger}!</h2>
"""
end
end

View File

@ -1,3 +0,0 @@
<section>
<h2>Hello World, from Phoenix!~</h2>
</section>

View File

@ -1,3 +0,0 @@
<section>
<.greet messenger={@messenger} />
</section>

View File

@ -1,21 +0,0 @@
defmodule BrightWeb.OrderController do
use BrightWeb, :controller
alias Bright.Orders
def create(conn, _) do
case Orders.complete_order(conn.assigns.cart) do
{:ok, order} ->
conn
|> put_flash(:info, "Order created successfully.")
|> redirect(to: ~p"/orders/#{order}")
{:error, _reason} ->
conn
|> put_flash(:error, "There was an error processing your order")
|> redirect(to: ~p"/cart")
end
end
def show(conn, %{"id" => id}) do
order = Orders.get_order!(conn.assigns.current_uuid, id)
render(conn, :show, order: order)
end
end

View File

@ -1,4 +0,0 @@
defmodule BrightWeb.OrderHTML do
use BrightWeb, :html
embed_templates "order_html/*"
end

View File

@ -1,20 +0,0 @@
<.header>
Thank you for your order!
<:subtitle>
<strong>User uuid: </strong>{@order.user_uuid}
</:subtitle>
</.header>
<.table id="items" rows={@order.line_items}>
<:col :let={item} label="Title">{item.product.title}</:col>
<:col :let={item} label="Quantity">{item.quantity}</:col>
<:col :let={item} label="Price">
{BrightWeb.CartHTML.currency_to_str(item.price)}
</:col>
</.table>
<strong>Total price:</strong>
{BrightWeb.CartHTML.currency_to_str(@order.total_price)}
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -6,10 +6,11 @@ defmodule BrightWeb.PageController do
# so skip the default app layout.
# render(conn, :home, layout: false)
# render(conn, "index.html", current_user: get_session(conn, :current_user))
# send_resp(conn, 201, "")
conn
|> put_status(202)
|> render(:home, layout: false)
|> render(:home, layout: false, current_user: get_session(conn, :current_user))
# redirect(conn, to: ~p"/redirect_test")
# redirect(conn, external: "https://elixir-lang.org/")
end
@ -22,6 +23,10 @@ defmodule BrightWeb.PageController do
render(conn, :api, layout: false)
end
def profile(conn, _params) do
render(conn, :profile, layout: false)
end
def health(conn, _params) do
data = %{message: "OK", status: "success"}
json(conn, data)
@ -30,4 +35,5 @@ defmodule BrightWeb.PageController do
def redirect_test(conn, _params) do
render(conn, :home, layout: false)
end
end

View File

@ -3,10 +3,16 @@
<main class="container">
<div class="section">
<h2 class="title is-2">About</h2>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Dedication to the preservation of Lewdtuber history</p>
<p class="subtitle"></p>
</div>
</section>
<p>Welcome to Futureporn, a platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesnt save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.</p>
<div class="section">
<p>A platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesnt save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.</p>
</div>
<div class="section">

View File

@ -26,20 +26,6 @@
</div>
<!--<%= inspect(@current_user) %>
<%= if @current_user do %>
<div>
<h1>Welcome, <%= @current_user[:name] %>!</h1>
<p>ID: <%= @current_user[:id] %></p>
<p>Avatar: <%= @current_user[:avatar] %></p>
</div>
<% else %>
<div>
<p>You are not logged in. Please <a href="/auth/github">log in</a>.</p>
</div>
<% end %>-->

View File

@ -0,0 +1,47 @@
<.flash_group flash={@flash} />
<%= if @current_user do%>
<main class="section">
<h2 class="title is-2">Profile</h2>
<p class="subtitle">{@current_user.name}</p>
</main>
<section class="section">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src={@current_user.avatar}
alt={@current_user.name}
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{@current_user.name}</p>
<p class="subtitle is-6">Github User {@current_user.github_id}</p>
</div>
</div>
<div class="content">
<p class="subtitle is-6">Futureporn User {@current_user.id}</p>
<p class="subtitle is-6"><i>n</i> uploads</p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec
iaculis mauris. <a>@bulmaio</a>. <a href="#">#css</a>
<a href="#">#responsive</a>
<br />
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
</section>
<% else %>
<p>Please <.link href={~p"/auth/github"}>sign in</.link></p>
<% end %>

View File

@ -1,62 +0,0 @@
defmodule BrightWeb.ProductController do
use BrightWeb, :controller
alias Bright.Catalog
alias Bright.Catalog.Product
def index(conn, _params) do
products = Catalog.list_products()
render(conn, :index, products: products)
end
def new(conn, _params) do
changeset = Catalog.change_product(%Product{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"product" => product_params}) do
case Catalog.create_product(product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product created successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
render(conn, :show, product: product)
end
def edit(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
changeset = Catalog.change_product(product)
render(conn, :edit, product: product, changeset: changeset)
end
def update(conn, %{"id" => id, "product" => product_params}) do
product = Catalog.get_product!(id)
case Catalog.update_product(product, product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product updated successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, product: product, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
{:ok, _product} = Catalog.delete_product(product)
conn
|> put_flash(:info, "Product deleted successfully.")
|> redirect(to: ~p"/products")
end
end

View File

@ -1,23 +0,0 @@
defmodule BrightWeb.ProductHTML do
use BrightWeb, :html
embed_templates "product_html/*"
@doc """
Renders a product form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def product_form(assigns)
def category_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:categories, [])
|> Enum.map(& &1.data.id)
for cat <- Bright.Catalog.list_categories(),
do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
end
end

View File

@ -1,8 +0,0 @@
<.header>
Edit Product {@product.id}
<:subtitle>Use this form to manage product records in the database.</:subtitle>
</.header>
<.product_form changeset={@changeset} action={~p"/products/#{@product}"} />
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -1,26 +0,0 @@
<.header>
Listing Products
<:actions>
<.link href={~p"/products/new"}>
<.button>New Product</.button>
</.link>
</:actions>
</.header>
<.table id="products" rows={@products} row_click={&JS.navigate(~p"/products/#{&1}")}>
<:col :let={product} label="Title">{product.title}</:col>
<:col :let={product} label="Description">{product.description}</:col>
<:col :let={product} label="Price">{product.price}</:col>
<:col :let={product} label="Views">{product.views}</:col>
<:action :let={product}>
<div class="sr-only">
<.link navigate={~p"/products/#{product}"}>Show</.link>
</div>
<.link navigate={~p"/products/#{product}/edit"}>Edit</.link>
</:action>
<:action :let={product}>
<.link href={~p"/products/#{product}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View File

@ -1,8 +0,0 @@
<.header>
New Product
<:subtitle>Use this form to manage product records in the database.</:subtitle>
</.header>
<.product_form changeset={@changeset} action={~p"/products"} />
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -1,12 +0,0 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:title]} type="text" label="Title" />
<.input field={f[:description]} type="text" label="Description" />
<.input field={f[:price]} type="number" label="Price" step="any" />
<.input field={f[:category_ids]} type="select" multiple={true} options={category_opts(@changeset)} />
<:actions>
<.button>Save Product</.button>
</:actions>
</.simple_form>

View File

@ -1,26 +0,0 @@
<.header>
Product {@product.id}
<:subtitle>This is a product record from the database.</:subtitle>
<:actions>
<.link href={~p"/products/#{@product}/edit"}>
<.button>Edit product</.button>
</.link>
<.link href={~p"/cart_items?product_id=#{@product.id}"} method="post">
<.button>Add to cart</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Title">{@product.title}</:item>
<:item title="Description">{@product.description}</:item>
<:item title="Price">{@product.price}</:item>
<:item title="Views">{@product.views}</:item>
<:item title="Categories">
<ul>
<li :for={cat <- @product.categories}>{cat.title}</li>
</ul>
</:item>
</.list>
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -1,59 +0,0 @@
defmodule BrightWeb.UserController do
use BrightWeb, :controller
alias Bright.{User, Repo}
require Logger
def index(conn, _params) do
render(conn, :index)
end
# def show(conn) do
# conn
# |> render(:show)
# end
def show(conn = %{assigns: %{current_user: me}}, _params) do
Logger.info(">>> me=#{inspect(me)}")
render(conn, :show, changeset: User.update_changeset(me))
end
# def show(conn) do
# user = User.get_user!(id)
# render(conn, :show, user: user)
# end
def join(conn = %{method: "GET"}, params) do
user = %User{
name: Map.get(params, "name"),
github_handle: Map.get(params, "github_handle"),
patreon_handle: Map.get(params, "patreon_handle")
}
render(conn, :join, changeset: User.insert_changeset(user), user: nil)
end
def join(conn = %{method: "POST"}, params = %{"user" => user_params}) do
changeset = User.insert_changeset(%User{}, user_params)
case Repo.insert(changeset) do
{:ok, user} ->
welcome_community(conn, user)
{:error, changeset} ->
conn
|> put_flash(:error, "Something went wrong. 😭")
|> render(:join, changeset: changeset, user: nil)
end
end
defp welcome_community(conn, user) do
user = User.refresh_auth_token(user)
conn
|> put_flash(:success, "Welcome #{user}")
|> redirect(to: ~p"/")
end
end

View File

@ -1,4 +0,0 @@
defmodule BrightWeb.UserHTML do
use BrightWeb, :html
embed_templates "user_html/*"
end

View File

@ -1,7 +0,0 @@
<.header>
Join Futureporn
</.header>
<.user_form changeset={@changeset} action={~p"/join"} />

View File

@ -1,11 +0,0 @@
<.header>
Visitor Profile
</.header>
<%= if @current_user do %>
<p>@current_user is {@current_user}</p>
<% else %>
<p>there is no @current_user</p>
<% end %>

View File

@ -1,12 +0,0 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" help="This name is displayed publicly to credit you for any contributions" />
<:actions>
<.button>Save User Profile</.button>
</:actions>
</.simple_form>

View File

@ -0,0 +1,42 @@
defmodule BrightWeb.UserSessionController do
use BrightWeb, :controller
alias Bright.Accounts
alias BrightWeb.UserAuth
def create(conn, %{"_action" => "registered"} = params) do
create(conn, params, "Account created successfully!")
end
def create(conn, %{"_action" => "password_updated"} = params) do
conn
|> put_session(:user_return_to, ~p"/users/settings")
|> create(params, "Password updated successfully!")
end
def create(conn, params) do
create(conn, params, "Welcome back!")
end
defp create(conn, %{"user" => user_params}, info) do
%{"email" => email, "password" => password} = user_params
if user = Accounts.get_user_by_email_and_password(email, password) do
conn
|> put_flash(:info, info)
|> UserAuth.log_in_user(user, user_params)
else
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:email, String.slice(email, 0, 160))
|> redirect(to: ~p"/users/log_in")
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
end

View File

@ -3,6 +3,7 @@ defmodule BrightWeb.VodController do
alias Bright.Streams
alias Bright.Streams.Vod
require Logger
def index(conn, _params) do
vods = Streams.list_vods()
@ -15,6 +16,9 @@ defmodule BrightWeb.VodController do
end
def create(conn, %{"vod" => vod_params}) do
# current_user = get_session(conn, :current_user)
# vod_params = Map.put(vod_params, "uploaded_by_id", current_user.id)
# Logger.info("current_user.id=#{current_user.id}")
case Streams.create_vod(vod_params) do
{:ok, vod} ->
conn

View File

@ -8,6 +8,7 @@
</.header>
<.table id="vods" rows={@vods} row_click={&JS.navigate(~p"/vods/#{&1}")}>
<%# <:col :let={vod} label="Uploader">{vod.uploaded_by_id}</:col> %>
<:col :let={vod} label="ID">{vod.id}</:col>
<:col :let={vod} label="S3 CDN URL">{vod.s3_cdn_url}</:col>
<:col :let={vod} label="Mux asset">{vod.mux_asset_id}</:col>

View File

@ -46,6 +46,7 @@
</script>
<.list>
<%# <:item title="Uploader"><img src={@vod.uploaded_by_id} /></:item> %>
<:item title="Source VOD File">
<%= if @vod.s3_cdn_url do %>
<a class="button is-secondary" href={@vod.s3_cdn_url} download={Path.basename(@vod.s3_cdn_url)}>
@ -53,7 +54,7 @@
</a>
<% end %>
</:item>
<:item title="Thumbnail URL"><img src={@vod.thumbnail_url} /></:item>
<:item title="Thumbnail"><img src={@vod.thumbnail_url} /></:item>
<:item title="HLS Playlist URL">{@vod.playlist_url}</:item>
<:item title="Torrent">{@vod.torrent}</:item>
<:item title="Ipfs CID">{@vod.ipfs_cid}</:item>

View File

@ -1,4 +1,5 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
@ -13,6 +14,7 @@
<.input field={f[:stream_id]} type="select" label="Stream" multiple={false} options={stream_opts(@changeset)}/>
<:actions>
<.button>Save Vod</.button>
</:actions>

View File

@ -1,83 +0,0 @@
defmodule BrightWeb.PostLive.FormComponent do
use BrightWeb, :live_component
alias Bright.Blog
@impl true
def render(assigns) do
~H"""
<div>
<.header>
{@title}
<:subtitle>Use this form to manage post records in the database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="post-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:body]} type="text" label="Body" />
<:actions>
<.button phx-disable-with="Saving...">Save Post</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{post: post} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Blog.change_post(post))
end)}
end
@impl true
def handle_event("validate", %{"post" => post_params}, socket) do
changeset = Blog.change_post(socket.assigns.post, post_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"post" => post_params}, socket) do
save_post(socket, socket.assigns.action, post_params)
end
defp save_post(socket, :edit, post_params) do
case Blog.update_post(socket.assigns.post, post_params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, "Post updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_post(socket, :new, post_params) do
case Blog.create_post(post_params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, "Post created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View File

@ -1,47 +0,0 @@
defmodule BrightWeb.PostLive.Index do
use BrightWeb, :live_view
alias Bright.Blog
alias Bright.Blog.Post
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :posts, Blog.list_posts())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Post")
|> assign(:post, Blog.get_post!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Post")
|> assign(:post, %Post{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Posts")
|> assign(:post, nil)
end
@impl true
def handle_info({BrightWeb.PostLive.FormComponent, {:saved, post}}, socket) do
{:noreply, stream_insert(socket, :posts, post)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(id)
{:ok, _} = Blog.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
end
end

View File

@ -1,42 +0,0 @@
<.header>
Listing Posts
<:actions>
<.link patch={~p"/posts/new"}>
<.button>New Post</.button>
</.link>
</:actions>
</.header>
<.table
id="posts"
rows={@streams.posts}
row_click={fn {_id, post} -> JS.navigate(~p"/posts/#{post}") end}
>
<:col :let={{_id, post}} label="Title">{post.title}</:col>
<:col :let={{_id, post}} label="Body">{post.body}</:col>
<:action :let={{_id, post}}>
<div class="sr-only">
<.link navigate={~p"/posts/#{post}"}>Show</.link>
</div>
<.link patch={~p"/posts/#{post}/edit"}>Edit</.link>
</:action>
<:action :let={{id, post}}>
<.link
phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="post-modal" show on_cancel={JS.patch(~p"/posts")}>
<.live_component
module={BrightWeb.PostLive.FormComponent}
id={@post.id || :new}
title={@page_title}
action={@live_action}
post={@post}
patch={~p"/posts"}
/>
</.modal>

View File

@ -1,21 +0,0 @@
defmodule BrightWeb.PostLive.Show do
use BrightWeb, :live_view
alias Bright.Blog
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:post, Blog.get_post!(id))}
end
defp page_title(:show), do: "Show Post"
defp page_title(:edit), do: "Edit Post"
end

View File

@ -1,34 +0,0 @@
<.header>
Post {@post.id}
<:subtitle>This is a post record from the database.</:subtitle>
<:actions>
<.link patch={~p"/posts/#{@post}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit post</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Title">{@post.title}</:item>
<:item title="Body">{@post.body}</:item>
</.list>
<.back navigate={~p"/posts"}>Back to posts</.back>
<.modal :if={@live_action == :edit} id="post-modal" show on_cancel={JS.patch(~p"/posts/#{@post}")}>
<.live_component
module={BrightWeb.PostLive.FormComponent}
id={@post.id}
title={@page_title}
action={@live_action}
post={@post}
patch={~p"/posts/#{@post}"}
/>
</.modal>

View File

@ -1,19 +0,0 @@
defmodule BrightWeb.ThermostatLive do
use BrightWeb, :live_view
def render(assigns) do
~H"""
Current temperature: {@temperature}°F
<button class="button" phx-click="inc_temperature">+</button>
"""
end
def mount(_params, _session, socket) do
temperature = 70 # Let's assume a fixed temperature for now
{:ok, assign(socket, :temperature, temperature)}
end
def handle_event("inc_temperature", _params, socket) do
{:noreply, update(socket, :temperature, &(&1 + 1))}
end
end

View File

@ -1,7 +1,9 @@
defmodule BrightWeb.Router do
use BrightWeb, :router
import BrightWeb.AuthController
import BrightWeb.UserAuth
import Oban.Web.Router
pipeline :browser do
plug(:accepts, ["html", "json"])
@ -13,29 +15,20 @@ defmodule BrightWeb.Router do
plug(:fetch_current_user)
end
defp fetch_current_user(conn, _) do
if user_uuid = get_session(conn, :current_user) do
assign(conn, :current_user, user_uuid)
else
conn
|> assign(:current_user, nil)
|> put_session(:current_user, nil)
end
end
pipeline :api do
plug(:accepts, ["json"])
end
scope "/" do
pipe_through([:browser, :require_authenticated_user, :require_admin_user])
## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
get("/platforms/new", PlatformController, :new)
post("/platforms", PlatformController, :create)
get("/platforms/:id/edit", PlatformController, :edit)
patch("/platforms/:id", PlatformController, :update)
put("/platforms/:id", PlatformController, :update)
end
# scope "/" do
# pipe_through([:browser, :require_auth, :require_admin_user])
# ## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
# get("/platforms/new", PlatformController, :new)
# post("/platforms", PlatformController, :create)
# get("/platforms/:id/edit", PlatformController, :edit)
# patch("/platforms/:id", PlatformController, :update)
# put("/platforms/:id", PlatformController, :update)
# end
scope "/auth", BrightWeb do
pipe_through(:browser)
@ -46,8 +39,15 @@ defmodule BrightWeb.Router do
delete("/logout", AuthController, :delete)
end
# scope "/account", BrightWeb do
# pipe_through([:browser, :require_auth])
# post("/", AuthController, :create_and_sign_in)
# end
scope "/" do
pipe_through([:browser, :require_authenticated_user])
pipe_through([:browser, :require_auth])
get("/streams/new", StreamController, :new)
post("/streams", StreamController, :create)
@ -77,7 +77,7 @@ defmodule BrightWeb.Router do
get("/", PageController, :home)
get("/profile", UserController, :show, as: :user)
get("/profile", PageController, :profile)
get("/patrons", PatronController, :index)
get("/about", PageController, :about)
@ -109,6 +109,9 @@ defmodule BrightWeb.Router do
get("/vods", VodController, :index)
get("/vods/:id", VodController, :show)
end
oban_dashboard "/oban"
end
# Other scopes may use custom stacks.
@ -135,47 +138,5 @@ defmodule BrightWeb.Router do
end
end
## Authentication routes
scope "/", BrightWeb do
pipe_through([:browser])
end
## Authentication routes
# scope "/", BrightWeb do
# pipe_through [:browser, :redirect_if_user_is_authenticated]
# live_session :redirect_if_user_is_authenticated,
# on_mount: [{BrightWeb.UserAuth, :redirect_if_user_is_authenticated}] do
# live "/users/register", UserRegistrationLive, :new
# live "/users/log_in", UserLoginLive, :new
# live "/users/reset_password", UserForgotPasswordLive, :new
# live "/users/reset_password/:token", UserResetPasswordLive, :edit
# end
# post "/users/log_in", UserSessionController, :create
# end
# scope "/", BrightWeb do
# pipe_through [:browser, :require_authenticated_user]
# live_session :require_authenticated_user,
# on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
# live "/users/settings", UserSettingsLive, :edit
# live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
# end
# end
# scope "/", BrightWeb do
# pipe_through [:browser]
# delete "/users/log_out", UserSessionController, :delete
# live_session :current_user,
# on_mount: [{BrightWeb.UserAuth, :mount_current_user}] do
# live "/users/confirm/:token", UserConfirmationLive, :edit
# live "/users/confirm", UserConfirmationInstructionsLive, :new
# end
# end
end

View File

@ -1,18 +1,18 @@
defmodule BrightWeb.AuthController do
@moduledoc """
Auth controller responsible for handling Ueberauth responses
"""
defmodule BrightWeb.UserAuth do
use BrightWeb, :verified_routes
require Logger
use BrightWeb, :controller
import Plug.Conn
import Phoenix.Controller
plug Ueberauth
alias Ueberauth.Strategy.Helpers
alias Bright.{Repo, User}
alias Bright.Accounts
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_bright_web_user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
@doc """
Logs the user in.
@ -68,56 +68,6 @@ defmodule BrightWeb.AuthController do
|> clear_session()
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/auth/github")
|> halt()
end
end
@doc """
Used for routes that require the user to be an administrator.
"""
def require_admin_user(conn, _opts) do
Logger.info("con.assigns[:current_user] as follows. #{inspect(conn.assigns)}")
case conn.assigns[:current_user] do
%User{is_admin: true} -> # Assuming the user struct has an `is_admin` field
conn
%User{} ->
conn
|> put_flash(:error, "You do not have permission to access this page.")
|> redirect(to: ~p"/")
|> halt()
nil ->
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/auth/github")
|> halt()
end
end
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
@doc """
Logs the user out.
@ -137,14 +87,18 @@ defmodule BrightWeb.AuthController do
|> redirect(to: ~p"/")
end
# def fetch_current_user(conn) do
# conn
# |> get_session(:user_id)
# |> case do
# nil -> nil
# user_id -> User.get(user_id)
# end
# end
@doc """
Authenticates the user by looking into the session
and remember me token.
"""
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
Logger.info("user_token=#{inspect(user_token)}")
user = user_token && Accounts.get_user_by_session_token(user_token)
Logger.info("fetch_current_user BEGIN. user=#{inspect(user)}")
assign(conn, :current_user, user)
end
defp ensure_user_token(conn) do
if token = get_session(conn, :user_token) do
@ -160,6 +114,108 @@ defmodule BrightWeb.AuthController do
end
end
@doc """
Handles mounting and authenticating the current_user in LiveViews.
## `on_mount` arguments
* `:mount_current_user` - Assigns current_user
to socket assigns based on user_token, or nil if
there's no user_token or no matching user.
* `:ensure_authenticated` - Authenticates the user from the session,
and assigns the current_user to socket assigns based
on user_token.
Redirects to login page if there's no logged user.
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
Redirects to signed_in_path if there's a logged user.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:
defmodule BrightWeb.PageLive do
use BrightWeb, :live_view
on_mount {BrightWeb.UserAuth, :mount_current_user}
...
end
Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, mount_current_user(socket, session)}
end
def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_user(socket, session)
if socket.assigns.current_user do
{:cont, socket}
else
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|> Phoenix.LiveView.redirect(to: ~p"/auth/github")
{:halt, socket}
end
end
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
socket = mount_current_user(socket, session)
if socket.assigns.current_user do
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
else
{:cont, socket}
end
end
defp mount_current_user(socket, session) do
Phoenix.Component.assign_new(socket, :current_user, fn ->
if user_token = session["user_token"] do
Accounts.get_user_by_session_token(user_token)
end
end)
end
@doc """
Used for routes that require the user to not be authenticated.
"""
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
else
conn
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_auth(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/auth/github")
|> halt()
end
end
defp put_token_in_session(conn, token) do
conn
@ -167,101 +223,11 @@ defmodule BrightWeb.AuthController do
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
def create(conn = %{method: "POST"}, %{"token" => token}) do
user = User.get_by_encoded_auth(token)
if user && Timex.before?(Timex.now(), user.auth_token_expires_at) do
sign_in_and_redirect(conn, user, ~p"/~")
else
conn
|> put_flash(:error, "Whoops!")
|> render("new.html", user: nil)
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "You have been logged out!")
|> clear_session()
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
conn
|> put_flash(:error, "Failed to authenticate.")
|> redirect(to: "/")
end
def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do
user_params = %{
github_handle: Map.get(auth, "nickname", nil),
patreon_handle: Map.get(auth, "full_name", nil),
name: "test"
}
changeset = User.insert_changeset(%User{}, user_params)
case Repo.insert(changeset) do
{:ok, user} ->
UserAuth.log_in_user(conn, user)
{:error, changeset} ->
conn
|> put_flash(:error, "Something went wrong. 😭")
|> render(:join, changeset: changeset, user: nil)
end
# case User.get_by_ueberauth(auth) do
# %User{} = user ->
# UserAuth.log_in_user(conn, user, %{})
# nil ->
# case User.create_from_ueberauth(auth) do
# {:ok, %User{} = user} ->
# UserAuth.log_in_user(conn, user, %{})
# {:error, changeset} ->
# Logger.error("failed to create user. auth=#{inspect(auth)}")
# conn
# |> put_flash(:error, "Failed to create user")
# |> redirect(to: ~p"/")
# end
# end
end
defp sign_in_and_redirect(conn, user, route) do
Logger.info("sign_in_and_redirect with user=#{inspect(user)}")
user
|> User.sign_in_changes()
|> Repo.update()
conn
|> assign(:current_user, user)
|> put_flash(:success, "Welcome to Futureporn!")
|> put_session("id", user.id)
|> configure_session(renew: true)
|> redirect(to: route)
end
defp params_from_ueberauth(%{provider: :github, info: info}) do
%{name: info.name, handle: info.nickname, github_handle: info.nickname, github_id: info.uid}
end
defp params_from_ueberauth(%{provider: :patreon, info: info}) do
%{name: info.name, handle: info.nickname, patreon_handle: info.full_name, patreon_id: info.id}
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: ~p"/"
end

View File

@ -54,7 +54,8 @@ defmodule Bright.MixProject do
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"},
{:oban, "~> 2.17"},
{:oban, "~> 2.19"},
{:oban_web, "~> 2.11"},
{:mox, "~> 0.5.0", only: :test},
{:httpoison, "~> 2.0"},
{:ueberauth, "~> 0.7.0"},
@ -63,7 +64,18 @@ defmodule Bright.MixProject do
{:ex_aws_s3, "~> 2.0"},
{:ex_aws, "~> 2.1"},
{:ffmpex, "~> 0.11.0"},
{:sweet_xml, "~> 0.6"}
{:sweet_xml, "~> 0.6"},
{:ex_m3u8, "~> 0.14.2"},
# {:membrane_core, "~> 1.0"},
# {:membrane_mpeg_ts_plugin, "~> 1.0.3"},
# {:membrane_file_plugin, "~> 0.17.2"},
# {:membrane_mp4_plugin, "~> 0.35.2"},
# {:membrane_http_adaptive_stream_plugin, "> 0.0.0"},
# {:membrane_h264_ffmpeg_plugin, "~> 0.32.5"},
# {:membrane_aac_plugin, "~> 0.11.0"},
# {:membrane_hackney_plugin, "~> 0.6.0"}, # incompatible with membrane_core 1.1.2
# {:membrane_mpegts_plugin, "~> 0.4.0"}, # official module is 4 years outdated
# {:membrane_mpegts_plugin, path: "/home/cj/Documents/membrane_mpegts_plugin"},
]
end

View File

@ -1,11 +1,19 @@
%{
"bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"},
"bandit": {:hex, :bandit, "1.6.6", "f2019a95261d400579075df5bc15641ba8e446cc4777ede6b4ec19e434c3340d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "ceb19bf154bc2c07ee0c9addf407d817c48107e36a66351500846fc325451bf9"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"},
"bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"},
"bulma": {:hex, :bulma, "1.0.2", "50dfffe8d28b0bd527418560223b407f9e80e990e187e1653b17eff818f8fcbe", [:mix], [], "hexpm", "27745727ff7f451d140a2438c0ca4448bc8ca73e0a6d2d4f24e1b5b9ced8a774"},
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
"bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
"bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
"bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
"crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"},
"credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
"dart_sass": {:hex, :dart_sass, "0.7.0", "7979e056cb74fd6843e1c72db763cffc7726a9192a657735b7d24c0d9c26a1ce", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4a8e70bca41aa00846398abdf5ad8a64d7907a0f7bf40145cd2e40d5971629f2"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
@ -13,59 +21,98 @@
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
"esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
"ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
"ex_m3u8": {:hex, :ex_m3u8, "0.14.2", "3eb17f936e2ca2fdcde11664f3a543e75a94814d928098e050bda5b1e149c021", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d2a1fb4382a521cce7f966502ecce6187f286ca2852dbb0dcc25dea72f8ba039"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"ffmpex": {:hex, :ffmpex, "0.11.0", "70d2e211a70e1d8cc1a81d73208d5efedda59d82db4c91160c79e5461529d291", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:rambo, "~> 0.3.0", [hex: :rambo, repo: "hexpm", optional: false]}], "hexpm", "2429d67badc91957ace572b9169615619740904a58791289ba54d99e57a164eb"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
"membrane_aac_format": {:hex, :membrane_aac_format, "0.8.0", "515631eabd6e584e0e9af2cea80471fee6246484dbbefc4726c1d93ece8e0838", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}], "hexpm", "a30176a94491033ed32be45e51d509fc70a5ee6e751f12fd6c0d60bd637013f6"},
"membrane_aac_plugin": {:hex, :membrane_aac_plugin, "0.11.1", "9513c87612d6d07fb6878c57fe9b31561c531981026de66517914ffc5a363d77", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:crc, "~> 0.10.2", [hex: :crc, repo: "hexpm", optional: false]}, {:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.6.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.8.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "0a61139d5422ffd865c70f8aea610f35769770de31753ee7a5925919e4c20d90"},
"membrane_caps_video_raw": {:hex, :membrane_caps_video_raw, "0.1.0", "6aa751b0c338ea6672540b7ec7ad2be0d23bad931b8a8776757da9b279070a3b", [:mix], [], "hexpm", "3f60d65189bd9e3b0ab77e0ebf2e0c1b04d0fd6f67c546fc1d54d9958c362ce4"},
"membrane_cmaf_format": {:hex, :membrane_cmaf_format, "0.7.1", "9ea858faefdcb181cdfa8001be827c35c5f854e9809ad57d7062cff1f0f703fd", [:mix], [], "hexpm", "3c7b4ed2a986e27f6f336d2f19e9442cb31d93b3142fc024c019572faca54a73"},
"membrane_common_c": {:hex, :membrane_common_c, "0.16.0", "caf3f29d2f5a1d32d8c2c122866110775866db2726e4272be58e66dfdf4bce40", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "a3c7e91de1ce1f8b23b9823188a5d13654d317235ea0ca781c05353ed3be9b1c"},
"membrane_core": {:hex, :membrane_core, "1.1.2", "3ca206893e1d3739a24d5092d21c06fcb4db326733a1798f9788fc53abb74829", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a989fd7e0516a7e66f5fb63950b1027315b7f8c8d82d8d685e178b0fb780901b"},
"membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.2", "650e134c2345d946f930082fac8bac9f5aba785a7817d38a9a9da41ffc56fa92", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "df50c6040004cd7b901cf057bd7e99c875bbbd6ae574efc93b2c753c96f43b9d"},
"membrane_h264_ffmpeg_plugin": {:hex, :membrane_h264_ffmpeg_plugin, "0.32.5", "30542fb5d6d36961a51906549b4338f4fc66a304bf92e7c7123e2b9971e3502d", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.4.1", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "8c80e11b9ec9ca23d44304ed7bb3daf665e98b91b2488608ee5718a88182e363"},
"membrane_h264_format": {:hex, :membrane_h264_format, "0.6.1", "44836cd9de0abe989b146df1e114507787efc0cf0da2368f17a10c47b4e0738c", [:mix], [], "hexpm", "4b79be56465a876d2eac2c3af99e115374bbdc03eb1dea4f696ee9a8033cd4b0"},
"membrane_h265_format": {:hex, :membrane_h265_format, "0.2.0", "1903c072cf7b0980c4d0c117ab61a2cd33e88782b696290de29570a7fab34819", [:mix], [], "hexpm", "6df418bdf242c0d9f7dbf2e5aea4c2d182e34ac9ad5a8b8cef2610c290002e83"},
"membrane_hackney_plugin": {:hex, :membrane_hackney_plugin, "0.6.0", "f495da8f8d3b55035d2f38a58b18d16549df9453b2e88517def98a6414a34655", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.8.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:mockery, "~> 2.3", [hex: :mockery, repo: "hexpm", optional: false]}], "hexpm", "16beedf5f829b5ba7aa6850882cbd209b1ff5be2dd84ef675c2a31f48837962f"},
"membrane_http_adaptive_stream_plugin": {:hex, :membrane_http_adaptive_stream_plugin, "0.5.0", "9c9b633d0aa12226676e5307735fd9fc56d9e4909054f2bf6d4d4ecf6a62595e", [:mix], [{:credo, "~> 1.6.1", [hex: :credo, repo: "hexpm", optional: false]}, {:membrane_cmaf_format, "~> 0.5.0", [hex: :membrane_cmaf_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.8.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_mp4_plugin, "~> 0.11.0", [hex: :membrane_mp4_plugin, repo: "hexpm", optional: false]}, {:membrane_tee_plugin, "~> 0.7.0", [hex: :membrane_tee_plugin, repo: "hexpm", optional: false]}], "hexpm", "e00cbbfb9bb2cb2a9d0abf80cc1aa8a245577278d793b4b980951479713a7684"},
"membrane_mp4_format": {:hex, :membrane_mp4_format, "0.8.0", "8c6e7d68829228117d333b4fbb030e7be829aab49dd8cb047fdc664db1812e6a", [:mix], [], "hexpm", "148dea678a1f82ccfd44dbde6f936d2f21255f496cb45a22cc6eec427f025522"},
"membrane_mp4_plugin": {:hex, :membrane_mp4_plugin, "0.35.2", "cbedb5272ef1c8f7d9cd3c44f820a90306469b1dc84b8db30ff55bb6195b7cb2", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_cmaf_format, "~> 0.7.0", [hex: :membrane_cmaf_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.17.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h265_format, "~> 0.2.0", [hex: :membrane_h265_format, repo: "hexpm", optional: false]}, {:membrane_mp4_format, "~> 0.8.0", [hex: :membrane_mp4_format, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.1", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}], "hexpm", "8afd4e7779a742dd56c23f1f23053933d1b0b34d397ad368a2f56f995edb2fe0"},
"membrane_mpeg_ts_plugin": {:hex, :membrane_mpeg_ts_plugin, "1.0.3", "6ca4edeee4d80d936214ed90be4bb43fc9cf75b2391a962182d3e277b517de69", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:mpeg_ts, "~> 1.0", [hex: :mpeg_ts, repo: "hexpm", optional: false]}], "hexpm", "90d8eae64b02e54924d8505653a57c91b9a72211f34a770b7f9ded25baf0fcc9"},
"membrane_mpegts_plugin": {:hex, :membrane_mpegts_plugin, "0.4.0", "e055da53a7a54cc42e280da229e4ff6c9257103400524ebfe8b33502841c14f5", [:mix], [{:membrane_core, "~> 0.8.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "4bb6e8a4d265147acd95a4672930233345807923c7f19280ca2293050ef961b8"},
"membrane_opus_format": {:hex, :membrane_opus_format, "0.3.0", "3804d9916058b7cfa2baa0131a644d8186198d64f52d592ae09e0942513cb4c2", [:mix], [], "hexpm", "8fc89c97be50de23ded15f2050fe603dcce732566fe6fdd15a2de01cb6b81afe"},
"membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"},
"membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.1", "d7344499c2d80f236a7ef962b5490c651341a501052ee43dec56cf0319fa3936", [:mix], [], "hexpm", "9920b7d445b5357608a364fec5685acdfce85334c647f745045237a0d296c442"},
"membrane_tee_plugin": {:hex, :membrane_tee_plugin, "0.7.0", "b4705938a388fba8ce973dbdce8a5e95c963c0370bcc48311c0a40c2ea5b0ad8", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.8.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "f029fb95ca4e2178559629691715a042252fd756914c5929cc054690205f391d"},
"membrane_timestamp_queue": {:hex, :membrane_timestamp_queue, "0.2.2", "1c831b2273d018a6548654aa9f7fa7c4b683f71d96ffe164934ef55f9d11f693", [:mix], [{:heap, "~> 2.0", [hex: :heap, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "7c830e760baaced0988421671cd2c83c7cda8d1bd2b61fd05332711675d1204f"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"mockery": {:hex, :mockery, "2.3.3", "3dba87bd0422a513e6af6e0d811383f38f82ac6be5d3d285a5fcca9c299bd0ac", [:mix], [], "hexpm", "17282be00613286254298117cd25e607a39f15ac03b41c631f60e52f5b5ec974"},
"mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"},
"mpeg_ts": {:hex, :mpeg_ts, "1.0.2", "dc548ea9de58df93c2e9ddd006a5f4523c29d0ecfeb1189bb87ed4c458f6b2a2", [:mix], [], "hexpm", "eaa3c179670f4bf326ff974d13845aac3107bfe42f894c8ed33130d89a818f67"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"},
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
"oban": {:hex, :oban, "2.19.0", "dfb8fa028ce7e7cf3be3481a47a7c8ebf9428d6df0aa58c1388a8e63f7ff2797", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa3eb7cfa2aea8ecc4df4787b92ddb61ad5a598f07560937d1dd5dbb1ed225e2"},
"oban_met": {:hex, :oban_met, "1.0.1", "737db0064567b923d3f35efd1d3009dd1435d60ee6f98dbb55dbb83db8f4f4fa", [:mix], [{:oban, "~> 2.18", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "0492d841f880b76c5b73081bc70ebea20ebacc08e871345f72c2270513f09957"},
"oban_web": {:hex, :oban_web, "2.11.0", "8b2a23331ef7e60eabb4118a141880d89812820321b21f289f1696bcf3058810", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "a573f27bf7cb054ff2a694116428dc6fedc18e20a20d10a74934b7c9e473e562"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"},
"phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.1", "5389a30658176c0de816636ce276567478bffd063c082515a6e8368b8fc9a0db", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0f517e6f290f10dbb94343ac22e0109437fb1fa6f0696e7c73967b789c1c285"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.2", "e7b1dd68c86326e2c45cc81da41e332cc8aa7228a7161e2c811dcd7f1dd14db1", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8a40265b0cd7d3a35f136dfa3cc048e3b198fc3718763411a78c323a44ebebee"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
"qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
"rambo": {:hex, :rambo, "0.3.4", "8962ac3bd1a633ee9d0e8b44373c7913e3ce3d875b4151dcd060886092d2dce7", [:mix], [], "hexpm", "0cc54ed089fbbc84b65f4b8a774224ebfe60e5c80186fafc7910b3e379ad58f1"},
"ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"},
"redirect": {:hex, :redirect, "0.4.0", "98b46053504ee517bc3ad2fd04c064b64b48d339e1e18266355b30c4f8bb52b0", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.3 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dfa29a8ecbad066ed0b73b34611cf24c78101719737f37bdf750f39197d67b97"},
"req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"},
"secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"},
"shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.17.5", "14910d267a2633d4335917b37846e376e2067815601592629366c39845dad145", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "629113d477bc82c4c3bffd15a25e8becc1c7ccc0f0e67743b017caddebb06f04"},
"swoosh": {:hex, :swoosh, "1.17.6", "27ff070f96246e35b7105ab1c52b2b689f523a3cb83ed9faadb2f33bd653ccba", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9798f3e72165f40c950f6762c06dab68afcdcf616138fc4a07965c09c250e1e2"},
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"tesla": {:hex, :tesla, "1.12.3", "7189f71ac607169a1bb2dfcf8747dedd4d9384ec00cec6c7b38c5f03811a73c7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4dfb0d6a81ca79c8662a4f03884843a5b3251825ba47ea6f9ab84dcc354fdeec"},
"thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"},
"thousand_island": {:hex, :thousand_island, "1.3.9", "095db3e2650819443e33237891271943fad3b7f9ba341073947581362582ab5a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25ab4c07badadf7f87adb4ab414e0ed374e5f19e72503aa85132caa25776e54f"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
"ueberauth": {:hex, :ueberauth, "0.7.0", "9c44f41798b5fa27f872561b6f7d2bb0f10f03fdd22b90f454232d7b087f4b75", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2efad9022e949834f16cc52cd935165049d81fa9e925690f91035c2e4b58d905"},
"ueberauth_github": {:hex, :ueberauth_github, "0.8.3", "1c478629b4c1dae446c68834b69194ad5cead3b6c67c913db6fdf64f37f0328f", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "ae0ab2879c32cfa51d7287a48219b262bfdab0b7ec6629f24160564247493cc6"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unifex": {:hex, :unifex, "1.2.1", "6841c170a6e16509fac30b19e4e0a19937c33155a59088b50c15fc2c36251b6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "8c9d2e3c48df031e9995dd16865bab3df402c0295ba3a31f38274bb5314c7d37"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"},
}

View File

@ -0,0 +1,16 @@
defmodule Bright.Repo.Migrations.RemoveAuthAndUsers do
use Ecto.Migration
def change do
drop_if_exists table(:users_tokens), cascade: true
drop_if_exists table(:users), cascade: true
drop_if_exists table(:cart_items), cascade: true
drop_if_exists table(:carts), cascade: true
drop_if_exists table(:product_categories), cascade: true
drop_if_exists table(:categories), cascade: true
drop_if_exists table(:order_line_items), cascade: true
drop_if_exists table(:orders), cascade: true
drop_if_exists table(:posts), cascade: true
drop_if_exists table(:products), cascade: true
end
end

View File

@ -0,0 +1,29 @@
defmodule Bright.Repo.Migrations.CreateUsersAuthTables do
use Ecto.Migration
def change do
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
create table(:users) do
add :email, :citext, null: false
add :hashed_password, :string, null: false
add :confirmed_at, :utc_datetime
timestamps(type: :utc_datetime)
end
create unique_index(:users, [:email])
create table(:users_tokens) do
add :user_id, references(:users, on_delete: :delete_all), null: false
add :token, :binary, null: false
add :context, :string, null: false
add :sent_to, :string
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:users_tokens, [:user_id])
create unique_index(:users_tokens, [:context, :token])
end
end

View File

@ -0,0 +1,11 @@
defmodule Bright.Repo.Migrations.AddUploadedByToVods do
use Ecto.Migration
def change do
alter table(:vods) do
add :uploaded_by_id, references(:users, on_delete: :nothing)
end
create index(:vods, [:uploaded_by_id])
end
end

View File

@ -0,0 +1,9 @@
defmodule Bright.Repo.Migrations.AddGithubId do
use Ecto.Migration
def change do
alter table(:users) do
add :github_id, :string
end
end
end

View File

@ -0,0 +1,10 @@
defmodule Bright.Repo.Migrations.AddUserAvatarName do
use Ecto.Migration
def change do
alter table(:users) do
add :avatar, :string
add :name, :string
end
end
end

View File

@ -0,0 +1,11 @@
defmodule Bright.Repo.Migrations.RemoveEmailAndPassword do
use Ecto.Migration
def change do
alter table(:users) do
remove :email
remove :hashed_password
remove :confirmed_at
end
end
end

View File

@ -0,0 +1,9 @@
defmodule Bright.Repo.Migrations.AddLocalPath do
use Ecto.Migration
def change do
alter table(:vods) do
add :local_path, :string
end
end
end

View File

@ -0,0 +1,9 @@
defmodule Bright.Repo.Migrations.AddDuration do
use Ecto.Migration
def change do
alter table(:vods) do
add :duration, :integer
end
end
end

View File

@ -1,3 +0,0 @@
HELLO MAMA
haha

View File

@ -3,7 +3,8 @@ defmodule Bright.ImagesTest do
alias Bright.Images
@test_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
@test_mp4_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
@test_ts_fixture "./test/fixtures/test-fixture.ts"
describe "thumbnails" do
@ -11,7 +12,7 @@ defmodule Bright.ImagesTest do
@tag :unit
test "create_thumbnail/1" do
{:ok, %{:output => output, :filename => filename}} = Images.create_thumbnail(@test_fixture)
{:ok, %{:output => output, :filename => filename}} = Images.create_thumbnail(@test_mp4_fixture)
assert output === ""
assert Regex.match?(~r/[a-zA-Z0-9]+-.*\.png$/, filename)
assert File.exists?(filename)
@ -25,9 +26,9 @@ defmodule Bright.ImagesTest do
basename = "thumb.jpg"
random_string = for _ <- 1..12, into: "", do: <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
output_file = "/tmp/#{random_string}-#{basename}"
IO.puts "output_file=#{inspect(output_file)} @test_fixture=#{inspect(@test_fixture)}"
IO.puts "output_file=#{inspect(output_file)} @test_mp4_fixture=#{inspect(@test_mp4_fixture)}"
{:ok, output } = Images.create_thumbnail(@test_fixture, output_file)
{:ok, output } = Images.create_thumbnail(@test_mp4_fixture, output_file)
assert File.exists?(output_file)
{:ok, stat} = File.stat(output_file)
@ -55,17 +56,23 @@ defmodule Bright.ImagesTest do
describe "get_video_duration" do
@tag :integration
test "should get video stream duration" do
{:ok, duration} = Images.get_video_duration(@test_fixture)
{:ok, duration} = Images.get_video_duration(@test_mp4_fixture)
assert duration === "5.280000"
end
end
describe "get_video_framecount" do
@tag :integration
test "should get video frame count" do
{:ok, nb_frames} = Images.get_video_framecount(@test_fixture)
test "should get video frame count from a mp4 which contains framecount in metadata" do
{:ok, nb_frames} = Images.get_video_framecount(@test_mp4_fixture)
assert nb_frames === 132
end
@tag :integration
test "should get video frame count from a ts which does not contain framecount in metadata" do
{:ok, nb_read_frames} = Images.get_video_framecount(@test_ts_fixture)
assert nb_read_frames === 99
end
end

View File

@ -54,6 +54,20 @@ defmodule Bright.ObanWorkers.CreateThumbnailTest do
refute_enqueued worker: CreateThumbnail
end
@tag :integration
test "not scheduled when playlist_url is missing" do
# we do this because .ts files dont usually have nb_frames in stream metadata, which we require to generate a thumbnail.
# we wait until we have processsed the hls playlist to create the thumbnail.
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
{:ok, _vod} = Streams.create_vod(%{stream_id: stream.id})
refute_enqueued worker: CreateThumbnail
end
end
end

View File

@ -66,7 +66,7 @@ defmodule Bright.StreamsTest do
import Bright.StreamsFixtures
@invalid_attrs %{stream_id: nil, s3_cdn_url: nil, s3_upload_id: nil, s3_key: nil, s3_bucket: nil, mux_asset_id: nil, mux_playback_id: nil, ipfs_cid: nil, torrent: nil}
@invalid_attrs %{stream_id: nil, s3_cdn_url: nil, s3_key: nil, s3_bucket: nil, mux_asset_id: nil, mux_playback_id: nil, ipfs_cid: nil, torrent: nil}
test "list_vods/0 returns all vods" do
stream = stream_fixture()
@ -82,15 +82,10 @@ defmodule Bright.StreamsTest do
test "create_vod/1 with valid data creates a vod" do
stream = stream_fixture()
valid_attrs = %{stream_id: stream.id, s3_cdn_url: "some s3_cdn_url", s3_upload_id: "some s3_upload_id", s3_key: "some s3_key", s3_bucket: "some s3_bucket", mux_asset_id: "some mux_asset_id", mux_playback_id: "some mux_playback_id", ipfs_cid: "some ipfs_cid", torrent: "some torrent"}
valid_attrs = %{stream_id: stream.id, s3_cdn_url: "some s3_cdn_url", s3_key: "some s3_key", s3_bucket: "some s3_bucket", mux_asset_id: "some mux_asset_id", mux_playback_id: "some mux_playback_id", ipfs_cid: "some ipfs_cid", torrent: "some torrent"}
assert {:ok, %Vod{} = vod} = Streams.create_vod(valid_attrs)
assert vod.s3_cdn_url == "some s3_cdn_url"
assert vod.s3_upload_id == "some s3_upload_id"
assert vod.s3_key == "some s3_key"
assert vod.s3_bucket == "some s3_bucket"
assert vod.mux_asset_id == "some mux_asset_id"
assert vod.mux_playback_id == "some mux_playback_id"
assert vod.ipfs_cid == "some ipfs_cid"
assert vod.torrent == "some torrent"
end
@ -106,9 +101,6 @@ defmodule Bright.StreamsTest do
assert {:ok, %Vod{} = vod} = Streams.update_vod(vod, update_attrs)
assert vod.s3_cdn_url == "some updated s3_cdn_url"
assert vod.s3_upload_id == "some updated s3_upload_id"
assert vod.s3_key == "some updated s3_key"
assert vod.s3_bucket == "some updated s3_bucket"
assert vod.mux_asset_id == "some updated mux_asset_id"
assert vod.mux_playback_id == "some updated mux_playback_id"
assert vod.ipfs_cid == "some updated ipfs_cid"
@ -133,6 +125,39 @@ defmodule Bright.StreamsTest do
stream = stream_fixture()
vod = vod_fixture(%{stream_id: stream.id})
assert %Ecto.Changeset{} = Streams.change_vod(vod)
end
end
describe "processing" do
alias Bright.Streams
import Bright.StreamsFixtures
# test "get_duration/1" do
# playlist_url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
# stream = stream_fixture()
# vod = vod_fixture(%{playlist_url: playlist_url, stream_id: stream.id})
# {:ok, duration } = Bright.Streams.get_duration(vod)
# assert :ok
# assert duration == 3
# end
test "transmux_to_hls/2" do
stream = stream_fixture()
vod = vod_fixture(%{stream_id: stream.id, playlist_url: nil, origin_temp_input_url: "https://futureporn-b2.b-cdn.net/test-fixture.ts"})
callback = fn progress -> send(self(), {:progress, progress}) end
{:ok, updated_vod} = Streams.transmux_to_hls(vod, callback)
assert :ok
assert updated_vod.local_path != nil
assert_received {:progress, %{stage: :transmuxing, done: 1, total: 1}}
assert_received {:progress, %{stage: :persisting, done: 1, total: _}}
# assert_received {:progress, %{stage: :generating_thumbnail, done: 1, total: 1}}
end
end
end

View File

@ -1,113 +0,0 @@
defmodule BrightWeb.PostLiveTest do
use BrightWeb.ConnCase
import Phoenix.LiveViewTest
import Bright.BlogFixtures
@create_attrs %{title: "some title", body: "some body"}
@update_attrs %{title: "some updated title", body: "some updated body"}
@invalid_attrs %{title: nil, body: nil}
defp create_post(_) do
post = post_fixture()
%{post: post}
end
describe "Index" do
setup [:create_post]
test "lists all posts", %{conn: conn, post: post} do
{:ok, _index_live, html} = live(conn, ~p"/posts")
assert html =~ "Listing Posts"
assert html =~ post.title
end
test "saves new post", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/posts")
assert index_live |> element("a", "New Post") |> render_click() =~
"New Post"
assert_patch(index_live, ~p"/posts/new")
assert index_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#post-form", post: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/posts")
html = render(index_live)
assert html =~ "Post created successfully"
assert html =~ "some title"
end
test "updates post in listing", %{conn: conn, post: post} do
{:ok, index_live, _html} = live(conn, ~p"/posts")
assert index_live |> element("#posts-#{post.id} a", "Edit") |> render_click() =~
"Edit Post"
assert_patch(index_live, ~p"/posts/#{post}/edit")
assert index_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#post-form", post: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/posts")
html = render(index_live)
assert html =~ "Post updated successfully"
assert html =~ "some updated title"
end
test "deletes post in listing", %{conn: conn, post: post} do
{:ok, index_live, _html} = live(conn, ~p"/posts")
assert index_live |> element("#posts-#{post.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#posts-#{post.id}")
end
end
describe "Show" do
setup [:create_post]
test "displays post", %{conn: conn, post: post} do
{:ok, _show_live, html} = live(conn, ~p"/posts/#{post}")
assert html =~ "Show Post"
assert html =~ post.title
end
test "updates post within modal", %{conn: conn, post: post} do
{:ok, show_live, _html} = live(conn, ~p"/posts/#{post}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Post"
assert_patch(show_live, ~p"/posts/#{post}/show/edit")
assert show_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#post-form", post: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/posts/#{post}")
html = render(show_live)
assert html =~ "Post updated successfully"
assert html =~ "some updated title"
end
end
end

View File

@ -1,272 +0,0 @@
defmodule BrightWeb.UserAuthTest do
use BrightWeb.ConnCase, async: true
alias Phoenix.LiveView
alias Bright.Accounts
alias BrightWeb.UserAuth
import Bright.AccountsFixtures
@remember_me_cookie "_bright_web_user_remember_me"
setup %{conn: conn} do
conn =
conn
|> Map.replace!(:secret_key_base, BrightWeb.Endpoint.config(:secret_key_base))
|> init_test_session(%{})
%{user: user_fixture(), conn: conn}
end
describe "log_in_user/3" do
test "stores the user token in the session", %{conn: conn, user: user} do
conn = UserAuth.log_in_user(conn, user)
assert token = get_session(conn, :user_token)
assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
assert redirected_to(conn) == ~p"/"
assert Accounts.get_user_by_session_token(token)
end
test "clears everything previously stored in the session", %{conn: conn, user: user} do
conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
refute get_session(conn, :to_be_removed)
end
test "redirects to the configured path", %{conn: conn, user: user} do
conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
assert redirected_to(conn) == "/hello"
end
test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
assert signed_token != get_session(conn, :user_token)
assert max_age == 5_184_000
end
end
describe "logout_user/1" do
test "erases session and cookies", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
conn =
conn
|> put_session(:user_token, user_token)
|> put_req_cookie(@remember_me_cookie, user_token)
|> fetch_cookies()
|> UserAuth.log_out_user()
refute get_session(conn, :user_token)
refute conn.cookies[@remember_me_cookie]
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
assert redirected_to(conn) == ~p"/"
refute Accounts.get_user_by_session_token(user_token)
end
test "broadcasts to the given live_socket_id", %{conn: conn} do
live_socket_id = "users_sessions:abcdef-token"
BrightWeb.Endpoint.subscribe(live_socket_id)
conn
|> put_session(:live_socket_id, live_socket_id)
|> UserAuth.log_out_user()
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
end
test "works even if user is already logged out", %{conn: conn} do
conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
refute get_session(conn, :user_token)
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
assert redirected_to(conn) == ~p"/"
end
end
describe "fetch_current_user/2" do
test "authenticates user from session", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([])
assert conn.assigns.current_user.id == user.id
end
test "authenticates user from cookies", %{conn: conn, user: user} do
logged_in_conn =
conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
user_token = logged_in_conn.cookies[@remember_me_cookie]
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
conn =
conn
|> put_req_cookie(@remember_me_cookie, signed_token)
|> UserAuth.fetch_current_user([])
assert conn.assigns.current_user.id == user.id
assert get_session(conn, :user_token) == user_token
assert get_session(conn, :live_socket_id) ==
"users_sessions:#{Base.url_encode64(user_token)}"
end
test "does not authenticate if data is missing", %{conn: conn, user: user} do
_ = Accounts.generate_user_session_token(user)
conn = UserAuth.fetch_current_user(conn, [])
refute get_session(conn, :user_token)
refute conn.assigns.current_user
end
end
describe "on_mount :mount_current_user" do
test "assigns current_user based on a valid user_token", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
session = conn |> put_session(:user_token, user_token) |> get_session()
{:cont, updated_socket} =
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
assert updated_socket.assigns.current_user.id == user.id
end
test "assigns nil to current_user assign if there isn't a valid user_token", %{conn: conn} do
user_token = "invalid_token"
session = conn |> put_session(:user_token, user_token) |> get_session()
{:cont, updated_socket} =
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
assert updated_socket.assigns.current_user == nil
end
test "assigns nil to current_user assign if there isn't a user_token", %{conn: conn} do
session = conn |> get_session()
{:cont, updated_socket} =
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
assert updated_socket.assigns.current_user == nil
end
end
describe "on_mount :ensure_authenticated" do
test "authenticates current_user based on a valid user_token", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
session = conn |> put_session(:user_token, user_token) |> get_session()
{:cont, updated_socket} =
UserAuth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})
assert updated_socket.assigns.current_user.id == user.id
end
test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
user_token = "invalid_token"
session = conn |> put_session(:user_token, user_token) |> get_session()
socket = %LiveView.Socket{
endpoint: BrightWeb.Endpoint,
assigns: %{__changed__: %{}, flash: %{}}
}
{:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
assert updated_socket.assigns.current_user == nil
end
test "redirects to login page if there isn't a user_token", %{conn: conn} do
session = conn |> get_session()
socket = %LiveView.Socket{
endpoint: BrightWeb.Endpoint,
assigns: %{__changed__: %{}, flash: %{}}
}
{:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
assert updated_socket.assigns.current_user == nil
end
end
describe "on_mount :redirect_if_user_is_authenticated" do
test "redirects if there is an authenticated user ", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
session = conn |> put_session(:user_token, user_token) |> get_session()
assert {:halt, _updated_socket} =
UserAuth.on_mount(
:redirect_if_user_is_authenticated,
%{},
session,
%LiveView.Socket{}
)
end
test "doesn't redirect if there is no authenticated user", %{conn: conn} do
session = conn |> get_session()
assert {:cont, _updated_socket} =
UserAuth.on_mount(
:redirect_if_user_is_authenticated,
%{},
session,
%LiveView.Socket{}
)
end
end
describe "redirect_if_user_is_authenticated/2" do
test "redirects if user is authenticated", %{conn: conn, user: user} do
conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([])
assert conn.halted
assert redirected_to(conn) == ~p"/"
end
test "does not redirect if user is not authenticated", %{conn: conn} do
conn = UserAuth.redirect_if_user_is_authenticated(conn, [])
refute conn.halted
refute conn.status
end
end
describe "require_authenticated_user/2" do
test "redirects if user is not authenticated", %{conn: conn} do
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
assert conn.halted
assert redirected_to(conn) == ~p"/users/log_in"
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
"You must log in to access this page."
end
test "stores the path to redirect to on GET", %{conn: conn} do
halted_conn =
%{conn | path_info: ["foo"], query_string: ""}
|> fetch_flash()
|> UserAuth.require_authenticated_user([])
assert halted_conn.halted
assert get_session(halted_conn, :user_return_to) == "/foo"
halted_conn =
%{conn | path_info: ["foo"], query_string: "bar=baz"}
|> fetch_flash()
|> UserAuth.require_authenticated_user([])
assert halted_conn.halted
assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
halted_conn =
%{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
|> fetch_flash()
|> UserAuth.require_authenticated_user([])
assert halted_conn.halted
refute get_session(halted_conn, :user_return_to)
end
test "does not redirect if user is authenticated", %{conn: conn, user: user} do
conn = conn |> assign(:current_user, user) |> UserAuth.require_authenticated_user([])
refute conn.halted
refute conn.status
end
end
end

Binary file not shown.

View File

@ -28,12 +28,7 @@ defmodule Bright.StreamsFixtures do
attrs
|> Enum.into(%{
ipfs_cid: "some ipfs_cid",
mux_asset_id: "some mux_asset_id",
mux_playback_id: "some mux_playback_id",
s3_bucket: "some s3_bucket",
s3_cdn_url: "some s3_cdn_url",
s3_key: "some s3_key",
s3_upload_id: "some s3_upload_id",
torrent: "some torrent",
playlist_url: "some playlist_url",
thumbnail_url: "some thumbnail_url"