• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

camatcode / basenji / d79e790927b7c12fb42dfc512eef9d5a90cc3956

18 Jul 2025 05:19PM UTC coverage: 59.326% (-0.1%) from 59.469%
d79e790927b7c12fb42dfc512eef9d5a90cc3956

Pull #53

github

camatcode
perf: caching Reader.info results using Cachex
Pull Request #53: perf: caching Reader.info results using Cachex

6 of 8 new or added lines in 2 files covered. (75.0%)

3 existing lines in 1 file now uncovered.

986 of 1662 relevant lines covered (59.33%)

351.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.55
/lib/basenji/reader/reader.ex
1
defmodule Basenji.Reader do
2
  @moduledoc false
3

4
  alias Basenji.Reader.CB7Reader
5
  alias Basenji.Reader.CBRReader
6
  alias Basenji.Reader.CBTReader
7
  alias Basenji.Reader.CBZReader
8
  alias Basenji.Reader.PDFReader
9
  alias Basenji.Reader.Process.JPEGOptimizer
10
  alias Basenji.Reader.Process.PNGOptimizer
11
  alias Porcelain.Result
12

13
  @readers [
14
    CBZReader,
15
    CBRReader,
16
    CB7Reader,
17
    CBTReader,
18
    PDFReader
19
  ]
20

21
  @optimizers [
22
    JPEGOptimizer,
23
    PNGOptimizer
24
  ]
25

26
  @image_extensions [
27
    "jpeg",
28
    "jpg",
29
    "jpe",
30
    "jif",
31
    "jfi",
32
    "jfif",
33
    "heif",
34
    "heic",
35
    "png",
36
    "gif",
37
    "svg",
38
    "eps",
39
    "webp",
40
    "tiff",
41
    "tif"
42
  ]
43

44
  def title_from_location(location) do
45
    location
46
    |> Path.basename()
47
    |> Path.rootname()
48
    |> ProperCase.snake_case()
49
    |> String.split("_")
50
    |> Enum.map_join(" ", &String.capitalize(&1))
45✔
51
  end
52

53
  defp info_cache_key(location, opts) do
54
    %{location: location, opts: opts}
158✔
55
  end
56

57
  def info(location, opts \\ []) do
58
    Cachex.fetch(
59
      :basenji_cache,
60
      info_cache_key(location, opts),
61
      fn _key ->
62
        any_result = get_info(location, opts)
47✔
63
        {:commit, any_result, [ttl: to_timeout(minute: 5)]}
47✔
64
      end
65
    )
66
    |> case do
158✔
NEW
67
      {:ignore, {:error, error}} ->
×
68
        {:error, error}
69

70
      {_, result} ->
71
        result
158✔
72

73
      response ->
NEW
74
        response
×
75
    end
76
  end
77

78
  defp get_info(location, opts) do
79
    reader = find_reader(location)
47✔
80

81
    info =
47✔
82
      if reader do
2✔
83
        title = title_from_location(location)
45✔
84

85
        {:ok, response} = reader.read(location, opts)
45✔
86
        %{entries: entries} = response
45✔
87
        reader.close(response[:file])
45✔
88
        %{format: reader.format(), resource_location: location, title: title, page_count: Enum.count(entries)}
45✔
89
      else
90
        {:error, :unreadable}
91
      end
92

93
    info
94
    |> case do
47✔
95
      {:error, e} -> {:error, e}
2✔
96
      inf -> {:ok, inf}
45✔
97
    end
98
  end
99

100
  def exec(cmd, args, opts \\ []) do
101
    Porcelain.exec(cmd, args, opts)
102
    |> case do
3,399✔
103
      %Result{out: output, status: 0} ->
3,397✔
104
        {:ok, output |> String.trim()}
105

106
      other ->
2✔
107
        {:error, other}
108
    end
109
  end
110

111
  def create_resource(make_func) do
112
    Stream.resource(
551✔
113
      make_func,
114
      fn
115
        :halt -> {:halt, nil}
551✔
116
        func -> {func, :halt}
551✔
117
      end,
118
      fn _ -> nil end
551✔
119
    )
120
  end
121

