comicbook 0.1.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.
@@ -0,0 +1,65 @@
1
+ class ComicBook
2
+ class CB7 < 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
+ extract_7z_contents destination
15
+ cleanup_archive_file if delete_original
16
+
17
+ destination
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :archive_path
23
+
24
+ def determine_extract_path extension
25
+ base_name = File.basename archive_path, '.*'
26
+ dir_name = File.dirname archive_path
27
+ archive_name = base_name
28
+
29
+ archive_name << ".#{extension}" if extension
30
+
31
+ full_path = File.join dir_name, archive_name
32
+ File.expand_path full_path
33
+ end
34
+
35
+ def extract_7z_contents destination
36
+ FileUtils.mkdir_p destination
37
+
38
+ 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
45
+ end
46
+ end
47
+ end
48
+
49
+ def extract_single_file entry, destination, szr
50
+ file_path = File.join(destination, entry.path)
51
+ FileUtils.mkdir_p File.dirname(file_path)
52
+
53
+ File.binwrite(file_path, szr.extract_data(entry))
54
+ end
55
+
56
+ def cleanup_archive_file
57
+ File.delete archive_path
58
+ end
59
+
60
+ def image_file? filename
61
+ ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ require 'seven_zip_ruby'
2
+ require_relative 'adapter'
3
+ require_relative 'cb7/archiver'
4
+ require_relative 'cb7/extractor'
5
+
6
+ class ComicBook
7
+ class CB7 < Adapter
8
+ def archive options = {}
9
+ Archiver.new(path).archive options
10
+ end
11
+
12
+ def extract options = {}
13
+ Extractor.new(path).extract options
14
+ end
15
+
16
+ def pages = collect_pages_from_7z
17
+
18
+ private
19
+
20
+ def collect_pages_from_7z
21
+ pages = []
22
+
23
+ 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)
29
+ end
30
+ end
31
+ end
32
+
33
+ pages.sort_by(&:name)
34
+ end
35
+
36
+ def create_page_from_entry entry
37
+ basename = File.basename(entry.path)
38
+
39
+ ComicBook::Page.new entry.path, basename
40
+ end
41
+
42
+ def image_file? filename
43
+ extension = File.extname(filename.downcase)
44
+
45
+ ComicBook::IMAGE_EXTENSIONS.include? extension
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ class ComicBook
2
+ class CBT < Adapter
3
+ class Archiver
4
+ def initialize source_folder
5
+ @source_folder = File.expand_path source_folder
6
+ end
7
+
8
+ def archive options = {}
9
+ extension = options.fetch :extension, :cbt
10
+ destination = options[:to] || determine_output_path(extension)
11
+ delete_original = options.fetch :delete_original, false
12
+
13
+ create_tar_file destination
14
+ cleanup_source_folder if delete_original
15
+
16
+ destination
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :source_folder
22
+
23
+ def determine_output_path extension
24
+ base_name = File.basename source_folder, '.*'
25
+ dir_name = File.dirname source_folder
26
+
27
+ File.expand_path File.join(dir_name, "#{base_name}.#{extension}")
28
+ end
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
34
+ end
35
+ end
36
+ end
37
+
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
51
+ end
52
+
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)
58
+ end
59
+ end
60
+ end
61
+
62
+ def image_file? filename
63
+ extension = File.extname(filename.downcase)
64
+ ComicBook::IMAGE_EXTENSIONS.include? extension
65
+ end
66
+
67
+ def cleanup_source_folder
68
+ FileUtils.rm_rf source_folder
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,81 @@
1
+ require 'rubygems/package'
2
+ require 'fileutils'
3
+
4
+ class ComicBook
5
+ class CBT < Adapter
6
+ class Extractor
7
+ def initialize path
8
+ @path = File.expand_path path
9
+ end
10
+
11
+ def extract options = {}
12
+ extension = options.fetch :extension, :cb
13
+ delete_original = options.fetch :delete_original, false
14
+ destination_folder = options[:to]
15
+
16
+ destination = destination_folder || determine_extract_path(extension)
17
+ create_destination_directory destination
18
+ extract_files destination, options
19
+ cleanup_original_archive if delete_original
20
+
21
+ destination
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :path
27
+
28
+ def determine_extract_path extension
29
+ base_name = File.basename path, '.*'
30
+ dir_name = File.dirname path
31
+ archive_name = base_name
32
+
33
+ if extension
34
+ extension_str = extension.to_s
35
+ extension_str = extension_str[1..] if extension_str.start_with?('.')
36
+ archive_name << ".#{extension_str}"
37
+ end
38
+
39
+ full_path = File.join dir_name, archive_name
40
+ File.expand_path full_path
41
+ end
42
+
43
+ def create_destination_directory destination
44
+ FileUtils.mkdir_p destination
45
+ end
46
+
47
+ def extract_files destination, options
48
+ File.open(path, 'rb') do |file|
49
+ Gem::Package::TarReader.new(file) do |tar|
50
+ tar.each do |entry|
51
+ next unless entry.file?
52
+ next unless options[:all] || image_file?(entry.full_name)
53
+
54
+ extract_entry entry, destination
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def extract_entry entry, destination
61
+ output_path = File.join destination, entry.full_name
62
+ create_parent_directory output_path
63
+
64
+ File.binwrite(output_path, entry.read)
65
+ end
66
+
67
+ def create_parent_directory file_path
68
+ parent_dir = File.dirname file_path
69
+ FileUtils.mkdir_p parent_dir
70
+ end
71
+
72
+ def image_file? filename
73
+ ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
74
+ end
75
+
76
+ def cleanup_original_archive
77
+ FileUtils.rm path
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,48 @@
1
+ require 'rubygems/package'
2
+ require_relative 'adapter'
3
+ require_relative 'cbt/archiver'
4
+ require_relative 'cbt/extractor'
5
+
6
+ class ComicBook
7
+ class CBT < Adapter
8
+ def archive options = {}
9
+ Archiver.new(path).archive options
10
+ end
11
+
12
+ def extract options = {}
13
+ Extractor.new(path).extract options
14
+ end
15
+
16
+ def pages = collect_pages_from_tar
17
+
18
+ private
19
+
20
+ def collect_pages_from_tar
21
+ pages = []
22
+
23
+ File.open(path, 'rb') do |file|
24
+ Gem::Package::TarReader.new(file) do |tar|
25
+ tar.each do |entry|
26
+ next unless entry.file? && image_file?(entry.full_name)
27
+
28
+ pages << create_page_from_entry(entry)
29
+ end
30
+ end
31
+ end
32
+
33
+ pages.sort_by &:name
34
+ end
35
+
36
+ def create_page_from_entry entry
37
+ basename = File.basename entry.full_name
38
+
39
+ ComicBook::Page.new entry.full_name, basename
40
+ end
41
+
42
+ def image_file? filename
43
+ extension = File.extname filename.downcase
44
+
45
+ ComicBook::IMAGE_EXTENSIONS.include? extension
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,56 @@
1
+ class ComicBook
2
+ class CBZ < Adapter
3
+ class Archiver
4
+ def initialize source_folder
5
+ @source_folder = File.expand_path source_folder
6
+ end
7
+
8
+ def archive options = {}
9
+ extension = options.fetch :extension, :cbz
10
+ delete_original = options.fetch :delete_original, false
11
+
12
+ output_path = options[:to] || determine_output_path(extension)
13
+ create_zip_archive output_path
14
+ cleanup_source_folder if delete_original
15
+
16
+ output_path
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :source_folder
22
+
23
+ def determine_output_path extension
24
+ base_name = File.basename source_folder, '.*'
25
+ dir_name = File.dirname source_folder
26
+
27
+ File.expand_path File.join(dir_name, "#{base_name}.#{extension}")
28
+ end
29
+
30
+ def create_zip_archive output_path
31
+ Zip::File.open(output_path, create: true) do |zipfile|
32
+ find_image_files.each do |file|
33
+ add_file_to_zip zipfile, file
34
+ end
35
+ end
36
+ end
37
+
38
+ def find_image_files
39
+ pattern = File.join(source_folder, '**', ComicBook::IMAGE_GLOB_PATTERN)
40
+ Dir.glob(pattern, File::FNM_CASEFOLD).sort
41
+ end
42
+
43
+ def add_file_to_zip zipfile, file
44
+ file_path = Pathname.new file
45
+ source_path = Pathname.new source_folder
46
+ relative_path = file_path.relative_path_from source_path
47
+
48
+ zipfile.add relative_path.to_s, file
49
+ end
50
+
51
+ def cleanup_source_folder
52
+ FileUtils.rm_rf source_folder
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ class ComicBook
2
+ class CBZ < 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
+ extract_zip_contents destination
15
+ cleanup_archive_file if delete_original
16
+
17
+ destination
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :archive_path
23
+
24
+ def determine_extract_path extension
25
+ base_name = File.basename archive_path, '.*'
26
+ dir_name = File.dirname archive_path
27
+ archive_name = base_name
28
+
29
+ archive_name << ".#{extension}" if extension
30
+
31
+ full_path = File.join dir_name, archive_name
32
+ File.expand_path full_path
33
+ end
34
+
35
+ def extract_zip_contents destination
36
+ FileUtils.mkdir_p destination
37
+
38
+ Dir.chdir(File.dirname(destination)) do
39
+ destination_basename = File.basename destination
40
+ extract_files_from_zip destination_basename
41
+ end
42
+ end
43
+
44
+ def extract_files_from_zip destination_basename
45
+ Zip::File.open(archive_path) do |zipfile|
46
+ zipfile.each do |entry|
47
+ next unless image_file?(entry.name)
48
+
49
+ extract_single_file entry, destination_basename
50
+ end
51
+ end
52
+ end
53
+
54
+ def extract_single_file entry, destination_basename
55
+ file_path = File.join(destination_basename, entry.name)
56
+ FileUtils.mkdir_p File.dirname(file_path)
57
+
58
+ entry.extract(file_path) { true }
59
+ end
60
+
61
+ def cleanup_archive_file
62
+ File.delete archive_path
63
+ end
64
+
65
+ def image_file? filename
66
+ ComicBook::IMAGE_EXTENSIONS.include? File.extname(filename.downcase)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,46 @@
1
+ require 'zip'
2
+ require_relative 'adapter'
3
+ require_relative 'cbz/archiver'
4
+ require_relative 'cbz/extractor'
5
+
6
+ class ComicBook
7
+ class CBZ < Adapter
8
+ def archive options = {}
9
+ Archiver.new(path).archive options
10
+ end
11
+
12
+ def extract options = {}
13
+ Extractor.new(path).extract options
14
+ end
15
+
16
+ def pages = collect_pages_from_zip
17
+
18
+ private
19
+
20
+ def collect_pages_from_zip
21
+ pages = []
22
+
23
+ Zip::File.open(path) do |zipfile|
24
+ zipfile.each do |entry|
25
+ next unless image_file?(entry.name)
26
+
27
+ pages << create_page_from_entry(entry)
28
+ end
29
+ end
30
+
31
+ pages.sort_by &:name
32
+ end
33
+
34
+ def create_page_from_entry entry
35
+ basename = File.basename entry.name
36
+
37
+ ComicBook::Page.new entry.name, basename
38
+ end
39
+
40
+ def image_file? filename
41
+ extension = File.extname filename.downcase
42
+
43
+ ComicBook::IMAGE_EXTENSIONS.include? extension
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,123 @@
1
+ require 'optparse'
2
+
3
+ class ComicBook
4
+ class CLI
5
+ SUPPORTED_FORMATS = %w[.cb7 .cbt .cbz].freeze
6
+ UNSUPPORTED_FORMATS = %w[.cbr .cba].freeze
7
+
8
+ def self.start argv
9
+ new.start Array(argv)
10
+ end
11
+
12
+ def start argv
13
+ argv = Array argv
14
+
15
+ if argv.empty? || argv.include?('-h') || argv.include?('--help')
16
+ show_help
17
+ return
18
+ end
19
+
20
+ case command = argv.shift
21
+ when 'extract' then extract(argv)
22
+ when 'archive' then archive(argv)
23
+ else
24
+ puts "Unknown command: #{command}"
25
+ show_help
26
+ exit 1
27
+ end
28
+ rescue ComicBook::Error, StandardError => e
29
+ puts "Error: #{e.message}"
30
+ exit 1
31
+ end
32
+
33
+ private
34
+
35
+ def show_help
36
+ puts <<~HELP
37
+ ComicBook CLI
38
+
39
+ Usage:
40
+ comicbook extract <file> [--to <path>]
41
+ comicbook archive <folder> [--to <path>]
42
+ comicbook -h, --help
43
+
44
+ Commands:
45
+ extract Extract comic book archive
46
+ archive Create comic book archive
47
+
48
+ Options:
49
+ --from Source path (optional, first arg is default)
50
+ --to Destination path
51
+ --help, -h Show this help
52
+ HELP
53
+ end
54
+
55
+ def extract argv
56
+ from_path = nil
57
+ to_path = nil
58
+
59
+ parser = OptionParser.new do |opts|
60
+ opts.on('--from PATH', 'Source file path') { from_path = it }
61
+ opts.on('--to PATH', 'Destination path') { to_path = it }
62
+ end
63
+
64
+ remaining = parser.parse argv
65
+ from_path ||= remaining.first
66
+
67
+ validate_extract_args! from_path, to_path
68
+ ComicBook.extract from_path, { to: to_path }.compact
69
+
70
+ puts "Extracted #{from_path}#{" to #{to_path}" if to_path}"
71
+ end
72
+
73
+ def archive argv
74
+ from_path = nil
75
+ to_path = nil
76
+
77
+ parser = OptionParser.new do |opts|
78
+ opts.on('--from PATH', 'Source folder path') { from_path = it }
79
+ opts.on('--to PATH', 'Destination path') { to_path = it }
80
+ end
81
+
82
+ remaining = parser.parse argv
83
+ from_path ||= remaining.first
84
+
85
+ validate_archive_args! from_path, to_path
86
+
87
+ cb = ComicBook.new from_path
88
+
89
+ if to_path
90
+ cb.archive to: to_path
91
+ else
92
+ cb.archive
93
+ end
94
+
95
+ puts "Archived #{from_path}#{" to #{to_path}" if to_path}"
96
+ end
97
+
98
+ def validate_extract_args! from_path, to_path
99
+ # from
100
+ raise ComicBook::Error, 'Source file required' unless from_path
101
+ raise ComicBook::Error, "Source file not found: #{from_path}" unless File.exist?(from_path)
102
+ # to
103
+ raise ComicBook::Error, "Destination already exists: #{to_path}" if to_path && File.exist?(to_path)
104
+
105
+ # formats
106
+ ext = File.extname(from_path).downcase
107
+ raise ComicBook::Error, "Unsupported format: #{ext} (not yet implemented)" unless SUPPORTED_FORMATS.include?(ext)
108
+
109
+ nil
110
+ end
111
+
112
+ def validate_archive_args! from_path, to_path
113
+ # from
114
+ raise ComicBook::Error, 'Source folder required' unless from_path
115
+ raise ComicBook::Error, "Source folder not found: #{from_path}" unless File.exist?(from_path)
116
+ raise ComicBook::Error, "Source must be a directory: #{from_path}" unless File.directory?(from_path)
117
+ # to
118
+ raise ComicBook::Error, "Destination already exists: #{to_path}" if to_path && File.exist?(to_path)
119
+
120
+ nil
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,27 @@
1
+ class ComicBook
2
+ class CLIHelpers
3
+ class << self
4
+ def platform_dir
5
+ case RUBY_PLATFORM
6
+ when /darwin/ then 'macos'
7
+ when /linux/ then 'linux'
8
+ when /mingw/ then 'windows'
9
+ else
10
+ raise "Unsupported platform: #{RUBY_PLATFORM}"
11
+ end
12
+ end
13
+
14
+ def binary_path name
15
+ File.expand_path("../vendor/#{platform_dir}/#{name}", __FILE__)
16
+ end
17
+
18
+ def run_lsar(*)
19
+ system(binary_path('lsar'), *)
20
+ end
21
+
22
+ def run_unar(*)
23
+ system(binary_path('unar'), *)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ class ComicBook
2
+ class Page
3
+ attr_reader :path, :name
4
+
5
+ def initialize path, name
6
+ @path = path
7
+ @name = name
8
+ end
9
+ end
10
+ end