From 8aa8f231edeac65e7482b005946d56b2b9c6c8d5 Mon Sep 17 00:00:00 2001 From: CJ_Clippy Date: Tue, 28 Jan 2025 23:15:42 -0800 Subject: [PATCH] ffmpeg hls playlist implementation --- devbox.json | 6 +- devbox.lock | 144 +++++++++ docker-compose.yml | 56 +--- dockerfiles/bright.dockerfile | 5 +- services/bright/config/config.exs | 1 + services/bright/config/runtime.exs | 2 + services/bright/lib/bright.ex | 14 + services/bright/lib/bright/blog.ex | 104 ------- services/bright/lib/bright/blog/post.ex | 18 -- services/bright/lib/bright/cache.ex | 4 + services/bright/lib/bright/catalog.ex | 214 ------------- .../bright/lib/bright/catalog/category.ex | 18 -- services/bright/lib/bright/catalog/product.ex | 23 -- services/bright/lib/bright/downloader.ex | 5 +- services/bright/lib/bright/events.ex | 23 ++ services/bright/lib/bright/images.ex | 79 ++++- services/bright/lib/bright/mailer.ex | 3 - .../oban_workers/create_hls_playlist.ex | 269 +++------------- .../oban_workers/create_hls_playlist.ex.old | 238 ++++++++++++++ .../bright/oban_workers/create_thumbnail.ex | 9 - services/bright/lib/bright/orders.ex | 231 -------------- .../bright/lib/bright/orders/line_item.ex | 21 -- services/bright/lib/bright/orders/order.ex | 21 -- services/bright/lib/bright/shopping_cart.ex | 272 ---------------- services/bright/lib/bright/storage.ex | 82 +++++ services/bright/lib/bright/streams.ex | 178 +++++++++++ services/bright/lib/bright/streams/vod.ex | 4 +- services/bright/lib/bright/user.ex | 145 --------- services/bright/lib/bright/user_from_auth.ex | 70 ----- services/bright/lib/bright/vtubers.ex | 8 +- .../components/layouts/root.html.heex | 49 ++- .../lib/bright_web/controllers/auth_html.ex | 7 - .../controllers/auth_html/request.html.heex | 1 - .../bright_web/controllers/cart_controller.ex | 32 -- .../lib/bright_web/controllers/cart_html.ex | 10 - .../controllers/cart_html/show.html.heex | 26 -- .../controllers/cart_item_controller.ex | 22 -- .../controllers/hello_controller.ex | 17 - .../lib/bright_web/controllers/hello_html.ex | 13 - .../controllers/hello_html/index.html.heex | 3 - .../controllers/hello_html/show.html.heex | 3 - .../controllers/order_controller.ex | 21 -- .../lib/bright_web/controllers/order_html.ex | 4 - .../controllers/order_html/show.html.heex | 20 -- .../bright_web/controllers/page_controller.ex | 8 +- .../controllers/page_html/about.html.heex | 12 +- .../controllers/page_html/home.html.heex | 14 - .../controllers/page_html/profile.html.heex | 47 +++ .../controllers/product_controller.ex | 62 ---- .../bright_web/controllers/product_html.ex | 23 -- .../controllers/product_html/edit.html.heex | 8 - .../controllers/product_html/index.html.heex | 26 -- .../controllers/product_html/new.html.heex | 8 - .../product_html/product_form.html.heex | 12 - .../controllers/product_html/show.html.heex | 26 -- .../bright_web/controllers/user_controller.ex | 59 ---- .../lib/bright_web/controllers/user_html.ex | 4 - .../controllers/user_html/join.html.heex | 7 - .../controllers/user_html/show.html.heex | 11 - .../controllers/user_html/user_form.html.heex | 12 - .../controllers/user_session_controller.ex | 42 +++ .../bright_web/controllers/vod_controller.ex | 4 + .../controllers/vod_html/index.html.heex | 1 + .../controllers/vod_html/show.html.heex | 3 +- .../controllers/vod_html/vod_form.html.heex | 2 + .../live/post_live/form_component.ex | 83 ----- .../lib/bright_web/live/post_live/index.ex | 47 --- .../bright_web/live/post_live/index.html.heex | 42 --- .../lib/bright_web/live/post_live/show.ex | 21 -- .../bright_web/live/post_live/show.html.heex | 34 -- .../lib/bright_web/live/thermostat_live.ex | 19 -- services/bright/lib/bright_web/router.ex | 87 ++---- .../auth_controller.ex => user_auth.ex} | 290 ++++++++---------- services/bright/mix.exs | 16 +- services/bright/mix.lock | 67 +++- .../20250126020211_remove_auth_and_users.exs | 16 + ...0250126022331_create_users_auth_tables.exs | 29 ++ ...20250126200332_add_uploaded_by_to_vods.exs | 11 + .../20250127063219_add_github_id.exs | 9 + .../20250127072102_add_user_avatar_name.exs | 10 + ...250127073215_remove_email_and_password.exs | 11 + .../20250128040801_add_local_path.exs | 9 + .../20250128043513_add_duration.exs | 9 + services/bright/test.txt | 3 - services/bright/test/bright/images_test.exs | 21 +- .../oban_workers/create_thumbnail_test.exs | 14 + services/bright/test/bright/streams_test.exs | 45 ++- .../test/bright_web/live/post_live_test.exs | 113 ------- .../bright/test/bright_web/user_auth_test.exs | 272 ---------------- services/bright/test/fixtures/test-fixture.ts | Bin 0 -> 1832436 bytes .../test/support/fixtures/streams_fixtures.ex | 5 - 91 files changed, 1388 insertions(+), 2781 deletions(-) delete mode 100644 services/bright/lib/bright/blog.ex delete mode 100644 services/bright/lib/bright/blog/post.ex delete mode 100644 services/bright/lib/bright/catalog.ex delete mode 100644 services/bright/lib/bright/catalog/category.ex delete mode 100644 services/bright/lib/bright/catalog/product.ex create mode 100644 services/bright/lib/bright/events.ex delete mode 100644 services/bright/lib/bright/mailer.ex create mode 100644 services/bright/lib/bright/oban_workers/create_hls_playlist.ex.old delete mode 100644 services/bright/lib/bright/orders.ex delete mode 100644 services/bright/lib/bright/orders/line_item.ex delete mode 100644 services/bright/lib/bright/orders/order.ex delete mode 100644 services/bright/lib/bright/shopping_cart.ex create mode 100644 services/bright/lib/bright/storage.ex delete mode 100644 services/bright/lib/bright/user.ex delete mode 100644 services/bright/lib/bright/user_from_auth.ex delete mode 100644 services/bright/lib/bright_web/controllers/auth_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/auth_html/request.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/cart_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/cart_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/cart_html/show.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/cart_item_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/hello_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/hello_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/hello_html/index.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/hello_html/show.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/order_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/order_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/order_html/show.html.heex create mode 100644 services/bright/lib/bright_web/controllers/page_html/profile.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/product_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/product_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/product_html/edit.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/product_html/index.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/product_html/new.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/product_html/product_form.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/product_html/show.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/user_controller.ex delete mode 100644 services/bright/lib/bright_web/controllers/user_html.ex delete mode 100644 services/bright/lib/bright_web/controllers/user_html/join.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/user_html/show.html.heex delete mode 100644 services/bright/lib/bright_web/controllers/user_html/user_form.html.heex create mode 100644 services/bright/lib/bright_web/controllers/user_session_controller.ex delete mode 100644 services/bright/lib/bright_web/live/post_live/form_component.ex delete mode 100644 services/bright/lib/bright_web/live/post_live/index.ex delete mode 100644 services/bright/lib/bright_web/live/post_live/index.html.heex delete mode 100644 services/bright/lib/bright_web/live/post_live/show.ex delete mode 100644 services/bright/lib/bright_web/live/post_live/show.html.heex delete mode 100644 services/bright/lib/bright_web/live/thermostat_live.ex rename services/bright/lib/bright_web/{controllers/auth_controller.ex => user_auth.ex} (50%) create mode 100644 services/bright/priv/repo/migrations/20250126020211_remove_auth_and_users.exs create mode 100644 services/bright/priv/repo/migrations/20250126022331_create_users_auth_tables.exs create mode 100644 services/bright/priv/repo/migrations/20250126200332_add_uploaded_by_to_vods.exs create mode 100644 services/bright/priv/repo/migrations/20250127063219_add_github_id.exs create mode 100644 services/bright/priv/repo/migrations/20250127072102_add_user_avatar_name.exs create mode 100644 services/bright/priv/repo/migrations/20250127073215_remove_email_and_password.exs create mode 100644 services/bright/priv/repo/migrations/20250128040801_add_local_path.exs create mode 100644 services/bright/priv/repo/migrations/20250128043513_add_duration.exs delete mode 100644 services/bright/test.txt delete mode 100644 services/bright/test/bright_web/live/post_live_test.exs delete mode 100644 services/bright/test/bright_web/user_auth_test.exs create mode 100644 services/bright/test/fixtures/test-fixture.ts diff --git a/devbox.json b/devbox.json index 962b648..af53baf 100644 --- a/devbox.json +++ b/devbox.json @@ -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" } } diff --git a/devbox.lock b/devbox.lock index 3fea3f4..65fd043 100644 --- a/devbox.lock +++ b/devbox.lock @@ -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", diff --git a/docker-compose.yml b/docker-compose.yml index 87c4280..cc7b53e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/dockerfiles/bright.dockerfile b/dockerfiles/bright.dockerfile index 51413ee..a9e6573 100644 --- a/dockerfiles/bright.dockerfile +++ b/dockerfiles/bright.dockerfile @@ -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"] diff --git a/services/bright/config/config.exs b/services/bright/config/config.exs index 1a4d2db..64affc0 100644 --- a/services/bright/config/config.exs +++ b/services/bright/config/config.exs @@ -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: [ diff --git a/services/bright/config/runtime.exs b/services/bright/config/runtime.exs index 08531d5..8b76c73 100644 --- a/services/bright/config/runtime.exs +++ b/services/bright/config/runtime.exs @@ -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, diff --git a/services/bright/lib/bright.ex b/services/bright/lib/bright.ex index 55d586e..cb24ebd 100644 --- a/services/bright/lib/bright.ex +++ b/services/bright/lib/bright.ex @@ -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 diff --git a/services/bright/lib/bright/blog.ex b/services/bright/lib/bright/blog.ex deleted file mode 100644 index 2e891fb..0000000 --- a/services/bright/lib/bright/blog.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/blog/post.ex b/services/bright/lib/bright/blog/post.ex deleted file mode 100644 index e5bc353..0000000 --- a/services/bright/lib/bright/blog/post.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/cache.ex b/services/bright/lib/bright/cache.ex index 24d2cef..ec32a92 100644 --- a/services/bright/lib/bright/cache.ex +++ b/services/bright/lib/bright/cache.ex @@ -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) diff --git a/services/bright/lib/bright/catalog.ex b/services/bright/lib/bright/catalog.ex deleted file mode 100644 index 5ced820..0000000 --- a/services/bright/lib/bright/catalog.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/catalog/category.ex b/services/bright/lib/bright/catalog/category.ex deleted file mode 100644 index 50ac10a..0000000 --- a/services/bright/lib/bright/catalog/category.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/catalog/product.ex b/services/bright/lib/bright/catalog/product.ex deleted file mode 100644 index cee7218..0000000 --- a/services/bright/lib/bright/catalog/product.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/downloader.ex b/services/bright/lib/bright/downloader.ex index 16354fd..2660e75 100644 --- a/services/bright/lib/bright/downloader.ex +++ b/services/bright/lib/bright/downloader.ex @@ -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]) diff --git a/services/bright/lib/bright/events.ex b/services/bright/lib/bright/events.ex new file mode 100644 index 0000000..28891ff --- /dev/null +++ b/services/bright/lib/bright/events.ex @@ -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 diff --git a/services/bright/lib/bright/images.ex b/services/bright/lib/bright/images.ex index 71b5001..f50d619 100644 --- a/services/bright/lib/bright/images.ex +++ b/services/bright/lib/bright/images.ex @@ -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 diff --git a/services/bright/lib/bright/mailer.ex b/services/bright/lib/bright/mailer.ex deleted file mode 100644 index 1c82853..0000000 --- a/services/bright/lib/bright/mailer.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule Bright.Mailer do - use Swoosh.Mailer, otp_app: :bright -end diff --git a/services/bright/lib/bright/oban_workers/create_hls_playlist.ex b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex index c2d9645..98990ec 100644 --- a/services/bright/lib/bright/oban_workers/create_hls_playlist.ex +++ b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex @@ -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/: #{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 diff --git a/services/bright/lib/bright/oban_workers/create_hls_playlist.ex.old b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex.old new file mode 100644 index 0000000..c924bdb --- /dev/null +++ b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex.old @@ -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/: #{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 diff --git a/services/bright/lib/bright/oban_workers/create_thumbnail.ex b/services/bright/lib/bright/oban_workers/create_thumbnail.ex index 18ace3d..0113846 100644 --- a/services/bright/lib/bright/oban_workers/create_thumbnail.ex +++ b/services/bright/lib/bright/oban_workers/create_thumbnail.ex @@ -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} diff --git a/services/bright/lib/bright/orders.ex b/services/bright/lib/bright/orders.ex deleted file mode 100644 index 458506b..0000000 --- a/services/bright/lib/bright/orders.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/orders/line_item.ex b/services/bright/lib/bright/orders/line_item.ex deleted file mode 100644 index a94009f..0000000 --- a/services/bright/lib/bright/orders/line_item.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/orders/order.ex b/services/bright/lib/bright/orders/order.ex deleted file mode 100644 index 44c5ba7..0000000 --- a/services/bright/lib/bright/orders/order.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/shopping_cart.ex b/services/bright/lib/bright/shopping_cart.ex deleted file mode 100644 index d2ecdb1..0000000 --- a/services/bright/lib/bright/shopping_cart.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/storage.ex b/services/bright/lib/bright/storage.ex new file mode 100644 index 0000000..84e5e3a --- /dev/null +++ b/services/bright/lib/bright/storage.ex @@ -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 diff --git a/services/bright/lib/bright/streams.ex b/services/bright/lib/bright/streams.ex index 5e41794..21ff355 100644 --- a/services/bright/lib/bright/streams.ex +++ b/services/bright/lib/bright/streams.ex @@ -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 diff --git a/services/bright/lib/bright/streams/vod.ex b/services/bright/lib/bright/streams/vod.ex index 0cf648e..87d1cb6 100644 --- a/services/bright/lib/bright/streams/vod.ex +++ b/services/bright/lib/bright/streams/vod.ex @@ -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 diff --git a/services/bright/lib/bright/user.ex b/services/bright/lib/bright/user.ex deleted file mode 100644 index 3e04b20..0000000 --- a/services/bright/lib/bright/user.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/user_from_auth.ex b/services/bright/lib/bright/user_from_auth.ex deleted file mode 100644 index 440f1b6..0000000 --- a/services/bright/lib/bright/user_from_auth.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/vtubers.ex b/services/bright/lib/bright/vtubers.ex index 0cc7130..7461ed6 100644 --- a/services/bright/lib/bright/vtubers.ex +++ b/services/bright/lib/bright/vtubers.ex @@ -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 diff --git a/services/bright/lib/bright_web/components/layouts/root.html.heex b/services/bright/lib/bright_web/components/layouts/root.html.heex index 00c461a..6aac60c 100644 --- a/services/bright/lib/bright_web/components/layouts/root.html.heex +++ b/services/bright/lib/bright_web/components/layouts/root.html.heex @@ -118,11 +118,13 @@ <% else %> <.link - href={~p"/users/register"} + href={~p"/auth/github"} + method="get" class="navbar-item" > - Register + Sign in via GH + <%#

