cbz_tools 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be6a34e670ac04d6ea7d87875e6ce57eb182dda4f6349f72404ae66ce6a863df
4
- data.tar.gz: 46d9ff6e0b8b52d13027a1ea06558947e6407cc0408637acb9acf9cbe03aafe5
3
+ metadata.gz: 96ee68c0b79bcb1256324ad692bf441f0926443ab71e87db3715c0ca5c89cac0
4
+ data.tar.gz: 15d6235b0d040d8836a752da8e304bb89d2fd0976f9eb371f6f514b6dd8c97c4
5
5
  SHA512:
6
- metadata.gz: 54063af8baff3b28a84f03d20339d85821f1d0326011faf81afecb4c5081840292a8083a936070d597040cc79ac4833d68c4717fb7a0d0575d68f2fceb823103
7
- data.tar.gz: e264ef924c0781ef429da2aa89ee71dbc9ff7c3d37307bf832dd10e633852e1326af140ced09b2aaf8c4d5185deefd76e6dcf45b0b1f6e09a8989619c8dbc59f
6
+ metadata.gz: dfaee61a3965d6901835d58ca0206ee5bd5f4a05eb1ffb00d2248ca16755aeb1d418b54f3550fe9ef6d0d125aff393955aec459990a279259bc05d874e7d1adc
7
+ data.tar.gz: 7e2e12a38206a9a6ea43d49eb4474a60770d094c48d47746a3cb6f249496abf382af564caacd41e5426684077a23135018ef108795232c87e592a835740ef435
@@ -1,3 +1,3 @@
1
1
  {
2
- "cSpell.words": ["comicinfo"]
2
+ "cSpell.words": ["comicinfo", "gtin", "penciller"]
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-08-03
4
+
5
+ - Add `CbzTools::CbrConverter` for converting CBR archives to CBZ. The gem ships the orchestration (extract → repack → error model) but no default extractor — callers pass `extractor:` (anything responding to `call(cbr_path, destination_dir)` and raising `ExtractionError` on failure).
6
+
7
+ ## [0.3.0] - 2026-08-03
8
+
9
+ - Add `CbzTools::ComicInfo` (parse/build ComicInfo.xml, `SCHEMA`); raises `ParseError` on bad XML.
10
+ - **Breaking**: `Reader#comic_info` returns parsed hash (`{}` when absent), not raw XML.
11
+ - Add `Reader#update_comic_info(metadata)` to write `ComicInfo.xml` back.
12
+ - `Reader#read` is now private; use `first_image`, `image`, `comic_info`.
13
+
3
14
  ## [0.2.0] - 2026-08-03
4
15
 
5
16
  - Add `CbzTools::Reader` for reading CBZ archives (images and `ComicInfo.xml`)
data/README.md CHANGED
@@ -18,7 +18,57 @@ gem install cbz_tools
18
18
 
19
19
  ## Usage
20
20
 
21
- Usage documentation will be added as the features land.
21
+ ### Reading a CBZ archive
22
+
23
+ ```ruby
24
+ CbzTools::Reader.open("path/to/comic.cbz") do |reader|
25
+ reader.image_names # => ["page_001.jpg", "page_002.jpg", ...]
26
+ reader.first_image # => "<image bytes>"
27
+ reader.comic_info # => {title: "...", page_count: "32", ...}
28
+ end
29
+ ```
30
+
31
+ ### Editing ComicInfo.xml inside a CBZ
32
+
33
+ ```ruby
34
+ CbzTools::Reader.open("path/to/comic.cbz") do |reader|
35
+ reader.update_comic_info(title: "New Title", page_count: "32")
36
+ end
37
+ ```
38
+
39
+ ### Converting CBR to CBZ
40
+
41
+ `CbzTools::CbrConverter` orchestrates the extract → repack → CBZ step,
42
+ but it does **not** ship an extractor. Pass anything responding to
43
+ `call(cbr_path, destination_dir)` (and that raises `ExtractionError`
44
+ on failure):
45
+
46
+ ```ruby
47
+ extractor = MyApp::UnrarExtractor.new
48
+ CbzTools::CbrConverter.convert("path/to/comic.cbr",
49
+ to: "path/to/comic.cbz",
50
+ extractor: extractor)
51
+ ```
52
+
53
+ Reuse one extractor across many conversions with `work_in:` to pool
54
+ the tmpdir:
55
+
56
+ ```ruby
57
+ extractor = MyApp::UnrarExtractor.new
58
+ work_in = Dir.mktmpdir("batch")
59
+ converter = CbzTools::CbrConverter.new(extractor: extractor)
60
+
61
+ paths.each do |cbr|
62
+ converter.convert(cbr, to: cbr.sub(/\.cbr\z/i, ".cbz"), work_in: work_in)
63
+ end
64
+ ```
65
+
66
+ Errors inherit from `CbzTools::Error`:
67
+
68
+ - `CbzTools::CbrConverter::ExtractionError` — the extractor failed.
69
+ - `CbzTools::CbrConverter::ArchiveError` — no extractor supplied,
70
+ the destination couldn't be written, or `overwrite: false` and the
71
+ destination already exists.
22
72
 
23
73
  ## Development
24
74
 
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "fileutils"
5
+ require "zip"
6
+ require "pathname"
7
+
8
+ module CbzTools
9
+ # Converts a CBR (Comic Book RAR) archive into a CBZ (Comic Book ZIP)
10
+ # archive.
11
+ #
12
+ # The CBR extraction step is delegated to an injectable +extractor+
13
+ # — anything that responds to +call(cbr_path, destination_dir)+ and
14
+ # raises {CbzTools::CbrConverter::ExtractionError} on failure. Callers (not this gem) own the
15
+ # extractor; this class is deliberately agnostic about which binary,
16
+ # library, or pure-Ruby implementation is used to expand the source.
17
+ #
18
+ # @example Single-shot conversion with an explicit extractor
19
+ # extractor = MyApp::UnrarExtractor.new
20
+ # CbzTools::CbrConverter.convert("path/to/comic.cbr",
21
+ # to: "path/to/comic.cbz",
22
+ # extractor: extractor)
23
+ #
24
+ # @example Reusing one extractor across many conversions
25
+ # extractor = MyApp::UnrarExtractor.new
26
+ # converter = CbzTools::CbrConverter.new(extractor: extractor)
27
+ # paths.each { |p| converter.convert(p, to: p.sub(/\.cbr\z/i, ".cbz")) }
28
+ class CbrConverter
29
+ # Raised when the underlying extractor fails to expand the source
30
+ # CBR into a directory. Propagated untouched so callers can rescue
31
+ # it without unwrapping.
32
+ class ExtractionError < Error
33
+ end
34
+
35
+ # Raised when the resulting CBZ cannot be written, when the
36
+ # destination already exists and +overwrite:+ is false, or when no
37
+ # +extractor:+ was supplied. Propagated untouched from the
38
+ # relevant step.
39
+ class ArchiveError < Error
40
+ end
41
+
42
+ # @return [#call, nil] the configured extractor, or +nil+ if none
43
+ # was supplied. Read-only; there is no corresponding writer.
44
+ attr_reader :extractor
45
+
46
+ # @see #convert
47
+ #
48
+ # @param cbr_path [String] source CBR path
49
+ # @param to [String] destination CBZ path
50
+ # @param overwrite [Boolean] when false, raise {ArchiveError} if
51
+ # +to+ already exists; default true
52
+ # @param work_in [String, nil] work directory; when nil (default),
53
+ # a fresh +Dir.mktmpdir+ is created and cleaned up. When set, the
54
+ # converter uses that directory and leaves cleanup to the caller
55
+ # (useful when batching many conversions).
56
+ # @param extractor [#call] extractor backend; required.
57
+ # @return [String] the destination CBZ path
58
+ # @raise [ArchiveError] when +extractor:+ is missing or target cannot be written
59
+ # @raise [ExtractionError] when the extractor fails
60
+ def self.convert(cbr_path, to:, overwrite: true, work_in: nil, extractor: nil)
61
+ new(extractor: extractor).convert(cbr_path, to: to, overwrite: overwrite, work_in: work_in)
62
+ end
63
+
64
+ # @param extractor [#call, nil] anything responding to
65
+ # +call(cbr_path, destination_dir)+ that raises {ExtractionError}
66
+ # on failure. Required — {#convert} raises {ArchiveError} if left
67
+ # as +nil+.
68
+ def initialize(extractor: nil)
69
+ @extractor = extractor
70
+ end
71
+
72
+ # @see .convert
73
+ #
74
+ # @param cbr_path [String] source CBR path
75
+ # @param to [String] destination CBZ path
76
+ # @param overwrite [Boolean] when false, raise {ArchiveError} if
77
+ # +to+ already exists; default true
78
+ # @param work_in [String, nil] see {.convert}
79
+ # @return [String] the destination CBZ path
80
+ # @raise [ArchiveError] when no extractor was configured or target cannot be written
81
+ # @raise [ExtractionError] when the extractor fails
82
+ def convert(cbr_path, to:, overwrite: true, work_in: nil)
83
+ ensure_extractor!
84
+ ensure_target_slot!(to, overwrite: overwrite)
85
+ File.delete(to) if overwrite && File.exist?(to)
86
+
87
+ in_workdir(work_in) do |temp_dir|
88
+ @extractor.call(cbr_path, temp_dir)
89
+ repack(temp_dir, to)
90
+ end
91
+
92
+ to
93
+ end
94
+
95
+ private
96
+
97
+ def ensure_extractor!
98
+ raise ArchiveError,
99
+ "No extractor configured; pass `extractor:` to CbrConverter.convert or CbrConverter.new" unless @extractor
100
+ end
101
+
102
+ def ensure_target_slot!(to, overwrite:)
103
+ return if overwrite || !File.exist?(to)
104
+
105
+ raise ArchiveError, "Target #{to} already exists (pass overwrite: true to replace)"
106
+ end
107
+
108
+ def in_workdir(work_in, &block)
109
+ if work_in
110
+ FileUtils.mkdir_p(work_in)
111
+ yield work_in
112
+ else
113
+ Dir.mktmpdir(&block)
114
+ end
115
+ end
116
+
117
+ def repack(source_dir, target_path)
118
+ Zip::File.open(target_path, create: true) do |zipfile|
119
+ each_file_in(source_dir) { |file| add_to_zip(zipfile, source_dir, file) }
120
+ end
121
+ rescue Zip::Error, SystemCallError => e
122
+ raise ArchiveError, "Failed to write CBZ #{target_path}: #{e.class}: #{e.message}"
123
+ end
124
+
125
+ def each_file_in(source_dir)
126
+ Dir["#{source_dir}/**/*"].each do |path|
127
+ yield path unless File.directory?(path)
128
+ end
129
+ end
130
+
131
+ def add_to_zip(zipfile, source_dir, file_path)
132
+ relative = Pathname.new(file_path).relative_path_from(Pathname.new(source_dir)).to_s
133
+ zipfile.add(relative, file_path)
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module CbzTools
6
+ # Reads and writes the ComicInfo.xml metadata embedded in CBZ archives.
7
+ #
8
+ # The two operations are symmetric over SCHEMA: every key on the left
9
+ # side of that hash is a field that {.parse} can read and {.build} can
10
+ # write. Extend the schema in place (for example, in an initializer)
11
+ # if you need to support custom fields.
12
+ module ComicInfo
13
+ class ParseError < Error
14
+ end
15
+
16
+ SCHEMA = {
17
+ title: "Title",
18
+ series: "Series",
19
+ series_sort: "SeriesSort",
20
+ localized_series: "LocalizedSeries",
21
+ number: "Number",
22
+ count: "Count",
23
+ volume: "Volume",
24
+ summary: "Summary",
25
+ notes: "Notes",
26
+ publisher: "Publisher",
27
+ imprint: "Imprint",
28
+ year: "Year",
29
+ month: "Month",
30
+ day: "Day",
31
+ writer: "Writer",
32
+ penciller: "Penciller",
33
+ inker: "Inker",
34
+ colorist: "Colorist",
35
+ letterer: "Letterer",
36
+ cover_artist: "CoverArtist",
37
+ editor: "Editor",
38
+ translator: "Translator",
39
+ genre: "Genre",
40
+ tags: "Tags",
41
+ web: "Web",
42
+ page_count: "PageCount",
43
+ language_iso: "LanguageISO",
44
+ format: "Format",
45
+ series_group: "SeriesGroup",
46
+ age_rating: "AgeRating",
47
+ community_rating: "CommunityRating",
48
+ black_and_white: "BlackAndWhite",
49
+ manga: "Manga",
50
+ characters: "Characters",
51
+ teams: "Teams",
52
+ locations: "Locations",
53
+ scan_information: "ScanInformation",
54
+ story_arc: "StoryArc",
55
+ gtin: "GTIN"
56
+ }
57
+
58
+ # Parses a ComicInfo.xml string into a symbol-keyed hash.
59
+ #
60
+ # Returns an empty hash when the input is empty, or when the XML is
61
+ # valid but has no <ComicInfo> root. Keys not in SCHEMA are ignored.
62
+ # Fields whose text is empty or whitespace-only are dropped.
63
+ #
64
+ # @param xml [String] the ComicInfo.xml document
65
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
66
+ # @raise [ParseError] when the XML is malformed
67
+ def self.parse(xml)
68
+ doc = Nokogiri::XML(xml)
69
+ raise ParseError, "Malformed XML: #{doc.errors.first&.message}" if doc.errors.any?
70
+
71
+ root = doc.at("ComicInfo")
72
+ return {} unless root
73
+
74
+ SCHEMA.each_with_object({}) do |(key, tag), hash|
75
+ text = root.at(tag)&.text&.strip
76
+ hash[key] = text if text && !text.empty?
77
+ end
78
+ end
79
+
80
+ # Builds a ComicInfo.xml string from a metadata hash.
81
+ #
82
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored. Values
83
+ # that are +nil+, empty, or whitespace-only are skipped so the
84
+ # resulting XML does not contain empty tags.
85
+ #
86
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
87
+ # @return [String] a ComicInfo.xml document
88
+ def self.build(metadata)
89
+ normalized = metadata.transform_keys(&:to_sym)
90
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
91
+ xml.ComicInfo(
92
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
93
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
94
+ ) do
95
+ SCHEMA.each do |key, tag|
96
+ value = normalized[key]
97
+ next if value.nil? || value.to_s.strip.empty?
98
+
99
+ xml.send(tag, value.to_s)
100
+ end
101
+ end
102
+ end
103
+ builder.to_xml
104
+ end
105
+ end
106
+ end
@@ -2,12 +2,33 @@
2
2
 
3
3
  require "zip"
4
4
 
5
+ require_relative "comic_info"
6
+
5
7
  module CbzTools
8
+ # Reads the contents of a CBZ (Comic Book ZIP) archive.
9
+ #
10
+ # The public surface is image access and parsed ComicInfo metadata;
11
+ # XML is an internal wire format and never appears on the API.
12
+ #
13
+ # @example Reading images and metadata
14
+ # CbzTools::Reader.open("path/to/comic.cbz") do |reader|
15
+ # reader.image_names.first # => "page_001.jpg"
16
+ # reader.first_image # => "<image bytes>"
17
+ # reader.comic_info # => {title: "The Amazing Spider-Man", page_count: "32"}
18
+ # end
6
19
  class Reader
20
+ # File extensions recognized as comic page images.
7
21
  IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp].freeze
