From 2b67a6476f89979dbb5f5294b091375c7f20252e Mon Sep 17 00:00:00 2001 From: rumia Date: Tue, 28 Oct 2025 22:47:17 +0900 Subject: [PATCH 1/5] =?UTF-8?q?UTF-8=E3=82=A8=E3=83=B3=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=92=E5=BC=B7=E5=88=B6?= =?UTF-8?q?=E3=81=97=E3=80=81WebSocket=E9=80=81=E4=BF=A1=E3=82=B9=E3=83=AC?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/novelconverter.rb | 6 +++--- lib/web/public/resources/js/narou.ui.js | 1 - lib/web/pushserver.rb | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index a120df02..51d7183a 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -280,17 +280,17 @@ def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) return :error end - content = opf_body.dup + content = opf_body.dup.force_encoding("UTF-8") content.gsub!(/.*?<\/dc:subject>\s*\n?\s*/m, "") dc_subject_lines = subjects.map(&:strip).reject(&:empty?).map { |s| esc = s.gsub("&","&").gsub("<","<").gsub(">",">").gsub("\"",""").gsub("'","'") - " #{esc}" + "\t\t#{esc}" } if dc_subject_lines.any? dc_subjects_xml = dc_subject_lines.join("\n") + "\n" content.sub!(/(\s*)<\/metadata>/, "\n#{dc_subjects_xml}\\1") end - entries[opf_name] = content + entries[opf_name] = content.b # 再Zip化 (mimetypeは無圧縮で先頭) File.delete(epub_path) diff --git a/lib/web/public/resources/js/narou.ui.js b/lib/web/public/resources/js/narou.ui.js index 459ef75a..59824556 100644 --- a/lib/web/public/resources/js/narou.ui.js +++ b/lib/web/public/resources/js/narou.ui.js @@ -222,7 +222,6 @@ $(function() { var isLightweightMode = false; var table, t; var action; // アクション管理インスタンス - window.action = action; var isInitialLoad = true; // 初回ロードフラグ /* diff --git a/lib/web/pushserver.rb b/lib/web/pushserver.rb index d7ed6487..6a80620b 100644 --- a/lib/web/pushserver.rb +++ b/lib/web/pushserver.rb @@ -50,9 +50,18 @@ def run end thread = Thread.new do - while true - data = que.pop - ws.send(data) + begin + while true + data = que.pop + ws.send(data) + end + rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e + # 接続が切れた場合、スレッドを終了 + break + rescue => e + # その他のエラーもログに出力してスレッド終了 + puts "[ERROR] WebSocket send thread error: #{e.class}: #{e.message}" if $DEBUG + break end end From 70e3e8c1f1ca8050c8b21ff59ecf7a288aef20db Mon Sep 17 00:00:00 2001 From: rumia Date: Tue, 28 Oct 2025 22:51:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?WebSocket=E9=80=81=E4=BF=A1=E3=82=B9?= =?UTF-8?q?=E3=83=AC=E3=83=83=E3=83=89=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84=E3=81=97=E3=80=81=E6=8E=A5=E7=B6=9A=E5=88=87?= =?UTF-8?q?=E6=96=AD=E6=99=82=E3=81=AE=E3=82=B9=E3=83=AC=E3=83=83=E3=83=89?= =?UTF-8?q?=E7=B5=82=E4=BA=86=E5=87=A6=E7=90=86=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/web/pushserver.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web/pushserver.rb b/lib/web/pushserver.rb index 6a80620b..26aecf50 100644 --- a/lib/web/pushserver.rb +++ b/lib/web/pushserver.rb @@ -57,11 +57,9 @@ def run end rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e # 接続が切れた場合、スレッドを終了 - break rescue => e # その他のエラーもログに出力してスレッド終了 puts "[ERROR] WebSocket send thread error: #{e.class}: #{e.message}" if $DEBUG - break end end From 49d7985cba2245d2e28b74d44ee9ff34bd289f62 Mon Sep 17 00:00:00 2001 From: rumia Date: Wed, 29 Oct 2025 00:11:27 +0900 Subject: [PATCH 3/5] =?UTF-8?q?ruby-zip=203.x=20=E7=B3=BB=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windowsでのスレッド内ファイル操作対策として、GCを強制実行しファイルハンドルを解放する処理を追加 --- lib/command/backup.rb | 7 ++++++- lib/command/update.rb | 12 ++++++++++++ lib/command/web.rb | 4 ++-- lib/device/ibunko.rb | 18 ++++++++++++++---- lib/downloader.rb | 27 +++++++++++++++++++++++++-- lib/novelconverter.rb | 6 +++++- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/lib/command/backup.rb b/lib/command/backup.rb index e1c8acc5..6b9d9095 100644 --- a/lib/command/backup.rb +++ b/lib/command/backup.rb @@ -46,7 +46,12 @@ def create_backup(data) backup_dir = novel_dir.join(BACKUP_DIR_NAME) backup_dir.mkdir unless backup_dir.exist? Zip.unicode_names = true unless Helper.os_windows? - Zip::File.open(backup_dir.join(zipfilename), Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(backup_dir.join(zipfilename), create: true) do |zip| paths.each do |path| relative_path = path_to_relative(novel_dir, path).to_s zipped_filename = diff --git a/lib/command/update.rb b/lib/command/update.rb index 44b53896..2d3ebdc8 100644 --- a/lib/command/update.rb +++ b/lib/command/update.rb @@ -210,7 +210,19 @@ def execute(argv) end convert_argv = [target] convert_argv << "--no-open" if @options["no-open"] + + # WebUI: 変換メッセージを変換コンソールに出力するため、一時的に$stdout2を切り替え + original_stdout2 = $stdout2 + if $stdout2.respond_to?(:push_server) && $stdout2.respond_to?(:target_console) + # WebUIの場合、変換用の別コンソールに出力 + $stdout2 = Narou::StreamingLogger.new($stdout2.push_server, $stdout2, target_console: "convert") + end + convert_status = Convert.execute!(convert_argv) + + # $stdout2を元に戻す + $stdout2 = original_stdout2 if original_stdout2 + if convert_status > 0 # 変換が失敗したか、中断された data["_convert_failure"] = true diff --git a/lib/command/web.rb b/lib/command/web.rb index 9bac54dd..42f7a785 100644 --- a/lib/command/web.rb +++ b/lib/command/web.rb @@ -180,12 +180,12 @@ def open_browser_when_server_boot(address) def send_rebooted_event_when_connection_recover(push_server) return unless @rebooted - Thread.new do |th| + Thread.new do timeout = Time.now + 20 # WebSocketのコネクションが回復するまで待つ until push_server.connections.count != 0 sleep 0.2 - th.kill if Time.now > timeout + Thread.current.kill if Time.now > timeout end puts "再起動が完了しました。".termcolor push_server.send_all(:"server.rebooted") diff --git a/lib/device/ibunko.rb b/lib/device/ibunko.rb index 2c51ba6e..ea4eee89 100644 --- a/lib/device/ibunko.rb +++ b/lib/device/ibunko.rb @@ -53,7 +53,12 @@ def create_pure_aozora_zip zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext) File.delete(zipfile_path) if File.exist?(zipfile_path) - Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(zipfile_path, create: true) do |zip| # テキスト本体(整形済み) zip.add(File.basename(@converted_txt_path), sanitized_txt_path) { true } # 挿絵(Aozora注記のまま。画像ファイルは同梱) @@ -72,7 +77,7 @@ def create_pure_aozora_zip end end FileUtils.rm_f(sanitized_txt_path) - output_io = (respond_to?(:stream_io) ? stream_io : nil) || $stdout + output_io = $stdout2 output_io.puts File.basename(zipfile_path) + " を出力しました" output_io.puts "#{@device.display_name}用ファイルを出力しました".termcolor if Narou.economy?("cleanup_temp") && @argument_target_type == :novel @@ -98,7 +103,12 @@ def hook_convert_txt_to_ebook_file(&original_func) translate_illust_chuki_to_img_tag zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext) File.delete(zipfile_path) if File.exist?(zipfile_path) - Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(zipfile_path, create: true) do |zip| # テキスト本体 zip.add(File.basename(@converted_txt_path), @converted_txt_path) { true } # 挿絵 @@ -116,7 +126,7 @@ def hook_convert_txt_to_ebook_file(&original_func) zip.add(cover_name, File.join(dirpath, cover_name)) { true } end end - output_io = (respond_to?(:stream_io) ? stream_io : nil) || $stdout + output_io = $stdout2 output_io.puts File.basename(zipfile_path) + " を出力しました" output_io.puts "#{@device.display_name}用ファイルを出力しました".termcolor if Narou.economy?("cleanup_temp") && @argument_target_type == :novel diff --git a/lib/downloader.rb b/lib/downloader.rb index 9fb28831..909dcc4b 100644 --- a/lib/downloader.rb +++ b/lib/downloader.rb @@ -755,6 +755,7 @@ def get_toc_source toc_url = @setting["toc_url"] return nil unless toc_url max_retry = 5 + retry_count = LIMIT_TO_RETRY_NETWORK toc_source = "" cookie = @setting["cookie"] || "" open_uri_options = make_open_uri_options("Cookie" => cookie, allow_redirections: :safe) @@ -787,6 +788,28 @@ def get_toc_source else raise end + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e + case e.message + when /^503/ + @stream&.error "server message: #{e.message}" + display_hint if @stream + raise SuspendDownload + when /^404/ + # 404は上位のget_latest_table_of_contentsで処理させるため、そのまま再raise + raise e + else + if retry_count == 0 + @stream&.error "上限までリトライしましたが目次がダウンロード出来ませんでした" + raise SuspendDownload + end + retry_count -= 1 + @stream&.puts <<~MSG + server message: #{e.message} + リトライ待機中... + MSG + sleep(WAIT_TIME_TO_RETRY_NETWORK) + retry + end end toc_source end @@ -834,7 +857,7 @@ def get_latest_table_of_contents(old_toc, through_error: false) "subtitles" => subtitles } toc_objects - rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError => e + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e raise if through_error # エラー処理はしなくていいからそのまま例外を受け取りたい時用 if e.message.include?("404") @stream.error "小説が削除されているか非公開な可能性があります" @@ -1235,7 +1258,7 @@ def download_raw_data(url) URI.open(url, "r:#{@setting["encoding"]}", open_uri_options) do |fp| raw = Helper.pretreatment_source(fp.read, @setting["encoding"]) end - rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError => e + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e case e.message when /^503/ # 503 はアクセス規制やメンテ等でリトライしてもほぼ意味がないことが多いため一度で諦める diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index 51d7183a..3ce386f1 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -292,6 +292,10 @@ def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) end entries[opf_name] = content.b + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + # 再Zip化 (mimetypeは無圧縮で先頭) File.delete(epub_path) Zip::OutputStream.open(epub_path) do |zos| @@ -315,7 +319,7 @@ def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) stream_io.puts "dc:subjectを追加しました: #{subjects.join(', ')}" :success rescue => e - stream_io.error "dc:subject追加中にエラーが発生しました: #{e.message}" + stream_io.error "dc:subject追加中にエラーが発生しました: #{e.class} - #{e.message}" :error end end From 2f2234f2c7c2641542f91fed85b3d03958ce90b0 Mon Sep 17 00:00:00 2001 From: rumia Date: Wed, 29 Oct 2025 00:40:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E8=A6=81=E7=B4=A0=E3=81=AE=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=99=82=E3=81=AB=E8=A1=8C=E9=81=B8=E6=8A=9E?= =?UTF-8?q?=E3=82=92=E7=84=A1=E5=8A=B9=E5=8C=96=E3=81=99=E3=82=8B=E3=82=A4?= =?UTF-8?q?=E3=83=99=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E9=96=A2=E9=80=A3=E3=81=99=E3=82=8B=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/web/public/resources/js/narou.library.js | 28 ++++++++++++-------- lib/web/public/resources/js/narou.ui.js | 11 +++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/web/public/resources/js/narou.library.js b/lib/web/public/resources/js/narou.library.js index e7a70e3e..778d8196 100644 --- a/lib/web/public/resources/js/narou.library.js +++ b/lib/web/public/resources/js/narou.library.js @@ -1902,6 +1902,23 @@ var Narou = (function () { return matched; }); + // 通常単語を追加するためのイベント(テーブル tbody に直接登録) + // サーバーサイド/クライアントサイド両方で必要 + $("#novel-list tbody").on("click", "[data-add-filter]", function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + self.appendWordToFilter($(this).data("addFilter")); + return false; + }); + // 追加イベントが発生した時に行選択イベントが起こらないように + $("#novel-list tbody").on("mousedown", "[data-add-filter]", function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; + }); + // サーバーサイド処理の場合はカスタムフィルタボックスのイベントを登録しない // (narou.ui.jsで登録されるハンドラを使用) if (this.table.settings()[0].oFeatures.bServerSide) { @@ -1931,17 +1948,6 @@ var Narou = (function () { self.search(); } }); - - this.table - // 通常単語を追加するためのイベント - .on("click", "[data-add-filter]", function (e) { - e.stopPropagation(); - self.appendWordToFilter($(this).data("addFilter")); - }) - // 追加イベントが発生した時に行選択イベントが起こらないように - .on("mousedown", "[data-add-filter]", function (e) { - e.stopPropagation(); - }); }, // フィルタにタグを追加 diff --git a/lib/web/public/resources/js/narou.ui.js b/lib/web/public/resources/js/narou.ui.js index 59824556..9c1a6e9d 100644 --- a/lib/web/public/resources/js/narou.ui.js +++ b/lib/web/public/resources/js/narou.ui.js @@ -1284,7 +1284,12 @@ $(function() { // ここだけはスマフォ系でも touchstart ではなく click で。 // touchstart だと画面スクロールのためのスワイプでも反応してしまう table - .on("click", "tr", function () { + .on("click", "tr", function (e) { + // data-add-filter要素がクリックされた場合は何もしない + if ($(e.target).closest('[data-add-filter]').length > 0) { + return; + } + // DOM の選択状態を変更 $(this).toggleClass("selected"); @@ -1310,6 +1315,10 @@ $(function() { // リンクをクリックした時は選択処理は行わない e.stopPropagation(); }) + .on("click", "[data-add-filter]", function(e) { + // フィルタ追加要素をクリックした時は選択処理は行わない + e.stopPropagation(); + }) $("#novel-list tbody").css("cursor", "pointer"); } From dba441e8823c910f965ac1907e375e781e029a5d Mon Sep 17 00:00:00 2001 From: rumia Date: Wed, 29 Oct 2025 01:56:40 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E3=82=BF=E3=82=B0=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=99=82=E3=81=A8=E3=83=9E=E3=82=A6=E3=82=B9?= =?UTF-8?q?=E3=83=80=E3=82=A6=E3=83=B3=E6=99=82=E3=81=AB=E3=83=86=E3=82=AD?= =?UTF-8?q?=E3=82=B9=E3=83=88=E9=81=B8=E6=8A=9E=E3=82=92=E9=98=B2=E3=81=90?= =?UTF-8?q?=E3=81=9F=E3=82=81=E3=81=AE=E3=82=A4=E3=83=99=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/web/public/resources/js/narou.library.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/web/public/resources/js/narou.library.js b/lib/web/public/resources/js/narou.library.js index 778d8196..2854faa5 100644 --- a/lib/web/public/resources/js/narou.library.js +++ b/lib/web/public/resources/js/narou.library.js @@ -2113,6 +2113,7 @@ var Narou = (function () { $target .on("click", ".tag", args, function (e) { if (e.data.stop_bubbling) e.stopPropagation(); + e.preventDefault(); // テキスト選択を防ぐ var tag_name = String($(this).data("tag")); self.search.appendTagToFilter(tag_name, e.altKey); self.table.$("[data-toggle=tooltip]").tooltip("hide"); @@ -2120,6 +2121,7 @@ var Narou = (function () { .on("mousedown", ".tag", args, function (e) { // 範囲選択モードでもクリック出来るように if (e.data.stop_bubbling) e.stopPropagation(); + e.preventDefault(); // テキスト選択を防ぐ }); },