Add `arrived` attribute (file creation timestamp) to books table.
[quanlib.git] / walk_dir.rb
1 # Walk the directory (and subdirectories), identifying books.
2 #
3 # Expected format:
4 #   .../AuthorName/Title_of_the_Awesome_Book.ext
5 #
6 # Author is given as FirstLast.  For example, 
7 # Robert Anson Heinlein is RobertHeinlein, and 
8 # JKRowling is JoanneRowling.
9 #
10 # Book titles have spaces replaced with underscores,
11 # and punctuation [,!?'] replaced with hyphens.
12 #
13 # If the book forms part of a series, then an all-capitals 
14 # series designator, followed by a numeric volume number, 
15 # followed by an underscore, is prefixed to the name.
16 # For example, Hardy Boys' volume 1, The Tower Treasure, 
17 # is rendered as .../FranklinDixon/HB001_The_Tower_Treasure.epub
18 # and Mrs. Pollifax volume 6, On the China Station, is
19 # .../DorothyGilman/P06_On_the_China_Station.epub.
20
21 require_relative 'book'
22 require_relative 'book_loader'
23 require_relative 'store'
24
25 class WalkDir
26   def initialize(config_file, root)
27     @queue = Queue.new
28     @root = root
29     @config_file = config_file
30     @threads = []
31
32     @files = walk(@root)
33   end
34
35   def books
36     @threads = []
37     num_threads.times do
38       @threads << Thread.new do
39         BookLoader.new(@config_file, @queue).run
40       end
41     end
42
43     result = []
44     @files = remove_duplicates(@files)
45     for file in @files.sort()
46       if Book.can_handle?(file) && (!is_duplicate?(file))
47         # Queue this book to be loaded and added to the DB by a BookLoader thread
48         @queue << file
49       end
50     end
51
52     @threads.count.times { @queue << BookLoader::DONE_MARKER }
53
54     @threads.each { |t| t.join }
55   end
56
57   # Duplicate versions of a text are named 
58   #   xxx_suffix.ext
59   # Where suffix is one of bis, ter, quater, quinquies
60   # for the 2nd, 3rd, 4th or 5th variant respectively.
61   def is_duplicate?(file)
62     s = file.to_s
63     suffix = ['_bis.', '_ter.', '_quater.', '_quinquies.']
64     suffix.each do |pat|
65       if s.include?(pat)
66         return true
67       end
68     end
69     
70     return false
71   end
72
73   def remove_duplicates(files)
74     unique = {}
75     for file in files
76       if Book.can_handle?(file)
77         key = File.dirname(file) + '/' + File.basename(file, '.*')
78         if unique.has_key?(key)
79           new_ext = File.extname(file)
80           old_ext = File.extname(unique[key])
81           if ('.pdf' == old_ext) && ('.epub' == new_ext)
82             # Prefer EPUB over PDF
83             puts 'REPLACED ' + unique[key].to_s + ' with ' + file.to_s
84             unique[key] = file
85           else
86             puts 'DROPPED ' + file.to_s + " because it's superceded by " + unique[key].to_s
87           end
88         else
89           unique[key] = file
90         end
91       end
92     end
93
94     return unique.values
95   end
96
97   def walk(path)
98     result = []
99     children = Dir.entries(path)
100     for child in children
101       fullName = (path.chomp("/")) + "/" + child
102       if (File.directory?(fullName)) and (child != ".") and (child != "..") and (!File.symlink?(fullName))
103         sub = walk(fullName)
104         if (sub != nil) and (sub.length > 0)
105           result.concat(sub)
106         end
107       elsif (! File.directory?(fullName))
108         result.push(fullName)
109       end
110     end
111     return result
112   end
113
114   def num_threads
115     # TOOD:  make this (auto?) configurable
116     12
117   end
118 end