Compare commits

..

4 Commits

Author SHA1 Message Date
Kieran Eglin 1b7fb6d14b Bumped version [skip ci] 2024-05-03 16:13:28 -07:00
Kieran 944d26b57d Added test button to apprise input (#226) 2024-05-03 15:42:22 -07:00
Kieran 5427c6247f [Enhancement] Add "Copy JSON" action button to help when troubleshooting (#224)
* [WIP] [skip ci]

* Added 'copy JSON' button to all dropdowns; hopefully fixed some CSS silliness
2024-05-03 10:07:59 -07:00
Kieran f655a8ae01 Improved robustness of file downloads (#225) 2024-05-03 09:15:12 -07:00
30 changed files with 387 additions and 93 deletions
+6
View File
@@ -22,6 +22,12 @@ window.copyTextToClipboard = async (text) => {
} }
} }
window.copyWithCallbacks = async (text, onCopy, onAfterDelay, delay = 4000) => {
await window.copyTextToClipboard(text)
onCopy()
setTimeout(onAfterDelay, delay)
}
window.markVersionAsSeen = (versionString) => { window.markVersionAsSeen = (versionString) => {
localStorage.setItem('seenVersion', versionString) localStorage.setItem('seenVersion', versionString)
} }
@@ -124,9 +124,18 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
[format_sort: "res:#{res},+codec:avc:m4a", remux_video: "mp4"] [format_sort: "res:#{res},+codec:avc:m4a", remux_video: "mp4"]
end end
audio_format_precedence = [
"bestaudio[ext=m4a]",
"bestaudio[ext=mp3]",
"bestaudio",
"best[ext=m4a]",
"best[ext=mp3]",
"best"
]
case media_profile.preferred_resolution do case media_profile.preferred_resolution do
# Also be aware that :audio disabled all embedding options for subtitles # Also be aware that :audio disabled all embedding options for subtitles
:audio -> [:extract_audio, format: "bestaudio[ext=m4a]"] :audio -> [:extract_audio, format: Enum.join(audio_format_precedence, "/")]
:"360p" -> video_codec_option.("360") :"360p" -> video_codec_option.("360")
:"480p" -> video_codec_option.("480") :"480p" -> video_codec_option.("480")
:"720p" -> video_codec_option.("720") :"720p" -> video_codec_option.("720")
@@ -54,24 +54,36 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
@doc """ @doc """
Downloads and stores a thumbnail for a media item, returning the filepath. Downloads and stores a thumbnail for a media item, returning the filepath.
Chooses the highest quality jpg thumbnail available. Chooses the highest quality thumbnail available (preferring jpg). Returns
nil if no thumbnails are available.
Returns binary() Returns binary() | nil
""" """
def download_and_store_thumbnail_for(database_record, metadata_map) do def download_and_store_thumbnail_for(database_record, metadata_map) do
thumbnail_url = thumbnails =
metadata_map["thumbnails"] (metadata_map["thumbnails"] || [])
|> Enum.filter(&(&1["preference"] && String.ends_with?(&1["url"], ".jpg"))) # Give it a low preference if the `preference` key doesn't exist
|> Enum.sort(&(&1["preference"] >= &2["preference"])) |> Enum.map(&Map.put_new(&1, "preference", -1000))
|> List.first() # Give it a low preference if image isn't a jpg
|> Map.get("url") |> Enum.map(fn t ->
preference_weight = if String.ends_with?(t["url"], ".jpg"), do: t["preference"], else: t["preference"] - 1000
Map.put(t, "preference", preference_weight)
end)
case Enum.sort_by(thumbnails, & &1["preference"], :desc) do
[thumbnail_map | _] ->
thumbnail_url = thumbnail_map["url"]
filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url)) filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url))
thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url) thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url)
:ok = FilesystemUtils.write_p!(filepath, thumbnail_blob) :ok = FilesystemUtils.write_p!(filepath, thumbnail_blob)
filepath filepath
_ ->
nil
end
end end
@doc """ @doc """
+1 -1
View File
@@ -30,7 +30,7 @@ defmodule Pinchflat.Metadata.MetadataParser do
original_url: metadata["original_url"], original_url: metadata["original_url"],
description: metadata["description"], description: metadata["description"],
media_filepath: metadata["filepath"], media_filepath: metadata["filepath"],
livestream: metadata["was_live"], livestream: !!metadata["was_live"],
duration_seconds: metadata["duration"] && round(metadata["duration"]) duration_seconds: metadata["duration"] && round(metadata["duration"])
} }
end end
+1 -1
View File
@@ -87,7 +87,7 @@ defmodule Pinchflat.YtDlp.Media do
title: response["title"], title: response["title"],
description: response["description"], description: response["description"],
original_url: response["webpage_url"], original_url: response["webpage_url"],
livestream: response["was_live"], livestream: !!response["was_live"],
duration_seconds: response["duration"] && round(response["duration"]), duration_seconds: response["duration"] && round(response["duration"]),
short_form_content: response["webpage_url"] && short_form_content?(response), short_form_content: response["webpage_url"] && short_form_content?(response),
upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"]) upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"])
@@ -266,6 +266,7 @@ defmodule PinchflatWeb.CoreComponents do
multiple pattern placeholder readonly required rows size step) multiple pattern placeholder readonly required rows size step)
slot :inner_block slot :inner_block
slot :input_append
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns assigns
@@ -424,20 +425,23 @@ defmodule PinchflatWeb.CoreComponents do
<.label for={@id}> <.label for={@id}>
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span> <%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
</.label> </.label>
<div class="flex items-center">
<input <input
type={@type} type={@type}
name={@name} name={@name}
id={@id} id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)} value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[ class={[
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black", "w-full rounded-lg border-[1.5px] px-5 py-3 font-normal border-form-strokedark bg-form-input",
"outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter", "outline-none transition active:border-primary disabled:cursor-default disabled:bg-whiter",
"dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary", "text-white focus:border-primary",
@inputclass, @inputclass,
@errors != [] && "border-rose-400 focus:border-rose-400" @errors != [] && "border-rose-400 focus:border-rose-400"
]} ]}
{@rest} {@rest}
/> />
<%= render_slot(@input_append) %>
</div>
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help> <.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
<.error :for={msg <- @errors}><%= msg %></.error> <.error :for={msg <- @errors}><%= msg %></.error>
</div> </div>
@@ -89,4 +89,46 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
</div> </div>
""" """
end end
@doc """
Render a button with an icon. Optionally include a tooltip.
## Examples
<.icon_button icon_name="hero-check" tooltip="Complete" />
"""
attr :icon_name, :string, required: true
attr :class, :string, default: ""
attr :tooltip, :string, default: nil
attr :rest, :global
def icon_button(assigns) do
~H"""
<div class="group relative inline-block">
<button
class={[
"flex justify-center items-center rounded-lg ",
"bg-form-input border-2 border-strokedark",
"hover:bg-meta-4 hover:border-form-strokedark",
@class
]}
type="button"
{@rest}
>
<CoreComponents.icon name={@icon_name} class="text-stroke" />
</button>
<div
:if={@tooltip}
class={[
"hidden absolute left-1/2 top-full z-20 mt-3 -translate-x-1/2 whitespace-nowrap rounded-md",
"px-4.5 py-1.5 text-sm font-medium opacity-0 drop-shadow-4 group-hover:opacity-100 group-hover:block bg-meta-4"
]}
>
<span class="border-light absolute -top-1 left-1/2 -z-10 h-2 w-2 -translate-x-1/2 rotate-45 rounded-sm bg-meta-4">
</span>
<span><%= @tooltip %></span>
</div>
</div>
"""
end
end end
@@ -1,4 +1,19 @@
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50"> <.button_dropdown text="Actions" class="justify-center w-full sm:w-50">
<:option>
<span x-data="{ copied: false }" x-on:click={~s"
copyWithCallbacks(
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_item))}`,
() => copied = true,
() => copied = false
)
"}>
Copy JSON
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
</span>
</:option>
<:option>
<div class="h-px w-full bg-bodydark2"></div>
</:option>
<:option> <:option>
<.link <.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}/force_download"} href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}/force_download"}
@@ -5,7 +5,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<.media_item_form changeset={@changeset} action={~p"/sources/#{@media_item.source_id}/media/#{@media_item}"} /> <.media_item_form changeset={@changeset} action={~p"/sources/#{@media_item.source_id}/media/#{@media_item}"} />
</div> </div>
@@ -17,7 +17,7 @@
</nav> </nav>
</div> </div>
<div class="rounded-sm border border-stroke bg-white py-5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark px-7.5"> <div class="rounded-sm border border-stroke bg-white py-5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark px-7.5">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<.tabbed_layout> <.tabbed_layout>
<:tab_append> <:tab_append>
<.actions_dropdown media_item={@media_item} /> <.actions_dropdown media_item={@media_item} />
@@ -32,7 +32,7 @@
</div> </div>
<aside class="mt-4 xl:mt-0"> <aside class="mt-4 xl:mt-0">
<div>Uploaded: <%= @media_item.upload_date %></div> <div>Uploaded: <%= @media_item.upload_date %></div>
<div> <div :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
<.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link> <.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link>
</div> </div>
<div class="mt-4 text-bodydark"> <div class="mt-4 text-bodydark">
@@ -0,0 +1,36 @@
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50">
<:option>
<span x-data="{ copied: false }" x-on:click={~s"
copyWithCallbacks(
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_profile))}`,
() => copied = true,
() => copied = false
)
"}>
Copy JSON
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
</span>
</:option>
<:option>
<div class="h-px w-full bg-bodydark2"></div>
</:option>
<:option>
<.link
href={~p"/media_profiles/#{@media_profile}"}
method="delete"
data-confirm="Are you sure you want to delete this profile and all its sources (leaving files in place)? This cannot be undone."
>
Delete Profile
</.link>
</:option>
<:option>
<.link
href={~p"/media_profiles/#{@media_profile}?delete_files=true"}
method="delete"
data-confirm="Are you sure you want to delete this profile, all its sources, and its files on disk? This cannot be undone."
class="mt-5 md:mt-0"
>
Delete Profile + Files
</.link>
</:option>
</.button_dropdown>
@@ -8,7 +8,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} /> <.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} />
</div> </div>
@@ -6,7 +6,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<.media_profile_form changeset={@changeset} action={~p"/media_profiles"} /> <.media_profile_form changeset={@changeset} action={~p"/media_profiles"} />
</div> </div>
@@ -17,30 +17,10 @@
</nav> </nav>
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<.tabbed_layout> <.tabbed_layout>
<:tab_append> <:tab_append>
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50"> <.actions_dropdown media_profile={@media_profile} />
<:option>
<.link
href={~p"/media_profiles/#{@media_profile}"}
method="delete"
data-confirm="Are you sure you want to delete this profile and all its sources (leaving files in place)? This cannot be undone."
>
Delete Profile
</.link>
</:option>
<:option>
<.link
href={~p"/media_profiles/#{@media_profile}?delete_files=true"}
method="delete"
data-confirm="Are you sure you want to delete this profile, all its sources, and its files on disk? This cannot be undone."
class="mt-5 md:mt-0"
>
Delete Profile + Files
</.link>
</:option>
</.button_dropdown>
</:tab_append> </:tab_append>
<:tab title="Media Profile"> <:tab title="Media Profile">
@@ -5,7 +5,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5"> <div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10 dark:text-white"> <div class="flex flex-col gap-10 dark:text-white">
<%= if match?([_|_], @search_results) do %> <%= if match?([_|_], @search_results) do %>
<.table rows={@search_results} table_class="text-black dark:text-white"> <.table rows={@search_results} table_class="text-black dark:text-white">
@@ -6,6 +6,7 @@ defmodule PinchflatWeb.Settings.SettingHTML do
@doc """ @doc """
Renders a setting form. Renders a setting form.
""" """
attr :conn, Plug.Conn, required: true
attr :changeset, Ecto.Changeset, required: true attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true attr :action, :string, required: true
@@ -0,0 +1,55 @@
defmodule Pinchflat.Settings.AppriseServerLive do
use PinchflatWeb, :live_view
alias PinchflatWeb.Settings.SettingHTML
def render(assigns) do
~H"""
<.input
type="text"
id="setting_apprise_server"
name="setting[apprise_server]"
value={@value}
label="Apprise Server"
help={SettingHTML.apprise_server_help()}
html_help={true}
inputclass="font-mono text-sm mr-4"
placeholder="https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}"
phx-change="apprise_server_changed"
>
<:input_append>
<.icon_button icon_name={@icon_name} class="h-12 w-12" phx-click="send_apprise_test" tooltip={@tooltip} />
</:input_append>
</.input>
"""
end
def mount(_params, session, socket) do
new_assigns = %{
value: session["value"],
icon_name: "hero-paper-airplane",
tooltip: "Send Test"
}
{:ok, assign(socket, new_assigns)}
end
def handle_event("send_apprise_test", _params, %{assigns: assigns} = socket) do
backend_runner().run([assigns.value], title: "Pinchflat Test", body: "This is a test message from Pinchflat")
Process.send_after(self(), :reset_button_icon, 4_000)
{:noreply, assign(socket, %{icon_name: "hero-check", tooltip: "Sent!"})}
end
def handle_event("apprise_server_changed", %{"setting" => setting}, socket) do
{:noreply, assign(socket, %{value: setting["apprise_server"]})}
end
def handle_info(:reset_button_icon, socket) do
{:noreply, assign(socket, %{icon_name: "hero-paper-airplane", tooltip: "Send Test"})}
end
defp backend_runner do
Application.get_env(:pinchflat, :apprise_runner)
end
end
@@ -3,19 +3,17 @@
Oops, something went wrong! Please check the errors below. Oops, something went wrong! Please check the errors below.
</.error> </.error>
<h3 class="mt-8 text-2xl text-black dark:text-white"> <h3 class="mt-2 md:mt-8 text-2xl text-black dark:text-white">
Notification Settings Notification Settings
</h3> </h3>
<.input <section>
field={f[:apprise_server]} <%= live_render(
type="text" @conn,
label="Apprise Server" Pinchflat.Settings.AppriseServerLive,
help={apprise_server_help()} session: %{"value" => f[:apprise_server].value}
html_help={true} ) %>
inputclass="font-mono text-sm" </section>
placeholder="https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}"
/>
<.button class="my-10 sm:mb-7.5 w-full sm:w-auto" rounding="rounded-lg">Save Settings</.button> <.button class="mt-10 mb-4 sm:mb-8 w-full sm:w-auto" rounding="rounded-lg">Save Settings</.button>
</.simple_form> </.simple_form>
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5"> <div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<.setting_form changeset={@changeset} action={~p"/settings"} /> <.setting_form conn={@conn} changeset={@changeset} action={~p"/settings"} />
</div> </div>
</div> </div>
@@ -3,15 +3,32 @@
<span <span
x-data="{ copied: false }" x-data="{ copied: false }"
x-on:click={" x-on:click={"
window.copyTextToClipboard('#{rss_feed_url(@conn, @source)}') copyWithCallbacks(
copied = true '#{rss_feed_url(@conn, @source)}',
setTimeout(() => copied = false, 4000) () => copied = true,
() => copied = false
)
"} "}
> >
Copy RSS Feed Copy RSS Feed
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span> <span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
</span> </span>
</:option> </:option>
<:option>
<span x-data="{ copied: false }" x-on:click={~s"
copyWithCallbacks(
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@source))}`,
() => copied = true,
() => copied = false
)
"}>
Copy JSON
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
</span>
</:option>
<:option>
<div class="h-px w-full bg-bodydark2"></div>
</:option>
<:option :if={@source.download_media}> <:option :if={@source.download_media}>
<.link <.link
href={~p"/sources/#{@source}/force_download"} href={~p"/sources/#{@source}/force_download"}
@@ -8,7 +8,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources/#{@source}"} /> <.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources/#{@source}"} />
</div> </div>
@@ -12,13 +12,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
def render(%{records: []} = assigns) do def render(%{records: []} = assigns) do
~H""" ~H"""
<div class="mb-4 flex items-center"> <div class="mb-4 flex items-center">
<button <.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" />
class="flex justify-center items-center rounded-lg bg-form-input border-2 border-strokedark h-10 w-10"
phx-click="reload_page"
type="button"
>
<.icon name="hero-arrow-path" class="text-stroke" />
</button>
<p class="ml-2">Nothing Here!</p> <p class="ml-2">Nothing Here!</p>
</div> </div>
""" """
@@ -28,13 +22,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
~H""" ~H"""
<div> <div>
<span class="mb-4 flex items-center"> <span class="mb-4 flex items-center">
<button <.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
class="flex justify-center items-center rounded-lg bg-form-input border-2 border-strokedark h-10 w-10"
phx-click="reload_page"
type="button"
>
<.icon name="hero-arrow-path" class="text-stroke" />
</button>
<span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span> <span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span>
</span> </span>
<.table rows={@records} table_class="text-white"> <.table rows={@records} table_class="text-white">
@@ -6,7 +6,7 @@
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<div class="flex flex-col gap-10"> <div class="flex flex-col gap-10">
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} /> <.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} />
</div> </div>
@@ -17,7 +17,7 @@
</nav> </nav>
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5"> <div class="rounded-sm border border-stroke bg-white px-5 py-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full">
<.tabbed_layout> <.tabbed_layout>
<:tab_append> <:tab_append>
<.actions_dropdown source={@source} conn={@conn} /> <.actions_dropdown source={@source} conn={@conn} />
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do def project do
[ [
app: :pinchflat, app: :pinchflat,
version: "0.1.16", version: "0.1.17",
elixir: "~> 1.16", elixir: "~> 1.16",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
@@ -259,7 +259,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
assert {:ok, res} = DownloadOptionBuilder.build(media_item) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :extract_audio in res assert :extract_audio in res
assert {:format, "bestaudio[ext=m4a]"} in res assert {:format, "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio/best[ext=m4a]/best[ext=mp3]/best"} in res
refute {:remux_video, "mp4"} in res refute {:remux_video, "mp4"} in res
end end
@@ -89,6 +89,48 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
assert filepath =~ ~r{/media_items/#{media_item.id}/img_2.jpg} assert filepath =~ ~r{/media_items/#{media_item.id}/img_2.jpg}
end end
test "will fall back to a non-jpg if it has to", %{media_item: media_item} do
metadata = %{
"thumbnails" => [
%{"url" => "https://i.ytimg.com/vi/ABC123/img_1.webp", "preference" => -1}
]
}
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
assert filepath =~ ~r{/media_items/#{media_item.id}/img_1.webp}
end
test "does not require a preference field", %{media_item: media_item} do
metadata = %{
"thumbnails" => [
%{"url" => "https://i.ytimg.com/vi/ABC123/img_1.webp"}
]
}
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
assert filepath =~ ~r{/media_items/#{media_item.id}/img_1.webp}
end
end
describe "download_and_store_thumbnail_for/2 when not downloading thumbnails" do
test "returns nil if there are no thumbnails", %{media_item: media_item} do
metadata = %{"thumbnails" => []}
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
assert filepath == nil
end
test "returns nil if there is no thumbnail field", %{media_item: media_item} do
metadata = %{}
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
assert filepath == nil
end
end end
describe "parse_upload_date/1" do describe "parse_upload_date/1" do
@@ -46,6 +46,14 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
assert result.livestream == metadata["was_live"] assert result.livestream == metadata["was_live"]
end end
test "the livestream flag defaults to false", %{metadata: metadata} do
metadata = Map.put(metadata, "was_live", nil)
result = Parser.parse_for_media_item(metadata)
assert result.livestream == false
end
test "it extracts the duration in seconds", %{metadata: metadata} do test "it extracts the duration in seconds", %{metadata: metadata} do
result = Parser.parse_for_media_item(metadata) result = Parser.parse_for_media_item(metadata)
+11
View File
@@ -199,5 +199,16 @@ defmodule Pinchflat.YtDlp.MediaTest do
assert %Media{duration_seconds: nil} = Media.response_to_struct(response) assert %Media{duration_seconds: nil} = Media.response_to_struct(response)
end end
test "sets livestream to false if the was_live field isn't present" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 60,
"upload_date" => "20210101"
}
assert %Media{livestream: false} = Media.response_to_struct(response)
end
end end
end end
@@ -0,0 +1,70 @@
defmodule PinchflatWeb.Settings.AppriseServerLiveTest do
use PinchflatWeb.ConnCase
import Mox
import Phoenix.LiveViewTest
alias Pinchflat.Settings.AppriseServerLive
setup :verify_on_exit!
describe "initial rendering" do
test "renders the input", %{conn: conn} do
{:ok, _view, html} = live_isolated(conn, AppriseServerLive, session: create_session(""))
assert html =~ ~s(input type="text" name="setting[apprise_server]")
end
test "sets the initial value from the session", %{conn: conn} do
{:ok, _view, html} = live_isolated(conn, AppriseServerLive, session: create_session("cool-value"))
assert html =~ ~s(value="cool-value")
end
test "shows a relevant button icon", %{conn: conn} do
{:ok, _view, html} = live_isolated(conn, AppriseServerLive, session: create_session(""))
assert html =~ "hero-paper-airplane"
refute html =~ "hero-check"
end
end
describe "pressing the button" do
setup do
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
:ok
end
test "sends a test message to the specified server", %{conn: conn} do
expect(AppriseRunnerMock, :run, fn servers, args ->
assert servers == ["cool-value"]
assert args == [title: "Pinchflat Test", body: "This is a test message from Pinchflat"]
{:ok, ""}
end)
{:ok, view, _html} = live_isolated(conn, AppriseServerLive, session: create_session("cool-value"))
assert view
|> element("button")
|> render_click()
end
test "sets the button icon to a checkmark", %{conn: conn} do
{:ok, view, _html} = live_isolated(conn, AppriseServerLive, session: create_session("cool-value"))
result =
view
|> element("button")
|> render_click()
refute result =~ "hero-paper-airplane"
assert result =~ "hero-check"
end
end
defp create_session(value) do
%{"value" => value}
end
end