diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..f5085d3 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,215 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/lib/pinchflat/media.ex b/lib/pinchflat/media.ex index fe864ae..85529a7 100644 --- a/lib/pinchflat/media.ex +++ b/lib/pinchflat/media.ex @@ -19,15 +19,31 @@ defmodule Pinchflat.Media do @doc """ Returns a list of pending media_items for a given source, where - pending means the `media_filepath` is `nil`. + pending means the `media_filepath` is `nil` AND the media_item + matches the format selection rules of the parent media_profile. + + See `build_format_clauses` but tl;dr is it _may_ filter based + on shorts or livestreams depending on the media_profile settings. Returns [%MediaItem{}, ...]. """ def list_pending_media_items_for(%Source{} = source) do - from( - m in MediaItem, - where: m.source_id == ^source.id and is_nil(m.media_filepath) - ) + media_profile = Repo.preload(source, :media_profile).media_profile + + MediaItem + |> where([mi], mi.source_id == ^source.id and is_nil(mi.media_filepath)) + |> where(^build_format_clauses(media_profile)) + |> Repo.all() + end + + @doc """ + Returns a list of downloaded media_items for a given source. + + Returns [%MediaItem{}, ...]. + """ + def list_downloaded_media_items_for(%Source{} = source) do + MediaItem + |> where([mi], mi.source_id == ^source.id and not is_nil(mi.media_filepath)) |> Repo.all() end @@ -72,4 +88,39 @@ defmodule Pinchflat.Media do def change_media_item(%MediaItem{} = media_item, attrs \\ %{}) do MediaItem.changeset(media_item, attrs) end + + defp build_format_clauses(media_profile) do + mapped_struct = Map.from_struct(media_profile) + + Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic -> + case {attr, media_profile} do + {{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} -> + dynamic([mi], ^dynamic and (mi.livestream == true or fragment("? ILIKE ?", mi.original_url, "%/shorts/%"))) + + # Technically redundant, but makes the other clauses easier to parse + # (redundant because this condition is the same as the condition above, just flipped) + {{:livestream_behaviour, :only}, %{shorts_behaviour: :only}} -> + dynamic + + {{:shorts_behaviour, :only}, _} -> + # return records with /shorts/ in the original_url + dynamic([mi], ^dynamic and fragment("? ILIKE ?", mi.original_url, "%/shorts/%")) + + {{:livestream_behaviour, :only}, _} -> + # return records with livestream: true + dynamic([mi], ^dynamic and mi.livestream == true) + + {{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only -> + # return records without /shorts/ in the original_url + dynamic([mi], ^dynamic and fragment("? NOT ILIKE ?", mi.original_url, "%/shorts/%")) + + {{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only -> + # return records with livestream: false + dynamic([mi], ^dynamic and mi.livestream == false) + + _ -> + dynamic + end + end) + end end diff --git a/lib/pinchflat/profiles/media_profile.ex b/lib/pinchflat/profiles/media_profile.ex index 0d7fad9..e92d1fb 100644 --- a/lib/pinchflat/profiles/media_profile.ex +++ b/lib/pinchflat/profiles/media_profile.ex @@ -45,7 +45,9 @@ defmodule Pinchflat.Profiles.MediaProfile do # through the entire collection to determine if a video is a short or # a livestream. # NOTE: these can BOTH be set to :only which will download shorts and - # livestreams _only_ and ignore regular videos. + # livestreams _only_ and ignore regular videos. The redundant case + # is when one is set to :only and the other is set to :exclude. + # See `build_format_clauses` in the Media context for more. field :shorts_behaviour, Ecto.Enum, values: [:include, :exclude, :only], default: :include field :livestream_behaviour, Ecto.Enum, values: [:include, :exclude, :only], default: :include diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index 234eba4..bcf758c 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -3,6 +3,7 @@ defmodule Pinchflat.MediaTest do import Pinchflat.TasksFixtures import Pinchflat.MediaFixtures + import Pinchflat.ProfilesFixtures import Pinchflat.MediaSourceFixtures alias Pinchflat.Media @@ -30,7 +31,7 @@ defmodule Pinchflat.MediaTest do end describe "list_pending_media_items_for/1" do - test "it returns pending media_items for a given source" do + test "it returns pending without a filepath for a given source" do source = source_fixture() media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil}) @@ -50,6 +51,138 @@ defmodule Pinchflat.MediaTest do end end + describe "list_pending_media_items_for/1 when testing shorts" do + test "returns shorts and normal media when shorts_behaviour is :include" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{shorts_behaviour: :include}).id}) + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [normal, short] + end + + test "returns only shorts when shorts_behaviour is :only" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{shorts_behaviour: :only}).id}) + _normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [short] + end + + test "returns only normal media when shorts_behaviour is :exclude" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{shorts_behaviour: :exclude}).id}) + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + _short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [normal] + end + end + + describe "list_pending_media_items_for/1 when testing livestreams" do + test "returns livestreams and normal media when livestream_behaviour is :include" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{livestream_behaviour: :include}).id}) + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + + assert Media.list_pending_media_items_for(source) == [normal, livestream] + end + + test "returns only livestreams when livestream_behaviour is :only" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{livestream_behaviour: :only}).id}) + _normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + + assert Media.list_pending_media_items_for(source) == [livestream] + end + + test "returns only normal media when livestream_behaviour is :exclude" do + source = source_fixture(%{media_profile_id: media_profile_fixture(%{livestream_behaviour: :exclude}).id}) + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + _livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + + assert Media.list_pending_media_items_for(source) == [normal] + end + end + + describe "list_pending_media_items_for/1 when testing all format options" do + test "returns livestreams, shorts, and normal media when behaviour is :include" do + source = + source_fixture(%{ + media_profile_id: + media_profile_fixture(%{ + shorts_behaviour: :include, + livestream_behaviour: :include + }).id + }) + + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [normal, livestream, short] + end + + test "returns only livestreams and shorts when behaviour is :only" do + source = + source_fixture(%{ + media_profile_id: + media_profile_fixture(%{ + shorts_behaviour: :only, + livestream_behaviour: :only + }).id + }) + + _normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [livestream, short] + end + + test "returns only normal media when behaviour is :exclude" do + source = + source_fixture(%{ + media_profile_id: + media_profile_fixture(%{ + shorts_behaviour: :exclude, + livestream_behaviour: :exclude + }).id + }) + + normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + _livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + _short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [normal] + end + + test ":only and :exclude return the expected results" do + source = + source_fixture(%{ + media_profile_id: + media_profile_fixture(%{ + shorts_behaviour: :only, + livestream_behaviour: :exclude + }).id + }) + + _normal = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + _livestream = media_item_fixture(%{source_id: source.id, media_filepath: nil, livestream: true}) + short = media_item_fixture(%{source_id: source.id, media_filepath: nil, original_url: "/shorts/"}) + + assert Media.list_pending_media_items_for(source) == [short] + end + end + + describe "list_downloaded_media_items_for/1" do + test "returns only media items with a media_filepath" do + source = source_fixture() + _media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + media_item = media_item_fixture(%{source_id: source.id, media_filepath: "/video/#{Faker.File.file_name(:video)}"}) + + assert Media.list_downloaded_media_items_for(source) == [media_item] + end + end + describe "get_media_item!/1" do test "it returns the media_item with given id" do media_item = media_item_fixture()