hello

%> <%# <.link href={~p"/auth/github"} class="navbar-item" @@ -139,6 +141,49 @@ +
    + <%= if @current_user do %> +
  • +
    + {@current_user.name} +
    +
  • +
  • + <.link + href={~p"/users/settings"} + class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" + > + Settings + +
  • +
  • + <.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 + +
  • + <% else %> +
  • + <.link + href={~p"/users/register"} + class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" + > + Register + +
  • +
  • + <.link + href={~p"/users/log_in"} + class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700" + > + Log in + +
  • + <% end %> +
<%= @inner_content %> diff --git a/services/bright/lib/bright_web/controllers/auth_html.ex b/services/bright/lib/bright_web/controllers/auth_html.ex deleted file mode 100644 index 04a3779..0000000 --- a/services/bright/lib/bright_web/controllers/auth_html.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule BrightWeb.AuthHTML do - use BrightWeb, :html - - - embed_templates "auth_html/*" - -end diff --git a/services/bright/lib/bright_web/controllers/auth_html/request.html.heex b/services/bright/lib/bright_web/controllers/auth_html/request.html.heex deleted file mode 100644 index 0387678..0000000 --- a/services/bright/lib/bright_web/controllers/auth_html/request.html.heex +++ /dev/null @@ -1 +0,0 @@ -