8
22
 
23
+ # Raised when {#image} is called with a name that does not match
24
+ # any entry in the archive.
9
25
  class ImageNotFoundError < Error; end
10
26
 
27
+ # Opens a CBZ archive, yields a reader, and closes it when the
28
+ # block exits — including on exception.
29
+ #
30
+ # @yieldparam reader [Reader] the open reader
31
+ # @return [void]
11
32
  def self.open(path, &block)
12
33
  reader = new(path)
13
34
  block.call(reader)
@@ -15,16 +36,26 @@ module CbzTools
15
36
  reader&.close
16
37
  end
17
38
 
39
+ # Opens the archive at the given path. Prefer {.open} unless you
40
+ # need to hold the reader open across multiple operations.
41
+ #
42
+ # @param path [String] path to a CBZ file
18
43
  def initialize(path)
19
44
  @path = path
20
45
  @zip = Zip::File.open(@path)
21
46
  end
22
47
 
48
+ # Releases the underlying zip handle. Safe to call more than once.
49
+ #
50
+ # @return [void]
23
51
  def close
24
52
  @zip&.close
25
53
  @zip = nil
26
54
  end
27
55
 
56
+ # Returns the names of all image entries in the archive, sorted.
57
+ #
58
+ # @return [Array<String>]
28
59
  def image_names
