narou 2.9.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of narou might be problematic. Click here for more details.

Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.reek +34 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +9 -1
  6. data/ChangeLog.md +90 -0
  7. data/Gemfile +4 -0
  8. data/README.md +85 -8
  9. data/lib/command/convert.rb +1 -1
  10. data/lib/command/list.rb +59 -73
  11. data/lib/command/list/novel_decorator.rb +107 -0
  12. data/lib/command/send.rb +23 -13
  13. data/lib/command/setting.rb +5 -1
  14. data/lib/command/update.rb +104 -138
  15. data/lib/command/update/general_lastup_updater.rb +105 -0
  16. data/lib/command/update/interval.rb +29 -0
  17. data/lib/command/web.rb +1 -1
  18. data/lib/commandbase.rb +24 -17
  19. data/lib/converterbase.rb +10 -5
  20. data/lib/database.rb +16 -5
  21. data/lib/device.rb +5 -1
  22. data/lib/device/library/linux.rb +68 -8
  23. data/lib/downloader.rb +24 -23
  24. data/lib/extension.rb +16 -3
  25. data/lib/helper.rb +26 -3
  26. data/lib/html.rb +1 -1
  27. data/lib/inspector.rb +9 -0
  28. data/lib/inventory.rb +20 -6
  29. data/lib/logger.rb +8 -2
  30. data/lib/mailer.rb +2 -1
  31. data/lib/narou.rb +28 -18
  32. data/lib/narou/api.rb +62 -31
  33. data/lib/novelconverter.rb +8 -1
  34. data/lib/novelinfo.rb +8 -7
  35. data/lib/novelsetting.rb +1 -0
  36. data/lib/progressbar.rb +6 -2
  37. data/lib/sitesetting.rb +2 -1
  38. data/lib/version.rb +1 -1
  39. data/lib/web/appserver.rb +72 -9
  40. data/lib/web/public/resources/narou.library.js +130 -13
  41. data/lib/web/public/resources/narou.ui.js +96 -9
  42. data/lib/web/settingmessages.rb +2 -1
  43. data/lib/web/views/_about.haml +15 -2
  44. data/lib/web/views/index.haml +12 -3
  45. data/lib/web/views/layout.haml +2 -1
  46. data/lib/web/views/style.scss +12 -0
  47. data/narou.gemspec +88 -11
  48. data/narou.rb +7 -1
  49. data/preset/mail_setting.yaml +40 -6
  50. data/webnovel/kakuyomu.jp.yaml +8 -8
  51. metadata +95 -139
  52. data/spec/README.txt +0 -4
  53. data/spec/convert_spec.rb +0 -119
  54. data/spec/converterbase_spec.rb +0 -298
  55. data/spec/data/convert_test/auto_indent/correct_test_auto_indent.txt +0 -28
  56. data/spec/data/convert_test/auto_indent/test_auto_indent.txt +0 -32
  57. data/spec/data/convert_test/auto_join_bracket/correct_test_auto_join_bracket.txt +0 -24
  58. data/spec/data/convert_test/auto_join_bracket/test_auto_join_bracket.txt +0 -28
  59. data/spec/data/convert_test/auto_join_line/correct_test_auto_join_line.txt +0 -43
  60. data/spec/data/convert_test/auto_join_line/test_auto_join_line.txt +0 -59
  61. data/spec/data/convert_test/convert_page_break/correct_test_convert_page_break.txt +0 -22
  62. data/spec/data/convert_test/convert_page_break/setting.ini +0 -5
  63. data/spec/data/convert_test/convert_page_break/test_convert_page_break.txt +0 -60
  64. data/spec/data/convert_test/double_dash_to_image/correct_test_double_dash_to_image.txt +0 -14
  65. data/spec/data/convert_test/double_dash_to_image/setting.ini +0 -1
  66. data/spec/data/convert_test/double_dash_to_image/test_double_dash_to_image.txt +0 -12
  67. data/spec/data/convert_test/english/correct_test_english.txt +0 -27
  68. data/spec/data/convert_test/english/test_english.txt +0 -27
  69. data/spec/data/convert_test/force_indent_special_chapter/correct_test_force_indent_special_chapter.txt +0 -64
  70. data/spec/data/convert_test/force_indent_special_chapter/test_force_indent_special_chapter.txt +0 -61
  71. data/spec/data/convert_test/horizontal_ellipsis/correct_test_horizontal_ellipsis.txt +0 -53
  72. data/spec/data/convert_test/horizontal_ellipsis/test_horizontal_ellipsis.txt +0 -57
  73. data/spec/data/convert_test/insert_separator/correct_test_insert_separator.txt +0 -13
  74. data/spec/data/convert_test/insert_separator/setting.ini +0 -3
  75. data/spec/data/convert_test/insert_separator/test_insert_separator.txt +0 -12
  76. data/spec/data/convert_test/insert_separator_and_replace_txt/correct_test_insert_separator_and_replace_txt.txt +0 -12
  77. data/spec/data/convert_test/insert_separator_and_replace_txt/setting.ini +0 -3
  78. data/spec/data/convert_test/insert_separator_and_replace_txt/test_insert_separator_and_replace_txt.txt +0 -11
  79. data/spec/data/convert_test/kanji_num/correct_test_kanji_num.txt +0 -50
  80. data/spec/data/convert_test/kanji_num/test_kanji_num.txt +0 -56
  81. data/spec/data/convert_test/nonokagi/correct_test_nonokagi.txt +0 -30
  82. data/spec/data/convert_test/nonokagi/test_nonokagi.txt +0 -34
  83. data/spec/data/convert_test/replace/correct_test_replace.txt +0 -13
  84. data/spec/data/convert_test/replace/test_replace.txt +0 -14
  85. data/spec/data/convert_test/ruby/correct_test_ruby.txt +0 -161
  86. data/spec/data/convert_test/ruby/test_ruby.txt +0 -205
  87. data/spec/data/convert_test/ruby_youon/correct_test_ruby_youon.txt +0 -13
  88. data/spec/data/convert_test/ruby_youon/setting.ini +0 -2
  89. data/spec/data/convert_test/ruby_youon/test_ruby_youon.txt +0 -14
  90. data/spec/data/convert_test/sesame/correct_test_sesame.txt +0 -41
  91. data/spec/data/convert_test/sesame/test_sesame.txt +0 -52
  92. data/spec/data/convert_test/to_odd_leader/correct_test_to_odd_leader.txt +0 -21
  93. data/spec/data/convert_test/to_odd_leader/test_to_odd_leader.txt +0 -21
  94. data/spec/data/html_test.html +0 -5
  95. data/spec/data/html_test.txt +0 -4
  96. data/spec/data/test.ini +0 -10
  97. data/spec/device_spec.rb +0 -39
  98. data/spec/downloader_spec.rb +0 -37
  99. data/spec/eventable_spec.rb +0 -172
  100. data/spec/exit_code_spec.rb +0 -67
  101. data/spec/generator/convert_spec_gen.rb +0 -96
  102. data/spec/generator/num_to_kanji_test_gen.rb +0 -33
  103. data/spec/helper_spec.rb +0 -90
  104. data/spec/html_spec.rb +0 -83
  105. data/spec/ini_spec.rb +0 -145
  106. data/spec/input_spec.rb +0 -76
  107. data/spec/logger_spec.rb +0 -92
  108. data/spec/novelinfo_spec.rb +0 -15
  109. data/spec/novelsetting_spec.rb +0 -35
  110. data/spec/num_to_kanji_spec.rb +0 -2482
  111. data/spec/spec_helper.rb +0 -16
  112. data/spec/worker_spec.rb +0 -75