122
  def sort_file_names(e), do: Enum.sort_by(e, & &1.file_name)
2,828✔
123

124
  def reject_macos_preview(e), do: Enum.reject(e, &String.contains?(&1.file_name, "__MACOSX"))
2,828✔
125

126
  def reject_directories(e), do: Enum.reject(e, &(Path.extname(&1.file_name) == ""))
2,828✔
127

128
  def reject_non_image(e) do
129
    Enum.filter(e, fn ent ->
2,828✔
130
      ext = Path.extname(ent.file_name) |> String.replace(".", "") |> String.downcase()
11,312✔
131
      ext in @image_extensions
11,312✔
132
    end)
133
  end
134

135
  def read(file_path, opts \\ []) do
136
    opts = Keyword.merge([optimize: true], opts)
3,460✔
137

138
    reader = find_reader(file_path)
3,460✔
139

140
    if reader do
3,460✔
141
      read_result = reader.read(file_path, opts)
3,460✔
142
      if opts[:optimize], do: optimize_entries(read_result), else: read_result
3,460✔
143
    else
144
      {:error, :no_reader_found}
145
    end
146
  end
147

148
  def find_reader(file_path) do
149
    @readers
150
    |> Enum.reduce_while(
3,507✔
151
      nil,
152
      fn reader, _acc ->
153
        if matches_extension?(reader, file_path) && matches_magic?(reader, file_path),
10,364✔
154
          do: {:halt, reader},
155
          else: {:cont, nil}
156
      end
157
    )
158
  end
159

160
  def stream_pages(file_path, opts \\ []) do
161
    opts = Keyword.merge([start_page: 1, optimize: true], opts)
17✔
162

163
    with {:ok, %{entries: entries}} <- read(file_path, opts) do
17✔
164
      stream =
17✔
165
        opts[:start_page]..Enum.count(entries)
166
        |> Stream.map(fn idx ->
167
          at = idx - 1
68✔
168
          Enum.at(entries, at).stream_fun.()
68✔
169
        end)
170

171
      {:ok, stream}
172
    end
173
  end
174

175
  def matches_extension?(reader, filepath) do
176
    file_ext = Path.extname(filepath) |> String.downcase()
10,364✔
177

178
    reader.file_extensions()
10,364✔
179
    |> Enum.reduce_while(false, fn ext, acc ->
10,364✔
180
      if String.ends_with?(file_ext, ext), do: {:halt, true}, else: {:cont, acc}
10,364✔
181
    end)
182
  end
183

184
  def matches_magic?(reader, file_path) do
185
    reader.get_magic_numbers()
3,505✔
186
    |> Enum.reduce_while(
3,505✔
187
      nil,
188
      fn %{offset: offset, magic: magic}, _acc ->
189
        try do
3,505✔
190
          bytes =
3,505✔
191
            File.stream!(file_path, offset + Enum.count(magic), [])
192
            |> Enum.take(1)
193
            |> hd()
194
            |> :binary.bin_to_list()
195
            |> Enum.take(-1 * Enum.count(magic))
196

197
          if bytes == magic, do: {:halt, true}, else: {:cont, nil}
3,505✔
198
        rescue
199
          _e ->
×
200
            {:cont, nil}
201
        end
202
      end
203
    )
204
  end
205

206
  defp optimize_entries({:ok, result}) do
207
    updated_entries =
3,460✔
208
      Map.get(result, :entries)
209
      |> Enum.map(fn entry ->
210
        stream_fun = fn ->
13,840✔
211
          create_resource(fn -> [optimize(entry.stream_fun.()) |> :binary.bin_to_list()] end)
270✔
212
        end
213

214
        Map.put(entry, :stream_fun, stream_fun)
13,840✔
215
      end)
216

217
    result = Map.put(result, :entries, updated_entries)
3,460✔
218

219
    {:ok, result}
220
  end
221

222
  defp optimize_entries(other), do: other
×
223

224
  defp optimize(bytes) do
225
    @optimizers
226
    |> Enum.reduce(bytes |> Enum.to_list(), fn reader, bytes ->
270✔
227
      reader.optimize!(bytes)
540✔
228
    end)
229
  end
230
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc