Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 993c57f853 | |||
| 63bb4d2327 | |||
| 80406c9e0e | |||
| 61ae50735f | |||
| d8d7353228 | |||
| 03a0afd657 | |||
| ca90da49f5 | |||
| 40cde43be1 | |||
| e9f6b45953 | |||
| fb27988963 | |||
| 8a40d296c4 | |||
| d8fdba8f6d | |||
| e4c186eacb | |||
| e150355874 | |||
| 967e21a8a1 | |||
| 9185f075ca | |||
| 09d1653f4b | |||
| 115f675ae3 | |||
| f51b219860 | |||
| 246ca3b299 | |||
| c9bd1ea7bd | |||
| a2a70fcce2 | |||
| 0be469dcb0 | |||
| 023f449dbe | |||
| e9d365ee9e | |||
| 53e106dac2 | |||
| e56f39a158 | |||
| 01c9afa1c3 |
@@ -15,7 +15,8 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/kieraneglin/pinchflat/releases)
|
[](https://github.com/kieraneglin/pinchflat/releases)
|
||||||
|
[](https://discord.gg/j7T6dCuwU4)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/kieraneglin/pinchflat)
|
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/kieraneglin/pinchflat)
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
- [Portainer](#portainer)
|
- [Portainer](#portainer)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Environment Variables](#environment-variables)
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [A note on reverse proxies](#reverse-proxies)
|
||||||
- [Username and Password (authentication)](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password)
|
- [Username and Password (authentication)](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password)
|
||||||
- [Frequently asked questions](https://github.com/kieraneglin/pinchflat/wiki/Frequently-Asked-Questions)
|
- [Frequently asked questions](https://github.com/kieraneglin/pinchflat/wiki/Frequently-Asked-Questions)
|
||||||
- [Documentation](https://github.com/kieraneglin/pinchflat/wiki)
|
- [Documentation](https://github.com/kieraneglin/pinchflat/wiki)
|
||||||
@@ -142,18 +144,23 @@ If you change this setting and it works well for you, please leave a comment on
|
|||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Name | Required? | Default | Notes |
|
| Name | Required? | Default | Notes |
|
||||||
| --------------------------- | --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `TZ` | No | `UTC` | Must follow IANA TZ format |
|
| `TZ` | No | `UTC` | Must follow IANA TZ format |
|
||||||
| `LOG_LEVEL` | No | `debug` | Can be set to `info` but `debug` is strongly recommended |
|
| `LOG_LEVEL` | No | `debug` | Can be set to `info` but `debug` is strongly recommended |
|
||||||
| `BASIC_AUTH_USERNAME` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
| `BASIC_AUTH_USERNAME` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
||||||
| `BASIC_AUTH_PASSWORD` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
| `BASIC_AUTH_PASSWORD` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
||||||
| `EXPOSE_FEED_ENDPOINTS` | No | `false` | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
| `EXPOSE_FEED_ENDPOINTS` | No | `false` | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
||||||
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
||||||
| `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, but prefix must be stripped. |
|
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies - prefixes must be stripped. |
|
||||||
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
||||||
|
| `ENABLE_PROMETHEUS` | No | `false` | Setting to _any_ non-blank value will enable Prometheus. See [docs](https://github.com/kieraneglin/pinchflat/wiki/Prometheus-and-Grafana) |
|
||||||
|
|
||||||
|
### Reverse Proxies
|
||||||
|
|
||||||
|
Pinchflat makes heavy use of websockets for real-time updates. If you're running Pinchflat behind a reverse proxy then you'll need to make sure it's configured to support websockets.
|
||||||
|
|
||||||
## EFF donations
|
## EFF donations
|
||||||
|
|
||||||
|
|||||||
@@ -40,5 +40,9 @@ window.dispatchFor = (elementOrId, eventName, detail = {}) => {
|
|||||||
const element =
|
const element =
|
||||||
typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId
|
typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId
|
||||||
|
|
||||||
element.dispatchEvent(new CustomEvent(eventName, { detail }))
|
// This is needed to ensure the DOM has updated before dispatching the event.
|
||||||
|
// Doing so ensures that the latest DOM state is what's sent to the server
|
||||||
|
setTimeout(() => {
|
||||||
|
element.dispatchEvent(new Event(eventName, { bubbles: true, detail }))
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,29 +47,6 @@ let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
|
||||||
'formless-input': {
|
|
||||||
mounted() {
|
|
||||||
const subscribedEvents = this.el.dataset.subscribe.split(' ')
|
|
||||||
const eventName = this.el.dataset.eventName || ''
|
|
||||||
const identifier = this.el.dataset.identifier || ''
|
|
||||||
|
|
||||||
subscribedEvents.forEach((domEvent) => {
|
|
||||||
this.el.addEventListener(domEvent, () => {
|
|
||||||
// This ensures that the event is pushed to the server after the input value has been updated
|
|
||||||
// so that the server has the most up-to-date value
|
|
||||||
setTimeout(() => {
|
|
||||||
this.pushEvent('formless-input', {
|
|
||||||
value: this.el.value,
|
|
||||||
id: identifier,
|
|
||||||
event: eventName,
|
|
||||||
dom_id: this.el.id,
|
|
||||||
dom_event: domEvent
|
|
||||||
})
|
|
||||||
}, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -347,6 +347,38 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{ values }
|
{ values }
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
plugin(function ({ matchComponents, theme }) {
|
||||||
|
let iconsDir = path.join(__dirname, './vendor/simple-icons')
|
||||||
|
let values = {}
|
||||||
|
|
||||||
|
fs.readdirSync(iconsDir).forEach((file) => {
|
||||||
|
let name = path.basename(file, '.svg')
|
||||||
|
values[name] = { name, fullPath: path.join(iconsDir, file) }
|
||||||
|
})
|
||||||
|
|
||||||
|
matchComponents(
|
||||||
|
{
|
||||||
|
si: ({ name, fullPath }) => {
|
||||||
|
let content = fs
|
||||||
|
.readFileSync(fullPath)
|
||||||
|
.toString()
|
||||||
|
.replace(/\r?\n|\r/g, '')
|
||||||
|
return {
|
||||||
|
[`--si-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
|
||||||
|
'-webkit-mask': `var(--si-${name})`,
|
||||||
|
mask: `var(--si-${name})`,
|
||||||
|
'mask-repeat': 'no-repeat',
|
||||||
|
'background-color': 'currentColor',
|
||||||
|
'vertical-align': 'middle',
|
||||||
|
display: 'inline-block',
|
||||||
|
width: theme('spacing.5'),
|
||||||
|
height: theme('spacing.5')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ values }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
+1
@@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
After Width: | Height: | Size: 823 B |
@@ -99,6 +99,12 @@ config :logger, :default_formatter,
|
|||||||
# Use Jason for JSON parsing in Phoenix
|
# Use Jason for JSON parsing in Phoenix
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
|
config :pinchflat, Pinchflat.PromEx,
|
||||||
|
disabled: true,
|
||||||
|
manual_metrics_start_delay: :no_delay,
|
||||||
|
drop_metrics_groups: [],
|
||||||
|
metrics_server: :disabled
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
|||||||
@@ -81,3 +81,5 @@ config :phoenix_live_view, :debug_heex_annotations, true
|
|||||||
|
|
||||||
# Disable swoosh api client as it is only required for production adapters.
|
# Disable swoosh api client as it is only required for production adapters.
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
|
config :pinchflat, Pinchflat.PromEx, disabled: false
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ if config_env() == :prod do
|
|||||||
# For running PF in a subdirectory via a reverse proxy
|
# For running PF in a subdirectory via a reverse proxy
|
||||||
base_route_path = System.get_env("BASE_ROUTE_PATH", "/")
|
base_route_path = System.get_env("BASE_ROUTE_PATH", "/")
|
||||||
enable_ipv6 = String.length(System.get_env("ENABLE_IPV6", "")) > 0
|
enable_ipv6 = String.length(System.get_env("ENABLE_IPV6", "")) > 0
|
||||||
|
enable_prometheus = String.length(System.get_env("ENABLE_PROMETHEUS", "")) > 0
|
||||||
|
|
||||||
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"))
|
||||||
|
|
||||||
@@ -95,6 +96,8 @@ if config_env() == :prod do
|
|||||||
database: db_path,
|
database: db_path,
|
||||||
journal_mode: journal_mode
|
journal_mode: journal_mode
|
||||||
|
|
||||||
|
config :pinchflat, Pinchflat.PromEx, disabled: !enable_prometheus
|
||||||
|
|
||||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||||
# A default value is used in config/dev.exs and config/test.exs but you
|
# A default value is used in config/dev.exs and config/test.exs but you
|
||||||
# want to use a different value for prod and you most likely don't want
|
# want to use a different value for prod and you most likely don't want
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ EXPOSE ${PORT}
|
|||||||
# Only copy the final release from the build stage
|
# Only copy the final release from the build stage
|
||||||
COPY --from=builder /app/_build/${MIX_ENV}/rel/pinchflat ./
|
COPY --from=builder /app/_build/${MIX_ENV}/rel/pinchflat ./
|
||||||
|
|
||||||
HEALTHCHECK --interval=120s --start-period=10s \
|
HEALTHCHECK --interval=30s --start-period=15s \
|
||||||
CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1
|
CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1
|
||||||
|
|
||||||
# Start the app
|
# Start the app
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ defmodule Pinchflat.Application do
|
|||||||
check_and_update_timezone()
|
check_and_update_timezone()
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
|
Pinchflat.PromEx,
|
||||||
PinchflatWeb.Telemetry,
|
PinchflatWeb.Telemetry,
|
||||||
Pinchflat.Repo,
|
Pinchflat.Repo,
|
||||||
# Must be before startup tasks
|
# Must be before startup tasks
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
case {behaviour, categories} do
|
case {behaviour, categories} do
|
||||||
{_, []} -> []
|
{_, []} -> []
|
||||||
{:remove, _} -> [sponsorblock_remove: Enum.join(categories, ",")]
|
{:remove, _} -> [sponsorblock_remove: Enum.join(categories, ",")]
|
||||||
|
{:mark, _} -> [sponsorblock_mark: Enum.join(categories, ",")]
|
||||||
{:disabled, _} -> []
|
{:disabled, _} -> []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -200,6 +201,9 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
source = media_item_with_preloads.source
|
source = media_item_with_preloads.source
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
"media_item_id" => to_string(media_item_with_preloads.id),
|
||||||
|
"source_id" => to_string(source.id),
|
||||||
|
"media_profile_id" => to_string(source.media_profile_id),
|
||||||
"source_custom_name" => source.custom_name,
|
"source_custom_name" => source.custom_name,
|
||||||
"source_collection_id" => source.collection_id,
|
"source_collection_id" => source.collection_id,
|
||||||
"source_collection_name" => source.collection_name,
|
"source_collection_name" => source.collection_name,
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
|
|
||||||
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
||||||
|
|
||||||
# If the source or media item is set to not download media, perform a no-op unless forced
|
if should_download_media?(media_item, should_force, is_quality_upgrade) do
|
||||||
if (media_item.source.download_media && !media_item.prevent_download) || should_force do
|
|
||||||
download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force)
|
download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force)
|
||||||
else
|
else
|
||||||
:ok
|
:ok
|
||||||
@@ -60,6 +59,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
|
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If this is a quality upgrade, only check if the source is set to download media
|
||||||
|
# or that the media item's download hasn't been prevented
|
||||||
|
defp should_download_media?(media_item, should_force, true = _is_quality_upgrade) do
|
||||||
|
(media_item.source.download_media && !media_item.prevent_download) || should_force
|
||||||
|
end
|
||||||
|
|
||||||
|
# If it's not a quality upgrade, additionally check if the media item is pending download
|
||||||
|
defp should_download_media?(media_item, should_force, _is_quality_upgrade) do
|
||||||
|
source = media_item.source
|
||||||
|
is_pending = Media.pending_download?(media_item)
|
||||||
|
|
||||||
|
(is_pending && source.download_media && !media_item.prevent_download) || should_force
|
||||||
|
end
|
||||||
|
|
||||||
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
|
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
|
||||||
# of the media item.
|
# of the media item.
|
||||||
defp fetch_and_run_prevent_download_user_script(media_item_id) do
|
defp fetch_and_run_prevent_download_user_script(media_item_id) do
|
||||||
@@ -115,7 +128,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
defp action_on_error(message) do
|
defp action_on_error(message) do
|
||||||
# This will attempt re-download at the next indexing, but it won't be retried
|
# This will attempt re-download at the next indexing, but it won't be retried
|
||||||
# immediately as part of job failure logic
|
# immediately as part of job failure logic
|
||||||
non_retryable_errors = ["Video unavailable"]
|
non_retryable_errors = ["Video unavailable", "Sign in to confirm"]
|
||||||
|
|
||||||
if String.contains?(to_string(message), non_retryable_errors) do
|
if String.contains?(to_string(message), non_retryable_errors) do
|
||||||
Logger.error("yt-dlp download will not be retried: #{inspect(message)}")
|
Logger.error("yt-dlp download will not be retried: #{inspect(message)}")
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|
|
||||||
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
||||||
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
||||||
runner_opts = [output_filepath: output_filepath, use_cookies: item_with_preloads.source.use_cookies]
|
use_cookies = item_with_preloads.source.use_cookies
|
||||||
|
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
||||||
|
|
||||||
case YtDlpMedia.get_downloadable_status(url) do
|
case YtDlpMedia.get_downloadable_status(url, use_cookies: use_cookies) do
|
||||||
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
|
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
|
||||||
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
|
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
|
||||||
err -> err
|
err -> err
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
||||||
|> update_upload_date_index()
|
|> update_upload_date_index()
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|
# Validate that the title does NOT start with "youtube video #" since that indicates a restriction by YouTube.
|
||||||
|
# See issue #549 for more information.
|
||||||
|
|> validate_format(:title, ~r/^(?!youtube video #)/)
|
||||||
|> unique_constraint([:media_id, :source_id])
|
|> unique_constraint([:media_id, :source_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
defmodule Pinchflat.Podcasts.OpmlFeedBuilder do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for building an OPML feed for a list of sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
||||||
|
|
||||||
|
alias PinchflatWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds an OPML feed for a given list of sources.
|
||||||
|
|
||||||
|
Returns an XML document as a string.
|
||||||
|
"""
|
||||||
|
def build(url_base, sources) do
|
||||||
|
sources_xml =
|
||||||
|
Enum.map(
|
||||||
|
sources,
|
||||||
|
&"""
|
||||||
|
<outline type="rss" text="#{safe(&1.custom_name)}" xmlUrl="#{safe(source_route(url_base, &1))}" />
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<opml version="2.0">
|
||||||
|
<head>
|
||||||
|
<title>All Sources</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
#{Enum.join(sources_xml, "\n")}
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp source_route(url_base, source) do
|
||||||
|
Path.join(url_base, "#{Routes.podcast_path(PinchflatWeb.Endpoint, :rss_feed, source.uuid)}.xml")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,11 +5,25 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use Pinchflat.Media.MediaQuery
|
use Pinchflat.Media.MediaQuery
|
||||||
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Metadata.MediaMetadata
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
alias Pinchflat.Metadata.SourceMetadata
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a list of sources that are not marked for deletion.
|
||||||
|
|
||||||
|
Returns: [%Source{}]
|
||||||
|
"""
|
||||||
|
def opml_sources() do
|
||||||
|
SourcesQuery.new()
|
||||||
|
|> select([s], %{custom_name: s.custom_name, uuid: s.uuid})
|
||||||
|
|> where([s], is_nil(s.marked_for_deletion_at))
|
||||||
|
|> order_by(asc: :custom_name)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of media items that have been downloaded to disk
|
Returns a list of media items that have been downloaded to disk
|
||||||
and have been proven to still exist there.
|
and have been proven to still exist there.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
field :embed_metadata, :boolean, default: false
|
field :embed_metadata, :boolean, default: false
|
||||||
|
|
||||||
field :download_nfo, :boolean, default: false
|
field :download_nfo, :boolean, default: false
|
||||||
field :sponsorblock_behaviour, Ecto.Enum, values: [:disabled, :remove], default: :disabled
|
field :sponsorblock_behaviour, Ecto.Enum, values: [:disabled, :mark, :remove], default: :disabled
|
||||||
field :sponsorblock_categories, {:array, :string}, default: []
|
field :sponsorblock_categories, {:array, :string}, default: []
|
||||||
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
||||||
# through the entire collection to determine if a media is a short or
|
# through the entire collection to determine if a media is a short or
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
defmodule Pinchflat.PromEx do
|
||||||
|
@moduledoc """
|
||||||
|
Configuration for the PromEx library which provides Prometheus metrics
|
||||||
|
"""
|
||||||
|
|
||||||
|
use PromEx, otp_app: :pinchflat
|
||||||
|
|
||||||
|
alias PromEx.Plugins
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def plugins do
|
||||||
|
[
|
||||||
|
Plugins.Application,
|
||||||
|
Plugins.Beam,
|
||||||
|
{Plugins.Phoenix, router: PinchflatWeb.Router, endpoint: PinchflatWeb.Endpoint},
|
||||||
|
Plugins.Ecto,
|
||||||
|
Plugins.Oban,
|
||||||
|
Plugins.PhoenixLiveView
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def dashboard_assigns do
|
||||||
|
[
|
||||||
|
default_selected_interval: "30s"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def dashboards do
|
||||||
|
[
|
||||||
|
{:prom_ex, "application.json"},
|
||||||
|
{:prom_ex, "beam.json"},
|
||||||
|
{:prom_ex, "phoenix.json"},
|
||||||
|
{:prom_ex, "ecto.json"},
|
||||||
|
{:prom_ex, "oban.json"},
|
||||||
|
{:prom_ex, "phoenix_live_view.json"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,15 +14,17 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
:apprise_server,
|
:apprise_server,
|
||||||
:video_codec_preference,
|
:video_codec_preference,
|
||||||
:audio_codec_preference,
|
:audio_codec_preference,
|
||||||
:youtube_api_key
|
:youtube_api_key,
|
||||||
|
:extractor_sleep_interval_seconds
|
||||||
]
|
]
|
||||||
|
|
||||||
@required_fields ~w(
|
@required_fields [
|
||||||
onboarding
|
:onboarding,
|
||||||
pro_enabled
|
:pro_enabled,
|
||||||
video_codec_preference
|
:video_codec_preference,
|
||||||
audio_codec_preference
|
:audio_codec_preference,
|
||||||
)a
|
:extractor_sleep_interval_seconds
|
||||||
|
]
|
||||||
|
|
||||||
schema "settings" do
|
schema "settings" do
|
||||||
field :onboarding, :boolean, default: true
|
field :onboarding, :boolean, default: true
|
||||||
@@ -31,6 +33,8 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
field :apprise_version, :string
|
field :apprise_version, :string
|
||||||
field :apprise_server, :string
|
field :apprise_server, :string
|
||||||
field :youtube_api_key, :string
|
field :youtube_api_key, :string
|
||||||
|
field :route_token, :string
|
||||||
|
field :extractor_sleep_interval_seconds, :integer, default: 0
|
||||||
|
|
||||||
field :video_codec_preference, :string
|
field :video_codec_preference, :string
|
||||||
field :audio_codec_preference, :string
|
field :audio_codec_preference, :string
|
||||||
@@ -41,5 +45,6 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
setting
|
setting
|
||||||
|> cast(attrs, @allowed_fields)
|
|> cast(attrs, @allowed_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|
|> validate_number(:extractor_sleep_interval_seconds, greater_than_or_equal_to: 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -79,21 +79,21 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
|
|||||||
case {source.index_frequency_minutes, source.last_indexed_at} do
|
case {source.index_frequency_minutes, source.last_indexed_at} do
|
||||||
{index_freq, _} when index_freq > 0 ->
|
{index_freq, _} when index_freq > 0 ->
|
||||||
# If the indexing is on a schedule simply run indexing and reschedule
|
# If the indexing is on a schedule simply run indexing and reschedule
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: args["force"])
|
||||||
maybe_enqueue_fast_indexing_task(source)
|
maybe_enqueue_fast_indexing_task(source)
|
||||||
reschedule_indexing(source)
|
reschedule_indexing(source)
|
||||||
|
|
||||||
{_, nil} ->
|
{_, nil} ->
|
||||||
# If the source has never been indexed, index it once
|
# If the source has never been indexed, index it once
|
||||||
# even if it's not meant to reschedule
|
# even if it's not meant to reschedule
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: args["force"])
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# If the source HAS been indexed and is not meant to reschedule,
|
# If the source HAS been indexed and is not meant to reschedule,
|
||||||
# perform a no-op (unless forced)
|
# perform a no-op (unless forced)
|
||||||
if args["force"] do
|
if args["force"] do
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -103,11 +103,11 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
|
|||||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp perform_indexing_and_notification(source) do
|
defp perform_indexing_and_notification(source, indexing_opts) do
|
||||||
apprise_server = Settings.get!(:apprise_server)
|
apprise_server = Settings.get!(:apprise_server)
|
||||||
|
|
||||||
SourceNotifications.wrap_new_media_notification(apprise_server, source, fn ->
|
SourceNotifications.wrap_new_media_notification(apprise_server, source, fn ->
|
||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, indexing_opts)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
Many of these methods are made to be kickoff or be consumed by workers.
|
Many of these methods are made to be kickoff or be consumed by workers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
@@ -14,6 +16,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.YtDlp.MediaCollection
|
alias Pinchflat.YtDlp.MediaCollection
|
||||||
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||||
@@ -22,13 +25,19 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts tasks for indexing a source's media regardless of the source's indexing
|
Kills old indexing tasks and starts a new task to index the media collection.
|
||||||
frequency. It's assumed the caller will check for indexing frequency.
|
|
||||||
|
The job is delayed based on the source's `index_frequency_minutes` setting unless
|
||||||
|
one of the following is true:
|
||||||
|
- The `force` option is set to true
|
||||||
|
- The source has never been indexed before
|
||||||
|
- The source has been indexed before, but the last indexing job was more than
|
||||||
|
`index_frequency_minutes` ago
|
||||||
|
|
||||||
Returns {:ok, %Task{}}
|
Returns {:ok, %Task{}}
|
||||||
"""
|
"""
|
||||||
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
||||||
job_offset_seconds = calculate_job_offset_seconds(source)
|
job_offset_seconds = if job_args[:force], do: 0, else: calculate_job_offset_seconds(source)
|
||||||
|
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
||||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
||||||
@@ -52,8 +61,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
@doc """
|
@doc """
|
||||||
Given a media source, creates (indexes) the media by creating media_items for each
|
Given a media source, creates (indexes) the media by creating media_items for each
|
||||||
media ID in the source. Afterward, kicks off a download task for each pending media
|
media ID in the source. Afterward, kicks off a download task for each pending media
|
||||||
item belonging to the source. You can't tell me the method name isn't descriptive!
|
item belonging to the source. Returns a list of media items or changesets
|
||||||
Returns a list of media items or changesets (if the media item couldn't be created).
|
(if the media item couldn't be created).
|
||||||
|
|
||||||
Indexing is slow and usually returns a list of all media data at once for record creation.
|
Indexing is slow and usually returns a list of all media data at once for record creation.
|
||||||
To help with this, we use a file follower to watch the file that yt-dlp writes to
|
To help with this, we use a file follower to watch the file that yt-dlp writes to
|
||||||
@@ -61,23 +70,33 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
clarity to the user experience. This has a few things to be aware of which are documented
|
clarity to the user experience. This has a few things to be aware of which are documented
|
||||||
below in the file watcher setup method.
|
below in the file watcher setup method.
|
||||||
|
|
||||||
|
Additionally, in the case of a repeat index we create a download archive file that
|
||||||
|
contains some media IDs that we've indexed in the past. Note that this archive doesn't
|
||||||
|
contain the most recent IDs but rather a subset of IDs that are offset by some amount.
|
||||||
|
Practically, this means that we'll re-index a small handful of media that we've recently
|
||||||
|
indexed, but this is a good thing since it'll let us pick up on any recent changes to the
|
||||||
|
most recent media items.
|
||||||
|
|
||||||
|
We don't create a download archive for playlists (only channels), nor do we create one if
|
||||||
|
the indexing was forced by the user.
|
||||||
|
|
||||||
NOTE: downloads are only enqueued if the source is set to download media. Downloads are
|
NOTE: downloads are only enqueued if the source is set to download media. Downloads are
|
||||||
also enqueued for ALL pending media items, not just the ones that were indexed in this
|
also enqueued for ALL pending media items, not just the ones that were indexed in this
|
||||||
job run. This should ensure that any stragglers are caught if, for some reason, they
|
job run. This should ensure that any stragglers are caught if, for some reason, they
|
||||||
weren't enqueued or somehow got de-queued.
|
weren't enqueued or somehow got de-queued.
|
||||||
|
|
||||||
Since indexing returns all media data EVERY TIME, we that that opportunity to update
|
Available options:
|
||||||
indexing metadata for media items that have already been created.
|
- `was_forced`: Whether the indexing was forced by the user
|
||||||
|
|
||||||
Returns [%MediaItem{} | %Ecto.Changeset{}]
|
Returns [%MediaItem{} | %Ecto.Changeset{}]
|
||||||
"""
|
"""
|
||||||
def index_and_enqueue_download_for_media_items(%Source{} = source) do
|
def index_and_enqueue_download_for_media_items(%Source{} = source, opts \\ []) do
|
||||||
# The media_profile is needed to determine the quality options to _then_ determine a more
|
# The media_profile is needed to determine the quality options to _then_ determine a more
|
||||||
# accurate predicted filepath
|
# accurate predicted filepath
|
||||||
source = Repo.preload(source, [:media_profile])
|
source = Repo.preload(source, [:media_profile])
|
||||||
# See the method definition below for more info on how file watchers work
|
# See the method definition below for more info on how file watchers work
|
||||||
# (important reading if you're not familiar with it)
|
# (important reading if you're not familiar with it)
|
||||||
{:ok, media_attributes} = setup_file_watcher_and_kickoff_indexing(source)
|
{:ok, media_attributes} = setup_file_watcher_and_kickoff_indexing(source, opts)
|
||||||
# Reload because the source may have been updated during the (long-running) indexing process
|
# Reload because the source may have been updated during the (long-running) indexing process
|
||||||
# and important settings like `download_media` may have changed.
|
# and important settings like `download_media` may have changed.
|
||||||
source = Repo.reload!(source)
|
source = Repo.reload!(source)
|
||||||
@@ -109,14 +128,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
# It attempts a graceful shutdown of the file follower after the indexing is done,
|
# It attempts a graceful shutdown of the file follower after the indexing is done,
|
||||||
# but the FileFollowerServer will also stop itself if it doesn't see any activity
|
# but the FileFollowerServer will also stop itself if it doesn't see any activity
|
||||||
# for a sufficiently long time.
|
# for a sufficiently long time.
|
||||||
defp setup_file_watcher_and_kickoff_indexing(source) do
|
defp setup_file_watcher_and_kickoff_indexing(source, opts) do
|
||||||
|
was_forced = Keyword.get(opts, :was_forced, false)
|
||||||
{:ok, pid} = FileFollowerServer.start_link()
|
{:ok, pid} = FileFollowerServer.start_link()
|
||||||
|
|
||||||
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
||||||
|
|
||||||
command_opts =
|
command_opts =
|
||||||
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
||||||
DownloadOptionBuilder.build_quality_options_for(source)
|
DownloadOptionBuilder.build_quality_options_for(source) ++
|
||||||
|
build_download_archive_options(source, was_forced)
|
||||||
|
|
||||||
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
||||||
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
||||||
@@ -166,4 +187,57 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
|
|
||||||
max(0, index_frequency_seconds - offset_seconds)
|
max(0, index_frequency_seconds - offset_seconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The download archive file works in tandem with --break-on-existing to stop
|
||||||
|
# yt-dlp once we've hit media items we've already indexed. But we generate
|
||||||
|
# this list with a bit of an offset so we do intentionally re-scan some media
|
||||||
|
# items to pick up any recent changes (see `get_media_items_for_download_archive`).
|
||||||
|
#
|
||||||
|
# From there, we format the media IDs in the way that yt-dlp expects (ie: "<extractor> <media_id>")
|
||||||
|
# and return the filepath to the caller.
|
||||||
|
defp create_download_archive_file(source) do
|
||||||
|
tmpfile = FilesystemUtils.generate_metadata_tmpfile(:txt)
|
||||||
|
|
||||||
|
archive_contents =
|
||||||
|
source
|
||||||
|
|> get_media_items_for_download_archive()
|
||||||
|
|> Enum.map_join("\n", fn media_item -> "youtube #{media_item.media_id}" end)
|
||||||
|
|
||||||
|
case File.write(tmpfile, archive_contents) do
|
||||||
|
:ok -> tmpfile
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sorting by `uploaded_at` is important because we want to re-index the most recent
|
||||||
|
# media items first but there is no guarantee of any correlation between ID and uploaded_at.
|
||||||
|
#
|
||||||
|
# The offset is important because we want to re-index some media items that we've
|
||||||
|
# recently indexed to pick up on any changes. The limit is because we want this mechanism
|
||||||
|
# to work even if, for example, the video we were using as a stopping point was deleted.
|
||||||
|
# It's not a perfect system, but it should do well enough.
|
||||||
|
#
|
||||||
|
# The chosen limit and offset are arbitary, independent, and vibes-based. Feel free to
|
||||||
|
# tweak as-needed
|
||||||
|
defp get_media_items_for_download_archive(source) do
|
||||||
|
MediaQuery.new()
|
||||||
|
|> where(^MediaQuery.for_source(source))
|
||||||
|
|> order_by(desc: :uploaded_at)
|
||||||
|
|> limit(50)
|
||||||
|
|> offset(20)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
# The download archive isn't useful for playlists (since those are ordered arbitrarily)
|
||||||
|
# and we don't want to use it if the indexing was forced by the user. In other words,
|
||||||
|
# only create an archive for channels that are being indexed as part of their regular
|
||||||
|
# indexing schedule
|
||||||
|
defp build_download_archive_options(%Source{collection_type: :playlist}, _was_forced), do: []
|
||||||
|
defp build_download_archive_options(_source, true), do: []
|
||||||
|
|
||||||
|
defp build_download_archive_options(source, _was_forced) do
|
||||||
|
archive_file = create_download_archive_file(source)
|
||||||
|
|
||||||
|
[:break_on_existing, download_archive: archive_file]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -180,9 +180,12 @@ defmodule Pinchflat.Sources do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp add_source_details_to_changeset(source, changeset) do
|
defp add_source_details_to_changeset(source, changeset) do
|
||||||
|
original_url = changeset.changes.original_url
|
||||||
use_cookies = Ecto.Changeset.get_field(changeset, :use_cookies)
|
use_cookies = Ecto.Changeset.get_field(changeset, :use_cookies)
|
||||||
|
# Skipping sleep interval since this is UI blocking and we want to keep this as fast as possible
|
||||||
|
addl_opts = [use_cookies: use_cookies, skip_sleep_interval: true]
|
||||||
|
|
||||||
case MediaCollection.get_source_details(changeset.changes.original_url, [], use_cookies: use_cookies) do
|
case MediaCollection.get_source_details(original_url, [], addl_opts) do
|
||||||
{:ok, source_details} ->
|
{:ok, source_details} ->
|
||||||
add_source_details_by_collection_type(source, changeset, source_details)
|
add_source_details_by_collection_type(source, changeset, source_details)
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,20 @@ defmodule Pinchflat.Utils.FilesystemUtils do
|
|||||||
Returns binary()
|
Returns binary()
|
||||||
"""
|
"""
|
||||||
def generate_metadata_tmpfile(type) do
|
def generate_metadata_tmpfile(type) do
|
||||||
|
filename = StringUtils.random_string(64)
|
||||||
|
# This "namespacing" is more to help with development since things get
|
||||||
|
# weird in my editor when there are thousands of files in a single directory
|
||||||
|
first_two = String.slice(filename, 0..1)
|
||||||
|
second_two = String.slice(filename, 2..3)
|
||||||
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
filepath = Path.join([tmpfile_directory, "#{StringUtils.random_string(64)}.#{type}"])
|
|
||||||
|
filepath =
|
||||||
|
Path.join([
|
||||||
|
tmpfile_directory,
|
||||||
|
first_two,
|
||||||
|
second_two,
|
||||||
|
"#{filename}.#{type}"
|
||||||
|
])
|
||||||
|
|
||||||
:ok = write_p!(filepath, "")
|
:ok = write_p!(filepath, "")
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,18 @@ defmodule Pinchflat.Utils.NumberUtils do
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds jitter to a number based on a percentage. Returns 0 if the number is less than or equal to 0.
|
||||||
|
|
||||||
|
Returns integer()
|
||||||
|
"""
|
||||||
|
def add_jitter(num, jitter_percentage \\ 0.5)
|
||||||
|
def add_jitter(num, _jitter_percentage) when num <= 0, do: 0
|
||||||
|
|
||||||
|
def add_jitter(num, jitter_percentage) do
|
||||||
|
jitter = :rand.uniform(round(num * jitter_percentage))
|
||||||
|
|
||||||
|
round(num + jitter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,23 +25,6 @@ defmodule Pinchflat.Utils.StringUtils do
|
|||||||
|> String.slice(0..(length - 1))
|
|> String.slice(0..(length - 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Truncates a string to the given length and adds `...` if the string is longer than the given length.
|
|
||||||
Will break on a word boundary. Nothing happens if the string is shorter than the given length.
|
|
||||||
|
|
||||||
Returns binary()
|
|
||||||
"""
|
|
||||||
def truncate(string, length) do
|
|
||||||
if String.length(string) > length do
|
|
||||||
string
|
|
||||||
|> String.slice(0..(length - 1))
|
|
||||||
|> String.replace(~r/\s+\S*$/, "")
|
|
||||||
|> Kernel.<>("...")
|
|
||||||
else
|
|
||||||
string
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Wraps a string in double braces. Useful as a UI helper now that
|
Wraps a string in double braces. Useful as a UI helper now that
|
||||||
LiveView 1.0.0 allows `{}` for interpolation so now we can't use braces
|
LiveView 1.0.0 allows `{}` for interpolation so now we can't use braces
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
Runs yt-dlp commands using the `System.cmd/3` function
|
Runs yt-dlp commands using the `System.cmd/3` function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Utils.CliUtils
|
alias Pinchflat.Utils.CliUtils
|
||||||
|
alias Pinchflat.Utils.NumberUtils
|
||||||
alias Pinchflat.YtDlp.YtDlpCommandRunner
|
alias Pinchflat.YtDlp.YtDlpCommandRunner
|
||||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||||
|
|
||||||
@@ -20,23 +24,30 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
for a file watcher.
|
for a file watcher.
|
||||||
- :use_cookies - if true, will add a cookie file to the command options. Will not
|
- :use_cookies - if true, will add a cookie file to the command options. Will not
|
||||||
attach a cookie file if the user hasn't set one up.
|
attach a cookie file if the user hasn't set one up.
|
||||||
|
- :skip_sleep_interval - if true, will not add the sleep interval options to the command.
|
||||||
|
Usually only used for commands that would be UI-blocking
|
||||||
|
|
||||||
Returns {:ok, binary()} | {:error, output, status}.
|
Returns {:ok, binary()} | {:error, output, status}.
|
||||||
"""
|
"""
|
||||||
@impl YtDlpCommandRunner
|
@impl YtDlpCommandRunner
|
||||||
def run(url, command_opts, output_template, addl_opts \\ []) do
|
def run(url, action_name, command_opts, output_template, addl_opts \\ []) do
|
||||||
# This approach lets us mock the command for testing
|
Logger.debug("Running yt-dlp command for action: #{action_name}")
|
||||||
command = backend_executable()
|
|
||||||
|
|
||||||
output_filepath = generate_output_filepath(addl_opts)
|
output_filepath = generate_output_filepath(addl_opts)
|
||||||
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
|
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
|
||||||
user_configured_opts = cookie_file_options(addl_opts)
|
user_configured_opts = cookie_file_options(addl_opts) ++ sleep_interval_opts(addl_opts)
|
||||||
# These must stay in exactly this order, hence why I'm giving it its own variable.
|
# These must stay in exactly this order, hence why I'm giving it its own variable.
|
||||||
all_opts = command_opts ++ print_to_file_opts ++ user_configured_opts ++ global_options()
|
all_opts = command_opts ++ print_to_file_opts ++ user_configured_opts ++ global_options()
|
||||||
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
|
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
|
||||||
|
|
||||||
case CliUtils.wrap_cmd(command, formatted_command_opts, stderr_to_stdout: true) do
|
case CliUtils.wrap_cmd(backend_executable(), formatted_command_opts, stderr_to_stdout: true) do
|
||||||
{_, 0} ->
|
# yt-dlp exit codes:
|
||||||
|
# 0 = Everything is successful
|
||||||
|
# 100 = yt-dlp must restart for update to complete
|
||||||
|
# 101 = Download cancelled by --max-downloads etc
|
||||||
|
# 2 = Error in user-provided options
|
||||||
|
# 1 = Any other error
|
||||||
|
{_, status} when status in [0, 101] ->
|
||||||
# IDEA: consider deleting the file after reading it. It's in the tmp dir, so it's not
|
# IDEA: consider deleting the file after reading it. It's in the tmp dir, so it's not
|
||||||
# a huge deal, but it's still a good idea to clean up after ourselves.
|
# a huge deal, but it's still a good idea to clean up after ourselves.
|
||||||
# (even on error? especially on error?)
|
# (even on error? especially on error?)
|
||||||
@@ -87,6 +98,20 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp sleep_interval_opts(addl_opts) do
|
||||||
|
sleep_interval = Settings.get!(:extractor_sleep_interval_seconds)
|
||||||
|
|
||||||
|
if sleep_interval <= 0 || Keyword.get(addl_opts, :skip_sleep_interval) do
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
[
|
||||||
|
sleep_requests: NumberUtils.add_jitter(sleep_interval),
|
||||||
|
sleep_interval: NumberUtils.add_jitter(sleep_interval),
|
||||||
|
sleep_subtitles: NumberUtils.add_jitter(sleep_interval)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp add_cookie_file do
|
defp add_cookie_file do
|
||||||
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||||
filename_options_map = %{cookies: "cookies.txt"}
|
filename_options_map = %{cookies: "cookies.txt"}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
def download(url, command_opts \\ [], addl_opts \\ []) do
|
def download(url, command_opts \\ [], addl_opts \\ []) do
|
||||||
all_command_opts = [:no_simulate] ++ command_opts
|
all_command_opts = [:no_simulate] ++ command_opts
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts),
|
with {:ok, output} <- backend_runner().run(url, :download, all_command_opts, "after_move:%()j", addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, parsed_json}
|
{:ok, parsed_json}
|
||||||
else
|
else
|
||||||
@@ -55,8 +55,11 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
|
|
||||||
Returns {:ok, :downloadable | :ignorable} | {:error, any}
|
Returns {:ok, :downloadable | :ignorable} | {:error, any}
|
||||||
"""
|
"""
|
||||||
def get_downloadable_status(url) do
|
def get_downloadable_status(url, addl_opts \\ []) do
|
||||||
case backend_runner().run(url, [:simulate, :skip_download], "%(.{live_status})j") do
|
action = :get_downloadable_status
|
||||||
|
command_opts = [:simulate, :skip_download]
|
||||||
|
|
||||||
|
case backend_runner().run(url, action, command_opts, "%(.{live_status})j", addl_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
output
|
output
|
||||||
|> Phoenix.json_library().decode!()
|
|> Phoenix.json_library().decode!()
|
||||||
@@ -78,7 +81,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
|
|
||||||
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
||||||
# we aren't downloading the main media file
|
# we aren't downloading the main media file
|
||||||
backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts)
|
backend_runner().run(url, :download_thumbnail, all_command_opts, "after_move:%()j", addl_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -92,7 +95,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
all_command_opts = [:simulate, :skip_download] ++ command_opts
|
all_command_opts = [:simulate, :skip_download] ++ command_opts
|
||||||
output_template = indexing_output_template()
|
output_template = indexing_output_template()
|
||||||
|
|
||||||
case backend_runner().run(url, all_command_opts, output_template, addl_opts) do
|
case backend_runner().run(url, :get_media_attributes, all_command_opts, output_template, addl_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
output
|
output
|
||||||
|> Phoenix.json_library().decode!()
|
|> Phoenix.json_library().decode!()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
Returns {:ok, [map()]} | {:error, any, ...}.
|
Returns {:ok, [map()]} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_media_attributes_for_collection(url, command_opts \\ [], addl_opts \\ []) do
|
def get_media_attributes_for_collection(url, command_opts \\ [], addl_opts \\ []) do
|
||||||
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
|
||||||
# `ignore_no_formats_error` is necessary because yt-dlp will error out if
|
# `ignore_no_formats_error` is necessary because yt-dlp will error out if
|
||||||
# the first video has not released yet (ie: is a premier). We don't care about
|
# the first video has not released yet (ie: is a premier). We don't care about
|
||||||
# available formats since we're just getting the media details
|
# available formats since we're just getting the media details
|
||||||
@@ -33,12 +32,13 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||||
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
|
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
|
||||||
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
||||||
|
action = :get_media_attributes_for_collection
|
||||||
|
|
||||||
if file_listener_handler do
|
if file_listener_handler do
|
||||||
file_listener_handler.(output_filepath)
|
file_listener_handler.(output_filepath)
|
||||||
end
|
end
|
||||||
|
|
||||||
case runner.run(url, all_command_opts, output_template, runner_opts) do
|
case backend_runner().run(url, action, all_command_opts, output_template, runner_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
parsed_lines =
|
parsed_lines =
|
||||||
output
|
output
|
||||||
@@ -82,8 +82,9 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
|
|
||||||
all_command_opts = default_opts ++ command_opts
|
all_command_opts = default_opts ++ command_opts
|
||||||
output_template = "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
output_template = "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||||
|
action = :get_source_details
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
with {:ok, output} <- backend_runner().run(source_url, action, all_command_opts, output_template, addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, format_source_details(parsed_json)}
|
{:ok, format_source_details(parsed_json)}
|
||||||
else
|
else
|
||||||
@@ -121,8 +122,9 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
|
|
||||||
all_command_opts = [:skip_download] ++ command_opts
|
all_command_opts = [:skip_download] ++ command_opts
|
||||||
output_template = "playlist:%()j"
|
output_template = "playlist:%()j"
|
||||||
|
action = :get_source_metadata
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
with {:ok, output} <- backend_runner().run(source_url, action, all_command_opts, output_template, addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, parsed_json}
|
{:ok, parsed_json}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ defmodule Pinchflat.YtDlp.YtDlpCommandRunner do
|
|||||||
yt-dlp command.
|
yt-dlp command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@callback run(binary(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback run(binary(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback version() :: {:ok, binary()} | {:error, binary()}
|
@callback version() :: {:ok, binary()} | {:error, binary()}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
attr :class, :string, default: nil
|
attr :class, :string, default: nil
|
||||||
attr :rest, :global
|
attr :rest, :global
|
||||||
|
|
||||||
def icon(%{name: "hero-" <> _} = assigns) do
|
def icon(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<span class={[@name, @class]} {@rest} />
|
<span class={[@name, @class]} {@rest} />
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<button
|
<button
|
||||||
class={[
|
class={[
|
||||||
"text-center font-medium text-white",
|
"text-center font-medium text-white whitespace-nowrap",
|
||||||
"#{@rounding} inline-flex items-center justify-center px-8 py-4",
|
"#{@rounding} inline-flex items-center justify-center px-8 py-4",
|
||||||
"#{@color}",
|
"#{@color}",
|
||||||
"hover:bg-opacity-90 lg:px-8 xl:px-10",
|
"hover:bg-opacity-90 lg:px-8 xl:px-10",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
"""
|
"""
|
||||||
attr :rows, :list, required: true
|
attr :rows, :list, required: true
|
||||||
attr :table_class, :string, default: ""
|
attr :table_class, :string, default: ""
|
||||||
|
attr :sort_key, :string, default: nil
|
||||||
|
attr :sort_direction, :string, default: nil
|
||||||
|
|
||||||
attr :row_item, :any,
|
attr :row_item, :any,
|
||||||
default: &Function.identity/1,
|
default: &Function.identity/1,
|
||||||
@@ -24,6 +26,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
slot :col, required: true do
|
slot :col, required: true do
|
||||||
attr :label, :string
|
attr :label, :string
|
||||||
attr :class, :string
|
attr :class, :string
|
||||||
|
attr :sort_key, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
def table(assigns) do
|
def table(assigns) do
|
||||||
@@ -31,8 +34,20 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<table class={["w-full table-auto bg-boxdark", @table_class]}>
|
<table class={["w-full table-auto bg-boxdark", @table_class]}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left bg-meta-4">
|
<tr class="text-left bg-meta-4">
|
||||||
<th :for={col <- @col} class="px-4 py-4 font-medium text-white xl:pl-11">
|
<th
|
||||||
{col[:label]}
|
:for={col <- @col}
|
||||||
|
class={["px-4 py-4 font-medium text-white", col[:sort_key] && "cursor-pointer"]}
|
||||||
|
phx-click={col[:sort_key] && "sort_update"}
|
||||||
|
phx-value-sort_key={col[:sort_key]}
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
{col[:label]}
|
||||||
|
<.icon
|
||||||
|
:if={to_string(@sort_key) == col[:sort_key]}
|
||||||
|
name={if @sort_direction == :asc, do: "hero-chevron-up", else: "hero-chevron-down"}
|
||||||
|
class="w-3 h-3 mt-2 ml-1 absolute"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -41,7 +56,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<td
|
<td
|
||||||
:for={col <- @col}
|
:for={col <- @col}
|
||||||
class={[
|
class={[
|
||||||
"px-4 py-5 pl-9 xl:pl-11",
|
"px-4 py-5",
|
||||||
col[:class]
|
col[:class]
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -70,9 +85,9 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class={[
|
class={[
|
||||||
"flex h-8 w-8 items-center justify-center rounded",
|
"pagination-prev h-8 w-8 items-center justify-center rounded",
|
||||||
@page_number != 1 && "cursor-pointer hover:bg-primary hover:text-white",
|
@page_number != 1 && "cursor-pointer hover:bg-primary hover:text-white",
|
||||||
@page_number == 1 && "cursor-not-allowed"
|
@page_number <= 1 && "cursor-not-allowed"
|
||||||
]}
|
]}
|
||||||
phx-click={@page_number != 1 && "page_change"}
|
phx-click={@page_number != 1 && "page_change"}
|
||||||
phx-value-direction="dec"
|
phx-value-direction="dec"
|
||||||
@@ -88,9 +103,9 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class={[
|
class={[
|
||||||
"flex h-8 w-8 items-center justify-center rounded",
|
"pagination-next flex h-8 w-8 items-center justify-center rounded",
|
||||||
@page_number != @total_pages && "cursor-pointer hover:bg-primary hover:text-white",
|
@page_number != @total_pages && "cursor-pointer hover:bg-primary hover:text-white",
|
||||||
@page_number == @total_pages && "cursor-not-allowed"
|
@page_number >= @total_pages && "cursor-not-allowed"
|
||||||
]}
|
]}
|
||||||
phx-click={@page_number != @total_pages && "page_change"}
|
phx-click={@page_number != @total_pages && "page_change"}
|
||||||
phx-value-direction="inc"
|
phx-value-direction="inc"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.NumberUtils
|
||||||
alias PinchflatWeb.CoreComponents
|
alias PinchflatWeb.CoreComponents
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -125,4 +126,24 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
{@word}{if @count == 1, do: "", else: @suffix}
|
{@word}{if @count == 1, do: "", else: @suffix}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a human-readable byte size
|
||||||
|
"""
|
||||||
|
|
||||||
|
attr :byte_size, :integer, required: true
|
||||||
|
|
||||||
|
def readable_filesize(assigns) do
|
||||||
|
{num, suffix} = NumberUtils.human_byte_size(assigns.byte_size, precision: 2)
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
Map.merge(assigns, %{
|
||||||
|
num: num,
|
||||||
|
suffix: suffix
|
||||||
|
})
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<.localized_number number={@num} /> {@suffix}
|
||||||
|
"""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
attr :text, :string, required: true
|
attr :text, :string, required: true
|
||||||
attr :href, :any, required: true
|
attr :href, :any, required: true
|
||||||
attr :target, :any, default: "_self"
|
attr :target, :any, default: "_self"
|
||||||
|
attr :icon_class, :string, default: ""
|
||||||
|
|
||||||
def sidebar_item(assigns) do
|
def sidebar_item(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<li class="text-bodydark1">
|
<li class="text-bodydark1">
|
||||||
<.sidebar_link icon={@icon} text={@text} href={@href} target={@target} />
|
<.sidebar_link icon={@icon} text={@text} href={@href} target={@target} icon_class={@icon_class} />
|
||||||
</li>
|
</li>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -89,6 +90,7 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
attr :href, :any, required: true
|
attr :href, :any, required: true
|
||||||
attr :target, :any, default: "_self"
|
attr :target, :any, default: "_self"
|
||||||
attr :class, :string, default: ""
|
attr :class, :string, default: ""
|
||||||
|
attr :icon_class, :string, default: ""
|
||||||
|
|
||||||
def sidebar_link(assigns) do
|
def sidebar_link(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@@ -103,7 +105,7 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
@class
|
@class
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<.icon :if={@icon} name={@icon} /> {@text}
|
<.icon :if={@icon} name={@icon} class={@icon_class} /> {@text}
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
text="Docs"
|
text="Docs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/kieraneglin/pinchflat/wiki"
|
href="https://github.com/kieraneglin/pinchflat/wiki"
|
||||||
|
icon_class="scale-110"
|
||||||
/>
|
/>
|
||||||
<.sidebar_item icon="hero-cog" text="Github" target="_blank" href="https://github.com/kieraneglin/pinchflat" />
|
<.sidebar_item icon="si-github" text="Github" target="_blank" href="https://github.com/kieraneglin/pinchflat" />
|
||||||
|
<.sidebar_item icon="si-discord" text="Discord" target="_blank" href="https://discord.gg/j7T6dCuwU4" />
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class={[
|
class={[
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
]}
|
]}
|
||||||
phx-click={show_modal("donate-modal")}
|
phx-click={show_modal("donate-modal")}
|
||||||
>
|
>
|
||||||
<.icon name="hero-currency-dollar" /> Donate
|
<.icon name="hero-currency-dollar" class="scale-110" /> Donate
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center">
|
<div class="mb-6 flex gap-3 flex-row items-center">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "{StringUtils.truncate(@media_item.title, 35)}"
|
Editing "{@media_item.title}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center min-w-0">
|
||||||
<.link href={~p"/sources/#{@media_item.source_id}"}>
|
<.link href={~p"/sources/#{@media_item.source_id}"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
{StringUtils.truncate(@media_item.title, 35)}
|
{@media_item.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
def friendly_sponsorblock_options do
|
def friendly_sponsorblock_options do
|
||||||
[
|
[
|
||||||
{"Disabled (default)", "disabled"},
|
{"Disabled (default)", "disabled"},
|
||||||
|
{"Mark Segments as Chapters", "mark"},
|
||||||
{"Remove Segments", "remove"}
|
{"Remove Segments", "remove"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -56,7 +57,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def media_center_custom_output_template_options do
|
def media_center_custom_output_template_options do
|
||||||
%{
|
[
|
||||||
season_by_year__episode_by_date: "<code>Season YYYY/sYYYYeMMDD</code>",
|
season_by_year__episode_by_date: "<code>Season YYYY/sYYYYeMMDD</code>",
|
||||||
season_by_year__episode_by_date_and_index:
|
season_by_year__episode_by_date_and_index:
|
||||||
"same as the above but it handles dates better. <strong>This is the recommended option</strong>",
|
"same as the above but it handles dates better. <strong>This is the recommended option</strong>",
|
||||||
@@ -64,11 +65,11 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
"<code>Season 1/s01eXX</code> where <code>XX</code> is the video's position in the playlist. Only recommended for playlists (not channels) that don't change",
|
"<code>Season 1/s01eXX</code> where <code>XX</code> is the video's position in the playlist. Only recommended for playlists (not channels) that don't change",
|
||||||
static_season__episode_by_date:
|
static_season__episode_by_date:
|
||||||
"<code>Season 1/s01eYYMMDD</code>. Recommended for playlists that might change or where order isn't important"
|
"<code>Season 1/s01eYYMMDD</code>. Recommended for playlists that might change or where order isn't important"
|
||||||
}
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def other_custom_output_template_options do
|
def other_custom_output_template_options do
|
||||||
%{
|
[
|
||||||
upload_day: nil,
|
upload_day: nil,
|
||||||
upload_month: nil,
|
upload_month: nil,
|
||||||
upload_year: nil,
|
upload_year: nil,
|
||||||
@@ -84,8 +85,11 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
season_episode_index_from_date:
|
season_episode_index_from_date:
|
||||||
"the upload date formatted as <code>sYYYYeMMDDII</code> where <code>II</code> is an index to prevent date collisions",
|
"the upload date formatted as <code>sYYYYeMMDDII</code> where <code>II</code> is an index to prevent date collisions",
|
||||||
media_playlist_index:
|
media_playlist_index:
|
||||||
"the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated"
|
"the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated",
|
||||||
}
|
media_item_id: "the ID of the media item in Pinchflat's database",
|
||||||
|
source_id: "the ID of the source in Pinchflat's database",
|
||||||
|
media_profile_id: "the ID of the media profile in Pinchflat's database"
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def common_output_template_options do
|
def common_output_template_options do
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<.link href={~p"/media_profiles"}>
|
<.link href={~p"/media_profiles"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "{@media_profile.name}"
|
Editing "{@media_profile.name}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<div class="flex flex-col gap-10 min-w-max">
|
<div class="flex flex-col gap-10 min-w-max">
|
||||||
<.table rows={@media_profiles} table_class="text-black dark:text-white">
|
<.table rows={@media_profiles} table_class="text-black dark:text-white">
|
||||||
<:col :let={media_profile} label="Name">
|
<:col :let={media_profile} label="Name" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/media_profiles/#{media_profile.id}"}>
|
<.subtle_link href={~p"/media_profiles/#{media_profile.id}"}>
|
||||||
{media_profile.name}
|
{media_profile.name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center min-w-0">
|
||||||
<.link href={~p"/media_profiles"}>
|
<.link href={~p"/media_profiles"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-2">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-2 truncate">
|
||||||
{@media_profile.name}
|
{@media_profile.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
</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">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item}"}>
|
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item}"}>
|
||||||
{StringUtils.truncate(media_item.title, 35)}
|
{media_item.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
<:col :let={media_item} label="Upload Date">
|
||||||
@@ -42,9 +42,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
<:col :let={media_item} label="Downloaded At">
|
<:col :let={media_item} label="Downloaded At">
|
||||||
{format_datetime(media_item.media_downloaded_at)}
|
{format_datetime(media_item.media_downloaded_at)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Source">
|
<:col :let={media_item} label="Source" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{media_item.source_id}"}>
|
<.subtle_link href={~p"/sources/#{media_item.source_id}"}>
|
||||||
{StringUtils.truncate(media_item.source.custom_name, 35)}
|
{media_item.source.custom_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.table>
|
||||||
@@ -56,9 +56,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, session, socket) do
|
||||||
page = 1
|
page = 1
|
||||||
base_query = generate_base_query()
|
base_query = generate_base_query(session["media_state"])
|
||||||
pagination_attrs = fetch_pagination_attributes(base_query, page)
|
pagination_attrs = fetch_pagination_attributes(base_query, page)
|
||||||
|
|
||||||
{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query}))}
|
{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query}))}
|
||||||
@@ -97,10 +97,17 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
|> Repo.preload(:source)
|
|> Repo.preload(:source)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_base_query do
|
defp generate_base_query("pending") do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> MediaQuery.require_assoc(:media_profile)
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|> where(^dynamic(^MediaQuery.downloaded() or ^MediaQuery.pending()))
|
|> where(^dynamic(^MediaQuery.pending()))
|
||||||
|
|> order_by(desc: :id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp generate_base_query("downloaded") do
|
||||||
|
MediaQuery.new()
|
||||||
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|
|> where(^dynamic(^MediaQuery.downloaded()))
|
||||||
|> order_by(desc: :id)
|
|> order_by(desc: :id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<span class="flex flex-col items-center py-2">
|
<span class="flex flex-col items-center py-2">
|
||||||
<span class="text-md font-medium">Library Size</span>
|
<span class="text-md font-medium">Library Size</span>
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<.readable_media_filesize media_filesize={@media_item_size} />
|
<.readable_filesize byte_size={@media_item_size} />
|
||||||
</h4>
|
</h4>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,9 +41,22 @@
|
|||||||
|
|
||||||
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
||||||
<span class="text-2xl font-medium mb-4">Media History</span>
|
<span class="text-2xl font-medium mb-4">Media History</span>
|
||||||
<section class="mt-6">
|
<.tabbed_layout>
|
||||||
{live_render(@conn, Pinchflat.Pages.HistoryTableLive)}
|
<:tab title="Downloaded" id="downloaded">
|
||||||
</section>
|
{live_render(
|
||||||
|
@conn,
|
||||||
|
Pinchflat.Pages.HistoryTableLive,
|
||||||
|
session: %{"media_state" => "downloaded"}
|
||||||
|
)}
|
||||||
|
</:tab>
|
||||||
|
<:tab title="Pending" id="pending">
|
||||||
|
{live_render(
|
||||||
|
@conn,
|
||||||
|
Pinchflat.Pages.HistoryTableLive,
|
||||||
|
session: %{"media_state" => "pending"}
|
||||||
|
)}
|
||||||
|
</:tab>
|
||||||
|
</.tabbed_layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ defmodule Pinchflat.Pages.JobTableLive do
|
|||||||
<:col :let={task} label="Task">
|
<:col :let={task} label="Task">
|
||||||
{worker_to_task_name(task.job.worker)}
|
{worker_to_task_name(task.job.worker)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Subject">
|
<:col :let={task} label="Subject" class="truncate max-w-xs">
|
||||||
<.subtle_link href={task_to_link(task)}>
|
<.subtle_link href={task_to_link(task)}>
|
||||||
{StringUtils.truncate(task_to_record_name(task), 35)}
|
{task_to_record_name(task)}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Attempt No.">
|
<:col :let={task} label="Attempt No.">
|
||||||
|
|||||||
@@ -6,8 +6,19 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
|||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Podcasts.RssFeedBuilder
|
alias Pinchflat.Podcasts.RssFeedBuilder
|
||||||
|
alias Pinchflat.Podcasts.OpmlFeedBuilder
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
|
def opml_feed(conn, _params) do
|
||||||
|
url_base = url(conn, ~p"/")
|
||||||
|
xml = OpmlFeedBuilder.build(url_base, PodcastHelpers.opml_sources())
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/opml+xml")
|
||||||
|
|> put_resp_header("content-disposition", "inline")
|
||||||
|
|> send_resp(200, xml)
|
||||||
|
end
|
||||||
|
|
||||||
def rss_feed(conn, %{"uuid" => uuid}) do
|
def rss_feed(conn, %{"uuid" => uuid}) do
|
||||||
source = Repo.get_by!(Source, uuid: uuid)
|
source = Repo.get_by!(Source, uuid: uuid)
|
||||||
url_base = url(conn, ~p"/")
|
url_base = url(conn, ~p"/")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white truncate">
|
||||||
Results for "{StringUtils.truncate(@search_term, 50)}"
|
Results for "{@search_term}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
<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">
|
||||||
<:col :let={result} label="Title">
|
<:col :let={result} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{result.source_id}/media/#{result.id}"}>
|
<.subtle_link href={~p"/sources/#{result.source_id}/media/#{result.id}"}>
|
||||||
{StringUtils.truncate(result.title, 35)}
|
{result.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={result} label="Excerpt">
|
<:col :let={result} label="Excerpt">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-2xl text-black dark:text-white">
|
<h3 class="text-2xl text-black dark:text-white">
|
||||||
Indexing Settings
|
Extractor Settings
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
@@ -41,6 +41,14 @@
|
|||||||
html_help={true}
|
html_help={true}
|
||||||
inputclass="font-mono text-sm mr-4"
|
inputclass="font-mono text-sm mr-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<.input
|
||||||
|
field={f[:extractor_sleep_interval_seconds]}
|
||||||
|
placeholder="0"
|
||||||
|
type="number"
|
||||||
|
label="Sleep Interval (seconds)"
|
||||||
|
help="Sleep interval in seconds between each extractor request. Must be a positive whole number (or set to 0 to disable)"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,15 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rss_feed_url(conn, source) do
|
def rss_feed_url(conn, source) do
|
||||||
|
# NOTE: The reason for this concatenation is to avoid what appears to be a bug in Phoenix
|
||||||
|
# See: https://github.com/phoenixframework/phoenix/issues/6033
|
||||||
url(conn, ~p"/sources/#{source.uuid}/feed") <> ".xml"
|
url(conn, ~p"/sources/#{source.uuid}/feed") <> ".xml"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def opml_feed_url(conn) do
|
||||||
|
url(conn, ~p"/sources/opml.xml?#{[route_token: Settings.get!(:route_token)]}")
|
||||||
|
end
|
||||||
|
|
||||||
def output_path_template_override_placeholders(media_profiles) do
|
def output_path_template_override_placeholders(media_profiles) do
|
||||||
media_profiles
|
media_profiles
|
||||||
|> Enum.map(&{&1.id, &1.output_path_template})
|
|> Enum.map(&{&1.id, &1.output_path_template})
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<.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>
|
<:option>
|
||||||
<span
|
<.link href={rss_feed_url(@conn, @source)} x-data="{ copied: false }" x-on:click={~s"
|
||||||
x-data="{ copied: false }"
|
$event.preventDefault();
|
||||||
x-on:click={"
|
copyWithCallbacks(
|
||||||
copyWithCallbacks(
|
'#{rss_feed_url(@conn, @source)}',
|
||||||
'#{rss_feed_url(@conn, @source)}',
|
() => copied = true,
|
||||||
() => copied = true,
|
() => copied = false
|
||||||
() => 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>
|
</.link>
|
||||||
</:option>
|
</:option>
|
||||||
<:option>
|
<:option>
|
||||||
<span x-data="{ copied: false }" x-on:click={~s"
|
<span x-data="{ copied: false }" x-on:click={~s"
|
||||||
@@ -56,7 +54,7 @@
|
|||||||
<.link
|
<.link
|
||||||
href={~p"/sources/#{@source}/force_index"}
|
href={~p"/sources/#{@source}/force_index"}
|
||||||
method="post"
|
method="post"
|
||||||
data-confirm="Are you sure you want to force an index of this source? This isn't normally needed."
|
data-confirm="Are you sure you want index all content from this source? This isn't normally needed."
|
||||||
>
|
>
|
||||||
Force Index
|
Force Index
|
||||||
</.link>
|
</.link>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<.link href={~p"/sources"}>
|
<.link href={~p"/sources"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "{@source.custom_name}"
|
Editing "{@source.custom_name}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white">Sources</h2>
|
<h2 class="text-title-md2 font-bold text-black dark:text-white">Sources</h2>
|
||||||
<nav>
|
<nav class="flex items-center justify-between gap-6">
|
||||||
|
<.link href={opml_feed_url(@conn)} x-data="{ copied: false }" x-on:click={~s"
|
||||||
|
$event.preventDefault();
|
||||||
|
copyWithCallbacks(
|
||||||
|
'#{opml_feed_url(@conn)}',
|
||||||
|
() => copied = true,
|
||||||
|
() => copied = false
|
||||||
|
)
|
||||||
|
"}>
|
||||||
|
Copy OPML <span class="hidden sm:inline">Feed</span>
|
||||||
|
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
||||||
|
</.link>
|
||||||
<.link href={~p"/sources/new"}>
|
<.link href={~p"/sources/new"}>
|
||||||
<.button color="bg-primary" rounding="rounded-lg">
|
<.button color="bg-primary" rounding="rounded-lg">
|
||||||
<span class="font-bold mx-2">+</span> New <span class="hidden sm:inline pl-1">Source</span>
|
<span class="font-bold mx-2">+</span> New <span class="hidden sm:inline pl-1">Source</span>
|
||||||
@@ -11,8 +22,12 @@
|
|||||||
|
|
||||||
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<div class="flex flex-col gap-10 min-w-max">
|
{live_render(@conn, PinchflatWeb.Sources.SourceLive.IndexTableLive,
|
||||||
{live_render(@conn, PinchflatWeb.Sources.IndexTableLive)}
|
session: %{
|
||||||
</div>
|
"initial_sort_key" => :custom_name,
|
||||||
|
"initial_sort_direction" => :asc,
|
||||||
|
"results_per_page" => 10
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ defmodule PinchflatWeb.Sources.IndexTableLive do
|
|||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.table rows={@sources} table_class="text-white">
|
<.table rows={@sources} table_class="text-white">
|
||||||
<:col :let={source} label="Name">
|
<:col :let={source} label="Name" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{source.id}"}>
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
{StringUtils.truncate(source.custom_name || source.collection_name, 35)}
|
{source.custom_name || source.collection_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Pending">
|
<:col :let={source} label="Pending">
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ defmodule PinchflatWeb.Sources.MediaItemTableLive do
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
||||||
{StringUtils.truncate(media_item.title, 50)}
|
{media_item.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center min-w-0">
|
||||||
<.link href={~p"/sources"}>
|
<.link href={~p"/sources"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
{@source.custom_name}
|
{@source.custom_name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.IndexTableLive do
|
||||||
|
use PinchflatWeb, :live_view
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
|
import PinchflatWeb.Helpers.SortingHelpers
|
||||||
|
import PinchflatWeb.Helpers.PaginationHelpers
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
|
||||||
|
def mount(_params, session, socket) do
|
||||||
|
limit = session["results_per_page"]
|
||||||
|
|
||||||
|
initial_params =
|
||||||
|
Map.merge(
|
||||||
|
%{
|
||||||
|
sort_key: session["initial_sort_key"],
|
||||||
|
sort_direction: session["initial_sort_direction"]
|
||||||
|
},
|
||||||
|
get_pagination_attributes(sources_query(), 1, limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(initial_params)
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("page_change", %{"direction" => direction}, %{assigns: assigns} = socket) do
|
||||||
|
new_page = update_page_number(assigns.page, direction, assigns.total_pages)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(get_pagination_attributes(sources_query(), new_page, assigns.limit))
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:noreply, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("sort_update", %{"sort_key" => sort_key}, %{assigns: assigns} = socket) do
|
||||||
|
new_sort_key = String.to_existing_atom(sort_key)
|
||||||
|
|
||||||
|
new_params = %{
|
||||||
|
sort_key: new_sort_key,
|
||||||
|
sort_direction: get_sort_direction(assigns.sort_key, new_sort_key, assigns.sort_direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(new_params)
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:noreply, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sort_attr(:pending_count), do: dynamic([s, mp, dl, pe], pe.pending_count)
|
||||||
|
defp sort_attr(:downloaded_count), do: dynamic([s, mp, dl], dl.downloaded_count)
|
||||||
|
defp sort_attr(:media_size_bytes), do: dynamic([s, mp, dl], dl.media_size_bytes)
|
||||||
|
defp sort_attr(:media_profile_name), do: dynamic([s, mp], mp.name)
|
||||||
|
defp sort_attr(:custom_name), do: dynamic([s], s.custom_name)
|
||||||
|
defp sort_attr(:enabled), do: dynamic([s], s.enabled)
|
||||||
|
|
||||||
|
defp set_sources(%{assigns: assigns} = socket) do
|
||||||
|
sources =
|
||||||
|
sources_query()
|
||||||
|
|> order_by(^[{assigns.sort_direction, sort_attr(assigns.sort_key)}, asc: :id])
|
||||||
|
|> limit(^assigns.limit)
|
||||||
|
|> offset(^assigns.offset)
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
assign(socket, %{sources: sources})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sources_query do
|
||||||
|
downloaded_subquery =
|
||||||
|
from(
|
||||||
|
m in MediaItem,
|
||||||
|
select: %{downloaded_count: count(m.id), source_id: m.source_id, media_size_bytes: sum(m.media_size_bytes)},
|
||||||
|
where: ^MediaQuery.downloaded(),
|
||||||
|
group_by: m.source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
pending_subquery =
|
||||||
|
from(
|
||||||
|
m in MediaItem,
|
||||||
|
inner_join: s in assoc(m, :source),
|
||||||
|
inner_join: mp in assoc(s, :media_profile),
|
||||||
|
select: %{pending_count: count(m.id), source_id: m.source_id},
|
||||||
|
where: ^MediaQuery.pending(),
|
||||||
|
group_by: m.source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
from s in Source,
|
||||||
|
as: :source,
|
||||||
|
inner_join: mp in assoc(s, :media_profile),
|
||||||
|
left_join: d in subquery(downloaded_subquery),
|
||||||
|
on: d.source_id == s.id,
|
||||||
|
left_join: p in subquery(pending_subquery),
|
||||||
|
on: p.source_id == s.id,
|
||||||
|
on: d.source_id == s.id,
|
||||||
|
where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at),
|
||||||
|
preload: [media_profile: mp],
|
||||||
|
select: map(s, ^Source.__schema__(:fields)),
|
||||||
|
select_merge: %{
|
||||||
|
downloaded_count: coalesce(d.downloaded_count, 0),
|
||||||
|
pending_count: coalesce(p.pending_count, 0),
|
||||||
|
media_size_bytes: coalesce(d.media_size_bytes, 0)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<div class="flex flex-col min-w-max">
|
||||||
|
<.table rows={@sources} table_class="text-white" sort_key={@sort_key} sort_direction={@sort_direction}>
|
||||||
|
<:col :let={source} label="Name" sort_key="custom_name" class="truncate max-w-xs">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
|
{source.custom_name}
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Pending" sort_key="pending_count">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-pending"}>
|
||||||
|
<.localized_number number={source.pending_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Downloaded" sort_key="downloaded_count">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-downloaded"}>
|
||||||
|
<.localized_number number={source.downloaded_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Size" sort_key="media_size_bytes">
|
||||||
|
<.readable_filesize byte_size={source.media_size_bytes} />
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Media Profile" sort_key="media_profile_name" class="truncate max-w-xs">
|
||||||
|
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
||||||
|
{source.media_profile.name}
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Enabled?" sort_key="enabled">
|
||||||
|
<.live_component
|
||||||
|
module={PinchflatWeb.Sources.SourceLive.SourceEnableToggle}
|
||||||
|
source={source}
|
||||||
|
id={"source_#{source.id}_enabled"}
|
||||||
|
/>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="" class="flex place-content-evenly">
|
||||||
|
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
|
||||||
|
</:col>
|
||||||
|
</.table>
|
||||||
|
|
||||||
|
<section class="flex justify-center my-5">
|
||||||
|
<.live_pagination_controls page_number={@page} total_pages={@total_pages} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggle do
|
||||||
|
use PinchflatWeb, :live_component
|
||||||
|
|
||||||
|
alias Pinchflat.Sources
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.form :let={f} for={@form} phx-change="update" phx-target={@myself} class="enabled_toggle_form">
|
||||||
|
<.input id={"source_#{@source_id}_enabled_input"} field={f[:enabled]} type="toggle" />
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(assigns, socket) do
|
||||||
|
initial_data = %{
|
||||||
|
source_id: assigns.source.id,
|
||||||
|
form: Sources.change_source(%Source{}, assigns.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(initial_data)
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("update", %{"source" => source_params}, %{assigns: assigns} = socket) do
|
||||||
|
assigns.source_id
|
||||||
|
|> Sources.get_source!()
|
||||||
|
|> Sources.update_source(source_params)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -32,6 +32,8 @@ defmodule PinchflatWeb.Endpoint do
|
|||||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :pinchflat
|
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :pinchflat
|
||||||
end
|
end
|
||||||
|
|
||||||
|
plug PromEx.Plug, prom_ex_module: Pinchflat.PromEx
|
||||||
|
|
||||||
plug Phoenix.LiveDashboard.RequestLogger,
|
plug Phoenix.LiveDashboard.RequestLogger,
|
||||||
param_key: "request_logger",
|
param_key: "request_logger",
|
||||||
cookie_key: "request_logger"
|
cookie_key: "request_logger"
|
||||||
@@ -73,6 +75,13 @@ defmodule PinchflatWeb.Endpoint do
|
|||||||
Phoenix.Controller.put_router_url(conn, new_base_url)
|
Phoenix.Controller.put_router_url(conn, new_base_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Some podcast clients require file extensions, and others still will _add_
|
||||||
|
# file extensions to XML files if they don't have them. This plug removes
|
||||||
|
# the extension from the path so that the correct route is matched, regardless
|
||||||
|
# of the provided extension.
|
||||||
|
#
|
||||||
|
# This has the downside of in-app generated verified routes not working with
|
||||||
|
# extensions so this behaviour may change in the future.
|
||||||
defp strip_trailing_extension(%{path_info: []} = conn, _opts), do: conn
|
defp strip_trailing_extension(%{path_info: []} = conn, _opts), do: conn
|
||||||
|
|
||||||
defp strip_trailing_extension(conn, _opts) do
|
defp strip_trailing_extension(conn, _opts) do
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.PaginationHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for working with pagination, usually in the context of LiveViews or LiveComponents.
|
||||||
|
|
||||||
|
These methods are fairly simple, but they're commonly repeated across different Live entities
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Utils.NumberUtils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a query, a page number, and a number of records per page, returns a map of pagination attributes.
|
||||||
|
|
||||||
|
Returns map()
|
||||||
|
"""
|
||||||
|
def get_pagination_attributes(query, page, records_per_page) do
|
||||||
|
total_record_count = Repo.aggregate(query, :count, :id)
|
||||||
|
total_pages = max(ceil(total_record_count / records_per_page), 1)
|
||||||
|
clamped_page = NumberUtils.clamp(page, 1, total_pages)
|
||||||
|
|
||||||
|
%{
|
||||||
|
page: clamped_page,
|
||||||
|
total_pages: total_pages,
|
||||||
|
total_record_count: total_record_count,
|
||||||
|
limit: records_per_page,
|
||||||
|
offset: (clamped_page - 1) * records_per_page
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a current page number, a direction to move in, and the total number of pages, returns the updated page number.
|
||||||
|
The updated page number is clamped to the range [1, total_pages].
|
||||||
|
|
||||||
|
Returns integer()
|
||||||
|
"""
|
||||||
|
def update_page_number(current_page, direction, total_pages) do
|
||||||
|
updated_page =
|
||||||
|
case to_string(direction) do
|
||||||
|
"inc" -> current_page + 1
|
||||||
|
"dec" -> current_page - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
NumberUtils.clamp(updated_page, 1, total_pages)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.SortingHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for working with sorting, usually in the context of LiveViews or LiveComponents.
|
||||||
|
|
||||||
|
These methods are fairly simple, but they're commonly repeated across different Live entities
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given the old sort attribute, the new sort attribute, and the old sort direction, returns the new sort direction.
|
||||||
|
|
||||||
|
Returns :asc | :desc
|
||||||
|
"""
|
||||||
|
def get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) do
|
||||||
|
case {new_sort_attr, old_sort_direction} do
|
||||||
|
{^old_sort_attr, :desc} -> :asc
|
||||||
|
{^old_sort_attr, _} -> :desc
|
||||||
|
_ -> :asc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
defmodule PinchflatWeb.Plugs do
|
||||||
|
@moduledoc """
|
||||||
|
Custom plugs for PinchflatWeb.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use PinchflatWeb, :router
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `expose_feed_endpoints` setting is true, this plug does nothing. Otherwise, it calls `basic_auth/2`.
|
||||||
|
"""
|
||||||
|
def maybe_basic_auth(conn, opts) do
|
||||||
|
if Application.get_env(:pinchflat, :expose_feed_endpoints) do
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
basic_auth(conn, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `basic_auth_username` and `basic_auth_password` settings are set, this plug calls `Plug.BasicAuth.basic_auth/3`.
|
||||||
|
"""
|
||||||
|
def basic_auth(conn, _opts) do
|
||||||
|
username = Application.get_env(:pinchflat, :basic_auth_username)
|
||||||
|
password = Application.get_env(:pinchflat, :basic_auth_password)
|
||||||
|
|
||||||
|
if credential_set?(username) && credential_set?(password) do
|
||||||
|
Plug.BasicAuth.basic_auth(conn, username: username, password: password, realm: "Pinchflat")
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Removes the `x-frame-options` header from the response to allow the page to be embedded in an iframe.
|
||||||
|
"""
|
||||||
|
def allow_iframe_embed(conn, _opts) do
|
||||||
|
delete_resp_header(conn, "x-frame-options")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `route_token` query parameter matches the `route_token` setting, this plug does nothing.
|
||||||
|
Otherwise, it sends a 401 response.
|
||||||
|
"""
|
||||||
|
def token_protected_route(%{query_params: %{"route_token" => route_token}} = conn, _opts) do
|
||||||
|
if Settings.get!(:route_token) == route_token do
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
send_unauthorized(conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def token_protected_route(conn, _opts) do
|
||||||
|
send_unauthorized(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp credential_set?(credential) do
|
||||||
|
credential && credential != ""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_unauthorized(conn) do
|
||||||
|
conn
|
||||||
|
|> send_resp(:unauthorized, "Unauthorized")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
+18
-41
@@ -1,5 +1,6 @@
|
|||||||
defmodule PinchflatWeb.Router do
|
defmodule PinchflatWeb.Router do
|
||||||
use PinchflatWeb, :router
|
use PinchflatWeb, :router
|
||||||
|
import PinchflatWeb.Plugs
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
||||||
@@ -19,8 +20,23 @@ defmodule PinchflatWeb.Router do
|
|||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :feeds do
|
scope "/", PinchflatWeb do
|
||||||
plug :maybe_basic_auth
|
pipe_through [:maybe_basic_auth, :token_protected_route]
|
||||||
|
|
||||||
|
# has to match before /sources/:id
|
||||||
|
get "/sources/opml", Podcasts.PodcastController, :opml_feed
|
||||||
|
end
|
||||||
|
|
||||||
|
# Routes in here _may not be_ protected by basic auth. This is necessary for
|
||||||
|
# media streaming to work for RSS podcast feeds.
|
||||||
|
scope "/", PinchflatWeb do
|
||||||
|
pipe_through :maybe_basic_auth
|
||||||
|
|
||||||
|
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
||||||
|
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
||||||
|
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
|
||||||
|
|
||||||
|
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", PinchflatWeb do
|
scope "/", PinchflatWeb do
|
||||||
@@ -48,18 +64,6 @@ defmodule PinchflatWeb.Router do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Routes in here _may not be_ protected by basic auth. This is necessary for
|
|
||||||
# media streaming to work for RSS podcast feeds.
|
|
||||||
scope "/", PinchflatWeb do
|
|
||||||
pipe_through :feeds
|
|
||||||
|
|
||||||
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
|
||||||
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
|
||||||
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
|
|
||||||
|
|
||||||
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
|
||||||
end
|
|
||||||
|
|
||||||
# No auth or CSRF protection for the health check endpoint
|
# No auth or CSRF protection for the health check endpoint
|
||||||
scope "/", PinchflatWeb do
|
scope "/", PinchflatWeb do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
@@ -74,31 +78,4 @@ defmodule PinchflatWeb.Router do
|
|||||||
metrics: PinchflatWeb.Telemetry,
|
metrics: PinchflatWeb.Telemetry,
|
||||||
ecto_repos: [Pinchflat.Repo]
|
ecto_repos: [Pinchflat.Repo]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_basic_auth(conn, opts) do
|
|
||||||
if Application.get_env(:pinchflat, :expose_feed_endpoints) do
|
|
||||||
conn
|
|
||||||
else
|
|
||||||
basic_auth(conn, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp basic_auth(conn, _opts) do
|
|
||||||
username = Application.get_env(:pinchflat, :basic_auth_username)
|
|
||||||
password = Application.get_env(:pinchflat, :basic_auth_password)
|
|
||||||
|
|
||||||
if credential_set?(username) && credential_set?(password) do
|
|
||||||
Plug.BasicAuth.basic_auth(conn, username: username, password: password, realm: "Pinchflat")
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp credential_set?(credential) do
|
|
||||||
credential && credential != ""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp allow_iframe_embed(conn, _opts) do
|
|
||||||
delete_resp_header(conn, "x-frame-options")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2024.12.10",
|
version: "2025.1.17",
|
||||||
elixir: "~> 1.17",
|
elixir: "~> 1.17",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
@@ -60,9 +60,9 @@ defmodule Pinchflat.MixProject do
|
|||||||
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
||||||
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
|
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
|
||||||
{:swoosh, "~> 1.3"},
|
{:swoosh, "~> 1.3"},
|
||||||
{:finch, "~> 0.13"},
|
{:finch, "~> 0.18"},
|
||||||
{:telemetry_metrics, "~> 0.6"},
|
{:telemetry_metrics, "~> 1.0"},
|
||||||
{:telemetry_poller, "~> 1.0"},
|
{:telemetry_poller, "~> 1.1"},
|
||||||
{:gettext, "~> 0.20"},
|
{:gettext, "~> 0.20"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.1.1"},
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
@@ -70,6 +70,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
{:oban, "~> 2.17"},
|
{:oban, "~> 2.17"},
|
||||||
{:nimble_parsec, "~> 1.4"},
|
{:nimble_parsec, "~> 1.4"},
|
||||||
{:timex, "~> 3.0"},
|
{:timex, "~> 3.0"},
|
||||||
|
{:prom_ex, "~> 1.11.0"},
|
||||||
{:mox, "~> 1.0", only: :test},
|
{:mox, "~> 1.0", only: :test},
|
||||||
{:credo, "~> 1.7.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7.7", only: [:dev, :test], runtime: false},
|
||||||
{:credo_naming, "~> 2.1", only: [:dev, :test], runtime: false},
|
{:credo_naming, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
%{
|
%{
|
||||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
|
"castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
@@ -23,23 +23,25 @@
|
|||||||
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
|
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
|
||||||
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||||
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
|
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||||
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
|
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
|
||||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
|
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
|
||||||
|
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||||
|
"peep": {:hex, :peep, "3.4.1", "0e5263710fa0b42675bd0a11fdcdd3ee4f484e319105b6ad9a576c91a5d3cb55", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "7a9b8c1f17b8b9475efb27b7048afa4d89ab84ef33a3d1df13696c85c12cd632"},
|
||||||
"phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"},
|
"phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||||
@@ -51,6 +53,7 @@
|
|||||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||||
|
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
|
||||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||||
@@ -58,8 +61,9 @@
|
|||||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
||||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||||
|
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||||
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||||
|
|||||||
@@ -0,0 +1,607 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "#73BF69",
|
||||||
|
"limit": 100,
|
||||||
|
"name": "PromEx service start",
|
||||||
|
"showIn": 0,
|
||||||
|
"tags": ["prom_ex", "pinchflat", "start"],
|
||||||
|
"type": "tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "#FF9830",
|
||||||
|
"limit": 100,
|
||||||
|
"name": "PromEx service stop",
|
||||||
|
"showIn": 0,
|
||||||
|
"tags": ["prom_ex", "pinchflat", "stop"],
|
||||||
|
"type": "tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "All the data that is presented here is captured by the PromEx Application plugin (https://github.com/akoutmos/prom_ex/blob/master/lib/prom_ex/plugins/application.ex)",
|
||||||
|
"editable": false,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 1,
|
||||||
|
"id": null,
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"asDropdown": false,
|
||||||
|
"icon": "bolt",
|
||||||
|
"includeVars": false,
|
||||||
|
"keepTime": false,
|
||||||
|
"tags": [],
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Sponsor PromEx",
|
||||||
|
"tooltip": "",
|
||||||
|
"type": "link",
|
||||||
|
"url": "https://github.com/sponsors/akoutmos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asDropdown": false,
|
||||||
|
"icon": "doc",
|
||||||
|
"includeVars": false,
|
||||||
|
"keepTime": false,
|
||||||
|
"tags": [],
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Application Plugin Docs",
|
||||||
|
"tooltip": "",
|
||||||
|
"type": "link",
|
||||||
|
"url": "https://hexdocs.pm/prom_ex/PromEx.Plugins.Application.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The amount of time that the application has been running.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"decimals": 1,
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "dtdurationms"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_uptime_milliseconds_count{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Uptime",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The data is populated by the PromEx Application plugin and provides information regarding your application's dependencies.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Status"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.displayMode",
|
||||||
|
"value": "color-background"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mappings",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"from": "",
|
||||||
|
"id": 0,
|
||||||
|
"text": "Started",
|
||||||
|
"to": "",
|
||||||
|
"type": 1,
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "",
|
||||||
|
"id": 1,
|
||||||
|
"text": "Loaded",
|
||||||
|
"to": "",
|
||||||
|
"type": 1,
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.align",
|
||||||
|
"value": "center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 202
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Name"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 349
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Version"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 187
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 36,
|
||||||
|
"w": 16,
|
||||||
|
"x": 8,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": [
|
||||||
|
{
|
||||||
|
"desc": false,
|
||||||
|
"displayName": "Name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_dependency_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Dependency Information",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {
|
||||||
|
"Time": true,
|
||||||
|
"__name__": true,
|
||||||
|
"instance": true,
|
||||||
|
"job": true,
|
||||||
|
"Value": true
|
||||||
|
},
|
||||||
|
"indexByName": {
|
||||||
|
"Time": 0,
|
||||||
|
"Value": 4,
|
||||||
|
"__name__": 1,
|
||||||
|
"instance": 2,
|
||||||
|
"job": 3,
|
||||||
|
"modules": 7,
|
||||||
|
"name": 5,
|
||||||
|
"version": 6
|
||||||
|
},
|
||||||
|
"renameByName": {
|
||||||
|
"Value": "Status",
|
||||||
|
"modules": "Number of Modules Loaded",
|
||||||
|
"name": "Name",
|
||||||
|
"version": "Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The name of the primary application that is running.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 6
|
||||||
|
},
|
||||||
|
"id": 11,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "/^name$/",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_primary_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Name",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The Git SHA of the application.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "/^sha$/",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_git_sha_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Git SHA",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The author of the application's last Git commit.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 12,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "/^author$/",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_git_author_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Git Author",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The version of the primary application that is running.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 24
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "/^version$/",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_primary_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Version",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"description": "The number of modules loaded by the primary application that is running.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["last"],
|
||||||
|
"fields": "/^modules$/",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.1.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "pinchflat_prom_ex_application_primary_info{job=\"$job\", instance=\"$instance\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Application Modules Loaded",
|
||||||
|
"type": "stat"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "5s",
|
||||||
|
"schemaVersion": 26,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["PromEx", "Application", "pinchflat"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"definition": "label_values(pinchflat_prom_ex_prom_ex_status_info, job)",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Prometheus Job",
|
||||||
|
"multi": false,
|
||||||
|
"name": "job",
|
||||||
|
"options": [],
|
||||||
|
"query": "label_values(pinchflat_prom_ex_prom_ex_status_info, job)",
|
||||||
|
"refresh": 2,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 6,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"definition": "label_values(pinchflat_prom_ex_prom_ex_status_info, instance)",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Application Instance",
|
||||||
|
"multi": false,
|
||||||
|
"name": "instance",
|
||||||
|
"options": [],
|
||||||
|
"query": "label_values(pinchflat_prom_ex_prom_ex_status_info{job=\"$job\"}, instance)",
|
||||||
|
"refresh": 2,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 0,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m"]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Pinchflat - PromEx Application Dashboard",
|
||||||
|
"uid": "7DBBC471C5775585391E8F24D1E62319",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 443 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddRouteTokenToSettings do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:settings) do
|
||||||
|
add :route_token, :string, null: false, default: "tmp-token"
|
||||||
|
end
|
||||||
|
|
||||||
|
execute "UPDATE settings SET route_token = gen_random_uuid();", "SELECT 1;"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddExtractorSleepIntervalToSettings do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:settings) do
|
||||||
|
add :extractor_sleep_interval_seconds, :number, null: false, default: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -287,7 +287,19 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
assert {:sponsorblock_remove, "sponsor,intro"} in res
|
assert {:sponsorblock_remove, "sponsor,intro"} in res
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not include :sponsorblock_remove option without categories", %{media_item: media_item} do
|
test "includes :sponsorblock_mark option when specified", %{media_item: media_item} do
|
||||||
|
media_item =
|
||||||
|
update_media_profile_attribute(media_item, %{
|
||||||
|
sponsorblock_behaviour: :mark,
|
||||||
|
sponsorblock_categories: ["sponsor", "intro"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
|
assert {:sponsorblock_mark, "sponsor,intro"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include any sponsorblock option without categories", %{media_item: media_item} do
|
||||||
media_item =
|
media_item =
|
||||||
update_media_profile_attribute(media_item, %{
|
update_media_profile_attribute(media_item, %{
|
||||||
sponsorblock_behaviour: :remove,
|
sponsorblock_behaviour: :remove,
|
||||||
@@ -296,9 +308,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
refute Keyword.has_key?(res, :sponsorblock_remove)
|
||||||
refute {:sponsorblock_remove, []} in res
|
refute Keyword.has_key?(res, :sponsorblock_mark)
|
||||||
refute :sponsorblock_remove in res
|
refute :sponsorblock_remove in res
|
||||||
|
refute :sponsorblock_mark in res
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not include any sponsorblock options when disabled", %{media_item: media_item} do
|
test "does not include any sponsorblock options when disabled", %{media_item: media_item} do
|
||||||
@@ -307,9 +320,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
refute Keyword.has_key?(res, :sponsorblock_remove)
|
||||||
refute {:sponsorblock_remove, []} in res
|
refute Keyword.has_key?(res, :sponsorblock_mark)
|
||||||
refute :sponsorblock_remove in res
|
refute :sponsorblock_remove in res
|
||||||
|
refute :sponsorblock_mark in res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
@@ -53,13 +57,17 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
test "it saves attributes to the media_item", %{media_item: media_item} do
|
setup do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves attributes to the media_item", %{media_item: media_item} do
|
||||||
assert media_item.media_filepath == nil
|
assert media_item.media_filepath == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
@@ -67,27 +75,24 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert media_item.media_filepath != nil
|
assert media_item.media_filepath != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
test "saves the metadata to the media_item", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert media_item.metadata == nil
|
assert media_item.metadata == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
assert Repo.reload(media_item).metadata != nil
|
assert Repo.reload(media_item).metadata != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it won't double-schedule downloading jobs", %{media_item: media_item} do
|
test "won't double-schedule downloading jobs", %{media_item: media_item} do
|
||||||
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
|
|
||||||
assert [_] = all_enqueued(worker: MediaDownloadWorker)
|
assert [_] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it sets the job to retryable if the download fails", %{media_item: media_item} do
|
test "sets the job to retryable if the download fails", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error"} end)
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "error"}
|
||||||
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
@@ -97,8 +102,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets the job to retryable if the download failed and was retried", %{media_item: media_item} do
|
test "sets the job to retryable if the download failed and was retried", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, "Unable to communicate with SponsorBlock", 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "Unable to communicate with SponsorBlock", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
@@ -109,8 +115,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set the job to retryable if retrying wouldn't fix the issue", %{media_item: media_item} do
|
test "does not set the job to retryable if retrying wouldn't fix the issue", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, "Something something Video unavailable something something", 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "Something something Video unavailable something something", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
@@ -120,14 +127,30 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
test "does not set the job to retryable if youtube thinks you're a bot", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error", 1} end)
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "Sign in to confirm you're not a bot", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
|
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id, quality_upgrade?: true}))
|
||||||
|
|
||||||
|
assert job.state == "completed"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "error", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
test "does not download if the source is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
|
|
||||||
@@ -135,23 +158,28 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not download if the media item is set to not download", %{media_item: media_item} do
|
test "does not download if the media item is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
Media.update_media_item(media_item, %{prevent_download: true})
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the file's size to the database", %{media_item: media_item} do
|
test "saves the file's size to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
FilesystemUtils.write_p!(metadata["filepath"], "test")
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
FilesystemUtils.write_p!(metadata["filepath"], "test")
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -159,10 +187,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set redownloaded_at by default", %{media_item: media_item} do
|
test "does not set redownloaded_at by default", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -174,14 +198,27 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
|
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :no_force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
refute :force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :no_force_overwrites in opts
|
||||||
|
refute :force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not download if the media item isn't pending download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
@@ -189,7 +226,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1 when testing non-downloadable media" do
|
describe "perform/1 when testing non-downloadable media" do
|
||||||
test "does not retry the job if the media is currently not downloadable", %{media_item: media_item} do
|
test "does not retry the job if the media is currently not downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -203,7 +240,11 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1 when testing forced downloads" do
|
describe "perform/1 when testing forced downloads" do
|
||||||
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
Media.update_media_item(media_item, %{prevent_download: true})
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
@@ -211,15 +252,32 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "ignores whether the media item is pending when forced", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
refute :no_force_overwrites in opts
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :force_overwrites in opts
|
||||||
|
refute :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
@@ -227,42 +285,74 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1 when testing re-downloads" do
|
describe "perform/1 when testing re-downloads" do
|
||||||
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
|
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
assert media_item.media_redownloaded_at != nil
|
assert media_item.media_redownloaded_at != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "ignores whether the media item is pending when re-downloaded", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
refute :no_force_overwrites in opts
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't redownload if the source is set to not download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't redownload if the media item is set to not download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :force_overwrites in opts
|
||||||
|
refute :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes old files if the media item has been updated" do
|
test "deletes old files if the media item has been updated" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
tmp_media_item = media_item_with_attachments()
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
{:ok, "{}"}
|
||||||
metadata = Map.put(metadata, "filepath", tmp_media_item.media_filepath)
|
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
tmp_media_item = media_item_with_attachments()
|
||||||
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
metadata = Map.put(metadata, "filepath", tmp_media_item.media_filepath)
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
old_media_item = media_item_with_attachments()
|
old_media_item = media_item_with_attachments()
|
||||||
perform_job(MediaDownloadWorker, %{id: old_media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: old_media_item.id, force: true})
|
||||||
updated_media_item = Repo.reload(old_media_item)
|
updated_media_item = Repo.reload(old_media_item)
|
||||||
@@ -274,14 +364,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1 when testing user script callbacks" do
|
describe "perform/1 when testing user script callbacks" do
|
||||||
setup do
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
test "calls the media_pre_download user script runner", %{media_item: media_item} do
|
test "calls the media_pre_download user script runner", %{media_item: media_item} do
|
||||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
|
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
|
||||||
assert data.id == media_item.id
|
assert data.id == media_item.id
|
||||||
|
|||||||
@@ -16,31 +16,36 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_for_media_item/3" do
|
describe "download_for_media_item/3" do
|
||||||
test "calls the backend runner", %{media_item: media_item} do
|
test "calls the backend runner", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn url, _opts, ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert url == media_item.original_url
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
assert ot == "after_move:%()j"
|
{:ok, "{}"}
|
||||||
assert [{:output_filepath, filepath} | _] = addl
|
|
||||||
assert is_binary(filepath)
|
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
|
||||||
|
url, :download, _opts, ot, addl ->
|
||||||
|
assert url == media_item.original_url
|
||||||
|
assert ot == "after_move:%()j"
|
||||||
|
assert [{:output_filepath, filepath} | _] = addl
|
||||||
|
assert is_binary(filepath)
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "saves the metadata filepath to the database", %{media_item: media_item} do
|
test "saves the metadata filepath to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert is_nil(media_item.metadata)
|
assert is_nil(media_item.metadata)
|
||||||
@@ -51,7 +56,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "errors for non-downloadable media are passed through", %{media_item: media_item} do
|
test "errors for non-downloadable media are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -59,16 +64,18 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, :some_error, 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error, 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "unknown errors are passed through", %{media_item: media_item} do
|
test "unknown errors are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, :some_error}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
@@ -78,29 +85,33 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing non-downloadable media" do
|
describe "download_for_media_item/3 when testing non-downloadable media" do
|
||||||
test "calls the download runner if the media is currently downloadable", %{media_item: media_item} do
|
test "calls the download runner if the media is currently downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
end)
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
_url, :download, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not call the download runner if the media is not downloadable", %{media_item: media_item} do
|
test "does not call the download runner if the media is not downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
|
|
||||||
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, :what_tha} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl -> {:error, :what_tha} end)
|
||||||
|
|
||||||
assert {:error, "Unknown error: {:error, :what_tha}"} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, "Unknown error: {:error, :what_tha}"} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
@@ -108,15 +119,20 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing override options" do
|
describe "download_for_media_item/3 when testing override options" do
|
||||||
test "includes override opts if specified", %{media_item: media_item} do
|
test "includes override opts if specified", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
refute :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
assert :no_force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
refute :force_overwrites in opts
|
||||||
|
assert :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
override_opts = [overwrite_behaviour: :no_force_overwrites]
|
override_opts = [overwrite_behaviour: :no_force_overwrites]
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
|
||||||
@@ -125,12 +141,18 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing cookie usage" do
|
describe "download_for_media_item/3 when testing cookie usage" do
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert {:use_cookies, true} in addl
|
_url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
assert {:use_cookies, true} in addl
|
||||||
end)
|
{:ok, "{}"}
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, true} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
source = source_fixture(%{use_cookies: true})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
@@ -139,12 +161,17 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert {:use_cookies, false} in addl
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, "{}"}
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
source = source_fixture(%{use_cookies: false})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
@@ -157,8 +184,12 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
||||||
message = "Unable to communicate with SponsorBlock"
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, message, 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
{:error, message, 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
@@ -167,15 +198,20 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
||||||
message = "Unable to communicate with SponsorBlock"
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
[{:output_filepath, filepath} | _] = addl
|
_url, :download, _opts, _ot, addl ->
|
||||||
File.write(filepath, render_metadata(:media_metadata))
|
[{:output_filepath, filepath} | _] = addl
|
||||||
|
File.write(filepath, render_metadata(:media_metadata))
|
||||||
|
|
||||||
{:error, message, 1}
|
{:error, message, 1}
|
||||||
|
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -186,8 +222,10 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing media_item attributes" do
|
describe "download_for_media_item/3 when testing media_item attributes" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -234,20 +272,27 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "extracts the thumbnail_filepath", %{media_item: media_item} do
|
test "extracts the thumbnail_filepath", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
thumbnail_filepath =
|
_url, :download, _opts, _ot, _addl ->
|
||||||
metadata["thumbnails"]
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|> Enum.reverse()
|
|
||||||
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|
|
||||||
|> String.split(~r{\.}, include_captures: true)
|
|
||||||
|> List.insert_at(-3, "-thumb")
|
|
||||||
|> Enum.join()
|
|
||||||
|
|
||||||
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
|
thumbnail_filepath =
|
||||||
|
metadata["thumbnails"]
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|
||||||
|
|> String.split(~r{\.}, include_captures: true)
|
||||||
|
|> List.insert_at(-3, "-thumb")
|
||||||
|
|> Enum.join()
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert media_item.thumbnail_filepath == nil
|
assert media_item.thumbnail_filepath == nil
|
||||||
@@ -258,13 +303,19 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "extracts the metadata_filepath", %{media_item: media_item} do
|
test "extracts the metadata_filepath", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
infojson_filepath = metadata["infojson_filename"]
|
_url, :download, _opts, _ot, _addl ->
|
||||||
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
infojson_filepath = metadata["infojson_filename"]
|
||||||
|
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert media_item.metadata_filepath == nil
|
assert media_item.metadata_filepath == nil
|
||||||
@@ -277,8 +328,10 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing NFO generation" do
|
describe "download_for_media_item/3 when testing NFO generation" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, _ot, _addl_opts ->
|
||||||
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
||||||
assert {:remux_video, "mp4"} in opts
|
assert {:remux_video, "mp4"} in opts
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -95,7 +95,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -109,7 +109,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -126,7 +126,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
||||||
source = source_fixture(%{media_profile_id: profile.id})
|
source = source_fixture(%{media_profile_id: profile.id})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
output =
|
output =
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
id: "video2",
|
id: "video2",
|
||||||
@@ -150,7 +150,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not blow up if a media item cannot be created", %{source: source} do
|
test "does not blow up if a media item cannot be created", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:error, "message", 1}
|
{:error, "message", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true)
|
source = source_fixture(fast_index: true)
|
||||||
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, fn servers, opts ->
|
expect(AppriseRunnerMock, :run, fn servers, opts ->
|
||||||
assert "server_1" = servers
|
assert "server_1" = servers
|
||||||
@@ -110,7 +113,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true, download_media: false)
|
source = source_fixture(fast_index: true, download_media: false)
|
||||||
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
perform_job(FastIndexingWorker, %{id: source.id})
|
perform_job(FastIndexingWorker, %{id: source.id})
|
||||||
@@ -120,7 +127,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true, title_filter_regex: "foobar")
|
source = source_fixture(fast_index: true, title_filter_regex: "foobar")
|
||||||
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
perform_job(FastIndexingWorker, %{id: source.id})
|
perform_job(FastIndexingWorker, %{id: source.id})
|
||||||
|
|||||||
@@ -757,7 +757,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does delete the media item's metadata files" do
|
test "does delete the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
@@ -789,7 +789,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "deletes the media item's metadata files" do
|
test "deletes the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
@@ -875,7 +875,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not delete the media item's metadata files" do
|
test "does not delete the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
@@ -921,6 +921,14 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_item = media_item_fixture()
|
media_item = media_item_fixture()
|
||||||
assert %Ecto.Changeset{} = Media.change_media_item(media_item)
|
assert %Ecto.Changeset{} = Media.change_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "validates the title doesn't start with 'youtube video #'" do
|
||||||
|
# This is to account for youtube restricting indexing. See issue #549 for more
|
||||||
|
media_item = media_item_fixture()
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{valid?: false} = Media.change_media_item(media_item, %{title: "youtube video #123"})
|
||||||
|
assert %Ecto.Changeset{valid?: true} = Media.change_media_item(media_item, %{title: "any other title"})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "change_media_item/1 when testing upload_date_index and source is a channel" do
|
describe "change_media_item/1 when testing upload_date_index and source is a channel" do
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
|
|
||||||
describe "download_and_store_thumbnail_for/2" do
|
describe "download_and_store_thumbnail_for/2" do
|
||||||
test "returns the filepath", %{media_item: media_item} do
|
test "returns the filepath", %{media_item: media_item} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn url, :download_thumbnail, opts, ot, _addl ->
|
||||||
assert url == media_item.original_url
|
assert url == media_item.original_url
|
||||||
assert ot == "after_move:%()j"
|
assert ot == "after_move:%()j"
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
@@ -101,7 +101,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
@@ -113,7 +113,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error"} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:error, "error"} end)
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
|
|
||||||
@source_details_ot "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
|
||||||
@metadata_ot "playlist:%()j"
|
|
||||||
|
|
||||||
describe "kickoff_with_task/1" do
|
describe "kickoff_with_task/1" do
|
||||||
test "enqueues a new worker for the source" do
|
test "enqueues a new worker for the source" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -32,8 +29,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
test "won't call itself in an infinite loop" do
|
test "won't call itself in an infinite loop" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -51,8 +48,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing attribute updates" do
|
describe "perform/1 when testing attribute updates" do
|
||||||
test "the source description is saved" do
|
test "the source description is saved" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{description: nil})
|
source = source_fixture(%{description: nil})
|
||||||
@@ -68,8 +65,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing metadata storage" do
|
describe "perform/1 when testing metadata storage" do
|
||||||
test "sets metadata location for source" do
|
test "sets metadata location for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = Repo.preload(source_fixture(), :metadata)
|
source = Repo.preload(source_fixture(), :metadata)
|
||||||
@@ -87,8 +84,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
|
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, file_contents}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, file_contents}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
@@ -100,8 +97,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets metadata image location for source" do
|
test "sets metadata image location for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -118,8 +115,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "stores metadata images for source" do
|
test "stores metadata images for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -138,12 +135,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing source image downloading" do
|
describe "perform/1 when testing source image downloading" do
|
||||||
test "downloads and stores source images" do
|
test "downloads and stores source images" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:convert_thumbnails, "jpg"} in opts
|
assert {:convert_thumbnails, "jpg"} in opts
|
||||||
|
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
@@ -168,10 +165,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "calls one set of yt-dlp metadata opts for channels" do
|
test "calls one set of yt-dlp metadata opts for channels" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:playlist_items, 0} in opts
|
assert {:playlist_items, 0} in opts
|
||||||
assert :write_all_thumbnails in opts
|
assert :write_all_thumbnails in opts
|
||||||
|
|
||||||
@@ -186,10 +183,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "calls another set of yt-dlp metadata opts for playlists" do
|
test "calls another set of yt-dlp metadata opts for playlists" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:playlist_items, 1} in opts
|
assert {:playlist_items, 1} in opts
|
||||||
assert :write_thumbnail in opts
|
assert :write_thumbnail in opts
|
||||||
|
|
||||||
@@ -204,12 +201,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store source images if the profile is not set to" do
|
test "does not store source images if the profile is not set to" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -226,12 +223,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store source images if the series directory cannot be determined" do
|
test "does not store source images if the series directory cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -248,10 +245,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -264,10 +261,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -282,12 +279,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when determining the series_directory" do
|
describe "perform/1 when determining the series_directory" do
|
||||||
test "sets the series directory based on the returned media filepath" do
|
test "sets the series directory based on the returned media filepath" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -300,12 +297,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set the series directory if it cannot be determined" do
|
test "does not set the series directory if it cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -318,11 +315,11 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets use_cookies if the source is set to use cookies" do
|
test "sets use_cookies if the source is set to use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -332,11 +329,11 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set use_cookies if the source is not set to use cookies" do
|
test "does not set use_cookies if the source is not set to use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -348,12 +345,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when storing the series NFO" do
|
describe "perform/1 when storing the series NFO" do
|
||||||
test "stores the NFO if specified" do
|
test "stores the NFO if specified" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -371,12 +368,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store the NFO if not specified" do
|
test "does not store the NFO if not specified" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -390,12 +387,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store the NFO if the series directory cannot be determined" do
|
test "does not store the NFO if the series directory cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
defmodule Pinchflat.Podcasts.OpmlFeedBuilderTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Podcasts.OpmlFeedBuilder
|
||||||
|
|
||||||
|
setup do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, source: source}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/2" do
|
||||||
|
test "returns an XML document", %{source: source} do
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<?xml version="1.0" encoding="UTF-8"?>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "escapes illegal characters" do
|
||||||
|
source = source_fixture(%{custom_name: "A & B"})
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(A & B))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "build podcast link with URL base", %{source: source} do
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(http://example.com/sources/#{source.uuid}/feed.xml))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,16 @@ defmodule Pinchflat.Podcasts.PodcastHelpersTest do
|
|||||||
|
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
|
describe "opml_sources" do
|
||||||
|
test "returns sources not marked for deletion" do
|
||||||
|
source = source_fixture()
|
||||||
|
source_fixture(%{marked_for_deletion_at: DateTime.utc_now()})
|
||||||
|
assert [found_source] = PodcastHelpers.opml_sources()
|
||||||
|
assert found_source.custom_name == source.custom_name
|
||||||
|
assert found_source.uuid == source.uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "persisted_media_items_for/2" do
|
describe "persisted_media_items_for/2" do
|
||||||
test "returns media items with files that exist on-disk" do
|
test "returns media items with files that exist on-disk" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -77,5 +77,20 @@ defmodule Pinchflat.SettingsTest do
|
|||||||
|
|
||||||
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
|
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "ensures the extractor sleep interval is positive" do
|
||||||
|
setting = Settings.record()
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 1})
|
||||||
|
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 0})
|
||||||
|
assert %Ecto.Changeset{valid?: false} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: -1})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows you to reset the extractor sleep interval" do
|
||||||
|
setting = Settings.record()
|
||||||
|
assert {:ok, setting} = Settings.update_setting(setting, %{extractor_sleep_interval_seconds: 1})
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 0})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
alias alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
|
|||||||
@@ -50,45 +50,69 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
setup do
|
setup do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
||||||
|
|
||||||
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
|
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source if it should be indexed" do
|
test "indexes the source if it should be indexed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source no matter what if the source has never been indexed before" do
|
test "indexes the source no matter what if the source has never been indexed before" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: nil)
|
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: nil)
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source no matter what if the 'force' arg is passed" do
|
test "indexes the source no matter what if the 'force' arg is passed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not do any indexing if the source has been indexed and shouldn't be rescheduled" do
|
test "doesn't use a download archive if the index has been forced" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source =
|
||||||
|
source_fixture(collection_type: :channel, index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not do any indexing if the source has been indexed and shouldn't be rescheduled" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: -1, last_indexed_at: DateTime.utc_now())
|
source = source_fixture(index_frequency_minutes: -1, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not reschedule if the source shouldn't be indexed" do
|
test "does not reschedule if the source shouldn't be indexed" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: -1)
|
source = source_fixture(index_frequency_minutes: -1)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
@@ -96,8 +120,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it kicks off a download job for each pending media item" do
|
test "kicks off a download job for each pending media item" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -107,8 +131,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker)) == 3
|
assert length(all_enqueued(worker: MediaDownloadWorker)) == 3
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it starts a job for any pending media item even if it's from another run" do
|
test "starts a job for any pending media item even if it's from another run" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -119,8 +143,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker)) == 4
|
assert length(all_enqueued(worker: MediaDownloadWorker)) == 4
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not kick off a job for media items that could not be saved" do
|
test "does not kick off a job for media items that could not be saved" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -132,9 +156,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker))
|
assert length(all_enqueued(worker: MediaDownloadWorker))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it reschedules the job based on the index frequency" do
|
test "reschedules the job based on the index frequency" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
@@ -145,9 +167,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a task for the rescheduled job" do
|
test "creates a task for the rescheduled job" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
task_count_fetcher = fn ->
|
task_count_fetcher = fn ->
|
||||||
@@ -159,9 +179,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a future task for fast indexing if appropriate" do
|
test "creates a future task for fast indexing if appropriate" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
@@ -172,9 +190,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it deletes existing fast indexing tasks if a new one is created" do
|
test "deletes existing fast indexing tasks if a new one is created" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
||||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
@@ -184,17 +200,17 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not create a task for fast indexing otherwise" do
|
test "does not create a task for fast indexing otherwise" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: false)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: false)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
refute_enqueued(worker: FastIndexingWorker)
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates the basic media_item records" do
|
test "creates the basic media_item records" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, source_attributes_return_fixture()} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
@@ -225,7 +241,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
test "sends a notification if new media was found" do
|
test "sends a notification if new media was found" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||||
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
||||||
|
|
||||||
|
setup do
|
||||||
|
{:ok, %{source: source_fixture()}}
|
||||||
|
end
|
||||||
|
|
||||||
describe "kickoff_indexing_task/3" do
|
describe "kickoff_indexing_task/3" do
|
||||||
test "schedules a job" do
|
test "schedules a job" do
|
||||||
source = source_fixture(index_frequency_minutes: 1)
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
@@ -53,6 +57,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "schedules a job immediately if the user is forcing an index" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 30, last_indexed_at: now_minus(5, :minutes))
|
||||||
|
|
||||||
|
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source, %{force: true})
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "creates and attaches a task" do
|
test "creates and attaches a task" do
|
||||||
source = source_fixture(index_frequency_minutes: 1)
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
|
|
||||||
@@ -123,12 +137,6 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_indexing_tasks/2" do
|
describe "delete_indexing_tasks/2" do
|
||||||
setup do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
{:ok, %{source: source}}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes slow indexing tasks for the source", %{source: source} do
|
test "deletes slow indexing tasks for the source", %{source: source} do
|
||||||
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
_task = task_fixture(source_id: source.id, job_id: job.id)
|
_task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
@@ -172,13 +180,13 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1" do
|
describe "index_and_enqueue_download_for_media_items/2" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, [source: source_fixture()]}
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates a media_item record for each media ID returned", %{source: source} do
|
test "creates a media_item record for each media ID returned", %{source: source} do
|
||||||
@@ -259,7 +267,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
test "doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
response =
|
response =
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
id: "video3",
|
id: "video3",
|
||||||
@@ -282,8 +290,31 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert %Ecto.Changeset{} = changeset
|
assert %Ecto.Changeset{} = changeset
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "doesn't blow up if the media item cannot be saved", %{source: source} do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
response =
|
||||||
|
Phoenix.json_library().encode!(%{
|
||||||
|
id: "video1",
|
||||||
|
# This is a disallowed title - see MediaItem changeset or issue #549
|
||||||
|
title: "youtube video #123",
|
||||||
|
original_url: "https://example.com/video1",
|
||||||
|
live_status: "not_live",
|
||||||
|
description: "desc1",
|
||||||
|
aspect_ratio: 1.67,
|
||||||
|
duration: 12.34,
|
||||||
|
upload_date: "20210101"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, response}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert [changeset] = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{} = changeset
|
||||||
|
end
|
||||||
|
|
||||||
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
||||||
assert {:remux_video, "mp4"} in opts
|
assert {:remux_video, "mp4"} in opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
@@ -293,7 +324,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -304,7 +335,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't set use_cookies if the source doesn't use cookies" do
|
test "doesn't set use_cookies if the source doesn't use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, false} in addl_opts
|
assert {:use_cookies, false} in addl_opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -315,15 +346,11 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1 when testing file watcher" do
|
describe "index_and_enqueue_download_for_media_items/2 when testing file watcher" do
|
||||||
setup do
|
|
||||||
{:ok, [source: source_fixture()]}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "creates a new media item for everything already in the file", %{source: source} do
|
test "creates a new media item for everything already in the file", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -342,7 +369,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "enqueues a download for everything already in the file", %{source: source} do
|
test "enqueues a download for everything already in the file", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -362,7 +389,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture(download_media: false)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -382,7 +409,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
||||||
source = source_fixture(%{media_profile_id: profile.id})
|
source = source_fixture(%{media_profile_id: profile.id})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
|
|
||||||
contents =
|
contents =
|
||||||
@@ -413,7 +440,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "does not enqueue multiple download jobs for the same media items", %{source: source} do
|
test "does not enqueue multiple download jobs for the same media items", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -432,7 +459,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "does not blow up if the file returns invalid json", %{source: source} do
|
test "does not blow up if the file returns invalid json", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, "INVALID")
|
File.write(filepath, "INVALID")
|
||||||
|
|
||||||
@@ -446,4 +473,62 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert [] = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
assert [] = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "index_and_enqueue_download_for_media_items when testing the download archive" do
|
||||||
|
test "a download archive is used if the source is a channel", %{source: source} do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
assert :break_on_existing in opts
|
||||||
|
assert Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a download archive is not used if the source is not a channel" do
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a download archive is not used if the index has been forced to run" do
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, was_forced: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "the download archive is formatted correctly and contains the right video", %{source: source} do
|
||||||
|
media_items =
|
||||||
|
1..21
|
||||||
|
|> Enum.map(fn n ->
|
||||||
|
media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(n, :days)})
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
archive_file = Keyword.get(opts, :download_archive)
|
||||||
|
last_media_item = List.last(media_items)
|
||||||
|
|
||||||
|
assert File.read!(archive_file) == "youtube #{last_media_item.media_id}"
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
|
|
||||||
describe "create_source/2" do
|
describe "create_source/2" do
|
||||||
test "automatically sets the UUID" do
|
test "automatically sets the UUID" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -97,7 +97,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "UUID is not writable by the user" do
|
test "UUID is not writable by the user" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -110,7 +110,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creates a source and adds name + ID from runner response for channels" do
|
test "creates a source and adds name + ID from runner response for channels" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -123,7 +123,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creates a source and adds name + ID for playlists" do
|
test "creates a source and adds name + ID for playlists" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -136,7 +136,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "adds an error if the runner fails" do
|
test "adds an error if the runner fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "some error", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl -> {:error, "some error", 1} end)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -148,7 +148,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "adds an error if the runner succeeds but the result was invalid JSON" do
|
test "adds an error if the runner succeeds but the result was invalid JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -160,7 +160,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "you can specify a custom custom_name" do
|
test "you can specify a custom custom_name" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -174,7 +174,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "friendly name is pulled from collection_name if not specified" do
|
test "friendly name is pulled from collection_name if not specified" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -187,7 +187,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation enforces uniqueness of collection_id scoped to the media_profile and title regex" do
|
test "creation enforces uniqueness of collection_id scoped to the media_profile and title regex" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -208,7 +208,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation lets you duplicate collection_ids and profiles as long as the regex is different" do
|
test "creation lets you duplicate collection_ids and profiles as long as the regex is different" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -232,7 +232,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -256,8 +256,8 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "collection_type is inferred from source details" do
|
test "collection_type is inferred from source details" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -276,13 +276,13 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation with invalid data fails fast and does not call the runner" do
|
test "creation with invalid data fails fast and does not call the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
|
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation will schedule the indexing task" do
|
test "creation will schedule the indexing task" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -295,7 +295,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation schedules an index test even if the index frequency is 0" do
|
test "creation schedules an index test even if the index frequency is 0" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -309,7 +309,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fast_index forces the index frequency to be a default value" do
|
test "fast_index forces the index frequency to be a default value" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -324,7 +324,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "disabling fast index will not change the index frequency" do
|
test "disabling fast index will not change the index frequency" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -339,7 +339,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creating will kickoff a metadata storage worker" do
|
test "creating will kickoff a metadata storage worker" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -354,9 +354,58 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "create_source/2 when testing options" do
|
describe "create_source/2 when testing yt-dlp options" do
|
||||||
|
test "sets use_cookies to true if the source has been set to use cookies" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
||||||
|
assert Keyword.get(addl, :use_cookies)
|
||||||
|
|
||||||
|
{:ok, playlist_return()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
use_cookies: true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies to false if the source has not been set to use cookies" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
||||||
|
refute Keyword.get(addl, :use_cookies)
|
||||||
|
|
||||||
|
{:ok, playlist_return()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
use_cookies: false
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "skips sleep interval" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
||||||
|
assert Keyword.get(addl, :skip_sleep_interval)
|
||||||
|
|
||||||
|
{:ok, playlist_return()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create_source/2 when testing its options" do
|
||||||
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
|
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -380,7 +429,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updates with invalid data fails fast and does not call the runner" do
|
test "updates with invalid data fails fast and does not call the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
@@ -388,7 +437,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating the original_url will re-fetch the source details for channels" do
|
test "updating the original_url will re-fetch the source details for channels" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
||||||
@@ -399,7 +448,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating the original_url will re-fetch the source details for playlists" do
|
test "updating the original_url will re-fetch the source details for playlists" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
|
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
|
||||||
@@ -410,7 +459,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "not updating the original_url will not re-fetch the source details" do
|
test "not updating the original_url will not re-fetch the source details" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{name: "some updated name"}
|
update_attrs = %{name: "some updated name"}
|
||||||
@@ -428,7 +477,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
||||||
|
|
||||||
@@ -901,29 +950,31 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp playlist_mock(_url, _opts, _ot, _addl) do
|
defp playlist_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
{
|
{:ok, playlist_return()}
|
||||||
:ok,
|
|
||||||
Phoenix.json_library().encode!(%{
|
|
||||||
channel: nil,
|
|
||||||
channel_id: nil,
|
|
||||||
playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}",
|
|
||||||
playlist_title: "some playlist name"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp channel_mock(_url, _opts, _ot, _addl) do
|
defp channel_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
|
{:ok, channel_return()}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp playlist_return do
|
||||||
|
Phoenix.json_library().encode!(%{
|
||||||
|
channel: nil,
|
||||||
|
channel_id: nil,
|
||||||
|
playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}",
|
||||||
|
playlist_title: "some playlist name"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp channel_return do
|
||||||
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
|
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
|
||||||
|
|
||||||
{
|
Phoenix.json_library().encode!(%{
|
||||||
:ok,
|
channel: "some channel name",
|
||||||
Phoenix.json_library().encode!(%{
|
channel_id: channel_id,
|
||||||
channel: "some channel name",
|
playlist_id: channel_id,
|
||||||
channel_id: channel_id,
|
playlist_title: "some channel name - videos"
|
||||||
playlist_id: channel_id,
|
})
|
||||||
playlist_title: "some channel name - videos"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,4 +47,21 @@ defmodule Pinchflat.Utils.NumberUtilsTest do
|
|||||||
assert NumberUtils.human_byte_size(nil) == {0, "B"}
|
assert NumberUtils.human_byte_size(nil) == {0, "B"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "add_jitter/2" do
|
||||||
|
test "returns 0 when the number is less than or equal to 0" do
|
||||||
|
assert NumberUtils.add_jitter(0) == 0
|
||||||
|
assert NumberUtils.add_jitter(-1) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the number with jitter added" do
|
||||||
|
assert NumberUtils.add_jitter(100) in 100..150
|
||||||
|
end
|
||||||
|
|
||||||
|
test "optionally takes a jitter percentage" do
|
||||||
|
assert NumberUtils.add_jitter(100, 0.1) in 90..110
|
||||||
|
assert NumberUtils.add_jitter(100, 0.5) in 50..150
|
||||||
|
assert NumberUtils.add_jitter(100, 1) in 0..200
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,20 +28,6 @@ defmodule Pinchflat.Utils.StringUtilsTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "truncate/2" do
|
|
||||||
test "truncates a string to the given length and adds ..." do
|
|
||||||
assert StringUtils.truncate("hello world", 5) == "hello..."
|
|
||||||
end
|
|
||||||
|
|
||||||
test "breaks on a word boundary" do
|
|
||||||
assert StringUtils.truncate("hello world", 7) == "hello..."
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not truncate a string shorter than the given length" do
|
|
||||||
assert StringUtils.truncate("hello", 10) == "hello"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "double_brace/1" do
|
describe "double_brace/1" do
|
||||||
test "wraps a string in double braces" do
|
test "wraps a string in double braces" do
|
||||||
assert StringUtils.double_brace("hello") == "{{ hello }}"
|
assert StringUtils.double_brace("hello") == "{{ hello }}"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.CommandRunner, as: Runner
|
alias Pinchflat.YtDlp.CommandRunner, as: Runner
|
||||||
@@ -13,30 +14,36 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "run/4" do
|
describe "run/4" do
|
||||||
test "it returns the output and status when the command succeeds" do
|
test "returns the output and status when the command succeeds" do
|
||||||
assert {:ok, _output} = Runner.run(@media_url, [], "")
|
assert {:ok, _output} = Runner.run(@media_url, :foo, [], "")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it includes the media url as the first argument" do
|
test "considers a 101 exit code as being successful" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [:ignore_errors], "")
|
wrap_executable("/app/test/support/scripts/yt-dlp-mocks/101_exit_code.sh", fn ->
|
||||||
|
assert {:ok, _output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes the media url as the first argument" do
|
||||||
|
assert {:ok, output} = Runner.run(@media_url, :foo, [:ignore_errors], "")
|
||||||
|
|
||||||
assert String.contains?(output, "#{@media_url} --ignore-errors")
|
assert String.contains?(output, "#{@media_url} --ignore-errors")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it automatically includes the --print-to-file flag" do
|
test "automatically includes the --print-to-file flag" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "%(id)s")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "%(id)s")
|
||||||
|
|
||||||
assert String.contains?(output, "--print-to-file %(id)s /tmp/")
|
assert String.contains?(output, "--print-to-file %(id)s /tmp/")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns the output and status when the command fails" do
|
test "returns the output and status when the command fails" do
|
||||||
wrap_executable("/bin/false", fn ->
|
wrap_executable("/bin/false", fn ->
|
||||||
assert {:error, "", 1} = Runner.run(@media_url, [], "")
|
assert {:error, "", 1} = Runner.run(@media_url, :foo, [], "")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "optionally lets you specify an output_filepath" do
|
test "optionally lets you specify an output_filepath" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "%(id)s", output_filepath: "/tmp/yt-dlp-output.json")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "%(id)s", output_filepath: "/tmp/yt-dlp-output.json")
|
||||||
|
|
||||||
assert String.contains?(output, "--print-to-file %(id)s /tmp/yt-dlp-output.json")
|
assert String.contains?(output, "--print-to-file %(id)s /tmp/yt-dlp-output.json")
|
||||||
end
|
end
|
||||||
@@ -54,7 +61,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "includes cookie options when cookies.txt exists and enabled", %{cookie_file: cookie_file} do
|
test "includes cookie options when cookies.txt exists and enabled", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: true)
|
||||||
|
|
||||||
assert String.contains?(output, "--cookies #{cookie_file}")
|
assert String.contains?(output, "--cookies #{cookie_file}")
|
||||||
end
|
end
|
||||||
@@ -62,7 +69,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt exists but disabled", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt exists but disabled", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: false)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: false)
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies #{cookie_file}")
|
refute String.contains?(output, "--cookies #{cookie_file}")
|
||||||
end
|
end
|
||||||
@@ -70,7 +77,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, " \n \n ")
|
FilesystemUtils.write_p!(cookie_file, " \n \n ")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: true)
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies")
|
refute String.contains?(output, "--cookies")
|
||||||
refute String.contains?(output, cookie_file)
|
refute String.contains?(output, cookie_file)
|
||||||
@@ -79,7 +86,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt doesn't exist", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt doesn't exist", %{cookie_file: cookie_file} do
|
||||||
File.rm(cookie_file)
|
File.rm(cookie_file)
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies")
|
refute String.contains?(output, "--cookies")
|
||||||
refute String.contains?(output, cookie_file)
|
refute String.contains?(output, cookie_file)
|
||||||
@@ -89,21 +96,51 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "run/4 when testing sleep interval options" do
|
||||||
|
test "includes sleep interval options by default" do
|
||||||
|
Settings.set(extractor_sleep_interval_seconds: 5)
|
||||||
|
|
||||||
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
|
assert String.contains?(output, "--sleep-interval")
|
||||||
|
assert String.contains?(output, "--sleep-requests")
|
||||||
|
assert String.contains?(output, "--sleep-subtitles")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't include sleep interval options when skip_sleep_interval is true" do
|
||||||
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", skip_sleep_interval: true)
|
||||||
|
|
||||||
|
refute String.contains?(output, "--sleep-interval")
|
||||||
|
refute String.contains?(output, "--sleep-requests")
|
||||||
|
refute String.contains?(output, "--sleep-subtitles")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't include sleep interval options when extractor_sleep_interval_seconds is 0" do
|
||||||
|
Settings.set(extractor_sleep_interval_seconds: 0)
|
||||||
|
|
||||||
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
|
refute String.contains?(output, "--sleep-interval")
|
||||||
|
refute String.contains?(output, "--sleep-requests")
|
||||||
|
refute String.contains?(output, "--sleep-subtitles")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "run/4 when testing global options" do
|
describe "run/4 when testing global options" do
|
||||||
test "creates windows-safe filenames" do
|
test "creates windows-safe filenames" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--windows-filenames")
|
assert String.contains?(output, "--windows-filenames")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "runs quietly" do
|
test "runs quietly" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--quiet")
|
assert String.contains?(output, "--quiet")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets the cache directory" do
|
test "sets the cache directory" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--cache-dir /tmp/test/tmpfiles/yt-dlp-cache")
|
assert String.contains?(output, "--cache-dir /tmp/test/tmpfiles/yt-dlp-cache")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_media_attributes_for_collection/2" do
|
describe "get_media_attributes_for_collection/2" do
|
||||||
test "returns a list of video attributes with no blank elements" do
|
test "returns a list of video attributes with no blank elements" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture() <> "\n\n"}
|
{:ok, source_attributes_return_fixture() <> "\n\n"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected default args" do
|
test "passes the expected default args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, ot, _addl_opts ->
|
||||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
||||||
assert ot == Media.indexing_output_template()
|
assert ot == Media.indexing_output_template()
|
||||||
|
|
||||||
@@ -30,13 +30,15 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns the error straight through when the command fails" do
|
test "returns the error straight through when the command fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:error, "Big issue", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_media_attributes_for_collection(@channel_url)
|
assert {:error, "Big issue", 1} = MediaCollection.get_media_attributes_for_collection(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "passes long additional command options" do
|
test "passes long additional command options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
assert :foo in opts
|
assert :foo in opts
|
||||||
|
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
@@ -46,7 +48,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to runner" do
|
test "passes additional args to runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert [{:output_filepath, filepath} | _] = addl_opts
|
assert [{:output_filepath, filepath} | _] = addl_opts
|
||||||
assert {:use_cookies, false} in addl_opts
|
assert {:use_cookies, false} in addl_opts
|
||||||
assert String.ends_with?(filepath, ".json")
|
assert String.ends_with?(filepath, ".json")
|
||||||
@@ -58,7 +60,10 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "supports an optional file_listener_handler that gets passed a filename" do
|
test "supports an optional file_listener_handler that gets passed a filename" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
current_self = self()
|
current_self = self()
|
||||||
|
|
||||||
handler = fn filename ->
|
handler = fn filename ->
|
||||||
@@ -73,7 +78,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "gracefully handles partially failed responses" do
|
test "gracefully handles partially failed responses" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, "INVALID\n\n" <> source_attributes_return_fixture() <> "\nINVALID\n"}
|
{:ok, "INVALID\n\n" <> source_attributes_return_fixture() <> "\nINVALID\n"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_source_details/1" do
|
describe "get_source_details/1" do
|
||||||
test "returns a map with data on success" do
|
test "returns a map with data on success" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts ->
|
||||||
Phoenix.json_library().encode(%{
|
Phoenix.json_library().encode(%{
|
||||||
channel: "PinchflatTestChannel",
|
channel: "PinchflatTestChannel",
|
||||||
channel_id: "UCQH2",
|
channel_id: "UCQH2",
|
||||||
@@ -104,7 +109,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected args to the runner" do
|
test "passes the expected args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, opts, ot, _addl_opts ->
|
||||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
|
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
|
||||||
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes custom args to the runner" do
|
test "passes custom args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, opts, _ot, _addl_opts ->
|
||||||
assert {:foo, :bar} in opts
|
assert {:foo, :bar} in opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -125,7 +130,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to the runner" do
|
test "passes additional args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -135,13 +140,13 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the runner returns an error" do
|
test "returns an error if the runner returns an error" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_details(@channel_url)
|
assert {:error, "Big issue", 1} = MediaCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the output is not JSON" do
|
test "returns an error if the output is not JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
@@ -149,7 +154,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_source_metadata/1" do
|
describe "get_source_metadata/1" do
|
||||||
test "returns a map with data on success" do
|
test "returns a map with data on success" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts ->
|
||||||
Phoenix.json_library().encode(%{channel: "PinchflatTestChannel"})
|
Phoenix.json_library().encode(%{channel: "PinchflatTestChannel"})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -159,7 +164,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected args to the backend runner" do
|
test "passes the expected args to the backend runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_metadata, opts, ot, _addl_opts ->
|
||||||
assert opts == [:skip_download, playlist_items: 0]
|
assert opts == [:skip_download, playlist_items: 0]
|
||||||
assert ot == "playlist:%()j"
|
assert ot == "playlist:%()j"
|
||||||
|
|
||||||
@@ -170,7 +175,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to the runner" do
|
test "passes additional args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_metadata, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -180,7 +185,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes custom args to the runner" do
|
test "passes custom args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, opts, _ot, _addl_opts ->
|
||||||
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -196,13 +201,15 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the runner returns an error" do
|
test "returns an error if the runner returns an error" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts ->
|
||||||
|
{:error, "Big issue", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the output is not JSON" do
|
test "returns an error if the output is not JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "download/3" do
|
describe "download/3" do
|
||||||
test "calls the backend runner with the expected arguments" do
|
test "calls the backend runner with the expected arguments" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn @media_url, :download, opts, ot, addl ->
|
||||||
assert [:no_simulate] = opts
|
assert [:no_simulate] = opts
|
||||||
assert "after_move:%()j" = ot
|
assert "after_move:%()j" = ot
|
||||||
assert addl == []
|
assert addl == []
|
||||||
@@ -21,7 +21,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along custom command args" do
|
test "passes along custom command args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, opts, _ot, _addl ->
|
||||||
assert [:no_simulate, :custom_arg] = opts
|
assert [:no_simulate, :custom_arg] = opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -31,7 +31,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -41,7 +41,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "parses and returns the generated file as JSON" do
|
test "parses and returns the generated file as JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns errors" do
|
test "returns errors" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opt, _ot, _addl ->
|
||||||
{:error, "something"}
|
{:error, "something"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "get_downloadable_status/1" do
|
describe "get_downloadable_status/1" do
|
||||||
test "returns :downloadable if the media was never live" do
|
test "returns :downloadable if the media was never live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :downloadable if the media was live and has been processed" do
|
test "returns :downloadable if the media was live and has been processed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :downloadable if the media's live_status is nil" do
|
test "returns :downloadable if the media's live_status is nil" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => nil})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => nil})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media is currently live" do
|
test "returns :ignorable if the media is currently live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media is scheduled to be live" do
|
test "returns :ignorable if the media is scheduled to be live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_upcoming"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_upcoming"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media was live but hasn't been processed" do
|
test "returns :ignorable if the media was live but hasn't been processed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "post_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "post_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -108,17 +108,27 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the downloadable status can't be determined" do
|
test "returns an error if the downloadable status can't be determined" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "what_tha"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "what_tha"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, "Unknown live status: what_tha"} = Media.get_downloadable_status(@media_url)
|
assert {:error, "Unknown live status: what_tha"} = Media.get_downloadable_status(@media_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "optionally accepts additional args" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, :downloadable} = Media.get_downloadable_status(@media_url, addl_arg: true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_thumbnail/2" do
|
describe "download_thumbnail/2" do
|
||||||
test "calls the backend runner with the expected arguments" do
|
test "calls the backend runner with the expected arguments" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn @media_url, :download_thumbnail, opts, ot, _addl ->
|
||||||
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
||||||
assert ot == "after_move:%()j"
|
assert ot == "after_move:%()j"
|
||||||
|
|
||||||
@@ -129,7 +139,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along custom command args" do
|
test "passes along custom command args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, opts, _ot, _addl ->
|
||||||
assert :custom_arg in opts
|
assert :custom_arg in opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -139,7 +149,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -149,7 +159,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns errors" do
|
test "returns errors" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opt, _ot, _addl ->
|
||||||
{:error, "something"}
|
{:error, "something"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -159,7 +169,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "get_media_attributes/1" do
|
describe "get_media_attributes/1" do
|
||||||
test "returns a list of video attributes" do
|
test "returns a list of video attributes" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -168,7 +178,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it passes the expected default args" do
|
test "it passes the expected default args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, ot, _addl ->
|
||||||
assert opts == [:simulate, :skip_download]
|
assert opts == [:simulate, :skip_download]
|
||||||
assert ot == Media.indexing_output_template()
|
assert ot == Media.indexing_output_template()
|
||||||
|
|
||||||
@@ -179,7 +189,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional command options" do
|
test "passes along additional command options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, _ot, _addl ->
|
||||||
assert [:simulate, :skip_download, :custom_arg] = opts
|
assert [:simulate, :skip_download, :custom_arg] = opts
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -188,7 +198,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -197,7 +207,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns the error straight through when the command fails" do
|
test "returns the error straight through when the command fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = Media.get_media_attributes(@media_url)
|
assert {:error, "Big issue", 1} = Media.get_media_attributes(@media_url)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Utils.StringUtils
|
|
||||||
alias Pinchflat.Pages.JobTableLive
|
alias Pinchflat.Pages.JobTableLive
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.FastIndexing.FastIndexingWorker
|
alias Pinchflat.FastIndexing.FastIndexingWorker
|
||||||
@@ -48,7 +47,7 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
{_source, media_item, _task, _job} = create_media_item_job()
|
{_source, media_item, _task, _job} = create_media_item_job()
|
||||||
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
||||||
|
|
||||||
assert html =~ StringUtils.truncate(media_item.title, 35)
|
assert html =~ media_item.title
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows a media item link", %{conn: conn} do
|
test "shows a media item link", %{conn: conn} do
|
||||||
@@ -62,7 +61,7 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
{source, _task, _job} = create_source_job()
|
{source, _task, _job} = create_source_job()
|
||||||
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
||||||
|
|
||||||
assert html =~ StringUtils.truncate(source.custom_name, 35)
|
assert html =~ source.custom_name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows a source link", %{conn: conn} do
|
test "shows a source link", %{conn: conn} do
|
||||||
|
|||||||
@@ -4,6 +4,37 @@ defmodule PinchflatWeb.PodcastControllerTest do
|
|||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
describe "opml_feed" do
|
||||||
|
test "renders the XML document", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
|
route_token = Settings.get!(:route_token)
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml?#{[route_token: route_token]}")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert {"content-type", "application/opml+xml; charset=utf-8"} in conn.resp_headers
|
||||||
|
assert {"content-disposition", "inline"} in conn.resp_headers
|
||||||
|
assert conn.resp_body =~ ~s"http://www.example.com/sources/#{source.uuid}/feed.xml"
|
||||||
|
assert conn.resp_body =~ "text=\"#{source.custom_name}\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 401 if the route token is incorrect", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml?route_token=incorrect")
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 401 if the route token is missing", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml")
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "rss_feed" do
|
describe "rss_feed" do
|
||||||
test "renders the XML document", %{conn: conn} do
|
test "renders the XML document", %{conn: conn} do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
|
|
||||||
describe "create source" do
|
describe "create source" do
|
||||||
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||||
|
|
||||||
assert %{id: id} = redirected_params(conn)
|
assert %{id: id} = redirected_params(conn)
|
||||||
@@ -82,7 +82,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "redirects to onboarding when onboarding", %{conn: conn, create_attrs: create_attrs} do
|
test "redirects to onboarding when onboarding", %{conn: conn, create_attrs: create_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
|
|
||||||
Settings.set(onboarding: true)
|
Settings.set(onboarding: true)
|
||||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||||
@@ -111,7 +111,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
setup [:create_source]
|
setup [:create_source]
|
||||||
|
|
||||||
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
|
|
||||||
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
|
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
|
||||||
assert redirected_to(conn) == ~p"/sources/#{source}"
|
assert redirected_to(conn) == ~p"/sources/#{source}"
|
||||||
@@ -276,7 +276,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
%{source: source, media_item: media_item}
|
%{source: source, media_item: media_item}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp runner_function_mock(_url, _opts, _ot, _addl) do
|
defp runner_function_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
{
|
{
|
||||||
:ok,
|
:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
defmodule PinchflatWeb.Sources.SourceLive.IndexTableLiveTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
@@ -6,13 +6,13 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias PinchflatWeb.Sources.IndexTableLive
|
alias PinchflatWeb.Sources.SourceLive.IndexTableLive
|
||||||
|
|
||||||
describe "initial rendering" do
|
describe "initial rendering" do
|
||||||
test "lists all sources", %{conn: conn} do
|
test "lists all sources", %{conn: conn} do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
assert html =~ source.custom_name
|
assert html =~ source.custom_name
|
||||||
end
|
end
|
||||||
@@ -20,7 +20,7 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
||||||
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
refute html =~ source.custom_name
|
refute html =~ source.custom_name
|
||||||
end
|
end
|
||||||
@@ -29,27 +29,102 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
source = source_fixture(media_profile_id: media_profile.id)
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
refute html =~ source.custom_name
|
refute html =~ source.custom_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when a source is enabled or disabled" do
|
describe "when testing sorting" do
|
||||||
|
test "sorts by the custom_name by default", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_B")
|
||||||
|
source2 = source_fixture(custom_name: "Source_A")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source2.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source1.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clicking the row will change the sort direction", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_B")
|
||||||
|
source2 = source_fixture(custom_name: "Source_A")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
|
# Click the row to change the sort direction
|
||||||
|
click_element(view, "th", "Name")
|
||||||
|
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clicking a different row will sort by that attribute", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_A", enabled: true)
|
||||||
|
source2 = source_fixture(custom_name: "Source_A", enabled: false)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
|
# Click the row to change the sort field
|
||||||
|
click_element(view, "th", "Enabled?")
|
||||||
|
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source2.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source1.custom_name
|
||||||
|
|
||||||
|
# Click the row to again change the sort direcation
|
||||||
|
click_element(view, "th", "Enabled?")
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when testing pagination" do
|
||||||
|
test "moving to the next page loads new records", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_A")
|
||||||
|
source2 = source_fixture(custom_name: "Source_B")
|
||||||
|
|
||||||
|
session = Map.merge(create_session(), %{"results_per_page" => 1})
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: session)
|
||||||
|
|
||||||
|
assert render_element(view, "tbody") =~ source1.custom_name
|
||||||
|
refute render_element(view, "tbody") =~ source2.custom_name
|
||||||
|
|
||||||
|
click_element(view, "span.pagination-next")
|
||||||
|
|
||||||
|
refute render_element(view, "tbody") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when testing the enable toggle" do
|
||||||
test "updates the source's enabled status", %{conn: conn} do
|
test "updates the source's enabled status", %{conn: conn} do
|
||||||
source = source_fixture(enabled: true)
|
source = source_fixture(enabled: true)
|
||||||
{:ok, view, _html} = live_isolated(conn, IndexTableLive)
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
params = %{
|
view
|
||||||
"event" => "toggle_enabled",
|
|> element(".enabled_toggle_form")
|
||||||
"id" => source.id,
|
|> render_change(%{source: %{"enabled" => false}})
|
||||||
"value" => "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send an event to the server directly
|
|
||||||
render_change(view, "formless-input", params)
|
|
||||||
|
|
||||||
assert %{enabled: false} = Repo.get!(Source, source.id)
|
assert %{enabled: false} = Repo.get!(Source, source.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp click_element(view, selector, text_filter \\ nil) do
|
||||||
|
view
|
||||||
|
|> element(selector, text_filter)
|
||||||
|
|> render_click()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_element(view, selector) do
|
||||||
|
view
|
||||||
|
|> element(selector)
|
||||||
|
|> render()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_session do
|
||||||
|
%{
|
||||||
|
"initial_sort_key" => :custom_name,
|
||||||
|
"initial_sort_direction" => :asc,
|
||||||
|
"results_per_page" => 10
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggleTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias PinchflatWeb.Sources.SourceLive.SourceEnableToggle
|
||||||
|
|
||||||
|
describe "initial rendering" do
|
||||||
|
test "renders a toggle in the on position if the source is enabled" do
|
||||||
|
source = %{id: 1, enabled: true}
|
||||||
|
|
||||||
|
html = render_component(SourceEnableToggle, %{id: :foo, source: source})
|
||||||
|
|
||||||
|
# This is checking the Alpine attrs which is a good-enough proxy for the toggle position
|
||||||
|
assert html =~ "{ enabled: true }"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders a toggle in the off position if the source is disabled" do
|
||||||
|
source = %{id: 1, enabled: false}
|
||||||
|
|
||||||
|
html = render_component(SourceEnableToggle, %{id: :foo, source: source})
|
||||||
|
|
||||||
|
assert html =~ "{ enabled: false }"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user