Added nfo builder for 'episodes'

This commit is contained in:
Kieran Eglin 2024-03-13 10:41:23 -07:00
parent cf59bf99cd
commit b8add0201e
No known key found for this signature in database
GPG key ID: 193984967FCF432D
7 changed files with 158 additions and 13 deletions

View file

@ -15,12 +15,25 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory) tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
filepath = Path.join([tmpfile_directory, "#{StringUtils.random_string(64)}.#{type}"]) filepath = Path.join([tmpfile_directory, "#{StringUtils.random_string(64)}.#{type}"])
:ok = File.mkdir_p!(Path.dirname(filepath)) :ok = write_p!(filepath, "")
:ok = File.write(filepath, "")
filepath filepath
end 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 """ @doc """
Fetches the file size of a media item and saves it to the database. Fetches the file size of a media item and saves it to the database.

View file

@ -9,6 +9,8 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
needed needed
""" """
alias Pinchflat.Filesystem.FilesystemHelpers
@doc """ @doc """
Compresses and stores metadata for a media item, returning the filepath. 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") filepath = generate_filepath_for(database_record, "metadata.json.gz")
{:ok, json} = Phoenix.json_library().encode(metadata_map) {:ok, json} = Phoenix.json_library().encode(metadata_map)
File.mkdir_p!(Path.dirname(filepath)) :ok = FilesystemHelpers.write_p!(filepath, json, [:compressed])
:ok = File.write(filepath, json, [:compressed])
filepath filepath
end end
@ -45,12 +46,23 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url)) filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url))
thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url) thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url)
File.mkdir_p!(Path.dirname(filepath)) :ok = FilesystemHelpers.write_p!(filepath, thumbnail_blob)
:ok = File.write(filepath, thumbnail_blob)
filepath filepath
end 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
<<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date
Date.from_iso8601!("#{year}-#{month}-#{day}")
end
defp fetch_thumbnail_from_url(url) do defp fetch_thumbnail_from_url(url) do
http_client = Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient) http_client = Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient)
{:ok, body} = http_client.get(url, [], body_format: :binary) {:ok, body} = http_client.get(url, [], body_format: :binary)

View file

@ -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?!
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<episodedetails>
<title>#{metadata["title"]}</title>
<showtitle>#{metadata["uploader"]}</showtitle>
<uniqueid type="youtube" default="true">#{metadata["id"]}</uniqueid>
<plot>#{metadata["description"]}</plot>
<premiered>#{upload_date}</premiered>
<season>#{upload_date.year}</season>
<episode>#{Calendar.strftime(upload_date, "%m%d")}</episode>
<genre>YouTube</genre>
</episodedetails>
"""
end
end

View file

@ -25,6 +25,7 @@ defmodule Pinchflat.YtDlp.Media do
alias __MODULE__ alias __MODULE__
alias Pinchflat.Utils.FunctionUtils alias Pinchflat.Utils.FunctionUtils
alias Pinchflat.Metadata.MetadataFileHelpers
@doc """ @doc """
Downloads a single piece of media (and possibly its metadata) directly to its 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"], original_url: response["webpage_url"],
livestream: response["was_live"], livestream: response["was_live"],
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"] && parse_upload_date(response["upload_date"]) upload_date: response["upload_date"] && MetadataFileHelpers.parse_upload_date(response["upload_date"])
} }
end end
@ -106,12 +107,6 @@ defmodule Pinchflat.YtDlp.Media do
end end
end end
defp parse_upload_date(upload_date) do
<<year::binary-size(4)>> <> <<month::binary-size(2)>> <> <<day::binary-size(2)>> = upload_date
Date.from_iso8601!("#{year}-#{month}-#{day}")
end
defp backend_runner do defp backend_runner do
# This approach lets us mock the command for testing # This approach lets us mock the command for testing
Application.get_env(:pinchflat, :yt_dlp_runner) Application.get_env(:pinchflat, :yt_dlp_runner)

View file

@ -33,4 +33,27 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
assert {:error, _} = FilesystemHelpers.compute_and_save_media_filesize(media_item) assert {:error, _} = FilesystemHelpers.compute_and_save_media_filesize(media_item)
end end
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 end

View file

@ -84,4 +84,12 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
assert Path.basename(filepath) == "maxres.webp" assert Path.basename(filepath) == "maxres.webp"
end end
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 end

View file

@ -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(<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>))
assert String.contains?(nfo, "<title>#{metadata["title"]}</title>")
File.rm!(result)
end
end
end