29
60
  @zip.entries
30
61
  .reject(&:directory?)
@@ -33,17 +64,19 @@ module CbzTools
33
64
  .sort
34
65
  end
35
66
 
36
- def read(name)
37
- entry = @zip.find_entry(name)
38
- return nil unless entry
39
-
40
- entry.get_input_stream.read
41
- end
42
-
67
+ # Returns the bytes of the first image in the archive, or +nil+
68
+ # when there are no images.
69
+ #
70
+ # @return [String, nil]
43
71
  def first_image
44
72
  read(image_names.first)
45
73
  end
46
74
 
75
+ # Returns the bytes of a specific image by name.
76
+ #
77
+ # @param name [String] entry name within the archive
78
+ # @return [String]
79
+ # @raise [ImageNotFoundError] when no entry with that name exists
47
80
  def image(name)
48
81
  data = read(name)
49
82
  raise ImageNotFoundError, "Image '#{name}' not found in #{@path}" unless data
@@ -51,12 +84,43 @@ module CbzTools
51
84
  data
52
85
  end
53
86
 
87
+ # Returns the parsed ComicInfo metadata as a symbol-keyed hash.
88
+ #
89
+ # Returns an empty hash when the archive has no ComicInfo.xml.
90
+ # The lookup is case-insensitive (+comicinfo.xml+ is also
91
+ # recognized).
92
+ #
93
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
94
+ # @raise [CbzTools::ComicInfo::ParseError] when ComicInfo.xml is malformed
54
95
  def comic_info
