Merge pull request #1 from Mozart409/copilot/add-recent-downloads-endpoint

Add GET /api/media/recent_downloads JSON endpoint
This commit is contained in:
Amadeus 2026-03-04 12:32:03 +01:00 committed by GitHub
commit d752afdee9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 172 additions and 0 deletions

View file

@ -0,0 +1,62 @@
defmodule PinchflatWeb.Api.MediaController do
use PinchflatWeb, :controller
import Ecto.Query, warn: false
alias Pinchflat.Repo
alias Pinchflat.Media.MediaItem
@default_limit 50
@min_limit 1
@max_limit 500
def recent_downloads(conn, params) do
limit =
params
|> Map.get("limit", "#{@default_limit}")
|> parse_int(@default_limit)
|> clamp(@min_limit, @max_limit)
media_items =
from(mi in MediaItem,
where: not is_nil(mi.media_downloaded_at),
order_by: [desc: mi.media_downloaded_at],
limit: ^limit,
select: %{
id: mi.id,
uuid: mi.uuid,
title: mi.title,
media_id: mi.media_id,
source_id: mi.source_id,
uploaded_at: mi.uploaded_at,
media_downloaded_at: mi.media_downloaded_at,
media_filepath: mi.media_filepath,
thumbnail_filepath: mi.thumbnail_filepath,
metadata_filepath: mi.metadata_filepath,
nfo_filepath: mi.nfo_filepath,
subtitle_filepaths: mi.subtitle_filepaths
}
)
|> Repo.all()
conn
|> put_status(:ok)
|> json(%{data: media_items})
end
defp parse_int(value, default) when is_binary(value) do
case Integer.parse(value) do
{int, _} -> int
:error -> default
end
end
defp parse_int(value, _default) when is_integer(value), do: value
defp parse_int(_, default), do: default
defp clamp(value, min_val, max_val) do
value
|> Kernel.max(min_val)
|> Kernel.min(max_val)
end
end

View file

@ -71,6 +71,13 @@ defmodule PinchflatWeb.Router do
get "/healthcheck", HealthController, :check, log: false
end
# No auth or CSRF protection for internal API endpoints
scope "/api", PinchflatWeb do
pipe_through :api
get "/media/recent_downloads", Api.MediaController, :recent_downloads
end
scope "/dev" do
pipe_through :browser

View file

@ -0,0 +1,103 @@
defmodule PinchflatWeb.Api.MediaControllerTest do
use PinchflatWeb.ConnCase
import Pinchflat.MediaFixtures
describe "GET /api/media/recent_downloads" do
test "returns only downloaded media items", %{conn: conn} do
downloaded = media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
_not_downloaded = media_item_fixture(%{media_downloaded_at: nil})
conn = get(conn, "/api/media/recent_downloads")
response = json_response(conn, 200)
ids = Enum.map(response["data"], & &1["id"])
assert downloaded.id in ids
assert length(response["data"]) == 1
end
test "returns items ordered by media_downloaded_at descending", %{conn: conn} do
older = media_item_fixture(%{media_downloaded_at: ~U[2024-01-01 00:00:00Z]})
newer = media_item_fixture(%{media_downloaded_at: ~U[2024-06-01 00:00:00Z]})
conn = get(conn, "/api/media/recent_downloads")
response = json_response(conn, 200)
ids = Enum.map(response["data"], & &1["id"])
assert ids == [newer.id, older.id]
end
test "default limit is 50", %{conn: conn} do
for _ <- 1..55 do
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
end
conn = get(conn, "/api/media/recent_downloads")
response = json_response(conn, 200)
assert length(response["data"]) == 50
end
test "respects custom limit param", %{conn: conn} do
for _ <- 1..10 do
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
end
conn = get(conn, "/api/media/recent_downloads?limit=5")
response = json_response(conn, 200)
assert length(response["data"]) == 5
end
test "clamps limit to minimum of 1", %{conn: conn} do
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
conn = get(conn, "/api/media/recent_downloads?limit=0")
response = json_response(conn, 200)
assert length(response["data"]) == 1
end
test "clamps limit to maximum of 500", %{conn: conn} do
for _ <- 1..5 do
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
end
conn = get(conn, "/api/media/recent_downloads?limit=9999")
response = json_response(conn, 200)
assert length(response["data"]) == 5
end
test "returns expected fields for each item", %{conn: conn} do
media_item_fixture(%{media_downloaded_at: DateTime.utc_now()})
conn = get(conn, "/api/media/recent_downloads")
response = json_response(conn, 200)
item = hd(response["data"])
assert Map.has_key?(item, "id")
assert Map.has_key?(item, "uuid")
assert Map.has_key?(item, "title")
assert Map.has_key?(item, "media_id")
assert Map.has_key?(item, "source_id")
assert Map.has_key?(item, "uploaded_at")
assert Map.has_key?(item, "media_downloaded_at")
assert Map.has_key?(item, "media_filepath")
assert Map.has_key?(item, "thumbnail_filepath")
assert Map.has_key?(item, "metadata_filepath")
assert Map.has_key?(item, "nfo_filepath")
assert Map.has_key?(item, "subtitle_filepaths")
end
test "returns empty data when no downloaded items exist", %{conn: conn} do
_not_downloaded = media_item_fixture(%{media_downloaded_at: nil})
conn = get(conn, "/api/media/recent_downloads")
response = json_response(conn, 200)
assert response["data"] == []
end
end
end