Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c70f211232 | |||
| ba0ded03a4 | |||
| 8513deef09 | |||
| 83b9adea59 | |||
| 10a639a976 | |||
| c79e27d9b3 | |||
| 8ef5e0f41f | |||
| 854502634c | |||
| fc8eed8b50 | |||
| f42aab286a | |||
| bf11cd0dd9 | |||
| e051a37f0d | |||
| 64ebf756ab | |||
| 377bc617ea | |||
| a03f69a8d4 | |||
| d39ebe7ac1 | |||
| 8b5dcc15d6 | |||
| 5e58dcd879 | |||
| a9d5407071 | |||
| 37c1da8014 | |||
| 937a14985f | |||
| a6a20c111f | |||
| 2bb96edc00 | |||
| 87f07ff634 | |||
| dd60cb5419 | |||
| 610b00566b | |||
| aba15ed886 | |||
| 87da716fdf | |||
| 45f056c6e0 | |||
| 11d160008c | |||
| ba0a1ba100 | |||
| 8c51a286c9 | |||
| 46c27e4875 | |||
| b639918555 | |||
| 5596351f58 | |||
| ad150b6f16 | |||
| f6d8e15670 | |||
| 2906ff1b30 | |||
| 0582a7bfd5 | |||
| 3db2e8190f | |||
| 087c9c2d0f | |||
| c046abf36f | |||
| 70dd95211f | |||
| 8491f8b317 | |||
| d0cb782082 | |||
| 7a7e8080cd | |||
| fbe21cb304 | |||
| 0f3329e97d | |||
| 25eb772896 | |||
| cf59bf99cd | |||
| 47e96e3780 | |||
| a7af6a9125 | |||
| 8f9d18dc71 | |||
| 513212faf2 | |||
| 5ec2c92a0c | |||
| 3c897e96e6 | |||
| c0885cfaf2 | |||
| b2f39a7b7f | |||
| bada14625b | |||
| 7e4f1a8412 | |||
| 0e537d8f57 | |||
| afe41d2e36 | |||
| f36bd5abab | |||
| b3f094fa69 | |||
| ccdcf8eec5 | |||
| 67d7f397d1 | |||
| 09a4bcb36b | |||
| dc0313d875 | |||
| 1caddd86c7 | |||
| 1c65b41002 | |||
| 6e195f6a6f | |||
| d0f55cd463 | |||
| 39caf4a94f | |||
| 254054c0c1 | |||
| 5c9933b684 | |||
| cbade0dd02 | |||
| 63e5058365 | |||
| a46dd19ec7 | |||
| 55f9676bb7 | |||
| fc52f16475 | |||
| 8cc36928f1 | |||
| 1ad9e2c331 | |||
| 6738071613 | |||
| b54e69c609 | |||
| 5b29f5036a | |||
| 9fc2edfd4a | |||
| 01b8856f22 | |||
| 3bb7edba0b | |||
| 52c0a1742a | |||
| e33a46bddc | |||
| 2ca395a5bf | |||
| ebebbf7571 | |||
| b8cec51c3b | |||
| 0d29f70a28 | |||
| c1f30dbd2c | |||
| fdb83ed3f3 | |||
| 94f777d1ed | |||
| 47f0fd533f | |||
| 7d62f7c7df | |||
| d4d653398b | |||
| fab77f4deb | |||
| 8fcd41f45d | |||
| 44bd105e80 | |||
| 9d3b9cc063 | |||
| 060e340558 | |||
| 219320ce11 | |||
| 8bdd189fa5 | |||
| dabec62e99 | |||
| 89497c4c6b | |||
| 013b2ccc21 | |||
| ca01f17b58 | |||
| bdef6c75bb | |||
| d25e65f822 | |||
| a5e7c48f70 | |||
| fee39151a8 | |||
| 4827da3106 | |||
| 536f868ba0 |
@@ -0,0 +1,54 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
image_tag:
|
||||||
|
type: choice
|
||||||
|
description: 'Docker Image Tag'
|
||||||
|
required: true
|
||||||
|
default: 'dev'
|
||||||
|
options:
|
||||||
|
- 'dev'
|
||||||
|
- 'latest'
|
||||||
|
platforms:
|
||||||
|
type: choice
|
||||||
|
description: 'Build Platforms'
|
||||||
|
required: true
|
||||||
|
default: 'linux/amd64,linux/arm64'
|
||||||
|
options:
|
||||||
|
- 'linux/amd64,linux/arm64'
|
||||||
|
- 'linux/amd64'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
platforms: ${{ github.event.inputs.platforms }}
|
||||||
|
push: true
|
||||||
|
file: ./selfhosted.Dockerfile
|
||||||
|
tags: |
|
||||||
|
keglin/pinchflat:${{ github.event.inputs.image_tag }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/pinchflat:${{ github.event.inputs.image_tag }}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Docker Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
platforms:
|
|
||||||
type: choice
|
|
||||||
description: 'Build Platforms'
|
|
||||||
required: true
|
|
||||||
default: 'linux/amd64'
|
|
||||||
options:
|
|
||||||
- 'linux/amd64'
|
|
||||||
- 'linux/amd64,linux/arm64'
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_push_docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
dev_arch: 'linux/amd64'
|
|
||||||
release_arch: 'linux/amd64,linux/arm64'
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
docker.io/keglin/pinchflat
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
# All non-release actions will be tagged as `dev` (ie: push, workflow_dispatch)
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}},prefix=v
|
|
||||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
|
||||||
type=semver,pattern={{major}},prefix=v
|
|
||||||
type=raw,value=dev,enable=${{ github.event_name != 'release' }}
|
|
||||||
flavor: |
|
|
||||||
latest=auto
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and Push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./selfhosted.Dockerfile
|
|
||||||
# If the event is a release, use the release_arch, otherwise use the
|
|
||||||
# platforms input if present, falling back to dev_arch
|
|
||||||
platforms: ${{ github.event_name == 'release' && env.release_arch || (github.event.inputs.platforms || env.dev_arch) }}
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
@@ -51,7 +51,6 @@ If it doesn't work for your use case, please make a feature request! You can als
|
|||||||
- Advanced options like setting cutoff dates and filtering by title
|
- Advanced options like setting cutoff dates and filtering by title
|
||||||
- Reliable hands-off operation
|
- Reliable hands-off operation
|
||||||
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
|
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
|
||||||
- Sponsorblock integration
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -66,8 +65,6 @@ Simply search for Pinchflat in the Community Apps store!
|
|||||||
|
|
||||||
### Portainer
|
### Portainer
|
||||||
|
|
||||||
Important: See the note below about storing config on a network file share. It's preferred to store the config on a local disk if at all possible.
|
|
||||||
|
|
||||||
Docker Compose file:
|
Docker Compose file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -107,14 +104,6 @@ You _must_ ensure the host directories you've mounted are writable by the user r
|
|||||||
|
|
||||||
It's recommended to not run the container as root. Doing so can create permission issues if other apps need to work with the downloaded media. If you need to run any command as root, you can run `su` from the container's shell as there is no password set for the root user.
|
It's recommended to not run the container as root. Doing so can create permission issues if other apps need to work with the downloaded media. If you need to run any command as root, you can run `su` from the container's shell as there is no password set for the root user.
|
||||||
|
|
||||||
### Advanced: storing Pinchflat config directory on a network share
|
|
||||||
|
|
||||||
README: This is currently in the testing phase and not a recommended option (yet). The implications of changing this setting isn't clear and this could, conceivably, result in data loss. Only change this setting if you know what you're doing, why this is important, and are okay with possible data loss or DB corruption. This may become the default in the future once it's been tested more thoroughly.
|
|
||||||
|
|
||||||
As pointed out in [#137](https://github.com/kieraneglin/pinchflat/issues/137), SQLite doesn't like being run in WAL mode on network shares. If you're running Pinchflat on a network share, you can disable WAL mode by setting the `JOURNAL_MODE` environment variable to `delete`. This will make Pinchflat run in rollback journal mode which is less performant but should work on network shares.
|
|
||||||
|
|
||||||
If you change this setting and it works well for you, please leave a comment on [#137](https://github.com/kieraneglin/pinchflat/issues/137)! Doubly so if it does _not_ work well.
|
|
||||||
|
|
||||||
## EFF donations
|
## EFF donations
|
||||||
|
|
||||||
A portion of all donations to Pinchflat will be donated to the [Electronic Frontier Foundation](https://www.eff.org/). The EFF defends your online liberties and [backed](https://github.com/github/dmca/blob/9a85e0f021f7967af80e186b890776a50443f06c/2020/11/2020-11-16-RIAA-reversal-effletter.pdf) `youtube-dl` when Google took them down. [See here](https://github.com/kieraneglin/pinchflat/wiki/EFF-Donation-Receipts) for a list of donation receipts.
|
A portion of all donations to Pinchflat will be donated to the [Electronic Frontier Foundation](https://www.eff.org/). The EFF defends your online liberties and [backed](https://github.com/github/dmca/blob/9a85e0f021f7967af80e186b890776a50443f06c/2020/11/2020-11-16-RIAA-reversal-effletter.pdf) `youtube-dl` when Google took them down. [See here](https://github.com/kieraneglin/pinchflat/wiki/EFF-Donation-Receipts) for a list of donation receipts.
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ config :pinchflat,
|
|||||||
expose_feed_endpoints: false,
|
expose_feed_endpoints: false,
|
||||||
file_watcher_poll_interval: 1000
|
file_watcher_poll_interval: 1000
|
||||||
|
|
||||||
config :pinchflat, Pinchflat.Repo,
|
|
||||||
journal_mode: :wal,
|
|
||||||
pool_size: 5
|
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :pinchflat, PinchflatWeb.Endpoint,
|
config :pinchflat, PinchflatWeb.Endpoint,
|
||||||
url: [host: "localhost", port: 8945],
|
url: [host: "localhost", port: 8945],
|
||||||
|
|||||||
+10
-4
@@ -50,10 +50,16 @@ if config_env() == :prod do
|
|||||||
# 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)
|
# We want to force _some_ level of useful logging in production
|
||||||
journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal"))
|
acceptable_log_levels = ~w(debug info)a
|
||||||
|
log_level = String.to_existing_atom(System.get_env("LOG_LEVEL", "info"))
|
||||||
|
|
||||||
config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "info"))
|
if log_level in acceptable_log_levels do
|
||||||
|
config :logger, level: log_level
|
||||||
|
else
|
||||||
|
Logger.error("Invalid log level: #{log_level}. Defaulting to info.")
|
||||||
|
config :logger, level: :info
|
||||||
|
end
|
||||||
|
|
||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||||
@@ -66,7 +72,7 @@ if config_env() == :prod do
|
|||||||
|
|
||||||
config :pinchflat, Pinchflat.Repo,
|
config :pinchflat, Pinchflat.Repo,
|
||||||
database: db_path,
|
database: db_path,
|
||||||
journal_mode: journal_mode
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
|
||||||
|
|
||||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||||
# A default value is used in config/dev.exs and config/test.exs but you
|
# A default value is used in config/dev.exs and config/test.exs but you
|
||||||
|
|||||||
+3
-1
@@ -17,6 +17,7 @@ config :pinchflat, Oban, testing: :manual
|
|||||||
# Run `mix help test` for more information.
|
# Run `mix help test` for more information.
|
||||||
config :pinchflat, Pinchflat.Repo,
|
config :pinchflat, Pinchflat.Repo,
|
||||||
database: Path.expand("../priv/repo/pinchflat_test.db", Path.dirname(__ENV__.file)),
|
database: Path.expand("../priv/repo/pinchflat_test.db", Path.dirname(__ENV__.file)),
|
||||||
|
pool_size: 5,
|
||||||
pool: Ecto.Adapters.SQL.Sandbox
|
pool: Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
@@ -32,7 +33,8 @@ config :pinchflat, Pinchflat.Mailer, adapter: Swoosh.Adapters.Test
|
|||||||
# Disable swoosh api client as it is only required for production adapters.
|
# Disable swoosh api client as it is only required for production adapters.
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
config :logger, level: :critical
|
# Print only warnings and errors during test
|
||||||
|
config :logger, level: :warning
|
||||||
|
|
||||||
# Initialize plugs at runtime for faster test compilation
|
# Initialize plugs at runtime for faster test compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
thumbnail_options(media_item_with_preloads) ++
|
thumbnail_options(media_item_with_preloads) ++
|
||||||
metadata_options(media_profile) ++
|
metadata_options(media_profile) ++
|
||||||
quality_options(media_profile) ++
|
quality_options(media_profile) ++
|
||||||
sponsorblock_options(media_profile) ++
|
|
||||||
output_options(media_item_with_preloads)
|
output_options(media_item_with_preloads)
|
||||||
|
|
||||||
{:ok, built_options}
|
{:ok, built_options}
|
||||||
@@ -117,17 +116,6 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sponsorblock_options(media_profile) do
|
|
||||||
categories = media_profile.sponsorblock_categories
|
|
||||||
behaviour = media_profile.sponsorblock_behaviour
|
|
||||||
|
|
||||||
case {behaviour, categories} do
|
|
||||||
{_, []} -> []
|
|
||||||
{:remove, _} -> [sponsorblock_remove: Enum.join(categories, ",")]
|
|
||||||
{:disabled, _} -> []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp output_options(media_item_with_preloads) do
|
defp output_options(media_item_with_preloads) do
|
||||||
[
|
[
|
||||||
output: build_output_path_for(media_item_with_preloads.source)
|
output: build_output_path_for(media_item_with_preloads.source)
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
{:ok, updated_media_item}
|
{:ok, updated_media_item}
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
Logger.error("Failed to download media for media item #{media_item.id}: #{inspect(err)}")
|
err
|
||||||
|
|
||||||
{:error, :download_failed}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
Many of these methods are made to be kickoff or be consumed by workers.
|
Many of these methods are made to be kickoff or be consumed by workers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaQuery
|
|
||||||
alias Pinchflat.FastIndexing.YoutubeRss
|
alias Pinchflat.FastIndexing.YoutubeRss
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.FastIndexing.MediaIndexingWorker
|
alias Pinchflat.FastIndexing.MediaIndexingWorker
|
||||||
@@ -29,7 +27,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
"""
|
"""
|
||||||
def kickoff_indexing_tasks_from_youtube_rss_feed(%Source{} = source) do
|
def kickoff_indexing_tasks_from_youtube_rss_feed(%Source{} = source) do
|
||||||
{:ok, media_ids} = YoutubeRss.get_recent_media_ids_from_rss(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 = Media.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)
|
||||||
|
|
||||||
Enum.each(new_media_ids, fn media_id ->
|
Enum.each(new_media_ids, fn media_id ->
|
||||||
@@ -63,13 +61,6 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp list_media_items_by_media_id_for(source, media_ids) do
|
|
||||||
MediaQuery.new()
|
|
||||||
|> MediaQuery.for_source(source)
|
|
||||||
|> MediaQuery.with_media_ids(media_ids)
|
|
||||||
|> Repo.all()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_media_item_from_url(source, url) do
|
defp create_media_item_from_url(source, url) do
|
||||||
{:ok, media_attrs} = YtDlpMedia.get_media_attributes(url)
|
{:ok, media_attrs} = YtDlpMedia.get_media_attributes(url)
|
||||||
|
|
||||||
|
|||||||
+103
-20
@@ -9,7 +9,6 @@ defmodule Pinchflat.Media do
|
|||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Media.MediaQuery
|
|
||||||
alias Pinchflat.Metadata.MediaMetadata
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
alias Pinchflat.Filesystem.FilesystemHelpers
|
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||||
|
|
||||||
@@ -22,6 +21,33 @@ defmodule Pinchflat.Media do
|
|||||||
Repo.all(MediaItem)
|
Repo.all(MediaItem)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a list of media_items for a given source.
|
||||||
|
|
||||||
|
Returns [%MediaItem{}, ...].
|
||||||
|
"""
|
||||||
|
def list_media_items_for(%Source{} = source) do
|
||||||
|
MediaItem
|
||||||
|
|> where([mi], mi.source_id == ^source.id)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches all media items belonging to a given source that have a media_id in the given list.
|
||||||
|
Useful for determining the what media items we DON'T already have for fast indexing.
|
||||||
|
|
||||||
|
NOTE: These queries are getting a little tedious. When I have the time, I should see about
|
||||||
|
implementing a query pattern and having these compose queries from a common base. This would
|
||||||
|
also let me compose simple queries in the module using them for one-off methods
|
||||||
|
|
||||||
|
Returns [%MediaItem{}, ...].
|
||||||
|
"""
|
||||||
|
def list_media_items_by_media_id_for(%Source{} = source, media_ids) do
|
||||||
|
MediaItem
|
||||||
|
|> where([mi], mi.source_id == ^source.id and mi.media_id in ^media_ids)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of pending media_items for a given source, where
|
Returns a list of pending media_items for a given source, where
|
||||||
pending means the `media_filepath` is `nil` AND the media_item
|
pending means the `media_filepath` is `nil` AND the media_item
|
||||||
@@ -34,11 +60,13 @@ defmodule Pinchflat.Media do
|
|||||||
"""
|
"""
|
||||||
def list_pending_media_items_for(%Source{} = source, opts \\ []) do
|
def list_pending_media_items_for(%Source{} = source, opts \\ []) do
|
||||||
limit = Keyword.get(opts, :limit, nil)
|
limit = Keyword.get(opts, :limit, nil)
|
||||||
source = Repo.preload(source, :media_profile)
|
media_profile = Repo.preload(source, :media_profile).media_profile
|
||||||
|
|
||||||
MediaQuery.new()
|
MediaItem
|
||||||
|> MediaQuery.for_source(source)
|
|> where([mi], mi.source_id == ^source.id and is_nil(mi.media_filepath))
|
||||||
|> matching_download_criteria_for(source)
|
|> where(^build_format_clauses(media_profile))
|
||||||
|
|> where(^maybe_apply_cutoff_date(source))
|
||||||
|
|> where(^maybe_apply_title_regex(source))
|
||||||
|> Repo.maybe_limit(limit)
|
|> Repo.maybe_limit(limit)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
@@ -51,9 +79,8 @@ defmodule Pinchflat.Media do
|
|||||||
def list_downloaded_media_items_for(%Source{} = source, opts \\ []) do
|
def list_downloaded_media_items_for(%Source{} = source, opts \\ []) do
|
||||||
limit = Keyword.get(opts, :limit, nil)
|
limit = Keyword.get(opts, :limit, nil)
|
||||||
|
|
||||||
MediaQuery.new()
|
MediaItem
|
||||||
|> MediaQuery.for_source(source)
|
|> where([mi], mi.source_id == ^source.id and not is_nil(mi.media_filepath))
|
||||||
|> MediaQuery.with_media_filepath()
|
|
||||||
|> Repo.maybe_limit(limit)
|
|> Repo.maybe_limit(limit)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
@@ -70,9 +97,11 @@ defmodule Pinchflat.Media do
|
|||||||
def pending_download?(%MediaItem{} = media_item) do
|
def pending_download?(%MediaItem{} = media_item) do
|
||||||
media_item = Repo.preload(media_item, source: :media_profile)
|
media_item = Repo.preload(media_item, source: :media_profile)
|
||||||
|
|
||||||
MediaQuery.new()
|
MediaItem
|
||||||
|> MediaQuery.with_id(media_item.id)
|
|> where([mi], mi.id == ^media_item.id and is_nil(mi.media_filepath))
|
||||||
|> matching_download_criteria_for(media_item.source)
|
|> where(^build_format_clauses(media_item.source.media_profile))
|
||||||
|
|> where(^maybe_apply_cutoff_date(media_item.source))
|
||||||
|
|> where(^maybe_apply_title_regex(media_item.source))
|
||||||
|> Repo.exists?()
|
|> Repo.exists?()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -91,9 +120,20 @@ defmodule Pinchflat.Media do
|
|||||||
def search(search_term, opts) do
|
def search(search_term, opts) do
|
||||||
limit = Keyword.get(opts, :limit, 50)
|
limit = Keyword.get(opts, :limit, 50)
|
||||||
|
|
||||||
MediaQuery.new()
|
from(mi in MediaItem,
|
||||||
|> MediaQuery.matching_search_term(search_term)
|
join: mi_search_index in assoc(mi, :media_items_search_index),
|
||||||
|> Repo.maybe_limit(limit)
|
where: fragment("media_items_search_index MATCH ?", ^search_term),
|
||||||
|
select_merge: %{
|
||||||
|
matching_search_term:
|
||||||
|
fragment("""
|
||||||
|
coalesce(snippet(media_items_search_index, 0, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '') ||
|
||||||
|
' ' ||
|
||||||
|
coalesce(snippet(media_items_search_index, 1, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '')
|
||||||
|
""")
|
||||||
|
},
|
||||||
|
order_by: [desc: fragment("rank")],
|
||||||
|
limit: ^limit
|
||||||
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -201,11 +241,54 @@ defmodule Pinchflat.Media do
|
|||||||
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp matching_download_criteria_for(query, source_with_preloads) do
|
defp maybe_apply_cutoff_date(source) do
|
||||||
query
|
if source.download_cutoff_date do
|
||||||
|> MediaQuery.with_no_media_filepath()
|
dynamic([mi], mi.upload_date >= ^source.download_cutoff_date)
|
||||||
|> MediaQuery.with_upload_date_after(source_with_preloads.download_cutoff_date)
|
else
|
||||||
|> MediaQuery.with_format_preference(source_with_preloads.media_profile)
|
dynamic(true)
|
||||||
|> MediaQuery.matching_title_regex(source_with_preloads.title_filter_regex)
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_apply_title_regex(source) do
|
||||||
|
if source.title_filter_regex do
|
||||||
|
dynamic([mi], fragment("regexp_like(?, ?)", mi.title, ^source.title_filter_regex))
|
||||||
|
else
|
||||||
|
dynamic(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_format_clauses(media_profile) do
|
||||||
|
mapped_struct = Map.from_struct(media_profile)
|
||||||
|
|
||||||
|
Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic ->
|
||||||
|
case {attr, media_profile} do
|
||||||
|
{{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} ->
|
||||||
|
dynamic(
|
||||||
|
[mi],
|
||||||
|
^dynamic and (mi.livestream == true or mi.short_form_content == true)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Technically redundant, but makes the other clauses easier to parse
|
||||||
|
# (redundant because this condition is the same as the condition above, just flipped)
|
||||||
|
{{:livestream_behaviour, :only}, %{shorts_behaviour: :only}} ->
|
||||||
|
dynamic
|
||||||
|
|
||||||
|
{{:shorts_behaviour, :only}, _} ->
|
||||||
|
dynamic([mi], ^dynamic and mi.short_form_content == true)
|
||||||
|
|
||||||
|
{{:livestream_behaviour, :only}, _} ->
|
||||||
|
dynamic([mi], ^dynamic and mi.livestream == true)
|
||||||
|
|
||||||
|
{{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only ->
|
||||||
|
dynamic([mi], ^dynamic and mi.short_form_content == false)
|
||||||
|
|
||||||
|
{{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only ->
|
||||||
|
# return records with livestream: false
|
||||||
|
dynamic([mi], ^dynamic and mi.livestream == false)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
dynamic
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
:source_id,
|
:source_id,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:upload_date,
|
:upload_date,
|
||||||
:duration_seconds,
|
|
||||||
# these fields are captured only on download
|
# these fields are captured only on download
|
||||||
:media_downloaded_at,
|
:media_downloaded_at,
|
||||||
:media_filepath,
|
:media_filepath,
|
||||||
@@ -58,7 +57,6 @@ 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 :upload_date, :date
|
field :upload_date, :date
|
||||||
field :duration_seconds, :integer
|
|
||||||
|
|
||||||
field :media_filepath, :string
|
field :media_filepath, :string
|
||||||
field :media_size_bytes, :integer
|
field :media_size_bytes, :integer
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
defmodule Pinchflat.Media.MediaQuery do
|
|
||||||
@moduledoc """
|
|
||||||
Query helpers for the Media context.
|
|
||||||
|
|
||||||
These methods are made to be one-ish liners used
|
|
||||||
to compose queries for media items. Each method should
|
|
||||||
strive to do _one_ thing. These don't need to be tested
|
|
||||||
as they are just building blocks for other functionality
|
|
||||||
which, itself, will be tested.
|
|
||||||
|
|
||||||
ALSO, this is me trying something new. If I like it,
|
|
||||||
I'll refactor other contexts to use this pattern.
|
|
||||||
"""
|
|
||||||
import Ecto.Query, warn: false
|
|
||||||
|
|
||||||
alias Pinchflat.Media.MediaItem
|
|
||||||
|
|
||||||
# Prefixes:
|
|
||||||
# - for_* - belonging to a certain record
|
|
||||||
# - with_* - for filtering based on full, concrete attributes
|
|
||||||
# - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search)
|
|
||||||
#
|
|
||||||
# Suffixes:
|
|
||||||
# - _for - the arg passed is an association record
|
|
||||||
|
|
||||||
def new do
|
|
||||||
MediaItem
|
|
||||||
end
|
|
||||||
|
|
||||||
def for_source(query, source) do
|
|
||||||
where(query, [mi], mi.source_id == ^source.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_id(query, id) do
|
|
||||||
where(query, [mi], mi.id == ^id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_media_ids(query, media_ids) do
|
|
||||||
where(query, [mi], mi.media_id in ^media_ids)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_media_filepath(query) do
|
|
||||||
where(query, [mi], not is_nil(mi.media_filepath))
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_no_media_filepath(query) do
|
|
||||||
where(query, [mi], is_nil(mi.media_filepath))
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_upload_date_after(query, nil), do: query
|
|
||||||
|
|
||||||
def with_upload_date_after(query, date) do
|
|
||||||
where(query, [mi], mi.upload_date >= ^date)
|
|
||||||
end
|
|
||||||
|
|
||||||
def matching_title_regex(query, nil), do: query
|
|
||||||
|
|
||||||
def matching_title_regex(query, regex) do
|
|
||||||
where(query, [mi], fragment("regexp_like(?, ?)", mi.title, ^regex))
|
|
||||||
end
|
|
||||||
|
|
||||||
def matching_search_term(query, nil), do: query
|
|
||||||
|
|
||||||
def matching_search_term(query, term) do
|
|
||||||
from(mi in query,
|
|
||||||
join: mi_search_index in assoc(mi, :media_items_search_index),
|
|
||||||
where: fragment("media_items_search_index MATCH ?", ^term),
|
|
||||||
select_merge: %{
|
|
||||||
matching_search_term:
|
|
||||||
fragment("""
|
|
||||||
coalesce(snippet(media_items_search_index, 0, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '') ||
|
|
||||||
' ' ||
|
|
||||||
coalesce(snippet(media_items_search_index, 1, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '')
|
|
||||||
""")
|
|
||||||
},
|
|
||||||
order_by: [desc: fragment("rank")]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# NOTE: this method breaks the contract set by other methods in that it
|
|
||||||
# takes a media_profile struct instead of taking just the attributes it
|
|
||||||
# cares about. Consider refactoring but low priority.
|
|
||||||
def with_format_preference(query, media_profile) do
|
|
||||||
mapped_struct = Map.from_struct(media_profile)
|
|
||||||
|
|
||||||
finders =
|
|
||||||
Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic ->
|
|
||||||
case {attr, media_profile} do
|
|
||||||
{{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} ->
|
|
||||||
dynamic(
|
|
||||||
[mi],
|
|
||||||
^dynamic and (mi.livestream == true or mi.short_form_content == true)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Technically redundant, but makes the other clauses easier to parse
|
|
||||||
# (redundant because this condition is the same as the condition above, just flipped)
|
|
||||||
{{:livestream_behaviour, :only}, %{shorts_behaviour: :only}} ->
|
|
||||||
dynamic
|
|
||||||
|
|
||||||
{{:shorts_behaviour, :only}, _} ->
|
|
||||||
dynamic([mi], ^dynamic and mi.short_form_content == true)
|
|
||||||
|
|
||||||
{{:livestream_behaviour, :only}, _} ->
|
|
||||||
dynamic([mi], ^dynamic and mi.livestream == true)
|
|
||||||
|
|
||||||
{{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only ->
|
|
||||||
dynamic([mi], ^dynamic and mi.short_form_content == false)
|
|
||||||
|
|
||||||
{{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only ->
|
|
||||||
# return records with livestream: false
|
|
||||||
dynamic([mi], ^dynamic and mi.livestream == false)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
dynamic
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
where(query, ^finders)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -30,8 +30,7 @@ defmodule Pinchflat.Metadata.MetadataParser do
|
|||||||
original_url: metadata["original_url"],
|
original_url: metadata["original_url"],
|
||||||
description: metadata["description"],
|
description: metadata["description"],
|
||||||
media_filepath: metadata["filepath"],
|
media_filepath: metadata["filepath"],
|
||||||
livestream: metadata["was_live"],
|
livestream: metadata["was_live"]
|
||||||
duration_seconds: metadata["duration"] && round(metadata["duration"])
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -43,25 +43,25 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
<channel>
|
<channel>
|
||||||
<title>#{safe(source.custom_name)}</title>
|
<title>#{safe(source.custom_name)}</title>
|
||||||
<link>#{safe(source.original_url)}</link>
|
<link>#{source.original_url}</link>
|
||||||
<description>#{safe(source.description)}</description>
|
<description>#{safe(source.description)}</description>
|
||||||
<category>TV & Film</category>
|
<category>TV & Film</category>
|
||||||
<generator>Generated by Pinchflat</generator>
|
<generator>Generated by Pinchflat</generator>
|
||||||
<language>en-us</language>
|
<language>en-us</language>
|
||||||
<lastBuildDate>#{Calendar.strftime(source.updated_at, @datetime_format)}</lastBuildDate>
|
<lastBuildDate>#{Calendar.strftime(source.updated_at, @datetime_format)}</lastBuildDate>
|
||||||
<pubDate>#{Calendar.strftime(source.inserted_at, @datetime_format)}</pubDate>
|
<pubDate>#{Calendar.strftime(source.inserted_at, @datetime_format)}</pubDate>
|
||||||
<atom:link href="#{safe(generate_self_link(url_base, source))}" rel="self" type="application/rss+xml" />
|
<atom:link href="#{generate_self_link(url_base, source)}" rel="self" type="application/rss+xml" />
|
||||||
<podcast:locked>yes</podcast:locked>
|
<podcast:locked>yes</podcast:locked>
|
||||||
<podcast:guid>#{source.uuid}</podcast:guid>
|
<podcast:guid>#{source.uuid}</podcast:guid>
|
||||||
<image>
|
<image>
|
||||||
<url>#{safe(feed_image_path)}</url>
|
<url>#{feed_image_path}</url>
|
||||||
<title>#{safe(source.custom_name)}</title>
|
<title>#{safe(source.custom_name)}</title>
|
||||||
<link>#{safe(source.original_url)}</link>
|
<link>#{source.original_url}</link>
|
||||||
</image>
|
</image>
|
||||||
<itunes:author>#{safe(source.custom_name)}</itunes:author>
|
<itunes:author>#{safe(source.custom_name)}</itunes:author>
|
||||||
<itunes:subtitle>#{safe(source.custom_name)}</itunes:subtitle>
|
<itunes:subtitle>#{safe(source.custom_name)}</itunes:subtitle>
|
||||||
<itunes:block>yes</itunes:block>
|
<itunes:block>yes</itunes:block>
|
||||||
<itunes:image href="#{safe(feed_image_path)}"></itunes:image>
|
<itunes:image href="#{feed_image_path}"></itunes:image>
|
||||||
<itunes:explicit>false</itunes:explicit>
|
<itunes:explicit>false</itunes:explicit>
|
||||||
<itunes:category text="TV & Film"></itunes:category>
|
<itunes:category text="TV & Film"></itunes:category>
|
||||||
|
|
||||||
@@ -77,10 +77,9 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
<item>
|
<item>
|
||||||
<guid isPermaLink="false">#{media_item.uuid}</guid>
|
<guid isPermaLink="false">#{media_item.uuid}</guid>
|
||||||
<title>#{safe(media_item.title)}</title>
|
<title>#{safe(media_item.title)}</title>
|
||||||
<link>#{safe(media_item.original_url)}</link>
|
<link>#{media_item.original_url}</link>
|
||||||
<description>#{safe(media_item.description)}</description>
|
<description>#{safe(media_item.description)}</description>
|
||||||
<pubDate>#{generate_upload_date(media_item)}</pubDate>
|
<pubDate>#{generate_upload_date(media_item)}</pubDate>
|
||||||
<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)}"
|
||||||
length="#{media_item.media_size_bytes}"
|
length="#{media_item.media_size_bytes}"
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
download_metadata
|
download_metadata
|
||||||
embed_metadata
|
embed_metadata
|
||||||
download_nfo
|
download_nfo
|
||||||
sponsorblock_behaviour
|
|
||||||
sponsorblock_categories
|
|
||||||
shorts_behaviour
|
shorts_behaviour
|
||||||
livestream_behaviour
|
livestream_behaviour
|
||||||
preferred_resolution
|
preferred_resolution
|
||||||
@@ -49,8 +47,6 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
field :embed_metadata, :boolean, default: false
|
field :embed_metadata, :boolean, default: false
|
||||||
|
|
||||||
field :download_nfo, :boolean, default: false
|
field :download_nfo, :boolean, default: false
|
||||||
field :sponsorblock_behaviour, Ecto.Enum, values: [:disabled, :remove], default: :disabled
|
|
||||||
field :sponsorblock_categories, {:array, :string}, default: []
|
|
||||||
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
||||||
# through the entire collection to determine if a media is a short or
|
# through the entire collection to determine if a media is a short or
|
||||||
# a livestream.
|
# a livestream.
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ defmodule Pinchflat.Release do
|
|||||||
def check_file_permissions do
|
def check_file_permissions do
|
||||||
load_app()
|
load_app()
|
||||||
|
|
||||||
directories =
|
directories = [
|
||||||
Enum.uniq([
|
"/config",
|
||||||
"/config",
|
"/downloads",
|
||||||
"/downloads",
|
Application.get_env(:pinchflat, :media_directory),
|
||||||
Application.get_env(:pinchflat, :media_directory),
|
Application.get_env(:pinchflat, :tmpfile_directory),
|
||||||
Application.get_env(:pinchflat, :tmpfile_directory),
|
Application.get_env(:pinchflat, :extras_directory),
|
||||||
Application.get_env(:pinchflat, :extras_directory),
|
Application.get_env(:pinchflat, :metadata_directory)
|
||||||
Application.get_env(:pinchflat, :metadata_directory)
|
]
|
||||||
])
|
|
||||||
|
|
||||||
Enum.each(directories, fn dir ->
|
Enum.each(directories, fn dir ->
|
||||||
Logger.info("Checking permissions for #{dir}")
|
Logger.info("Checking permissions for #{dir}")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ defmodule Pinchflat.Sources do
|
|||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaQuery
|
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
alias Pinchflat.YtDlp.MediaCollection
|
alias Pinchflat.YtDlp.MediaCollection
|
||||||
alias Pinchflat.Metadata.SourceMetadata
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
@@ -113,9 +112,8 @@ defmodule Pinchflat.Sources do
|
|||||||
delete_files = Keyword.get(opts, :delete_files, false)
|
delete_files = Keyword.get(opts, :delete_files, false)
|
||||||
Tasks.delete_tasks_for(source)
|
Tasks.delete_tasks_for(source)
|
||||||
|
|
||||||
MediaQuery.new()
|
source
|
||||||
|> MediaQuery.for_source(source)
|
|> Media.list_media_items_for()
|
||||||
|> Repo.all()
|
|
||||||
|> Enum.each(fn media_item ->
|
|> Enum.each(fn media_item ->
|
||||||
Media.delete_media_item(media_item, delete_files: delete_files)
|
Media.delete_media_item(media_item, delete_files: delete_files)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
:original_url,
|
:original_url,
|
||||||
:livestream,
|
:livestream,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:upload_date,
|
:upload_date
|
||||||
:duration_seconds
|
|
||||||
]
|
]
|
||||||
|
|
||||||
defstruct [
|
defstruct [
|
||||||
@@ -21,8 +20,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
:original_url,
|
:original_url,
|
||||||
:livestream,
|
:livestream,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:upload_date,
|
:upload_date
|
||||||
:duration_seconds
|
|
||||||
]
|
]
|
||||||
|
|
||||||
alias __MODULE__
|
alias __MODULE__
|
||||||
@@ -88,7 +86,6 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
description: response["description"],
|
description: response["description"],
|
||||||
original_url: response["webpage_url"],
|
original_url: response["webpage_url"],
|
||||||
livestream: response["was_live"],
|
livestream: response["was_live"],
|
||||||
duration_seconds: response["duration"] && round(response["duration"]),
|
|
||||||
short_form_content: response["webpage_url"] && short_form_content?(response),
|
short_form_content: response["webpage_url"] && short_form_content?(response),
|
||||||
upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"])
|
upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
attr :type, :string,
|
attr :type, :string,
|
||||||
default: "text",
|
default: "text",
|
||||||
values: ~w(checkbox color date datetime-local email file hidden month number password
|
values: ~w(checkbox color date datetime-local email file hidden month number password
|
||||||
checkbox_group toggle range radio search select tel text textarea time url week)
|
toggle range radio search select tel text textarea time url week)
|
||||||
|
|
||||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||||
|
|
||||||
@@ -304,33 +304,6 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def input(%{type: "checkbox_group"} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<div phx-feedback-for={@name}>
|
|
||||||
<.label for={@id}>
|
|
||||||
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
|
||||||
</.label>
|
|
||||||
<section class="grid grid-cols-1 gap-2 md:grid-cols-2 max-w-prose mb-4 ml-1">
|
|
||||||
<div :for={{option_name, option_value} <- @options} class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={"#{@id}-#{option_value}"}
|
|
||||||
name={"#{@name}[]"}
|
|
||||||
value={option_value}
|
|
||||||
checked={option_value in @value}
|
|
||||||
class={["rounded focus:ring-offset-0 ring-offset-0 focus:ring-0 h-5 w-5 ", @inputclass]}
|
|
||||||
/>
|
|
||||||
<label for={"#{@id}-#{option_value}"} class="ml-2 cursor-pointer select-none">
|
|
||||||
<%= option_name %>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<.help :if={@help}><%= @help %></.help>
|
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def input(%{type: "toggle"} = assigns) do
|
def input(%{type: "toggle"} = assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
assign_new(assigns, :checked, fn ->
|
assign_new(assigns, :checked, fn ->
|
||||||
@@ -628,10 +601,6 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
_ ->
|
_ ->
|
||||||
true
|
true
|
||||||
end)
|
end)
|
||||||
|> Enum.map(fn
|
|
||||||
{k, v} when is_list(v) -> {k, Enum.join(v, ", ")}
|
|
||||||
rest -> rest
|
|
||||||
end)
|
|
||||||
|
|
||||||
assigns = assign(assigns, iterable_attributes: attrs)
|
assigns = assign(assigns, iterable_attributes: attrs)
|
||||||
|
|
||||||
|
|||||||
@@ -32,26 +32,6 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def friendly_sponsorblock_options do
|
|
||||||
[
|
|
||||||
{"Disabled (default)", "disabled"},
|
|
||||||
{"Remove Segments", "remove"}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
def frieldly_sponsorblock_categories do
|
|
||||||
[
|
|
||||||
{"Sponsor", "sponsor"},
|
|
||||||
{"Intro/Intermission", "intro"},
|
|
||||||
{"Outro/Credits", "outro"},
|
|
||||||
{"Self Promotion", "selfpromo"},
|
|
||||||
{"Preview/Recap", "preview"},
|
|
||||||
{"Filler Tangent", "filler"},
|
|
||||||
{"Interaction Reminder", "interaction"},
|
|
||||||
{"Non-music Section", "music_offtopic"}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_output_template_options do
|
def custom_output_template_options do
|
||||||
%{
|
%{
|
||||||
upload_day: nil,
|
upload_day: nil,
|
||||||
|
|||||||
-30
@@ -238,36 +238,6 @@
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h3 class="mt-10 text-2xl text-black dark:text-white">
|
|
||||||
SponsorBlock Options
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<section x-data="{ sponsorblockBehaviour: null }">
|
|
||||||
<section x-data="{ presets: { default: 'disabled', media_center: 'disabled', audio: 'disabled', archiving: 'disabled' } }">
|
|
||||||
<.input
|
|
||||||
field={f[:sponsorblock_behaviour]}
|
|
||||||
options={friendly_sponsorblock_options()}
|
|
||||||
type="select"
|
|
||||||
label="SponsorBlock Behaviour"
|
|
||||||
help="Action to take when SponsorBlock segments are found. 'Disabled' won't take any action"
|
|
||||||
x-model="sponsorblockBehaviour"
|
|
||||||
x-init="
|
|
||||||
sponsorblockBehaviour = $el.value
|
|
||||||
$watch('selectedPreset', p => p && ($el.value = presets[p]))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section x-show="sponsorblockBehaviour !== 'disabled'" x-transition>
|
|
||||||
<.input
|
|
||||||
field={f[:sponsorblock_categories]}
|
|
||||||
options={frieldly_sponsorblock_categories()}
|
|
||||||
type="checkbox_group"
|
|
||||||
label="SponsorBlock Categories"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<.button class="my-10 sm:mb-7.5 w-full sm:w-auto" rounding="rounded-lg">Save Media profile</.button>
|
<.button class="my-10 sm:mb-7.5 w-full sm:w-auto" rounding="rounded-lg">Save Media profile</.button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<div class="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="mt-4 flex flex-col items-center justify-center">
|
<div class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">Media Profile(s)</span>
|
<span class="text-md font-medium">Media Profile(s)</span>
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<%= @media_profile_count %>
|
<%= @media_profile_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="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="mt-4 flex flex-col items-center justify-center">
|
<div class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">Source(s)</span>
|
<span class="text-md font-medium">Source(s)</span>
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<%= @source_count %>
|
<%= @source_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="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="#" class="mt-4 flex flex-col items-center justify-center">
|
<div class="mt-4 flex flex-col items-center justify-center">
|
||||||
<span class="text-md font-medium">Media Item(s)</span>
|
<span class="text-md font-medium">Media Item(s)</span>
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<%= @media_item_count %>
|
<%= @media_item_count %>
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-strokedark">I know this page isn't super useful yet, but give it time :)</span>
|
<span class="text-strokedark">I know this page isn't super useful yet, but give it time :)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Profiles
|
alias Pinchflat.Profiles
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaQuery
|
alias Pinchflat.Media.MediaItem
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
sources = Repo.preload(Sources.list_sources(), :media_profile)
|
sources = Repo.preload(Sources.list_sources(), :media_profile)
|
||||||
@@ -111,9 +111,11 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
# NOTE: should move this out of the controller
|
# NOTE: should move this out of the controller
|
||||||
# once I finally add some query fragment layer
|
# once I finally add some query fragment layer
|
||||||
defp total_downloaded_for(source) do
|
defp total_downloaded_for(source) do
|
||||||
MediaQuery.new()
|
from(
|
||||||
|> MediaQuery.for_source(source)
|
m in MediaItem,
|
||||||
|> MediaQuery.with_media_filepath()
|
where: m.source_id == ^source.id,
|
||||||
|
where: not is_nil(m.media_filepath)
|
||||||
|
)
|
||||||
|> Repo.aggregate(:count, :id)
|
|> Repo.aggregate(:count, :id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+14
-11
@@ -1,6 +1,5 @@
|
|||||||
defmodule PinchflatWeb.Router do
|
defmodule PinchflatWeb.Router do
|
||||||
use PinchflatWeb, :router
|
use PinchflatWeb, :router
|
||||||
import Phoenix.LiveDashboard.Router
|
|
||||||
|
|
||||||
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
||||||
# the extension from the path
|
# the extension from the path
|
||||||
@@ -12,7 +11,6 @@ defmodule PinchflatWeb.Router do
|
|||||||
plug :put_root_layout, html: {PinchflatWeb.Layouts, :root}
|
plug :put_root_layout, html: {PinchflatWeb.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
plug :allow_iframe_embed
|
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
@@ -47,12 +45,21 @@ defmodule PinchflatWeb.Router do
|
|||||||
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/dev" do
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||||
pipe_through :browser
|
if Application.compile_env(:pinchflat, :dev_routes) do
|
||||||
|
# If you want to use the LiveDashboard in production, you should put
|
||||||
|
# it behind authentication and allow only admins to access it.
|
||||||
|
# If your application does not have an admins-only section yet,
|
||||||
|
# you can use Plug.BasicAuth to set up some basic authentication
|
||||||
|
# as long as you are also using SSL (which you should anyway).
|
||||||
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
live_dashboard "/dashboard",
|
scope "/dev" do
|
||||||
metrics: PinchflatWeb.Telemetry,
|
pipe_through :browser
|
||||||
ecto_repos: [Pinchflat.Repo]
|
|
||||||
|
live_dashboard "/dashboard", metrics: PinchflatWeb.Telemetry
|
||||||
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_basic_auth(conn, opts) do
|
defp maybe_basic_auth(conn, opts) do
|
||||||
@@ -77,8 +84,4 @@ defmodule PinchflatWeb.Router do
|
|||||||
defp credential_set?(credential) do
|
defp credential_set?(credential) do
|
||||||
credential && credential != ""
|
credential && credential != ""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp allow_iframe_embed(conn, _opts) do
|
|
||||||
delete_resp_header(conn, "x-frame-options")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "0.1.6",
|
version: "0.1.4",
|
||||||
elixir: "~> 1.16",
|
elixir: "~> 1.16",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
@@ -33,7 +33,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def application do
|
def application do
|
||||||
[
|
[
|
||||||
mod: {Pinchflat.Application, []},
|
mod: {Pinchflat.Application, []},
|
||||||
extra_applications: [:logger, :runtime_tools, :inets, :os_mon]
|
extra_applications: [:logger, :runtime_tools, :inets]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,7 +50,6 @@ defmodule Pinchflat.MixProject do
|
|||||||
{:phoenix_ecto, "~> 4.4"},
|
{:phoenix_ecto, "~> 4.4"},
|
||||||
{:ecto_sql, "~> 3.10"},
|
{:ecto_sql, "~> 3.10"},
|
||||||
{:ecto_sqlite3, ">= 0.0.0"},
|
{:ecto_sqlite3, ">= 0.0.0"},
|
||||||
{:ecto_sqlite3_extras, "~> 1.2.0"},
|
|
||||||
{:phoenix_html, "~> 3.3"},
|
{:phoenix_html, "~> 3.3"},
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_view, "~> 0.20.1"},
|
{:phoenix_live_view, "~> 0.20.1"},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
|
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
|
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
|
||||||
"ecto_sqlite3_extras": {:hex, :ecto_sqlite3_extras, "1.2.2", "36e60b561a11441d15f26c791817999269fb578b985162207ebb08b04ca71e40", [:mix], [{:exqlite, ">= 0.13.2", [hex: :exqlite, repo: "hexpm", optional: false]}, {:table_rex, "~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "2b66ba7246bb4f7e39e2578acd4a0e4e4be54f60ff52d450a01be95eeb78ff1e"},
|
|
||||||
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
||||||
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
|
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
|
||||||
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||||
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
||||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
|
||||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.AddSponsorblockToMediaProfiles do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
alter table(:media_profiles) do
|
|
||||||
add :sponsorblock_behaviour, :string, default: "disabled"
|
|
||||||
add :sponsorblock_categories, {:array, :string}, default: []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
defmodule Pinchflat.Repo.Migrations.AddDurationToMediaItems do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def change do
|
|
||||||
alter table(:media_items) do
|
|
||||||
add :duration_seconds, :integer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -222,45 +222,6 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "build/1 when testing sponsorblock options" do
|
|
||||||
test "includes :sponsorblock_remove option when specified", %{media_item: media_item} do
|
|
||||||
media_item =
|
|
||||||
update_media_profile_attribute(media_item, %{
|
|
||||||
sponsorblock_behaviour: :remove,
|
|
||||||
sponsorblock_categories: ["sponsor", "intro"]
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
|
||||||
|
|
||||||
assert {:sponsorblock_remove, "sponsor,intro"} in res
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not include :sponsorblock_remove option without categories", %{media_item: media_item} do
|
|
||||||
media_item =
|
|
||||||
update_media_profile_attribute(media_item, %{
|
|
||||||
sponsorblock_behaviour: :remove,
|
|
||||||
sponsorblock_categories: []
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
|
||||||
refute {:sponsorblock_remove, []} in res
|
|
||||||
refute :sponsorblock_remove in res
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not include any sponsorblock options when disabled", %{media_item: media_item} do
|
|
||||||
media_item =
|
|
||||||
update_media_profile_attribute(media_item, %{sponsorblock_behaviour: :disabled})
|
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
|
||||||
refute {:sponsorblock_remove, []} in res
|
|
||||||
refute :sponsorblock_remove in res
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "build_output_path_for/1" do
|
describe "build_output_path_for/1" do
|
||||||
test "builds an output path for a source", %{media_item: media_item} do
|
test "builds an output path for a source", %{media_item: media_item} do
|
||||||
path = DownloadOptionBuilder.build_output_path_for(media_item.source)
|
path = DownloadOptionBuilder.build_output_path_for(media_item.source)
|
||||||
|
|||||||
@@ -74,12 +74,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "error", 1} end)
|
|
||||||
|
|
||||||
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> :ok end)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the metadata filepath to the database", %{media_item: media_item} do
|
test "it saves the metadata filepatha to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -93,12 +93,6 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
assert [["de", _], ["en", _] | _rest] = updated_media_item.subtitle_filepaths
|
assert [["de", _], ["en", _] | _rest] = updated_media_item.subtitle_filepaths
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it extracts the duration_seconds", %{media_item: media_item} do
|
|
||||||
assert media_item.duration_seconds == nil
|
|
||||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
|
||||||
assert is_integer(updated_media_item.duration_seconds)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it extracts the thumbnail_filepath", %{media_item: media_item} do
|
test "it extracts the thumbnail_filepath", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
|||||||
@@ -38,6 +38,32 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "list_media_items_for/1" do
|
||||||
|
test "it returns media_items for a given source" do
|
||||||
|
source = source_fixture()
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert Media.list_media_items_for(source) == [media_item]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "list_media_items_by_media_id_for/2" do
|
||||||
|
test "returns media_items for a given source and media_ids" do
|
||||||
|
source = source_fixture()
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id, media_id: "123"})
|
||||||
|
|
||||||
|
assert Media.list_media_items_by_media_id_for(source, ["123"]) == [media_item]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return matching media_ids for a different source" do
|
||||||
|
source = source_fixture()
|
||||||
|
other_source = source_fixture()
|
||||||
|
_media_item = media_item_fixture(%{source_id: other_source.id, media_id: "123"})
|
||||||
|
|
||||||
|
assert Media.list_media_items_by_media_id_for(source, ["123"]) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "list_pending_media_items_for/1" do
|
describe "list_pending_media_items_for/1" do
|
||||||
test "it returns pending without a filepath for a given source" do
|
test "it returns pending without a filepath for a given source" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -45,12 +45,6 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
|
|||||||
|
|
||||||
assert result.livestream == metadata["was_live"]
|
assert result.livestream == metadata["was_live"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it extracts the duration in seconds", %{metadata: metadata} do
|
|
||||||
result = Parser.parse_for_media_item(metadata)
|
|
||||||
|
|
||||||
assert result.duration_seconds == round(metadata["duration"])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "parse_for_media_item/1 when testing subtitle metadata" do
|
describe "parse_for_media_item/1 when testing subtitle metadata" do
|
||||||
|
|||||||
@@ -104,8 +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,
|
||||||
upload_date: Date.from_iso8601!("2021-01-01"),
|
upload_date: Date.from_iso8601!("2021-01-01")
|
||||||
duration_seconds: 60
|
|
||||||
} == Media.response_to_struct(response)
|
} == Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -175,27 +174,5 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
assert %Media{upload_date: nil} = Media.response_to_struct(response)
|
assert %Media{upload_date: nil} = Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parses the duration" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => 60.4,
|
|
||||||
"upload_date" => "20210101"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert %Media{duration_seconds: 60} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "doesn't blow up if duration is missing" do
|
|
||||||
response = %{
|
|
||||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
|
||||||
"aspect_ratio" => 1.0,
|
|
||||||
"duration" => nil,
|
|
||||||
"upload_date" => "20210101"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert %Media{duration_seconds: nil} = Media.response_to_struct(response)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user