hello this is request.html

\ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/cart_controller.ex b/services/bright/lib/bright_web/controllers/cart_controller.ex deleted file mode 100644 index 9f52a2d..0000000 --- a/services/bright/lib/bright_web/controllers/cart_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/cart_html.ex b/services/bright/lib/bright_web/controllers/cart_html.ex deleted file mode 100644 index c4e25d5..0000000 --- a/services/bright/lib/bright_web/controllers/cart_html.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/cart_html/show.html.heex b/services/bright/lib/bright_web/controllers/cart_html/show.html.heex deleted file mode 100644 index 46e9ccf..0000000 --- a/services/bright/lib/bright_web/controllers/cart_html/show.html.heex +++ /dev/null @@ -1,26 +0,0 @@ - - -<.header> - My Cart - <:subtitle :if={@cart.items == []}>Your cart is empty - <:actions> - <.link href={~p"/orders"} method="post"> - <.button>Complete order - - - - -
- <.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))} - - <:actions> - <.button>Update cart - - - Total: {currency_to_str(ShoppingCart.total_cart_price(@cart))} -
- -<.back navigate={~p"/products"}>Back to products diff --git a/services/bright/lib/bright_web/controllers/cart_item_controller.ex b/services/bright/lib/bright_web/controllers/cart_item_controller.ex deleted file mode 100644 index 3372521..0000000 --- a/services/bright/lib/bright_web/controllers/cart_item_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/hello_controller.ex b/services/bright/lib/bright_web/controllers/hello_controller.ex deleted file mode 100644 index 1d3a3ef..0000000 --- a/services/bright/lib/bright_web/controllers/hello_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/hello_html.ex b/services/bright/lib/bright_web/controllers/hello_html.ex deleted file mode 100644 index 40a235d..0000000 --- a/services/bright/lib/bright_web/controllers/hello_html.ex +++ /dev/null @@ -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""" -

Hello World, from {@messenger}!

- """ - end -end diff --git a/services/bright/lib/bright_web/controllers/hello_html/index.html.heex b/services/bright/lib/bright_web/controllers/hello_html/index.html.heex deleted file mode 100644 index b01c4a2..0000000 --- a/services/bright/lib/bright_web/controllers/hello_html/index.html.heex +++ /dev/null @@ -1,3 +0,0 @@ -
-

