shoko 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.
Files changed (294) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +4 -0
  3. data/.bundle/config.bak +3 -0
  4. data/.rspec_status +42 -0
  5. data/.rubocop.yml +124 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE +21 -0
  8. data/README.md +82 -0
  9. data/Rakefile +29 -0
  10. data/bin/start +15 -0
  11. data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
  12. data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
  13. data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
  14. data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
  15. data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
  16. data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
  17. data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
  18. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
  19. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
  20. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
  21. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
  22. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
  23. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
  24. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
  25. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
  26. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
  27. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
  28. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
  29. data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
  30. data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
  31. data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
  32. data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
  33. data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
  34. data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
  35. data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
  36. data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
  37. data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
  38. data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
  39. data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
  40. data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
  41. data/lib/shoko/adapters/input/command_bridge.rb +148 -0
  42. data/lib/shoko/adapters/input/command_factory.rb +255 -0
  43. data/lib/shoko/adapters/input/commands.rb +60 -0
  44. data/lib/shoko/adapters/input/dispatcher.rb +69 -0
  45. data/lib/shoko/adapters/input/input_controller.rb +250 -0
  46. data/lib/shoko/adapters/input/key_definitions.rb +108 -0
  47. data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
  48. data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
  49. data/lib/shoko/adapters/monitoring/logger.rb +150 -0
  50. data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
  51. data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
  52. data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
  53. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
  54. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
  55. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
  56. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
  57. data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
  58. data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
  59. data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
  60. data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
  61. data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
  62. data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
  63. data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
  64. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
  65. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
  66. data/lib/shoko/adapters/output/notification_service.rb +58 -0
  67. data/lib/shoko/adapters/output/render_registry.rb +45 -0
  68. data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
  69. data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
  70. data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
  71. data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
  72. data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
  73. data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
  74. data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
  75. data/lib/shoko/adapters/output/terminal/input.rb +161 -0
  76. data/lib/shoko/adapters/output/terminal/output.rb +105 -0
  77. data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
  78. data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
  79. data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
  80. data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
  81. data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
  82. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
  83. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
  84. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
  85. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
  86. data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
  87. data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
  88. data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
  89. data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
  90. data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
  91. data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
  92. data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
  93. data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
  94. data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
  95. data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
  96. data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
  97. data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
  98. data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
  99. data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
  100. data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
  101. data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
  102. data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
  103. data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
  104. data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
  105. data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
  106. data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
  107. data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
  108. data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
  109. data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
  110. data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
  111. data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
  112. data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
  113. data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
  114. data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
  115. data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
  116. data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
  117. data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
  118. data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
  119. data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
  120. data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
  121. data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
  122. data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
  123. data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
  124. data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
  125. data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
  126. data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
  127. data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
  128. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
  129. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
  130. data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
  131. data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
  132. data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
  133. data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
  134. data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
  135. data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
  136. data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
  137. data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
  138. data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
  139. data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
  140. data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
  141. data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
  142. data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
  143. data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
  144. data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
  145. data/lib/shoko/adapters/storage/background_worker.rb +66 -0
  146. data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
  147. data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
  148. data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
  149. data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
  150. data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
  151. data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
  152. data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
  153. data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
  154. data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
  155. data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
  156. data/lib/shoko/adapters/storage/config_paths.rb +30 -0
  157. data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
  158. data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
  159. data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
  160. data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
  161. data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
  162. data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
  163. data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
  164. data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
  165. data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
  166. data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
  167. data/lib/shoko/adapters/storage/recent_files.rb +78 -0
  168. data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
  169. data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
  170. data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
  171. data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
  172. data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
  173. data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
  174. data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
  175. data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
  176. data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
  177. data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
  178. data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
  179. data/lib/shoko/application/cli.rb +134 -0
  180. data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
  181. data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
  182. data/lib/shoko/application/controllers/menu_controller.rb +469 -0
  183. data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
  184. data/lib/shoko/application/controllers/reader_controller.rb +449 -0
  185. data/lib/shoko/application/controllers/state_controller.rb +410 -0
  186. data/lib/shoko/application/controllers/ui_controller.rb +782 -0
  187. data/lib/shoko/application/dependency_container.rb +301 -0
  188. data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
  189. data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
  190. data/lib/shoko/application/infrastructure/state_store.rb +413 -0
  191. data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
  192. data/lib/shoko/application/pending_jump_handler.rb +122 -0
  193. data/lib/shoko/application/reader_lifecycle.rb +65 -0
  194. data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
  195. data/lib/shoko/application/selectors/config_selectors.rb +62 -0
  196. data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
  197. data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
  198. data/lib/shoko/application/state/actions/base_action.rb +24 -0
  199. data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
  200. data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
  201. data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
  202. data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
  203. data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
  204. data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
  205. data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
  206. data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
  207. data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
  208. data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
  209. data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
  210. data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
  211. data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
  212. data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
  213. data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
  214. data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
  215. data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
  216. data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
  217. data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
  218. data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
  219. data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
  220. data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
  221. data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
  222. data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
  223. data/lib/shoko/application/unified_application.rb +48 -0
  224. data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
  225. data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
  226. data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
  227. data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
  228. data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
  229. data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
  230. data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
  231. data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
  232. data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
  233. data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
  234. data/lib/shoko/application/use_cases/settings_service.rb +123 -0
  235. data/lib/shoko/core/events/annotation_events.rb +94 -0
  236. data/lib/shoko/core/events/base_domain_event.rb +169 -0
  237. data/lib/shoko/core/events/bookmark_events.rb +41 -0
  238. data/lib/shoko/core/events/domain_event_bus.rb +163 -0
  239. data/lib/shoko/core/events/progress_events.rb +108 -0
  240. data/lib/shoko/core/models/bookmark.rb +36 -0
  241. data/lib/shoko/core/models/bookmark_data.rb +10 -0
  242. data/lib/shoko/core/models/chapter.rb +25 -0
  243. data/lib/shoko/core/models/content_block.rb +44 -0
  244. data/lib/shoko/core/models/reader_settings.rb +20 -0
  245. data/lib/shoko/core/models/selection_anchor.rb +73 -0
  246. data/lib/shoko/core/models/toc_entry.rb +14 -0
  247. data/lib/shoko/core/ports/annotation_repository.rb +0 -0
  248. data/lib/shoko/core/ports/book_repository.rb +0 -0
  249. data/lib/shoko/core/ports/book_source.rb +0 -0
  250. data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
  251. data/lib/shoko/core/ports/cache.rb +0 -0
  252. data/lib/shoko/core/ports/input_handler.rb +0 -0
  253. data/lib/shoko/core/ports/renderer.rb +0 -0
  254. data/lib/shoko/core/ports/storage.rb +0 -0
  255. data/lib/shoko/core/services/annotation_service.rb +102 -0
  256. data/lib/shoko/core/services/base_service.rb +60 -0
  257. data/lib/shoko/core/services/bookmark_service.rb +267 -0
  258. data/lib/shoko/core/services/coordinate_service.rb +265 -0
  259. data/lib/shoko/core/services/layout_service.rb +95 -0
  260. data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
  261. data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
  262. data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
  263. data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
  264. data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
  265. data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
  266. data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
  267. data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
  268. data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
  269. data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
  270. data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
  271. data/lib/shoko/core/services/navigation_service.rb +150 -0
  272. data/lib/shoko/core/services/page_calculator_service.rb +242 -0
  273. data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
  274. data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
  275. data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
  276. data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
  277. data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
  278. data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
  279. data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
  280. data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
  281. data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
  282. data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
  283. data/lib/shoko/core/services/pagination.rb +10 -0
  284. data/lib/shoko/core/services/progress_helper.rb +22 -0
  285. data/lib/shoko/core/services/selection_service.rb +126 -0
  286. data/lib/shoko/core/validator.rb +76 -0
  287. data/lib/shoko/shared/errors.rb +97 -0
  288. data/lib/shoko/shared/version.rb +5 -0
  289. data/lib/shoko/test_support/terminal_double.rb +175 -0
  290. data/lib/shoko/test_support/test_mode.rb +78 -0
  291. data/lib/shoko.rb +279 -0
  292. data/lib/zip.rb +732 -0
  293. data/zip.rb +5 -0
  294. metadata +370 -0
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../monitoring/logger.rb'
4
+ require_relative '../monitoring/performance_monitor.rb'
5
+ require_relative '../monitoring/perf_tracer.rb'
6
+ require_relative '../storage/book_cache_pipeline.rb'
7
+ require_relative 'epub/parsers/html_processor.rb'
8
+ require_relative '../output/terminal/terminal_sanitizer.rb'
9
+ require_relative '../../core/models/chapter.rb'
10
+ require_relative '../../core/models/toc_entry.rb'
11
+
12
+ module Shoko
13
+ module Adapters::BookSources
14
+ # Represents an EPUB document backed by the cache pipeline.
15
+ # The document always operates on in-memory chapter objects; no temporary
16
+ # extraction to disk is required.
17
+ class EPUBDocument
18
+ attr_reader :title, :chapters, :language, :source_path,
19
+ :cache_path, :cache_sha, :toc_entries, :metadata, :resources
20
+
21
+ def initialize(path, formatting_service: nil, background_worker: nil, progress_reporter: nil)
22
+ @open_path = File.expand_path(path)
23
+ @formatting_service = formatting_service
24
+ @background_worker = background_worker
25
+ @progress_reporter = progress_reporter
26
+ @formatting_pending = {}
27
+ @formatting_pending_mutex = Mutex.new
28
+
29
+ @title = fallback_title(@open_path)
30
+ @language = 'en_US'
31
+ @chapters = []
32
+ @toc_entries = []
33
+ @metadata = {}
34
+ @resources = {}
35
+ @chapter_hrefs = []
36
+ @spine_relative_paths = []
37
+ @opf_path = nil
38
+ @cache_path = nil
39
+ @source_path = @open_path
40
+ @loaded_from_cache = false
41
+ @book_payload = nil
42
+
43
+ load_via_pipeline!
44
+ rescue Shoko::Error => e
45
+ create_error_chapter(e)
46
+ rescue StandardError => e
47
+ Adapters::Monitoring::Logger.error('EPUBDocument initialization failed', path: @open_path, error: e.message)
48
+ create_error_chapter(e)
49
+ end
50
+
51
+ def chapter_count
52
+ @chapters.size
53
+ end
54
+
55
+ def get_chapter(index)
56
+ return nil unless index.is_a?(Integer) && index >= 0 && index < @chapters.length
57
+
58
+ chapter = @chapters[index]
59
+ return nil unless chapter
60
+
61
+ ensure_formatted_chapter(chapter, index)
62
+ chapter.lines = fallback_plain_lines(chapter.raw_content) if chapter.lines.nil? || chapter.lines.empty?
63
+ chapter
64
+ end
65
+
66
+ def cached?
67
+ @loaded_from_cache
68
+ end
69
+
70
+ def canonical_path
71
+ @source_path || @open_path
72
+ end
73
+
74
+ # Backwards compatibility for components that previously expected a cache
75
+ # directory. Returns the directory containing the `.cache` file.
76
+ def cache_dir
77
+ return nil unless @cache_path
78
+
79
+ File.dirname(@cache_path)
80
+ end
81
+
82
+ private
83
+
84
+ def load_via_pipeline!
85
+ result = Adapters::Monitoring::PerformanceMonitor.time('import.pipeline') do
86
+ Adapters::Monitoring::PerfTracer.measure('cache.pipeline') do
87
+ Adapters::Storage::BookCachePipeline.new(progress_reporter: @progress_reporter)
88
+ .load(@open_path, formatting_service: @formatting_service)
89
+ end
90
+ end
91
+
92
+ apply_pipeline_result(result)
93
+ end
94
+
95
+ def apply_pipeline_result(result)
96
+ book = result.book
97
+ if defined?(Adapters::Monitoring::PerfTracer)
98
+ Adapters::Monitoring::PerfTracer.annotate(
99
+ cache_hit: result.loaded_from_cache,
100
+ chapters: Array(book&.chapters).size,
101
+ book: result.source_path || @open_path
102
+ )
103
+ end
104
+ @cache_path = result.cache_path
105
+ @cache_sha = derive_cache_sha(@cache_path)
106
+ @source_path = result.source_path || @open_path
107
+ @loaded_from_cache = result.loaded_from_cache
108
+ @book_payload = result.payload
109
+
110
+ @title = present_or_fallback(book.title, fallback_title(@source_path))
111
+ @language = book.language || @language
112
+ @metadata = book.metadata || {}
113
+ @chapters = Array(book.chapters).dup
114
+ @toc_entries = Array(book.toc_entries).dup
115
+ @resources = (book.resources || {}).dup
116
+ @chapter_hrefs = Array(book.chapter_hrefs).dup
117
+ @spine_relative_paths = Array(book.spine).dup
118
+ @opf_path = book.opf_path
119
+
120
+ ensure_chapters_exist
121
+ end
122
+
123
+ def derive_cache_sha(path)
124
+ return nil unless path && !path.to_s.empty?
125
+
126
+ File.basename(path.to_s, File.extname(path.to_s))
127
+ rescue StandardError
128
+ nil
129
+ end
130
+
131
+ def present_or_fallback(value, fallback)
132
+ str = value.to_s.strip
133
+ str.empty? ? fallback : value
134
+ end
135
+
136
+ def ensure_chapters_exist
137
+ return unless @chapters.empty?
138
+
139
+ @chapters << Core::Models::Chapter.new(
140
+ number: '1',
141
+ title: 'Empty Book',
142
+ lines: ['This EPUB appears to be empty.'],
143
+ metadata: nil,
144
+ blocks: nil,
145
+ raw_content: nil
146
+ )
147
+ @toc_entries = []
148
+ end
149
+
150
+ def create_error_chapter(error)
151
+ @chapters = [
152
+ Core::Models::Chapter.new(
153
+ number: '1',
154
+ title: 'Error Loading',
155
+ lines: ["Error: #{error.message}"],
156
+ metadata: nil,
157
+ blocks: nil,
158
+ raw_content: nil
159
+ ),
160
+ ]
161
+ @toc_entries = []
162
+ end
163
+
164
+ def ensure_formatted_chapter(chapter, index)
165
+ return unless @formatting_service && chapter
166
+
167
+ chapter_index = index.to_i
168
+ return if chapter.blocks && !chapter.blocks.empty?
169
+
170
+ # Always format synchronously so rendering/pagination receives structured lines immediately.
171
+ format_chapter_sync(chapter_index, chapter, raise_on_error: true)
172
+ end
173
+
174
+ def fallback_plain_lines(content)
175
+ return [] unless content
176
+
177
+ text = Adapters::BookSources::Epub::Parsers::HTMLProcessor.html_to_text(content.to_s)
178
+ text.split("\n").map(&:rstrip)
179
+ end
180
+
181
+ def fallback_title(path)
182
+ raw = File.basename(path, File.extname(path)).tr('_', ' ')
183
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(raw, preserve_newlines: false, preserve_tabs: false)
184
+ end
185
+
186
+ def enqueue_async_formatting(index, chapter)
187
+ already_enqueued = false
188
+ @formatting_pending_mutex.synchronize do
189
+ already_enqueued = @formatting_pending[index]
190
+ @formatting_pending[index] = true unless already_enqueued
191
+ end
192
+ return if already_enqueued
193
+
194
+ @background_worker.submit do
195
+ format_chapter_sync(index, chapter, raise_on_error: false)
196
+ ensure
197
+ @formatting_pending_mutex.synchronize do
198
+ @formatting_pending.delete(index)
199
+ end
200
+ end
201
+ rescue StandardError => e
202
+ Adapters::Monitoring::Logger.debug('Async formatting enqueue failed', error: e.message)
203
+ end
204
+
205
+ def format_chapter_sync(index, chapter, raise_on_error:)
206
+ Adapters::Monitoring::PerfTracer.measure('formatting.ensure') do
207
+ @formatting_service.ensure_formatted!(self, index, chapter)
208
+ end
209
+ rescue Shoko::FormattingError => e
210
+ Adapters::Monitoring::Logger.error('Formatting error', error: e.message, chapter: index + 1)
211
+ raise if raise_on_error
212
+ rescue StandardError => e
213
+ Adapters::Monitoring::Logger.debug('Formatting service failed', error: e.message, chapter: index + 1)
214
+ nil
215
+ end
216
+
217
+ def assign_toc_entries(entries)
218
+ href_to_index = {}
219
+ Array(@chapter_hrefs).each_with_index do |href, idx|
220
+ href_to_index[href] = idx if href
221
+ end
222
+
223
+ @toc_entries = Array(entries).map do |entry|
224
+ title = entry[:title] || entry['title']
225
+ href = entry[:href] || entry['href']
226
+ level = (entry[:level] || entry['level']).to_i
227
+ resolved = normalize_toc_href(href)
228
+ chapter_index = href_to_index[resolved]
229
+
230
+ if chapter_index && (chapter = @chapters[chapter_index]) && chapter.respond_to?(:title=) && (chapter.title.nil? || chapter.title.to_s.strip.empty?)
231
+ chapter.title = title
232
+ end
233
+
234
+ Core::Models::TOCEntry.new(
235
+ title: title,
236
+ href: href,
237
+ level: level,
238
+ chapter_index: chapter_index,
239
+ navigable: !chapter_index.nil?
240
+ )
241
+ end
242
+ end
243
+
244
+ def normalize_toc_href(href)
245
+ return nil unless href
246
+
247
+ base = @opf_path ? File.dirname(@opf_path) : '.'
248
+ core = href.to_s.split('#', 2).first
249
+ File.expand_path(File.join('/', base, core), '/').sub(%r{^/}, '')
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative '../../output/terminal/terminal_sanitizer.rb'
6
+ require_relative '../../storage/config_paths.rb'
7
+
8
+ module Shoko
9
+ module Adapters::BookSources
10
+ class EPUBFinder
11
+ # Scans directories to locate EPUB files
12
+ class DirectoryScanner
13
+ def initialize(context)
14
+ @context = context
15
+ end
16
+
17
+ def scan_all_directories
18
+ all_dirs = build_directory_list
19
+ warn_debug "Scanning directories: #{all_dirs.join(', ')}"
20
+
21
+ all_dirs.each do |dir|
22
+ break if @context.epubs.length >= EPUBFinder::MAX_FILES
23
+
24
+ warn_debug "Scanning: #{dir}"
25
+ scan_directory(dir)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def build_directory_list
32
+ directories = priority_directories + other_directories
33
+ directories.uniq.select { |dir| safe_directory_exists?(dir) }
34
+ end
35
+
36
+ def priority_directories
37
+ [
38
+ Adapters::Storage::ConfigPaths.downloads_root,
39
+ '~/Books',
40
+ '~/Bücher', # German books directory
41
+ '~/Documents/Books',
42
+ '~/Downloads',
43
+ '~/Desktop',
44
+ '~/Documents',
45
+ '~/Library/Mobile Documents',
46
+ ].map { |dir| File.expand_path(dir) }
47
+ end
48
+
49
+ def other_directories
50
+ [
51
+ '~',
52
+ '~/Dropbox',
53
+ '~/Google Drive',
54
+ '~/OneDrive',
55
+ ].map { |dir| File.expand_path(dir) }
56
+ end
57
+
58
+ def safe_directory_exists?(dir)
59
+ Dir.exist?(dir)
60
+ rescue StandardError
61
+ false
62
+ end
63
+
64
+ def scan_directory(dir)
65
+ return unless @context.can_scan?(dir, EPUBFinder::MAX_DEPTH, EPUBFinder::MAX_FILES)
66
+
67
+ @context.mark_visited(dir)
68
+ process_entries(dir)
69
+ rescue Errno::EACCES, Errno::ENOENT, Errno::EPERM
70
+ # Skip directories we can't access
71
+ rescue StandardError => e
72
+ warn_debug "Error scanning #{dir}: #{e.message}"
73
+ end
74
+
75
+ def process_entries(dir)
76
+ Dir.entries(dir).each do |entry|
77
+ next if entry.start_with?('.')
78
+
79
+ path = File.join(dir, entry)
80
+ next if @context.visited_paths.include?(path)
81
+
82
+ process_path(path)
83
+ end
84
+ end
85
+
86
+ def process_path(path)
87
+ if File.directory?(path)
88
+ process_directory(path)
89
+ elsif epub_file?(path)
90
+ add_epub(path)
91
+ end
92
+ rescue StandardError
93
+ # Skip items we can't process
94
+ end
95
+
96
+ def process_directory(path)
97
+ return if skip_directory?(path)
98
+
99
+ DirectoryScanner.new(@context.with_deeper_depth).scan_directory(path)
100
+ end
101
+
102
+ def skip_directory?(path)
103
+ base = File.basename(path).downcase
104
+ EPUBFinder::SKIP_DIRS.map(&:downcase).include?(base)
105
+ end
106
+
107
+ def epub_file?(path)
108
+ path.downcase.end_with?('.epub') &&
109
+ File.readable?(path) &&
110
+ File.size(path).positive?
111
+ end
112
+
113
+ def add_epub(path)
114
+ raw_name = File.basename(path, '.epub').gsub(/[_-]/, ' ')
115
+ display_name = Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(raw_name, preserve_newlines: false, preserve_tabs: false)
116
+
117
+ @context.epubs << {
118
+ 'path' => path,
119
+ 'name' => display_name,
120
+ 'size' => File.size(path),
121
+ 'modified' => File.mtime(path).iso8601,
122
+ 'dir' => File.dirname(path),
123
+ }
124
+ end
125
+
126
+ def debug? = EPUBFinder::DEBUG_MODE
127
+
128
+ def warn_debug(msg)
129
+ warn msg if debug?
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::BookSources
5
+ class EPUBFinder
6
+ # Context for directory scanning operations
7
+ ScannerContext = Struct.new(:epubs, :visited_paths, :depth, keyword_init: true) do
8
+ def can_scan?(dir, max_depth, max_files)
9
+ depth <= max_depth &&
10
+ epubs.length < max_files &&
11
+ !visited_paths.include?(dir)
12
+ end
13
+
14
+ def mark_visited(dir)
15
+ visited_paths.add(dir)
16
+ end
17
+
18
+ def with_deeper_depth
19
+ self.class.new(
20
+ epubs: epubs,
21
+ visited_paths: visited_paths,
22
+ depth: depth + 1
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'set'
6
+ require 'time'
7
+ require 'timeout'
8
+
9
+ require_relative 'epub_finder/scanner_context'
10
+ require_relative 'epub_finder/directory_scanner'
11
+ require_relative '../storage/atomic_file_writer.rb'
12
+ require_relative '../storage/config_paths.rb'
13
+
14
+ module Shoko
15
+ module Adapters::BookSources
16
+ # EPUB file finder with robust error handling
17
+ class EPUBFinder
18
+ SCAN_TIMEOUT = 20
19
+ MAX_DEPTH = 3
20
+ MAX_FILES = 500
21
+ CACHE_DURATION = 86_400
22
+ CONFIG_DIR = Adapters::Storage::ConfigPaths.config_root
23
+ CACHE_FILE = File.join(CONFIG_DIR, 'epub_cache.json')
24
+ DEBUG_MODE = ARGV.include?('--debug') || ENV['DEBUG']
25
+ SKIP_DIRS = %w[
26
+ node_modules vendor cache tmp temp .git .svn
27
+ __pycache__ build dist bin obj debug release
28
+ .idea .vscode .atom .sublime library frameworks
29
+ applications system windows programdata appdata
30
+ .Trash .npm .gem .bundle .cargo .rustup .cache
31
+ .local .config backup backups old archive
32
+ ].freeze
33
+
34
+ class << self
35
+ def scan_system(force_refresh: false)
36
+ cache = load_cache
37
+ return cache['files'] if !force_refresh && cache_valid?(cache)
38
+
39
+ scan_with_timeout
40
+ end
41
+
42
+ def clear_cache
43
+ FileUtils.rm_f(CACHE_FILE)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+
48
+ private
49
+
50
+ def cached_files
51
+ cache = load_cache
52
+ return [] unless cache_valid?(cache)
53
+
54
+ cache['files']
55
+ end
56
+
57
+ def cache_valid?(cache)
58
+ return false unless cache.is_a?(Hash)
59
+
60
+ files = cache['files']
61
+ ts = cache['timestamp']
62
+ files.is_a?(Array) && ts && !cache_expired?(ts)
63
+ end
64
+
65
+ def cache_expired?(timestamp = nil)
66
+ ts = timestamp.to_s.strip
67
+ return true if ts.empty?
68
+
69
+ Time.now - Time.parse(ts) >= CACHE_DURATION
70
+ rescue StandardError
71
+ true
72
+ end
73
+
74
+ def scan_with_timeout
75
+ epubs = []
76
+ epubs = perform_scan_with_timeout
77
+ rescue Timeout::Error
78
+ handle_timeout_error(epubs)
79
+ rescue StandardError
80
+ epubs = cached_files_fallback
81
+ ensure
82
+ save_and_return_epubs(epubs)
83
+ end
84
+
85
+ def perform_scan_with_timeout
86
+ Timeout.timeout(SCAN_TIMEOUT) { perform_scan }
87
+ end
88
+
89
+ def handle_timeout_error(epubs)
90
+ save_cache(epubs) unless epubs.empty?
91
+ epubs
92
+ end
93
+
94
+ def save_and_return_epubs(epubs)
95
+ save_cache(epubs)
96
+ epubs
97
+ end
98
+
99
+ def cached_files_fallback
100
+ cache = load_cache
101
+ cache && cache['files'] ? cache['files'] : []
102
+ end
103
+
104
+ def perform_scan
105
+ epubs = []
106
+ context = ScannerContext.new(
107
+ epubs: epubs,
108
+ visited_paths: Set.new,
109
+ depth: 0
110
+ )
111
+
112
+ scanner = DirectoryScanner.new(context)
113
+ scanner.scan_all_directories
114
+
115
+ warn_debug "Found #{epubs.length} EPUB files"
116
+ epubs
117
+ end
118
+
119
+ def load_cache
120
+ return nil unless File.exist?(CACHE_FILE)
121
+
122
+ parse_cache_file(CACHE_FILE)
123
+ rescue StandardError => e
124
+ warn_debug "Cache load error: #{e.message}"
125
+ delete_cache_file(CACHE_FILE)
126
+ nil
127
+ end
128
+
129
+ def parse_cache_file(path)
130
+ data = File.read(path)
131
+ json = JSON.parse(data)
132
+ json if json.is_a?(Hash)
133
+ end
134
+
135
+ def delete_cache_file(path)
136
+ File.delete(path)
137
+ rescue StandardError
138
+ nil
139
+ end
140
+
141
+ def save_cache(files)
142
+ FileUtils.mkdir_p(File.dirname(CACHE_FILE))
143
+ payload = JSON.pretty_generate({
144
+ 'timestamp' => Time.now.iso8601,
145
+ 'files' => files || [],
146
+ 'version' => VERSION,
147
+ })
148
+ Adapters::Storage::AtomicFileWriter.write(CACHE_FILE, payload)
149
+ rescue StandardError => e
150
+ warn_debug "Cache save error: #{e.message}"
151
+ end
152
+
153
+ def debug? = !!DEBUG_MODE
154
+
155
+ def warn_debug(msg)
156
+ warn msg if debug?
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end