Add RAR/CBR support

This commit is contained in:
Alex Ling 2020-05-29 13:45:25 +00:00
parent 8665616c2e
commit 6b43ee7fe5
6 changed files with 77 additions and 24 deletions

View File

@ -4,6 +4,10 @@ shards:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: 0.12.0 version: 0.12.0
archive:
github: hkalexling/archive.cr
commit: 64223b64d79afafcec67a127b92f21f6625ce5ce
baked_file_system: baked_file_system:
github: schovi/baked_file_system github: schovi/baked_file_system
version: 0.9.8 version: 0.9.8

View File

@ -19,6 +19,8 @@ dependencies:
github: crystal-lang/crystal-sqlite3 github: crystal-lang/crystal-sqlite3
baked_file_system: baked_file_system:
github: schovi/baked_file_system github: schovi/baked_file_system
archive:
github: hkalexling/archive.cr
development_dependencies: development_dependencies:
ameba: ameba:

53
src/archive.cr Normal file
View File

@ -0,0 +1,53 @@
require "zip"
require "archive"
# A unified class to handle all supported archive formats. It uses the ::Zip
# module in crystal standard library if the target file is a zip archive.
# Otherwise it uses `archive.cr`.
class ArchiveFile
def initialize(@filename : String)
if [".cbz", ".zip"].includes? File.extname filename
@archive_file = Zip::File.new filename
else
@archive_file = Archive::File.new filename
end
end
def self.open(filename : String, &)
s = self.new filename
yield s
s.close
end
def close
if @archive_file.is_a? Zip::File
@archive_file.as(Zip::File).close
end
end
# Lists all file entries
def entries
ary = [] of Zip::File::Entry | Archive::Entry
@archive_file.entries.map do |e|
if (e.is_a? Zip::File::Entry && e.file?) ||
(e.is_a? Archive::Entry && e.info.file?)
ary.push e
end
end
ary
end
def read_entry(e : Zip::File::Entry | Archive::Entry) : Bytes?
if e.is_a? Zip::File::Entry
data = nil
e.open do |io|
slice = Bytes.new e.uncompressed_size
bytes_read = io.read_fully? slice
data = slice if bytes_read
end
data
else
e.read
end
end
end

View File

@ -1,8 +1,8 @@
require "zip"
require "mime" require "mime"
require "json" require "json"
require "uri" require "uri"
require "./util" require "./util"
require "./archive"
struct Image struct Image
property data : Bytes property data : Bytes
@ -25,7 +25,7 @@ class Entry
@title = File.basename path, File.extname path @title = File.basename path, File.extname path
@encoded_title = URI.encode @title @encoded_title = URI.encode @title
@size = (File.size path).humanize_bytes @size = (File.size path).humanize_bytes
file = Zip::File.new path file = ArchiveFile.new path
@pages = file.entries.count do |e| @pages = file.entries.count do |e|
["image/jpeg", "image/png"].includes? \ ["image/jpeg", "image/png"].includes? \
MIME.from_filename? e.filename MIME.from_filename? e.filename
@ -68,7 +68,8 @@ class Entry
end end
def read_page(page_num) def read_page(page_num)
Zip::File.open @zip_path do |file| img = nil
ArchiveFile.open @zip_path do |file|
page = file.entries page = file.entries
.select { |e| .select { |e|
["image/jpeg", "image/png"].includes? \ ["image/jpeg", "image/png"].includes? \
@ -78,16 +79,13 @@ class Entry
compare_alphanumerically a.filename, b.filename compare_alphanumerically a.filename, b.filename
} }
.[page_num - 1] .[page_num - 1]
page.open do |io| data = file.read_entry page
slice = Bytes.new page.uncompressed_size if data
bytes_read = io.read_fully? slice img = Image.new data, MIME.from_filename(page.filename), page.filename,
unless bytes_read data.size
return nil
end
return Image.new slice, MIME.from_filename(page.filename),
page.filename, bytes_read
end end
end end
img
end end
end end
@ -115,12 +113,12 @@ class Title
@title_ids << title.id @title_ids << title.id
next next
end end
if [".zip", ".cbz"].includes? File.extname path if [".zip", ".cbz", ".rar", ".cbr"].includes? File.extname path
zip_exception = validate_zip path archive_exception = validate_archive path
unless zip_exception.nil? unless archive_exception.nil?
Logger.warn "File #{path} is corrupted or is not a valid zip " \ Logger.warn "File #{path} is corrupted or is not a valid archive. " \
"archive. Ignoring it." "Ignoring it."
Logger.debug "Zip error: #{zip_exception}" Logger.debug "Archive error: #{archive_exception}"
next next
end end
entry = Entry.new path, self, @id, storage entry = Entry.new path, self, @id, storage

View File

@ -371,7 +371,7 @@ module MangaDex
writer.close writer.close
Logger.debug "cbz File created at #{zip_path}" Logger.debug "cbz File created at #{zip_path}"
zip_exception = validate_zip zip_path zip_exception = validate_archive zip_path
if !zip_exception.nil? if !zip_exception.nil?
@queue.add_message "The downloaded archive is corrupted. " \ @queue.add_message "The downloaded archive is corrupted. " \
"Error: #{zip_exception}", job "Error: #{zip_exception}", job

View File

@ -85,12 +85,8 @@ def compare_alphanumerically(a : String, b : String)
compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b) compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b)
end end
# When downloading from MangaDex, the zip/cbz file would not be valid def validate_archive(path : String) : Exception?
# before the download is completed. If we scan the zip file, file = ArchiveFile.new path
# Entry.new would throw, so we use this method to check before
# constructing Entry
def validate_zip(path : String) : Exception?
file = Zip::File.new path
file.close file.close
return return
rescue e rescue e