Hello World, from Phoenix!~

-
\ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/hello_html/show.html.heex b/services/bright/lib/bright_web/controllers/hello_html/show.html.heex deleted file mode 100644 index 5cc1584..0000000 --- a/services/bright/lib/bright_web/controllers/hello_html/show.html.heex +++ /dev/null @@ -1,3 +0,0 @@ -
- <.greet messenger={@messenger} /> -
\ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/order_controller.ex b/services/bright/lib/bright_web/controllers/order_controller.ex deleted file mode 100644 index 88a8440..0000000 --- a/services/bright/lib/bright_web/controllers/order_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/order_html.ex b/services/bright/lib/bright_web/controllers/order_html.ex deleted file mode 100644 index 5813054..0000000 --- a/services/bright/lib/bright_web/controllers/order_html.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule BrightWeb.OrderHTML do - use BrightWeb, :html - embed_templates "order_html/*" -end diff --git a/services/bright/lib/bright_web/controllers/order_html/show.html.heex b/services/bright/lib/bright_web/controllers/order_html/show.html.heex deleted file mode 100644 index df41e43..0000000 --- a/services/bright/lib/bright_web/controllers/order_html/show.html.heex +++ /dev/null @@ -1,20 +0,0 @@ -<.header> - Thank you for your order! - <:subtitle> - User uuid: {@order.user_uuid} - - - - -<.table id="items" rows={@order.line_items}> - <:col :let={item} label="Title">{item.product.title} - <:col :let={item} label="Quantity">{item.quantity} - <:col :let={item} label="Price"> - {BrightWeb.CartHTML.currency_to_str(item.price)} - - - -Total price: -{BrightWeb.CartHTML.currency_to_str(@order.total_price)} - -<.back navigate={~p"/products"}>Back to products \ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/page_controller.ex b/services/bright/lib/bright_web/controllers/page_controller.ex index e95330a..6ba6f1d 100644 --- a/services/bright/lib/bright_web/controllers/page_controller.ex +++ b/services/bright/lib/bright_web/controllers/page_controller.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/page_html/about.html.heex b/services/bright/lib/bright_web/controllers/page_html/about.html.heex index 7987ab5..ae9e19f 100644 --- a/services/bright/lib/bright_web/controllers/page_html/about.html.heex +++ b/services/bright/lib/bright_web/controllers/page_html/about.html.heex @@ -3,10 +3,16 @@
-
-

