comicbook 0.1.0 → 0.2.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: a990cb3f4650a5a8efb0de5f872f7242c6a3e9f7028f137022d003f92f401f4b
4
- data.tar.gz: b10b662cfd46bbe3126b4c834b2e5c057a227d18bed5d0a97d4bc899af843192
3
+ metadata.gz: a0e1f16bc54c8073078765c5edd09bf2e886ee042a859e6f6ec7c601876fc010
4
+ data.tar.gz: c7fbaa136dc9705ef955dd272c83a22864fc541200b9b435a34c850afedea34b
5
5
  SHA512:
6
- metadata.gz: 4d7e88350b4c8f9c111d68579509bdc90f8dbee09abf326a5255b9f2c4065255c1d6d863ff57df3cb42279565aee09ed67873bce779f3d734537b57c03745330
7
- data.tar.gz: '0059293bd6b56943810b2fd5bb7ed730dfc1ad214371a5467e2ab73cd7fd8604b4ca66b822ca74051beb10187c15d193d589f58b6bfa02e7ed0d64d81cb3571c'
6
+ metadata.gz: 3ad1f9e9e8d24c143e6dfb22a293e62d758ce6a865e19f7bf324e908e339f7557d303e873d0cc5448378e190214ce9e8c18bed3743480203fa708e9f313fb3a9
7
+ data.tar.gz: 8f7e8f708e4ff746e132d94a6a6fec8406196cabe246c2769ae1600684a2dfb7c2fe4d7dab32cd5f9c70e22967e35d942b2a649cc1784bea2734702c04036ac6
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.7
1
+ 4.0.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-01-17
4
+
5
+ ### Added
6
+
7
+ - CBR extraction support using vendored `unar`/`lsar` binaries
8
+ - `--delete-original` CLI option for both extract and archive commands
9
+ - `--images-only` CLI option for extract command
10
+ - `images_only` option for Ruby API extraction
11
+ - `delete_original` option for Ruby API extraction and archiving
12
+ - CLI validation for unsupported archive output formats (CBR, CBA)
13
+
3
14
  ## [0.1.0] - 2025-10-26
4
15
 
16
+ ### Added
17
+
5
18
  - Initial release
19
+ - CBZ support (extract and archive)
20
+ - CB7 support (extract and archive)
21
+ - CBT support (extract and archive)
22
+ - CLI tool with `extract` and `archive` commands
23
+ - Ruby API with `ComicBook.extract`, `ComicBook.new().archive`, and `ComicBook.new().pages`
data/README.md CHANGED
@@ -7,14 +7,15 @@ A Ruby library and CLI tool for managing comic books archives.
7
7
 
8
8
  **`archive`** — to create a `.cb*` file (default: `.cbz`).
9
9
 