@@ -18,8 +18,21 @@ end
18
18
  require "securerandom"
19
19
 
20
20
  def File.write(path, string, *options)
21
- dirname = File.dirname(path)
22
- temp_path = File.join(dirname, SecureRandom.hex(15))
21
+ dirpath = File.dirname(path)
22
+ backup = false
23
+ temp_path =
24
+ if File.extname(path) == ".yaml" && File.basename(dirpath) != Downloader::SECTION_SAVE_DIR_NAME
25
+ backup = true
26
+ "#{path}.backup"
27
+ else
28
+ File.join(dirpath, SecureRandom.hex(15))
29
+ end
30
+
23
31
  super(temp_path, string, *options)
24
- File.rename(temp_path, path)
32
+
33
+ if backup
34
+ super(path, string, *options)
35
+ else
36
+ File.rename(temp_path, path)
37
+ end
25
38
  end
@@ -99,6 +99,12 @@ module Helper
99
99
 
100
100
  def replace_filename_special_chars(str, invalid_replace = false)
101
101
  result = str.tr("/:*?\"<>|.", "/:*?”〈〉|.").gsub("\\", "¥").gsub("\t", "").gsub("\n", "")
102
+ if Inventory.load("local_setting")["normalize-filename"]
103
+ begin
104
+ result.unicode_normalize!
105
+ rescue Encoding::CompatibilityError
106
+ end
107
+ end
102
108
  if invalid_replace
