# Getting Started

This guide walks you through installing PhxMediaLibrary, configuring storage, and adding your first media files.

## Installation

Add `phx_media_library` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:phx_media_library, "~> 0.7.0"},

    # Optional: Image processing (requires libvips)
    {:image, "~> 0.54"},

    # Optional: S3 storage
    {:ex_aws, "~> 2.5"},
    {:ex_aws_s3, "~> 2.5"},
    {:sweet_xml, "~> 0.7"},

    # Optional: Async processing with Oban
    {:oban, "~> 2.18"}
  ]
end
```

> **Note:** The `:image` dependency (libvips) is **optional**. PhxMediaLibrary works for file storage (PDFs, CSVs, documents) without it. Image conversions and responsive images require `:image` to be installed. If it's missing, you'll get clear error messages guiding you to install it.

Then fetch dependencies:

```bash
mix deps.get
```

## Configuration

Add the required configuration to your `config/config.exs`:

```elixir
config :phx_media_library,
  repo: MyApp.Repo,
  default_disk: :local,
  disks: [
    local: [
      adapter: PhxMediaLibrary.Storage.Disk,
      root: "priv/static/uploads",
      base_url: "/uploads"
    ]
  ]
```

### Choosing a mediable_id type

The `mediable_id` column stores the primary key of every associated model.
Because Postgres is strict about column types, this must match the actual type
of those PKs. Three values are supported:

| Value | Database column | Use when |
|-------|----------------|----------|
| `:binary_id` | `uuid` | All your models use UUID primary keys |
| `:integer` | `bigint` | All your models use integer primary keys |
| `:string` | `varchar` | Mixed or non-standard primary key types |

```elixir
# config/config.exs
config :phx_media_library,
  repo: MyApp.Repo,
  mediable_id_type: :string   # or :integer or :binary_id
```

**Default and roadmap**

The current default is `:binary_id`, kept for backwards compatibility with
existing installations — no existing app needs to change anything.

Starting in **0.8**, the default will change to `:string`. A `varchar` column
accepts UUID strings, integer-to-string coercions, and slug values equally
well, making it the best choice for a library used across diverse apps. The
tradeoff is a marginal index overhead compared to a native `bigint` or `uuid`
column on very large tables; if that matters, set `:integer` or `:binary_id`
explicitly.

**Recommendation for new apps:** set `:string` today. You will need no change
when 0.8 ships.

```elixir
# New app — set explicitly now, nothing to change in 0.8
config :phx_media_library, mediable_id_type: :string

# Integer PK app that prefers native bigint
config :phx_media_library, mediable_id_type: :integer

# UUID PK app that prefers native uuid
config :phx_media_library, mediable_id_type: :binary_id
```

The installer generates the migration column to match your choice:

```bash
mix phx_media_library.install                  # binary_id column (default)
mix phx_media_library.install --id-type string  # varchar column
mix phx_media_library.install --id-type integer # bigint column
```

### Storage Options

#### Local Disk (Default)

```elixir
config :phx_media_library,
  disks: [
    local: [
      adapter: PhxMediaLibrary.Storage.Disk,
      root: "priv/static/uploads",
      base_url: "/uploads"
    ]
  ]
```

#### Amazon S3

```elixir
config :phx_media_library,
  default_disk: :s3,
  disks: [
    s3: [
      adapter: PhxMediaLibrary.Storage.S3,
      bucket: "my-bucket",
      region: "us-east-1"
    ]
  ]

# Configure ExAws credentials
config :ex_aws,
  access_key_id: {:system, "AWS_ACCESS_KEY_ID"},
  secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"}
```

See the [Storage guide](storage.md) for custom adapters and advanced configuration.

### Responsive Images (Optional)

```elixir
config :phx_media_library,
  responsive_images: [
    enabled: true,
    widths: [320, 640, 960, 1280, 1920],
    tiny_placeholder: true
  ]
```

### Async Processing with Oban (Optional)

```elixir
config :phx_media_library,
  async_processor: PhxMediaLibrary.AsyncProcessor.Oban
