Migrate from embedded covers to assets

This commit is contained in:
Claudio Ortolina
2025-09-01 16:10:50 +03:00
parent cbf567a502
commit dc77d337c0
19 changed files with 80 additions and 144 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
import Ecto.Changeset
alias MusicLibrary.Records.Cover
alias MusicLibrary.Assets.Asset
@primary_key {:id, :binary_id, autogenerate: true}
schema "artist_infos" do
@@ -49,7 +49,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
end
def generate_image_hash(%__MODULE__{image_data: image_data} = artist_info) do
change(artist_info, image_data_hash: Cover.hash(image_data))
change(artist_info, image_data_hash: Asset.hash(image_data))
end
def generate_image_hash(changeset) do
@@ -58,7 +58,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
changeset
image_data ->
put_change(changeset, :image_data_hash, Cover.hash(image_data))
put_change(changeset, :image_data_hash, Asset.hash(image_data))
end
end
+1 -1
View File
@@ -60,7 +60,7 @@ defmodule MusicLibrary.Assets.Asset do
}
end
defp hash(content) do
def hash(content) do
:crypto.hash(:sha256, content) |> Base.encode16()
end
end
+13 -20
View File
@@ -148,15 +148,6 @@ defmodule MusicLibrary.Records do
Repo.all(q)
end
def get_cover(id) do
q =
from r in Record,
where: r.id == ^id,
select: %{cover_data: r.cover_data, cover_hash: r.cover_hash}
Repo.one(q)
end
def import_from_musicbrainz_release(musicbrainz_id, opts \\ []) do
case MusicBrainz.get_release(musicbrainz_id) do
{:ok, release} ->
@@ -175,10 +166,11 @@ defmodule MusicLibrary.Records do
with {:ok, release_group} <- MusicBrainz.get_release_group(musicbrainz_id),
{:ok, release_group_with_releases} <- merge_releases(musicbrainz_id, release_group),
{:ok, cover_data} <- get_cover_art_or_default(musicbrainz_id) do
{:ok, cover_data} <- get_cover_art_or_default(musicbrainz_id),
{:ok, asset} <- Assets.store_image(%{content: cover_data, format: "image/jpeg"}) do
release_group_with_releases
|> build_record_attrs(%{
"cover_data" => cover_data,
"cover_hash" => asset.hash,
"format" => format,
"purchased_at" => purchased_at,
"selected_release_id" => selected_release_id
@@ -239,11 +231,11 @@ defmodule MusicLibrary.Records do
end
def refresh_cover(record) do
with {:ok, cover_data} <- MusicBrainz.get_cover_art({:url, record.cover_url}) do
{:ok, thumb_data} = Cover.resize(cover_data)
with {:ok, cover_data} <- MusicBrainz.get_cover_art({:url, record.cover_url}),
{:ok, thumb_data} <- Cover.resize(cover_data),
{:ok, asset} <- Assets.store_image(%{content: thumb_data, format: "image/jpeg"}) do
record
|> Record.add_cover_data(thumb_data)
|> Record.set_cover_hash(asset.hash)
|> Repo.update()
end
end
@@ -267,11 +259,12 @@ defmodule MusicLibrary.Records do
end
def resize_cover(record) do
{:ok, thumb_data} = Cover.resize(record.cover_data)
record
|> Record.add_cover_data(thumb_data)
|> Repo.update()
with {:ok, thumb_data} <- Cover.resize(record.cover_data),
{:ok, asset} <- Assets.store_image(%{content: thumb_data, format: "image/jpeg"}) do
record
|> Record.set_cover_hash(asset.hash)
|> Repo.update()
end
end
def refresh_musicbrainz_data(record) do
+1 -13
View File
@@ -2,7 +2,7 @@ defmodule MusicLibrary.Records.Batch do
import Ecto.Query
alias MusicLibrary.Records
alias MusicLibrary.Records.{Cover, Record}
alias MusicLibrary.Records.Record
alias MusicLibrary.Repo
require Logger
@@ -21,18 +21,6 @@ defmodule MusicLibrary.Records.Batch do
end)
end
def refresh_old_artwork do
run_on_all_records(&refresh_old_artwork/1)
end
def refresh_old_artwork(record) do
if Cover.correct_size?(record.cover_data) do
:ok
else
MusicLibrary.Records.refresh_cover(record)
end
end
defp run_on_all_records(fun) do
q = from(r in Record)
stream = Repo.stream(q, max_rows: 50)
-4
View File
@@ -15,10 +15,6 @@ defmodule MusicLibrary.Records.Cover do
Image.write_to_buffer(thumb, ".jpg")
end
def hash(cover_data) do
:crypto.hash(:sha256, cover_data) |> Base.encode16()
end
def correct_size?(cover_data) do
{:ok, image} = Image.new_from_buffer(cover_data)
+3 -52
View File
@@ -5,8 +5,6 @@ defmodule MusicLibrary.Records.Record do
alias MusicBrainz.{Release, ReleaseGroup}
alias MusicLibrary.Artists.Artist
alias MusicLibrary.Colors.ColorFrequencyExtractor
alias MusicLibrary.Records.Cover
@formats [:cd, :backup, :vinyl, :blu_ray, :dvd, :multi]
@types [:album, :ep, :live, :compilation, :single, :other]
@@ -18,7 +16,6 @@ defmodule MusicLibrary.Records.Record do
field :format, Ecto.Enum, values: @formats
field :title, :string
field :cover_url, :string
field :cover_data, :binary
field :cover_hash, :string
field :musicbrainz_id, Ecto.UUID
field :musicbrainz_data, :map, default: %{}
@@ -128,38 +125,24 @@ defmodule MusicLibrary.Records.Record do
:selected_release_id,
:included_release_group_ids,
:cover_url,
:cover_data,
:cover_hash,
:dominant_colors,
:purchased_at
])
|> cast_embed(:artists)
|> validate_required([:type, :title, :musicbrainz_id, :genres])
|> unique_constraint(:musicbrainz_id, name: "records_musicbrainz_id_format_index")
|> generate_cover_hash()
|> maybe_generate_dominant_colors()
|> update_release_ids()
|> update_included_release_group_ids()
end
defp maybe_generate_dominant_colors(changeset) do
case get_change(changeset, :dominant_colors) do
nil ->
generate_dominant_colors(changeset)
_dominant_colors ->
changeset
end
end
def add_genres(record, genres) do
change(record, genres: genres)
end
def add_cover_data(record, cover_data) do
def set_cover_hash(record, cover_hash) do
record
|> change(cover_data: cover_data)
|> generate_cover_hash()
|> generate_dominant_colors()
|> change(cover_hash: cover_hash)
end
def add_musicbrainz_data(record, musicbrainz_data) do
@@ -171,38 +154,6 @@ defmodule MusicLibrary.Records.Record do
|> update_included_release_group_ids()
end
def generate_cover_hash(%__MODULE__{cover_data: cover_data} = record) do
change(record, cover_hash: Cover.hash(cover_data))
end
def generate_cover_hash(changeset) do
case get_change(changeset, :cover_data) do
nil ->
changeset
cover_data ->
put_change(changeset, :cover_hash, Cover.hash(cover_data))
end
end
def generate_dominant_colors(%__MODULE__{cover_data: cover_data} = record) do
change(record, dominant_colors: ColorFrequencyExtractor.extract_dominant_colors!(cover_data))
end
def generate_dominant_colors(changeset) do
case get_change(changeset, :cover_data) do
nil ->
changeset
cover_data ->
put_change(
changeset,
:dominant_colors,
ColorFrequencyExtractor.extract_dominant_colors!(cover_data)
)
end
end
def rotate_dominant_colors(%__MODULE__{dominant_colors: dominant_colors} = record) do
change(record, dominant_colors: rotate(dominant_colors))
end
+3 -2
View File
@@ -1,14 +1,15 @@
defmodule MusicLibrary.Worker.ExtractColors do
use Oban.Worker, queue: :heavy_writes, max_attempts: 3
alias MusicLibrary.{Colors, Records}
alias MusicLibrary.{Assets, Colors, Records}
@impl Oban.Worker
def perform(%Oban.Job{args: %{"id" => record_id, "method" => method}}) do
record = MusicLibrary.Records.get_record!(record_id)
asset = Assets.get!(record.cover_hash)
method = String.to_existing_atom(method)
with {:ok, colors} <- Colors.extract_colors(record.cover_data, method),
with {:ok, colors} <- Colors.extract_colors(asset.content, method),
{:ok, updated_record} <- Records.update_record(record, %{dominant_colors: colors}) do
MusicLibrary.Records.notify_update(updated_record)
end
@@ -14,7 +14,7 @@ defmodule MusicLibraryWeb.RecordComponents do
class={@class}
alt={@record.title}
loading="lazy"
src={~p"/covers/#{@record.id}?vsn=#{@record.cover_hash}"}
src={~p"/covers/#{@record.cover_hash}"}
/>
"""
end
@@ -199,7 +199,7 @@ defmodule MusicLibraryWeb.RecordFormComponent do
[cover_params] ->
{:ok, asset} = Assets.store_image(cover_params)
Map.put(record_params, "cover_data", asset.content)
Map.put(record_params, "cover_hash", asset.hash)
end
case Records.update_record(socket.assigns.record, params) do
@@ -31,11 +31,11 @@ defmodule MusicLibraryWeb.ArtistController do
|> send_resp(304, "")
end
defp respond_with_cache(conn, cover_data, etag) do
defp respond_with_cache(conn, image_data, etag) do
conn
|> put_resp_content_type("image/jpeg", "utf-8")
|> put_resp_header("cache-control", "public, max-age=#{@cache_duration}")
|> put_resp_header("etag", etag)
|> send_resp(200, cover_data)
|> send_resp(200, image_data)
end
end
@@ -19,8 +19,8 @@ defmodule MusicLibraryWeb.CollectionJSON do
id: record.id,
artists: Enum.map(record.artists, & &1.name),
title: record.title,
cover_url: url(~p"/api/covers/#{record.id}?#{[vsn: record.cover_hash]}"),
thumb_url: url(~p"/api/covers/#{record.id}?#{[vsn: record.cover_hash, size: 480]}")
cover_url: url(~p"/api/covers/#{record.cover_hash}"),
thumb_url: url(~p"/api/covers/#{record.cover_hash}?#{[size: 480]}")
}
end
end
@@ -2,6 +2,7 @@ defmodule MusicLibraryWeb.CoverController do
use MusicLibraryWeb, :controller
alias MusicLibrary.Assets
alias MusicLibrary.Assets.Asset
alias MusicLibrary.Records.Cover
# 1 year in seconds
@@ -15,7 +16,7 @@ defmodule MusicLibraryWeb.CoverController do
%{content: content} ->
# TODO: find a way to cache computation, or pre-compute thumb and store it
{:ok, thumb_data} = Cover.resize(content, String.to_integer(size))
hash = Cover.hash(thumb_data)
hash = Asset.hash(thumb_data)
case get_req_header(conn, "if-none-match") do
[^hash] -> extend_cache(conn)
@@ -57,11 +58,11 @@ defmodule MusicLibraryWeb.CoverController do
|> send_resp(200, asset.content)
end
defp respond_with_cache(conn, cover_data, etag) do
defp respond_with_cache(conn, data, etag) do
conn
|> put_resp_content_type("image/jpeg", "utf-8")
|> put_resp_header("cache-control", "public, max-age=#{@cache_duration}")
|> put_resp_header("etag", etag)
|> send_resp(200, cover_data)
|> send_resp(200, data)
end
end