55
- read("ComicInfo.xml") || read("comicinfo.xml")
96
+ xml = read("ComicInfo.xml") || read("comicinfo.xml")
97
+ xml ? CbzTools::ComicInfo.parse(xml) : {}
98
+ end
99
+
100
+ # Writes the given metadata hash back into the archive as
101
+ # ComicInfo.xml, replacing any existing entry with that name.
102
+ #
103
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored.
104
+ #
105
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
106
+ # @return [void]
107
+ # @raise [Zip::Error] when the archive cannot be opened for writing
108
+ # @raise [SystemCallError] when an underlying I/O operation fails
109
+ def update_comic_info(metadata)
110
+ @zip.get_output_stream("ComicInfo.xml") do |f|
111
+ f.write(CbzTools::ComicInfo.build(metadata))
112
+ end
56
113
  end
57
114
 
58
115
  private
59
116
 
117
+ def read(name)
118
+ entry = @zip.find_entry(name)
119
+ return nil unless entry
120
+
121
+ entry.get_input_stream.read
122
+ end
123
+
60
124
  def image?(name)
61
125
  IMAGE_EXTENSIONS.any? { |ext| name.downcase.end_with?(ext) }
62
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CbzTools
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/cbz_tools.rb CHANGED
@@ -3,3 +3,5 @@
3
3
  require_relative "cbz_tools/version"