```

## Run the Installer

```bash
mix phx_media_library.install
mix ecto.migrate
```

This generates the `media` table migration with all required fields. Pass
`--id-type` to match the `mediable_id_type` you set in config:

```bash
mix phx_media_library.install --id-type string   # varchar (recommended for new apps)
mix phx_media_library.install --id-type integer  # bigint
mix phx_media_library.install --id-type binary_id # uuid (default)
```

## Tailwind CSS Setup

PhxMediaLibrary ships with styled LiveView components that use Tailwind CSS
utility classes. For Tailwind v4 to detect and include these classes in your
CSS bundle, add the library's source path to your `assets/css/app.css`:

```css
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../lib/my_app_web";

/* PhxMediaLibrary — include both paths to support Hex deps and path deps.
   Tailwind v4 silently ignores paths that don't exist. */
@source "../../deps/phx_media_library/lib";
@source "../../../phx_media_library/lib";
```

> **Why two `@source` lines?** When installed from Hex, the library lives in
> `deps/phx_media_library/lib/`. When used as a path dependency (e.g.
> `{:phx_media_library, path: "../phx_media_library"}`), it lives outside
> your project's `deps/` folder. Tailwind v4 silently skips any `@source`
> path that doesn't exist, so including both ensures the classes are scanned
> regardless of how the dependency is consumed.

## Define Your Schema

PhxMediaLibrary supports two styles for defining collections and conversions. You can use either — or mix them.

### Declarative DSL — nested style (recommended)

Nest `convert` calls inside `collection ... do ... end` blocks so it's
immediately clear which conversions apply to which collections. Collections
without image content (like `:documents`) omit the `do` block — no
conversions will run for those uploads:

```elixir
defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string

    has_media()          # injects has_many :media (all media for this model)
    has_media(:images)   # injects has_many :images (scoped to "images" collection)
    has_media(:avatar)   # injects has_many :avatar (scoped to "avatar" collection)

    timestamps()
  end

  media_collections do
    collection :images, max_files: 20 do
      convert :thumb, width: 150, height: 150, fit: :cover
      convert :preview, width: 800, quality: 85
      convert :banner, width: 1200, height: 400, fit: :crop
    end

    # No conversions for documents — PDFs are stored as-is
    collection :documents, accepts: ~w(application/pdf text/plain)

    collection :avatar, single_file: true, fallback_url: "/images/default.png" do
      convert :thumb, width: 150, height: 150, fit: :cover
    end
  end
end
```

### Declarative DSL — flat style

Define collections and conversions in separate blocks. **Always use the
`:collections` option** to scope conversions explicitly — without it, a
conversion runs for every collection (including non-image ones like
documents, which will cause processing errors):

```elixir
defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  media_collections do
    collection :images, max_files: 20
    collection :documents, accepts: ~w(application/pdf text/plain)
    collection :avatar, single_file: true, fallback_url: "/images/default.png"
  end

  media_conversions do
    convert :thumb, width: 150, height: 150, fit: :cover, collections: [:images, :avatar]
    convert :preview, width: 800, quality: 85, collections: [:images]
    convert :banner, width: 1200, height: 400, fit: :crop, collections: [:images]
  end
end
```

### Function-based approach

```elixir
defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  def media_collections do
    [
      collection(:images),
      collection(:documents, accepts: ~w(application/pdf)),
      collection(:avatar, single_file: true)
    ]
  end

  def media_conversions do
    [
      # Always scope conversions to specific collections
      conversion(:thumb, width: 150, height: 150, fit: :cover, collections: [:images, :avatar]),
      conversion(:preview, width: 800, quality: 85, collections: [:images])
    ]
  end
end
```

The `has_media()` macro injects a polymorphic `has_many :media` association so you can use standard Ecto preloading:

```elixir
post = Repo.get!(Post, id) |> Repo.preload([:media, :images, :avatar])
```

Collection-scoped variants like `has_media(:images)` add a scoped `has_many` filtered by both model type and collection name.

## Add Media

```elixir
# From a file path
{:ok, media} =
  post
  |> PhxMediaLibrary.add("/path/to/image.jpg")
  |> PhxMediaLibrary.to_collection(:images)

