rindle

Rindle

CI Hex.pm Docs

The CI badge tracks the `ci.yml` workflow run on `main`; that run's verdict is the`CI Summary` gate — the sole required check for merge. GitHub has no native per-check badge, so the workflow-run badge is the meaningful signal. Reproduce that gate locally with `mix ci` (see [CONTRIBUTING](CONTRIBUTING.md)). **Media, made durable.** Phoenix/Ecto-native media lifecycle library. Rindle owns the durable work that happens after upload: session tracking, verification, asset state, variants, background processing, signed delivery, and cleanup. The first-tier adopter concepts are `Rindle` and `Rindle.Profile`: define a profile once, then use the facade for upload lifecycle, attachments, and delivery. This file is the narrow quickstart. [Getting Started](getting_started.html) is the canonical deep adopter guide for the same first-run path. That path is validated in CI from generated Phoenix apps (image-only and AV-enabled install smoke) before each Hex publish. Existing adopters upgrading from the pre-0.1.4 image-only shape should use [Upgrading](upgrading.html) instead of stretching the greenfield quickstart into an upgrade runbook. ## Install Add Rindle to your deps: ```elixir def deps do [ {:rindle, "~> 0.1"} ] end ``` If you use the S3 adapter, also choose an ExAws HTTP client. `:hackney` is the most-tested path in this repo: ```elixir def deps do [ {:rindle, "~> 0.1"}, {:hackney, "~> 1.20"} ] end ``` Run `mix deps.get`. Each release is exercised from a generated Phoenix app in CI before it ships to Hex. Adopters follow the same public setup contract described here and in [Getting Started](getting_started.html). For AV profiles, install `FFmpeg >= 6.0` before you touch background jobs, then run `mix rindle.doctor`. The per-platform install/runtime matrix lives in [Running](running.html). For image variants, install **libvips** on the host before background image processing jobs run (`libvips-dev` on Debian/Ubuntu, `vips` via Homebrew on macOS). See [Running](running.html) for the install matrix. ## Runtime Ownership Rindle persists through your adopter-owned Repo. Configure that explicitly: ```elixir config :rindle, :repo, MyApp.Repo ``` Rindle also requires the default `Oban` path for background work. Adopters own the Oban supervision tree, queue config, and default Oban Repo: ```elixir config :my_app, Oban, repo: MyApp.Repo, queues: [ rindle_promote: 5, rindle_process: 10, rindle_purge: 2, rindle_maintenance: 1 ] ``` ## Migrations Run your host app migrations and the packaged Rindle migrations explicitly: ```elixir rindle_path = Application.app_dir(:rindle, "priv/repo/migrations") host_path = Path.join([File.cwd!(), "priv", "repo", "migrations"]) {:ok, _, _} = Ecto.Migrator.with_repo(MyApp.Repo, fn repo -> for path <- [host_path, rindle_path] do Ecto.Migrator.run(repo, path, :up, all: true) end end) ``` Rindle does not ship a public `mix rindle.*` install task for migrations. The public path is the docs snippet above. ## First Run: AV Quickstart The locked onboarding path is: 1. `mix deps.get` 2. install `FFmpeg >= 6.0` from [Running](running.html) 3. declare one `kind: :video` variant plus the stock poster 4. run `mix rindle.doctor` 5. follow the normal facade-first upload lifecycle The canonical deep guide expands the same path in [Getting Started](getting_started.html). The stock onboarding story is `Rindle.Profile.Presets.Web`: `web_720p` video output plus `poster` image output. The equivalent explicit profile looks like this: ```elixir defmodule MyApp.VideoProfile do use Rindle.Profile, storage: Rindle.Storage.S3, variants: [ web_720p: [kind: :video, preset: :web_720p], poster: [kind: :image, preset: :video_poster_scene] ], allow_mime: ["video/mp4", "video/quicktime", "video/webm"], max_bytes: 250_000_000 end ``` If you prefer the stock preset module directly, use `Rindle.Profile.Presets.Web` with the same storage and upload constraints, then verify the host with: ```bash mix rindle.doctor ``` Once the runtime is healthy, the first-run path is still direct upload by presigned PUT. Multipart upload is supported, but it is an advanced capability and not the default onboarding story. ```elixir {:ok, session} = Rindle.initiate_upload(MyApp.VideoProfile, filename: "clip.mp4") {:ok, %{session: signed, presigned: presigned}} = Rindle.Upload.Broker.sign_url(session.id) # your client PUTs bytes to presigned.url {:ok, %{session: completed, asset: asset}} = Rindle.verify_completion(session.id) {:ok, attachment} = Rindle.attach(asset.id, current_user, "hero_video") {:ok, signed_url} = Rindle.url(MyApp.VideoProfile, asset.storage_key) ``` That keeps the first-run story on the facade while leaving `Rindle.Upload.Broker.sign_url/1` as an advanced transport step. ## After First Run: Querying Attachments and Variants Once an asset is attached, you'll typically render it from a Phoenix controller or LiveView. Two helpers cover the common reads without writing raw Ecto queries: ```elixir # In MyAppWeb.UserController.show/2 def show(conn, _params) do user = conn.assigns.current_user {avatar, thumbs} = case Rindle.attachment_for(user, "avatar") do %{asset: asset} = attachment -> {attachment, Rindle.ready_variants_for(asset)} nil -> {nil, []} end # avatar is %Rindle.Domain.MediaAttachment{} | nil # thumbs is [] when no attachment exists render(conn, :show, avatar: avatar, thumbs: thumbs) end ``` `Rindle.attachment_for/2` returns the most recent attachment for an `(owner, slot)` pair (tie-broken by `inserted_at desc`) with `:asset` preloaded. Pass `Rindle.attachment_for(user, "avatar", preload: [:asset, :variants])` to override the preload list (REPLACE semantics, not merge). `Rindle.ready_variants_for/1` accepts either a `%MediaAsset{}` struct or a binary asset id and returns variants with `state == "ready"`, ordered by `:name asc`. Pending or failed variants are filtered out. ### Bang Variants Five bang variants are available for happy-path code that prefers exceptions over `{:error, reason}` tuples. Each delegates to its non-bang twin and raises `Rindle.Error` on generic failures: ```elixir # Raises Rindle.Error{action: :attach, reason: :not_found} if the asset is missing. attachment = Rindle.attach!(asset.id, current_user, "avatar") # Raises Rindle.Error{action: :detach, reason: ...} on storage failure. :ok = Rindle.detach!(current_user, "avatar") # Raises Rindle.Error{action: :upload, reason: ...} on validation/storage failure. asset = Rindle.upload!(MyApp.MediaProfile, %{ path: "/tmp/photo.png", filename: "photo.png", byte_size: File.stat!("/tmp/photo.png").size }) # Raises Rindle.Error{action: :url, reason: :delivery_unsupported} if the # configured storage adapter does not advertise :signed_url capability. signed = Rindle.url!(MyApp.MediaProfile, asset.storage_key) # Raises Rindle.Error{action: :variant_url, reason: :variant_not_ready} if # the named variant has not finished processing. thumb_url = Rindle.variant_url!(MyApp.MediaProfile, asset, :thumb) ``` Bangs are intended for happy-path callers (controllers, scripts, tests). For user-facing flows that must render validation errors, prefer the non-bang twins (`Rindle.attach/4`, `Rindle.detach/3`, `Rindle.upload/3`, `Rindle.url/3`, `Rindle.variant_url/4`) which return `{:ok, value}` / `{:error, reason}` tuples. ## Streaming with Mux (optional) For HLS streaming via signed playback URLs, opt a profile into a streaming provider: ```elixir defmodule MyApp.Streaming do use Rindle.Profile.Presets.MuxWeb, storage: Rindle.Storage.S3, allow_mime: ["video/mp4", "video/quicktime", "video/webm"], max_bytes: 524_288_000 end ``` End-to-end onboarding — signing keys, webhook plug, cron, local tunnel, secret rotation, and `mix rindle.doctor --streaming` — lives in [Streaming Providers](streaming_providers.html). ## Storage with GCS (optional) GCS resumable upload is a shipped advanced path, not the canonical first-run story. If you need `Rindle.Storage.GCS`, adopter-owned `MyApp.Goth` and `MyApp.Finch` supervision, bucket CORS, and resumable session hygiene, validate the runtime with `mix rindle.doctor` and use [Storage (GCS)](storage_gcs.html). ## Admin Console (optional) Rindle ships a mountable, host-authenticated admin console in the package — `Rindle.Admin.Router.rindle_admin/2`. Mount it from an authenticated router scope (LiveDashboard / Oban Web style) for an operator view of assets, upload sessions, variants/jobs, runtime health, and guarded owner-erasure / repair actions. It needs the optional `phoenix_live_view` dependency, the host owns auth, and production refuses unguarded mounts. See [Admin Console](admin_console.html). ## Next Reads - [User Flows](user_flows.html): map your job to the right guide (start here when evaluating) - [Admin Console](admin_console.html): mount the optional host-authenticated operator console - [Upgrading](upgrading.html): existing-adopter upgrade runbook (pre-0.1.4 image-only → current) - [Getting Started](getting_started.html): deep greenfield guide — Repo, Oban, migrations, profiles - [Running](running.html): libvips and FFmpeg install matrix (macOS, Linux, Fly, Heroku, Render, CI) - [Background Processing](background_processing.html): Oban queues and worker behavior - [Storage Capabilities](storage_capabilities.html): adapter capability boundaries ## Documentation conventions Every public `@callback` must be preceded by `@doc """..."""`. Use `@doc false` only for internal compatibility shims. ## License MIT