Compare commits

...

10 Commits

Author SHA1 Message Date
Kieran Eglin 1c9f8f6abc Bumped version 2024-05-30 15:16:33 -07:00
Kieran Eglin 2ae1ee1c99 Updated portainer instructions to use GHCR 2024-05-30 15:16:15 -07:00
Kieran b73a9a4a67 [Enhancement] Added support for subdirectory-style reverse proxies (#276)
* Updated routes and other fun stuff to support reverse proxies

* Updated README

* Linting
2024-05-30 12:58:51 -07:00
Kieran 3dd901ff3e Prevented error when media item doesn't have description (#274) 2024-05-29 13:52:43 -07:00
Kieran e751c65424 [Enhancement] Improve handling of large media collections (#272)
* Added compound index for updated media item table for performance

* Improved large number display on homepage

* Improved UI around large numbers

* Centered the content of homepage data cards

* Renamed migration
2024-05-29 12:38:03 -07:00
Kieran 22439e0273 [Housekeeping] Store the upload event as a datetime rather than a date (#269)
* [WIP] renamed and migrated upload_date column

* Refactored yt-dlp media module

* Refactored parse_upload_date

* Refactored media item upload_date_index

* Got media tests running

* Refactored media item table live

* Cleaned up the stragglers

* Fixed old oversight re: original_url
2024-05-28 13:01:40 -07:00
Kieran Eglin 7603ba017f Improved wording re: livestreams on media profile form 2024-05-27 11:15:11 -07:00
Kieran 10880e9699 [Enhancement] Added link to local stream on media item show page (#265)
* Adds link for opening local stream

* Put conditional on correct element
2024-05-27 11:07:23 -07:00
Kieran b2e16d50cd Added library size to homepage (#264) 2024-05-27 10:43:50 -07:00
Kieran 3a5c06fe64 Added upload date (#260) 2024-05-24 10:04:37 -07:00
45 changed files with 568 additions and 344 deletions
+2 -1
View File
@@ -91,7 +91,7 @@ Docker Compose file:
version: '3' version: '3'
services: services:
pinchflat: pinchflat:
image: keglin/pinchflat:latest image: ghcr.io/kieraneglin/pinchflat:latest
environment: environment:
# Set the timezone to your local timezone # Set the timezone to your local timezone
- TZ=America/New_York - TZ=America/New_York
@@ -153,6 +153,7 @@ If you change this setting and it works well for you, please leave a comment on
| EXPOSE_FEED_ENDPOINTS | No | | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) | | EXPOSE_FEED_ENDPOINTS | No | | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
| JOURNAL_MODE | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) | | JOURNAL_MODE | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
| TZ_DATA_DIR | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored | | TZ_DATA_DIR | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
| BASE_ROUTE_PATH | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
## EFF donations ## EFF donations
+21 -18
View File
@@ -22,8 +22,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Light.woff2'), url('/fonts/satoshi/Satoshi-Light.woff'), src: url('../fonts/satoshi/Satoshi-Light.woff2'), url('../fonts/satoshi/Satoshi-Light.woff'),
url('/fonts/satoshi/Satoshi-Light.ttf'); url('../fonts/satoshi/Satoshi-Light.ttf');
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@@ -31,8 +31,9 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-LightItalic.woff2'), src: url('../fonts/satoshi/Satoshi-LightItalic.woff2'),
url('/fonts/satoshi/Satoshi-LightItalic.woff'), url('/fonts/satoshi/Satoshi-LightItalic.ttf'); url('../fonts/satoshi/Satoshi-LightItalic.woff'),
url('../fonts/satoshi/Satoshi-LightItalic.ttf');
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
@@ -40,8 +41,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Regular.woff2'), url('/fonts/satoshi/Satoshi-Regular.woff'), src: url('../fonts/satoshi/Satoshi-Regular.woff2'), url('../fonts/satoshi/Satoshi-Regular.woff'),
url('/fonts/satoshi/Satoshi-Regular.ttf'); url('../fonts/satoshi/Satoshi-Regular.ttf');
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@@ -49,8 +50,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Italic.woff2'), url('/fonts/satoshi/Satoshi-Italic.woff'), src: url('../fonts/satoshi/Satoshi-Italic.woff2'), url('../fonts/satoshi/Satoshi-Italic.woff'),
url('/fonts/satoshi/Satoshi-Italic.ttf'); url('../fonts/satoshi/Satoshi-Italic.ttf');
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
@@ -58,8 +59,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Medium.woff2'), url('/fonts/satoshi/Satoshi-Medium.woff'), src: url('../fonts/satoshi/Satoshi-Medium.woff2'), url('../fonts/satoshi/Satoshi-Medium.woff'),
url('/fonts/satoshi/Satoshi-Medium.ttf'); url('../fonts/satoshi/Satoshi-Medium.ttf');
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@@ -67,8 +68,9 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-MediumItalic.woff2'), src: url('../fonts/satoshi/Satoshi-MediumItalic.woff2'),
url('/fonts/satoshi/Satoshi-MediumItalic.woff'), url('/fonts/satoshi/Satoshi-MediumItalic.ttf'); url('../fonts/satoshi/Satoshi-MediumItalic.woff'),
url('../fonts/satoshi/Satoshi-MediumItalic.ttf');
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
@@ -76,8 +78,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-Bold.woff2'), url('/fonts/satoshi/Satoshi-Bold.woff'), src: url('../fonts/satoshi/Satoshi-Bold.woff2'), url('../fonts/satoshi/Satoshi-Bold.woff'),
url('/fonts/satoshi/Satoshi-Bold.ttf'); url('../fonts/satoshi/Satoshi-Bold.ttf');
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@@ -85,8 +87,8 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-BoldItalic.woff2'), url('/fonts/satoshi/Satoshi-BoldItalic.woff'), src: url('../fonts/satoshi/Satoshi-BoldItalic.woff2'),
url('/fonts/satoshi/Satoshi-BoldItalic.ttf'); url('../fonts/satoshi/Satoshi-BoldItalic.woff'), url('../fonts/satoshi/Satoshi-BoldItalic.ttf');
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
@@ -94,8 +96,9 @@
@font-face { @font-face {
font-family: 'Satoshi'; font-family: 'Satoshi';
src: url('/fonts/satoshi/Satoshi-BlackItalic.woff2'), src: url('../fonts/satoshi/Satoshi-BlackItalic.woff2'),
url('/fonts/satoshi/Satoshi-BlackItalic.woff'), url('/fonts/satoshi/Satoshi-BlackItalic.ttf'); url('../fonts/satoshi/Satoshi-BlackItalic.woff'),
url('../fonts/satoshi/Satoshi-BlackItalic.ttf');
font-weight: 900; font-weight: 900;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
+1 -1
View File
@@ -29,7 +29,7 @@ window.Alpine = Alpine
Alpine.start() Alpine.start()
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
let liveSocket = new LiveSocket('/live', Socket, { let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, {
params: { _csrf_token: csrfToken }, params: { _csrf_token: csrfToken },
dom: { dom: {
onBeforeElUpdated(from, to) { onBeforeElUpdated(from, to) {
+2 -1
View File
@@ -26,7 +26,8 @@ config :pinchflat,
basic_auth_password: "", basic_auth_password: "",
expose_feed_endpoints: false, expose_feed_endpoints: false,
file_watcher_poll_interval: 1000, file_watcher_poll_interval: 1000,
timezone: "UTC" timezone: "UTC",
base_route_path: "/"
config :pinchflat, Pinchflat.Repo, config :pinchflat, Pinchflat.Repo,
journal_mode: :wal, journal_mode: :wal,
+5 -3
View File
@@ -46,12 +46,12 @@ if config_env() == :prod do
log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"])) log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"]))
metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"])) metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"]))
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"])) extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
# For running PF as a podcast host on self-hosted environments # For running PF as a podcast host on self-hosted environments
expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0 expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0
# For testing alternate journal modes (see issue #137) # For testing alternate journal modes (see issue #137)
journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal")) journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal"))
# For running PF in a subdirectory via a reverse proxy
base_route_path = System.get_env("BASE_ROUTE_PATH", "/")
config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug")) config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug"))
@@ -65,7 +65,8 @@ if config_env() == :prod do
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"), dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"),
expose_feed_endpoints: expose_feed_endpoints, expose_feed_endpoints: expose_feed_endpoints,
timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC", timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC",
log_path: log_path log_path: log_path,
base_route_path: base_route_path
config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/etc/elixir_tzdata_data") config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/etc/elixir_tzdata_data")
@@ -108,6 +109,7 @@ if config_env() == :prod do
ip: {0, 0, 0, 0}, ip: {0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000") port: String.to_integer(System.get_env("PORT") || "4000")
], ],
url: [path: base_route_path],
secret_key_base: secret_key_base secret_key_base: secret_key_base
config :pinchflat, :logger, [ config :pinchflat, :logger, [
+5 -4
View File
@@ -41,9 +41,10 @@ defmodule Pinchflat.Media do
Returns a list of media_items that are redownloadable based on the redownload delay Returns a list of media_items that are redownloadable based on the redownload delay
of the media_profile their source belongs to. of the media_profile their source belongs to.
The logic is that a media_item is past_redownload_delay if the media_item's The logic is that a media_item is past_redownload_delay if the media_item's uploaded_at is
upload_date is at least redownload_delay_days ago AND at least redownload_delay_days ago AND `media_downloaded_at` - `redownload_delay_days`
`media_downloaded_at` - `redownload_delay_days` is before the media_item's `upload_date`. is before the media_item's `uploaded_at`.
This logic grabs media that we've recently downloaded AND is recently uploaded, but This logic grabs media that we've recently downloaded AND is recently uploaded, but
doesn't grab media that we've recently downloaded and was uploaded a long time ago. doesn't grab media that we've recently downloaded and was uploaded a long time ago.
This also makes things work as expected when downloading media from a source for the This also makes things work as expected when downloading media from a source for the
@@ -135,7 +136,7 @@ defmodule Pinchflat.Media do
Unlike `create_media_item`, this will attempt an update if the media_item Unlike `create_media_item`, this will attempt an update if the media_item
already exists. This is so that future indexing can pick up attributes that already exists. This is so that future indexing can pick up attributes that
we may not have asked for in the past (eg: upload_date) we may not have asked for in the past (eg: uploaded_at)
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}} Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}
""" """
+5 -5
View File
@@ -26,7 +26,7 @@ defmodule Pinchflat.Media.MediaItem do
:livestream, :livestream,
:source_id, :source_id,
:short_form_content, :short_form_content,
:upload_date, :uploaded_at,
:upload_date_index, :upload_date_index,
:duration_seconds, :duration_seconds,
# these fields are captured only on download # these fields are captured only on download
@@ -51,7 +51,7 @@ defmodule Pinchflat.Media.MediaItem do
livestream livestream
media_id media_id
source_id source_id
upload_date uploaded_at
short_form_content short_form_content
)a )a
@@ -69,7 +69,7 @@ defmodule Pinchflat.Media.MediaItem do
field :short_form_content, :boolean, default: false field :short_form_content, :boolean, default: false
field :media_downloaded_at, :utc_datetime field :media_downloaded_at, :utc_datetime
field :media_redownloaded_at, :utc_datetime field :media_redownloaded_at, :utc_datetime
field :upload_date, :date field :uploaded_at, :utc_datetime
field :upload_date_index, :integer, default: 0 field :upload_date_index, :integer, default: 0
field :duration_seconds, :integer field :duration_seconds, :integer
@@ -130,7 +130,7 @@ defmodule Pinchflat.Media.MediaItem do
~w(__meta__ __struct__ metadata tasks media_items_search_index)a ~w(__meta__ __struct__ metadata tasks media_items_search_index)a
end end
defp update_upload_date_index(%{changes: changes} = changeset) when is_map_key(changes, :upload_date) do defp update_upload_date_index(%{changes: changes} = changeset) when is_map_key(changes, :uploaded_at) do
source_id = get_field(changeset, :source_id) source_id = get_field(changeset, :source_id)
source = Sources.get_source!(source_id) source = Sources.get_source!(source_id)
# Channels should count down from 99, playlists should count up from 0 # Channels should count down from 99, playlists should count up from 0
@@ -142,7 +142,7 @@ defmodule Pinchflat.Media.MediaItem do
current_max = current_max =
MediaQuery.new() MediaQuery.new()
|> where(^dynamic([mi], mi.upload_date == ^changes.upload_date and ^MediaQuery.for_source(source))) |> where(^dynamic([mi], ^MediaQuery.upload_date_matches(changes.uploaded_at) and ^MediaQuery.for_source(source)))
|> Repo.aggregate(aggregator, :upload_date_index) |> Repo.aggregate(aggregator, :upload_date_index)
case current_max do case current_max do
+9 -4
View File
@@ -35,9 +35,14 @@ defmodule Pinchflat.Media.MediaQuery do
def culling_prevented, do: dynamic([mi], mi.prevent_culling == true) def culling_prevented, do: dynamic([mi], mi.prevent_culling == true)
def culled, do: dynamic([mi], not is_nil(mi.culled_at)) def culled, do: dynamic([mi], not is_nil(mi.culled_at))
def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at)) def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at))
def upload_date_matches(other_date), do: dynamic([mi], fragment("date(?) = date(?)", mi.uploaded_at, ^other_date))
def upload_date_after_source_cutoff do def upload_date_after_source_cutoff do
dynamic([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date) dynamic(
[mi, source],
is_nil(source.download_cutoff_date) or
fragment("date(?) >= ?", mi.uploaded_at, source.download_cutoff_date)
)
end end
def format_matching_profile_preference do def format_matching_profile_preference do
@@ -84,12 +89,12 @@ defmodule Pinchflat.Media.MediaQuery do
def past_redownload_delay do def past_redownload_delay do
dynamic( dynamic(
[mi, source, media_profile], [mi, source, media_profile],
# Returns media items where the upload_date is at least redownload_delay_days ago AND # Returns media items where the uploaded_at is at least redownload_delay_days ago AND
# downloaded_at minus the redownload_delay_days is before the upload date # downloaded_at minus the redownload_delay_days is before the upload date
fragment(""" fragment("""
IFNULL(redownload_delay_days, 0) > 0 AND IFNULL(redownload_delay_days, 0) > 0 AND
DATETIME('now', '-' || redownload_delay_days || ' day') > upload_date AND DATETIME('now', '-' || redownload_delay_days || ' day') > uploaded_at AND
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < upload_date DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < uploaded_at
""") """)
) )
end end
@@ -88,14 +88,17 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
@doc """ @doc """
Parses an upload date from the YYYYMMDD string returned in yt-dlp metadata Parses an upload date from the YYYYMMDD string returned in yt-dlp metadata
and returns a Date struct. and returns a DateTime struct, appending a time of 00:00:00Z.
Returns Date.t() Returns DateTime.t()
""" """
def parse_upload_date(upload_date) do def parse_upload_date(upload_date) do
<<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date <<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date
Date.from_iso8601!("#{year}-#{month}-#{day}") case DateTime.from_iso8601("#{year}-#{month}-#{day}T00:00:00Z") do
{:ok, datetime, _} -> datetime
_ -> raise "Invalid upload date: #{upload_date}"
end
end end
@doc """ @doc """
+6 -7
View File
@@ -8,6 +8,8 @@ defmodule Pinchflat.Metadata.MetadataParser do
and not have it, ya know? and not have it, ya know?
""" """
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@doc """ @doc """
Parses the given JSON response from yt-dlp and returns a map of Parses the given JSON response from yt-dlp and returns a map of
the needful media_item attributes, along with anything needed for the needful media_item attributes, along with anything needed for
@@ -24,15 +26,12 @@ defmodule Pinchflat.Metadata.MetadataParser do
end end
defp parse_media_metadata(metadata) do defp parse_media_metadata(metadata) do
Map.merge(
Map.from_struct(YtDlpMedia.response_to_struct(metadata)),
%{ %{
media_id: metadata["id"], media_filepath: metadata["filepath"]
title: metadata["title"],
original_url: metadata["original_url"],
description: metadata["description"],
media_filepath: metadata["filepath"],
livestream: !!metadata["was_live"],
duration_seconds: metadata["duration"] && round(metadata["duration"])
} }
)
end end
defp parse_subtitle_metadata(metadata) do defp parse_subtitle_metadata(metadata) do
+1 -1
View File
@@ -28,7 +28,7 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
MediaQuery.new() MediaQuery.new()
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> order_by(desc: :upload_date) |> order_by(desc: :uploaded_at)
|> Repo.maybe_limit(limit) |> Repo.maybe_limit(limit)
|> Repo.all() |> Repo.all()
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end) |> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
+1 -8
View File
@@ -7,7 +7,6 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
import Pinchflat.Utils.XmlUtils, only: [safe: 1] import Pinchflat.Utils.XmlUtils, only: [safe: 1]
alias Pinchflat.Utils.DatetimeUtils
alias Pinchflat.Podcasts.PodcastHelpers alias Pinchflat.Podcasts.PodcastHelpers
alias PinchflatWeb.Router.Helpers, as: Routes alias PinchflatWeb.Router.Helpers, as: Routes
@@ -83,7 +82,7 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
<title>#{safe(media_item.title)}</title> <title>#{safe(media_item.title)}</title>
<link>#{safe(media_item.original_url)}</link> <link>#{safe(media_item.original_url)}</link>
<description>#{safe(media_item.description)}</description> <description>#{safe(media_item.description)}</description>
<pubDate>#{generate_upload_date(media_item)}</pubDate> <pubDate>#{Calendar.strftime(media_item.uploaded_at, @datetime_format)}</pubDate>
<itunes:duration>#{media_item.duration_seconds}</itunes:duration> <itunes:duration>#{media_item.duration_seconds}</itunes:duration>
<enclosure <enclosure
url="#{media_stream_path(url_base, media_item)}" url="#{media_stream_path(url_base, media_item)}"
@@ -133,12 +132,6 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
end end
end end
defp generate_upload_date(media_item) do
media_item.upload_date
|> DatetimeUtils.date_to_datetime()
|> Calendar.strftime(@datetime_format)
end
defp podcast_route(action, params) do defp podcast_route(action, params) do
Routes.podcast_path(PinchflatWeb.Endpoint, action, params) Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
end end
-17
View File
@@ -1,17 +0,0 @@
defmodule Pinchflat.Utils.DatetimeUtils do
@moduledoc """
Utility methods for working with dates and datetimes
"""
@doc """
Converts a Date to a DateTime
Returns %DateTime{}
"""
def date_to_datetime(date) do
date
|> Date.to_gregorian_days()
|> Kernel.*(86_400)
|> DateTime.from_gregorian_seconds()
end
end
+23
View File
@@ -13,4 +13,27 @@ defmodule Pinchflat.Utils.NumberUtils do
|> max(minimum) |> max(minimum)
|> min(maximum) |> min(maximum)
end end
@doc """
Converts a number to a human readable byte size. Can take a precision
option to specify the number of decimal places to round to.
Returns {integer(), String.t()}
"""
def human_byte_size(number, opts \\ [])
def human_byte_size(nil, opts), do: human_byte_size(0, opts)
def human_byte_size(number, opts) do
precision = Keyword.get(opts, :precision, 2)
suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
base = 1024
Enum.reduce_while(suffixes, {number / 1.0, "B"}, fn suffix, {value, _} ->
if value < base do
{:halt, {Float.round(value, precision), suffix}}
else
{:cont, {value / base, suffix}}
end
end)
end
end end
+16 -4
View File
@@ -10,7 +10,7 @@ defmodule Pinchflat.YtDlp.Media do
:original_url, :original_url,
:livestream, :livestream,
:short_form_content, :short_form_content,
:upload_date, :uploaded_at,
:duration_seconds :duration_seconds
] ]
@@ -21,7 +21,7 @@ defmodule Pinchflat.YtDlp.Media do
:original_url, :original_url,
:livestream, :livestream,
:short_form_content, :short_form_content,
:upload_date, :uploaded_at,
:duration_seconds :duration_seconds
] ]
@@ -72,7 +72,7 @@ defmodule Pinchflat.YtDlp.Media do
Returns the output template for yt-dlp's indexing command. Returns the output template for yt-dlp's indexing command.
""" """
def indexing_output_template do def indexing_output_template do
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date})j" "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j"
end end
@doc """ @doc """
@@ -90,7 +90,7 @@ defmodule Pinchflat.YtDlp.Media do
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"]) uploaded_at: response["upload_date"] && parse_uploaded_at(response)
} }
end end
@@ -110,6 +110,18 @@ defmodule Pinchflat.YtDlp.Media do
end end
end end
defp parse_uploaded_at(%{"timestamp" => ts} = response) when is_number(ts) do
case DateTime.from_unix(ts) do
{:ok, datetime} -> datetime
_ -> MetadataFileHelpers.parse_upload_date(response["upload_date"])
end
end
# This field is needed before inserting into the database, but absence
# of this field should fail at insert-time rather than here
defp parse_uploaded_at(%{"upload_date" => nil}), do: nil
defp parse_uploaded_at(response), do: MetadataFileHelpers.parse_upload_date(response["upload_date"])
defp backend_runner do defp backend_runner do
# This approach lets us mock the command for testing # This approach lets us mock the command for testing
Application.get_env(:pinchflat, :yt_dlp_runner) Application.get_env(:pinchflat, :yt_dlp_runner)
@@ -2,7 +2,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
@moduledoc false @moduledoc false
use Phoenix.Component use Phoenix.Component
alias PinchflatWeb.CoreComponents import PinchflatWeb.CoreComponents
import PinchflatWeb.CustomComponents.TextComponents
@doc """ @doc """
Renders a table component with the given rows and columns. Renders a table component with the given rows and columns.
@@ -76,11 +77,13 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
phx-click={@page_number != 1 && "page_change"} phx-click={@page_number != 1 && "page_change"}
phx-value-direction="dec" phx-value-direction="dec"
> >
<CoreComponents.icon name="hero-chevron-left" /> <.icon name="hero-chevron-left" />
</span> </span>
</li> </li>
<li> <li>
<span class="mx-2">Page <%= @page_number %> of <%= @total_pages %></span> <span class="mx-2">
Page <.localized_number number={@page_number} /> of <.localized_number number={@total_pages} />
</span>
</li> </li>
<li> <li>
<span <span
@@ -92,7 +95,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
phx-click={@page_number != @total_pages && "page_change"} phx-click={@page_number != @total_pages && "page_change"}
phx-value-direction="inc" phx-value-direction="inc"
> >
<CoreComponents.icon name="hero-chevron-right" /> <.icon name="hero-chevron-right" />
</span> </span>
</li> </li>
</ul> </ul>
@@ -94,4 +94,28 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
<time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time> <time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time>
""" """
end end
@doc """
Renders a localized number using the Intl.NumberFormat API, falling back to the raw number if needed
"""
attr :number, :any, required: true
def localized_number(assigns) do
~H"""
<span x-data x-text={"Intl.NumberFormat().format(#{@number})"}><%= @number %></span>
"""
end
@doc """
Renders a word with a suffix if the count is not 1
"""
attr :word, :string, required: true
attr :count, :integer, required: true
attr :suffix, :string, default: "s"
def pluralize(assigns) do
~H"""
<%= @word %><%= if @count == 1, do: "", else: @suffix %>
"""
end
end end
@@ -19,6 +19,7 @@
onboarding: #{Settings.get!(:onboarding)} onboarding: #{Settings.get!(:onboarding)}
}"} }"}
class="dark text-bodydark bg-boxdark-2" class="dark text-bodydark bg-boxdark-2"
data-socket-path={Path.join(Application.get_env(:pinchflat, :base_route_path), "/live")}
> >
<%= @inner_content %> <%= @inner_content %>
@@ -85,6 +85,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
|> put_resp_header("accept-ranges", "bytes") |> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-range", "bytes #{start_pos}-#{end_pos}/#{file_size}") |> put_resp_header("content-range", "bytes #{start_pos}-#{end_pos}/#{file_size}")
|> put_resp_header("content-length", to_string(length)) |> put_resp_header("content-length", to_string(length))
|> put_resp_header("content-disposition", "inline; filename=\"#{media_item.title}\"")
|> send_file(206, media_item.media_filepath, start_pos, length) |> send_file(206, media_item.media_filepath, start_pos, length)
{:error, :invalid_range} -> {:error, :invalid_range} ->
@@ -92,8 +93,10 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
conn conn
|> put_resp_content_type(mime_type) |> put_resp_content_type(mime_type)
|> put_resp_header("content-length", to_string(file_size))
|> put_resp_header("accept-ranges", "bytes") |> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-range", "bytes 0-#{file_size - 1}/#{file_size}")
|> put_resp_header("content-length", to_string(file_size))
|> put_resp_header("content-disposition", "inline; filename=\"#{media_item.title}\"")
|> send_file(200, media_item.media_filepath) |> send_file(200, media_item.media_filepath)
end end
else else
@@ -31,11 +31,19 @@
<.media_preview media_item={@media_item} /> <.media_preview media_item={@media_item} />
</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: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
<div :if={URI.parse(@media_item.original_url).scheme =~ "http"}> <div>
<span :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>
<span class="mx-2">or</span>
</span>
<span>
<.subtle_link href={~p"/media/#{@media_item.uuid}/stream"} target="_blank">
Open Local Stream
</.subtle_link>
</span>
</div> </div>
<div class="mt-4 text-bodydark"> <div :if={@media_item.description} class="mt-4 text-bodydark">
<.break_on_newline text={@media_item.description} /> <.break_on_newline text={@media_item.description} />
</div> </div>
</aside> </aside>
@@ -183,7 +183,7 @@
options={friendly_format_type_options()} options={friendly_format_type_options()}
type="select" type="select"
label="Include Livestreams" label="Include Livestreams"
help="Excludes media that comes from a past livestream" help="How to handle past livestreams"
x-init="$watch('selectedPreset', p => p && ($el.value = presets[p]))" x-init="$watch('selectedPreset', p => p && ($el.value = presets[p]))"
/> />
</section> </section>
@@ -20,14 +20,14 @@ defmodule PinchflatWeb.Pages.PageController do
end end
defp render_home_page(conn) do defp render_home_page(conn) do
downloaded_media_items = where(MediaQuery.new(), ^MediaQuery.downloaded())
conn conn
|> render(:home, |> render(:home,
media_profile_count: Repo.aggregate(MediaProfile, :count, :id), media_profile_count: Repo.aggregate(MediaProfile, :count, :id),
source_count: Repo.aggregate(Source, :count, :id), source_count: Repo.aggregate(Source, :count, :id),
media_item_count: media_item_size: Repo.aggregate(downloaded_media_items, :sum, :media_size_bytes),
MediaQuery.new() media_item_count: Repo.aggregate(downloaded_media_items, :count, :id)
|> where(^MediaQuery.downloaded())
|> Repo.aggregate(:count, :id)
) )
end end
@@ -1,5 +1,23 @@
defmodule PinchflatWeb.Pages.PageHTML do defmodule PinchflatWeb.Pages.PageHTML do
use PinchflatWeb, :html use PinchflatWeb, :html
alias Pinchflat.Utils.NumberUtils
embed_templates "page_html/*" embed_templates "page_html/*"
attr :media_filesize, :integer, required: true
def readable_media_filesize(assigns) do
{num, suffix} = NumberUtils.human_byte_size(assigns.media_filesize, precision: 2)
assigns =
Map.merge(assigns, %{
num: num,
suffix: suffix
})
~H"""
<.localized_number number={@num} /> <%= @suffix %>
"""
end
end end
@@ -22,7 +22,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
<div> <div>
<span class="mb-4 flex items-center"> <span class="mb-4 flex items-center">
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" /> <.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
<span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span> <span class="ml-2">
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@total_record_count} />
</span>
</span> </span>
<div class="max-w-full overflow-x-auto"> <div class="max-w-full overflow-x-auto">
<.table rows={@records} table_class="text-white"> <.table rows={@records} table_class="text-white">
@@ -32,7 +34,7 @@ defmodule Pinchflat.Pages.HistoryTableLive do
</.subtle_link> </.subtle_link>
</:col> </:col>
<:col :let={media_item} label="Upload Date"> <:col :let={media_item} label="Upload Date">
<%= media_item.upload_date %> <%= DateTime.to_date(media_item.uploaded_at) %>
</:col> </:col>
<:col :let={media_item} label="Indexed At"> <:col :let={media_item} label="Indexed At">
<%= format_datetime(media_item.inserted_at) %> <%= format_datetime(media_item.inserted_at) %>
@@ -1,25 +1,39 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-3"> <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark"> <div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
<a href={~p"/media_profiles"} class="mt-4 flex flex-col items-center justify-center"> <a href={~p"/media_profiles"} class="flex flex-col items-center py-2">
<span class="text-md font-medium">Media Profile(s)</span> <span class="text-md font-medium">
Media <.pluralize count={@media_profile_count} word="Profile" />
</span>
<h4 class="text-title-md font-bold text-white"> <h4 class="text-title-md font-bold text-white">
<%= @media_profile_count %> <.localized_number number={@media_profile_count} />
</h4> </h4>
</a> </a>
</div> </div>
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark"> <div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
<a href={~p"/sources"} class="mt-4 flex flex-col items-center justify-center"> <a href={~p"/sources"} class="flex flex-col items-center py-2">
<span class="text-md font-medium">Source(s)</span> <span class="text-md font-medium">
<.pluralize count={@source_count} word="Source" />
</span>
<h4 class="text-title-md font-bold text-white"> <h4 class="text-title-md font-bold text-white">
<%= @source_count %> <.localized_number number={@source_count} />
</h4> </h4>
</a> </a>
</div> </div>
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark"> <div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
<span class="mt-4 flex flex-col items-center justify-center"> <span class="flex flex-col items-center py-2">
<span class="text-md font-medium">Downloaded Media</span> <span class="text-md font-medium">
<.pluralize count={@media_item_count} word="Download" />
</span>
<h4 class="text-title-md font-bold text-white"> <h4 class="text-title-md font-bold text-white">
<%= @media_item_count %> <.localized_number number={@media_item_count} />
</h4>
</span>
</div>
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
<span class="flex flex-col items-center py-2">
<span class="text-md font-medium">Library Size</span>
<h4 class="text-title-md font-bold text-white">
<.readable_media_filesize media_filesize={@media_item_size} />
</h4> </h4>
</span> </span>
</div> </div>
@@ -19,11 +19,12 @@
</.subtle_link> </.subtle_link>
</:col> </:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col> <:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Pending"><%= source.pending_count %></:col> <:col :let={source} label="Pending"><.localized_number number={source.pending_count} /></:col>
<:col :let={source} label="Downloaded"><%= source.downloaded_count %></:col> <:col :let={source} label="Downloaded"><.localized_number number={source.downloaded_count} /></:col>
<:col :let={source} label="Retention"> <:col :let={source} label="Retention">
<%= if source.retention_period_days && source.retention_period_days > 0 do %> <%= if source.retention_period_days && source.retention_period_days > 0 do %>
<%= source.retention_period_days %> day(s) <.localized_number number={source.retention_period_days} />
<.pluralize count={source.retention_period_days} word="day" />
<% else %> <% else %>
<span class="text-lg">∞</span> <span class="text-lg">∞</span>
<% end %> <% end %>
@@ -23,7 +23,9 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
<header class="flex justify-between items-center mb-4"> <header class="flex justify-between items-center mb-4">
<span class="flex items-center"> <span class="flex items-center">
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" /> <.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
<span class="ml-2">Showing <%= length(@records) %> of <%= @filtered_record_count %></span> <span class="ml-2">
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@filtered_record_count} />
</span>
</span> </span>
<div class="bg-meta-4 rounded-md"> <div class="bg-meta-4 rounded-md">
<div class="relative"> <div class="relative">
@@ -52,6 +54,9 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?"> <:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} /> <.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
</:col> </:col>
<:col :let={media_item} label="Upload Date">
<%= DateTime.to_date(media_item.uploaded_at) %>
</:col>
<:col :let={media_item} label="" class="flex justify-end"> <:col :let={media_item} label="" class="flex justify-end">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" /> <.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
</:col> </:col>
@@ -114,14 +119,40 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
{:noreply, assign(socket, new_assigns)} {:noreply, assign(socket, new_assigns)}
end end
defp fetch_pagination_attributes(base_query, page, ""), do: fetch_pagination_attributes(base_query, page, nil)
defp fetch_pagination_attributes(base_query, page, nil) do
total_record_count = Repo.aggregate(base_query, :count, :id)
total_pages = max(ceil(total_record_count / @limit), 1)
page = NumberUtils.clamp(page, 1, total_pages)
records =
fetch_records(base_query, page)
|> order_by(desc: :uploaded_at)
|> Repo.all()
%{
page: page,
total_pages: total_pages,
records: records,
search_term: nil,
total_record_count: total_record_count,
filtered_record_count: total_record_count
}
end
defp fetch_pagination_attributes(base_query, page, search_term) do defp fetch_pagination_attributes(base_query, page, search_term) do
filtered_base_query = filter_base_query(base_query, search_term) filtered_base_query = filtered_base_query(base_query, search_term)
total_record_count = Repo.aggregate(base_query, :count, :id) total_record_count = Repo.aggregate(base_query, :count, :id)
filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id) filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id)
total_pages = max(ceil(filtered_record_count / @limit), 1) total_pages = max(ceil(filtered_record_count / @limit), 1)
page = NumberUtils.clamp(page, 1, total_pages) page = NumberUtils.clamp(page, 1, total_pages)
records = fetch_records(filtered_base_query, page)
records =
fetch_records(filtered_base_query, page)
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|> Repo.all()
%{ %{
page: page, page: page,
@@ -139,39 +170,41 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
base_query base_query
|> limit(^@limit) |> limit(^@limit)
|> offset(^offset) |> offset(^offset)
|> Repo.all()
end end
defp generate_base_query(source, "pending") do defp generate_base_query(source, "pending") do
MediaQuery.new() MediaQuery.new()
|> select(^select_fields())
|> MediaQuery.require_assoc(:media_profile) |> MediaQuery.require_assoc(:media_profile)
|> MediaQuery.require_assoc(:media_items_search_index)
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending())) |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|> order_by(desc: fragment("rank"), desc: :id)
end end
defp generate_base_query(source, "downloaded") do defp generate_base_query(source, "downloaded") do
MediaQuery.new() MediaQuery.new()
|> MediaQuery.require_assoc(:media_items_search_index) |> select(^select_fields())
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> order_by(desc: fragment("rank"), desc: :id)
end end
defp generate_base_query(source, "other") do defp generate_base_query(source, "other") do
MediaQuery.new() MediaQuery.new()
|> select(^select_fields())
|> MediaQuery.require_assoc(:media_profile) |> MediaQuery.require_assoc(:media_profile)
|> MediaQuery.require_assoc(:media_items_search_index)
|> where( |> where(
^dynamic( ^dynamic(
^MediaQuery.for_source(source) and ^MediaQuery.for_source(source) and
(not (^MediaQuery.downloaded()) and not (^MediaQuery.pending())) (not (^MediaQuery.downloaded()) and not (^MediaQuery.pending()))
) )
) )
|> order_by(desc: fragment("rank"), desc: :id)
end end
defp filter_base_query(base_query, search_term) do defp filtered_base_query(base_query, search_term) do
base_query base_query
|> MediaQuery.require_assoc(:media_items_search_index)
|> where(^MediaQuery.matches_search_term(search_term)) |> where(^MediaQuery.matches_search_term(search_term))
end end
# Selecting only what we need GREATLY speeds up queries on large tables
defp select_fields do
[:id, :title, :uploaded_at, :prevent_download]
end
end end
@@ -95,7 +95,8 @@
type="number" type="number"
label="Retention Period (days)" label="Retention Period (days)"
min="0" min="0"
help="Days between when media is *downloaded* and when it's deleted. Leave blank to keep media indefinitely" help="Days between when media is <em>downloaded</em> and when it's deleted. Leave blank to keep media indefinitely"
html_help={true}
/> />
<section x-show="advancedMode"> <section x-show="advancedMode">
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do def project do
[ [
app: :pinchflat, app: :pinchflat,
version: "2024.5.24", version: "2024.5.30",
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,
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 471 KiB

@@ -0,0 +1,21 @@
defmodule Pinchflat.Repo.Migrations.RenameUploadDateToUploadedAt do
use Ecto.Migration
def up do
rename table(:media_items), :upload_date, to: :uploaded_at
execute """
UPDATE media_items
SET uploaded_at = uploaded_at || 'T00:00:00'
"""
end
def down do
rename table(:media_items), :uploaded_at, to: :upload_date
execute """
UPDATE media_items
SET upload_date = DATE(upload_date)
"""
end
end
@@ -0,0 +1,8 @@
defmodule Pinchflat.Repo.Migrations.ModifyUploadDateIndex do
use Ecto.Migration
def change do
drop index("media_items", [:upload_date])
create index("media_items", [:uploaded_at])
end
end
@@ -0,0 +1,19 @@
defmodule Pinchflat.Repo.Migrations.AddIndexesForLargeCollections do
use Ecto.Migration
def change do
create index(
"media_items",
[
:source_id,
:media_filepath,
:uploaded_at,
:prevent_download,
:livestream,
:short_form_content,
:title
],
name: "media_items_pending_and_downloaded_index"
)
end
end
@@ -16,7 +16,7 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorkerTest do
media_item = media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(6, :days), uploaded_at: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days) media_downloaded_at: now_minus(5, :days)
}) })
@@ -32,7 +32,7 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorkerTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(6, :days), uploaded_at: now_minus(6, :days),
media_downloaded_at: now_minus(1, :day) media_downloaded_at: now_minus(1, :day)
}) })
+147 -165
View File
@@ -34,150 +34,6 @@ defmodule Pinchflat.MediaTest do
end end
end end
describe "schema when testing upload_date_index and source is a channel" do
test "upload_date_index is set to 99 if it's the only video uploaded that day" do
upload_date = Date.utc_today()
source = source_fixture(%{collection_type: :channel})
media_item = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
assert media_item.upload_date_index == 99
end
test "upload_date_index is set to 98 if it's the second video uploaded that day" do
upload_date = Date.utc_today()
source = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
media_item_two = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
assert media_item_one.upload_date_index == 99
assert media_item_two.upload_date_index == 98
end
test "upload_date_index doesn't decrement if the video is uploaded on a different day" do
today = Date.utc_today()
one_day_ago = Date.add(today, -1)
source = source_fixture(%{collection_type: :channel})
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
assert media_item_new.upload_date_index == 99
assert media_item_old.upload_date_index == 99
end
test "recomputes upload_date_index if an upload_date is changed...somehow" do
today = Date.utc_today()
one_day_ago = Date.add(today, -1)
source = source_fixture(%{collection_type: :channel})
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{upload_date: today})
assert media_item_new.upload_date_index == 99
assert updated_media_item.upload_date_index == 98
end
test "upload_date_index doesn't decrement if the video is for a different source" do
today = Date.utc_today()
source_one = source_fixture(%{collection_type: :channel})
source_two = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source_one.id, upload_date: today})
media_item_two = media_item_fixture(%{source_id: source_two.id, upload_date: today})
assert media_item_one.upload_date_index == 99
assert media_item_two.upload_date_index == 99
end
test "upload_date_index doesn't decrement if the a video's upload_date is updated but doesn't change" do
today = Date.utc_today()
source = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: today})
_media_item_two = media_item_fixture(%{source_id: source.id, upload_date: today})
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{upload_date: today, title: "New title"})
assert updated_media_item.upload_date_index == 99
end
end
describe "schema when testing upload_date_index and source is a playlist" do
test "upload_date_index is set to 0 if it's the only video uploaded that day" do
upload_date = Date.utc_today()
source = source_fixture(%{collection_type: :playlist})
media_item = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
assert media_item.upload_date_index == 0
end
test "upload_date_index is set to 1 if it's the second video uploaded that day" do
upload_date = Date.utc_today()
source = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
media_item_two = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
assert media_item_one.upload_date_index == 0
assert media_item_two.upload_date_index == 1
end
test "upload_date_index doesn't increment if the video is uploaded on a different day" do
today = Date.utc_today()
one_day_ago = Date.add(today, -1)
source = source_fixture(%{collection_type: :playlist})
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
assert media_item_new.upload_date_index == 0
assert media_item_old.upload_date_index == 0
end
test "recomputes upload_date_index if an upload_date is changed...somehow" do
today = Date.utc_today()
one_day_ago = Date.add(today, -1)
source = source_fixture(%{collection_type: :playlist})
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{upload_date: today})
assert media_item_new.upload_date_index == 0
assert updated_media_item.upload_date_index == 1
end
test "upload_date_index doesn't increment if the video is for a different source" do
today = Date.utc_today()
source_one = source_fixture(%{collection_type: :playlist})
source_two = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source_one.id, upload_date: today})
media_item_two = media_item_fixture(%{source_id: source_two.id, upload_date: today})
assert media_item_one.upload_date_index == 0
assert media_item_two.upload_date_index == 0
end
test "upload_date_index doesn't increment if the a video's upload_date is updated but doesn't change" do
today = Date.utc_today()
source = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: today})
_media_item_two = media_item_fixture(%{source_id: source.id, upload_date: today})
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{upload_date: today, title: "New title"})
assert updated_media_item.upload_date_index == 0
end
end
describe "list_media_items/0" do describe "list_media_items/0" do
test "it returns all media_items" do test "it returns all media_items" do
media_item = media_item_fixture() media_item = media_item_fixture()
@@ -289,7 +145,7 @@ defmodule Pinchflat.MediaTest do
media_item = media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(6, :days), uploaded_at: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days) media_downloaded_at: now_minus(5, :days)
}) })
@@ -300,7 +156,7 @@ defmodule Pinchflat.MediaTest do
media_item = media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(20, :days), uploaded_at: now_minus(20, :days),
media_downloaded_at: now_minus(19, :days) media_downloaded_at: now_minus(19, :days)
}) })
@@ -311,7 +167,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(5, :days), uploaded_at: now_minus(5, :days),
media_downloaded_at: nil media_downloaded_at: nil
}) })
@@ -322,7 +178,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(5, :days), uploaded_at: now_minus(5, :days),
media_downloaded_at: now(), media_downloaded_at: now(),
prevent_download: true prevent_download: true
}) })
@@ -334,7 +190,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(5, :days), uploaded_at: now_minus(5, :days),
media_downloaded_at: now(), media_downloaded_at: now(),
culled_at: now() culled_at: now()
}) })
@@ -346,7 +202,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(3, :days), uploaded_at: now_minus(3, :days),
media_downloaded_at: now_minus(3, :days) media_downloaded_at: now_minus(3, :days)
}) })
@@ -357,7 +213,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(5, :days), uploaded_at: now_minus(5, :days),
media_downloaded_at: now(), media_downloaded_at: now(),
media_redownloaded_at: now() media_redownloaded_at: now()
}) })
@@ -365,12 +221,12 @@ defmodule Pinchflat.MediaTest do
assert Media.list_redownloadable_media_items() == [] assert Media.list_redownloadable_media_items() == []
end end
test "does not return media items that were first downloaded well after the upload_date", %{source: source} do test "does not return media items that were first downloaded well after the uploaded_at", %{source: source} do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
media_downloaded_at: now(), media_downloaded_at: now(),
upload_date: now_minus(20, :days) uploaded_at: now_minus(20, :days)
}) })
assert Media.list_redownloadable_media_items() == [] assert Media.list_redownloadable_media_items() == []
@@ -381,7 +237,7 @@ defmodule Pinchflat.MediaTest do
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
media_downloaded_at: now(), media_downloaded_at: now(),
upload_date: now_minus(2, :days) uploaded_at: now_minus(2, :days)
}) })
assert Media.list_redownloadable_media_items() == [] assert Media.list_redownloadable_media_items() == []
@@ -394,7 +250,7 @@ defmodule Pinchflat.MediaTest do
_media_item = _media_item =
media_item_fixture(%{ media_item_fixture(%{
source_id: source.id, source_id: source.id,
upload_date: now_minus(6, :days), uploaded_at: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days) media_downloaded_at: now_minus(5, :days)
}) })
@@ -552,9 +408,9 @@ defmodule Pinchflat.MediaTest do
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)}) source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
_old_media_item = _old_media_item =
media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)}) media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now()}) new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now()})
assert Media.list_pending_media_items_for(source) == [new_media_item] assert Media.list_pending_media_items_for(source) == [new_media_item]
end end
@@ -563,9 +419,9 @@ defmodule Pinchflat.MediaTest do
source = source_fixture(%{download_cutoff_date: nil}) source = source_fixture(%{download_cutoff_date: nil})
old_media_item = old_media_item =
media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)}) media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now()}) new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now()})
assert Media.list_pending_media_items_for(source) == [old_media_item, new_media_item] assert Media.list_pending_media_items_for(source) == [old_media_item, new_media_item]
end end
@@ -630,21 +486,21 @@ defmodule Pinchflat.MediaTest do
test "returns true if there is a cutoff date before the media's upload date" do test "returns true if there is a cutoff date before the media's upload date" do
source = source_fixture(%{download_cutoff_date: now_minus(2, :days)}) source = source_fixture(%{download_cutoff_date: now_minus(2, :days)})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(1, :day)}) media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(1, :day)})
assert Media.pending_download?(media_item) assert Media.pending_download?(media_item)
end end
test "returns false if there is a cutoff date after the media's upload date" do test "returns false if there is a cutoff date after the media's upload date" do
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)}) source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)}) media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
refute Media.pending_download?(media_item) refute Media.pending_download?(media_item)
end end
test "returns true if there is no cutoff date" do test "returns true if there is no cutoff date" do
source = source_fixture(%{download_cutoff_date: nil}) source = source_fixture(%{download_cutoff_date: nil})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(1, :day)}) media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(1, :day)})
assert Media.pending_download?(media_item) assert Media.pending_download?(media_item)
end end
@@ -755,7 +611,7 @@ defmodule Pinchflat.MediaTest do
media_filepath: "/video/#{Faker.File.file_name(:video)}", media_filepath: "/video/#{Faker.File.file_name(:video)}",
source_id: source_fixture().id, source_id: source_fixture().id,
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}", original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
upload_date: Date.utc_today() uploaded_at: now()
} }
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs) assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
@@ -772,7 +628,7 @@ defmodule Pinchflat.MediaTest do
media_filepath: "/video/#{Faker.File.file_name(:video)}", media_filepath: "/video/#{Faker.File.file_name(:video)}",
source_id: source_fixture().id, source_id: source_fixture().id,
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}", original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
upload_date: Date.utc_today() uploaded_at: now()
} }
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs) assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
@@ -787,7 +643,7 @@ defmodule Pinchflat.MediaTest do
media_filepath: "/video/#{Faker.File.file_name(:video)}", media_filepath: "/video/#{Faker.File.file_name(:video)}",
source_id: source_fixture().id, source_id: source_fixture().id,
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}", original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
upload_date: Date.utc_today(), uploaded_at: now(),
uuid: "some-uuid" uuid: "some-uuid"
} }
@@ -1052,4 +908,130 @@ defmodule Pinchflat.MediaTest do
assert %Ecto.Changeset{} = Media.change_media_item(media_item) assert %Ecto.Changeset{} = Media.change_media_item(media_item)
end end
end end
describe "change_media_item/1 when testing upload_date_index and source is a channel" do
test "upload_date_index is set to 99 if it's the only video uploaded that day" do
source = source_fixture(%{collection_type: :channel})
media_item = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
assert media_item.upload_date_index == 99
end
test "upload_date_index is set to 98 if it's the second video uploaded that day" do
source = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
assert media_item_one.upload_date_index == 99
assert media_item_two.upload_date_index == 98
end
test "upload_date_index doesn't decrement if the video is uploaded on a different day" do
source = source_fixture(%{collection_type: :channel})
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
assert media_item_new.upload_date_index == 99
assert media_item_old.upload_date_index == 99
end
test "recomputes upload_date_index if an upload_date is changed...somehow" do
source = source_fixture(%{collection_type: :channel})
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{uploaded_at: now()})
assert media_item_new.upload_date_index == 99
assert updated_media_item.upload_date_index == 98
end
test "upload_date_index doesn't decrement if the video is for a different source" do
source_one = source_fixture(%{collection_type: :channel})
source_two = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source_one.id, uploaded_at: now()})
media_item_two = media_item_fixture(%{source_id: source_two.id, uploaded_at: now()})
assert media_item_one.upload_date_index == 99
assert media_item_two.upload_date_index == 99
end
test "upload_date_index doesn't decrement if the a video's upload_date is updated but doesn't change" do
source = source_fixture(%{collection_type: :channel})
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{uploaded_at: now(), title: "New title"})
assert updated_media_item.upload_date_index == 99
end
end
describe "change_media_item/1 when testing upload_date_index and source is a playlist" do
test "upload_date_index is set to 0 if it's the only video uploaded that day" do
source = source_fixture(%{collection_type: :playlist})
media_item = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
assert media_item.upload_date_index == 0
end
test "upload_date_index is set to 1 if it's the second video uploaded that day" do
source = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
assert media_item_one.upload_date_index == 0
assert media_item_two.upload_date_index == 1
end
test "upload_date_index doesn't increment if the video is uploaded on a different day" do
source = source_fixture(%{collection_type: :playlist})
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
assert media_item_new.upload_date_index == 0
assert media_item_old.upload_date_index == 0
end
test "recomputes upload_date_index if an upload_date is changed...somehow" do
source = source_fixture(%{collection_type: :playlist})
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{uploaded_at: now()})
assert media_item_new.upload_date_index == 0
assert updated_media_item.upload_date_index == 1
end
test "upload_date_index doesn't increment if the video is for a different source" do
source_one = source_fixture(%{collection_type: :playlist})
source_two = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source_one.id, uploaded_at: now()})
media_item_two = media_item_fixture(%{source_id: source_two.id, uploaded_at: now()})
assert media_item_one.upload_date_index == 0
assert media_item_two.upload_date_index == 0
end
test "upload_date_index doesn't increment if the a video's upload_date is updated but doesn't change" do
source = source_fixture(%{collection_type: :playlist})
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{uploaded_at: now(), title: "New title"})
assert updated_media_item.upload_date_index == 0
end
end
end end
@@ -132,10 +132,10 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
end end
describe "parse_upload_date/1" do describe "parse_upload_date/1" do
test "returns a date from the given metadata upload date" do test "returns a datetime from the given metadata upload date" do
upload_date = "20210101" upload_date = "20210101"
assert Helpers.parse_upload_date(upload_date) == ~D[2021-01-01] assert Helpers.parse_upload_date(upload_date) == ~U[2021-01-01 00:00:00Z]
end end
end end
@@ -26,9 +26,9 @@ defmodule Pinchflat.Podcasts.PodcastHelpersTest do
test "orders by upload date where newest is first" do test "orders by upload date where newest is first" do
source = source_fixture() source = source_fixture()
oldest = media_item_with_attachments(%{source_id: source.id, upload_date: now_minus(2, :day)}) oldest = media_item_with_attachments(%{source_id: source.id, uploaded_at: now_minus(2, :day)})
current = media_item_with_attachments(%{source_id: source.id, upload_date: now()}) current = media_item_with_attachments(%{source_id: source.id, uploaded_at: now()})
older = media_item_with_attachments(%{source_id: source.id, upload_date: now_minus(1, :days)}) older = media_item_with_attachments(%{source_id: source.id, uploaded_at: now_minus(1, :days)})
assert [^current, ^older, ^oldest] = PodcastHelpers.persisted_media_items_for(source) assert [^current, ^older, ^oldest] = PodcastHelpers.persisted_media_items_for(source)
end end
@@ -117,8 +117,8 @@ defmodule Pinchflat.Podcasts.RssFeedBuilderTest do
assert String.contains?(item_xml, ~s(<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>)) assert String.contains?(item_xml, ~s(<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>))
end end
test "returns pubDate based off the media's upload_date", %{source: source} do test "returns pubDate based off the media's uploaded_at", %{source: source} do
media_item_with_attachments(%{source_id: source.id, upload_date: ~D[2020-01-01]}) media_item_with_attachments(%{source_id: source.id, uploaded_at: ~U[2020-01-01 00:00:00Z]})
res = RssFeedBuilder.build(source) res = RssFeedBuilder.build(source)
[_before, item_xml, _after] = String.split(res, ~r(</?item>)) [_before, item_xml, _after] = String.split(res, ~r(</?item>))
@@ -1,14 +0,0 @@
defmodule Pinchflat.Utils.DatetimeUtilsTest do
use Pinchflat.DataCase
alias Pinchflat.Utils.DatetimeUtils
describe "date_to_datetime/1" do
test "converts a Date to a DateTime" do
date = ~D[2022-01-01]
datetime = DatetimeUtils.date_to_datetime(date)
assert datetime == ~U[2022-01-01 00:00:00Z]
end
end
end
@@ -16,4 +16,35 @@ defmodule Pinchflat.Utils.NumberUtilsTest do
assert NumberUtils.clamp(2, 1, 3) == 2 assert NumberUtils.clamp(2, 1, 3) == 2
end end
end end
describe "human_byte_size/1" do
test "converts byte size to human readable format" do
assert NumberUtils.human_byte_size(1024) == {1, "KB"}
assert NumberUtils.human_byte_size(1024 * 1024) == {1, "MB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024) == {1, "GB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024 * 1024) == {1, "TB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024 * 1024 * 1024) == {1, "PB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024 * 1024 * 1024 * 1024) == {1, "EB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == {1, "ZB"}
assert NumberUtils.human_byte_size(1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == {1, "YB"}
end
test "returns the number when it is less than 1024" do
assert NumberUtils.human_byte_size(512) == {512, "B"}
end
test "optionally takes a precision" do
assert NumberUtils.human_byte_size(1234 * 1024, precision: 0) == {1, "MB"}
assert NumberUtils.human_byte_size(1234 * 1024, precision: 1) == {1.2, "MB"}
assert NumberUtils.human_byte_size(1234 * 1024, precision: 2) == {1.21, "MB"}
end
test "handles 0's well" do
assert NumberUtils.human_byte_size(0) == {0, "B"}
end
test "handles nil well" do
assert NumberUtils.human_byte_size(nil) == {0, "B"}
end
end
end end
+60 -28
View File
@@ -79,7 +79,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
describe "indexing_output_template/0" do describe "indexing_output_template/0" do
test "contains all the greatest hits" do test "contains all the greatest hits" do
assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date})j" == assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" ==
Media.indexing_output_template() Media.indexing_output_template()
end end
end end
@@ -94,7 +94,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
"was_live" => false, "was_live" => false,
"aspect_ratio" => 1.0, "aspect_ratio" => 1.0,
"duration" => 60, "duration" => 60,
"upload_date" => "20210101" "upload_date" => "20210101",
"timestamp" => 1_600_000_000
} }
assert %Media{ assert %Media{
@@ -104,7 +105,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
original_url: "https://www.youtube.com/watch?v=TiZPUDkDYbk", original_url: "https://www.youtube.com/watch?v=TiZPUDkDYbk",
livestream: false, livestream: false,
short_form_content: false, short_form_content: false,
upload_date: Date.from_iso8601!("2021-01-01"), uploaded_at: ~U[2020-09-13 12:26:40Z],
duration_seconds: 60 duration_seconds: 60
} == Media.response_to_struct(response) } == Media.response_to_struct(response)
end end
@@ -146,34 +147,11 @@ defmodule Pinchflat.YtDlp.MediaTest do
response = %{ response = %{
"webpage_url" => nil, "webpage_url" => nil,
"aspect_ratio" => nil, "aspect_ratio" => nil,
"duration" => nil "duration" => nil,
}
assert %Media{short_form_content: nil} = Media.response_to_struct(response)
end
test "parses the upload date" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => "20210101" "upload_date" => "20210101"
} }
expected_date = Date.from_iso8601!("2021-01-01") assert %Media{short_form_content: nil} = Media.response_to_struct(response)
assert %Media{upload_date: ^expected_date} = Media.response_to_struct(response)
end
test "doesn't blow up if upload date is missing" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => nil
}
assert %Media{upload_date: nil} = Media.response_to_struct(response)
end end
test "parses the duration" do test "parses the duration" do
@@ -209,4 +187,58 @@ defmodule Pinchflat.YtDlp.MediaTest do
assert %Media{livestream: false} = Media.response_to_struct(response) assert %Media{livestream: false} = Media.response_to_struct(response)
end end
end end
describe "response_to_struct/1 when testing uploaded_at" do
test "parses the upload date from the timestamp if present" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => "20210101",
"timestamp" => 1_600_000_000
}
expected_date = ~U[2020-09-13 12:26:40Z]
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
end
test "parses the upload date from the uploaded_at if timestamp is present but nil" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => "20210101",
"timestamp" => nil
}
expected_date = ~U[2021-01-01 00:00:00Z]
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
end
test "parses the upload date from the uploaded_at if timestamp absent" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => "20210101"
}
expected_date = ~U[2021-01-01 00:00:00Z]
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
end
test "doesn't blow up if upload date is missing" do
response = %{
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
"aspect_ratio" => 1.0,
"duration" => 61,
"upload_date" => nil
}
assert %Media{uploaded_at: nil} = Media.response_to_struct(response)
end
end
end end
@@ -14,6 +14,13 @@ defmodule PinchflatWeb.MediaItemControllerTest do
assert html_response(conn, 200) =~ "#{media_item.title}" assert html_response(conn, 200) =~ "#{media_item.title}"
end end
test "renders the page when the media item has no description", %{conn: conn} do
media_item = media_item_with_attachments(%{description: nil})
conn = get(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item}")
assert html_response(conn, 200) =~ "#{media_item.title}"
end
end end
describe "edit media" do describe "edit media" do
@@ -166,6 +173,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
assert conn.status == 206 assert conn.status == 206
assert {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers assert {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers
assert {"content-length", "101"} in conn.resp_headers assert {"content-length", "101"} in conn.resp_headers
assert {"content-disposition", "inline; filename=\"#{media_item.title}\""} in conn.resp_headers
end end
test "streams the specified range", %{conn: conn, media_item: media_item} do test "streams the specified range", %{conn: conn, media_item: media_item} do
@@ -241,6 +249,8 @@ defmodule PinchflatWeb.MediaItemControllerTest do
assert conn.status == 200 assert conn.status == 200
assert {"content-length", to_string(filesize)} in conn.resp_headers assert {"content-length", to_string(filesize)} in conn.resp_headers
assert {"content-range", "bytes 0-#{filesize - 1}/#{filesize}"} in conn.resp_headers
assert {"content-disposition", "inline; filename=\"#{media_item.title}\""} in conn.resp_headers
end end
test "streams the entire file", %{conn: conn, media_item: media_item} do test "streams the entire file", %{conn: conn, media_item: media_item} do
@@ -27,7 +27,7 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source)) {:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))
assert html =~ "Showing 1 of 1" assert html =~ "Showing"
assert html =~ "Title" assert html =~ "Title"
assert html =~ media_item.title assert html =~ media_item.title
end end
+1 -1
View File
@@ -9637,7 +9637,7 @@
"uploader_url": "https://www.youtube.com/@PinchflatTestChannel", "uploader_url": "https://www.youtube.com/@PinchflatTestChannel",
"upload_date": "20210720", "upload_date": "20210720",
"availability": "public", "availability": "public",
"original_url": "ABC123", "original_url": "https://www.youtube.com/watch?v=ABC123",
"webpage_url_basename": "watch", "webpage_url_basename": "watch",
"webpage_url_domain": "youtube.com", "webpage_url_domain": "youtube.com",
"extractor": "youtube", "extractor": "youtube",
+3 -2
View File
@@ -23,7 +23,7 @@ defmodule Pinchflat.MediaFixtures do
short_form_content: false, short_form_content: false,
media_filepath: "/video/#{Faker.File.file_name(:video)}", media_filepath: "/video/#{Faker.File.file_name(:video)}",
source_id: SourcesFixtures.source_fixture().id, source_id: SourcesFixtures.source_fixture().id,
upload_date: DateTime.utc_now() uploaded_at: DateTime.utc_now()
}) })
|> Pinchflat.Media.create_media_item() |> Pinchflat.Media.create_media_item()
@@ -97,7 +97,8 @@ defmodule Pinchflat.MediaFixtures do
description: "desc1", description: "desc1",
aspect_ratio: 1.67, aspect_ratio: 1.67,
duration: 123.45, duration: 123.45,
upload_date: "20210101" upload_date: "20210101",
timestamp: 1_600_000_000
} }
Phoenix.json_library().encode!(media_attributes) Phoenix.json_library().encode!(media_attributes)