About

+
+
+

Dedication to the preservation of Lewdtuber history

+

+
+
-

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 doesn’t 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.

+
+ +

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 doesn’t 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.

diff --git a/services/bright/lib/bright_web/controllers/page_html/home.html.heex b/services/bright/lib/bright_web/controllers/page_html/home.html.heex index ad2b3d5..dca830e 100644 --- a/services/bright/lib/bright_web/controllers/page_html/home.html.heex +++ b/services/bright/lib/bright_web/controllers/page_html/home.html.heex @@ -26,20 +26,6 @@
- - diff --git a/services/bright/lib/bright_web/controllers/page_html/profile.html.heex b/services/bright/lib/bright_web/controllers/page_html/profile.html.heex new file mode 100644 index 0000000..dbadb58 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/page_html/profile.html.heex @@ -0,0 +1,47 @@ +<.flash_group flash={@flash} /> + + +<%= if @current_user do%> +
+

Profile

+

{@current_user.name}

+ + +
+ + +
+ +
+
+
+
+
+ {@current_user.name} +
+
+
+

{@current_user.name}

+

Github User {@current_user.github_id}

+
+
+ +
+

Futureporn User {@current_user.id}

+

n uploads

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec + iaculis mauris. @bulmaio. #css + #responsive +
+ +
+
+
+ +
+<% else %> +

Please <.link href={~p"/auth/github"}>sign in

