diff --git a/lib/pinchflat/filesystem/filesystem_helpers.ex b/lib/pinchflat/filesystem/filesystem_helpers.ex index 7ec8975..528e107 100644 --- a/lib/pinchflat/filesystem/filesystem_helpers.ex +++ b/lib/pinchflat/filesystem/filesystem_helpers.ex @@ -15,12 +15,25 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory) filepath = Path.join([tmpfile_directory, "#{StringUtils.random_string(64)}.#{type}"]) - :ok = File.mkdir_p!(Path.dirname(filepath)) - :ok = File.write(filepath, "") + :ok = write_p!(filepath, "") filepath end + @doc """ + Writes content to a file, creating directories as needed. + Takes the same args as File.write!/3. + + Returns :ok | raises on error + """ + def write_p!(filepath, content, modes \\ []) do + filepath + |> Path.dirname() + |> File.mkdir_p!() + + File.write!(filepath, content, modes) + end + @doc """ Fetches the file size of a media item and saves it to the database. diff --git a/lib/pinchflat/metadata/metadata_file_helpers.ex b/lib/pinchflat/metadata/metadata_file_helpers.ex index 794f30f..f1e768d 100644 --- a/lib/pinchflat/metadata/metadata_file_helpers.ex +++ b/lib/pinchflat/metadata/metadata_file_helpers.ex @@ -9,6 +9,8 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do needed """ + alias Pinchflat.Filesystem.FilesystemHelpers + @doc """ Compresses and stores metadata for a media item, returning the filepath. @@ -18,8 +20,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do filepath = generate_filepath_for(database_record, "metadata.json.gz") {:ok, json} = Phoenix.json_library().encode(metadata_map) - File.mkdir_p!(Path.dirname(filepath)) - :ok = File.write(filepath, json, [:compressed]) + :ok = FilesystemHelpers.write_p!(filepath, json, [:compressed]) filepath end @@ -45,12 +46,23 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url)) thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url) - File.mkdir_p!(Path.dirname(filepath)) - :ok = File.write(filepath, thumbnail_blob) + :ok = FilesystemHelpers.write_p!(filepath, thumbnail_blob) filepath end + @doc """ + Parses an upload date from the YYYYMMDD string returned in yt-dlp metadata + and returns a Date struct. + + Returns Date.t() + """ + def parse_upload_date(upload_date) do + <> <> <> <> <> = upload_date + + Date.from_iso8601!("#{year}-#{month}-#{day}") + end + defp fetch_thumbnail_from_url(url) do http_client = Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient) {:ok, body} = http_client.get(url, [], body_format: :binary) diff --git a/lib/pinchflat/metadata/nfo_builder.ex b/lib/pinchflat/metadata/nfo_builder.ex new file mode 100644 index 0000000..f328fb7 --- /dev/null +++ b/lib/pinchflat/metadata/nfo_builder.ex @@ -0,0 +1,44 @@ +defmodule Pinchflat.Metadata.NfoBuilder do + @moduledoc """ + Provides methods for building and storing NFO files for + use by Kodi/Jellyfin and other media center software. + """ + + alias Pinchflat.Metadata.MetadataFileHelpers + alias Pinchflat.Filesystem.FilesystemHelpers + + @doc """ + Builds an NFO file for a media item (read: single "episode") and + stores it in the same directory as the media file. Has the same name + as the media file, but with a .nfo extension. + + Returns the filepath of the NFO file. + """ + def build_and_store_for_media_item(metadata) do + filepath = Path.rootname(metadata["filepath"]) <> ".nfo" + nfo = build_for_media_item(metadata) + + FilesystemHelpers.write_p!(filepath, nfo) + + filepath + end + + defp build_for_media_item(metadata) do + upload_date = MetadataFileHelpers.parse_upload_date(metadata["upload_date"]) + # Cribbed from a combination of the Kodi wiki, ytdl-nfo, and ytdl-sub. + # WHO NEEDS A FANCY XML PARSER ANYWAY?! + """ + + + #{metadata["title"]} + #{metadata["uploader"]} + #{metadata["id"]} + #{metadata["description"]} + #{upload_date} + #{upload_date.year} + #{Calendar.strftime(upload_date, "%m%d")} + YouTube + + """ + end +end diff --git a/lib/pinchflat/yt_dlp/media.ex b/lib/pinchflat/yt_dlp/media.ex index b8bd614..6b2a363 100644 --- a/lib/pinchflat/yt_dlp/media.ex +++ b/lib/pinchflat/yt_dlp/media.ex @@ -25,6 +25,7 @@ defmodule Pinchflat.YtDlp.Media do alias __MODULE__ alias Pinchflat.Utils.FunctionUtils + alias Pinchflat.Metadata.MetadataFileHelpers @doc """ Downloads a single piece of media (and possibly its metadata) directly to its @@ -86,7 +87,7 @@ defmodule Pinchflat.YtDlp.Media do original_url: response["webpage_url"], livestream: response["was_live"], short_form_content: response["webpage_url"] && short_form_content?(response), - upload_date: response["upload_date"] && parse_upload_date(response["upload_date"]) + upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"]) } end @@ -106,12 +107,6 @@ defmodule Pinchflat.YtDlp.Media do end end - defp parse_upload_date(upload_date) do - <> <> <> <> <> = upload_date - - Date.from_iso8601!("#{year}-#{month}-#{day}") - end - defp backend_runner do # This approach lets us mock the command for testing Application.get_env(:pinchflat, :yt_dlp_runner) diff --git a/test/pinchflat/filesystem/filesystem_helpers_test.exs b/test/pinchflat/filesystem/filesystem_helpers_test.exs index da2dd8f..d776632 100644 --- a/test/pinchflat/filesystem/filesystem_helpers_test.exs +++ b/test/pinchflat/filesystem/filesystem_helpers_test.exs @@ -33,4 +33,27 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do assert {:error, _} = FilesystemHelpers.compute_and_save_media_filesize(media_item) 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 end diff --git a/test/pinchflat/metadata/metadata_file_helpers_test.exs b/test/pinchflat/metadata/metadata_file_helpers_test.exs index e95a6f8..b28e497 100644 --- a/test/pinchflat/metadata/metadata_file_helpers_test.exs +++ b/test/pinchflat/metadata/metadata_file_helpers_test.exs @@ -84,4 +84,12 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do assert Path.basename(filepath) == "maxres.webp" end end + + describe "parse_upload_date/1" do + test "returns a date from the given metadata upload date" do + upload_date = "20210101" + + assert Helpers.parse_upload_date(upload_date) == ~D[2021-01-01] + end + end end diff --git a/test/pinchflat/metadata/nfo_builder_test.exs b/test/pinchflat/metadata/nfo_builder_test.exs new file mode 100644 index 0000000..e7c3511 --- /dev/null +++ b/test/pinchflat/metadata/nfo_builder_test.exs @@ -0,0 +1,50 @@ +defmodule Pinchflat.Metadata.NfoBuilderTest do + use Pinchflat.DataCase + + alias Pinchflat.Metadata.NfoBuilder + + setup do + json_filepath = + Path.join([ + File.cwd!(), + "test", + "support", + "files", + "media_metadata.json" + ]) + + {:ok, file_body} = File.read(json_filepath) + {:ok, parsed_json} = Phoenix.json_library().decode(file_body) + + {:ok, %{metadata: parsed_json}} + 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) + + 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) + nfo = File.read!(result) + + assert String.contains?(nfo, ~S()) + assert String.contains?(nfo, "#{metadata["title"]}") + + File.rm!(result) + end + end +end