10
- Currently supported formats, `archive` and `extract`:
10
+ Currently supported formats for `archive` and `extract`:
11
11
  - CB7 — [7zip](https://en.wikipedia.org/wiki/7-Zip)
12
12
  - CBT — [Tar](https://en.wikipedia.org/wiki/Tar_(computing))
13
13
  - CBZ — [Zip](https://en.wikipedia.org/wiki/ZIP_(file_format))
14
14
 
15
- Planned formats , only `extract`:
15
+ Currently supported formats for `extract` only:
16
+ - **CBR** — [RAR](https://en.wikipedia.org/wiki/WinRAR) is proprietary without an open source implementation license. Extracting support is provided using vendored [`unar`](https://theunarchiver.com/command-line) binaries because a large number of comic books are archived in .cbr/.rar format. No support for creating `.cbr` files will ever be added until RAR is open source (or reverse engineered).
16
17
 
17
- - **CBR** [RAR](https://en.wikipedia.org/wiki/WinRAR) is proprietary without an open source implementation license. People use WinRAR (Windows-only) to create .rar files. Or `unrar` on Linux/macOS to open .rar files. Extracting support is provided because a large number of comic books are archived in .cbr/.rar format, primarily by Windows users. No support for creating `.cbr` files will ever be added until RAR is opensource (or reverse engineered).
18
+ Planned formats for `extract` only:
18
19
  - **CBA** — [ACE](https://en.wikipedia.org/wiki/WinAce) is both proprietary and very old/outdated/unsupported. ACE extracting support is provided for historical posterity and completeness.
19
20
 
20
21
  ## Installation
@@ -33,17 +34,49 @@ gem install comicbook
33
34
 
34
35
  ## Usage
35
36
 
36
- In Ruby, you can use the `ComicBook` class to `extract` comic books archives from various formats. You `archive` a folder of images to create a comic book archive.
37
+ ### CLI
37
38
 
38
- ### Extracting
39
+ ```sh
40
+ # Extract a comic book archive
41
+ comicbook extract path/to/archive.cbz
39
42
 
40
- ```ruby
41
- ComicBook.extract 'path/to/archive.cbz'
43
+ # Extract to a specific destination
44
+ comicbook extract path/to/archive.cbz --to path/to/output
45
+
46
+ # Extract only image files (exclude metadata like ComicInfo.xml)
47
+ comicbook extract path/to/archive.cbz --images-only
48
+
49
+ # Create a comic book archive from a folder
50
+ comicbook archive path/to/folder
51
+
52
+ # Create archive at a specific destination
53
+ comicbook archive path/to/folder --to path/to/output.cbz
54
+
55
+ # Show help
56
+ comicbook --help
42
57
  ```
43
- ### Archiving
58
+
59
+ ### Ruby API
44
60
 
45
61
  ```ruby
46
- ComicBook.archive 'path/to/archive'
62
+ # Extract a comic book archive (extracts all files by default)
63
+ ComicBook.extract 'path/to/archive.cbz'
64
+
65
+ # Extract to a specific destination
66
+ ComicBook.extract 'path/to/archive.cbz', to: 'path/to/output'
67
+
68
+ # Extract only image files (exclude metadata like ComicInfo.xml)
69
+ ComicBook.extract 'path/to/archive.cbz', images_only: true
70
+
71
+ # Create a comic book archive from a folder (creates .cbz by default)
72
+ ComicBook.new('path/to/folder').archive
73
+
74
+ # Create archive at a specific destination
75
+ ComicBook.new('path/to/folder').archive to: 'path/to/output.cbz'
76
+
77
+ # Get pages from an archive
78
+ comic = ComicBook.new 'path/to/archive.cbz'
79
+ comic.pages # => [#<ComicBook::Page>, ...]
47
80
  ```
48
81
 
49
82
  ## Development
data/Rakefile CHANGED
@@ -5,4 +5,8 @@ require 'rubocop/rake_task'
5
5
  RSpec::Core::RakeTask.new :spec
6
6
  RuboCop::RakeTask.new
7
7
 
8
+ desc 'Run tests and linter'
8
9
  task default: %i[spec rubocop]
10
+
11
+ desc 'Alias of default: spec linter'
12
+ task test: %i[spec rubocop]
data/SYSTEM.md CHANGED
@@ -230,6 +230,6 @@ ComicBook::Error < StandardError
230
230
  The adapter pattern makes it easy to add new formats or extend existing ones without modifying core functionality.
231
231
 
232
232
  ## Version Information
233
- - **Current Version**: 0.1.0
234
- - **Ruby Requirement**: >= 3.4.7
233
+ - **Current Version**: 0.2.0
234
+ - **Ruby Requirement**: >= 4.0.0
235
235
  - **License**: MIT
@@ -0,0 +1,49 @@
1
+ class ComicBook
2
+ class CB < Adapter
3
+ class Archiver
4
+ def initialize source_path
5
+ @source_path = File.expand_path source_path
6
+ end
7
+
8
+ def archive options = {}
9
+ output_path = options[:to] || determine_output_path
10
+
11
+ validate_destination! output_path
12
+
13
+ if File.directory? source_path
14
+ archive_folder output_path
15
+ else
16
+ archive_file output_path
17
+ end
18
+
19
+ output_path
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :source_path
25
+
26
+ def determine_output_path
27
+ base_name = File.basename source_path, '.*'
28
+ dir_name = File.dirname source_path
29
+
30
+ File.expand_path File.join(dir_name, "#{base_name}.cb")
31
+ end
32
+
33
+ def validate_destination! output_path
34
+ return unless File.exist? output_path
35
+
36
+ raise ComicBook::Error, "Destination already exists: #{output_path}"
37
+ end
38
+
39
+ def archive_folder output_path
40
+ FileUtils.mv source_path, output_path
41
+ end
42
+
43
+ def archive_file output_path
44
+ FileUtils.mkdir_p output_path
45
+ FileUtils.mv source_path, output_path
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ class ComicBook
2
+ class CB < Adapter
3
+ class Extractor
4
+ def initialize archive_path
5
+ @archive_path = File.expand_path archive_path
6
+ end
7
+
8
+ def extract
9
+ raise ComicBook::Error, '.cb folders are already extracted (they are uncompressed folders)'
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :archive_path
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'adapter'
2
+ require_relative 'cb/archiver'
3
+ require_relative 'cb/extractor'
4
+
5
+ class ComicBook
6
+ class CB < Adapter
7
+ def archive options = {}
8
+ Archiver.new(path).archive options
9
+ end
10
+
11
+ def extract _options = {}
12
+ Extractor.new(path).extract
13
+ end
14
+
15
+ def pages
16
+ pattern = ComicBook::IMAGE_GLOB_PATTERN
17
+ search_path = File.join path, '**', pattern
18
+ image_files = Dir.glob search_path, File::FNM_CASEFOLD
19
+
20
+ image_files.sort.map do |file|
21
+ relative_path = Pathname.new(file).relative_path_from(Pathname.new(path)).to_s
22
+ basename = File.basename file
23
+
24
+ ComicBook::Page.new relative_path, basename
25
+ end
26
+ end
27
+ end
28
+ end
@@ -10,7 +10,7 @@ class ComicBook
10
10
  delete_original = options.fetch :delete_original, false
11
11
 
12
12
  output_path = options[:to] || determine_output_path(extension)
13
- create_7z_archive output_path
13
+ create_archive output_path
14
14
  cleanup_source_folder if delete_original
15
15
 
16
16
  output_path
@@ -27,11 +27,11 @@ class ComicBook
27
27
  File.expand_path File.join(dir_name, "#{base_name}.#{extension}")
28
28
  end
29
29
 
30
- def create_7z_archive output_path
30
+ def create_archive output_path
31
31
  File.open(output_path, 'wb') do |file|
32
- SevenZipRuby::Writer.open(file) do |szw|
32
+ SevenZipRuby::Writer.open(file) do |writer|
33
33
  find_image_files.each do |image_file|
34
- add_file_to_7z szw, image_file
34
+ add_file writer, image_file
35
35
  end
36
36
  end
37
37
  end
@@ -42,12 +42,12 @@ class ComicBook
42
42
  Dir.glob(pattern, File::FNM_CASEFOLD).sort
43
43
  end
44
44
 
45
- def add_file_to_7z szw, file
45
+ def add_file writer, file
46
46
  file_path = Pathname.new file
47
47
  source_path = Pathname.new source_folder
48
48
  relative_path = file_path.relative_path_from source_path
49
49
 
50
- szw.add_file file, as: relative_path.to_s
50
+ writer.add_file file, as: relative_path.to_s
51
51
  end
52
52
 
53
53
  def cleanup_source_folder
@@ -2,16 +2,17 @@ class ComicBook
2
2
  class CB7 < Adapter
3
3
  class Extractor
4
4
  def initialize archive_path
5
- @archive_path = File.expand_path(archive_path)
5
+ @archive_path = File.expand_path archive_path
6
6
  end
7
7
 
8
8
  def extract options = {}
9
- extension = options.fetch :extension, :cb
10
- delete_original = options.fetch :delete_original, false
9
+ extension = options.fetch :extension, :cb
10
+ delete_original = options.fetch :delete_original, false
11
11
  destination_folder = options[:to]
12
12
 
13
13
  destination = destination_folder || determine_extract_path(extension)
14
- extract_7z_contents destination
14
+ create_destination_directory destination
15
+ extract_contents destination, options
15
16
  cleanup_archive_file if delete_original
16
17
 
17
18
  destination
@@ -21,9 +22,13 @@ class ComicBook
21
22
 
22
23
  attr_reader :archive_path
23
24
 
25
+ def create_destination_directory destination
26
+ FileUtils.mkdir_p destination
27
+ end
28
+
24
29
  def determine_extract_path extension
25
30
  base_name = File.basename archive_path, '.*'
26
- dir_name = File.dirname archive_path
31
+ dir_name = File.dirname archive_path
27
32
  archive_name = base_name
28
33
 
29
34
  archive_name << ".#{extension}" if extension
@@ -32,33 +37,43 @@ class ComicBook
32
37
  File.expand_path full_path
33
38
  end
34
39
 
35
- def extract_7z_contents destination
40
+ def image_file? filename
41
+ ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
42
+ end
43
+
44
+ def cleanup_archive_file
45
+ File.delete archive_path
46
+ end
47
+
48
+ def create_parent_directory file_path
49
+ parent_dir = File.dirname file_path
50
+ FileUtils.mkdir_p parent_dir
51
+ end
52
+
53
+ def extract_contents destination, options
36
54
  FileUtils.mkdir_p destination
37
55
 
38
56
  File.open(archive_path, 'rb') do |file|
39
- SevenZipRuby::Reader.open(file) do |szr|
40
- szr.entries.each do |entry|
41
- next unless entry.file? && image_file?(entry.path)
42
-
43
- extract_single_file entry, destination, szr
44
- end
57
+ SevenZipRuby::Reader.open(file) do |seven_zip_reader|
58
+ extract_files destination, options, seven_zip_reader
45
59
  end
46
60
  end
47
61
  end
48
62
 
49
- def extract_single_file entry, destination, szr
50
- file_path = File.join(destination, entry.path)
51
- FileUtils.mkdir_p File.dirname(file_path)
63
+ def extract_files destination, options, seven_zip_reader
64
+ seven_zip_reader.entries.each do |entry|
65
+ next unless entry.file?
66
+ next if options[:images_only] && !image_file?(entry.path)
52
67
 
53
- File.binwrite(file_path, szr.extract_data(entry))
68
+ extract_single_file entry, destination, seven_zip_reader
69
+ end
54
70
  end
55
71
 
56
- def cleanup_archive_file
57
- File.delete archive_path
58
- end
72
+ def extract_single_file entry, destination, seven_zip_reader
73
+ file_path = File.join destination, entry.path
74
+ create_parent_directory file_path
59
75
 
60
- def image_file? filename
61
- ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
76
+ File.binwrite(file_path, seven_zip_reader.extract_data(entry))
62
77
  end
63
78
  end
64
79
  end
@@ -13,34 +13,32 @@ class ComicBook
13
13
  Extractor.new(path).extract options
14
14
  end
15
15
 
16
- def pages = collect_pages_from_7z
17
-
18
- private
19
-
20
- def collect_pages_from_7z
21
- pages = []
16
+ def pages
17
+ entries = []
22
18
 
23
19
  File.open(path, 'rb') do |file|
24
- SevenZipRuby::Reader.open(file) do |szr|
25
- szr.entries.each do |entry|
26
- next unless entry.file? && image_file?(entry.path)
27
-
28
- pages << create_page_from_entry(entry)
20
+ SevenZipRuby::Reader.open(file) do |reader|
21
+ reader.entries.each do |entry|
22
+ entries << entry.path if entry.file?
29
23
  end
30
24
  end
31
25
  end
32
26
 
33
- pages.sort_by(&:name)
27
+ entries.select { image_file? it }
28
+ .map { create_page_from_entry it }
29
+ .sort_by(&:name)
34
30
  end
35
31
 
32
+ private
33
+
36
34
  def create_page_from_entry entry
37
- basename = File.basename(entry.path)
35
+ basename = File.basename entry
38
36
 
39
- ComicBook::Page.new entry.path, basename
37
+ ComicBook::Page.new entry, basename
40
38
  end
41
39
 
42
40
  def image_file? filename
43
- extension = File.extname(filename.downcase)
41
+ extension = File.extname filename.downcase
44
42
 
45
43
  ComicBook::IMAGE_EXTENSIONS.include? extension
46
44
  end
@@ -0,0 +1,17 @@
1
+ require_relative 'adapter'
2
+
3
+ class ComicBook
4
+ class CBA < Adapter
5
+ def archive _options = {}
6
+ raise Error, 'CBA archiving not supported (ACE is proprietary)'
7
+ end
8
+
9
+ def extract _options = {}
10
+ raise Error, 'CBA extraction not yet implemented'
11
+ end
12
+
13
+ def pages
14
+ raise Error, 'CBA page listing not yet implemented'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,75 @@
1
+ class ComicBook
2
+ class CBR < Adapter
3
+ class Extractor
4
+ def initialize archive_path
5
+ @archive_path = File.expand_path archive_path
6
+ end
7
+
8
+ def extract options = {}
9
+ extension = options.fetch :extension, :cb
10
+ delete_original = options.fetch :delete_original, false
11
+ destination_folder = options[:to]
12
+
13
+ destination = destination_folder || determine_extract_path(extension)
14
+ create_destination_directory destination
15
+ extract_contents destination, options
16
+ cleanup_archive_file if delete_original
17
+
18
+ destination
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :archive_path
24
+
25
+ def create_destination_directory destination
26
+ FileUtils.mkdir_p destination
27
+ end
28
+
29
+ def determine_extract_path extension
30
+ base_name = File.basename archive_path, '.*'
31
+ dir_name = File.dirname archive_path
32
+ archive_name = base_name
33
+
34
+ archive_name << ".#{extension}" if extension
35
+
36
+ full_path = File.join dir_name, archive_name
37
+ File.expand_path full_path
38
+ end
39
+
40
+ def image_file? filename
41
+ ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
42
+ end
43
+
44
+ def cleanup_archive_file
45
+ File.delete archive_path
46
+ end
47
+
48
+ def create_parent_directory file_path
49
+ parent_dir = File.dirname file_path
50
+ FileUtils.mkdir_p parent_dir
51
+ end
52
+
53
+ def extract_contents destination, options
54
+ FileUtils.mkdir_p destination
55
+ extract_files destination, options
56
+ end
57
+
58
+ def extract_files destination, options
59
+ CLIHelpers.unar_extract archive_path, destination
60
+ delete_non_images destination if options[:images_only]
61
+ end
62
+
63
+ def delete_non_images destination
64
+ archive_entries = CLIHelpers.lsar_list archive_path
65
+
66
+ archive_entries.each do |entry|
67
+ next if image_file? entry
68
+
69
+ file_path = File.join(destination, entry)
70
+ FileUtils.rm_f file_path
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,36 @@
1
+ require 'shellwords'
2
+ require_relative 'adapter'
3
+ require_relative 'cbr/extractor'
4
+
5
+ class ComicBook
6
+ class CBR < Adapter
7
+ def archive _options = {}
8
+ raise Error, 'CBR archiving not supported (RAR is proprietary)'
9
+ end
10
+
11
+ def extract options = {}
12
+ Extractor.new(path).extract options
13
+ end
14
+
15
+ def pages
16
+ CLIHelpers.lsar_list(path)
17
+ .select { image_file? it }
18
+ .map { create_page_from_entry it }
19
+ .sort_by(&:name)
20
+ end
21
+
22
+ private
23
+
24
+ def create_page_from_entry entry
25
+ basename = File.basename entry
26
+
27
+ ComicBook::Page.new entry, basename
28
+ end
29
+
30
+ def image_file? filename
31
+ extension = File.extname filename.downcase
32
+
33
+ ComicBook::IMAGE_EXTENSIONS.include? extension
34
+ end
35
+ end
36
+ end
@@ -6,14 +6,14 @@ class ComicBook
6
6
  end
7
7
 
8
8
  def archive options = {}
9
- extension = options.fetch :extension, :cbt
10
- destination = options[:to] || determine_output_path(extension)
9
+ extension = options.fetch :extension, :cbt
11
10
  delete_original = options.fetch :delete_original, false
12
11
 
13
- create_tar_file destination
12
+ output_path = options[:to] || determine_output_path(extension)
13
+ create_archive output_path
14
14
  cleanup_source_folder if delete_original
15
15
 
16
- destination
16
+ output_path
17
17
  end
18
18
 
19
19
  private
@@ -27,43 +27,34 @@ class ComicBook
27
27
  File.expand_path File.join(dir_name, "#{base_name}.#{extension}")
28
28
  end
29
29
 
30
- def create_tar_file destination
31
- File.open(destination, 'wb') do |file|
32
- Gem::Package::TarWriter.new(file) do |tar|
33
- add_files_to_tar tar, source_folder
30
+ def create_archive output_path
31
+ File.open(output_path, 'wb') do |file|
32
+ Gem::Package::TarWriter.new(file) do |writer|
33
+ find_image_files.each do |image_file|
34
+ add_file writer, image_file
35
+ end
34
36
  end
35
37
  end
36
38
  end
37
39
 
38
- def add_files_to_tar tar, folder, prefix = ''
39
- Dir.entries(folder).sort.each do |entry|
40
- next if ['.', '..'].include?(entry)
41
-
42
- full_path = File.join(folder, entry)
43
- tar_path = prefix.empty? ? entry : File.join(prefix, entry)
44
-
45
- if File.directory?(full_path)
46
- add_files_to_tar tar, full_path, tar_path
47
- elsif image_file?(entry)
48
- add_file_to_tar tar, full_path, tar_path
49
- end
50
- end
40
+ def find_image_files
41
+ pattern = File.join(source_folder, '**', ComicBook::IMAGE_GLOB_PATTERN)
42
+ Dir.glob(pattern, File::FNM_CASEFOLD).sort
51
43
  end
52
44
 
53
- def add_file_to_tar tar, file_path, tar_path
54
- stat = File.stat(file_path)
55
- tar.add_file(tar_path, stat.mode) do |io|
56
- File.open(file_path, 'rb') do |file|
57
- io.write(file.read)
45
+ def add_file writer, file
46
+ file_path = Pathname.new file
47
+ source_path = Pathname.new source_folder
48
+ relative_path = file_path.relative_path_from source_path
49
+
50
+ stat = File.stat file
51
+ writer.add_file(relative_path.to_s, stat.mode) do |io|
52
+ File.open(file, 'rb') do |f|
53
+ io.write f.read
58
54
  end
59
55
  end
60
56
  end
61
57
 
62
- def image_file? filename
63
- extension = File.extname(filename.downcase)
64
- ComicBook::IMAGE_EXTENSIONS.include? extension
65
- end
66
-
67
58
  def cleanup_source_folder
68
59
  FileUtils.rm_rf source_folder
69
60
  end