Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5bf51dfb3 | |||
| 5d624f545b | |||
| 1305a485a0 |
@@ -16,7 +16,7 @@
|
|||||||
{:compiler, env: %{"MIX_ENV" => "test"}},
|
{:compiler, env: %{"MIX_ENV" => "test"}},
|
||||||
{:formatter, env: %{"MIX_ENV" => "test"}},
|
{:formatter, env: %{"MIX_ENV" => "test"}},
|
||||||
{:sobelow, "mix sobelow --config"},
|
{:sobelow, "mix sobelow --config"},
|
||||||
{:prettier_formatting, "yarn run lint:check", fix: "yarn run lint:fix"},
|
{:prettier_formatting, "yarn run prettier . --check", fix: "yarn run prettier . --write"},
|
||||||
{:npm_test, false}
|
{:npm_test, false}
|
||||||
|
|
||||||
## curated tools may be disabled (e.g. the check for compilation warnings)
|
## curated tools may be disabled (e.g. the check for compilation warnings)
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# TODO: figure out why my vscode extension doesn't respect the formatter.exs file
|
|
||||||
# if it's in a subdirectory
|
|
||||||
[
|
[
|
||||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||||
subdirectories: ["priv/*/migrations"],
|
subdirectories: ["priv/*/migrations"],
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ on:
|
|||||||
options:
|
options:
|
||||||
- 'linux/amd64'
|
- 'linux/amd64'
|
||||||
- 'linux/amd64,linux/arm64'
|
- 'linux/amd64,linux/arm64'
|
||||||
docker_tags:
|
|
||||||
type: string
|
|
||||||
description: 'Docker Tags'
|
|
||||||
required: true
|
|
||||||
default: 'dev'
|
|
||||||
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -47,8 +41,7 @@ jobs:
|
|||||||
# All non-release actions will be tagged as `dev` (ie: push, workflow_dispatch)
|
# All non-release actions will be tagged as `dev` (ie: push, workflow_dispatch)
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=raw,value=dev,enable=${{ github.event_name != 'release' && github.event_name != 'workflow_dispatch' }}
|
type=raw,value=dev,enable=${{ github.event_name != 'release' }}
|
||||||
type=raw,value=${{ inputs.docker_tags }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=auto
|
latest=auto
|
||||||
|
|
||||||
@@ -75,7 +68,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/selfhosted.Dockerfile
|
file: ./selfhosted.Dockerfile
|
||||||
# If the event is a release, use the release_arch, otherwise use the
|
# If the event is a release, use the release_arch, otherwise use the
|
||||||
# platforms input if present, falling back to dev_arch
|
# platforms input if present, falling back to dev_arch
|
||||||
platforms: ${{ github.event_name == 'release' && env.release_arch || (github.event.inputs.platforms || env.dev_arch) }}
|
platforms: ${{ github.event_name == 'release' && env.release_arch || (github.event.inputs.platforms || env.dev_arch) }}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
assets/vendor/
|
||||||
|
deps/
|
||||||
|
_build/
|
||||||
@@ -71,8 +71,8 @@ If it doesn't work for your use case, please make a feature request! You can als
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<img src="priv/static/images/app-form-screenshot.jpg" alt="Pinchflat screenshot" width="700" />
|
<img src="priv/static/images/app-form-screenshot.png" alt="Pinchflat screenshot" width="700" />
|
||||||
<img src="priv/static/images/app-screenshot.jpg" alt="Pinchflat screenshot" width="700" />
|
<img src="priv/static/images/app-screenshot.png" alt="Pinchflat screenshot" width="700" />
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ Docker Compose file:
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
pinchflat:
|
pinchflat:
|
||||||
image: ghcr.io/kieraneglin/pinchflat:latest
|
image: keglin/pinchflat:latest
|
||||||
environment:
|
environment:
|
||||||
# Set the timezone to your local timezone
|
# Set the timezone to your local timezone
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
@@ -108,7 +108,7 @@ services:
|
|||||||
2. Prepare the docker image in one of the two ways below:
|
2. Prepare the docker image in one of the two ways below:
|
||||||
- **From GHCR:** `docker pull ghcr.io/kieraneglin/pinchflat:latest`
|
- **From GHCR:** `docker pull ghcr.io/kieraneglin/pinchflat:latest`
|
||||||
- NOTE: also available on Docker Hub at `keglin/pinchflat:latest`
|
- NOTE: also available on Docker Hub at `keglin/pinchflat:latest`
|
||||||
- **Building locally:** `docker build . --file docker/selfhosted.Dockerfile -t ghcr.io/kieraneglin/pinchflat:latest`
|
- **Building locally:** `docker build . --file selfhosted.Dockerfile -t ghcr.io/kieraneglin/pinchflat:latest`
|
||||||
3. Run the container:
|
3. Run the container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -153,7 +153,6 @@ If you change this setting and it works well for you, please leave a comment on
|
|||||||
| EXPOSE_FEED_ENDPOINTS | No | | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
| EXPOSE_FEED_ENDPOINTS | No | | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
||||||
| JOURNAL_MODE | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
| JOURNAL_MODE | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
||||||
| TZ_DATA_DIR | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
| TZ_DATA_DIR | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
||||||
| BASE_ROUTE_PATH | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
|
|
||||||
|
|
||||||
## EFF donations
|
## EFF donations
|
||||||
|
|
||||||
|
|||||||
+18
-21
@@ -22,8 +22,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-Light.woff2'), url('../fonts/satoshi/Satoshi-Light.woff'),
|
src: url('/fonts/satoshi/Satoshi-Light.woff2'), url('/fonts/satoshi/Satoshi-Light.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-Light.ttf');
|
url('/fonts/satoshi/Satoshi-Light.ttf');
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -31,9 +31,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-LightItalic.woff2'),
|
src: url('/fonts/satoshi/Satoshi-LightItalic.woff2'),
|
||||||
url('../fonts/satoshi/Satoshi-LightItalic.woff'),
|
url('/fonts/satoshi/Satoshi-LightItalic.woff'), url('/fonts/satoshi/Satoshi-LightItalic.ttf');
|
||||||
url('../fonts/satoshi/Satoshi-LightItalic.ttf');
|
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -41,8 +40,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-Regular.woff2'), url('../fonts/satoshi/Satoshi-Regular.woff'),
|
src: url('/fonts/satoshi/Satoshi-Regular.woff2'), url('/fonts/satoshi/Satoshi-Regular.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-Regular.ttf');
|
url('/fonts/satoshi/Satoshi-Regular.ttf');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -50,8 +49,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-Italic.woff2'), url('../fonts/satoshi/Satoshi-Italic.woff'),
|
src: url('/fonts/satoshi/Satoshi-Italic.woff2'), url('/fonts/satoshi/Satoshi-Italic.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-Italic.ttf');
|
url('/fonts/satoshi/Satoshi-Italic.ttf');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -59,8 +58,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-Medium.woff2'), url('../fonts/satoshi/Satoshi-Medium.woff'),
|
src: url('/fonts/satoshi/Satoshi-Medium.woff2'), url('/fonts/satoshi/Satoshi-Medium.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-Medium.ttf');
|
url('/fonts/satoshi/Satoshi-Medium.ttf');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -68,9 +67,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-MediumItalic.woff2'),
|
src: url('/fonts/satoshi/Satoshi-MediumItalic.woff2'),
|
||||||
url('../fonts/satoshi/Satoshi-MediumItalic.woff'),
|
url('/fonts/satoshi/Satoshi-MediumItalic.woff'), url('/fonts/satoshi/Satoshi-MediumItalic.ttf');
|
||||||
url('../fonts/satoshi/Satoshi-MediumItalic.ttf');
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -78,8 +76,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-Bold.woff2'), url('../fonts/satoshi/Satoshi-Bold.woff'),
|
src: url('/fonts/satoshi/Satoshi-Bold.woff2'), url('/fonts/satoshi/Satoshi-Bold.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-Bold.ttf');
|
url('/fonts/satoshi/Satoshi-Bold.ttf');
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -87,8 +85,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-BoldItalic.woff2'),
|
src: url('/fonts/satoshi/Satoshi-BoldItalic.woff2'), url('/fonts/satoshi/Satoshi-BoldItalic.woff'),
|
||||||
url('../fonts/satoshi/Satoshi-BoldItalic.woff'), url('../fonts/satoshi/Satoshi-BoldItalic.ttf');
|
url('/fonts/satoshi/Satoshi-BoldItalic.ttf');
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -96,9 +94,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../fonts/satoshi/Satoshi-BlackItalic.woff2'),
|
src: url('/fonts/satoshi/Satoshi-BlackItalic.woff2'),
|
||||||
url('../fonts/satoshi/Satoshi-BlackItalic.woff'),
|
url('/fonts/satoshi/Satoshi-BlackItalic.woff'), url('/fonts/satoshi/Satoshi-BlackItalic.ttf');
|
||||||
url('../fonts/satoshi/Satoshi-BlackItalic.ttf');
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ window.Alpine = Alpine
|
|||||||
Alpine.start()
|
Alpine.start()
|
||||||
|
|
||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
||||||
let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, {
|
let liveSocket = new LiveSocket('/live', Socket, {
|
||||||
params: { _csrf_token: csrfToken },
|
params: { _csrf_token: csrfToken },
|
||||||
dom: {
|
dom: {
|
||||||
onBeforeElUpdated(from, to) {
|
onBeforeElUpdated(from, to) {
|
||||||
|
|||||||
+1
-2
@@ -26,8 +26,7 @@ config :pinchflat,
|
|||||||
basic_auth_password: "",
|
basic_auth_password: "",
|
||||||
expose_feed_endpoints: false,
|
expose_feed_endpoints: false,
|
||||||
file_watcher_poll_interval: 1000,
|
file_watcher_poll_interval: 1000,
|
||||||
timezone: "UTC",
|
timezone: "UTC"
|
||||||
base_route_path: "/"
|
|
||||||
|
|
||||||
config :pinchflat, Pinchflat.Repo,
|
config :pinchflat, Pinchflat.Repo,
|
||||||
journal_mode: :wal,
|
journal_mode: :wal,
|
||||||
|
|||||||
+3
-5
@@ -46,12 +46,12 @@ if config_env() == :prod do
|
|||||||
log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"]))
|
log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"]))
|
||||||
metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"]))
|
metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"]))
|
||||||
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
|
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
|
||||||
|
|
||||||
# For running PF as a podcast host on self-hosted environments
|
# For running PF as a podcast host on self-hosted environments
|
||||||
expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0
|
expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0
|
||||||
|
|
||||||
# For testing alternate journal modes (see issue #137)
|
# For testing alternate journal modes (see issue #137)
|
||||||
journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal"))
|
journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal"))
|
||||||
# For running PF in a subdirectory via a reverse proxy
|
|
||||||
base_route_path = System.get_env("BASE_ROUTE_PATH", "/")
|
|
||||||
|
|
||||||
config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug"))
|
config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug"))
|
||||||
|
|
||||||
@@ -65,8 +65,7 @@ if config_env() == :prod do
|
|||||||
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"),
|
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"),
|
||||||
expose_feed_endpoints: expose_feed_endpoints,
|
expose_feed_endpoints: expose_feed_endpoints,
|
||||||
timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC",
|
timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC",
|
||||||
log_path: log_path,
|
log_path: log_path
|
||||||
base_route_path: base_route_path
|
|
||||||
|
|
||||||
config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/etc/elixir_tzdata_data")
|
config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/etc/elixir_tzdata_data")
|
||||||
|
|
||||||
@@ -109,7 +108,6 @@ if config_env() == :prod do
|
|||||||
ip: {0, 0, 0, 0},
|
ip: {0, 0, 0, 0},
|
||||||
port: String.to_integer(System.get_env("PORT") || "4000")
|
port: String.to_integer(System.get_env("PORT") || "4000")
|
||||||
],
|
],
|
||||||
url: [path: base_route_path],
|
|
||||||
secret_key_base: secret_key_base
|
secret_key_base: secret_key_base
|
||||||
|
|
||||||
config :pinchflat, :logger, [
|
config :pinchflat, :logger, [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
ARG ELIXIR_VERSION=1.17.0
|
ARG ELIXIR_VERSION=1.16.2
|
||||||
ARG OTP_VERSION=26.2.5
|
ARG OTP_VERSION=26.2.2
|
||||||
ARG DEBIAN_VERSION=bookworm-20240612-slim
|
ARG DEBIAN_VERSION=bookworm-20240130
|
||||||
ARG DEV_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
ARG DEV_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
||||||
|
|
||||||
FROM ${DEV_IMAGE}
|
FROM ${DEV_IMAGE}
|
||||||
@@ -53,6 +53,9 @@ ENV LC_ALL en_US.UTF-8
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
|
# Needs permissions to be updated AFTER the copy step
|
||||||
|
RUN chmod +x ./docker-run.dev.sh
|
||||||
|
|
||||||
# Install Elixir deps
|
# Install Elixir deps
|
||||||
# RUN mix archive.install github hexpm/hex branch latest
|
# RUN mix archive.install github hexpm/hex branch latest
|
||||||
RUN mix deps.get
|
RUN mix deps.get
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
phx:
|
phx:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./docker/dev.Dockerfile
|
dockerfile: dev.Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- MIX_ENV=test
|
- MIX_ENV=test
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+4
-4
@@ -1,14 +1,14 @@
|
|||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
phx:
|
phx:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./docker/dev.Dockerfile
|
dockerfile: dev.Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/app'
|
- '.:/app'
|
||||||
ports:
|
ports:
|
||||||
- '4008:4008'
|
- '4008:4008'
|
||||||
command: bash -c "chmod +x docker/docker-run.dev.sh && docker/docker-run.dev.sh"
|
command:
|
||||||
|
- ./docker-run.dev.sh
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
defmodule Pinchflat.Downloading.CodecParser do
|
||||||
|
@moduledoc """
|
||||||
|
Functions for generating yt-dlp codec strings
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a video codec string based on the value of the video_codec_preference setting.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def generate_vcodec_string_from_settings do
|
||||||
|
generate_vcodec_string(Settings.get!(:video_codec_preference))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate an audio codec string based on the value of the audio_codec_preference setting.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def generate_acodec_string_from_settings do
|
||||||
|
generate_acodec_string(Settings.get!(:audio_codec_preference))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a video codec string from a list of video codecs.
|
||||||
|
|
||||||
|
If the list is nil or empty, the default video codec is AVC.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def generate_vcodec_string(nil), do: "bestvideo[vcodec~='^avc']/bestvideo"
|
||||||
|
def generate_vcodec_string([]), do: generate_vcodec_string(nil)
|
||||||
|
|
||||||
|
def generate_vcodec_string(video_codecs) do
|
||||||
|
video_codecs
|
||||||
|
|> Enum.map(&video_codec_map()[&1])
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.map(&"bestvideo[vcodec~='^#{&1}']")
|
||||||
|
|> Enum.concat(["bestvideo"])
|
||||||
|
|> Enum.join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate an audio codec string from a list of audio codecs.
|
||||||
|
|
||||||
|
If the list is nil or empty, the default audio codec is MP4A.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def generate_acodec_string(nil), do: "bestaudio[acodec~='^mp4a']/bestaudio"
|
||||||
|
def generate_acodec_string([]), do: generate_acodec_string(nil)
|
||||||
|
|
||||||
|
def generate_acodec_string(audio_codecs) do
|
||||||
|
audio_codecs
|
||||||
|
|> Enum.map(&audio_codec_map()[&1])
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.map(&"bestaudio[acodec~='^#{&1}']")
|
||||||
|
|> Enum.concat(["bestaudio"])
|
||||||
|
|> Enum.join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def video_codec_map do
|
||||||
|
%{
|
||||||
|
"av01" => "av01",
|
||||||
|
"avc" => "avc",
|
||||||
|
"vp9" => "vp0?9"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def audio_codec_map do
|
||||||
|
%{
|
||||||
|
"aac" => "aac",
|
||||||
|
"mp4a" => "mp4a",
|
||||||
|
"mp3" => "mp3",
|
||||||
|
"opus" => "opus"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,9 +4,9 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.Downloading.CodecParser
|
||||||
alias Pinchflat.Downloading.OutputPathBuilder
|
alias Pinchflat.Downloading.OutputPathBuilder
|
||||||
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||||
@@ -122,13 +122,13 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp quality_options(media_profile) do
|
defp quality_options(media_profile) do
|
||||||
vcodec = Settings.get!(:video_codec_preference)
|
vcodec_string = CodecParser.generate_vcodec_string_from_settings()
|
||||||
acodec = Settings.get!(:audio_codec_preference)
|
acodec_string = CodecParser.generate_acodec_string_from_settings()
|
||||||
|
|
||||||
case media_profile.preferred_resolution do
|
case media_profile.preferred_resolution do
|
||||||
# Also be aware that :audio disabled all embedding options for subtitles
|
# Also be aware that :audio disabled all embedding options for subtitles
|
||||||
:audio ->
|
:audio ->
|
||||||
[:extract_audio, format_sort: "+acodec:#{acodec}"]
|
[:extract_audio, format: "#{acodec_string}/best"]
|
||||||
|
|
||||||
resolution_atom ->
|
resolution_atom ->
|
||||||
{resolution_string, _} =
|
{resolution_string, _} =
|
||||||
@@ -137,9 +137,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
|> Integer.parse()
|
|> Integer.parse()
|
||||||
|
|
||||||
[
|
[
|
||||||
|
format_sort: "res:#{resolution_string}",
|
||||||
# Since Plex doesn't support reading metadata from MKV
|
# Since Plex doesn't support reading metadata from MKV
|
||||||
remux_video: "mp4",
|
remux_video: "mp4",
|
||||||
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}"
|
format: "((#{vcodec_string})+(#{acodec_string}))/best"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
|
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts tasks for downloading media for any of a sources _pending_ media items.
|
Starts tasks for downloading media for any of a sources _pending_ media items.
|
||||||
Jobs are not enqueued if the source is set to not download media. This will return :ok.
|
Jobs are not enqueued if the source is set to not download media. This will return :ok.
|
||||||
|
|
||||||
|
You can optionally set the `kickoff_delay` option to delay when the jobs are enqueued.
|
||||||
|
|
||||||
NOTE: this starts a download for each media item that is pending,
|
NOTE: this starts a download for each media item that is pending,
|
||||||
not just the ones that were indexed in this job run. This should ensure
|
not just the ones that were indexed in this job run. This should ensure
|
||||||
that any stragglers are caught if, for some reason, they weren't enqueued
|
that any stragglers are caught if, for some reason, they weren't enqueued
|
||||||
@@ -27,13 +31,17 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
|
|
||||||
Returns :ok
|
Returns :ok
|
||||||
"""
|
"""
|
||||||
def enqueue_pending_download_tasks(%Source{download_media: true} = source) do
|
def enqueue_pending_download_tasks(source, opts \\ [])
|
||||||
|
|
||||||
|
def enqueue_pending_download_tasks(%Source{download_media: true} = source, opts) do
|
||||||
|
kickoff_delay = Keyword.get(opts, :kickoff_delay, 0)
|
||||||
|
|
||||||
source
|
source
|
||||||
|> Media.list_pending_media_items_for()
|
|> Media.list_pending_media_items_for()
|
||||||
|> Enum.each(&MediaDownloadWorker.kickoff_with_task/1)
|
|> Enum.each(&MediaDownloadWorker.kickoff_with_task(&1, %{}, schedule_in: kickoff_delay))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_pending_download_tasks(%Source{download_media: false}) do
|
def enqueue_pending_download_tasks(%Source{download_media: false}, _opts) do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -53,15 +61,18 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
downloaded, based on the source's download settings and whether media is
|
downloaded, based on the source's download settings and whether media is
|
||||||
considered pending.
|
considered pending.
|
||||||
|
|
||||||
|
You can optionally set the `kickoff_delay` option to delay when the jobs are enqueued.
|
||||||
|
|
||||||
Returns {:ok, %Task{}} | {:error, :should_not_download} | {:error, any()}
|
Returns {:ok, %Task{}} | {:error, :should_not_download} | {:error, any()}
|
||||||
"""
|
"""
|
||||||
def kickoff_download_if_pending(%MediaItem{} = media_item) do
|
def kickoff_download_if_pending(%MediaItem{} = media_item, opts \\ []) do
|
||||||
|
kickoff_delay = Keyword.get(opts, :kickoff_delay, 0)
|
||||||
media_item = Repo.preload(media_item, :source)
|
media_item = Repo.preload(media_item, :source)
|
||||||
|
|
||||||
if media_item.source.download_media && Media.pending_download?(media_item) do
|
if media_item.source.download_media && Media.pending_download?(media_item) do
|
||||||
Logger.info("Kicking off download for media item ##{media_item.id} (#{media_item.media_id})")
|
Logger.info("Kicking off download for media item ##{media_item.id} (#{media_item.media_id})")
|
||||||
|
|
||||||
MediaDownloadWorker.kickoff_with_task(media_item)
|
MediaDownloadWorker.kickoff_with_task(media_item, %{}, schedule_in: kickoff_delay)
|
||||||
else
|
else
|
||||||
{:error, :should_not_download}
|
{:error, :should_not_download}
|
||||||
end
|
end
|
||||||
@@ -98,4 +109,32 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Enum.map(&MediaDownloadWorker.kickoff_with_task/1)
|
|> Enum.map(&MediaDownloadWorker.kickoff_with_task/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a media item from the attributes returned by the video backend
|
||||||
|
(read: yt-dlp) and runs the user script with a `media_indexed` event type.
|
||||||
|
|
||||||
|
Only runs the user script if the media item was created successfully and the media item
|
||||||
|
doesn't already exist in the database.
|
||||||
|
|
||||||
|
Returns {:ok, %MediaItem{}} | {:error, any()}
|
||||||
|
"""
|
||||||
|
def create_media_item_and_run_script(%Source{} = source, media_attrs_struct) do
|
||||||
|
media_already_exists =
|
||||||
|
MediaQuery.new()
|
||||||
|
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.media_id(media_attrs_struct.media_id)))
|
||||||
|
|> Repo.exists?()
|
||||||
|
|
||||||
|
case Media.create_media_item_from_backend_attrs(source, media_attrs_struct) do
|
||||||
|
{:ok, media_item} ->
|
||||||
|
if !media_already_exists do
|
||||||
|
UserScriptRunner.run(:media_indexed, media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, media_item}
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
# IDEA: might be worth kicking off a job for this since thumbnail fetching
|
# IDEA: might be worth kicking off a job for this since thumbnail fetching
|
||||||
# could fail and I want to handle that in isolation
|
# could fail and I want to handle that in isolation
|
||||||
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_with_preloads, parsed_json),
|
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_with_preloads, parsed_json),
|
||||||
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_with_preloads)
|
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_with_preloads, parsed_json)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,14 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
use Pinchflat.Media.MediaQuery
|
use Pinchflat.Media.MediaQuery
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.YoutubeRss
|
alias Pinchflat.FastIndexing.YoutubeRss
|
||||||
alias Pinchflat.FastIndexing.YoutubeApi
|
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches new media IDs for a source from YT's API or RSS, indexes them, and kicks off downloading
|
Fetches new media IDs from a source's YouTube RSS feed, indexes them, and kicks off downloading
|
||||||
tasks for any pending media items. See comments in `FastIndexingWorker` for more info on the
|
tasks for any pending media items. See comments in `FastIndexingWorker` for more info on the
|
||||||
order of operations and how this fits into the indexing process.
|
order of operations and how this fits into the indexing process.
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
downloaded_.
|
downloaded_.
|
||||||
"""
|
"""
|
||||||
def kickoff_download_tasks_from_youtube_rss_feed(%Source{} = source) do
|
def kickoff_download_tasks_from_youtube_rss_feed(%Source{} = source) do
|
||||||
{:ok, media_ids} = get_recent_media_ids(source)
|
{:ok, media_ids} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
existing_media_items = list_media_items_by_media_id_for(source, media_ids)
|
existing_media_items = list_media_items_by_media_id_for(source, media_ids)
|
||||||
new_media_ids = media_ids -- Enum.map(existing_media_items, & &1.media_id)
|
new_media_ids = media_ids -- Enum.map(existing_media_items, & &1.media_id)
|
||||||
|
|
||||||
@@ -43,22 +41,12 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
# Wait 5s before enqueuing downloads to give the post-indexing user script a chance to run
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source, kickoff_delay: 5)
|
||||||
|
|
||||||
Enum.filter(maybe_new_media_items, & &1)
|
Enum.filter(maybe_new_media_items, & &1)
|
||||||
end
|
end
|
||||||
|
|
||||||
# If possible, use the YouTube API to fetch media IDs. If that fails, fall back to the RSS feed.
|
|
||||||
# If the YouTube API isn't set up, just use the RSS feed.
|
|
||||||
defp get_recent_media_ids(source) do
|
|
||||||
with true <- YoutubeApi.enabled?(),
|
|
||||||
{:ok, media_ids} <- YoutubeApi.get_recent_media_ids(source) do
|
|
||||||
{:ok, media_ids}
|
|
||||||
else
|
|
||||||
_ -> YoutubeRss.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp list_media_items_by_media_id_for(source, media_ids) do
|
defp list_media_items_by_media_id_for(source, media_ids) do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> where(^dynamic([mi], ^MediaQuery.for_source(source) and mi.media_id in ^media_ids))
|
|> where(^dynamic([mi], ^MediaQuery.for_source(source) and mi.media_id in ^media_ids))
|
||||||
@@ -69,8 +57,8 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
url = "https://www.youtube.com/watch?v=#{media_id}"
|
url = "https://www.youtube.com/watch?v=#{media_id}"
|
||||||
|
|
||||||
case YtDlpMedia.get_media_attributes(url) do
|
case YtDlpMedia.get_media_attributes(url) do
|
||||||
{:ok, media_attrs} ->
|
{:ok, media_attrs_struct} ->
|
||||||
Media.create_media_item_from_backend_attrs(source, media_attrs)
|
DownloadingHelpers.create_media_item_and_run_script(source, media_attrs_struct)
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
err
|
err
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
defmodule Pinchflat.FastIndexing.YoutubeApi do
|
|
||||||
@moduledoc """
|
|
||||||
Methods for interacting with the YouTube API for fast indexing
|
|
||||||
"""
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.Sources.Source
|
|
||||||
alias Pinchflat.Utils.FunctionUtils
|
|
||||||
alias Pinchflat.FastIndexing.YoutubeBehaviour
|
|
||||||
|
|
||||||
@behaviour YoutubeBehaviour
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Determines if the YouTube API is enabled for fast indexing by checking
|
|
||||||
if the user has an API key set
|
|
||||||
|
|
||||||
Returns boolean()
|
|
||||||
"""
|
|
||||||
@impl YoutubeBehaviour
|
|
||||||
def enabled?(), do: is_binary(api_key())
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Fetches the recent media IDs from the YouTube API for a given source.
|
|
||||||
|
|
||||||
Returns {:ok, [binary()]} | {:error, binary()}
|
|
||||||
"""
|
|
||||||
@impl YoutubeBehaviour
|
|
||||||
def get_recent_media_ids(%Source{} = source) do
|
|
||||||
api_response =
|
|
||||||
source
|
|
||||||
|> determine_playlist_id()
|
|
||||||
|> do_api_request()
|
|
||||||
|
|
||||||
case api_response do
|
|
||||||
{:ok, parsed_json} -> get_media_ids_from_response(parsed_json)
|
|
||||||
{:error, reason} -> {:error, reason}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# The UC prefix is for channels which won't work with this API endpoint. Swapping
|
|
||||||
# the prefix to UU will get us the playlist that represents the channel's uploads
|
|
||||||
defp determine_playlist_id(%{collection_id: c_id}) do
|
|
||||||
String.replace_prefix(c_id, "UC", "UU")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_api_request(playlist_id) do
|
|
||||||
Logger.debug("Fetching recent media IDs from YouTube API for playlist: #{playlist_id}")
|
|
||||||
|
|
||||||
playlist_id
|
|
||||||
|> construct_api_endpoint()
|
|
||||||
|> http_client().get(accept: "application/json")
|
|
||||||
|> case do
|
|
||||||
{:ok, response} ->
|
|
||||||
Phoenix.json_library().decode(response)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Logger.error("Failed to fetch YouTube API: #{inspect(reason)}")
|
|
||||||
{:error, reason}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_media_ids_from_response(parsed_json) do
|
|
||||||
parsed_json
|
|
||||||
|> Map.get("items", [])
|
|
||||||
|> Enum.map(fn item ->
|
|
||||||
item
|
|
||||||
|> Map.get("contentDetails", %{})
|
|
||||||
|> Map.get("videoId", nil)
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
|> Enum.uniq()
|
|
||||||
|> FunctionUtils.wrap_ok()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp api_key do
|
|
||||||
Settings.get!(:youtube_api_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp construct_api_endpoint(playlist_id) do
|
|
||||||
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
|
||||||
property_type = "contentDetails"
|
|
||||||
max_results = 50
|
|
||||||
|
|
||||||
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{api_key()}"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp http_client do
|
|
||||||
Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
defmodule Pinchflat.FastIndexing.YoutubeBehaviour do
|
|
||||||
@moduledoc """
|
|
||||||
This module defines the behaviour for clients that interface with YouTube
|
|
||||||
for the purpose of fast indexing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Pinchflat.Sources.Source
|
|
||||||
|
|
||||||
@callback enabled?() :: boolean()
|
|
||||||
@callback get_recent_media_ids(%Source{}) :: {:ok, [String.t()]} | {:error, String.t()}
|
|
||||||
end
|
|
||||||
@@ -1,31 +1,18 @@
|
|||||||
defmodule Pinchflat.FastIndexing.YoutubeRss do
|
defmodule Pinchflat.FastIndexing.YoutubeRss do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Methods for interacting with YouTube RSS feeds for fast indexing
|
Methods for interacting with YouTube RSS feeds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.YoutubeBehaviour
|
|
||||||
|
|
||||||
@behaviour YoutubeBehaviour
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Determines if the YouTube RSS feed is enabled for fast indexing. Used to satisfy
|
|
||||||
the `YoutubeBehaviour` behaviour.
|
|
||||||
|
|
||||||
Returns true
|
|
||||||
"""
|
|
||||||
@impl YoutubeBehaviour
|
|
||||||
def enabled?(), do: true
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches the recent media IDs from a YouTube RSS feed for a given source.
|
Fetches the recent media IDs from a YouTube RSS feed for a given source.
|
||||||
|
|
||||||
Returns {:ok, [binary()]} | {:error, binary()}
|
Returns {:ok, [binary()]} | {:error, binary()}
|
||||||
"""
|
"""
|
||||||
@impl YoutubeBehaviour
|
def get_recent_media_ids_from_rss(%Source{} = source) do
|
||||||
def get_recent_media_ids(%Source{} = source) do
|
|
||||||
Logger.debug("Fetching recent media IDs from YouTube RSS feed for source: #{source.collection_id}")
|
Logger.debug("Fetching recent media IDs from YouTube RSS feed for source: #{source.collection_id}")
|
||||||
|
|
||||||
case http_client().get(rss_url_for_source(source)) do
|
case http_client().get(rss_url_for_source(source)) do
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ defmodule Pinchflat.HTTP.HTTPClient do
|
|||||||
"""
|
"""
|
||||||
@impl HTTPBehaviour
|
@impl HTTPBehaviour
|
||||||
def get(url, headers \\ [], opts \\ []) do
|
def get(url, headers \\ [], opts \\ []) do
|
||||||
headers = parse_headers(headers)
|
|
||||||
|
|
||||||
case :httpc.request(:get, {url, headers}, [], opts) do
|
case :httpc.request(:get, {url, headers}, [], opts) do
|
||||||
{:ok, {{_version, 200, _reason_phrase}, _headers, body}} ->
|
{:ok, {{_version, 200, _reason_phrase}, _headers, body}} ->
|
||||||
{:ok, to_string(body)}
|
{:ok, body}
|
||||||
|
|
||||||
{:ok, {{_version, status_code, reason_phrase}, _headers, _body}} ->
|
{:ok, {{_version, status_code, reason_phrase}, _headers, _body}} ->
|
||||||
{:error, "HTTP request failed with status code #{status_code}: #{reason_phrase}"}
|
{:error, "HTTP request failed with status code #{status_code}: #{reason_phrase}"}
|
||||||
@@ -34,8 +32,4 @@ defmodule Pinchflat.HTTP.HTTPClient do
|
|||||||
{:error, "HTTP request failed: #{reason}"}
|
{:error, "HTTP request failed: #{reason}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_headers(headers) do
|
|
||||||
Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
|||||||
@behaviour UserScriptCommandRunner
|
@behaviour UserScriptCommandRunner
|
||||||
|
|
||||||
@event_types [
|
@event_types [
|
||||||
|
:media_indexed,
|
||||||
:media_downloaded,
|
:media_downloaded,
|
||||||
:media_deleted
|
:media_deleted
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -41,10 +41,9 @@ defmodule Pinchflat.Media do
|
|||||||
Returns a list of media_items that are redownloadable based on the redownload delay
|
Returns a list of media_items that are redownloadable based on the redownload delay
|
||||||
of the media_profile their source belongs to.
|
of the media_profile their source belongs to.
|
||||||
|
|
||||||
The logic is that a media_item is past_redownload_delay if the media_item's uploaded_at is
|
The logic is that a media_item is past_redownload_delay if the media_item's
|
||||||
at least redownload_delay_days ago AND `media_downloaded_at` - `redownload_delay_days`
|
upload_date is at least redownload_delay_days ago AND
|
||||||
is before the media_item's `uploaded_at`.
|
`media_downloaded_at` - `redownload_delay_days` is before the media_item's `upload_date`.
|
||||||
|
|
||||||
This logic grabs media that we've recently downloaded AND is recently uploaded, but
|
This logic grabs media that we've recently downloaded AND is recently uploaded, but
|
||||||
doesn't grab media that we've recently downloaded and was uploaded a long time ago.
|
doesn't grab media that we've recently downloaded and was uploaded a long time ago.
|
||||||
This also makes things work as expected when downloading media from a source for the
|
This also makes things work as expected when downloading media from a source for the
|
||||||
@@ -136,7 +135,7 @@ defmodule Pinchflat.Media do
|
|||||||
|
|
||||||
Unlike `create_media_item`, this will attempt an update if the media_item
|
Unlike `create_media_item`, this will attempt an update if the media_item
|
||||||
already exists. This is so that future indexing can pick up attributes that
|
already exists. This is so that future indexing can pick up attributes that
|
||||||
we may not have asked for in the past (eg: uploaded_at)
|
we may not have asked for in the past (eg: upload_date)
|
||||||
|
|
||||||
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}
|
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
:livestream,
|
:livestream,
|
||||||
:source_id,
|
:source_id,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:uploaded_at,
|
:upload_date,
|
||||||
:upload_date_index,
|
:upload_date_index,
|
||||||
:duration_seconds,
|
:duration_seconds,
|
||||||
# these fields are captured only on download
|
# these fields are captured only on download
|
||||||
@@ -51,7 +51,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
livestream
|
livestream
|
||||||
media_id
|
media_id
|
||||||
source_id
|
source_id
|
||||||
uploaded_at
|
upload_date
|
||||||
short_form_content
|
short_form_content
|
||||||
)a
|
)a
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
field :short_form_content, :boolean, default: false
|
field :short_form_content, :boolean, default: false
|
||||||
field :media_downloaded_at, :utc_datetime
|
field :media_downloaded_at, :utc_datetime
|
||||||
field :media_redownloaded_at, :utc_datetime
|
field :media_redownloaded_at, :utc_datetime
|
||||||
field :uploaded_at, :utc_datetime
|
field :upload_date, :date
|
||||||
field :upload_date_index, :integer, default: 0
|
field :upload_date_index, :integer, default: 0
|
||||||
field :duration_seconds, :integer
|
field :duration_seconds, :integer
|
||||||
|
|
||||||
@@ -130,31 +130,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
~w(__meta__ __struct__ metadata tasks media_items_search_index)a
|
~w(__meta__ __struct__ metadata tasks media_items_search_index)a
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run it on new records no matter what. The method we delegate to
|
defp update_upload_date_index(%{changes: changes} = changeset) when is_map_key(changes, :upload_date) do
|
||||||
# will handle the case where `uploaded_at` is `nil`
|
|
||||||
defp update_upload_date_index(%{data: %{id: nil}} = changeset) do
|
|
||||||
do_update_upload_date_index(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
# For the update case, we only want to recalculate if the day itself has changed.
|
|
||||||
# For instance, this is useful in the migration from `upload_date` to `uploaded_at`
|
|
||||||
defp update_upload_date_index(%{changes: changes} = changeset) when is_map_key(changes, :uploaded_at) do
|
|
||||||
old_uploaded_at = changeset.data.uploaded_at
|
|
||||||
new_uploaded_at = get_change(changeset, :uploaded_at)
|
|
||||||
upload_dates_match = DateTime.to_date(old_uploaded_at) == DateTime.to_date(new_uploaded_at)
|
|
||||||
|
|
||||||
if upload_dates_match do
|
|
||||||
changeset
|
|
||||||
else
|
|
||||||
do_update_upload_date_index(changeset)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# If the record is persisted and the `uploaded_at` field is not being changed,
|
|
||||||
# we don't need to recalculate the index.
|
|
||||||
defp update_upload_date_index(changeset), do: changeset
|
|
||||||
|
|
||||||
defp do_update_upload_date_index(%{changes: changes} = changeset) when is_map_key(changes, :uploaded_at) do
|
|
||||||
source_id = get_field(changeset, :source_id)
|
source_id = get_field(changeset, :source_id)
|
||||||
source = Sources.get_source!(source_id)
|
source = Sources.get_source!(source_id)
|
||||||
# Channels should count down from 99, playlists should count up from 0
|
# Channels should count down from 99, playlists should count up from 0
|
||||||
@@ -166,7 +142,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
|
|
||||||
current_max =
|
current_max =
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> where(^dynamic([mi], ^MediaQuery.upload_date_matches(changes.uploaded_at) and ^MediaQuery.for_source(source)))
|
|> where(^dynamic([mi], mi.upload_date == ^changes.upload_date and ^MediaQuery.for_source(source)))
|
||||||
|> Repo.aggregate(aggregator, :upload_date_index)
|
|> Repo.aggregate(aggregator, :upload_date_index)
|
||||||
|
|
||||||
case current_max do
|
case current_max do
|
||||||
@@ -175,7 +151,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_update_upload_date_index(changeset), do: changeset
|
defp update_upload_date_index(changeset), do: changeset
|
||||||
|
|
||||||
defimpl Jason.Encoder, for: MediaItem do
|
defimpl Jason.Encoder, for: MediaItem do
|
||||||
def encode(value, opts) do
|
def encode(value, opts) do
|
||||||
|
|||||||
@@ -35,14 +35,11 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
def culling_prevented, do: dynamic([mi], mi.prevent_culling == true)
|
def culling_prevented, do: dynamic([mi], mi.prevent_culling == true)
|
||||||
def culled, do: dynamic([mi], not is_nil(mi.culled_at))
|
def culled, do: dynamic([mi], not is_nil(mi.culled_at))
|
||||||
def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at))
|
def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at))
|
||||||
def upload_date_matches(other_date), do: dynamic([mi], fragment("date(?) = date(?)", mi.uploaded_at, ^other_date))
|
def media_id(nil), do: dynamic(false)
|
||||||
|
def media_id(media_id), do: dynamic([mi], mi.media_id == ^media_id)
|
||||||
|
|
||||||
def upload_date_after_source_cutoff do
|
def upload_date_after_source_cutoff do
|
||||||
dynamic(
|
dynamic([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date)
|
||||||
[mi, source],
|
|
||||||
is_nil(source.download_cutoff_date) or
|
|
||||||
fragment("date(?) >= ?", mi.uploaded_at, source.download_cutoff_date)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_matching_profile_preference do
|
def format_matching_profile_preference do
|
||||||
@@ -89,12 +86,12 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
def past_redownload_delay do
|
def past_redownload_delay do
|
||||||
dynamic(
|
dynamic(
|
||||||
[mi, source, media_profile],
|
[mi, source, media_profile],
|
||||||
# Returns media items where the uploaded_at is at least redownload_delay_days ago AND
|
# Returns media items where the upload_date is at least redownload_delay_days ago AND
|
||||||
# downloaded_at minus the redownload_delay_days is before the upload date
|
# downloaded_at minus the redownload_delay_days is before the upload date
|
||||||
fragment("""
|
fragment("""
|
||||||
IFNULL(redownload_delay_days, 0) > 0 AND
|
IFNULL(redownload_delay_days, 0) > 0 AND
|
||||||
DATETIME('now', '-' || redownload_delay_days || ' day') > uploaded_at AND
|
DATETIME('now', '-' || redownload_delay_days || ' day') > upload_date AND
|
||||||
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < uploaded_at
|
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < upload_date
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the directory where metadata for a database record should be stored.
|
Returns the directory where metadata for a database record should be stored.
|
||||||
|
|
||||||
@@ -49,42 +47,55 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
Returns {:ok, map()} | {:error, any}
|
Returns {:ok, map()} | {:error, any}
|
||||||
"""
|
"""
|
||||||
def read_compressed_metadata(filepath) do
|
def read_compressed_metadata(filepath) do
|
||||||
{:ok, json} = File.open(filepath, [:read, :compressed], &IO.read(&1, :eof))
|
{:ok, json} = File.open(filepath, [:read, :compressed], &IO.read(&1, :all))
|
||||||
|
|
||||||
Phoenix.json_library().decode(json)
|
Phoenix.json_library().decode(json)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Downloads and stores a thumbnail for a media item, returning the filepath.
|
Downloads and stores a thumbnail for a media item, returning the filepath.
|
||||||
Chooses the highest quality thumbnail available and converts it to a JPG
|
Chooses the highest quality thumbnail available (preferring jpg). Returns
|
||||||
|
nil if no thumbnails are available.
|
||||||
Returns nil if no thumbnail is available or if yt-dlp encounters an error
|
|
||||||
|
|
||||||
Returns binary() | nil
|
Returns binary() | nil
|
||||||
"""
|
"""
|
||||||
def download_and_store_thumbnail_for(database_record) do
|
def download_and_store_thumbnail_for(database_record, metadata_map) do
|
||||||
yt_dlp_filepath = generate_filepath_for(database_record, "thumbnail.%(ext)s")
|
thumbnails =
|
||||||
real_filepath = generate_filepath_for(database_record, "thumbnail.jpg")
|
(metadata_map["thumbnails"] || [])
|
||||||
|
# Give it a low preference if the `preference` key doesn't exist
|
||||||
|
|> Enum.map(&Map.put_new(&1, "preference", -1000))
|
||||||
|
# Give it a low preference if image isn't a jpg
|
||||||
|
|> Enum.map(fn t ->
|
||||||
|
preference_weight = if String.ends_with?(t["url"], ".jpg"), do: t["preference"], else: t["preference"] - 1000
|
||||||
|
|
||||||
case YtDlpMedia.download_thumbnail(database_record.original_url, output: yt_dlp_filepath) do
|
Map.put(t, "preference", preference_weight)
|
||||||
{:ok, _} -> real_filepath
|
end)
|
||||||
_ -> nil
|
|
||||||
|
case Enum.sort_by(thumbnails, & &1["preference"], :desc) do
|
||||||
|
[thumbnail_map | _] ->
|
||||||
|
thumbnail_url = thumbnail_map["url"]
|
||||||
|
filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url))
|
||||||
|
thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url)
|
||||||
|
|
||||||
|
:ok = FilesystemUtils.write_p!(filepath, thumbnail_blob)
|
||||||
|
|
||||||
|
filepath
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Parses an upload date from the YYYYMMDD string returned in yt-dlp metadata
|
Parses an upload date from the YYYYMMDD string returned in yt-dlp metadata
|
||||||
and returns a DateTime struct, appending a time of 00:00:00Z.
|
and returns a Date struct.
|
||||||
|
|
||||||
Returns DateTime.t()
|
Returns Date.t()
|
||||||
"""
|
"""
|
||||||
def parse_upload_date(upload_date) do
|
def parse_upload_date(upload_date) do
|
||||||
<<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date
|
<<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date
|
||||||
|
|
||||||
case DateTime.from_iso8601("#{year}-#{month}-#{day}T00:00:00Z") do
|
Date.from_iso8601!("#{year}-#{month}-#{day}")
|
||||||
{:ok, datetime, _} -> datetime
|
|
||||||
_ -> raise "Invalid upload date: #{upload_date}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -124,6 +135,13 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fetch_thumbnail_from_url(url) do
|
||||||
|
http_client = Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient)
|
||||||
|
{:ok, body} = http_client.get(url, [], body_format: :binary)
|
||||||
|
|
||||||
|
body
|
||||||
|
end
|
||||||
|
|
||||||
defp generate_filepath_for(database_record, filename) do
|
defp generate_filepath_for(database_record, filename) do
|
||||||
Path.join([
|
Path.join([
|
||||||
metadata_directory_for(database_record),
|
metadata_directory_for(database_record),
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ defmodule Pinchflat.Metadata.MetadataParser do
|
|||||||
and not have it, ya know?
|
and not have it, ya know?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Parses the given JSON response from yt-dlp and returns a map of
|
Parses the given JSON response from yt-dlp and returns a map of
|
||||||
the needful media_item attributes, along with anything needed for
|
the needful media_item attributes, along with anything needed for
|
||||||
@@ -26,12 +24,15 @@ defmodule Pinchflat.Metadata.MetadataParser do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp parse_media_metadata(metadata) do
|
defp parse_media_metadata(metadata) do
|
||||||
Map.merge(
|
|
||||||
Map.from_struct(YtDlpMedia.response_to_struct(metadata)),
|
|
||||||
%{
|
%{
|
||||||
media_filepath: metadata["filepath"]
|
media_id: metadata["id"],
|
||||||
|
title: metadata["title"],
|
||||||
|
original_url: metadata["original_url"],
|
||||||
|
description: metadata["description"],
|
||||||
|
media_filepath: metadata["filepath"],
|
||||||
|
livestream: !!metadata["was_live"],
|
||||||
|
duration_seconds: metadata["duration"] && round(metadata["duration"])
|
||||||
}
|
}
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_subtitle_metadata(metadata) do
|
defp parse_subtitle_metadata(metadata) do
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
|
|||||||
|
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|
||||||
|> order_by(desc: :uploaded_at)
|
|> order_by(desc: :upload_date)
|
||||||
|> Repo.maybe_limit(limit)
|
|> Repo.maybe_limit(limit)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
|
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
|
|
||||||
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.DatetimeUtils
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
alias PinchflatWeb.Router.Helpers, as: Routes
|
alias PinchflatWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
<title>#{safe(media_item.title)}</title>
|
<title>#{safe(media_item.title)}</title>
|
||||||
<link>#{safe(media_item.original_url)}</link>
|
<link>#{safe(media_item.original_url)}</link>
|
||||||
<description>#{safe(media_item.description)}</description>
|
<description>#{safe(media_item.description)}</description>
|
||||||
<pubDate>#{Calendar.strftime(media_item.uploaded_at, @datetime_format)}</pubDate>
|
<pubDate>#{generate_upload_date(media_item)}</pubDate>
|
||||||
<itunes:duration>#{media_item.duration_seconds}</itunes:duration>
|
<itunes:duration>#{media_item.duration_seconds}</itunes:duration>
|
||||||
<enclosure
|
<enclosure
|
||||||
url="#{media_stream_path(url_base, media_item)}"
|
url="#{media_stream_path(url_base, media_item)}"
|
||||||
@@ -132,6 +133,12 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp generate_upload_date(media_item) do
|
||||||
|
media_item.upload_date
|
||||||
|
|> DatetimeUtils.date_to_datetime()
|
||||||
|
|> Calendar.strftime(@datetime_format)
|
||||||
|
end
|
||||||
|
|
||||||
defp podcast_route(action, params) do
|
defp podcast_route(action, params) do
|
||||||
Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
|
Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
:apprise_version,
|
:apprise_version,
|
||||||
:apprise_server,
|
:apprise_server,
|
||||||
:video_codec_preference,
|
:video_codec_preference,
|
||||||
:audio_codec_preference,
|
:audio_codec_preference
|
||||||
:youtube_api_key
|
]
|
||||||
|
|
||||||
|
@virtual_fields [
|
||||||
|
:video_codec_preference_string,
|
||||||
|
:audio_codec_preference_string
|
||||||
]
|
]
|
||||||
|
|
||||||
@required_fields ~w(
|
@required_fields ~w(
|
||||||
onboarding
|
onboarding
|
||||||
pro_enabled
|
pro_enabled
|
||||||
video_codec_preference
|
|
||||||
audio_codec_preference
|
|
||||||
)a
|
)a
|
||||||
|
|
||||||
schema "settings" do
|
schema "settings" do
|
||||||
@@ -30,16 +32,43 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
field :yt_dlp_version, :string
|
field :yt_dlp_version, :string
|
||||||
field :apprise_version, :string
|
field :apprise_version, :string
|
||||||
field :apprise_server, :string
|
field :apprise_server, :string
|
||||||
field :youtube_api_key, :string
|
|
||||||
|
|
||||||
field :video_codec_preference, :string
|
field :video_codec_preference, {:array, :string}, default: []
|
||||||
field :audio_codec_preference, :string
|
field :audio_codec_preference, {:array, :string}, default: []
|
||||||
|
field :video_codec_preference_string, :string, default: nil, virtual: true
|
||||||
|
field :audio_codec_preference_string, :string, default: nil, virtual: true
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(setting, attrs) do
|
def changeset(setting, attrs) do
|
||||||
setting
|
setting
|
||||||
|> cast(attrs, @allowed_fields)
|
|> cast(attrs, @allowed_fields)
|
||||||
|
|> cast(attrs, @virtual_fields, empty_values: [])
|
||||||
|
|> convert_codec_preference_strings()
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp convert_codec_preference_strings(changeset) do
|
||||||
|
fields = [
|
||||||
|
video_codec_preference_string: :video_codec_preference,
|
||||||
|
audio_codec_preference_string: :audio_codec_preference
|
||||||
|
]
|
||||||
|
|
||||||
|
Enum.reduce(fields, changeset, fn {virtual_field, actual_field}, changeset ->
|
||||||
|
case get_change(changeset, virtual_field) do
|
||||||
|
nil ->
|
||||||
|
changeset
|
||||||
|
|
||||||
|
value ->
|
||||||
|
new_value =
|
||||||
|
value
|
||||||
|
|> String.split(">")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.reject(&(String.trim(&1) == ""))
|
||||||
|
|> Enum.map(&String.downcase/1)
|
||||||
|
|
||||||
|
put_change(changeset, actual_field, new_value)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
@@ -39,6 +38,9 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
item belonging to the source. You can't tell me the method name isn't descriptive!
|
item belonging to the source. You can't tell me the method name isn't descriptive!
|
||||||
Returns a list of media items or changesets (if the media item couldn't be created).
|
Returns a list of media items or changesets (if the media item couldn't be created).
|
||||||
|
|
||||||
|
For each new media item, the method will also run a user script with the `media_indexed`
|
||||||
|
event, if the script is present.
|
||||||
|
|
||||||
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
|
||||||
so we can create media items as they come in. This parallelizes the process and adds
|
so we can create media items as they come in. This parallelizes the process and adds
|
||||||
@@ -64,15 +66,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
source = Repo.reload!(source)
|
source = Repo.reload!(source)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
Enum.map(media_attributes, fn media_attrs ->
|
Enum.map(media_attributes, fn media_attrs_struct ->
|
||||||
case Media.create_media_item_from_backend_attrs(source, media_attrs) do
|
case DownloadingHelpers.create_media_item_and_run_script(source, media_attrs_struct) do
|
||||||
{:ok, media_item} -> media_item
|
{:ok, media_item} -> media_item
|
||||||
{:error, changeset} -> changeset
|
{:error, changeset} -> changeset
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Sources.update_source(source, %{last_indexed_at: DateTime.utc_now()})
|
Sources.update_source(source, %{last_indexed_at: DateTime.utc_now()})
|
||||||
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
# Wait 5s before enqueuing downloads to give the post-indexing user script a chance to run
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source, kickoff_delay: 5)
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
@@ -118,14 +121,15 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_media_item_and_enqueue_download(source, media_attrs) do
|
defp create_media_item_and_enqueue_download(source, media_attrs_struct) do
|
||||||
# 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)
|
||||||
|
|
||||||
case Media.create_media_item_from_backend_attrs(source, media_attrs) do
|
case DownloadingHelpers.create_media_item_and_run_script(source, media_attrs_struct) do
|
||||||
{:ok, %MediaItem{} = media_item} ->
|
{:ok, %MediaItem{} = media_item} ->
|
||||||
DownloadingHelpers.kickoff_download_if_pending(media_item)
|
# Wait 5s before enqueuing downloads to give the post-indexing user script a chance to run
|
||||||
|
DownloadingHelpers.kickoff_download_if_pending(media_item, kickoff_delay: 5)
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
changeset
|
changeset
|
||||||
|
|||||||
@@ -112,11 +112,9 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
|> dynamic_default(:custom_name, fn cs -> get_field(cs, :collection_name) end)
|
|> dynamic_default(:custom_name, fn cs -> get_field(cs, :collection_name) end)
|
||||||
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
||||||
|> validate_required(required_fields)
|
|> validate_required(required_fields)
|
||||||
|> validate_title_regex()
|
|
||||||
|> validate_number(:retention_period_days, greater_than_or_equal_to: 0)
|
|> validate_number(:retention_period_days, greater_than_or_equal_to: 0)
|
||||||
# Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room)
|
# Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room)
|
||||||
|> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}")
|
|> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}")
|
||||||
|> validate_format(:original_url, youtube_channel_or_playlist_regex(), message: "must be a channel or playlist URL")
|
|
||||||
|> cast_assoc(:metadata, with: &SourceMetadata.changeset/2, required: false)
|
|> cast_assoc(:metadata, with: &SourceMetadata.changeset/2, required: false)
|
||||||
|> unique_constraint([:collection_id, :media_profile_id, :title_filter_regex], error_key: :original_url)
|
|> unique_constraint([:collection_id, :media_profile_id, :title_filter_regex], error_key: :original_url)
|
||||||
end
|
end
|
||||||
@@ -130,7 +128,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
@doc false
|
@doc false
|
||||||
def fast_index_frequency do
|
def fast_index_frequency do
|
||||||
# minutes
|
# minutes
|
||||||
10
|
15
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@@ -143,22 +141,6 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
~w(__meta__ __struct__ metadata tasks media_items)a
|
~w(__meta__ __struct__ metadata tasks media_items)a
|
||||||
end
|
end
|
||||||
|
|
||||||
def youtube_channel_or_playlist_regex do
|
|
||||||
# Validate that the original URL is not a video URL
|
|
||||||
# Also matches if the string does NOT contain youtube.com or youtu.be. This preserves my tenuous support
|
|
||||||
# for non-youtube sources.
|
|
||||||
~r<^(?:(?!youtube\.com/(watch|shorts|embed)|youtu\.be).)*$>
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_title_regex(%{changes: %{title_filter_regex: regex}} = changeset) when is_binary(regex) do
|
|
||||||
case Ecto.Adapters.SQL.query(Repo, "SELECT regexp_like('', ?)", [regex]) do
|
|
||||||
{:ok, _} -> changeset
|
|
||||||
_ -> add_error(changeset, :title_filter_regex, "is invalid")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_title_regex(changeset), do: changeset
|
|
||||||
|
|
||||||
defimpl Jason.Encoder, for: Source do
|
defimpl Jason.Encoder, for: Source do
|
||||||
def encode(value, opts) do
|
def encode(value, opts) do
|
||||||
value
|
value
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
defmodule Pinchflat.Utils.DatetimeUtils do
|
||||||
|
@moduledoc """
|
||||||
|
Utility methods for working with dates and datetimes
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Converts a Date to a DateTime
|
||||||
|
|
||||||
|
Returns %DateTime{}
|
||||||
|
"""
|
||||||
|
def date_to_datetime(date) do
|
||||||
|
date
|
||||||
|
|> Date.to_gregorian_days()
|
||||||
|
|> Kernel.*(86_400)
|
||||||
|
|> DateTime.from_gregorian_seconds()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
:original_url,
|
:original_url,
|
||||||
:livestream,
|
:livestream,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:uploaded_at,
|
:upload_date,
|
||||||
:duration_seconds
|
:duration_seconds
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
:original_url,
|
:original_url,
|
||||||
:livestream,
|
:livestream,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:uploaded_at,
|
:upload_date,
|
||||||
:duration_seconds
|
:duration_seconds
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -46,20 +46,6 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Downloads a thumbnail for a single piece of media. Usually used for
|
|
||||||
downloading thumbnails for internal use
|
|
||||||
|
|
||||||
Returns {:ok, ""} | {:error, any, ...}.
|
|
||||||
"""
|
|
||||||
def download_thumbnail(url, command_opts \\ []) do
|
|
||||||
opts = [:no_simulate, :skip_download, :write_thumbnail, convert_thumbnail: "jpg"] ++ command_opts
|
|
||||||
|
|
||||||
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
|
||||||
# we aren't downloading the main media file
|
|
||||||
backend_runner().run(url, opts, "after_move:%()j")
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a map representing the media at the given URL.
|
Returns a map representing the media at the given URL.
|
||||||
|
|
||||||
@@ -86,7 +72,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
Returns the output template for yt-dlp's indexing command.
|
Returns the output template for yt-dlp's indexing command.
|
||||||
"""
|
"""
|
||||||
def indexing_output_template do
|
def indexing_output_template do
|
||||||
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j"
|
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date})j"
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -104,7 +90,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
livestream: !!response["was_live"],
|
livestream: !!response["was_live"],
|
||||||
duration_seconds: response["duration"] && round(response["duration"]),
|
duration_seconds: response["duration"] && round(response["duration"]),
|
||||||
short_form_content: response["webpage_url"] && short_form_content?(response),
|
short_form_content: response["webpage_url"] && short_form_content?(response),
|
||||||
uploaded_at: response["upload_date"] && parse_uploaded_at(response)
|
upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"])
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -124,18 +110,6 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_uploaded_at(%{"timestamp" => ts} = response) when is_number(ts) do
|
|
||||||
case DateTime.from_unix(ts) do
|
|
||||||
{:ok, datetime} -> datetime
|
|
||||||
_ -> MetadataFileHelpers.parse_upload_date(response["upload_date"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# This field is needed before inserting into the database, but absence
|
|
||||||
# of this field should fail at insert-time rather than here
|
|
||||||
defp parse_uploaded_at(%{"upload_date" => nil}), do: nil
|
|
||||||
defp parse_uploaded_at(response), do: MetadataFileHelpers.parse_upload_date(response["upload_date"])
|
|
||||||
|
|
||||||
defp backend_runner do
|
defp backend_runner do
|
||||||
# This approach lets us mock the command for testing
|
# This approach lets us mock the command for testing
|
||||||
Application.get_env(:pinchflat, :yt_dlp_runner)
|
Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
import PinchflatWeb.CoreComponents
|
alias PinchflatWeb.CoreComponents
|
||||||
import PinchflatWeb.CustomComponents.TextComponents
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a table component with the given rows and columns.
|
Renders a table component with the given rows and columns.
|
||||||
@@ -77,13 +76,11 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
phx-click={@page_number != 1 && "page_change"}
|
phx-click={@page_number != 1 && "page_change"}
|
||||||
phx-value-direction="dec"
|
phx-value-direction="dec"
|
||||||
>
|
>
|
||||||
<.icon name="hero-chevron-left" />
|
<CoreComponents.icon name="hero-chevron-left" />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="mx-2">
|
<span class="mx-2">Page <%= @page_number %> of <%= @total_pages %></span>
|
||||||
Page <.localized_number number={@page_number} /> of <.localized_number number={@total_pages} />
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
@@ -95,7 +92,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
phx-click={@page_number != @total_pages && "page_change"}
|
phx-click={@page_number != @total_pages && "page_change"}
|
||||||
phx-value-direction="inc"
|
phx-value-direction="inc"
|
||||||
>
|
>
|
||||||
<.icon name="hero-chevron-right" />
|
<CoreComponents.icon name="hero-chevron-right" />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -94,28 +94,4 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
<time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time>
|
<time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a localized number using the Intl.NumberFormat API, falling back to the raw number if needed
|
|
||||||
"""
|
|
||||||
attr :number, :any, required: true
|
|
||||||
|
|
||||||
def localized_number(assigns) do
|
|
||||||
~H"""
|
|
||||||
<span x-data x-text={"Intl.NumberFormat().format(#{@number})"}><%= @number %></span>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a word with a suffix if the count is not 1
|
|
||||||
"""
|
|
||||||
attr :word, :string, required: true
|
|
||||||
attr :count, :integer, required: true
|
|
||||||
attr :suffix, :string, default: "s"
|
|
||||||
|
|
||||||
def pluralize(assigns) do
|
|
||||||
~H"""
|
|
||||||
<%= @word %><%= if @count == 1, do: "", else: @suffix %>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
onboarding: #{Settings.get!(:onboarding)}
|
onboarding: #{Settings.get!(:onboarding)}
|
||||||
}"}
|
}"}
|
||||||
class="dark text-bodydark bg-boxdark-2"
|
class="dark text-bodydark bg-boxdark-2"
|
||||||
data-socket-path={Path.join(Application.get_env(:pinchflat, :base_route_path), "/live")}
|
|
||||||
>
|
>
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<.media_preview media_item={@media_item} />
|
<.media_preview media_item={@media_item} />
|
||||||
</div>
|
</div>
|
||||||
<aside class="mt-4 xl:mt-0">
|
<aside class="mt-4 xl:mt-0">
|
||||||
<div>Uploaded: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
|
<div>Uploaded: <%= @media_item.upload_date %></div>
|
||||||
<div>
|
<div>
|
||||||
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
||||||
<.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link>
|
<.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :if={@media_item.description} class="mt-4 text-bodydark">
|
<div class="mt-4 text-bodydark">
|
||||||
<.break_on_newline text={@media_item.description} />
|
<.break_on_newline text={@media_item.description} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -5,19 +5,9 @@ defmodule PinchflatWeb.Pages.PageHTML do
|
|||||||
|
|
||||||
embed_templates "page_html/*"
|
embed_templates "page_html/*"
|
||||||
|
|
||||||
attr :media_filesize, :integer, required: true
|
def readable_media_filesize(media_filesize) do
|
||||||
|
{num, suffix} = NumberUtils.human_byte_size(media_filesize, precision: 1)
|
||||||
|
|
||||||
def readable_media_filesize(assigns) do
|
"#{Float.round(num)} #{suffix}"
|
||||||
{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
|
end
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
<div>
|
<div>
|
||||||
<span class="mb-4 flex items-center">
|
<span class="mb-4 flex items-center">
|
||||||
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
||||||
<span class="ml-2">
|
<span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span>
|
||||||
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@total_record_count} />
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
@@ -34,7 +32,7 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
<:col :let={media_item} label="Upload Date">
|
||||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
<%= media_item.upload_date %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Indexed At">
|
<:col :let={media_item} label="Indexed At">
|
||||||
<%= format_datetime(media_item.inserted_at) %>
|
<%= format_datetime(media_item.inserted_at) %>
|
||||||
|
|||||||
@@ -1,39 +1,33 @@
|
|||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||||
<a href={~p"/media_profiles"} class="flex flex-col items-center py-2">
|
<a href={~p"/media_profiles"} class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">
|
<span class="text-md font-medium">Media Profile(s)</span>
|
||||||
Media <.pluralize count={@media_profile_count} word="Profile" />
|
|
||||||
</span>
|
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<.localized_number number={@media_profile_count} />
|
<%= @media_profile_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||||
<a href={~p"/sources"} class="flex flex-col items-center py-2">
|
<a href={~p"/sources"} class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">
|
<span class="text-md font-medium">Source(s)</span>
|
||||||
<.pluralize count={@source_count} word="Source" />
|
|
||||||
</span>
|
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<.localized_number number={@source_count} />
|
<%= @source_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||||
<span class="flex flex-col items-center py-2">
|
<span class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">
|
<span class="text-md font-medium">Downloaded Media</span>
|
||||||
<.pluralize count={@media_item_count} word="Download" />
|
|
||||||
</span>
|
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<.localized_number number={@media_item_count} />
|
<%= @media_item_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||||
<span class="flex flex-col items-center py-2">
|
<span class="mt-4 flex flex-col items-center justify-center">
|
||||||
<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_media_filesize(@media_item_size) %>
|
||||||
</h4>
|
</h4>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
defmodule PinchflatWeb.Settings.SettingHTML do
|
defmodule PinchflatWeb.Settings.SettingHTML do
|
||||||
use PinchflatWeb, :html
|
use PinchflatWeb, :html
|
||||||
|
|
||||||
|
alias Pinchflat.Downloading.CodecParser
|
||||||
|
|
||||||
embed_templates "setting_html/*"
|
embed_templates "setting_html/*"
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -14,14 +16,9 @@ defmodule PinchflatWeb.Settings.SettingHTML do
|
|||||||
|
|
||||||
def apprise_server_help do
|
def apprise_server_help do
|
||||||
url = "https://github.com/caronc/apprise/wiki/URLBasics"
|
url = "https://github.com/caronc/apprise/wiki/URLBasics"
|
||||||
|
classes = "underline decoration-bodydark decoration-1 hover:decoration-white"
|
||||||
|
|
||||||
~s(Server endpoint for Apprise notifications when new media is found. See <a href="#{url}" class="#{help_link_classes()}" target="_blank">Apprise docs</a> for more information)
|
~s(Server endpoint for Apprise notifications when new media is found. See <a href="#{url}" class="#{classes}" target="_blank">Apprise docs</a> for more information)
|
||||||
end
|
|
||||||
|
|
||||||
def youtube_api_help do
|
|
||||||
url = "https://github.com/kieraneglin/pinchflat/wiki/Generating-a-YouTube-API-key"
|
|
||||||
|
|
||||||
~s(API key for YouTube Data API v3. Greatly improves the accuracy of Fast Indexing. See <a href="#{url}" class="#{help_link_classes()}" target="_blank">here</a> for details on generating an API key)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def diagnostic_info_string do
|
def diagnostic_info_string do
|
||||||
@@ -33,8 +30,4 @@ defmodule PinchflatWeb.Settings.SettingHTML do
|
|||||||
- Timezone: #{Application.get_env(:pinchflat, :timezone)}
|
- Timezone: #{Application.get_env(:pinchflat, :timezone)}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp help_link_classes do
|
|
||||||
"underline decoration-bodydark decoration-1 hover:decoration-white"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<aside>
|
<aside>
|
||||||
<h2 class="text-xl font-bold mb-2">Codec Preference</h2>
|
<h2 class="text-xl font-bold mb-2">Codec Preferences</h2>
|
||||||
<section class="ml-2 md:ml-4 mb-2 max-w-prose">
|
<section class="ml-2 md:ml-4 mb-2 max-w-prose">
|
||||||
<p>
|
<section>
|
||||||
Some users may want to specify the audio codec that <code class="text-sm">yt-dlp</code>
|
Available video codecs:
|
||||||
searches for when downloading content. For these users, you can specify one audio and video
|
<ul class="list-disc ml-8">
|
||||||
codec to prefer above all others.
|
<li :for={{codec, _} <- CodecParser.video_codec_map()}><%= codec %></li>
|
||||||
</p>
|
</ul>
|
||||||
<p class="mt-4">
|
</section>
|
||||||
NOTE: this is a <em>soft</em>
|
<section class="mt-4">
|
||||||
preference. If the codec you specify is not available, <code class="text-sm">yt-dlp</code>
|
Available audio codecs:
|
||||||
will download the next best available codec. This is expected and intended behavior.
|
<ul class="list-disc ml-8">
|
||||||
<.inline_link href="https://github.com/yt-dlp/yt-dlp#sorting-formats">See here</.inline_link>
|
<li :for={{codec, _} <- CodecParser.audio_codec_map()}><%= codec %></li>
|
||||||
for more information and a list of available codecs.
|
</ul>
|
||||||
</p>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
Notification Settings
|
Notification Settings
|
||||||
</h3>
|
</h3>
|
||||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||||
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Standard'"></span>
|
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Basic'"></span>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -26,25 +26,7 @@
|
|||||||
) %>
|
) %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-10" x-show="advancedMode">
|
||||||
<section>
|
|
||||||
<h3 class="text-2xl text-black dark:text-white">
|
|
||||||
Indexing Settings
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<.input
|
|
||||||
field={f[:youtube_api_key]}
|
|
||||||
placeholder="ABC123"
|
|
||||||
type="text"
|
|
||||||
label="YouTube API Key"
|
|
||||||
help={youtube_api_help()}
|
|
||||||
html_help={true}
|
|
||||||
inputclass="font-mono text-sm mr-4"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mt-8" x-show="advancedMode">
|
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-2xl text-black dark:text-white">
|
<h3 class="text-2xl text-black dark:text-white">
|
||||||
Codec Options
|
Codec Options
|
||||||
@@ -55,20 +37,24 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:video_codec_preference]}
|
id="video_codec_preference_string"
|
||||||
placeholder="avc"
|
name="setting[video_codec_preference_string]"
|
||||||
|
value={Enum.join(f[:video_codec_preference].value, ">")}
|
||||||
|
placeholder="avc>vp9>av01"
|
||||||
type="text"
|
type="text"
|
||||||
label="Video Codec Preference"
|
label="Video Codec Preference"
|
||||||
help="Video codec preference. Will be remuxed into an MP4 container. See below for more details"
|
help="Order of preference for video codecs. Separate with >. Will be remuxed into an MP4 container. See below for available codecs"
|
||||||
inputclass="font-mono text-sm mr-4"
|
inputclass="font-mono text-sm mr-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:audio_codec_preference]}
|
id="audio_codec_preference_string"
|
||||||
placeholder="m4a"
|
name="setting[audio_codec_preference_string]"
|
||||||
|
value={Enum.join(f[:audio_codec_preference].value, ">")}
|
||||||
|
placeholder="mp4a>opus>aac"
|
||||||
type="text"
|
type="text"
|
||||||
label="Audio Codec Preference"
|
label="Audio Codec Preference"
|
||||||
help="Audio codec preference. See below for more details"
|
help="Order of preference for audio codecs. Separate with >. See below for available codecs"
|
||||||
inputclass="font-mono text-sm mr-4"
|
inputclass="font-mono text-sm mr-4"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
def friendly_index_frequencies do
|
def friendly_index_frequencies do
|
||||||
[
|
[
|
||||||
{"Only once when first created", -1},
|
{"Only once when first created", -1},
|
||||||
{"30 minutes", 30},
|
|
||||||
{"1 Hour", 60},
|
{"1 Hour", 60},
|
||||||
{"3 Hours", 3 * 60},
|
{"3 Hours", 3 * 60},
|
||||||
{"6 Hours", 6 * 60},
|
{"6 Hours", 6 * 60},
|
||||||
|
|||||||
@@ -1,63 +1,21 @@
|
|||||||
<aside>
|
<aside>
|
||||||
<h2 class="text-2xl font-bold mb-2">What is fast indexing?</h2>
|
<h2 class="text-xl font-bold mb-2">What is fast indexing (experimental)?</h2>
|
||||||
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
|
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
|
||||||
<p>
|
<p>
|
||||||
Indexing is the act of scanning a channel or playlist (aka: source) for new media.
|
Indexing is the act of scanning a channel or playlist (aka: source) for new media.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
Normal indexing uses <code class="text-sm">yt-dlp</code>
|
Normal indexing uses <code class="text-sm">yt-dlp</code>
|
||||||
to scan the entire source on your specified frequency, but it's very slow for large sources. This is the most accurate way to find uploaded media with the tradeoff being that pairing a large source that's indexed frequently will result in you spending most of your time indexing. Only so many indexing operations can be running at the same time so this can impact your other source's ability to index.
|
to scan the entire source on your specified frequency, but it's very slow for large sources. This is the most accurate way to find uploaded media with the tradeoff being that pairing a large source with a low index frequency will result in you spending most of your time indexing. Only so many indexing operations can be running at the same time, so this can impact your other source's ability to index.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
Fast indexing takes a different approach. It still does an initial scan the slow way but after that it uses a secondary mechanism (either RSS or YouTube's API) to frequently check for new videos. This has the potential to be hundreds of times faster, but it can miss videos if the uploader un-privates an old video or uploads dozens of videos in the space of a few minutes.
|
Fast indexing takes a different approach. It still does an initial scan the slow way but after that it uses an RSS feed to frequently check for new videos. This has the potential to be hundreds of times faster, but it can miss videos if the uploader un-privates an old video or uploads dozens of videos in the space of a few minutes. It works well for most channels or playlists but it's not perfect.
|
||||||
</p>
|
|
||||||
<p class="mt-2">
|
|
||||||
RSS is used by default but you should enable the YouTube API if you want the best version of fast indexing. This isn't needed for most users but it provides the fastest and most reliable media updates.
|
|
||||||
<.inline_link href="https://github.com/kieraneglin/pinchflat/wiki/Generating-a-YouTube-API-key">
|
|
||||||
Here is some documentation
|
|
||||||
</.inline_link>
|
|
||||||
on how to get your API key which you can add in the
|
|
||||||
<.inline_link href={~p"/settings"}>
|
|
||||||
settings
|
|
||||||
</.inline_link>
|
|
||||||
page.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
To make up for this limitation, a normal index is still run monthly to catch any videos that were missed by fast indexing. Fast indexing overrides the normal index frequency.
|
To make up for this limitation, a normal index is still run monthly to catch any videos that were missed by fast indexing. Fast indexing overrides the normal index frequency.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4">
|
<p class="mt-2">
|
||||||
<h4 class="font-bold text-xl">TL;DR</h4>
|
Fast indexing is experimental so please report any issues on GitHub. It's only recommended for sources with over 200-ish videos and that upload frequently. Not recommended for small or inactive sources.
|
||||||
|
|
||||||
<strong class="mt-2 inline-block">In general:</strong>
|
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
|
||||||
<li>
|
|
||||||
Uses RSS by default which is fine for most users
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.inline_link href="https://github.com/kieraneglin/pinchflat/wiki/Generating-a-YouTube-API-key">
|
|
||||||
Create a YouTube API key
|
|
||||||
</.inline_link>
|
|
||||||
and add it in your
|
|
||||||
<.inline_link href={~p"/settings"}>
|
|
||||||
settings
|
|
||||||
</.inline_link>
|
|
||||||
for the fastest possible media updates
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<strong class="mt-2 inline-block">Fast indexing is great if any of these apply:</strong>
|
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
|
||||||
<li>The source is large channel and uploads frequently</li>
|
|
||||||
<li>You want to download a source's new content as soon as possible</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<strong class="mt-2 inline-block">Consider <em>not</em> using fast indexing if any of these apply:</strong>
|
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
|
||||||
<li>The source is a playlist</li>
|
|
||||||
<li>The source has under 200 videos</li>
|
|
||||||
<li>The source rarely uploads</li>
|
|
||||||
<li>You don't mind if it takes longer for new content to be picked up</li>
|
|
||||||
</ul>
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Type"><%= source.collection_type %></:col>
|
<:col :let={source} label="Type"><%= source.collection_type %></:col>
|
||||||
<:col :let={source} label="Pending"><.localized_number number={source.pending_count} /></:col>
|
<:col :let={source} label="Pending"><%= source.pending_count %></:col>
|
||||||
<:col :let={source} label="Downloaded"><.localized_number number={source.downloaded_count} /></:col>
|
<:col :let={source} label="Downloaded"><%= source.downloaded_count %></:col>
|
||||||
<:col :let={source} label="Retention">
|
<:col :let={source} label="Retention">
|
||||||
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
|
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
|
||||||
<.localized_number number={source.retention_period_days} />
|
<%= source.retention_period_days %> day(s)
|
||||||
<.pluralize count={source.retention_period_days} word="day" />
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-lg">∞</span>
|
<span class="text-lg">∞</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||||||
<header class="flex justify-between items-center mb-4">
|
<header class="flex justify-between items-center mb-4">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
||||||
<span class="ml-2">
|
<span class="ml-2">Showing <%= length(@records) %> of <%= @filtered_record_count %></span>
|
||||||
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@filtered_record_count} />
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="bg-meta-4 rounded-md">
|
<div class="bg-meta-4 rounded-md">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -51,12 +49,12 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||||||
<%= StringUtils.truncate(media_item.title, 50) %>
|
<%= StringUtils.truncate(media_item.title, 50) %>
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
|
<:col :let={media_item} label="Upload Date">
|
||||||
|
<%= media_item.upload_date %>
|
||||||
|
</:col>
|
||||||
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
||||||
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
|
||||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
|
||||||
</:col>
|
|
||||||
<:col :let={media_item} label="" class="flex justify-end">
|
<:col :let={media_item} label="" class="flex justify-end">
|
||||||
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
||||||
</:col>
|
</:col>
|
||||||
@@ -119,40 +117,14 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||||||
{:noreply, assign(socket, new_assigns)}
|
{:noreply, assign(socket, new_assigns)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_pagination_attributes(base_query, page, ""), do: fetch_pagination_attributes(base_query, page, nil)
|
|
||||||
|
|
||||||
defp fetch_pagination_attributes(base_query, page, nil) do
|
|
||||||
total_record_count = Repo.aggregate(base_query, :count, :id)
|
|
||||||
total_pages = max(ceil(total_record_count / @limit), 1)
|
|
||||||
page = NumberUtils.clamp(page, 1, total_pages)
|
|
||||||
|
|
||||||
records =
|
|
||||||
fetch_records(base_query, page)
|
|
||||||
|> order_by(desc: :uploaded_at)
|
|
||||||
|> Repo.all()
|
|
||||||
|
|
||||||
%{
|
|
||||||
page: page,
|
|
||||||
total_pages: total_pages,
|
|
||||||
records: records,
|
|
||||||
search_term: nil,
|
|
||||||
total_record_count: total_record_count,
|
|
||||||
filtered_record_count: total_record_count
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_pagination_attributes(base_query, page, search_term) do
|
defp fetch_pagination_attributes(base_query, page, search_term) do
|
||||||
filtered_base_query = filtered_base_query(base_query, search_term)
|
filtered_base_query = filter_base_query(base_query, search_term)
|
||||||
|
|
||||||
total_record_count = Repo.aggregate(base_query, :count, :id)
|
total_record_count = Repo.aggregate(base_query, :count, :id)
|
||||||
filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id)
|
filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id)
|
||||||
total_pages = max(ceil(filtered_record_count / @limit), 1)
|
total_pages = max(ceil(filtered_record_count / @limit), 1)
|
||||||
page = NumberUtils.clamp(page, 1, total_pages)
|
page = NumberUtils.clamp(page, 1, total_pages)
|
||||||
|
records = fetch_records(filtered_base_query, page)
|
||||||
records =
|
|
||||||
fetch_records(filtered_base_query, page)
|
|
||||||
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|
|
||||||
|> Repo.all()
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
page: page,
|
page: page,
|
||||||
@@ -170,41 +142,39 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||||||
base_query
|
base_query
|
||||||
|> limit(^@limit)
|
|> limit(^@limit)
|
||||||
|> offset(^offset)
|
|> offset(^offset)
|
||||||
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_base_query(source, "pending") do
|
defp generate_base_query(source, "pending") do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> select(^select_fields())
|
|
||||||
|> MediaQuery.require_assoc(:media_profile)
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||||
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|
||||||
|
|> order_by(desc: fragment("rank"), desc: :upload_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_base_query(source, "downloaded") do
|
defp generate_base_query(source, "downloaded") do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> select(^select_fields())
|
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||||
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|
||||||
|
|> order_by(desc: fragment("rank"), desc: :upload_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_base_query(source, "other") do
|
defp generate_base_query(source, "other") do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> select(^select_fields())
|
|
||||||
|> MediaQuery.require_assoc(:media_profile)
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||||
|> where(
|
|> where(
|
||||||
^dynamic(
|
^dynamic(
|
||||||
^MediaQuery.for_source(source) and
|
^MediaQuery.for_source(source) and
|
||||||
(not (^MediaQuery.downloaded()) and not (^MediaQuery.pending()))
|
(not (^MediaQuery.downloaded()) and not (^MediaQuery.pending()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|> order_by(desc: fragment("rank"), desc: :upload_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filtered_base_query(base_query, search_term) do
|
defp filter_base_query(base_query, search_term) do
|
||||||
base_query
|
base_query
|
||||||
|> MediaQuery.require_assoc(:media_items_search_index)
|
|
||||||
|> where(^MediaQuery.matches_search_term(search_term))
|
|> where(^MediaQuery.matches_search_term(search_term))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Selecting only what we need GREATLY speeds up queries on large tables
|
|
||||||
defp select_fields do
|
|
||||||
[:id, :title, :uploaded_at, :prevent_download]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
General Options
|
General Options
|
||||||
</h3>
|
</h3>
|
||||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||||
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Standard'"></span>
|
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Basic'"></span>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
label="Index Frequency"
|
label="Index Frequency"
|
||||||
x-bind:disabled="fastIndexingEnabled == true"
|
x-bind:disabled="fastIndexingEnabled == true"
|
||||||
x-init="$watch('fastIndexingEnabled', v => v && ($el.value = 30 * 24 * 60))"
|
x-init="$watch('fastIndexingEnabled', v => v && ($el.value = 30 * 24 * 60))"
|
||||||
help="Indexing is the process of checking for media to download. For best results, set this to the longest delay you can tolerate for this source"
|
help="Indexing is the process of checking for media to download. Sets the time between one index of this source finishing and the next one starting"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div phx-click={show_modal("upgrade-modal")}>
|
<div phx-click={show_modal("upgrade-modal")}>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
type="toggle"
|
type="toggle"
|
||||||
label="Use Fast Indexing"
|
label="Use Fast Indexing"
|
||||||
label_suffix="(pro)"
|
label_suffix="(pro)"
|
||||||
help="Not recommended for playlists. Overrides 'Index Frequency'. See below for more details (seriously, there's a TL;DR that's worth reading)"
|
help="Experimental. Overrides 'Index Frequency'. Recommended for large channels that upload frequently. Does not work with private playlists. See below for more info"
|
||||||
x-init="
|
x-init="
|
||||||
// `enabled` is the data attribute that the toggle uses internally
|
// `enabled` is the data attribute that the toggle uses internally
|
||||||
fastIndexingEnabled = enabled
|
fastIndexingEnabled = enabled
|
||||||
@@ -95,8 +95,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
label="Retention Period (days)"
|
label="Retention Period (days)"
|
||||||
min="0"
|
min="0"
|
||||||
help="Days between when media is <em>downloaded</em> and when it's deleted. Leave blank to keep media indefinitely"
|
help="Days between when media is *downloaded* and when it's deleted. Leave blank to keep media indefinitely"
|
||||||
html_help={true}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section x-show="advancedMode">
|
<section x-show="advancedMode">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defmodule PinchflatWeb.Router do
|
|||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
plug :fetch_live_flash
|
plug :fetch_live_flash
|
||||||
plug :put_root_layout, html: {PinchflatWeb.Layouts, :root}
|
plug :put_root_layout, html: {PinchflatWeb.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery, with: :clear_session
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
plug :allow_iframe_embed
|
plug :allow_iframe_embed
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2024.6.25",
|
version: "2024.5.24",
|
||||||
elixir: "~> 1.17",
|
elixir: "~> 1.16",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
@@ -71,7 +71,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
{:nimble_parsec, "~> 1.4"},
|
{:nimble_parsec, "~> 1.4"},
|
||||||
{:timex, "~> 3.0"},
|
{:timex, "~> 3.0"},
|
||||||
{:mox, "~> 1.0", only: :test},
|
{:mox, "~> 1.0", only: :test},
|
||||||
{:credo, "~> 1.7.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:credo_naming, "~> 2.1", only: [:dev, :test], runtime: false},
|
{:credo_naming, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||||
{:ex_check, "~> 0.14.0", only: [:dev, :test], runtime: false},
|
{:ex_check, "~> 0.14.0", only: [:dev, :test], runtime: false},
|
||||||
{:faker, "~> 0.17", only: :test},
|
{:faker, "~> 0.17", only: :test},
|
||||||
@@ -87,8 +87,6 @@ defmodule Pinchflat.MixProject do
|
|||||||
# See the documentation for `Mix` for more info on aliases.
|
# See the documentation for `Mix` for more info on aliases.
|
||||||
defp aliases do
|
defp aliases do
|
||||||
[
|
[
|
||||||
check: "check --config=tooling/.check.exs",
|
|
||||||
credo: "credo --config-file=tooling/.credo.exs",
|
|
||||||
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
||||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
"credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
|
||||||
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
||||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
|
|||||||
+1
-3
@@ -5,9 +5,7 @@
|
|||||||
"sqleton": "^2.2.0"
|
"sqleton": "^2.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"create-erd": "sqleton -o priv/repo/erd.png priv/repo/pinchflat_dev.db",
|
"create-erd": "sqleton -o priv/repo/erd.png priv/repo/pinchflat_dev.db"
|
||||||
"lint:check": "prettier . --check --config=tooling/.prettierrc.js --ignore-path=tooling/.prettierignore --ignore-path=.gitignore",
|
|
||||||
"lint:fix": "prettier . --write --config=tooling/.prettierrc.js --ignore-path=tooling/.prettierignore --ignore-path=.gitignore"
|
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 449 KiB |
@@ -1,21 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.RenameUploadDateToUploadedAt do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def up do
|
|
||||||
rename table(:media_items), :upload_date, to: :uploaded_at
|
|
||||||
|
|
||||||
execute """
|
|
||||||
UPDATE media_items
|
|
||||||
SET uploaded_at = uploaded_at || 'T00:00:00'
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def down do
|
|
||||||
rename table(:media_items), :uploaded_at, to: :upload_date
|
|
||||||
|
|
||||||
execute """
|
|
||||||
UPDATE media_items
|
|
||||||
SET upload_date = DATE(upload_date)
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.ModifyUploadDateIndex do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
drop index("media_items", [:upload_date])
|
|
||||||
create index("media_items", [:uploaded_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.AddIndexesForLargeCollections do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
create index(
|
|
||||||
"media_items",
|
|
||||||
[
|
|
||||||
:source_id,
|
|
||||||
:media_filepath,
|
|
||||||
:uploaded_at,
|
|
||||||
:prevent_download,
|
|
||||||
:livestream,
|
|
||||||
:short_form_content,
|
|
||||||
:title
|
|
||||||
],
|
|
||||||
name: "media_items_pending_and_downloaded_index"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.RefactorCodecPreferences do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
alter table(:settings) do
|
|
||||||
remove :video_codec_preference, {:array, :string}, default: []
|
|
||||||
remove :audio_codec_preference, {:array, :string}, default: []
|
|
||||||
end
|
|
||||||
|
|
||||||
alter table(:settings) do
|
|
||||||
add :video_codec_preference, :string, default: "avc", null: false
|
|
||||||
add :audio_codec_preference, :string, default: "m4a", null: false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.AddYoutubeApiKeySetting do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
alter table(:settings) do
|
|
||||||
add :youtube_api_key, :string
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -1,8 +1,8 @@
|
|||||||
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
|
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
|
||||||
# instead of Alpine to avoid DNS resolution issues in production.
|
# instead of Alpine to avoid DNS resolution issues in production.
|
||||||
ARG ELIXIR_VERSION=1.17.0
|
ARG ELIXIR_VERSION=1.16.2
|
||||||
ARG OTP_VERSION=26.2.5
|
ARG OTP_VERSION=26.2.2
|
||||||
ARG DEBIAN_VERSION=bookworm-20240612-slim
|
ARG DEBIAN_VERSION=bookworm-20240130-slim
|
||||||
|
|
||||||
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
||||||
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
defmodule Pinchflat.Downloading.CodecParserTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.Downloading.CodecParser
|
||||||
|
|
||||||
|
describe "generate_vcodec_string_from_settings/1" do
|
||||||
|
test "returns a default vcodec string when setting isn't set" do
|
||||||
|
Settings.set(video_codec_preference: [])
|
||||||
|
|
||||||
|
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string_from_settings()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates a vcodec string" do
|
||||||
|
Settings.set(video_codec_preference: ["av01"])
|
||||||
|
|
||||||
|
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string_from_settings()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "generate_acodec_string_from_settings/1" do
|
||||||
|
test "returns a default acodec string when setting isn't set" do
|
||||||
|
Settings.set(audio_codec_preference: [])
|
||||||
|
|
||||||
|
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string_from_settings()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates an acodec string" do
|
||||||
|
Settings.set(audio_codec_preference: ["mp3"])
|
||||||
|
|
||||||
|
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string_from_settings()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "generate_vcodec_string/1" do
|
||||||
|
test "returns a default vcodec string when nil" do
|
||||||
|
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a default vcodec string when empty" do
|
||||||
|
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string([])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates a vcodec string" do
|
||||||
|
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignores options that don't exist" do
|
||||||
|
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01", "foo"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "generate_acodec_string/1" do
|
||||||
|
test "returns a default acodec string when nil" do
|
||||||
|
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a default acodec string when empty" do
|
||||||
|
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string([])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates an acodec string" do
|
||||||
|
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignores options that don't exist" do
|
||||||
|
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3", "foo"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -256,8 +256,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
media_item = Repo.preload(media_item_fixture(source_id: source.id), source: :media_profile)
|
media_item = Repo.preload(media_item_fixture(source_id: source.id), source: :media_profile)
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
assert {:format_sort, "res:#{resolution}"} in res
|
||||||
|
|
||||||
|
assert {:format, "((bestvideo[vcodec~='^avc']/bestvideo)+(bestaudio[acodec~='^mp4a']/bestaudio))/best"} in res
|
||||||
|
|
||||||
assert {:format_sort, "res:#{resolution},+codec:avc:m4a"} in res
|
|
||||||
assert {:remux_video, "mp4"} in res
|
assert {:remux_video, "mp4"} in res
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -268,20 +270,20 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
assert :extract_audio in res
|
assert :extract_audio in res
|
||||||
assert {:format_sort, "+acodec:m4a"} in res
|
assert {:format, "bestaudio[acodec~='^mp4a']/bestaudio/best"} in res
|
||||||
|
|
||||||
refute {:remux_video, "mp4"} in res
|
refute {:remux_video, "mp4"} in res
|
||||||
end
|
end
|
||||||
|
|
||||||
test "includes custom quality options if specified", %{media_item: media_item} do
|
test "includes custom quality options if specified", %{media_item: media_item} do
|
||||||
Settings.set(video_codec_preference: "av01")
|
Settings.set(video_codec_preference: ["av01"])
|
||||||
Settings.set(audio_codec_preference: "aac")
|
Settings.set(audio_codec_preference: ["aac"])
|
||||||
|
|
||||||
media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :"1080p"})
|
media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :"1080p"})
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
assert {:format_sort, "res:1080,+codec:av01:aac"} in res
|
assert {:format, "((bestvideo[vcodec~='^av01']/bestvideo)+(bestaudio[acodec~='^aac']/bestaudio))/best"} in res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
|
|
||||||
describe "enqueue_pending_download_tasks/1" do
|
describe "enqueue_pending_download_tasks/1" do
|
||||||
test "it enqueues a job for each pending media item" do
|
test "it enqueues a job for each pending media item" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -19,6 +23,16 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it can optionally delay when those jobs are enqueued" do
|
||||||
|
source = source_fixture()
|
||||||
|
_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
|
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source, kickoff_delay: 60)
|
||||||
|
[job] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, now()), 60, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "it does not enqueue a job for media items with a filepath" do
|
test "it does not enqueue a job for media items with a filepath" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
_media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
|
_media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
|
||||||
@@ -84,6 +98,13 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it can optionally delay when those jobs are enqueued", %{media_item: media_item} do
|
||||||
|
assert {:ok, _} = DownloadingHelpers.kickoff_download_if_pending(media_item, kickoff_delay: 60)
|
||||||
|
[job] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, now()), 60, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "creates and returns a download task record", %{media_item: media_item} do
|
test "creates and returns a download task record", %{media_item: media_item} do
|
||||||
assert {:ok, task} = DownloadingHelpers.kickoff_download_if_pending(media_item)
|
assert {:ok, task} = DownloadingHelpers.kickoff_download_if_pending(media_item)
|
||||||
|
|
||||||
@@ -138,4 +159,67 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "create_media_item_and_run_script/2" do
|
||||||
|
setup do
|
||||||
|
FilesystemUtils.write_p!(filepath(), "")
|
||||||
|
File.chmod(filepath(), 0o755)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm(filepath()) end)
|
||||||
|
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
media_attrs =
|
||||||
|
media_attributes_return_fixture()
|
||||||
|
|> Phoenix.json_library().decode!()
|
||||||
|
|> YtDlpMedia.response_to_struct()
|
||||||
|
|
||||||
|
{:ok, source: source, media_attrs: media_attrs}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates a media item for a given source and attributes", %{source: source, media_attrs: media_attrs} do
|
||||||
|
assert {:ok, %MediaItem{} = media_item} = DownloadingHelpers.create_media_item_and_run_script(source, media_attrs)
|
||||||
|
|
||||||
|
assert media_item.source_id == source.id
|
||||||
|
assert media_item.title == media_attrs.title
|
||||||
|
assert media_item.media_id == media_attrs.media_id
|
||||||
|
assert media_item.original_url == media_attrs.original_url
|
||||||
|
assert media_item.description == media_attrs.description
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an error if the media item cannot be created", %{source: source, media_attrs: media_attrs} do
|
||||||
|
media_attrs = %YtDlpMedia{media_attrs | media_id: nil}
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{}} = DownloadingHelpers.create_media_item_and_run_script(source, media_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "runs a script if the media item is created", %{source: source, media_attrs: media_attrs} do
|
||||||
|
# We *love* indirectly testing side effects
|
||||||
|
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
|
filename = "#{tmp_dir}/test_file-#{Enum.random(1..1000)}"
|
||||||
|
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
|
||||||
|
|
||||||
|
refute File.exists?(filename)
|
||||||
|
assert {:ok, %MediaItem{}} = DownloadingHelpers.create_media_item_and_run_script(source, media_attrs)
|
||||||
|
assert File.exists?(filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not run a script if the media item already exists", %{source: source, media_attrs: media_attrs} do
|
||||||
|
{:ok, %MediaItem{}} = DownloadingHelpers.create_media_item_and_run_script(source, media_attrs)
|
||||||
|
|
||||||
|
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
|
filename = "#{tmp_dir}/test_file-#{Enum.random(1..1000)}"
|
||||||
|
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
|
||||||
|
|
||||||
|
refute File.exists?(filename)
|
||||||
|
assert {:ok, %MediaItem{}} = DownloadingHelpers.create_media_item_and_run_script(source, media_attrs)
|
||||||
|
refute File.exists?(filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filepath do
|
||||||
|
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||||
|
|
||||||
|
Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
[:metadata, source: :media_profile]
|
[:metadata, source: :media_profile]
|
||||||
)
|
)
|
||||||
|
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts ->
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
end
|
end
|
||||||
@@ -43,7 +44,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
|
||||||
assert updated_media_item.metadata.metadata_filepath =~ "media_items/#{media_item.id}/metadata.json.gz"
|
assert updated_media_item.metadata.metadata_filepath =~ "media_items/#{media_item.id}/metadata.json.gz"
|
||||||
assert updated_media_item.metadata.thumbnail_filepath =~ "media_items/#{media_item.id}/thumbnail.jpg"
|
assert updated_media_item.metadata.thumbnail_filepath =~ "media_items/#{media_item.id}/maxresdefault.jpg"
|
||||||
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
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorkerTest do
|
|||||||
media_item =
|
media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(6, :days),
|
upload_date: now_minus(6, :days),
|
||||||
media_downloaded_at: now_minus(5, :days)
|
media_downloaded_at: now_minus(5, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorkerTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(6, :days),
|
upload_date: now_minus(6, :days),
|
||||||
media_downloaded_at: now_minus(1, :day)
|
media_downloaded_at: now_minus(1, :day)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
|
|
||||||
|
describe "kickoff_download_tasks_from_youtube_rss_feed/1" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -19,7 +19,6 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:ok, [source: source_fixture()]}
|
{:ok, [source: source_fixture()]}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "kickoff_download_tasks_from_youtube_rss_feed/1" do
|
|
||||||
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
|
test "enqueues a new worker for each new media_id in the source's RSS feed", %{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)
|
||||||
|
|
||||||
@@ -29,6 +28,16 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
assert worker.args["id"] == media_item.id
|
assert worker.args["id"] == media_item.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "enqueues the worker with a small delay", %{source: source} do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
||||||
|
|
||||||
|
[job | _] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, now()), 5, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "does not enqueue a new worker for the source's media IDs we already know about", %{source: source} do
|
test "does not enqueue a new worker for the source's media IDs we already know about", %{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)
|
||||||
media_item_fixture(source_id: source.id, media_id: "test_1")
|
media_item_fixture(source_id: source.id, media_id: "test_1")
|
||||||
@@ -108,49 +117,4 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "kickoff_download_tasks_from_youtube_rss_feed/1 when testing backends" do
|
|
||||||
test "uses the YouTube API if it is enabled", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn url, _headers ->
|
|
||||||
assert url =~ "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
|
||||||
|
|
||||||
{:ok, "{}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
|
||||||
|
|
||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "the YouTube API creates records as expected", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn _url, _headers ->
|
|
||||||
{:ok, ~s({ "items": [ {"contentDetails": {"videoId": "test_1"}} ] })}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "RSS is used as a backup if the API fails", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn _url, _headers -> {:error, ""} end)
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
|
||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "RSS is used if the API is not enabled", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn url ->
|
|
||||||
assert url =~ "https://www.youtube.com/feeds/videos.xml"
|
|
||||||
|
|
||||||
{:ok, "<yt:videoId>test_1</yt:videoId>"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Settings.set(youtube_api_key: nil)
|
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
defmodule Pinchflat.FastIndexing.YoutubeApiTest do
|
|
||||||
use Pinchflat.DataCase
|
|
||||||
|
|
||||||
import Pinchflat.SourcesFixtures
|
|
||||||
|
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.FastIndexing.YoutubeApi
|
|
||||||
|
|
||||||
describe "enabled?/0" do
|
|
||||||
test "returns true if the user has set a YouTube API key" do
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
|
||||||
|
|
||||||
assert YoutubeApi.enabled?()
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns false if the user has not set an API key" do
|
|
||||||
Settings.set(youtube_api_key: nil)
|
|
||||||
|
|
||||||
refute YoutubeApi.enabled?()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_recent_media_ids/1" do
|
|
||||||
setup do
|
|
||||||
source = source_fixture()
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
|
||||||
|
|
||||||
{:ok, source: source}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "calls the expected URL", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn url, headers ->
|
|
||||||
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
|
||||||
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=test_key"
|
|
||||||
|
|
||||||
assert url == request_url
|
|
||||||
assert headers == [accept: "application/json"]
|
|
||||||
|
|
||||||
{:ok, "{}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, _} = YoutubeApi.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "replaces channel IDs with playlist IDs if needed" do
|
|
||||||
source = source_fixture(collection_id: "UC_ABC123")
|
|
||||||
|
|
||||||
expect(HTTPClientMock, :get, fn url, _headers ->
|
|
||||||
assert url =~ "playlistId=UU_ABC123&"
|
|
||||||
|
|
||||||
{:ok, "{}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, _} = YoutubeApi.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns an empty list if no media is returned", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn _url, _headers -> {:ok, "{}"} end)
|
|
||||||
|
|
||||||
assert {:ok, []} = YoutubeApi.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns media IDs if present", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn _url, _headers ->
|
|
||||||
{:ok,
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{"contentDetails": {"videoId": "test_1"}},
|
|
||||||
{"contentDetails": {"videoId": "test_2"}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, ["test_1", "test_2"]} = YoutubeApi.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns an error if the HTTP request fails", %{source: source} do
|
|
||||||
expect(HTTPClientMock, :get, fn _url, _headers -> {:error, "error"} end)
|
|
||||||
|
|
||||||
assert {:error, "error"} = YoutubeApi.get_recent_media_ids(source)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -11,13 +11,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, source: source}
|
{:ok, source: source}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "enabled?/0" do
|
describe "get_recent_media_ids_from_rss/1" do
|
||||||
test "returns true" do
|
|
||||||
assert YoutubeRss.enabled?()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_recent_media_ids/1" do
|
|
||||||
test "calls the expected URL for channel sources" do
|
test "calls the expected URL for channel sources" do
|
||||||
source = source_fixture(collection_type: :channel, collection_id: "channel_id")
|
source = source_fixture(collection_type: :channel, collection_id: "channel_id")
|
||||||
|
|
||||||
@@ -27,7 +21,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, _} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calls the expected URL for playlist sources" do
|
test "calls the expected URL for playlist sources" do
|
||||||
@@ -39,13 +33,13 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, _} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the HTTP request fails", %{source: source} do
|
test "returns an error if the HTTP request fails", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:error, ""} end)
|
expect(HTTPClientMock, :get, fn _url -> {:error, ""} end)
|
||||||
|
|
||||||
assert {:error, "Failed to fetch RSS feed"} = YoutubeRss.get_recent_media_ids(source)
|
assert {:error, "Failed to fetch RSS feed"} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the media IDs from the RSS feed", %{source: source} do
|
test "returns the media IDs from the RSS feed", %{source: source} do
|
||||||
@@ -53,7 +47,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId>test_2</yt:videoId>"}
|
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId>test_2</yt:videoId>"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "strips whitespace from media IDs", %{source: source} do
|
test "strips whitespace from media IDs", %{source: source} do
|
||||||
@@ -61,7 +55,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, "<yt:videoId> test_1 </yt:videoId><yt:videoId> test_2 </yt:videoId>"}
|
{:ok, "<yt:videoId> test_1 </yt:videoId><yt:videoId> test_2 </yt:videoId>"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "removes empty media IDs", %{source: source} do
|
test "removes empty media IDs", %{source: source} do
|
||||||
@@ -69,7 +63,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId></yt:videoId>"}
|
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId></yt:videoId>"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "removes duplicate media IDs", %{source: source} do
|
test "removes duplicate media IDs", %{source: source} do
|
||||||
@@ -77,7 +71,7 @@ defmodule Pinchflat.FastIndexing.YoutubeRssTest do
|
|||||||
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId>test_1</yt:videoId>"}
|
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId>test_1</yt:videoId>"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids(source)
|
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+186
-177
@@ -34,6 +34,150 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "schema when testing upload_date_index and source is a channel" do
|
||||||
|
test "upload_date_index is set to 99 if it's the only video uploaded that day" do
|
||||||
|
upload_date = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
|
||||||
|
assert media_item.upload_date_index == 99
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index is set to 98 if it's the second video uploaded that day" do
|
||||||
|
upload_date = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
media_item_two = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
|
||||||
|
assert media_item_one.upload_date_index == 99
|
||||||
|
assert media_item_two.upload_date_index == 98
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't decrement if the video is uploaded on a different day" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
one_day_ago = Date.add(today, -1)
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
|
||||||
|
|
||||||
|
assert media_item_new.upload_date_index == 99
|
||||||
|
assert media_item_old.upload_date_index == 99
|
||||||
|
end
|
||||||
|
|
||||||
|
test "recomputes upload_date_index if an upload_date is changed...somehow" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
one_day_ago = Date.add(today, -1)
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
|
||||||
|
|
||||||
|
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{upload_date: today})
|
||||||
|
|
||||||
|
assert media_item_new.upload_date_index == 99
|
||||||
|
assert updated_media_item.upload_date_index == 98
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't decrement if the video is for a different source" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
|
||||||
|
source_one = source_fixture(%{collection_type: :channel})
|
||||||
|
source_two = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source_one.id, upload_date: today})
|
||||||
|
media_item_two = media_item_fixture(%{source_id: source_two.id, upload_date: today})
|
||||||
|
|
||||||
|
assert media_item_one.upload_date_index == 99
|
||||||
|
assert media_item_two.upload_date_index == 99
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't decrement if the a video's upload_date is updated but doesn't change" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
_media_item_two = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
|
||||||
|
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{upload_date: today, title: "New title"})
|
||||||
|
|
||||||
|
assert updated_media_item.upload_date_index == 99
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "schema when testing upload_date_index and source is a playlist" do
|
||||||
|
test "upload_date_index is set to 0 if it's the only video uploaded that day" do
|
||||||
|
upload_date = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
|
||||||
|
assert media_item.upload_date_index == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index is set to 1 if it's the second video uploaded that day" do
|
||||||
|
upload_date = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
media_item_two = media_item_fixture(%{source_id: source.id, upload_date: upload_date})
|
||||||
|
|
||||||
|
assert media_item_one.upload_date_index == 0
|
||||||
|
assert media_item_two.upload_date_index == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't increment if the video is uploaded on a different day" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
one_day_ago = Date.add(today, -1)
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
|
||||||
|
|
||||||
|
assert media_item_new.upload_date_index == 0
|
||||||
|
assert media_item_old.upload_date_index == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "recomputes upload_date_index if an upload_date is changed...somehow" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
one_day_ago = Date.add(today, -1)
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
media_item_new = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
media_item_old = media_item_fixture(%{source_id: source.id, upload_date: one_day_ago})
|
||||||
|
|
||||||
|
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{upload_date: today})
|
||||||
|
|
||||||
|
assert media_item_new.upload_date_index == 0
|
||||||
|
assert updated_media_item.upload_date_index == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't increment if the video is for a different source" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
|
||||||
|
source_one = source_fixture(%{collection_type: :playlist})
|
||||||
|
source_two = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source_one.id, upload_date: today})
|
||||||
|
media_item_two = media_item_fixture(%{source_id: source_two.id, upload_date: today})
|
||||||
|
|
||||||
|
assert media_item_one.upload_date_index == 0
|
||||||
|
assert media_item_two.upload_date_index == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "upload_date_index doesn't increment if the a video's upload_date is updated but doesn't change" do
|
||||||
|
today = Date.utc_today()
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
media_item_one = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
_media_item_two = media_item_fixture(%{source_id: source.id, upload_date: today})
|
||||||
|
|
||||||
|
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{upload_date: today, title: "New title"})
|
||||||
|
|
||||||
|
assert updated_media_item.upload_date_index == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "list_media_items/0" do
|
describe "list_media_items/0" do
|
||||||
test "it returns all media_items" do
|
test "it returns all media_items" do
|
||||||
media_item = media_item_fixture()
|
media_item = media_item_fixture()
|
||||||
@@ -145,7 +289,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_item =
|
media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(6, :days),
|
upload_date: now_minus(6, :days),
|
||||||
media_downloaded_at: now_minus(5, :days)
|
media_downloaded_at: now_minus(5, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -156,7 +300,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_item =
|
media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(20, :days),
|
upload_date: now_minus(20, :days),
|
||||||
media_downloaded_at: now_minus(19, :days)
|
media_downloaded_at: now_minus(19, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,7 +311,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(5, :days),
|
upload_date: now_minus(5, :days),
|
||||||
media_downloaded_at: nil
|
media_downloaded_at: nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -178,7 +322,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(5, :days),
|
upload_date: now_minus(5, :days),
|
||||||
media_downloaded_at: now(),
|
media_downloaded_at: now(),
|
||||||
prevent_download: true
|
prevent_download: true
|
||||||
})
|
})
|
||||||
@@ -190,7 +334,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(5, :days),
|
upload_date: now_minus(5, :days),
|
||||||
media_downloaded_at: now(),
|
media_downloaded_at: now(),
|
||||||
culled_at: now()
|
culled_at: now()
|
||||||
})
|
})
|
||||||
@@ -202,7 +346,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(3, :days),
|
upload_date: now_minus(3, :days),
|
||||||
media_downloaded_at: now_minus(3, :days)
|
media_downloaded_at: now_minus(3, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -213,7 +357,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(5, :days),
|
upload_date: now_minus(5, :days),
|
||||||
media_downloaded_at: now(),
|
media_downloaded_at: now(),
|
||||||
media_redownloaded_at: now()
|
media_redownloaded_at: now()
|
||||||
})
|
})
|
||||||
@@ -221,12 +365,12 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_redownloadable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that were first downloaded well after the uploaded_at", %{source: source} do
|
test "does not return media items that were first downloaded well after the upload_date", %{source: source} do
|
||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
media_downloaded_at: now(),
|
media_downloaded_at: now(),
|
||||||
uploaded_at: now_minus(20, :days)
|
upload_date: now_minus(20, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_redownloadable_media_items() == []
|
||||||
@@ -237,7 +381,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
media_downloaded_at: now(),
|
media_downloaded_at: now(),
|
||||||
uploaded_at: now_minus(2, :days)
|
upload_date: now_minus(2, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_redownloadable_media_items() == []
|
||||||
@@ -250,7 +394,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
source_id: source.id,
|
source_id: source.id,
|
||||||
uploaded_at: now_minus(6, :days),
|
upload_date: now_minus(6, :days),
|
||||||
media_downloaded_at: now_minus(5, :days)
|
media_downloaded_at: now_minus(5, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -408,9 +552,9 @@ defmodule Pinchflat.MediaTest do
|
|||||||
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
||||||
|
|
||||||
_old_media_item =
|
_old_media_item =
|
||||||
media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)})
|
||||||
|
|
||||||
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now()})
|
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now()})
|
||||||
|
|
||||||
assert Media.list_pending_media_items_for(source) == [new_media_item]
|
assert Media.list_pending_media_items_for(source) == [new_media_item]
|
||||||
end
|
end
|
||||||
@@ -419,9 +563,9 @@ defmodule Pinchflat.MediaTest do
|
|||||||
source = source_fixture(%{download_cutoff_date: nil})
|
source = source_fixture(%{download_cutoff_date: nil})
|
||||||
|
|
||||||
old_media_item =
|
old_media_item =
|
||||||
media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)})
|
||||||
|
|
||||||
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now()})
|
new_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now()})
|
||||||
|
|
||||||
assert Media.list_pending_media_items_for(source) == [old_media_item, new_media_item]
|
assert Media.list_pending_media_items_for(source) == [old_media_item, new_media_item]
|
||||||
end
|
end
|
||||||
@@ -486,21 +630,21 @@ defmodule Pinchflat.MediaTest do
|
|||||||
|
|
||||||
test "returns true if there is a cutoff date before the media's upload date" do
|
test "returns true if there is a cutoff date before the media's upload date" do
|
||||||
source = source_fixture(%{download_cutoff_date: now_minus(2, :days)})
|
source = source_fixture(%{download_cutoff_date: now_minus(2, :days)})
|
||||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(1, :day)})
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(1, :day)})
|
||||||
|
|
||||||
assert Media.pending_download?(media_item)
|
assert Media.pending_download?(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns false if there is a cutoff date after the media's upload date" do
|
test "returns false if there is a cutoff date after the media's upload date" do
|
||||||
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
||||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(2, :days)})
|
||||||
|
|
||||||
refute Media.pending_download?(media_item)
|
refute Media.pending_download?(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns true if there is no cutoff date" do
|
test "returns true if there is no cutoff date" do
|
||||||
source = source_fixture(%{download_cutoff_date: nil})
|
source = source_fixture(%{download_cutoff_date: nil})
|
||||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(1, :day)})
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, upload_date: now_minus(1, :day)})
|
||||||
|
|
||||||
assert Media.pending_download?(media_item)
|
assert Media.pending_download?(media_item)
|
||||||
end
|
end
|
||||||
@@ -611,7 +755,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
source_id: source_fixture().id,
|
source_id: source_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
uploaded_at: now()
|
upload_date: Date.utc_today()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
||||||
@@ -628,7 +772,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
source_id: source_fixture().id,
|
source_id: source_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
uploaded_at: now()
|
upload_date: Date.utc_today()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
||||||
@@ -643,7 +787,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
source_id: source_fixture().id,
|
source_id: source_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
uploaded_at: now(),
|
upload_date: Date.utc_today(),
|
||||||
uuid: "some-uuid"
|
uuid: "some-uuid"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,6 +835,18 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert media_item_1.id == media_item_2.id
|
assert media_item_1.id == media_item_2.id
|
||||||
assert media_item_2.title == different_attrs.title
|
assert media_item_2.title == different_attrs.title
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns an error if the media item cannot be created" do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
media_attrs =
|
||||||
|
media_attributes_return_fixture()
|
||||||
|
|> Phoenix.json_library().decode!()
|
||||||
|
|> Map.put("id", nil)
|
||||||
|
|> YtDlpMedia.response_to_struct()
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Media.create_media_item_from_backend_attrs(source, media_attrs)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "update_media_item/2" do
|
describe "update_media_item/2" do
|
||||||
@@ -740,13 +896,14 @@ 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 -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
metadata: %{
|
metadata: %{
|
||||||
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
||||||
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_item)
|
thumbnail_filepath:
|
||||||
|
MetadataFileHelpers.download_and_store_thumbnail_for(media_item, render_parsed_metadata(:media_metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,13 +929,14 @@ 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 -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
metadata: %{
|
metadata: %{
|
||||||
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
||||||
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_item)
|
thumbnail_filepath:
|
||||||
|
MetadataFileHelpers.download_and_store_thumbnail_for(media_item, render_parsed_metadata(:media_metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,13 +1016,14 @@ 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 -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
metadata: %{
|
metadata: %{
|
||||||
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_item, %{}),
|
||||||
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_item)
|
thumbnail_filepath:
|
||||||
|
MetadataFileHelpers.download_and_store_thumbnail_for(media_item, render_parsed_metadata(:media_metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,154 +1064,4 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert %Ecto.Changeset{} = Media.change_media_item(media_item)
|
assert %Ecto.Changeset{} = Media.change_media_item(media_item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "change_media_item/1 when testing upload_date_index and source is a channel" do
|
|
||||||
test "upload_date_index is set to 99 if it's the only video uploaded that day" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
media_item = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item.upload_date_index == 99
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index is set to 98 if it's the second video uploaded that day" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_one.upload_date_index == 99
|
|
||||||
assert media_item_two.upload_date_index == 98
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't decrement if the video is uploaded on a different day" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
|
|
||||||
|
|
||||||
assert media_item_new.upload_date_index == 99
|
|
||||||
assert media_item_old.upload_date_index == 99
|
|
||||||
end
|
|
||||||
|
|
||||||
test "recomputes upload_date_index if an upload_date is changed...somehow" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_new.upload_date_index == 99
|
|
||||||
assert updated_media_item.upload_date_index == 98
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't decrement if the video is for a different source" do
|
|
||||||
source_one = source_fixture(%{collection_type: :channel})
|
|
||||||
source_two = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source_one.id, uploaded_at: now()})
|
|
||||||
media_item_two = media_item_fixture(%{source_id: source_two.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_one.upload_date_index == 99
|
|
||||||
assert media_item_two.upload_date_index == 99
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't decrement if the a video's upload_date is updated but doesn't change" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{uploaded_at: now(), title: "New title"})
|
|
||||||
|
|
||||||
assert updated_media_item.upload_date_index == 99
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't increment if the a video's upload_date is changed to the same day" do
|
|
||||||
source = source_fixture(%{collection_type: :channel})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} =
|
|
||||||
Media.update_media_item(media_item_one, %{uploaded_at: now_plus(1, :minute), title: "New title"})
|
|
||||||
|
|
||||||
assert updated_media_item.upload_date_index == 99
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "change_media_item/1 when testing upload_date_index and source is a playlist" do
|
|
||||||
test "upload_date_index is set to 0 if it's the only video uploaded that day" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
media_item = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item.upload_date_index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index is set to 1 if it's the second video uploaded that day" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_one.upload_date_index == 0
|
|
||||||
assert media_item_two.upload_date_index == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't increment if the video is uploaded on a different day" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
|
|
||||||
|
|
||||||
assert media_item_new.upload_date_index == 0
|
|
||||||
assert media_item_old.upload_date_index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "recomputes upload_date_index if an upload_date is changed...somehow" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_new = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
media_item_old = media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(1, :day)})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} = Media.update_media_item(media_item_old, %{uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_new.upload_date_index == 0
|
|
||||||
assert updated_media_item.upload_date_index == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't increment if the video is for a different source" do
|
|
||||||
source_one = source_fixture(%{collection_type: :playlist})
|
|
||||||
source_two = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source_one.id, uploaded_at: now()})
|
|
||||||
media_item_two = media_item_fixture(%{source_id: source_two.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
assert media_item_one.upload_date_index == 0
|
|
||||||
assert media_item_two.upload_date_index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't increment if the a video's upload_date is updated but doesn't change" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} = Media.update_media_item(media_item_one, %{uploaded_at: now(), title: "New title"})
|
|
||||||
|
|
||||||
assert updated_media_item.upload_date_index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "upload_date_index doesn't increment if the a video's upload_date is changed to the same day" do
|
|
||||||
source = source_fixture(%{collection_type: :playlist})
|
|
||||||
|
|
||||||
media_item_one = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
_media_item_two = media_item_fixture(%{source_id: source.id, uploaded_at: now()})
|
|
||||||
|
|
||||||
{:ok, updated_media_item} =
|
|
||||||
Media.update_media_item(media_item_one, %{uploaded_at: now_plus(1, :minute), title: "New title"})
|
|
||||||
|
|
||||||
assert updated_media_item.upload_date_index == 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
metadata_map = %{"foo" => "bar"}
|
metadata_map = %{"foo" => "bar"}
|
||||||
|
|
||||||
filepath = Helpers.compress_and_store_metadata_for(media_item, metadata_map)
|
filepath = Helpers.compress_and_store_metadata_for(media_item, metadata_map)
|
||||||
{:ok, json} = File.open(filepath, [:read, :compressed], &IO.read(&1, :eof))
|
{:ok, json} = File.open(filepath, [:read, :compressed], &IO.read(&1, :all))
|
||||||
|
|
||||||
assert json == Phoenix.json_library().encode!(metadata_map)
|
assert json == Phoenix.json_library().encode!(metadata_map)
|
||||||
end
|
end
|
||||||
@@ -50,47 +50,92 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
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
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
# This tests that the HTTP endpoint is being called with every test
|
||||||
|
expect(HTTPClientMock, :get, fn _url, _headers, _opts ->
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
{:ok, "thumbnail data"}
|
||||||
|
|
||||||
assert filepath =~ ~r{/media_items/#{media_item.id}/thumbnail.jpg}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn url, opts, ot ->
|
|
||||||
assert url == media_item.original_url
|
|
||||||
assert ot == "after_move:%()j"
|
|
||||||
|
|
||||||
assert opts == [
|
|
||||||
:no_simulate,
|
|
||||||
:skip_download,
|
|
||||||
:write_thumbnail,
|
|
||||||
convert_thumbnail: "jpg",
|
|
||||||
output: "/tmp/test/metadata/media_items/1/thumbnail.%(ext)s"
|
|
||||||
]
|
|
||||||
|
|
||||||
{:ok, ""}
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Helpers.download_and_store_thumbnail_for(media_item)
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
|
||||||
|
{:ok, %{metadata: metadata}}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
test "returns the filepath", %{media_item: media_item, metadata: metadata} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "error"} end)
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
assert filepath =~ ~r{/media_items/#{media_item.id}/maxresdefault.jpg}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates folder structure based on passed record", %{media_item: media_item, metadata: metadata} do
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
|
assert File.exists?(Path.dirname(filepath))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "chooses the highest preference jpg thumbnail available", %{media_item: media_item} do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_1.jpg", "preference" => -1},
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_2.jpg", "preference" => 1},
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_3.jpg", "preference" => -10},
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_4.webp", "preference" => 10}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
|
assert filepath =~ ~r{/media_items/#{media_item.id}/img_2.jpg}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "will fall back to a non-jpg if it has to", %{media_item: media_item} do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_1.webp", "preference" => -1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
|
assert filepath =~ ~r{/media_items/#{media_item.id}/img_1.webp}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not require a preference field", %{media_item: media_item} do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{"url" => "https://i.ytimg.com/vi/ABC123/img_1.webp"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
|
assert filepath =~ ~r{/media_items/#{media_item.id}/img_1.webp}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "download_and_store_thumbnail_for/2 when not downloading thumbnails" do
|
||||||
|
test "returns nil if there are no thumbnails", %{media_item: media_item} do
|
||||||
|
metadata = %{"thumbnails" => []}
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
|
assert filepath == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil if there is no thumbnail field", %{media_item: media_item} do
|
||||||
|
metadata = %{}
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item, metadata)
|
||||||
|
|
||||||
assert filepath == nil
|
assert filepath == nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "parse_upload_date/1" do
|
describe "parse_upload_date/1" do
|
||||||
test "returns a datetime from the given metadata upload date" do
|
test "returns a date from the given metadata upload date" do
|
||||||
upload_date = "20210101"
|
upload_date = "20210101"
|
||||||
|
|
||||||
assert Helpers.parse_upload_date(upload_date) == ~U[2021-01-01 00:00:00Z]
|
assert Helpers.parse_upload_date(upload_date) == ~D[2021-01-01]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ defmodule Pinchflat.Podcasts.PodcastHelpersTest do
|
|||||||
test "orders by upload date where newest is first" do
|
test "orders by upload date where newest is first" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
oldest = media_item_with_attachments(%{source_id: source.id, uploaded_at: now_minus(2, :day)})
|
oldest = media_item_with_attachments(%{source_id: source.id, upload_date: now_minus(2, :day)})
|
||||||
current = media_item_with_attachments(%{source_id: source.id, uploaded_at: now()})
|
current = media_item_with_attachments(%{source_id: source.id, upload_date: now()})
|
||||||
older = media_item_with_attachments(%{source_id: source.id, uploaded_at: now_minus(1, :days)})
|
older = media_item_with_attachments(%{source_id: source.id, upload_date: now_minus(1, :days)})
|
||||||
|
|
||||||
assert [^current, ^older, ^oldest] = PodcastHelpers.persisted_media_items_for(source)
|
assert [^current, ^older, ^oldest] = PodcastHelpers.persisted_media_items_for(source)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ defmodule Pinchflat.Podcasts.RssFeedBuilderTest do
|
|||||||
assert String.contains?(item_xml, ~s(<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>))
|
assert String.contains?(item_xml, ~s(<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns pubDate based off the media's uploaded_at", %{source: source} do
|
test "returns pubDate based off the media's upload_date", %{source: source} do
|
||||||
media_item_with_attachments(%{source_id: source.id, uploaded_at: ~U[2020-01-01 00:00:00Z]})
|
media_item_with_attachments(%{source_id: source.id, upload_date: ~D[2020-01-01]})
|
||||||
|
|
||||||
res = RssFeedBuilder.build(source)
|
res = RssFeedBuilder.build(source)
|
||||||
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
||||||
|
|||||||
@@ -78,4 +78,64 @@ defmodule Pinchflat.SettingsTest do
|
|||||||
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
|
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "change_setting/2 when testing codec preferences" do
|
||||||
|
test "converts (video|audio)_codec_preference_string to an array" do
|
||||||
|
setting = Settings.record()
|
||||||
|
|
||||||
|
new_setting = %{
|
||||||
|
video_codec_preference_string: "avc>vp9",
|
||||||
|
audio_codec_preference_string: "aac>opus"
|
||||||
|
}
|
||||||
|
|
||||||
|
changeset = Settings.change_setting(setting, new_setting)
|
||||||
|
|
||||||
|
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
|
||||||
|
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
|
||||||
|
end
|
||||||
|
|
||||||
|
test "removes whitespace from (video|audio)_codec_preference" do
|
||||||
|
setting = Settings.record()
|
||||||
|
|
||||||
|
new_setting = %{
|
||||||
|
video_codec_preference_string: " avc > > vp9 ",
|
||||||
|
audio_codec_preference_string: "aac> opus "
|
||||||
|
}
|
||||||
|
|
||||||
|
changeset = Settings.change_setting(setting, new_setting)
|
||||||
|
|
||||||
|
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
|
||||||
|
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
|
||||||
|
end
|
||||||
|
|
||||||
|
test "downcases (video|audio)_codec_preference" do
|
||||||
|
setting = Settings.record()
|
||||||
|
|
||||||
|
new_setting = %{
|
||||||
|
video_codec_preference_string: "AVC>VP9",
|
||||||
|
audio_codec_preference_string: "AAC>OPUS"
|
||||||
|
}
|
||||||
|
|
||||||
|
changeset = Settings.change_setting(setting, new_setting)
|
||||||
|
|
||||||
|
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
|
||||||
|
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
|
||||||
|
end
|
||||||
|
|
||||||
|
test "an empty value will remove the codec settings" do
|
||||||
|
Settings.set(video_codec_preference: ["avc", "vp9"])
|
||||||
|
Settings.set(audio_codec_preference: ["aac", "opus"])
|
||||||
|
setting = Settings.record()
|
||||||
|
|
||||||
|
new_setting = %{
|
||||||
|
video_codec_preference_string: "",
|
||||||
|
audio_codec_preference_string: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
changeset = Settings.change_setting(setting, new_setting)
|
||||||
|
|
||||||
|
assert [] = changeset.changes.video_codec_preference
|
||||||
|
assert [] = changeset.changes.audio_codec_preference
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -158,6 +158,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it enqueues the job with a small delay", %{source: source} do
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, now()), 5, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "it does not attach tasks if the source is set to not download" do
|
test "it does not attach tasks if the source is set to not download" do
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture(download_media: false)
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
@@ -235,6 +245,26 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker)
|
assert_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sets a small delay on the download job", %{source: source} do
|
||||||
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
||||||
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
|
# Need to add a delay to ensure the file watcher has time to read the file
|
||||||
|
:timer.sleep(watcher_poll_interval * 2)
|
||||||
|
# We know we're testing the file watcher since the syncronous call will only
|
||||||
|
# return an empty string (creating no records)
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
[job | _] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, now()), 5, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "does not enqueue downloads if the source is set to not download" do
|
test "does not enqueue downloads if the source is set to not download" 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)
|
||||||
|
|||||||
@@ -659,83 +659,6 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "change_source/3 when testing regex validation" do
|
|
||||||
test "succeeds when a valid regex is provided" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
assert %{errors: []} = Sources.change_source(source, %{title_filter_regex: "(?i)^How to Bike$"})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "succeeds when a regex is set back to nil" do
|
|
||||||
source = source_fixture(%{title_filter_regex: "(?i)^How to Bike$"})
|
|
||||||
|
|
||||||
assert %{errors: []} = Sources.change_source(source, %{title_filter_regex: nil})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "fails when an invalid regex is provided" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
changeset = Sources.change_source(source, %{title_filter_regex: "*FOO"})
|
|
||||||
|
|
||||||
assert "is invalid" in errors_on(changeset).title_filter_regex
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "change_source/3 when testing original_url validation" do
|
|
||||||
test "succeeds when an original URL is valid" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
valid_urls = [
|
|
||||||
"https://www.youtube.com/channel/UCkRfArvrzheW2E7b6SVT7vQ",
|
|
||||||
"https://www.youtube.com/channel/UCkRfArvrzheW2E7b6SVT7vQ/videos",
|
|
||||||
"https://www.youtube.com/@youtubecreators/featured",
|
|
||||||
"https://www.youtube.com/@youtubecreators",
|
|
||||||
"https://www.youtube.com/c/YouTubeCreators",
|
|
||||||
"https://www.youtube.com/user/YouTubeCreators",
|
|
||||||
"https://www.youtube.com/YouTubeCreators",
|
|
||||||
"https://www.youtube.com/playlist?list=PLpjK416fmKwRtq-9-O_NbZlkW0k6zu2Wn",
|
|
||||||
"https://www.youtube.com/playlist?list=UUkRfArvrzheW2E7b6SVT7vQ"
|
|
||||||
]
|
|
||||||
|
|
||||||
Enum.each(valid_urls, fn url ->
|
|
||||||
assert %{errors: []} = Sources.change_source(source, %{original_url: url})
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "fails when an original URL points to a video" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
invalid_urls = [
|
|
||||||
"https://www.youtube.com/watch?v=72maj9FLQZI",
|
|
||||||
"https://youtu.be/72maj9FLQZI",
|
|
||||||
"https://www.youtube.com/watch?v=1FwGFhMAmBo&list=PLpjK416fmKwRtq-9-O_NbZlkW0k6zu2Wn",
|
|
||||||
"https://www.youtube.com/shorts/Dq0eH-ZhQTU",
|
|
||||||
"https://www.youtube.com/embed/X64LHlfx4qg"
|
|
||||||
]
|
|
||||||
|
|
||||||
Enum.each(invalid_urls, fn url ->
|
|
||||||
assert %{errors: [_]} = Sources.change_source(source, %{original_url: url})
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "passes when a non-youtube link is provided" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
valid_urls = [
|
|
||||||
"https://www.example.com",
|
|
||||||
"https://www.example.com/playlist",
|
|
||||||
"https://www.example.com/channel",
|
|
||||||
"https://www.example.com/user",
|
|
||||||
"https://www.example.com/watch?v=72maj9FLQZI",
|
|
||||||
"https://www.example.com/embed/X64LHlfx4qg"
|
|
||||||
]
|
|
||||||
|
|
||||||
Enum.each(valid_urls, fn url ->
|
|
||||||
assert %{errors: []} = Sources.change_source(source, %{original_url: url})
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp playlist_mock(_url, _opts, _ot) do
|
defp playlist_mock(_url, _opts, _ot) do
|
||||||
{
|
{
|
||||||
:ok,
|
:ok,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
defmodule Pinchflat.Utils.DatetimeUtilsTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.DatetimeUtils
|
||||||
|
|
||||||
|
describe "date_to_datetime/1" do
|
||||||
|
test "converts a Date to a DateTime" do
|
||||||
|
date = ~D[2022-01-01]
|
||||||
|
datetime = DatetimeUtils.date_to_datetime(date)
|
||||||
|
|
||||||
|
assert datetime == ~U[2022-01-01 00:00:00Z]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,8 +7,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
@media_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
|
@media_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
|
||||||
|
|
||||||
describe "download/3" do
|
describe "download/2" do
|
||||||
test "calls the backend runner with the expected arguments" do
|
test "it calls the backend runner with the expected arguments" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, addl ->
|
||||||
assert [:no_simulate] = opts
|
assert [:no_simulate] = opts
|
||||||
assert "after_move:%()j" = ot
|
assert "after_move:%()j" = ot
|
||||||
@@ -20,7 +20,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
assert {:ok, _} = Media.download(@media_url)
|
assert {:ok, _} = Media.download(@media_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "it passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, addl ->
|
||||||
assert [:no_simulate, :custom_arg] = opts
|
assert [:no_simulate, :custom_arg] = opts
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
@@ -31,7 +31,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
assert {:ok, _} = Media.download(@media_url, [:custom_arg], addl_arg: true)
|
assert {:ok, _} = Media.download(@media_url, [:custom_arg], addl_arg: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parses and returns the generated file as JSON" do
|
test "it parses and returns the generated file as JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -40,7 +40,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
Media.download(@media_url)
|
Media.download(@media_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns errors" do
|
test "it returns errors" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
||||||
{:error, "something"}
|
{:error, "something"}
|
||||||
end)
|
end)
|
||||||
@@ -49,37 +49,6 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_thumbnail/2" do
|
|
||||||
test "calls the backend runner with the expected arguments" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot ->
|
|
||||||
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
|
||||||
assert ot == "after_move:%()j"
|
|
||||||
|
|
||||||
{:ok, ""}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, _} = Media.download_thumbnail(@media_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "passes along additional options" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
|
||||||
assert :custom_arg in opts
|
|
||||||
|
|
||||||
{:ok, "{}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, _} = Media.download_thumbnail(@media_url, [:custom_arg])
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns errors" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot ->
|
|
||||||
{:error, "something"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:error, "something"} = Media.download_thumbnail(@media_url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 ->
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
@@ -110,7 +79,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "indexing_output_template/0" do
|
describe "indexing_output_template/0" do
|
||||||
test "contains all the greatest hits" do
|
test "contains all the greatest hits" do
|
||||||
assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" ==
|
assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date})j" ==
|
||||||
Media.indexing_output_template()
|
Media.indexing_output_template()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -125,8 +94,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
"was_live" => false,
|
"was_live" => false,
|
||||||
"aspect_ratio" => 1.0,
|
"aspect_ratio" => 1.0,
|
||||||
"duration" => 60,
|
"duration" => 60,
|
||||||
"upload_date" => "20210101",
|
"upload_date" => "20210101"
|
||||||
"timestamp" => 1_600_000_000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert %Media{
|
assert %Media{
|
||||||
@@ -136,7 +104,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
original_url: "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
original_url: "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||||
livestream: false,
|
livestream: false,
|
||||||
short_form_content: false,
|
short_form_content: false,
|
||||||
uploaded_at: ~U[2020-09-13 12:26:40Z],
|
upload_date: Date.from_iso8601!("2021-01-01"),
|
||||||
duration_seconds: 60
|
duration_seconds: 60
|
||||||
} == Media.response_to_struct(response)
|
} == Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
@@ -178,13 +146,36 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
response = %{
|
response = %{
|
||||||
"webpage_url" => nil,
|
"webpage_url" => nil,
|
||||||
"aspect_ratio" => nil,
|
"aspect_ratio" => nil,
|
||||||
"duration" => nil,
|
"duration" => nil
|
||||||
"upload_date" => "20210101"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert %Media{short_form_content: nil} = Media.response_to_struct(response)
|
assert %Media{short_form_content: nil} = Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "parses the upload date" do
|
||||||
|
response = %{
|
||||||
|
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||||
|
"aspect_ratio" => 1.0,
|
||||||
|
"duration" => 61,
|
||||||
|
"upload_date" => "20210101"
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_date = Date.from_iso8601!("2021-01-01")
|
||||||
|
|
||||||
|
assert %Media{upload_date: ^expected_date} = Media.response_to_struct(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't blow up if upload date is missing" do
|
||||||
|
response = %{
|
||||||
|
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||||
|
"aspect_ratio" => 1.0,
|
||||||
|
"duration" => 61,
|
||||||
|
"upload_date" => nil
|
||||||
|
}
|
||||||
|
|
||||||
|
assert %Media{upload_date: nil} = Media.response_to_struct(response)
|
||||||
|
end
|
||||||
|
|
||||||
test "parses the duration" do
|
test "parses the duration" do
|
||||||
response = %{
|
response = %{
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||||
@@ -218,58 +209,4 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
assert %Media{livestream: false} = Media.response_to_struct(response)
|
assert %Media{livestream: false} = Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "response_to_struct/1 when testing uploaded_at" do
|
|
||||||
test "parses the upload date from the timestamp if present" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => 61,
|
|
||||||
"upload_date" => "20210101",
|
|
||||||
"timestamp" => 1_600_000_000
|
|
||||||
}
|
|
||||||
|
|
||||||
expected_date = ~U[2020-09-13 12:26:40Z]
|
|
||||||
|
|
||||||
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "parses the upload date from the uploaded_at if timestamp is present but nil" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => 61,
|
|
||||||
"upload_date" => "20210101",
|
|
||||||
"timestamp" => nil
|
|
||||||
}
|
|
||||||
|
|
||||||
expected_date = ~U[2021-01-01 00:00:00Z]
|
|
||||||
|
|
||||||
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "parses the upload date from the uploaded_at if timestamp absent" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => 61,
|
|
||||||
"upload_date" => "20210101"
|
|
||||||
}
|
|
||||||
|
|
||||||
expected_date = ~U[2021-01-01 00:00:00Z]
|
|
||||||
|
|
||||||
assert %Media{uploaded_at: ^expected_date} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "doesn't blow up if upload date is missing" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => 61,
|
|
||||||
"upload_date" => nil
|
|
||||||
}
|
|
||||||
|
|
||||||
assert %Media{uploaded_at: nil} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ defmodule PinchflatWeb.MediaItemControllerTest do
|
|||||||
|
|
||||||
assert html_response(conn, 200) =~ "#{media_item.title}"
|
assert html_response(conn, 200) =~ "#{media_item.title}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "renders the page when the media item has no description", %{conn: conn} do
|
|
||||||
media_item = media_item_with_attachments(%{description: nil})
|
|
||||||
conn = get(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item}")
|
|
||||||
|
|
||||||
assert html_response(conn, 200) =~ "#{media_item.title}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "edit media" do
|
describe "edit media" do
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
|
|||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))
|
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))
|
||||||
|
|
||||||
assert html =~ "Showing"
|
assert html =~ "Showing 1 of 1"
|
||||||
assert html =~ "Title"
|
assert html =~ "Title"
|
||||||
assert html =~ media_item.title
|
assert html =~ media_item.title
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9637,7 +9637,7 @@
|
|||||||
"uploader_url": "https://www.youtube.com/@PinchflatTestChannel",
|
"uploader_url": "https://www.youtube.com/@PinchflatTestChannel",
|
||||||
"upload_date": "20210720",
|
"upload_date": "20210720",
|
||||||
"availability": "public",
|
"availability": "public",
|
||||||
"original_url": "https://www.youtube.com/watch?v=ABC123",
|
"original_url": "ABC123",
|
||||||
"webpage_url_basename": "watch",
|
"webpage_url_basename": "watch",
|
||||||
"webpage_url_domain": "youtube.com",
|
"webpage_url_domain": "youtube.com",
|
||||||
"extractor": "youtube",
|
"extractor": "youtube",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ defmodule Pinchflat.MediaFixtures do
|
|||||||
short_form_content: false,
|
short_form_content: false,
|
||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
source_id: SourcesFixtures.source_fixture().id,
|
source_id: SourcesFixtures.source_fixture().id,
|
||||||
uploaded_at: DateTime.utc_now()
|
upload_date: DateTime.utc_now()
|
||||||
})
|
})
|
||||||
|> Pinchflat.Media.create_media_item()
|
|> Pinchflat.Media.create_media_item()
|
||||||
|
|
||||||
@@ -97,8 +97,7 @@ defmodule Pinchflat.MediaFixtures do
|
|||||||
description: "desc1",
|
description: "desc1",
|
||||||
aspect_ratio: 1.67,
|
aspect_ratio: 1.67,
|
||||||
duration: 123.45,
|
duration: 123.45,
|
||||||
upload_date: "20210101",
|
upload_date: "20210101"
|
||||||
timestamp: 1_600_000_000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Phoenix.json_library().encode!(media_attributes)
|
Phoenix.json_library().encode!(media_attributes)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ defmodule Pinchflat.SourcesFixtures do
|
|||||||
collection_type: "channel",
|
collection_type: "channel",
|
||||||
custom_name: "Cool and good internal name!",
|
custom_name: "Cool and good internal name!",
|
||||||
description: "This is a description",
|
description: "This is a description",
|
||||||
original_url: "https://www.youtube.com/@#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||||
index_frequency_minutes: 60
|
index_frequency_minutes: 60
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../assets/vendor/
|
|
||||||
Reference in New Issue
Block a user