+<% end %> \ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/product_controller.ex b/services/bright/lib/bright_web/controllers/product_controller.ex deleted file mode 100644 index 5984005..0000000 --- a/services/bright/lib/bright_web/controllers/product_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/product_html.ex b/services/bright/lib/bright_web/controllers/product_html.ex deleted file mode 100644 index 1ad749a..0000000 --- a/services/bright/lib/bright_web/controllers/product_html.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/product_html/edit.html.heex b/services/bright/lib/bright_web/controllers/product_html/edit.html.heex deleted file mode 100644 index 5179947..0000000 --- a/services/bright/lib/bright_web/controllers/product_html/edit.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - Edit Product {@product.id} - <:subtitle>Use this form to manage product records in the database. - - -<.product_form changeset={@changeset} action={~p"/products/#{@product}"} /> - -<.back navigate={~p"/products"}>Back to products diff --git a/services/bright/lib/bright_web/controllers/product_html/index.html.heex b/services/bright/lib/bright_web/controllers/product_html/index.html.heex deleted file mode 100644 index 687b052..0000000 --- a/services/bright/lib/bright_web/controllers/product_html/index.html.heex +++ /dev/null @@ -1,26 +0,0 @@ -<.header> - Listing Products - <:actions> - <.link href={~p"/products/new"}> - <.button>New Product - - - - -<.table id="products" rows={@products} row_click={&JS.navigate(~p"/products/#{&1}")}> - <:col :let={product} label="Title">{product.title} - <:col :let={product} label="Description">{product.description} - <:col :let={product} label="Price">{product.price} - <:col :let={product} label="Views">{product.views} - <:action :let={product}> -
- <.link navigate={~p"/products/#{product}"}>Show -
- <.link navigate={~p"/products/#{product}/edit"}>Edit - - <:action :let={product}> - <.link href={~p"/products/#{product}"} method="delete" data-confirm="Are you sure?"> - Delete - - - diff --git a/services/bright/lib/bright_web/controllers/product_html/new.html.heex b/services/bright/lib/bright_web/controllers/product_html/new.html.heex deleted file mode 100644 index e64df6b..0000000 --- a/services/bright/lib/bright_web/controllers/product_html/new.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - New Product - <:subtitle>Use this form to manage product records in the database. - - -<.product_form changeset={@changeset} action={~p"/products"} /> - -<.back navigate={~p"/products"}>Back to products diff --git a/services/bright/lib/bright_web/controllers/product_html/product_form.html.heex b/services/bright/lib/bright_web/controllers/product_html/product_form.html.heex deleted file mode 100644 index d2dc1ba..0000000 --- a/services/bright/lib/bright_web/controllers/product_html/product_form.html.heex +++ /dev/null @@ -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. - - <.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 - - diff --git a/services/bright/lib/bright_web/controllers/product_html/show.html.heex b/services/bright/lib/bright_web/controllers/product_html/show.html.heex deleted file mode 100644 index a29e6f7..0000000 --- a/services/bright/lib/bright_web/controllers/product_html/show.html.heex +++ /dev/null @@ -1,26 +0,0 @@ -<.header> - Product {@product.id} - <:subtitle>This is a product record from the database. - <:actions> - <.link href={~p"/products/#{@product}/edit"}> - <.button>Edit product - - <.link href={~p"/cart_items?product_id=#{@product.id}"} method="post"> - <.button>Add to cart - - - - -<.list> - <:item title="Title">{@product.title} - <:item title="Description">{@product.description} - <:item title="Price">{@product.price} - <:item title="Views">{@product.views} - <:item title="Categories"> -
    -
  • {cat.title}
  • -
- - - -<.back navigate={~p"/products"}>Back to products diff --git a/services/bright/lib/bright_web/controllers/user_controller.ex b/services/bright/lib/bright_web/controllers/user_controller.ex deleted file mode 100644 index c9c922b..0000000 --- a/services/bright/lib/bright_web/controllers/user_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/controllers/user_html.ex b/services/bright/lib/bright_web/controllers/user_html.ex deleted file mode 100644 index 60eac6b..0000000 --- a/services/bright/lib/bright_web/controllers/user_html.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule BrightWeb.UserHTML do - use BrightWeb, :html - embed_templates "user_html/*" -end diff --git a/services/bright/lib/bright_web/controllers/user_html/join.html.heex b/services/bright/lib/bright_web/controllers/user_html/join.html.heex deleted file mode 100644 index ed6e1ab..0000000 --- a/services/bright/lib/bright_web/controllers/user_html/join.html.heex +++ /dev/null @@ -1,7 +0,0 @@ - -<.header> - Join Futureporn - - - -<.user_form changeset={@changeset} action={~p"/join"} /> \ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/user_html/show.html.heex b/services/bright/lib/bright_web/controllers/user_html/show.html.heex deleted file mode 100644 index a595a7e..0000000 --- a/services/bright/lib/bright_web/controllers/user_html/show.html.heex +++ /dev/null @@ -1,11 +0,0 @@ - -<.header> - Visitor Profile - - -<%= if @current_user do %> -

@current_user is {@current_user}

-<% else %> -

there is no @current_user

