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

camatcode / basenji / a1f49bf5a97a466439527ec19c2212108e84f1aa

07 Jul 2025 09:50PM UTC coverage: 66.997% (+1.2%) from 65.799%
a1f49bf5a97a466439527ec19c2212108e84f1aa

Pull #27

github

camatcode
feat(collections): collection processing
Pull Request #27: feat(collections): collection processing

37 of 45 new or added lines in 6 files covered. (82.22%)

16 existing lines in 4 files now uncovered.

406 of 606 relevant lines covered (67.0%)

374.21 hits per line

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

91.67
/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.Process.JPEGOptimizer
9
  alias Basenji.Reader.Process.PNGOptimizer
10
  alias Porcelain.Result
11

12
  @readers [
13
    CBZReader,
14
    CBRReader,
15
    CB7Reader,
16
    CBTReader
17
  ]
18

19
  @optimizers [
20
    JPEGOptimizer,
21
    PNGOptimizer
22
  ]
23

24
  def info(location, opts \\ []) do
25
    reader = find_reader(location)
40✔
26

27
    info =
40✔
28
      if reader do
2✔
29
        title = location |> Path.basename() |> Path.rootname()
38✔
30
        {:ok, response} = reader.read(location, opts)
38✔
31
        %{entries: entries} = response
38✔
32
        reader.close(response[:file])
38✔
33
        %{format: reader.format(), resource_location: location, title: title, page_count: Enum.count(entries)}
38✔
34
      else
35
        {:error, :unreadable}
36
      end
37

38
    info
39
    |> case do
40✔
40
      {:error, e} -> {:error, e}
2✔
41
      inf -> {:ok, inf}
38✔
42
    end
43
  end
44

45
  def exec(cmd, args, opts \\ []) do
46
    Porcelain.exec(cmd, args, opts)
47
    |> case do
1,374✔
48
      %Result{out: output, status: 0} ->
1,374✔
49
        {:ok, output |> String.trim()}
50

UNCOV
51
      other ->
×
52
        {:error, other}
53
    end
54
  end
55

56
  def create_resource(make_func) do
57
    Stream.resource(
115✔
58
      make_func,
59
      fn
60
        :halt -> {:halt, nil}
115✔
61
        func -> {func, :halt}
115✔
62
      end,
63
      fn _ -> nil end
115✔
64
    )
65
  end
66

67
  def sort_file_names(e), do: Enum.sort_by(e, & &1.file_name)
2,540✔
68

69
  def reject_macos_preview(e), do: Enum.reject(e, &String.contains?(&1.file_name, "__MACOSX"))
2,540✔
70

71
  def reject_directories(e), do: Enum.reject(e, &(Path.extname(&1.file_name) == ""))
2,540✔
72

73
  def read(file_path, opts \\ []) do
74
    opts = Keyword.merge([optimize: false], opts)
2,488✔
75

76
    reader = find_reader(file_path)
2,488✔
77

78
    if reader do
2,488✔
79
      read_result = reader.read(file_path, opts)
2,488✔
80
      if opts[:optimize], do: optimize_entries(read_result), else: read_result
2,488✔
81
    else
UNCOV
82
      {:error, "No Reader found for: #{file_path}. Unknown file type"}
×
83
    end
84
  end
85

86
  defp find_reader(file_path) do
87
    @readers
88
    |> Enum.reduce_while(
2,528✔
89
      nil,
90
      fn reader, _acc ->
91
        if matches_magic?(reader, file_path), do: {:halt, reader}, else: {:cont, nil}
6,350✔
92
      end
93
    )
94
  end
95

96
  def stream_pages(file_path, opts \\ []) do
97
    opts = Keyword.merge([start_page: 1], opts)
8✔
98

99
    with {:ok, %{entries: entries}} <- read(file_path, opts) do
8✔
100
      stream =
8✔
101
        opts[:start_page]..Enum.count(entries)
102
        |> Stream.map(fn idx ->
103
          at = idx - 1
32✔
104
          Enum.at(entries, at).stream_fun.()
32✔
105
        end)
106

107
      {:ok, stream}
108
    end
109
  end
110

111
  def matches_magic?(reader, file_path) do
112
    reader.get_magic_numbers()
6,350✔
113
    |> Enum.reduce_while(
6,350✔
114
      nil,
115
      fn %{offset: offset, magic: magic}, _acc ->
116
        try do
6,350✔
117
          bytes =
6,350✔
118
            File.stream!(file_path, offset + Enum.count(magic), [])
119
            |> Enum.take(1)
120
            |> hd()
121
            |> :binary.bin_to_list()
122
            |> Enum.take(-1 * Enum.count(magic))
123

124
          if bytes == magic, do: {:halt, true}, else: {:cont, nil}
6,350✔
125
        rescue
NEW
UNCOV
126
          _e ->
×
127
            {:cont, nil}
128
        end
129
      end
130
    )
131
  end
132

133
  defp optimize_entries({:ok, result}) do
134
    updated_entries =
37✔
135
      Map.get(result, :entries)
136
      |> Enum.map(fn entry ->
137
        stream_fun = fn ->
148✔
138
          create_resource(fn -> [optimize(entry.stream_fun.()) |> :binary.bin_to_list()] end)
40✔
139
        end
140

141
        Map.put(entry, :stream_fun, stream_fun)
148✔
142
      end)
143

144
    result = Map.put(result, :entries, updated_entries)
37✔
145

146
    {:ok, result}
147
  end
148

UNCOV
149
  defp optimize_entries(other), do: other
×
150

151
  defp optimize(bytes) do
152
    @optimizers
153
    |> Enum.reduce(bytes |> Enum.to_list(), fn reader, bytes ->
40✔
154
      reader.optimize!(bytes)
80✔
155
    end)
156
  end
157
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