Source NFO downloads (#95)

* Hooked up series directory finding to source metadata runner

* Fixed aired NFO tag for episodes

* Updated MI NFO builder to take in a filepath

* Hooked up NFO generation to the source worker

* Added NFO controls to form

* Improved the way the source metadata worker updates the source

* Consolidated NFO selection options in media profile instead of source
This commit is contained in:
Kieran
2024-03-18 14:27:28 -07:00
committed by GitHub
parent d0cb782082
commit 8491f8b317
18 changed files with 513 additions and 109 deletions
@@ -222,6 +222,14 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
end
end
describe "build_output_path_for/1" do
test "builds an output path for a source", %{media_item: media_item} do
path = DownloadOptionBuilder.build_output_path_for(media_item.source)
assert path == "/tmp/test/media/%(title)S.%(ext)s"
end
end
defp update_media_profile_attribute(media_item_with_preloads, attrs) do
media_item_with_preloads.source.media_profile
|> Profiles.change_media_profile(attrs)
@@ -92,4 +92,52 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
assert Helpers.parse_upload_date(upload_date) == ~D[2021-01-01]
end
end
describe "series_directory_from_media_filepath/1" do
test "returns base series directory if filepaths are setup as expected" do
good_filepaths = [
"/media/season1/episode.mp4",
"/media/season 1/episode.mp4",
"/media/season.1/episode.mp4",
"/media/season_1/episode.mp4",
"/media/season-1/episode.mp4",
"/media/SEASON 1/episode.mp4",
"/media/SEASON.1/episode.mp4",
"/media/s1/episode.mp4",
"/media/s.1/episode.mp4",
"/media/s_1/episode.mp4",
"/media/s-1/episode.mp4",
"/media/s 1/episode.mp4",
"/media/S1/episode.mp4",
"/media/S.1/episode.mp4"
]
for filepath <- good_filepaths do
assert {:ok, "/media"} = Helpers.series_directory_from_media_filepath(filepath)
end
end
test "returns an error if the season filepath can't be determined" do
bad_filepaths = [
"/media/1/episode.mp4",
"/media/(s1)/episode.mp4",
"/media/episode.mp4",
"/media/s1e1/episode.mp4",
"/media/s1 e1/episode.mp4",
"/media/s1 (something else)/episode.mp4",
"/media/season1e1/episode.mp4",
"/media/season1 e1/episode.mp4",
"/media/seasoning1/episode.mp4",
"/media/season/episode.mp4",
"/media/series1/episode.mp4",
"/media/s/episode.mp4",
"/media/foo",
"/media/bar/"
]
for filepath <- bad_filepaths do
assert {:error, :indeterminable} = Helpers.series_directory_from_media_filepath(filepath)
end
end
end
end
+30 -18
View File
@@ -2,37 +2,49 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
use Pinchflat.DataCase
alias Pinchflat.Metadata.NfoBuilder
alias Pinchflat.Filesystem.FilesystemHelpers
setup do
{:ok, %{metadata: render_parsed_metadata(:media_metadata)}}
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
on_exit(fn -> File.rm!(filepath) end)
{:ok,
%{
metadata: render_parsed_metadata(:media_metadata),
filepath: filepath
}}
end
describe "build_and_store_for_media_item/1" do
test "returns the filepath", %{metadata: metadata} do
result = NfoBuilder.build_and_store_for_media_item(metadata)
describe "build_and_store_for_media_item/2" do
test "returns the filepath", %{metadata: metadata, filepath: filepath} do
result = NfoBuilder.build_and_store_for_media_item(filepath, metadata)
assert File.exists?(result)
File.rm!(result)
end
test "builds filepath based on media location", %{metadata: metadata} do
result = NfoBuilder.build_and_store_for_media_item(metadata)
assert String.contains?(result, Path.rootname(metadata["filepath"]))
assert String.ends_with?(result, ".nfo")
File.rm!(result)
end
test "builds an NFO file", %{metadata: metadata} do
result = NfoBuilder.build_and_store_for_media_item(metadata)
test "builds an NFO file", %{metadata: metadata, filepath: filepath} do
result = NfoBuilder.build_and_store_for_media_item(filepath, metadata)
nfo = File.read!(result)
assert String.contains?(nfo, ~S(<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>))
assert String.contains?(nfo, "<title>#{metadata["title"]}</title>")
end
end
File.rm!(result)
describe "build_and_store_for_source/2" do
test "returns the filepath", %{metadata: metadata, filepath: filepath} do
result = NfoBuilder.build_and_store_for_source(filepath, metadata)
assert File.exists?(result)
end
test "builds an NFO file", %{metadata: metadata, filepath: filepath} do
result = NfoBuilder.build_and_store_for_source(filepath, metadata)
nfo = File.read!(result)
assert String.contains?(nfo, ~S(<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>))
assert String.contains?(nfo, "<title>#{metadata["title"]}</title>")
end
end
end
@@ -2,10 +2,14 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
use Pinchflat.DataCase
import Mox
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Metadata.SourceMetadataStorageWorker
@source_details_ot "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
@metadata_ot "playlist:%()j"
setup :verify_on_exit!
describe "kickoff_with_task/1" do
@@ -27,8 +31,31 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
end
describe "perform/1" do
test "won't call itself in an infinite loop" 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, "{}"}
end)
source = source_fixture()
perform_job(SourceMetadataStorageWorker, %{id: source.id})
assert [] = all_enqueued(worker: SourceMetadataStorageWorker)
end
test "does not blow up if the record doesn't exist" do
assert :ok = perform_job(SourceMetadataStorageWorker, %{id: 0})
end
end
describe "perform/1 when testing metadata storage" do
test "sets metadata location for source" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
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, "{}"}
end)
source = Repo.preload(source_fixture(), :metadata)
refute source.metadata
@@ -43,7 +70,11 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
test "fetches and stores returned metadata for source" do
source = source_fixture()
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, file_contents} end)
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, file_contents}
end)
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
@@ -51,32 +82,106 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
assert metadata == %{"title" => "test"}
end
end
test "won't call itself in an infinite loop" do
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
source = source_fixture()
describe "perform/1 when determining the series_directory" do
test "sets the series directory based on the returned media filepath" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot ->
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
{:ok, source_details_return_fixture(%{filename: filename})}
_url, _opts, ot when ot == @metadata_ot ->
{:ok, "{}"}
end)
source = source_fixture(%{series_directory: nil})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.reload(source)
assert [_] = all_enqueued(worker: SourceMetadataStorageWorker)
assert source.series_directory
end
test "doesn't prevent over source jobs from running" do
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
source_1 = source_fixture()
source_2 = source_fixture()
test "does not set the series directory if it cannot be determined" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot ->
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
perform_job(SourceMetadataStorageWorker, %{id: source_1.id})
perform_job(SourceMetadataStorageWorker, %{id: source_1.id})
perform_job(SourceMetadataStorageWorker, %{id: source_2.id})
perform_job(SourceMetadataStorageWorker, %{id: source_2.id})
{:ok, source_details_return_fixture(%{filename: filename})}
assert [_, _] = all_enqueued(worker: SourceMetadataStorageWorker)
_url, _opts, ot when ot == @metadata_ot ->
{:ok, "{}"}
end)
source = source_fixture(%{series_directory: nil})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.reload(source)
refute source.series_directory
end
end
describe "perform/1 when storing the series NFO" do
test "stores the NFO if specified" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot ->
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
{:ok, source_details_return_fixture(%{filename: filename})}
_url, _opts, ot when ot == @metadata_ot ->
{:ok, "{}"}
end)
profile = media_profile_fixture(%{download_nfo: true})
source = source_fixture(%{nfo_filepath: nil, media_profile_id: profile.id})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.reload(source)
assert source.nfo_filepath
assert source.nfo_filepath == Path.join([source.series_directory, "tvshow.nfo"])
assert File.exists?(source.nfo_filepath)
File.rm!(source.nfo_filepath)
end
test "does not blow up if the record doesn't exist" do
assert :ok = perform_job(SourceMetadataStorageWorker, %{id: 0})
test "does not store the NFO if not specified" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot ->
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
{:ok, source_details_return_fixture(%{filename: filename})}
_url, _opts, ot when ot == @metadata_ot ->
{:ok, "{}"}
end)
profile = media_profile_fixture(%{download_nfo: false})
source = source_fixture(%{nfo_filepath: nil, media_profile_id: profile.id})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.reload(source)
refute source.nfo_filepath
end
test "does not store the NFO if the series directory cannot be determined" do
stub(YtDlpRunnerMock, :run, fn
_url, _opts, ot when ot == @source_details_ot ->
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
{:ok, source_details_return_fixture(%{filename: filename})}
_url, _opts, ot when ot == @metadata_ot ->
{:ok, "{}"}
end)
profile = media_profile_fixture(%{download_nfo: true})
source = source_fixture(%{nfo_filepath: nil, media_profile_id: profile.id})
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.reload(source)
refute source.nfo_filepath
end
end
end
+53 -3
View File
@@ -8,6 +8,7 @@ defmodule Pinchflat.SourcesTest do
alias Pinchflat.Sources
alias Pinchflat.Sources.Source
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingWorker
@@ -57,7 +58,7 @@ defmodule Pinchflat.SourcesTest do
end
end
describe "create_source/1" do
describe "create_source/2" do
test "creates a source and adds name + ID from runner response for channels" do
expect(YtDlpRunnerMock, :run, &channel_mock/3)
@@ -253,7 +254,23 @@ defmodule Pinchflat.SourcesTest do
end
end
describe "update_source/2" do
describe "create_source/2 when testing options" do
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{
media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123"
}
assert {:ok, %Source{}} = Sources.create_source(valid_attrs, run_post_commit_tasks: false)
refute_enqueued(worker: MediaCollectionIndexingWorker)
refute_enqueued(worker: SourceMetadataStorageWorker)
end
end
describe "update_source/3" do
test "updates with valid data updates the source" do
source = source_fixture()
update_attrs = %{collection_name: "some updated name"}
@@ -426,6 +443,20 @@ defmodule Pinchflat.SourcesTest do
end
end
describe "update_source/3 when testing options" do
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
source = source_fixture(%{fast_index: false, download_media: false, index_frequency_minutes: -1})
update_attrs = %{fast_index: true, download_media: true, index_frequency_minutes: 100}
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs, run_post_commit_tasks: false)
refute_enqueued(worker: MediaCollectionIndexingWorker)
refute_enqueued(worker: SourceMetadataStorageWorker)
refute_enqueued(worker: MediaDownloadWorker)
refute_enqueued(worker: FastIndexingWorker)
end
end
describe "delete_source/2" do
test "it deletes the source" do
source = source_fixture()
@@ -474,9 +505,19 @@ defmodule Pinchflat.SourcesTest do
{:ok, updated_source} = Sources.update_source(source, update_attrs)
assert {:ok, _} = Sources.delete_source(updated_source, delete_files: true)
assert {:ok, _} = Sources.delete_source(updated_source)
refute File.exists?(updated_source.metadata.metadata_filepath)
end
test "does not delete the source's non-metadata files" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo)
source = source_fixture(%{nfo_filepath: filepath})
assert {:ok, _} = Sources.delete_source(source)
assert File.exists?(filepath)
File.rm!(filepath)
end
end
describe "delete_source/2 when deleting files" do
@@ -498,6 +539,15 @@ defmodule Pinchflat.SourcesTest do
refute File.exists?(media_item.media_filepath)
end
test "deletes the source's non-metadata files" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo)
source = source_fixture(%{nfo_filepath: filepath})
assert {:ok, _} = Sources.delete_source(source, delete_files: true)
refute File.exists?(filepath)
end
end
describe "change_source/3" do
@@ -97,7 +97,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
test "it passes the expected args to the backend runner" do
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title})j"
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
{:ok, "{}"}
end)
+14
View File
@@ -85,4 +85,18 @@ defmodule Pinchflat.SourcesFixtures do
source_attributes
|> Enum.map_join("\n", &Phoenix.json_library().encode!(&1))
end
def source_details_return_fixture(attrs \\ %{}) do
channel_id = Faker.String.base64(12)
%{
channel_id: channel_id,
channel: "Channel Name",
playlist_id: channel_id,
playlist_title: "Channel Name",
filename: Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
}
|> Map.merge(attrs)
|> Phoenix.json_library().encode!()
end
end