-<% end %> - diff --git a/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex b/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex deleted file mode 100644 index 989e9bd..0000000 --- a/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex +++ /dev/null @@ -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. - - <.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 - - - diff --git a/services/bright/lib/bright_web/controllers/user_session_controller.ex b/services/bright/lib/bright_web/controllers/user_session_controller.ex new file mode 100644 index 0000000..bb69007 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/user_session_controller.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/vod_controller.ex b/services/bright/lib/bright_web/controllers/vod_controller.ex index 9714258..e4bb204 100644 --- a/services/bright/lib/bright_web/controllers/vod_controller.ex +++ b/services/bright/lib/bright_web/controllers/vod_controller.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/vod_html/index.html.heex b/services/bright/lib/bright_web/controllers/vod_html/index.html.heex index af2e106..6cfd299 100644 --- a/services/bright/lib/bright_web/controllers/vod_html/index.html.heex +++ b/services/bright/lib/bright_web/controllers/vod_html/index.html.heex @@ -8,6 +8,7 @@ <.table id="vods" rows={@vods} row_click={&JS.navigate(~p"/vods/#{&1}")}> + <%# <:col :let={vod} label="Uploader">{vod.uploaded_by_id} %> <:col :let={vod} label="ID">{vod.id} <:col :let={vod} label="S3 CDN URL">{vod.s3_cdn_url} <:col :let={vod} label="Mux asset">{vod.mux_asset_id} diff --git a/services/bright/lib/bright_web/controllers/vod_html/show.html.heex b/services/bright/lib/bright_web/controllers/vod_html/show.html.heex index cace0f5..e0ffe67 100644 --- a/services/bright/lib/bright_web/controllers/vod_html/show.html.heex +++ b/services/bright/lib/bright_web/controllers/vod_html/show.html.heex @@ -46,6 +46,7 @@ <.list> + <%# <:item title="Uploader"> %> <:item title="Source VOD File"> <%= if @vod.s3_cdn_url do %> @@ -53,7 +54,7 @@ <% end %> - <:item title="Thumbnail URL"> + <:item title="Thumbnail"> <:item title="HLS Playlist URL">{@vod.playlist_url} <:item title="Torrent">{@vod.torrent} <:item title="Ipfs CID">{@vod.ipfs_cid} diff --git a/services/bright/lib/bright_web/controllers/vod_html/vod_form.html.heex b/services/bright/lib/bright_web/controllers/vod_html/vod_form.html.heex index 1f9f31b..baa2088 100644 --- a/services/bright/lib/bright_web/controllers/vod_html/vod_form.html.heex +++ b/services/bright/lib/bright_web/controllers/vod_html/vod_form.html.heex @@ -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. @@ -12,6 +13,7 @@ <.input field={f[:notes]} type="textarea" label="Notes" /> <.input field={f[:stream_id]} type="select" label="Stream" multiple={false} options={stream_opts(@changeset)}/> + <:actions> <.button>Save Vod diff --git a/services/bright/lib/bright_web/live/post_live/form_component.ex b/services/bright/lib/bright_web/live/post_live/form_component.ex deleted file mode 100644 index 94db4e6..0000000 --- a/services/bright/lib/bright_web/live/post_live/form_component.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule BrightWeb.PostLive.FormComponent do - use BrightWeb, :live_component - - alias Bright.Blog - - @impl true - def render(assigns) do - ~H""" -
- <.header> - {@title} - <:subtitle>Use this form to manage post records in the database. - - - <.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 - - -
- """ - 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 diff --git a/services/bright/lib/bright_web/live/post_live/index.ex b/services/bright/lib/bright_web/live/post_live/index.ex deleted file mode 100644 index a732076..0000000 --- a/services/bright/lib/bright_web/live/post_live/index.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/live/post_live/index.html.heex b/services/bright/lib/bright_web/live/post_live/index.html.heex deleted file mode 100644 index d361b94..0000000 --- a/services/bright/lib/bright_web/live/post_live/index.html.heex +++ /dev/null @@ -1,42 +0,0 @@ -<.header> - Listing Posts - <:actions> - <.link patch={~p"/posts/new"}> - <.button>New Post - - - - -<.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 :let={{_id, post}} label="Body">{post.body} - <:action :let={{_id, post}}> -
- <.link navigate={~p"/posts/#{post}"}>Show -
- <.link patch={~p"/posts/#{post}/edit"}>Edit - - <:action :let={{id, post}}> - <.link - phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")} - data-confirm="Are you sure?" - > - Delete - - - - -<.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"} - /> - diff --git a/services/bright/lib/bright_web/live/post_live/show.ex b/services/bright/lib/bright_web/live/post_live/show.ex deleted file mode 100644 index c974a2c..0000000 --- a/services/bright/lib/bright_web/live/post_live/show.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/live/post_live/show.html.heex b/services/bright/lib/bright_web/live/post_live/show.html.heex deleted file mode 100644 index 954f97e..0000000 --- a/services/bright/lib/bright_web/live/post_live/show.html.heex +++ /dev/null @@ -1,34 +0,0 @@ -<.header> - Post {@post.id} - <:subtitle>This is a post record from the database. - <:actions> - <.link patch={~p"/posts/#{@post}/show/edit"} phx-click={JS.push_focus()}> - <.button>Edit post - - - - - - - - - - - -<.list> - <:item title="Title">{@post.title} - <:item title="Body">{@post.body} - - -<.back navigate={~p"/posts"}>Back to posts - -<.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}"} - /> - diff --git a/services/bright/lib/bright_web/live/thermostat_live.ex b/services/bright/lib/bright_web/live/thermostat_live.ex deleted file mode 100644 index 280db96..0000000 --- a/services/bright/lib/bright_web/live/thermostat_live.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule BrightWeb.ThermostatLive do - use BrightWeb, :live_view - - def render(assigns) do - ~H""" - Current temperature: {@temperature}°F - - """ - 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 diff --git a/services/bright/lib/bright_web/router.ex b/services/bright/lib/bright_web/router.ex index f6a9c4a..259c61f 100644 --- a/services/bright/lib/bright_web/router.ex +++ b/services/bright/lib/bright_web/router.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/auth_controller.ex b/services/bright/lib/bright_web/user_auth.ex similarity index 50% rename from services/bright/lib/bright_web/controllers/auth_controller.ex rename to services/bright/lib/bright_web/user_auth.ex index 5f4c07d..0f1e022 100644 --- a/services/bright/lib/bright_web/controllers/auth_controller.ex +++ b/services/bright/lib/bright_web/user_auth.ex @@ -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 diff --git a/services/bright/mix.exs b/services/bright/mix.exs index bee29b2..a52f834 100644 --- a/services/bright/mix.exs +++ b/services/bright/mix.exs @@ -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 diff --git a/services/bright/mix.lock b/services/bright/mix.lock index cee8fe5..caff7b3 100644 --- a/services/bright/mix.lock +++ b/services/bright/mix.lock @@ -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"}, } diff --git a/services/bright/priv/repo/migrations/20250126020211_remove_auth_and_users.exs b/services/bright/priv/repo/migrations/20250126020211_remove_auth_and_users.exs new file mode 100644 index 0000000..49df9b7 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250126020211_remove_auth_and_users.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250126022331_create_users_auth_tables.exs b/services/bright/priv/repo/migrations/20250126022331_create_users_auth_tables.exs new file mode 100644 index 0000000..45039cd --- /dev/null +++ b/services/bright/priv/repo/migrations/20250126022331_create_users_auth_tables.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250126200332_add_uploaded_by_to_vods.exs b/services/bright/priv/repo/migrations/20250126200332_add_uploaded_by_to_vods.exs new file mode 100644 index 0000000..3de3c9e --- /dev/null +++ b/services/bright/priv/repo/migrations/20250126200332_add_uploaded_by_to_vods.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250127063219_add_github_id.exs b/services/bright/priv/repo/migrations/20250127063219_add_github_id.exs new file mode 100644 index 0000000..b0a9bad --- /dev/null +++ b/services/bright/priv/repo/migrations/20250127063219_add_github_id.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250127072102_add_user_avatar_name.exs b/services/bright/priv/repo/migrations/20250127072102_add_user_avatar_name.exs new file mode 100644 index 0000000..0760979 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250127072102_add_user_avatar_name.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250127073215_remove_email_and_password.exs b/services/bright/priv/repo/migrations/20250127073215_remove_email_and_password.exs new file mode 100644 index 0000000..552b301 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250127073215_remove_email_and_password.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250128040801_add_local_path.exs b/services/bright/priv/repo/migrations/20250128040801_add_local_path.exs new file mode 100644 index 0000000..4a1ad4d --- /dev/null +++ b/services/bright/priv/repo/migrations/20250128040801_add_local_path.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250128043513_add_duration.exs b/services/bright/priv/repo/migrations/20250128043513_add_duration.exs new file mode 100644 index 0000000..7133616 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250128043513_add_duration.exs @@ -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 diff --git a/services/bright/test.txt b/services/bright/test.txt deleted file mode 100644 index 875a0a2..0000000 --- a/services/bright/test.txt +++ /dev/null @@ -1,3 +0,0 @@ -HELLO MAMA - -haha \ No newline at end of file diff --git a/services/bright/test/bright/images_test.exs b/services/bright/test/bright/images_test.exs index 39e65f4..509a8c0 100644 --- a/services/bright/test/bright/images_test.exs +++ b/services/bright/test/bright/images_test.exs @@ -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: <> 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 diff --git a/services/bright/test/bright/oban_workers/create_thumbnail_test.exs b/services/bright/test/bright/oban_workers/create_thumbnail_test.exs index 31de3c0..05796c7 100644 --- a/services/bright/test/bright/oban_workers/create_thumbnail_test.exs +++ b/services/bright/test/bright/oban_workers/create_thumbnail_test.exs @@ -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 diff --git a/services/bright/test/bright/streams_test.exs b/services/bright/test/bright/streams_test.exs index 7849dcc..88a5991 100644 --- a/services/bright/test/bright/streams_test.exs +++ b/services/bright/test/bright/streams_test.exs @@ -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 diff --git a/services/bright/test/bright_web/live/post_live_test.exs b/services/bright/test/bright_web/live/post_live_test.exs deleted file mode 100644 index af2b7d9..0000000 --- a/services/bright/test/bright_web/live/post_live_test.exs +++ /dev/null @@ -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'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'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'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 diff --git a/services/bright/test/bright_web/user_auth_test.exs b/services/bright/test/bright_web/user_auth_test.exs deleted file mode 100644 index 07653df..0000000 --- a/services/bright/test/bright_web/user_auth_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/fixtures/test-fixture.ts b/services/bright/test/fixtures/test-fixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..43d67d9b9b27498d2761af45ebebac7867792829 GIT binary patch literal 1832436 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