cbz_tools 0.3.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +51 -1
- data/lib/cbz_tools/cbr_converter.rb +136 -0
- data/lib/cbz_tools/version.rb +1 -1
- data/lib/cbz_tools.rb +1 -0
- data/sig/cbz_tools.rbs +37 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96ee68c0b79bcb1256324ad692bf441f0926443ab71e87db3715c0ca5c89cac0
|
|
4
|
+
data.tar.gz: 15d6235b0d040d8836a752da8e304bb89d2fd0976f9eb371f6f514b6dd8c97c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfaee61a3965d6901835d58ca0206ee5bd5f4a05eb1ffb00d2248ca16755aeb1d418b54f3550fe9ef6d0d125aff393955aec459990a279259bc05d874e7d1adc
|
|
7
|
+
data.tar.gz: 7e2e12a38206a9a6ea43d49eb4474a60770d094c48d47746a3cb6f249496abf382af564caacd41e5426684077a23135018ef108795232c87e592a835740ef435
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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
|
+
|
|
3
7
|
## [0.3.0] - 2026-08-03
|
|
4
8
|
|
|
5
9
|
- Add `CbzTools::ComicInfo` (parse/build ComicInfo.xml, `SCHEMA`); raises `ParseError` on bad XML.
|
data/README.md
CHANGED
|
@@ -18,7 +18,57 @@ gem install cbz_tools
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
|
|
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
|
data/lib/cbz_tools/version.rb
CHANGED
data/lib/cbz_tools.rb
CHANGED
data/sig/cbz_tools.rbs
CHANGED
|
@@ -4,6 +4,43 @@ module CbzTools
|
|
|
4
4
|
class Error < StandardError
|
|
5
5
|
end
|
|
6
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
|
+
|
|
7
44
|
class Reader
|
|
8
45
|
IMAGE_EXTENSIONS: Array[String]
|
|
9
46
|
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Figueroa
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- LICENSE.txt
|
|
52
52
|
- README.md
|
|
53
53
|
- lib/cbz_tools.rb
|
|
54
|
+
- lib/cbz_tools/cbr_converter.rb
|
|
54
55
|
- lib/cbz_tools/comic_info.rb
|
|
55
56
|
- lib/cbz_tools/error.rb
|
|
56
57
|
- lib/cbz_tools/reader.rb
|