103
109
  org_encoding = result.encoding
104
110
  result = result.encode(Encoding::Windows_31J, invalid: :replace, undef: :replace, replace: "_")
@@ -111,16 +117,24 @@ module Helper
111
117
  # ダウンロードした文字列をエンコード及び不正な文字列除去、改行コード統一
112
118
  #
113
119
  def pretreatment_source(src, encoding = Encoding::UTF_8)
120
+ encoding_class = Encoding.find(encoding)
114
121
  src.force_encoding(encoding)
122
+ .tap do |this|
123
+ if encoding_class != Encoding::UTF_8
124
+ this.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
125
+ end
126
+ end
115
127
  .scrub("?")
116
128
  .gsub("\r", "")
129
+ .gsub(/&#x([0-9a-f]+);/i) { [$1.hex].pack("U") }
130
+ .gsub(/&#(\d+);/) { [$1.to_i].pack("U") }
117
131
  end
118
132
 
119
133
  ENTITIES = { quot: '"', amp: "&", nbsp: " ", lt: "<", gt: ">", copy: "(c)", "#39" => "'" }
120
134
  #
121
135
  # エンティティ復号
122
136
  #
123
- def restor_entity(str)
137
+ def restore_entity(str)
124
138
  result = str.dup
125
139
  ENTITIES.each do |key, value|
126
140
  result.gsub!("&#{key};", value)
@@ -251,8 +265,9 @@ module Helper
251
265
  #
252
266
  # from: ファイルパスをまとめた Array
253
267
  # dest_dir: コピー先のディレクトリ
268
+ # check_timestamp: タイムスタンプを比較して新しければコピーする
254
269
  #
255
- def copy_files(from, dest_dir)
270
+ def copy_files(from, dest_dir, check_timestamp: true)
256
271
  from.each do |path|
257
272
  basename = File.basename(path)
258
273
  dirname = File.basename(File.dirname(path))
@@ -260,7 +275,13 @@ module Helper
260
275
  unless File.directory?(save_dir)
261
276
  FileUtils.mkdir_p(save_dir)
262
277
  end
263
- FileUtils.copy(path, File.join(save_dir, basename))
278
+ dest = File.join(save_dir, basename)
279
+ if check_timestamp && File.exist?(dest)
280
+ src_mtime = File.mtime(path)
281
+ dest_mtime = File.mtime(dest)
282
+ next if dest_mtime >= src_mtime
283
+ end
284
+ FileUtils.copy(path, dest)
264
285
  end
265
286
  end
266
287
 
@@ -269,6 +290,8 @@ module Helper
269
290
  #
270
291
  def date_string_to_time(date)
271
292
  date ? Time.parse(date.sub(/[\((].+?[\))]/, "").tr("年月日時分秒@;", "///::: :")) : nil
293
+ rescue ArgumentError
294
+ nil
272
295
  end
273
296
 
274
297
  #
@@ -49,7 +49,7 @@ class HTML
49
49
  @string = img_to_aozora
50
50
  @string = em_to_sesame
51
51
  @string = delete_tag
52
- @string = Helper.restor_entity(@string)
52
+ @string = Helper.restore_entity(@string)
53
53
  @string
54
54
  end
55
55
 
@@ -58,6 +58,15 @@ class Inspector
58
58
  @info
59
59
  end
60
60
 
61
+ def display_summary(target = $stdout)
62
+ target.print "小説状態の調査結果を #{Inspector::INSPECT_LOG_NAME} に出力しました("
63
+ target.print KLASS_TAG.values.map { |klass_type|
64
+ num = @messages.count { |msg| msg =~ /^\[#{klass_type}\]/ }
65
+ "#{klass_type}:#{num}件"
66
+ }.join("、")
67
+ target.puts ")"
68
+ end
69
+
61
70
  def display(klass = ALL, target = $stdout)
62
71
  target.puts @messages.map { |msg|
63
72
  if msg =~ /^\[(.+)\]/
@@ -30,20 +30,27 @@ module Inventory
30
30
  def init(name, scope)
31
31
  dir = case scope
32
32
  when :local
33
- Narou.get_local_setting_dir
33
+ Narou.local_setting_dir
34
34
  when :global
35
- Narou.get_global_setting_dir
35
+ Narou.global_setting_dir
36
36
  else
37
37
  raise "Unknown scope"
38
38
  end
39
39
  return nil unless dir
40
40
  @mutex = Mutex.new
41
41
  @inventory_file_path = File.join(dir, name + ".yaml")
42
- if File.exist?(@inventory_file_path)
43
- self.merge!(Helper::CacheLoader.memo(@inventory_file_path) { |yaml|
42
+ return unless File.exist?(@inventory_file_path)
43
+ self.merge!(Helper::CacheLoader.memo(@inventory_file_path) { |yaml|
44
+ begin
44
45
  YAML.load(yaml)
45
- })
46
- end
46
+ rescue Psych::SyntaxError
47
+ unless restore(@inventory_file_path)
48
+ error "#{@inventory_file_path} が壊れてるっぽい"
49
+ raise
50
+ end
51
+ YAML.load_file(@inventory_file_path)
52
+ end
53
+ })
47
54
  end
48
55
 
49
56
  def save
@@ -54,5 +61,12 @@ module Inventory
54
61
  File.write(@inventory_file_path, YAML.dump(self))
55
62
  end
56
63
  end
64
+
65
+ def restore(path)
66
+ backup_path = "#{path}.backup"
67
+ return nil unless File.exist?(backup_path)
68
+ FileUtils.copy(backup_path, path)
69
+ true
70
+ end
57
71
  end
58
72
 
@@ -83,7 +83,12 @@ module Narou::LoggerModule
83
83
  $stdout.capturing = false
84
84
  buffer = $stdout.string
85
85
  $stdout = temp_stream
86
- options[:ansicolor_strip] ? strip_color(buffer) : buffer
86
+ result = options[:ansicolor_strip] ? strip_color(buffer) : buffer
87
+ if $stdout.capturing && !options[:quiet]
88
+ # 多段キャプチャ中かつ quite: false の場合は外側にも伝播する
89
+ $stdout.string << result
90
+ end
91
+ result
87
92
  end
88
93
 
89
94
  def strip_color(str)
@@ -151,8 +156,9 @@ class Narou::LoggerError < StringIO
151
156
  end
152
157
  end
153
158
 
154
- def warn(str)
159
+ def warn(str, trace = nil)
155
160
  $stdout.warn str
161
+ $stdout.warn trace if trace
156
162
  end
157
163
 
158
164
  def error(str)
@@ -56,7 +56,8 @@ class Mailer
56
56
  params[:charset] = "UTF-8"
57
57
  params[:text_part_charset] = "UTF-8"
58
58
  if attached_file_path
59
- params[:attachments] = { File.basename(attached_file_path) => File.binread(attached_file_path) }
59
+ basename = File.basename(attached_file_path).tr("@", "@")
60
+ params[:attachments] = { basename => File.binread(attached_file_path) }
60
61
  end
61
62
  begin
62
63
  Pony.mail(params)
@@ -12,8 +12,8 @@ if Helper.engine_jruby?
12
12
  end
13
13
 
14
14
  module Narou
15
- LOCAL_SETTING_DIR = ".narou"
16
- GLOBAL_SETTING_DIR = ".narousetting"
15
+ LOCAL_SETTING_DIR_NAME = ".narou"
16
+ GLOBAL_SETTING_DIR_NAME = ".narousetting"
17
17
  AOZORAEPUB3_JAR_NAME = "AozoraEpub3.jar"
18
18
  AOZORAEPUB3_DIR = "AozoraEpub3"
19
19
  PRESET_DIR = "preset"
@@ -22,6 +22,7 @@ module Narou
22
22
  EXIT_ERROR_CODE = 127
23
23
  EXIT_INTERRUPT = 126
24
24
  EXIT_REQUEST_REBOOT = 125
25
+ MODIFIED_TAG = "modified"
25
26
 
26
27
  UPDATE_SORT_KEYS = {
27
28
  "id" => "ID", "last_update" => "更新日", "title" => "タイトル", "author" => "作者名",
@@ -33,6 +34,10 @@ module Narou
33
34
 
34
35
  @@is_web = false
35
36
 
37
+ def last_commit_year
38
+ 2016
39
+ end
40
+
36
41
  def get_root_dir
37
42
  root_dir = nil
38
43
  path = Dir.pwd
@@ -43,7 +48,7 @@ module Narou
43
48
  drive_letter = $&
44
49
  end
45
50
  while path != ""
46
- if File.directory?("#{drive_letter}#{path}/#{LOCAL_SETTING_DIR}")
51
+ if File.directory?("#{drive_letter}#{path}/#{LOCAL_SETTING_DIR_NAME}")
47
52
  root_dir = drive_letter + path
48
53
  break
49
54
  end
@@ -53,24 +58,27 @@ module Narou
53
58
  end
54
59
  memoize :get_root_dir
55
60
 
56
- def get_local_setting_dir
61
+ def local_setting_dir
57
62
  local_setting_dir = nil
58
63
  root_dir = get_root_dir
59
64
  if root_dir
60
- local_setting_dir = File.join(root_dir, LOCAL_SETTING_DIR)
65
+ local_setting_dir = File.join(root_dir, LOCAL_SETTING_DIR_NAME)
61
66
  end
62
67
  local_setting_dir
63
68
  end
64
- memoize :get_local_setting_dir
69
+ memoize :local_setting_dir
65
70
 
66
- def get_global_setting_dir
67
- global_setting_dir = File.expand_path(File.join("~", GLOBAL_SETTING_DIR))
68
- unless File.exist?(global_setting_dir)
69
- FileUtils.mkdir(global_setting_dir)
71
+ def global_setting_dir
72
+ root_dir = get_root_dir
73
+ if root_dir
74
+ dir = File.join(root_dir, GLOBAL_SETTING_DIR_NAME)
75
+ return dir if File.directory?(dir)
70
76
  end
71
- global_setting_dir
77
+ dir = File.expand_path(GLOBAL_SETTING_DIR_NAME, "~")
78
+ FileUtils.mkdir(dir) unless File.exist?(dir)
79
+ dir
72
80
  end
73
- memoize :get_global_setting_dir
81
+ memoize :global_setting_dir
74
82
 
75
83
  def get_script_dir
76
84
  File.expand_path(File.join(File.dirname(__FILE__), ".."))
@@ -83,8 +91,8 @@ module Narou
83
91
 
84
92
  def init
85
93
  return nil if already_init?
86
- FileUtils.mkdir(LOCAL_SETTING_DIR)
87
- puts LOCAL_SETTING_DIR + "/ を作成しました"
94
+ FileUtils.mkdir(LOCAL_SETTING_DIR_NAME)
95
+ puts "#{LOCAL_SETTING_DIR}/ を作成しました"
88
96
  Database.init
89
97
  end
90
98
 
@@ -245,10 +253,12 @@ module Narou
245
253
  @@is_web
246
254
  end
247
255
 
248
- def update_sort_key_summaries(width = 27)
249
- ({"KEY" => "対象"}.merge(UPDATE_SORT_KEYS)).map { |(key, summary)|
250
- "#{key.rjust(width)} : #{summary}"
251
- }.join("\n")
256
+ def update_sort_key_summaries(left_space = 28)
257
+ summaries = { "KEY" => " 対象" }.merge(UPDATE_SORT_KEYS)
258
+ key_max_width = summaries.keys.max_by(&:length).length
259
+ summaries.map do |(key, summary)|
260
+ "#{" " * left_space}| #{key.center(key_max_width)} | #{summary}"
261
+ end.join("\n")
252
262
  end
253
263
 
254
264
  def get_theme
@@ -4,7 +4,10 @@
4
4
  #
5
5
 
6
6
  require "open-uri"
7
+ require "zlib"
7
8
  require "yaml"
9
+ require "json"
10
+ require "memoist"
8
11
  require_relative "../novelinfo"
9
12
 
10
13
  module Narou
@@ -12,50 +15,78 @@ module Narou
12
15
  # 小説家になろうデベロッパーAPI操作クラス
13
16
  #
14
17
  class API
18
+ extend Memoist
19
+
20
+ # 一度に問い合わせする件数
21
+ BATCH_LIMIT = 300
22
+
23
+ # なろうAPIの結果の圧縮レベル(1〜5)
24
+ GZIP_LEVEL = 5
25
+
26
+ # リクエスト間ウェイト(sec)
27
+ REQUEST_INTERVAL = 3
28
+
15
29
  #
16
30
  # なろうデベロッパーAPIから情報を取得
17
31
  #
18
- # setting: なろうの SiteSetting
32
+ # api_url: なろうAPIのエンドポイントを指定。通常小説用と18禁小説用と分かれているため
33
+ # ncodes: 取得したい小説のNコードを指定。文字列か配列を指定可能
19
34
  # of: 出力パラメータ(http://dev.syosetu.com/man/api/#of_parm 参照)
20
35
  #
21
- def initialize(setting, of)
22
- @setting = setting
23
- @api_url = @setting["narou_api_url"]
24
- @ncode = @setting["ncode"]
25
- request_api(of)
36
+ def initialize(api_url:, ncodes:, of:)
37
+ @api_url = api_url
38
+ @ncodes = Array(ncodes).map(&:downcase)
39
+ @of = "n-#{of}"
40
+ @splited_of = @of.split("-")
26
41
  end
27
42
 
28
43
  def [](key)
29
44
  @api_result[key]
30
45
  end
31
46
 
32
- def request_api(of, gzip = 5)
33
- # Ruby 2.0 以上だと gzip 自動デコード
34
- gzip_opt = RUBY_VERSION >= "2.0.0" ? "gzip=#{gzip}&" : ""
35
- url = "#{@api_url}?#{gzip_opt}ncode=#{@ncode}&of=#{of}"
47
+ def request
48
+ @stores = []
49
+ @ncodes.each_slice(BATCH_LIMIT).with_index do |ncodes, index|
50
+ sleep REQUEST_INTERVAL unless index.zero?
51
+ request_api(ncodes)
52
+ end
53
+ @stores
54
+ end
55
+
56
+ def request_api(ncodes, limit = BATCH_LIMIT)
57
+ url = "#{@api_url}?gzip=#{GZIP_LEVEL}&ncode=#{ncodes.join("-")}&of=#{@of}&lim=#{limit}&out=json"
36
58
  open(url) do |fp|
37
- result = YAML.load(fp.read.force_encoding(Encoding::UTF_8))
38
- if result[0]["allcount"] == 1
39
- @api_result = result[1]
40
- if of.length > 0
41
- @api_result["novel_type"] = @api_result["noveltype"]
42
- # なろうAPIが返すデータが数値の場合があるため強制変換
43
- @api_result["writer"] = @api_result["writer"].to_s
44
- stat_end = @api_result["end"]
45
- if stat_end
46
- @api_result["end"] = stat_end == 0
47
- end
48
- end
49
- else
50
- # なろうAPIからデータを取得出来なかった
51
- # 開示設定が検索から除外に設定されるとAPIからはアクセスできなくなる
52
- result = NovelInfo.load(@setting)
53
- unless result
54
- error "小説家になろうからデータを取得出来ませんでした"
55
- exit Narou::EXIT_ERROR_CODE
56
- end
57
- @api_result = result
59
+ data = Zlib::GzipReader.wrap(fp).read.force_encoding(Encoding::UTF_8)
60
+ results = JSON.parse(data)
61
+ return if results[0]["allcount"].zero?
62
+ store_results(results.drop(1))
63
+ end
64
+ end
65
+
66
+ def store_results(results)
67
+ @stores += results.map do |result|
68
+ result["ncode"].downcase!
69
+ result["novel_type"] = result["noveltype"] if has_of?("nt")
70
+ result["writer"] = result["writer"].to_s if has_of?("w")
71
+ %w(general_firstup general_lastup novelupdated_at).each do |key|
72
+ result[key] &&= Time.parse(result[key])
58
73
  end
74
+ stat_end = result["end"]
75
+ if stat_end
76
+ result["end"] = stat_end.zero?
77
+ end
78
+ result
79
+ end
80
+ end
81
+
82
+ def has_of?(type)
83
+ @splited_of.include?(type)
84
+ end
85
+ memoize :has_of?
86
+
87
+ def private_novels
88
+ (@ncodes - @stores.map { |st| st["ncode"] }).map do |ncode|
89
+ Downloader.get_data_by_target(ncode)["id"]
59
90
  end
60
91
  end
61
92
  end
@@ -451,6 +451,7 @@ class NovelConverter
451
451
  # $t タイトル自身。書式の中で自由な位置にタイトルを埋め込める
452
452
  # $ns 小説が掲載されているサイト名
453
453
  # $nt 小説種別(短編 or 連載)
454
+ # $ntag 小説のタグをカンマ区切りにしたもの
454
455
  #
455
456
  # ※ $t を使用した場合、title_date_align を無視する
456
457
  #
@@ -462,6 +463,7 @@ class NovelConverter
462
463
  special_format_chars = [
463
464
  ["$s", calc_reverse_short_time(new_arrivals_date)],
464
465
  ["$ns", @data["sitename"]],
466
+ ["$ntag", tags_join_comma(@data)],
465
467
  ["$nt", Narou.novel_type_text(@data["novel_type"])],
466
468
  ["$t", title]
467
469
  ]
@@ -487,6 +489,11 @@ class NovelConverter
487
489
  result
488
490
  end
489
491
 
492
+ def tags_join_comma(data)
493
+ tags = data["tags"] || []
494
+ tags.sort.join(",")
495
+ end
496
+
490
497
  def decorate_title(title)
491
498
  processed_title = add_date_to_title(title)
492
499
  # タイトルに完結したかどうかを付加する
@@ -678,7 +685,7 @@ class NovelConverter
678
685
 
679
686
  if !@display_inspector
680
687
  unless @inspector.empty?
681
- puts "小説状態の調査結果を #{Inspector::INSPECT_LOG_NAME} に出力しました"
688
+ @inspector.display_summary
682
689
  end
683
690
  else
684
691
  # 小説の監視・検査状況を表示する