Skip to content

Commit

Permalink
feat: use Stow for Lastfm client playcount and scrobbles functions
Browse files Browse the repository at this point in the history
  • Loading branch information
boonious committed Apr 13, 2024
1 parent a0202b8 commit 247e062
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 82 deletions.
7 changes: 4 additions & 3 deletions lib/lastfm_archive/archive/file_archive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ defmodule LastfmArchive.Archive.FileArchive do

def archive(%{creator: user} = metadata, options, api) do
setup(user, options)
api = %{api | user: user}

with {:ok, metadata} <- update_metadata(metadata, options, api),
options <- Keyword.validate!(options, default_opts()),
Expand Down Expand Up @@ -160,7 +161,7 @@ defmodule LastfmArchive.Archive.FileArchive do
:timer.sleep(Keyword.fetch!(options, :interval))
year = DateTime.from_unix!(from).year

with {:ok, {playcount, _}} <- client_impl().playcount(metadata.identifier, time_range, api),
with {:ok, {playcount, _}} <- client_impl().playcount(time_range, api),
num_pages <- num_pages(playcount, Keyword.fetch!(options, :per_page)) do
Logger.info("#{date(from)}: #{playcount} scrobble(s), #{num_pages} page(s)")
results = write_to_archive(metadata, time_range, num_pages, api, options)
Expand All @@ -182,7 +183,7 @@ defmodule LastfmArchive.Archive.FileArchive do
:timer.sleep(Keyword.fetch!(opts, :interval))
per_page = Keyword.fetch!(opts, :per_page)

with {:ok, scrobbles} <- client_impl().scrobbles(user, {page, per_page, from, to}, api),
with {:ok, scrobbles} <- client_impl().scrobbles({page, per_page, from, to}, api),
:ok <- write(metadata, scrobbles, filepath: page_path(from, page, per_page)) do
Logger.info("✓ page #{page} written to #{user_dir(user, opts)}/#{page_path(from, page, per_page)}.gz")
:ok
Expand All @@ -198,7 +199,7 @@ defmodule LastfmArchive.Archive.FileArchive do
now = DateTime.utc_now() |> DateTime.to_unix()

