image saving with basic error handling

This commit is contained in:
Owen
2024-12-16 16:25:38 +01:00
parent 5a5700fab7
commit c04b7f2d22
13 changed files with 116 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import Config import Config
config :plantid_discord_bot, config :plantid_discord_bot,
image_folder: "priv/static" image_path: "priv/static"
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"
+26 -4
View File
@@ -1,8 +1,8 @@
defmodule PlantIdDiscordBot.Cog.PlantNet do defmodule PlantIdDiscordBot.Cog.PlantNet do
use Nostrum.Consumer use Nostrum.Consumer
# alias PlantIdDiscordBot.Cog.PlantNet
alias PlantIdDiscordBot.RateLimiter alias PlantIdDiscordBot.RateLimiter
alias PlantIdDiscordBot.PlantNet.Parser alias PlantIdDiscordBot.PlantNet.Parser
alias PlantIdDiscordBot.FileServer.File
@api Application.compile_env(:plantid_discord_bot, :api) @api Application.compile_env(:plantid_discord_bot, :api)
@@ -26,18 +26,40 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do
{:ok, _} -> {:ok, _} ->
do_identification(interaction) do_identification(interaction)
end end
# TODO increase on success (failed id or otherwise)
# RateLimiter.increase_counter(guild_id)
end end
defp do_identification(interaction) do defp do_identification(interaction) do
attachment_urls = get_attachment_urls(interaction) attachment_urls = get_attachment_urls(interaction)
original_images = get_original_images(attachment_urls) original_images = get_original_images(attachment_urls)
try do
Enum.each(attachment_urls, fn url ->
binary = File.download_file!(url)
File.save_file!(binary)
end)
rescue
e in RuntimeError ->
# Logger.error("An error occurred while processing the images.")
@api.create_interaction_response(interaction, %{
type: 4,
data: %{
content:
"An error occurred while processing the images." <> "\n\nReason:\n#{e.message}"
}
})
end
# TODO call PlantNet API and get response, with error handling
# TODO pass this response to the parser as below, with error handling
# temp data # temp data
response_message = Parser.parse(@plantnet_raw_response) response_message = Parser.parse(@plantnet_raw_response)
# TODO increase on success (failed id or otherwise)
# RateLimiter.increase_counter(guild_id)
@api.create_interaction_response(interaction, %{ @api.create_interaction_response(interaction, %{
type: 4, type: 4,
data: %{content: response_message <> "\n#{original_images}"} data: %{content: response_message <> "\n#{original_images}"}
@@ -0,0 +1,36 @@
defmodule PlantIdDiscordBot.FileServer.File do
@moduledoc """
File utilities
"""
alias PlantIdDiscordBot.FileServer.ImageConverter
@image_path Application.compile_env(:plantid_discord_bot, :image_path)
@doc """
Downloads a file from the given URL. Throws an error if unsuccessful.
"""
@spec download_file!(String.t()) :: binary
def download_file!(url) do
HTTPoison.get!(url)
|> Map.get(:body)
end
@spec save_file!(binary) :: :ok
def save_file!(binary) do
File.mkdir_p!(@image_path)
jpg_binary = ImageConverter.to_jpg!(binary)
generate_unique_filename("jpg")
|> File.write!(binary)
end
@doc """
Generates a unique filename with the given file extension.
"""
@spec generate_unique_filename(String.t()) :: String.t()
def generate_unique_filename(file_extension) do
filename = :crypto.strong_rand_bytes(8) |> Base.encode16()
Path.join(@image_path, "#{filename}.#{file_extension}")
end
end
@@ -0,0 +1,19 @@
defmodule PlantIdDiscordBot.FileServer.ImageConverter do
@moduledoc """
Image conversion utilities
"""
alias PlantIdDiscordBot.FileServer.MimeTypes
def to_jpg!(binary) do
case MimeTypes.detect_mime_type(binary) do
{:error, message} -> raise RuntimeError, message
"image/jpeg" -> binary
_ -> do_to_jpg(binary)
end
end
defp do_to_jpg(binary) do
Image.from_binary!(binary)
|> Image.write!(:memory, suffix: ".jpg")
end
end
@@ -0,0 +1,17 @@
defmodule PlantIdDiscordBot.FileServer.MimeTypes do
@moduledoc """
Mime type utilities
"""
@doc """
Detect the mime type of a binary
"""
@spec detect_mime_type(binary) :: String.t()
def detect_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>), do: "image/webp"
def detect_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>), do: "image/jpeg"
def detect_mime_type(<<0x89, 0x50, 0x4E, 0x47, _::binary>>), do: "image/png"
def detect_mime_type(<<0x47, 0x49, 0x46, 0x38, _::binary>>), do: "image/gif"
def detect_mime_type(<<0x42, 0x4D, _::binary>>), do: "image/bmp"
def detect_mime_type(<<0x49, 0x49, 0x2A, 0x00, _::binary>>), do: "image/tiff"
def detect_mime_type(_), do: {:error, "Unsupported file type"}
end
+3 -3
View File
@@ -36,14 +36,14 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
@spec parse(String.t()) :: map() @spec parse(String.t()) :: map()
def parse(response) do def parse(response) do
response response
|> to_map() |> to_map!()
|> filter_by_score() |> filter_by_score()
|> add_external_urls() |> add_external_urls()
|> generate_response_message() |> generate_response_message()
end end
@spec to_map(String.t()) :: map() @spec to_map!(String.t()) :: map()
def to_map(response), do: Jason.decode!(response) def to_map!(response), do: Jason.decode!(response)
@doc """ @doc """
Filters the data by score. Data is a parsed response from the PlantNet API. Filters the data by score. Data is a parsed response from the PlantNet API.
Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

+13
View File
@@ -0,0 +1,13 @@
defmodule PlantIdDiscordBotTest.FileServer.File do
use ExUnit.Case
doctest PlantIdDiscordBot.FileServer.File
@image_path Application.compile_env(:plantid_discord_bot, :image_path)
test "generate_unique_filename" do
file_path = PlantIdDiscordBot.FileServer.File.generate_unique_filename("jpg")
file_name = Path.basename(file_path)
assert Regex.match?(~r/^[0-9A-F]{16}\.jpg$/, file_name)
assert file_path == Path.join(@image_path, file_name)
end
end
+1 -1
View File
@@ -7,7 +7,7 @@ defmodule PlantIdDiscordBotTest.PlantNet.Parser do
test "to_map" do test "to_map" do
result = result =
PlantNetFixtures.raw_response() PlantNetFixtures.raw_response()
|> Parser.to_map() |> Parser.to_map!()
assert result == PlantNetFixtures.parsed_response() assert result == PlantNetFixtures.parsed_response()
end end