From 582eb53698bc1e88d76646dc99f9969876f71364 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 6 Jun 2024 11:54:39 -0700 Subject: [PATCH] Add regex for validating source URL (#285) --- lib/pinchflat/sources/source.ex | 8 ++++ test/pinchflat/sources_test.exs | 55 +++++++++++++++++++++++ test/support/fixtures/sources_fixtures.ex | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/pinchflat/sources/source.ex b/lib/pinchflat/sources/source.ex index 02af5bd..e409725 100644 --- a/lib/pinchflat/sources/source.ex +++ b/lib/pinchflat/sources/source.ex @@ -115,6 +115,7 @@ defmodule Pinchflat.Sources.Source do |> validate_number(:retention_period_days, greater_than_or_equal_to: 0) # Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room) |> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}") + |> validate_format(:original_url, youtube_channel_or_playlist_regex(), message: "must be a channel or playlist URL") |> cast_assoc(:metadata, with: &SourceMetadata.changeset/2, required: false) |> unique_constraint([:collection_id, :media_profile_id, :title_filter_regex], error_key: :original_url) end @@ -141,6 +142,13 @@ defmodule Pinchflat.Sources.Source do ~w(__meta__ __struct__ metadata tasks media_items)a end + def youtube_channel_or_playlist_regex do + # Validate that the original URL is not a video URL + # Also matches if the string does NOT contain youtube.com or youtu.be. This preserves my tenuous support + # for non-youtube sources. + ~r<^(?:(?!youtube\.com/(watch|shorts|embed)|youtu\.be).)*$> + end + defimpl Jason.Encoder, for: Source do def encode(value, opts) do value diff --git a/test/pinchflat/sources_test.exs b/test/pinchflat/sources_test.exs index a63c049..22e4fc7 100644 --- a/test/pinchflat/sources_test.exs +++ b/test/pinchflat/sources_test.exs @@ -659,6 +659,61 @@ defmodule Pinchflat.SourcesTest do end end + describe "change_source/3 when testing original_url validation" do + test "succeeds when an original URL is valid" do + source = source_fixture() + + valid_urls = [ + "https://www.youtube.com/channel/UCkRfArvrzheW2E7b6SVT7vQ", + "https://www.youtube.com/channel/UCkRfArvrzheW2E7b6SVT7vQ/videos", + "https://www.youtube.com/@youtubecreators/featured", + "https://www.youtube.com/@youtubecreators", + "https://www.youtube.com/c/YouTubeCreators", + "https://www.youtube.com/user/YouTubeCreators", + "https://www.youtube.com/YouTubeCreators", + "https://www.youtube.com/playlist?list=PLpjK416fmKwRtq-9-O_NbZlkW0k6zu2Wn", + "https://www.youtube.com/playlist?list=UUkRfArvrzheW2E7b6SVT7vQ" + ] + + Enum.each(valid_urls, fn url -> + assert %{errors: []} = Sources.change_source(source, %{original_url: url}) + end) + end + + test "fails when an original URL points to a video" do + source = source_fixture() + + invalid_urls = [ + "https://www.youtube.com/watch?v=72maj9FLQZI", + "https://youtu.be/72maj9FLQZI", + "https://www.youtube.com/watch?v=1FwGFhMAmBo&list=PLpjK416fmKwRtq-9-O_NbZlkW0k6zu2Wn", + "https://www.youtube.com/shorts/Dq0eH-ZhQTU", + "https://www.youtube.com/embed/X64LHlfx4qg" + ] + + Enum.each(invalid_urls, fn url -> + assert %{errors: [_]} = Sources.change_source(source, %{original_url: url}) + end) + end + + test "passes when a non-youtube link is provided" do + source = source_fixture() + + valid_urls = [ + "https://www.example.com", + "https://www.example.com/playlist", + "https://www.example.com/channel", + "https://www.example.com/user", + "https://www.example.com/watch?v=72maj9FLQZI", + "https://www.example.com/embed/X64LHlfx4qg" + ] + + Enum.each(valid_urls, fn url -> + assert %{errors: []} = Sources.change_source(source, %{original_url: url}) + end) + end + end + defp playlist_mock(_url, _opts, _ot) do { :ok, diff --git a/test/support/fixtures/sources_fixtures.ex b/test/support/fixtures/sources_fixtures.ex index bb768db..a06329a 100644 --- a/test/support/fixtures/sources_fixtures.ex +++ b/test/support/fixtures/sources_fixtures.ex @@ -25,7 +25,7 @@ defmodule Pinchflat.SourcesFixtures do collection_type: "channel", custom_name: "Cool and good internal name!", description: "This is a description", - original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}", + original_url: "https://www.youtube.com/@#{Faker.String.base64(12)}", media_profile_id: ProfilesFixtures.media_profile_fixture().id, index_frequency_minutes: 60 }