4
4
  require_relative "cbz_tools/error"
5
5
  require_relative "cbz_tools/reader"
6
+ require_relative "cbz_tools/comic_info"
7
+ require_relative "cbz_tools/cbr_converter"
data/sig/cbz_tools.rbs CHANGED
@@ -1,4 +1,85 @@
1
1
  module CbzTools
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class CbrConverter
8
+ class ExtractionError < CbzTools::Error
9
+ end
10
+
11
+ class ArchiveError < CbzTools::Error
12
+ end
13
+
14
+ attr_reader extractor: untyped
15
+
16
+ def self.convert: (
17
+ String cbr_path,
18
+ to: String,
19
+ ?overwrite: bool,
20
+ ?work_in: String?,
21
+ ?extractor: untyped
22
+ ) -> String
23
+
24
+ def initialize: (?extractor: untyped) -> void
25
+
26
+ def convert: (
27
+ String cbr_path,
28
+ to: String,
29
+ ?overwrite: bool,
30
+ ?work_in: String?
31
+ ) -> String
32
+
33
+ private
34
+
35
+ def ensure_extractor!: -> void
36
+
37
+ def ensure_target_slot!: (String to, overwrite: bool) -> void
38
+
39
+ def in_workdir: (String? work_in) { (String) -> void } -> void
40
+
41
+ def repack: (String source_dir, String target_path) -> void
42
+ end
43
+
44
+ class Reader
45
+ IMAGE_EXTENSIONS: Array[String]
46
+
47
+ class ImageNotFoundError < CbzTools::Error
48
+ end
49
+
50
+ def self.open: (String path) { (Reader reader) -> void } -> void
51
+
52
+ def initialize: (String path) -> void
53
+
54
+ def close: -> void
55
+
56
+ def image_names: -> Array[String]
57
+
58
+ def read: (String name) -> String?
59
+
60
+ def first_image: -> String?
61
+
62
+ def image: (String name) -> String
63
+
64
+ def comic_info: -> Hash[Symbol, String]
65
+
66
+ def update_comic_info: (Hash[Symbol | String, String] metadata) -> void
67
+
68
+ private
69
+
70
+ def read: (String name) -> String?
71
+
72
+ def image?: (String name) -> bool
73
+ end
74
+
75
+ module ComicInfo
76
+ SCHEMA: Hash[Symbol, String]
77
+
78
+ class ParseError < CbzTools::Error
79
+ end
80
+
81
+ def self.parse: (String xml) -> Hash[Symbol, String]
82
+
83
+ def self.build: (Hash[Symbol | String, String] metadata) -> String
84
+ end
4
85
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cbz_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Figueroa
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.15'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.15'
26
40
  description: 'Tools for working with CBZ comic book archives: reading and extracting
27
41
  their contents, reading and updating their embedded metadata, and converting CBR
28
42
  archives to CBZ.'
@@ -37,6 +51,8 @@ files:
37
51
  - LICENSE.txt
38
52
  - README.md
39
53
  - lib/cbz_tools.rb
54
+ - lib/cbz_tools/cbr_converter.rb
55
+ - lib/cbz_tools/comic_info.rb
40
56
  - lib/cbz_tools/error.rb
41
57
  - lib/cbz_tools/reader.rb
42
58
  - lib/cbz_tools/version.rb