cbz_tools 0.2.0 → 0.3.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/.vscode/settings.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/cbz_tools/comic_info.rb +106 -0
- data/lib/cbz_tools/reader.rb +72 -8
- data/lib/cbz_tools/version.rb +1 -1
- data/lib/cbz_tools.rb +1 -0
- data/sig/cbz_tools.rbs +45 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34014f96dd63882bd66f0de3f3a3548faa507e279d37f335fc12ecf8cef3ab41
|
|
4
|
+
data.tar.gz: f54439b98ddad12995b30e74ca1d6e45f559acc5c7df0190af5610623e1911e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37bc01a7b250c5382507314c57d6c3c208f122c8fd39802ce5308aec16baa07860530e120f37cb5ce69bbf5dcdcc0a145e7a3ce0ad81fc2276ba4ae0e461c835
|
|
7
|
+
data.tar.gz: 9b83a50b99751749896e4d395af34665d85004a49768b7d7b9dac9d578bf4d90fd58190e87c18415620ae3935def84893a61ed40c7dc93ccc682ca31cd819fd5
|
data/.vscode/settings.json
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-08-03
|
|
4
|
+
|
|
5
|
+
- Add `CbzTools::ComicInfo` (parse/build ComicInfo.xml, `SCHEMA`); raises `ParseError` on bad XML.
|
|
6
|
+
- **Breaking**: `Reader#comic_info` returns parsed hash (`{}` when absent), not raw XML.
|
|
7
|
+
- Add `Reader#update_comic_info(metadata)` to write `ComicInfo.xml` back.
|
|
8
|
+
- `Reader#read` is now private; use `first_image`, `image`, `comic_info`.
|
|
9
|
+
|
|
3
10
|
## [0.2.0] - 2026-08-03
|
|
4
11
|
|
|
5
12
|
- Add `CbzTools::Reader` for reading CBZ archives (images and `ComicInfo.xml`)
|
|
@@ -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
|
data/lib/cbz_tools/reader.rb
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
data/lib/cbz_tools/version.rb
CHANGED
data/lib/cbz_tools.rb
CHANGED
data/sig/cbz_tools.rbs
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
module CbzTools
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Reader
|
|
8
|
+
IMAGE_EXTENSIONS: Array[String]
|
|
9
|
+
|
|
10
|
+
class ImageNotFoundError < CbzTools::Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.open: (String path) { (Reader reader) -> void } -> void
|
|
14
|
+
|
|
15
|
+
def initialize: (String path) -> void
|
|
16
|
+
|
|
17
|
+
def close: -> void
|
|
18
|
+
|
|
19
|
+
def image_names: -> Array[String]
|
|
20
|
+
|
|
21
|
+
def read: (String name) -> String?
|
|
22
|
+
|
|
23
|
+
def first_image: -> String?
|
|
24
|
+
|
|
25
|
+
def image: (String name) -> String
|
|
26
|
+
|
|
27
|
+
def comic_info: -> Hash[Symbol, String]
|
|
28
|
+
|
|
29
|
+
def update_comic_info: (Hash[Symbol | String, String] metadata) -> void
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def read: (String name) -> String?
|
|
34
|
+
|
|
35
|
+
def image?: (String name) -> bool
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module ComicInfo
|
|
39
|
+
SCHEMA: Hash[Symbol, String]
|
|
40
|
+
|
|
41
|
+
class ParseError < CbzTools::Error
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.parse: (String xml) -> Hash[Symbol, String]
|
|
45
|
+
|
|
46
|
+
def self.build: (Hash[Symbol | String, String] metadata) -> String
|
|
47
|
+
end
|
|
4
48
|
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.
|
|
4
|
+
version: 0.3.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,7 @@ files:
|
|
|
37
51
|
- LICENSE.txt
|
|
38
52
|
- README.md
|
|
39
53
|
- lib/cbz_tools.rb
|
|
54
|
+
- lib/cbz_tools/comic_info.rb
|
|
40
55
|
- lib/cbz_tools/error.rb
|
|
41
56
|
- lib/cbz_tools/reader.rb
|
|
42
57
|
- lib/cbz_tools/version.rb
|