with {:ok, {total, registered_time}} <- client_impl().info(%{api | method: "user.getinfo", user: user}),
{:ok, {_, last_scrobble_time}} <- client_impl().playcount(user, {registered_time, now}, api) do
{:ok, {_, last_scrobble_time}} <- client_impl().playcount({registered_time, now}, api) do
Metadata.new(archive, total, {registered_time, last_scrobble_time})
|> Archive.impl().update_metadata(options)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/lastfm_archive/behaviour/lastfm_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule LastfmArchive.Behaviour.LastfmClient do
See Lastfm API [documentation](https://www.last.fm/api/show/user.getRecentTracks) for more details.
"""
@callback scrobbles(user, {page, limit, from, to}, lastfm_api) :: {:ok, map} | {:error, term()}
@callback scrobbles({page, limit, from, to}, lastfm_api) :: {:ok, map} | {:error, term()}

@doc """
Returns the total playcount, registered, i.e. first scrobble time for a user.
Expand All @@ -34,7 +34,7 @@ defmodule LastfmArchive.Behaviour.LastfmClient do
@doc """
Returns the playcount and the latest scrobble date of a user for a given time range.
"""
@callback playcount(user, {from, to}, lastfm_api) :: {:ok, {playcount, latest_scrobble_time}} | {:error, term()}
@callback playcount({from, to}, lastfm_api) :: {:ok, {playcount, latest_scrobble_time}} | {:error, term()}

@doc false
def impl, do: Application.get_env(:lastfm_archive, :lastfm_client, LastfmArchive.LastfmClient.Impl)
Expand Down
60 changes: 23 additions & 37 deletions lib/lastfm_archive/lastfm_client/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule LastfmArchive.LastfmClient.Impl do
@behaviour LastfmArchive.Behaviour.LastfmClient

import Plug.Conn, only: [put_req_header: 3]
import LastfmArchive.LastfmClient.LastfmApi, only: [extended: 2, pagination: 2, time_range: 2]

alias LastfmArchive.LastfmClient.LastfmApi
alias Plug.Conn
Expand All @@ -19,16 +20,21 @@ defmodule LastfmArchive.LastfmClient.Impl do
@impl true
def info(api \\ LastfmApi.new("user.getinfo")) do
api.key
|> req_read_source(api |> to_string())
|> handle_response(:info)
end

defp req_read_source(api_key, uri) do
api_key
|> new_conn()
|> source(api |> to_string())
|> source(uri)
|> read_body()
|> handle_response(:info)
end

defp new_conn(api_key), do: %Plug.Conn{} |> put_req_header("Authorization", "Bearer #{api_key}")
defp source(conn, uri), do: Source.call(conn, Source.init(uri: uri))

# TODO: need Stow to better return plug private (error) status
# TODO: need Stow to have API to better return plug private (error) status
defp read_body(%Conn{resp_body: body}) when not is_nil(body), do: body |> Jason.decode!()
defp read_body(%{private: %{stow: %{source: %{status: status}}}}), do: status

Expand All @@ -38,24 +44,28 @@ defmodule LastfmArchive.LastfmClient.Impl do
See Lastfm API [documentation](https://www.last.fm/api/show/user.getRecentTracks) for more details.
"""
@impl true
def scrobbles(user, {page, limit, from, to}, api) do
# can incorporate these into the Api struct
extra_query = [limit: limit, page: page, from: from, to: to, extended: 1] |> encode() |> Enum.join()

"#{api.endpoint}?method=#{api.method}&user=#{user}&api_key=#{api.key}&format=json#{extra_query}"
|> get(api.key)
def scrobbles({page, limit, from, to}, api) do
api.key
|> req_read_source(
api
|> time_range({from, to})
|> pagination({page, limit})
|> extended(1)
|> to_string()
)
|> handle_response(:scrobbles)
end

@doc """
Returns the playcount of a user for a given time range.
"""
@impl true
def playcount(user \\ default_user(), {from, to} \\ {nil, nil}, api \\ LastfmApi.new()) do
extra_query = [limit: 1, page: 1, from: from, to: to] |> encode() |> Enum.join()
def playcount(time_range \\ {nil, nil}, api \\ LastfmApi.new())
def playcount({nil, nil}, api), do: req_read_source(api.key, api |> to_string()) |> handle_response(:playcount)

"#{api.endpoint}?method=#{api.method}&user=#{user}&api_key=#{api.key}&format=json#{extra_query}"
|> get(api.key)
def playcount(time_range, api) do
api.key
|> req_read_source(api |> time_range(time_range) |> to_string())
|> handle_response(:playcount)
end

Expand Down Expand Up @@ -90,28 +100,4 @@ defmodule LastfmArchive.LastfmClient.Impl do
defp format(number) when is_integer(number), do: number
defp format(number) when is_binary(number), do: number |> String.to_integer()
defp format(nil), do: nil

defp get(url, key) do
:httpc.request(:get, {to_charlist(url), [{'Authorization', to_charlist("Bearer #{key}")}]}, [ssl: ssl_opts()], [])
|> case do
{:ok, {{_scheme, _status, _}, _headers, body}} -> body |> Jason.decode!()
{:error, error} -> {:error, error}
end
end

defp encode({_k, 0}), do: ""
defp encode({k, v}), do: "&#{k}=#{v}"
defp encode(args), do: for({k, v} <- args, v != nil, do: encode({k, v}))

defp ssl_opts do
[
verify: :verify_peer,
cacertfile: '#{CAStore.file_path()}',
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
],
versions: [:"tlsv1.2"],
depth: 4
]
end
end
12 changes: 12 additions & 0 deletions lib/lastfm_archive/lastfm_client/lastfm_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ defmodule LastfmArchive.LastfmClient.LastfmApi do
def new(method \\ lastfm_api_method(), user \\ lastfm_user()) do
%__MODULE__{endpoint: lastfm_api_endpoint(), key: lastfm_api_key(), method: method, user: user}
end

def time_range(api, {from, to}) when is_integer(from) and is_integer(to) do
%{api | from: from, to: to}
end

def pagination(api, {page, limit}) when is_integer(page) and is_integer(limit) do
%{api | limit: limit, page: page}
end

def extended(api, extended) when is_integer(extended) do
%{api | extended: extended}
end
end

defimpl String.Chars, for: LastfmArchive.LastfmClient.LastfmApi do
Expand Down
2 changes: 1 addition & 1 deletion lib/lastfm_archive/livebook.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule LastfmArchive.Livebook do
info_api = LastfmApi.new("user.getinfo")
time_range = {nil, nil}

case {user, impl.info(info_api), impl.playcount(user, time_range, playcount_api)} do
case {user, impl.info(info_api), impl.playcount(time_range, playcount_api)} do
{"", _, _} ->
Kino.Markdown.new("""
Please specify a Lastfm user in configuration.
Expand Down
38 changes: 20 additions & 18 deletions test/lastfm_archive/archive/file_archive_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule LastfmArchive.Archive.FileArchiveTest do
stub_with(LastfmArchive.CacheMock, LastfmArchive.CacheStub)
stub_with(LastfmArchive.FileIOMock, LastfmArchive.FileIOStub)

stub(LastfmClient.impl(), :scrobbles, fn _, _, _ -> {:ok, context.scrobbles} end)
stub(LastfmClient.impl(), :scrobbles, fn _, _ -> {:ok, context.scrobbles} end)
Archive.impl() |> stub(:update_metadata, fn metadata, _options -> {:ok, metadata} end)

:ok
Expand All @@ -52,9 +52,9 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmClient.impl()
|> expect(:info, fn %{user: ^user} = _client -> {:ok, {total_scrobbles, first_scrobble_time}} end)
|> expect(:playcount, fn ^user, _time_range, _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn ^user, _time_range, _client -> {:ok, {daily_playcount, last_scrobble_time}} end)
|> stub(:scrobbles, fn ^user, _client_args, _client -> {:ok, scrobbles} end)
|> expect(:playcount, fn _time_range, %{user: ^user} = _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn _time_range, %{user: ^user} = _client -> {:ok, {daily_playcount, last_scrobble_time}} end)
|> stub(:scrobbles, fn _client_args, %{user: ^user} = _client -> {:ok, scrobbles} end)

Archive.impl()
|> expect(:update_metadata, fn ^metadata, _options -> {:ok, metadata} end)
Expand Down Expand Up @@ -119,7 +119,7 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmArchive.CacheMock |> expect(:get, fn {^user, 2021}, _cache -> cache_ok_status end)

LastfmClient.impl() |> expect(:scrobbles, 3, fn _user, _client_args, _client -> {:ok, %{}} end)
LastfmClient.impl() |> expect(:scrobbles, 3, fn _client_args, _client -> {:ok, %{}} end)
LastfmArchive.FileIOMock |> expect(:write, 3, fn _path, _data, [:compressed] -> :ok end)

refute capture_log(fn -> assert {:ok, %Metadata{}} = FileArchive.archive(metadata, opts) end) =~ "Skipping"
Expand All @@ -141,7 +141,7 @@ defmodule LastfmArchive.Archive.FileArchiveTest do
end

test "caches status on Lastfm API call errors", %{metadata: metadata, user: user} do
LastfmClient.impl() |> expect(:scrobbles, 3, fn _user, _client_args, _client -> {:error, "error"} end)
LastfmClient.impl() |> expect(:scrobbles, 3, fn _client_args, _client -> {:error, "error"} end)

LastfmArchive.CacheMock
|> expect(:put, 3, fn {^user, 2021}, _time, {_playcount, [error: _data]}, _opts, _cache -> :ok end)
Expand All @@ -165,8 +165,10 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmClient.impl()
|> expect(:info, fn %{user: ^user} = _client -> {:ok, {total_scrobbles, first_scrobble_time}} end)
|> expect(:playcount, 2, fn ^user, _time_range, _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> expect(:scrobbles, fn ^user, _client_args, _client -> {:ok, %{}} end)
|> expect(:playcount, 2, fn _time_range, %{user: ^user} = _client ->
{:ok, {total_scrobbles, last_scrobble_time}}
end)
|> expect(:scrobbles, fn _client_args, %{user: ^user} = _client -> {:ok, %{}} end)

LastfmArchive.CacheMock
|> expect(:put, 0, fn {_user, _year}, {_from, _to}, {_total_scrobbles, _status}, _opts, _cache -> :ok end)
Expand All @@ -175,7 +177,7 @@ defmodule LastfmArchive.Archive.FileArchiveTest do
end

test "handles first total playcount API call error", %{metadata: metadata, user: user} do
LastfmClient.impl() |> expect(:playcount, fn ^user, _time_range, _client -> {:error, "error"} end)
LastfmClient.impl() |> expect(:playcount, fn _time_range, %{user: ^user} = _client -> {:error, "error"} end)
assert FileArchive.archive(metadata, []) == {:error, "error"}
end

Expand All @@ -195,9 +197,9 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmClient.impl()
|> expect(:info, fn %{user: ^user} = _client -> {:ok, {total_scrobbles, first_scrobble_time}} end)
|> expect(:playcount, fn ^user, _time_range, _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn ^user, _time_range, _client -> {:error, api_error} end)
|> stub(:scrobbles, fn ^user, _client_args, _client -> {:ok, scrobbles} end)
|> expect(:playcount, fn _time_range, %{user: ^user} = _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn _time_range, %{user: ^user} = _client -> {:error, api_error} end)
|> stub(:scrobbles, fn _client_args, %{user: ^user} = _client -> {:ok, scrobbles} end)

LastfmArchive.CacheMock
|> expect(:put, 0, fn {^user, 2021}, _time, {_playcount, _status}, _opts, _cache -> :ok end)
Expand All @@ -208,8 +210,8 @@ defmodule LastfmArchive.Archive.FileArchiveTest do
test "does nothing when user have 0 scrobble", %{metadata: metadata} do
LastfmClient.impl()
|> expect(:info, 0, fn _client -> {:ok, ""} end)
|> expect(:playcount, 0, fn _user, _time_range, _client -> {:ok, ""} end)
|> expect(:scrobbles, 0, fn _user, _client_args, _client -> {:ok, ""} end)
|> expect(:playcount, 0, fn _time_range, _client -> {:ok, ""} end)
|> expect(:scrobbles, 0, fn _client_args, _client -> {:ok, ""} end)

LastfmArchive.FileIOMock |> expect(:write, 0, fn _path, _data, [:compressed] -> :ok end)

Expand All @@ -223,9 +225,9 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmClient.impl()
|> expect(:info, fn _client -> {:ok, {total_scrobbles, first_scrobble_time}} end)
|> expect(:playcount, fn _user, _time_range, _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn _user, _time_range, _client -> {:ok, {daily_playcount, last_scrobble_time}} end)
|> expect(:scrobbles, 0, fn _user, _client_args, _client -> {:ok, ""} end)
|> expect(:playcount, fn _time_range, _client -> {:ok, {total_scrobbles, last_scrobble_time}} end)
|> stub(:playcount, fn _time_range, _client -> {:ok, {daily_playcount, last_scrobble_time}} end)
|> expect(:scrobbles, 0, fn _client_args, _client -> {:ok, ""} end)

LastfmArchive.FileIOMock |> expect(:write, 0, fn _path, _data, [:compressed] -> :ok end)

Expand All @@ -242,7 +244,7 @@ defmodule LastfmArchive.Archive.FileArchiveTest do

LastfmArchive.CacheMock |> expect(:get, fn {^user, 2021}, _cache -> cache_ok_status end)

LastfmClient.impl() |> expect(:scrobbles, 0, fn _user, _client_args, _client -> {:ok, ""} end)
LastfmClient.impl() |> expect(:scrobbles, 0, fn _client_args, _client -> {:ok, ""} end)
LastfmArchive.FileIOMock |> expect(:write, 0, fn _path, _data, [:compressed] -> :ok end)

assert capture_log(fn -> assert {:ok, %Metadata{}} = FileArchive.archive(metadata, []) end) =~ "Skipping"
Expand Down
Loading

0 comments on commit 247e062

Please sign in to comment.