# With custom filename and metadata
{:ok, media} =
  post
  |> PhxMediaLibrary.add(upload)
  |> PhxMediaLibrary.using_filename("custom-name.jpg")
  |> PhxMediaLibrary.with_custom_properties(%{"alt" => "My image"})
  |> PhxMediaLibrary.to_collection(:images)

# From a URL
{:ok, media} =
  post
  |> PhxMediaLibrary.add_from_url("https://example.com/image.jpg")
  |> PhxMediaLibrary.to_collection(:images)

# From an authenticated URL
{:ok, media} =
  post
  |> PhxMediaLibrary.add_from_url("https://api.example.com/files/123.pdf",
       headers: [{"Authorization", "Bearer my-token"}],
       timeout: 15_000)
  |> PhxMediaLibrary.to_collection(:documents)

# Bang version raises on error
media = PhxMediaLibrary.to_collection!(adder, :images)
```

> URL downloads validate the scheme (`http`/`https` only), reject `ftp://` and
> `file://` URLs, and automatically store the source URL in
> `custom_properties["source_url"]`.

## Retrieve Media

```elixir
# Get all media in a collection
PhxMediaLibrary.get_media(post, :images)

# Get the first media item
PhxMediaLibrary.get_first_media(post, :images)

# Get URLs
PhxMediaLibrary.get_first_media_url(post, :images)
PhxMediaLibrary.get_first_media_url(post, :images, :thumb)
PhxMediaLibrary.get_first_media_url(post, :avatar, fallback: "/default.jpg")

# Get URL for a specific media item
PhxMediaLibrary.url(media)
PhxMediaLibrary.url(media, :thumb)

# Composable Ecto queries
PhxMediaLibrary.media_query(post, :images)
|> where([m], m.mime_type == "image/png")
|> limit(5)
|> Repo.all()
```

## Metadata Extraction

PhxMediaLibrary automatically extracts metadata from uploaded files and stores
it in the `metadata` field:

```elixir
{:ok, media} =
  post
  |> PhxMediaLibrary.add("/path/to/photo.jpg")
  |> PhxMediaLibrary.to_collection(:images)

media.metadata
# => %{
#   "type" => "image",
#   "format" => "jpeg",
#   "width" => 1920,
#   "height" => 1080,
#   "has_alpha" => false,
#   "exif" => %{"orientation" => 1, ...},
#   "extracted_at" => "2026-02-27T16:00:00Z"
# }
```

Image dimensions and EXIF data require the optional `:image` dependency.
Without it, you still get type classification and format detection.

To skip extraction for a specific upload:

```elixir
post
|> PhxMediaLibrary.add(upload)
|> PhxMediaLibrary.without_metadata()
|> PhxMediaLibrary.to_collection(:images)
```

Or disable globally:

```elixir
config :phx_media_library, extract_metadata: false
```

See the [Collections & Conversions](collections-and-conversions.md) guide for
details on custom extractors and supported metadata fields.

## Delete Media

```elixir
# Delete a single media item (removes files from storage too)
PhxMediaLibrary.delete(media)

# Clear all media in a collection (batch-optimized, single DELETE query)
{:ok, count} = PhxMediaLibrary.clear_collection(post, :images)

# Clear all media for a model (batch-optimized)
{:ok, count} = PhxMediaLibrary.clear_media(post)
```

## Next Steps

- [Collections & Conversions](collections-and-conversions.md) — Validation rules, image processing, responsive images, metadata extraction
- [LiveView Integration](liveview.md) — Drop-in upload and gallery components
- [Storage](storage.md) — Multiple backends, custom adapters
- [Error Handling](error-handling.md) — Tagged tuples, custom exceptions, MIME detection
- [Telemetry](telemetry.md) — Monitoring and observability events (including download events)
- [Advanced Usage](advanced.md) — Reordering, mix tasks, Oban setup, testing strategies