Compare commits

...

18 Commits

Author SHA1 Message Date
Kieran Eglin 64ebf756ab Bumped version 2024-03-25 11:56:31 -07:00
Kieran 377bc617ea Removed unneeded startup tasks (#121) 2024-03-25 11:52:02 -07:00
Kieran a03f69a8d4 Started storing source images in internal metadata folder (#120) 2024-03-25 11:36:03 -07:00
Kieran d39ebe7ac1 Improve default recommendation for artist placeholder (#119)
* Improved default recommendation for audio artist placeholder

* Updated docs
2024-03-25 10:34:22 -07:00
Kieran 8b5dcc15d6 Fixed filepath bug when parsing metadata (#118) 2024-03-25 10:20:29 -07:00
Kieran 5e58dcd879 Cookie auth (#116)
* Added new extras directory

* Automatically create cookie file on boot

* Added cookie file option to DL option builder

* Put cookie auth in correct spot

* Updated Docs

* Removed useless setter for config path
2024-03-24 20:23:25 -07:00
Kieran Eglin a9d5407071 Bumped version 2024-03-24 15:42:03 -07:00
Kieran 37c1da8014 Added GHCR to README (#115) 2024-03-24 15:40:16 -07:00
Kieran Eglin 937a14985f One more GHCR shot 2024-03-24 15:15:43 -07:00
Kieran Eglin a6a20c111f Added GHCR tags 2024-03-24 15:13:12 -07:00
Kieran Eglin 2bb96edc00 Merge branch 'master' of https://github.com/kieraneglin/pinchflat 2024-03-24 15:07:13 -07:00
Kieran Eglin 87f07ff634 Testing GHCR support 2024-03-24 15:07:00 -07:00
Kieran dd60cb5419 Improve preflight file permissions check (#114)
* Added startup permissions check with more helpful message

* Updated README

* Fixes faulty test
2024-03-24 15:06:08 -07:00
Kieran Eglin 610b00566b Added logging for job reset task 2024-03-24 09:28:05 -07:00
Kieran aba15ed886 Adds a method for retrying stuck jobs (#112) 2024-03-24 09:21:01 -07:00
Kieran Eglin 87da716fdf Adds platform option when building docker containers 2024-03-24 09:20:07 -07:00
Kieran Eglin 45f056c6e0 Enabled perf profiler for erlang JIT 2024-03-22 23:34:56 -07:00
Kieran Eglin 11d160008c Updated Docker image generation for GH action 2024-03-22 23:15:51 -07:00
35 changed files with 10271 additions and 180 deletions
+15 -1
View File
@@ -7,6 +7,10 @@ on:
description: 'Docker Image Tag'
required: true
default: 'dev'
platforms:
description: 'Build Platforms'
required: true
default: 'linux/amd64,linux/arm64'
jobs:
docker:
@@ -24,9 +28,19 @@ jobs:
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 }}'
tags: |
keglin/pinchflat:${{ github.event.inputs.image_tag }}
ghcr.io/${{ github.repository_owner }}/pinchflat:${{ github.event.inputs.image_tag }}
+1 -27
View File
@@ -22,30 +22,4 @@ alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.SlowIndexing.FileFollowerServer
defmodule IexHelpers do
def last_media_item do
Repo.one(from m in MediaItem, limit: 1)
end
def details(type) do
source =
case type do
:playlist -> playlist_url()
:channel -> channel_url()
end
YtDlpCollection.get_source_details(source)
end
def ids(type) do
source =
case type do
:playlist -> playlist_url()
:channel -> channel_url()
end
YtDlpCollection.get_media_attributes_for_collection(source)
end
end
import IexHelpers
Pinchflat.Release.check_file_permissions()
+27 -5
View File
@@ -49,6 +49,7 @@ If it doesn't work for your use case, please make a feature request! You can als
- Custom rules for handling YouTube Shorts and livestreams
- Advanced options like setting cutoff dates and filtering by title
- Reliable hands-off operation
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
## Screenshots
@@ -61,12 +62,29 @@ If it doesn't work for your use case, please make a feature request! You can als
Simply search for Pinchflat in the Community Apps store!
### Portainer
Docker Compose file:
```yaml
version: '3'
services:
pinchflat:
image: keglin/pinchflat:latest
ports:
- '8945:8945'
volumes:
- /host/path/to/config:/config
- /host/path/to/downloads:/downloads
```
### Docker
1. Create two directories on your host machine: one for storing config and one for storing downloaded media. Make sure they're both writable by the user running the Docker container.
2. Prepare the docker image in one of the two ways below:
- **From Docker Hub:** `docker pull keglin/pinchflat:latest`, or;
- **Building locally:** `docker build . --file selfhosted.Dockerfile -t keglin/pinchflat:latest`
- **From GHCR:** `docker pull ghcr.io/kieraneglin/pinchflat:latest`
- NOTE: also available on Docker Hub at `keglin/pinchflat:latest`
- **Building locally:** `docker build . --file selfhosted.Dockerfile -t ghcr.io/kieraneglin/pinchflat:latest`
3. Run the container:
```bash
@@ -76,12 +94,16 @@ docker run \
-p 8945:8945 \
-v /host/path/to/config:/config \
-v /host/path/to/downloads:/downloads \
keglin/pinchflat:latest
ghcr.io/kieraneglin/pinchflat:latest
```
NOTE: 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.
### IMPORTANT: File permissions
## Authentication
You _must_ ensure the host directories you've mounted are writable by the user running the Docker container. If you get a permission error follow the steps it suggests. See [#106](https://github.com/kieraneglin/pinchflat/issues/106) for more.
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.
## Username and Password
HTTP basic authentication is optionally supported. To use it, set the `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables when starting the container. No authentication will be required unless you set _both_ of these.
+1
View File
@@ -16,6 +16,7 @@ config :pinchflat,
media_directory: "/downloads",
# The user may or may not store metadata for their needs, but the app will always store its copy
metadata_directory: "/config/metadata",
extras_directory: "/config/extras",
tmpfile_directory: Path.join([System.tmp_dir!(), "pinchflat", "data"]),
# Setting BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD implies you want to use basic auth.
# If either is unset, basic auth will not be used.
+1
View File
@@ -3,6 +3,7 @@ import Config
config :pinchflat,
media_directory: Path.join([File.cwd!(), "tmp", "media"]),
metadata_directory: Path.join([File.cwd!(), "tmp", "metadata"]),
extras_directory: Path.join([File.cwd!(), "tmp", "extras"]),
tmpfile_directory: Path.join([File.cwd!(), "tmp", "tmpfiles"])
# Configure your database
+5 -7
View File
@@ -41,16 +41,11 @@ config :pinchflat, Pinchflat.Repo,
]
if config_env() == :prod do
config_path =
System.get_env("CONFIG_PATH") ||
raise """
environment variable CONFIG_PATH is missing.
For example: /etc/pinchflat/config
"""
config_path = "/config"
db_path = System.get_env("DATABASE_PATH", Path.join([config_path, "db", "pinchflat.db"]))
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"]))
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
# We want to force _some_ level of useful logging in production
acceptable_log_levels = ~w(debug info)a
@@ -65,7 +60,10 @@ if config_env() == :prod do
config :pinchflat,
yt_dlp_executable: System.find_executable("yt-dlp"),
media_directory: "/downloads",
metadata_directory: metadata_path,
extras_directory: extras_path,
tmpfile_directory: Path.join([System.tmp_dir!(), "pinchflat", "data"]),
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY")
config :pinchflat, Pinchflat.Repo,
+2 -15
View File
@@ -18,7 +18,6 @@ defmodule Pinchflat.Boot.DataBackfillWorker do
alias __MODULE__
alias Pinchflat.Repo
alias Pinchflat.Media.MediaItem
@doc """
Cancels all pending backfill jobs. Useful for ensuring worker runs immediately
@@ -44,26 +43,14 @@ defmodule Pinchflat.Boot.DataBackfillWorker do
"""
def perform(%Oban.Job{}) do
Logger.info("Running data backfill worker")
backfill_shorts_data()
# Nothing to do for now - just reschedule
# Keeping in-place because we _will_ need it in the future
reschedule_backfill()
:ok
end
defp backfill_shorts_data do
query =
from(
m in MediaItem,
where: fragment("? like ?", m.original_url, "%/shorts/%"),
where: m.short_form_content == false
)
{count, _} = Repo.update_all(query, set: [short_form_content: true])
Logger.info("Backfill worker set short_form_content to true for #{count} media items.")
end
defp reschedule_backfill do
# Run hourly
next_run_in = 60 * 60
+25 -46
View File
@@ -31,59 +31,38 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
"""
@impl true
def init(state) do
reset_executing_jobs()
create_blank_cookie_file()
apply_default_settings()
ensure_directories_are_writeable()
rename_old_job_workers()
{:ok, state}
end
# If a node cannot gracefully shut down, the currently executing jobs get stuck
# in the "executing" state. This is a problem because the job runner will not
# pick them up again
defp reset_executing_jobs do
{count, _} =
Oban.Job
|> where(state: "executing")
|> Repo.update_all(set: [state: "retryable"])
Logger.info("Reset #{count} executing jobs")
end
defp create_blank_cookie_file do
base_dir = Application.get_env(:pinchflat, :extras_directory)
filepath = Path.join(base_dir, "cookies.txt")
if !File.exists?(filepath) do
Logger.info("Cookies does not exist - creating it")
FilesystemHelpers.write_p!(filepath, "")
end
end
defp apply_default_settings do
Settings.fetch!(:onboarding, true)
Settings.fetch!(:pro_enabled, false)
end
defp ensure_directories_are_writeable do
directories = [
Application.get_env(:pinchflat, :media_directory),
Application.get_env(:pinchflat, :tmpfile_directory),
Application.get_env(:pinchflat, :metadata_directory)
]
Enum.each(directories, fn dir ->
file = Path.join([dir, ".keep"])
# This will fail if the directory is not writeable, stopping boot
FilesystemHelpers.write_p!(file, "")
end)
end
# As part of a large refactor, I ended up moving a bunch of workers around. This
# is a problem because the workers are stored in the database and the runner
# will try to run the OLD jobs. This is also why these tasks run before the job
# runner starts up.
#
# Can be removed after a few months (created: 2024-03-12)
defp rename_old_job_workers do
# [ [old_name, new_name], ...]
rename_map = [
["Pinchflat.Workers.MediaIndexingWorker", "Pinchflat.FastIndexing.MediaIndexingWorker"],
["Pinchflat.Workers.MediaDownloadWorker", "Pinchflat.Downloading.MediaDownloadWorker"],
["Pinchflat.Workers.FastIndexingWorker", "Pinchflat.FastIndexing.FastIndexingWorker"],
["Pinchflat.Workers.MediaCollectionIndexingWorker", "Pinchflat.SlowIndexing.MediaCollectionIndexingWorker"],
["Pinchflat.Workers.DataBackfillWorker", "Pinchflat.Boot.DataBackfillWorker"]
]
jobs_renamed =
Enum.reduce(rename_map, 0, fn [old_name, new_name], acc ->
{count, _} =
Oban.Job
|> where(worker: ^old_name)
|> Repo.update_all(set: [worker: new_name])
acc + count
end)
Logger.info("Renamed #{jobs_renamed} old job workers")
end
end
@@ -42,7 +42,8 @@ defmodule Pinchflat.Downloading.OutputPathBuilder do
"upload_day" => "%(upload_date>%d)S",
"upload_yyyy_mm_dd" => "%(upload_date>%Y-%m-%d)S",
"season_from_date" => "%(upload_date>%Y)S",
"season_episode_from_date" => "s%(upload_date>%Y)Se%(upload_date>%m%d)S"
"season_episode_from_date" => "s%(upload_date>%Y)Se%(upload_date>%m%d)S",
"artist_name" => "%(artist,creator,uploader,uploader_id)S"
}
end
end
+16 -5
View File
@@ -20,6 +20,21 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
filepath
end
@doc """
Writes content to a file, creating directories as needed.
Takes the same args as File.write/3.
Returns :ok | {:error, any()}
"""
def write_p(file, content, modes \\ []) do
dirname = Path.dirname(file)
case File.mkdir_p(dirname) do
:ok -> File.write(file, content, modes)
err -> err
end
end
@doc """
Writes content to a file, creating directories as needed.
Takes the same args as File.write!/3.
@@ -27,11 +42,7 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
Returns :ok | raises on error
"""
def write_p!(filepath, content, modes \\ []) do
filepath
|> Path.dirname()
|> File.mkdir_p!()
File.write!(filepath, content, modes)
:ok = write_p(filepath, content, modes)
end
@doc """
@@ -11,6 +11,22 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
alias Pinchflat.Filesystem.FilesystemHelpers
@doc """
Returns the directory where metadata for a database record should be stored.
Returns binary()
"""
def metadata_directory_for(database_record) do
metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
record_table_name = database_record.__meta__.source
Path.join([
metadata_directory,
record_table_name,
to_string(database_record.id)
])
end
@doc """
Compresses and stores metadata for a media item, returning the filepath.
@@ -108,13 +124,8 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
end
defp generate_filepath_for(database_record, filename) do
metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
record_table_name = database_record.__meta__.source
Path.join([
metadata_directory,
record_table_name,
to_string(database_record.id),
metadata_directory_for(database_record),
filename
])
end
+16 -4
View File
@@ -58,23 +58,35 @@ defmodule Pinchflat.Metadata.MetadataParser do
# NOTE: whole ordeal needed due to a bug I found in yt-dlp
# https://github.com/yt-dlp/yt-dlp/issues/9445
# Can be reverted to remove this entire conditional once fixed
%{
thumbnail_filepath:
filepath =
thumbnail_filepath
|> String.split(~r{\.}, include_captures: true)
|> List.insert_at(-3, "-thumb")
|> Enum.join()
%{
thumbnail_filepath: filepath_if_exists(filepath)
}
else
%{
thumbnail_filepath: thumbnail_filepath
thumbnail_filepath: nil
}
end
end
defp parse_infojson_metadata(metadata) do
%{
metadata_filepath: metadata["infojson_filename"]
metadata_filepath: filepath_if_exists(metadata["infojson_filename"])
}
end
defp filepath_if_exists(nil), do: nil
defp filepath_if_exists(filepath) do
if File.exists?(filepath) do
filepath
else
nil
end
end
end
+16 -2
View File
@@ -10,11 +10,20 @@ defmodule Pinchflat.Metadata.SourceMetadata do
alias Pinchflat.Sources.Source
@allowed_fields ~w(metadata_filepath)a
@allowed_fields ~w(
metadata_filepath
fanart_filepath
poster_filepath
banner_filepath
)a
@required_fields ~w(metadata_filepath)a
schema "source_metadata" do
field :metadata_filepath, :string
field :fanart_filepath, :string
field :poster_filepath, :string
field :banner_filepath, :string
belongs_to :source, Source
@@ -31,6 +40,11 @@ defmodule Pinchflat.Metadata.SourceMetadata do
@doc false
def filepath_attributes do
~w(metadata_filepath)a
~w(
metadata_filepath
fanart_filepath
poster_filepath
banner_filepath
)a
end
end
@@ -47,21 +47,23 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
def perform(%Oban.Job{args: %{"id" => source_id}}) do
source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile])
series_directory = determine_series_directory(source)
{source_metadata, image_filepath_attrs} = fetch_source_metadata_and_images(series_directory, source)
# `run_post_commit_tasks: false` prevents this from running in an infinite loop
{source_metadata, source_image_attrs, metadata_image_attrs} =
fetch_source_metadata_and_images(series_directory, source)
source_metadata_filepath = MetadataFileHelpers.compress_and_store_metadata_for(source, source_metadata)
Sources.update_source(
source,
Map.merge(
%{
series_directory: series_directory,
nfo_filepath: store_source_nfo(source, series_directory, source_metadata),
metadata: %{
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(source, source_metadata)
}
metadata: Map.merge(%{metadata_filepath: source_metadata_filepath}, metadata_image_attrs)
},
image_filepath_attrs
source_image_attrs
),
# `run_post_commit_tasks: false` prevents this from running in an infinite loop
run_post_commit_tasks: false
)
@@ -72,18 +74,19 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
end
defp fetch_source_metadata_and_images(series_directory, source) do
if source.media_profile.download_source_images && series_directory do
output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
opts = [:write_all_thumbnails, convert_thumbnails: "jpg", output: output_path]
metadata_directory = MetadataFileHelpers.metadata_directory_for(source)
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
opts = [:write_all_thumbnails, convert_thumbnails: "jpg", output: tmp_output_path]
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, opts)
image_attrs = SourceImageParser.store_source_images(series_directory, metadata)
metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata)
{metadata, image_attrs}
if source.media_profile.download_source_images && series_directory do
source_image_attrs = SourceImageParser.store_source_images(series_directory, metadata)
{metadata, source_image_attrs, metadata_image_attrs}
else
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, [])
{metadata, %{}}
{metadata, %{}, metadata_image_attrs}
end
end
+62
View File
@@ -5,6 +5,10 @@ defmodule Pinchflat.Release do
"""
@app :pinchflat
require Logger
alias Pinchflat.Filesystem.FilesystemHelpers
def migrate do
load_app()
@@ -18,6 +22,37 @@ defmodule Pinchflat.Release do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
def check_file_permissions do
load_app()
directories = [
"/config",
"/downloads",
Application.get_env(:pinchflat, :media_directory),
Application.get_env(:pinchflat, :tmpfile_directory),
Application.get_env(:pinchflat, :extras_directory),
Application.get_env(:pinchflat, :metadata_directory)
]
Enum.each(directories, fn dir ->
Logger.info("Checking permissions for #{dir}")
filepath = Path.join([dir, ".keep"])
case FilesystemHelpers.write_p(filepath, "") do
:ok ->
Logger.info("Permissions OK")
{:error, :eacces} ->
Logger.error(permission_denied_screed(dir))
raise "Permission denied"
err ->
Logger.error("Permissions check failed: #{inspect(err)}")
raise "Unknown error"
end
end)
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
@@ -25,4 +60,31 @@ defmodule Pinchflat.Release do
defp load_app do
Application.load(@app)
end
defp permission_denied_screed(dir) do
"""
The directory "#{dir}" is not writeable by the Docker container.
Please ensure that the directory exists and is writeable by the Docker
container. All setups are different, but you may be able to run something
like this on the *host*:
chown nobody -R <host path that maps to #{dir}>
chmod 755 -R <host path that maps to #{dir}>
Swapping in your real host path. Then, you should set the user running
this container by editing your `docker run` command like so:
docker run --user 99:100 <rest of the command>
Or adding `user: '99:100'` to the Pinchflat service of your Docker Compose
file. Again, there are many ways to do this depending on your setup and
this is just one example. See issue #106 in the Pinchflat Github for more.
No matter the case, this _is_ a permissions error and allowing the container
to write to the directory is the only way to fix it. It is not recommended
to run the container as `root` because files created by Pinchflat may not
be accessible to other apps that want to modify them.
"""
end
end
+15 -1
View File
@@ -31,7 +31,8 @@ defmodule Pinchflat.YtDlp.CommandRunner do
# Also, can't use RAM file since yt-dlp needs a concrete filepath.
output_filepath = Keyword.get(addl_opts, :output_filepath, FSUtils.generate_metadata_tmpfile(:json))
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
formatted_command_opts = [url] ++ parse_options(command_opts ++ print_to_file_opts)
cookie_opts = build_cookie_options()
formatted_command_opts = [url] ++ parse_options(command_opts ++ print_to_file_opts ++ cookie_opts)
Logger.info("[yt-dlp] called with: #{Enum.join(formatted_command_opts, " ")}")
@@ -47,6 +48,19 @@ defmodule Pinchflat.YtDlp.CommandRunner do
end
end
defp build_cookie_options do
base_dir = Application.get_env(:pinchflat, :extras_directory)
cookie_file = Path.join(base_dir, "cookies.txt")
case File.read(cookie_file) do
{:ok, cookie_data} ->
if String.trim(cookie_data) != "", do: [cookies: cookie_file], else: []
{:error, _} ->
[]
end
end
# We want to satisfy the following behaviours:
#
# 1. If the key is an atom, convert it to a string and convert it to kebab case (for convenience)
@@ -39,7 +39,8 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
upload_year: nil,
upload_yyyy_mm_dd: "the upload date in the format YYYY-MM-DD",
source_custom_name: "the name of the sources that use this profile",
source_collection_type: "the collection type of the sources that use this profile. Either 'channel' or 'playlist'"
source_collection_type: "the collection type of the sources using this profile. Either 'channel' or 'playlist'",
artist_name: "the name of the artist with fallbacks to other uploader fields"
}
end
@@ -74,6 +75,6 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
end
defp audio_output_template do
"/music/{{ source_custom_name }}/{{ title }}.{{ ext }}"
"/music/{{ artist_name }}/{{ title }}.{{ ext }}"
end
end
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do
[
app: :pinchflat,
version: "0.1.0",
version: "0.1.2",
elixir: "~> 1.16",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@@ -0,0 +1,11 @@
defmodule Pinchflat.Repo.Migrations.AddImageAttrsToSourceMetadata do
use Ecto.Migration
def change do
alter table(:source_metadata) do
add :fanart_filepath, :string
add :poster_filepath, :string
add :banner_filepath, :string
end
end
end
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cd -P -- "$(dirname -- "$0")"
exec ./pinchflat eval Pinchflat.Release.check_file_permissions
+7
View File
@@ -1,4 +1,11 @@
#!/bin/sh
/app/bin/check_file_permissions
if [ $? -ne 0 ]; then
echo "Filesystem error. Exiting."
exit 1
fi
/app/bin/migrate
cd -P -- "$(dirname -- "$0")"
+2 -2
View File
@@ -39,6 +39,7 @@ RUN mix local.hex --force && \
# set build ENV
ENV MIX_ENV="prod"
ENV ERL_FLAGS="+JPperf true"
# install mix dependencies
COPY mix.exs mix.lock ./
@@ -76,7 +77,7 @@ FROM ${RUNNER_IMAGE}
RUN apt-get update -y
RUN apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
ffmpeg curl git openssh-client
ffmpeg curl git openssh-client nano
RUN apt-get clean && rm -f /var/lib/apt/lists/*_*
# Download and update YT-DLP
@@ -100,7 +101,6 @@ RUN chown nobody /config /downloads
# set runner ENV
ENV MIX_ENV="prod"
ENV CONFIG_PATH="/config"
ENV PORT=8945
ENV RUN_CONTEXT="selfhosted"
@@ -1,8 +1,6 @@
defmodule Pinchflat.Boot.DataBackfillWorkerTest do
use Pinchflat.DataCase
import Pinchflat.MediaFixtures
alias Pinchflat.Boot.DataBackfillWorker
alias Pinchflat.JobFixtures.TestJobWorker
@@ -45,26 +43,4 @@ defmodule Pinchflat.Boot.DataBackfillWorkerTest do
assert_enqueued(worker: DataBackfillWorker, scheduled_at: now_plus(60, :minutes))
end
end
describe "perform/1 when testing backfill_shorts_data" do
test "sets short_form_content to true for media items with shorts in the URL" do
media_item = media_item_with_attachments(%{original_url: "https://example.com/shorts/123"})
refute media_item.short_form_content
perform_job(DataBackfillWorker, %{})
assert Repo.reload!(media_item).short_form_content
end
test "does not set short_form_content to true for media items without shorts in the URL" do
media_item = media_item_with_attachments(%{original_url: "https://example.com/longs/123"})
refute media_item.short_form_content
perform_job(DataBackfillWorker, %{})
refute Repo.reload!(media_item).short_form_content
end
end
end
@@ -1,8 +1,8 @@
defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
use Pinchflat.DataCase
import Pinchflat.MediaFixtures
import Pinchflat.ProfilesFixtures
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Profiles
alias Pinchflat.Downloading.DownloadOptionBuilder
@@ -58,7 +58,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
describe "download_for_media_item/3 when testing media_item attributes" do
setup do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
{:ok, render_metadata(:media_metadata)}
end)
@@ -94,15 +94,44 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
end
test "it extracts the thumbnail_filepath", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
metadata = render_parsed_metadata(:media_metadata)
thumbnail_filepath =
metadata["thumbnails"]
|> Enum.reverse()
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|> String.split(~r{\.}, include_captures: true)
|> List.insert_at(-3, "-thumb")
|> Enum.join()
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
{:ok, Phoenix.json_library().encode!(metadata)}
end)
assert media_item.thumbnail_filepath == nil
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
assert String.ends_with?(updated_media_item.thumbnail_filepath, ".webp")
File.rm(updated_media_item.thumbnail_filepath)
end
test "it extracts the metadata_filepath", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
metadata = render_parsed_metadata(:media_metadata)
infojson_filepath = metadata["infojson_filename"]
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
{:ok, Phoenix.json_library().encode!(metadata)}
end)
assert media_item.metadata_filepath == nil
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
assert String.ends_with?(updated_media_item.metadata_filepath, ".info.json")
File.rm(updated_media_item.metadata_filepath)
end
end
@@ -34,6 +34,29 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
end
end
describe "write_p/3" do
test "writes content to a file" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
content = "{}"
assert :ok = FilesystemHelpers.write_p(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
end
test "creates directories as needed" do
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
filepath = Path.join([tmpfile_directory, "foo", "bar", "file.json"])
content = "{}"
assert :ok = FilesystemHelpers.write_p(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
end
end
describe "write_p!/3" do
test "writes content to a file" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
@@ -140,4 +140,14 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
end
end
end
describe "metadata_directory_for/1" do
test "returns the metadata directory for the given record", %{media_item: media_item} do
base_metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
metadata_directory = Helpers.metadata_directory_for(media_item)
assert metadata_directory == Path.join([base_metadata_directory, "media_items", "#{media_item.id}"])
end
end
end
@@ -1,5 +1,6 @@
defmodule Pinchflat.Metadata.MetadataParserTest do
use Pinchflat.DataCase
import Pinchflat.MediaFixtures
alias Pinchflat.Metadata.MetadataParser, as: Parser
@@ -88,6 +89,22 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
end
describe "parse_for_media_item/1 when testing thumbnail metadata" do
setup %{metadata: metadata} do
thumbnail_filepath =
metadata["thumbnails"]
|> Enum.reverse()
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|> String.split(~r{\.}, include_captures: true)
|> List.insert_at(-3, "-thumb")
|> Enum.join()
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
on_exit(fn -> File.rm(thumbnail_filepath) end)
{:ok, filepath: thumbnail_filepath}
end
test "extracts the thumbnail filepath", %{metadata: metadata} do
result = Parser.parse_for_media_item(metadata)
@@ -103,6 +120,14 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
assert String.contains?(result.thumbnail_filepath, "-thumb.webp")
end
test "doesn't include thumbnail if the file doesn't exist on-disk", %{metadata: metadata, filepath: filepath} do
File.rm(filepath)
result = Parser.parse_for_media_item(metadata)
assert result.thumbnail_filepath == nil
end
test "doesn't freak out if the media has no thumbnails", %{metadata: metadata} do
metadata = Map.put(metadata, "thumbnails", %{})
@@ -121,12 +146,29 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
end
describe "parse_for_media_item/1 when testing infojson metadata" do
setup %{metadata: metadata} do
infojson_filepath = metadata["infojson_filename"]
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
on_exit(fn -> File.rm(infojson_filepath) end)
{:ok, filepath: infojson_filepath}
end
test "extracts the metadata filepath", %{metadata: metadata} do
result = Parser.parse_for_media_item(metadata)
assert String.ends_with?(result.metadata_filepath, ".info.json")
end
test "doesn't include metadata if the file doesn't exist on-disk", %{metadata: metadata, filepath: filepath} do
File.rm(filepath)
result = Parser.parse_for_media_item(metadata)
assert result.metadata_filepath == nil
end
test "doesn't freak out if the media has no infojson", %{metadata: metadata} do
metadata = Map.put(metadata, "infojson_filename", nil)
@@ -59,7 +59,6 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
source = Repo.preload(source_fixture(), :metadata)
refute source.metadata
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
@@ -83,6 +82,42 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
assert metadata == %{"title" => "test"}
end
test "sets metadata image location for source" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
end)
source = source_fixture()
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
assert source.metadata.fanart_filepath
assert source.metadata.poster_filepath
assert source.metadata.banner_filepath
Sources.delete_source(source, delete_files: true)
end
test "stores metadata images for source" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
end)
source = source_fixture()
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
assert File.exists?(source.metadata.fanart_filepath)
assert File.exists?(source.metadata.poster_filepath)
assert File.exists?(source.metadata.banner_filepath)
Sources.delete_source(source, delete_files: true)
end
end
describe "perform/1 when testing source image downloading" do
@@ -1,6 +1,8 @@
defmodule Pinchflat.YtDlp.CommandRunnerTest do
use ExUnit.Case, async: true
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.YtDlp.CommandRunner, as: Runner
@original_executable Application.compile_env(:pinchflat, :yt_dlp_executable)
@@ -65,6 +67,41 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
end
end
describe "run/4 when testing cookie options" do
setup do
base_dir = Application.get_env(:pinchflat, :extras_directory)
cookie_file = Path.join(base_dir, "cookies.txt")
{:ok, cookie_file: cookie_file}
end
test "includes cookie options when cookies.txt exists", %{cookie_file: cookie_file} do
FilesystemHelpers.write_p!(cookie_file, "cookie data")
assert {:ok, output} = Runner.run(@media_url, [], "")
assert String.contains?(output, "--cookies #{cookie_file}")
end
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
FilesystemHelpers.write_p!(cookie_file, " \n \n ")
assert {:ok, output} = Runner.run(@media_url, [], "")
refute String.contains?(output, "--cookies")
refute String.contains?(output, cookie_file)
end
test "doesn't include cookie options when cookies.txt doesn't exist", %{cookie_file: cookie_file} do
File.rm(cookie_file)
assert {:ok, output} = Runner.run(@media_url, [], "")
refute String.contains?(output, "--cookies")
refute String.contains?(output, cookie_file)
end
end
defp wrap_executable(new_executable, fun) do
Application.put_env(:pinchflat, :yt_dlp_executable, new_executable)
fun.()
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1513,7 +1513,7 @@
"url": "https://i.ytimg.com/vi_webp/ABC123/maxresdefault.webp",
"preference": 0,
"id": "41",
"filepath": "/app/tmp/videos/ABC123/Pinchflat Example Video-ABC123.webp"
"filepath": "/tmp/test/tmpfiles/Pinchflat Example Video-ABC123.webp"
}
],
"thumbnail": "https://i.ytimg.com/vi/ABC123/maxresdefault.jpg",
@@ -9769,7 +9769,7 @@
"release_git_head": "5498729c59b03a9511c64552da3ba2f802166f8d",
"repository": "yt-dlp/yt-dlp-nightly-builds"
},
"infojson_filename": "/app/tmp/videos/ABC123/Pinchflat Example Video-ABC123.info.json",
"infojson_filename": "/tmp/test/tmpfiles/Pinchflat Example Video-ABC123.info.json",
"__infojson_filename": "/app/tmp/videos/ABC123/Pinchflat Example Video-ABC123.info.json",
"__real_download": true,
"__files_to_merge": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

+31 -8
View File
@@ -52,14 +52,7 @@ defmodule Pinchflat.MediaFixtures do
"#{:rand.uniform(1_000_000)}_media.mkv"
])
fixture_media_filepath =
Path.join([
File.cwd!(),
"test",
"support",
"files",
"media.mkv"
])
fixture_media_filepath = media_filepath_fixture()
:ok = File.mkdir_p(Path.dirname(stored_media_filepath))
:ok = File.cp(fixture_media_filepath, stored_media_filepath)
@@ -82,4 +75,34 @@ defmodule Pinchflat.MediaFixtures do
Phoenix.json_library().encode!(media_attributes)
end
def media_filepath_fixture do
Path.join([
File.cwd!(),
"test",
"support",
"files",
"media.mkv"
])
end
def thumbnail_filepath_fixture do
Path.join([
File.cwd!(),
"test",
"support",
"files",
"thumbnail.jpg"
])
end
def infojson_filepath_fixture do
Path.join([
File.cwd!(),
"test",
"support",
"files",
"example.info.json"
])
end
end
+2
View File
@@ -11,9 +11,11 @@ Faker.start()
ExUnit.after_suite(fn _ ->
File.rm_rf!(Application.get_env(:pinchflat, :media_directory))
File.rm_rf!(Application.get_env(:pinchflat, :metadata_directory))
File.rm_rf!(Application.get_env(:pinchflat, :extras_directory))
File.rm_rf!(Application.get_env(:pinchflat, :tmpfile_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :media_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :metadata_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :extras_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :tmpfile_directory))
end)