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,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ # In-memory caching helpers for loaded payloads + layouts.
6
+ class EpubCache
7
+ private
8
+
9
+ def load_payload
10
+ return @payload_cache if @payload_cache
11
+
12
+ ensure_sha!
13
+ cache_payload(load_payload_from_store(@source_sha))
14
+ end
15
+
16
+ def cache_payload(payload)
17
+ return nil unless payload
18
+
19
+ @payload_cache = payload
20
+ @layout_cache = normalize_layout_cache(payload.layouts)
21
+ refresh_payload_layouts!
22
+ payload
23
+ end
24
+
25
+ def load_payload_from_store(sha)
26
+ raw = fetch_raw_payload(sha)
27
+ return nil unless raw
28
+
29
+ ensure_pointer_from_metadata(raw.metadata_row)
30
+ Serializer.build_payload_from_store(raw, cache_root: @cache_root, book_sha: sha)
31
+ rescue StandardError => e
32
+ Shoko::Adapters::Monitoring::Logger.debug('EpubCache: failed to load cache', sha: sha.to_s, error: e.message)
33
+ nil
34
+ end
35
+
36
+ def fetch_raw_payload(sha)
37
+ return nil unless sha
38
+
39
+ @cache_store.fetch_payload(sha)
40
+ end
41
+
42
+ def cache_layout!(key, payload)
43
+ @layout_cache ||= {}
44
+ @layout_cache[key] = deep_dup(payload)
45
+ return unless @payload_cache
46
+
47
+ @payload_cache.layouts ||= {}
48
+ @payload_cache.layouts[key] = deep_dup(payload)
49
+ end
50
+
51
+ def update_layout_cache_from_layouts(layouts)
52
+ @layout_cache = normalize_layout_cache(layouts)
53
+ refresh_payload_layouts!
54
+ end
55
+
56
+ def refresh_payload_layouts!
57
+ return unless @payload_cache
58
+
59
+ @payload_cache.layouts = @layout_cache.transform_values do |value|
60
+ deep_dup(value)
61
+ end
62
+ end
63
+
64
+ def normalize_layout_cache(layouts)
65
+ (layouts || {}).each_with_object({}) do |(key, payload), acc|
66
+ acc[key.to_s] = deep_dup(payload)
67
+ end
68
+ end
69
+
70
+ def deep_dup(obj)
71
+ deep_dup_value(obj)
72
+ end
73
+
74
+ def deep_dup_value(value)
75
+ case value
76
+ when String
77
+ value.dup
78
+ when Array
79
+ value.map { |item| deep_dup_value(item) }
80
+ when Hash
81
+ value.transform_values { |item| deep_dup_value(item) }
82
+ else
83
+ value
84
+ end
85
+ end
86
+
87
+ def invalidate_and_nil
88
+ invalidate!
89
+ nil
90
+ end
91
+
92
+ def payload_valid?(payload)
93
+ payload.is_a?(CachePayload) &&
94
+ payload.version.to_i == CACHE_VERSION &&
95
+ payload.book.is_a?(BookData)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Shoko
6
+ module Adapters::Storage
7
+ # Persistence helpers for writing cache payloads + pointer metadata.
8
+ class EpubCache
9
+ private
10
+
11
+ def persist_payload(book_data, layouts_hash:)
12
+ ensure_sha!
13
+ generated_at = Time.now.utc
14
+
15
+ success = @cache_store.write_payload(
16
+ **payload_write_params(book_data, layouts_hash, generated_at)
17
+ )
18
+ return nil unless success
19
+
20
+ metadata = pointer_metadata_for_write(generated_at, engine: cache_engine)
21
+ write_pointer_metadata(metadata)
22
+ metadata
23
+ end
24
+
25
+ def payload_write_params(book_data, layouts_hash, generated_at)
26
+ serialized = Serializer.serialize(book_data, json: false)
27
+ layouts_serialized = Serializer.serialize_layouts(layouts_hash)
28
+
29
+ {
30
+ sha: @source_sha,
31
+ source_path: @source_path,
32
+ source_mtime: safe_mtime(@source_path),
33
+ generated_at: generated_at,
34
+ serialized_book: serialized[:book],
35
+ serialized_chapters: serialized[:chapters],
36
+ serialized_resources: serialized[:resources],
37
+ serialized_layouts: layouts_serialized,
38
+ }
39
+ end
40
+
41
+ def write_pointer_metadata(metadata)
42
+ @pointer_manager ||= CachePointerManager.new(@cache_path)
43
+ @pointer_manager.write(metadata)
44
+ @pointer_metadata = metadata
45
+ @source_type = :cache_pointer
46
+ end
47
+
48
+ def pointer_metadata_for_write(generated_at, engine:)
49
+ pointer_metadata(
50
+ sha: @source_sha,
51
+ source_path: @source_path,
52
+ generated_at: generated_at.iso8601,
53
+ engine: engine
54
+ )
55
+ end
56
+
57
+ def pointer_metadata(sha:, source_path:, generated_at:, engine:)
58
+ {
59
+ 'format' => CachePointerManager::POINTER_FORMAT,
60
+ 'version' => CachePointerManager::POINTER_VERSION,
61
+ 'sha256' => sha,
62
+ 'source_path' => source_path,
63
+ 'generated_at' => generated_at,
64
+ 'engine' => engine,
65
+ }
66
+ end
67
+
68
+ def ensure_pointer_from_metadata(record)
69
+ return unless record
70
+
71
+ ensure_sha!
72
+ pointer_metadata = pointer_metadata_for_record(record)
73
+ return if pointer_current?(pointer_metadata['sha256'])
74
+
75
+ write_pointer_metadata(pointer_metadata)
76
+ end
77
+
78
+ def pointer_metadata_for_record(record)
79
+ record_engine = Serializer.value_for(record, :engine) || cache_engine
80
+ pointer_metadata(
81
+ sha: Serializer.value_for(record, :source_sha),
82
+ source_path: Serializer.value_for(record, :source_path),
83
+ generated_at: pointer_generated_at(record),
84
+ engine: record_engine
85
+ )
86
+ end
87
+
88
+ def pointer_generated_at(record)
89
+ raw = Serializer.value_for(record, :generated_at)
90
+ time = Serializer.coerce_time(raw)
91
+ (time || Time.now.utc).iso8601
92
+ end
93
+
94
+ def pointer_current?(sha)
95
+ current = @pointer_manager&.read
96
+ current && current['sha256'] == sha
97
+ end
98
+
99
+ def cache_engine
100
+ engine = @cache_store.respond_to?(:engine) ? @cache_store.engine : nil
101
+ engine || JsonCacheStore::ENGINE
102
+ rescue StandardError
103
+ JsonCacheStore::ENGINE
104
+ end
105
+
106
+ def payload_matches_source?(payload, strict:)
107
+ return true if cache_file? && !payload.source_path
108
+
109
+ ensure_sha!
110
+ return false unless payload.source_sha256 == @source_sha
111
+
112
+ mtime_matches?(payload, strict: strict)
113
+ end
114
+
115
+ def mtime_matches?(payload, strict:)
116
+ source_mtime = safe_mtime(@source_path)
117
+ payload_mtime = payload.source_mtime
118
+ return true unless source_mtime && payload_mtime
119
+
120
+ tolerance = strict ? 1e-3 : 1.0
121
+ (source_mtime.to_f - payload_mtime.to_f).abs <= tolerance
122
+ end
123
+
124
+ def safe_mtime(path)
125
+ File.mtime(path)&.utc
126
+ rescue StandardError
127
+ nil
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ class EpubCache
6
+ # Deserialization helpers for reading cache payloads from JsonCacheStore.
7
+ module Serializer
8
+ module_function
9
+
10
+ def build_payload_from_store(raw_payload, cache_root:, book_sha:)
11
+ metadata = raw_payload.metadata_row || {}
12
+ book = deserialize_book(
13
+ metadata,
14
+ raw_payload.chapters,
15
+ raw_payload.resources,
16
+ cache_root: cache_root,
17
+ book_sha: book_sha
18
+ )
19
+ CachePayload.new(
20
+ **cache_payload_attributes(metadata, raw_payload, book_sha: book_sha, book: book)
21
+ )
22
+ end
23
+
24
+ def deserialize_book(book_row, chapter_rows, resource_rows, cache_root:, book_sha:)
25
+ expected_sha = book_sha.to_s
26
+ validate_payload_sha!(book_row, cache_root, expected_sha)
27
+ generation = chapters_generation!(book_row, cache_root)
28
+ json_fields = parse_book_json_fields(book_row)
29
+
30
+ chapters = deserialize_chapters(
31
+ chapter_rows,
32
+ cache_root: cache_root,
33
+ book_sha: expected_sha,
34
+ generation: generation
35
+ )
36
+ resources = deserialize_resources(resource_rows)
37
+ fields = book_display_fields(book_row, json_fields)
38
+ fields.merge!(book_navigation_fields(book_row, json_fields))
39
+ fields.merge!(book_storage_fields(json_fields, chapters, resources, generation))
40
+ BookData.new(**fields)
41
+ end
42
+
43
+ def cache_payload_attributes(metadata, raw_payload, book_sha:, book:)
44
+ {
45
+ version: value_for(metadata, :cache_version) || CACHE_VERSION,
46
+ source_sha256: book_sha.to_s,
47
+ source_path: value_for(metadata, :source_path),
48
+ source_mtime: coerce_time(value_for(metadata, :source_mtime)),
49
+ generated_at: coerce_time(value_for(metadata, :generated_at)),
50
+ book: book,
51
+ layouts: normalize_layouts(raw_payload.layouts),
52
+ }
53
+ end
54
+ private_class_method :cache_payload_attributes
55
+
56
+ def deserialize_chapters(rows, cache_root:, book_sha:, generation:)
57
+ Array(rows).map do |row|
58
+ idx = chapter_index(row)
59
+ Shoko::Core::Models::Chapter.new(
60
+ number: value_for(row, :number),
61
+ title: sanitize_display(value_for(row, :title)),
62
+ lines: [],
63
+ metadata: chapter_metadata(row),
64
+ blocks: nil,
65
+ raw_content: chapter_raw_content(cache_root, book_sha, generation, idx)
66
+ )
67
+ end
68
+ end
69
+
70
+ def deserialize_toc(json)
71
+ data = json.is_a?(String) ? JSON.parse(json || '[]') : json
72
+ Array(data).map do |entry|
73
+ Shoko::Core::Models::TOCEntry.new(
74
+ title: sanitize_display(value_for(entry, :title)),
75
+ href: sanitize_display(value_for(entry, :href)),
76
+ level: value_for(entry, :level),
77
+ chapter_index: value_for(entry, :chapter_index),
78
+ navigable: toc_navigable?(entry)
79
+ )
80
+ end
81
+ end
82
+
83
+ def deserialize_resources(rows)
84
+ Array(rows).each_with_object({}) do |row, acc|
85
+ data = value_for(row, :data).to_s.dup
86
+ data.force_encoding(Encoding::BINARY)
87
+ path = value_for(row, :path)
88
+ acc[path] = data if path
89
+ end
90
+ end
91
+
92
+ def normalize_layouts(raw_layouts)
93
+ return {} unless raw_layouts
94
+
95
+ raw_layouts.is_a?(Hash) ? normalize_layouts_hash(raw_layouts) : normalize_layouts_rows(raw_layouts)
96
+ end
97
+
98
+ def normalize_layout_payload(payload)
99
+ return nil unless payload
100
+
101
+ payload.is_a?(String) ? JSON.parse(payload) : payload
102
+ rescue JSON::ParserError
103
+ nil
104
+ end
105
+
106
+ def validate_payload_sha!(book_row, cache_root, expected_sha)
107
+ declared_sha = value_for(book_row, :source_sha).to_s
108
+ return if declared_sha == expected_sha
109
+
110
+ raise Shoko::CacheLoadError.new(cache_root, 'sha mismatch in payload')
111
+ end
112
+ private_class_method :validate_payload_sha!
113
+
114
+ def chapters_generation!(book_row, cache_root)
115
+ generation = value_for(book_row, :chapters_generation).to_s
116
+ return generation if generation.match?(JsonCacheStore::CHAPTERS_GENERATION_PATTERN)
117
+
118
+ raise Shoko::CacheLoadError.new(cache_root, 'invalid chapters generation')
119
+ end
120
+ private_class_method :chapters_generation!
121
+
122
+ def parse_book_json_fields(book_row)
123
+ {
124
+ authors: parse_json_array(value_for(book_row, :authors_json)),
125
+ metadata: parse_json_hash(value_for(book_row, :metadata_json)),
126
+ spine: parse_json_array(value_for(book_row, :spine_json)),
127
+ hrefs: parse_json_array(value_for(book_row, :chapter_hrefs_json)),
128
+ toc: parse_json_array(value_for(book_row, :toc_json)),
129
+ }
130
+ end
131
+ private_class_method :parse_book_json_fields
132
+
133
+ def book_display_fields(book_row, json_fields)
134
+ authors = Array(json_fields[:authors]).map { |name| sanitize_display(name) }
135
+ {
136
+ title: sanitize_display(value_for(book_row, :title)),
137
+ language: sanitize_display(value_for(book_row, :language)),
138
+ authors: authors,
139
+ container_path: value_for(book_row, :container_path),
140
+ container_xml: sanitize_content(value_for(book_row, :container_xml)),
141
+ }
142
+ end
143
+ private_class_method :book_display_fields
144
+
145
+ def book_navigation_fields(book_row, json_fields)
146
+ {
147
+ toc_entries: deserialize_toc(json_fields[:toc]),
148
+ opf_path: value_for(book_row, :opf_path),
149
+ spine: Array(json_fields[:spine]),
150
+ chapter_hrefs: Array(json_fields[:hrefs]),
151
+ }
152
+ end
153
+ private_class_method :book_navigation_fields
154
+
155
+ def book_storage_fields(json_fields, chapters, resources, generation)
156
+ {
157
+ metadata: json_fields[:metadata] || {},
158
+ chapters: chapters,
159
+ resources: resources,
160
+ chapters_generation: generation,
161
+ }
162
+ end
163
+ private_class_method :book_storage_fields
164
+
165
+ def chapter_index(row)
166
+ pos = value_for(row, :position)
167
+ Integer(pos)
168
+ end
169
+ private_class_method :chapter_index
170
+
171
+ def chapter_metadata(row)
172
+ metadata = value_for(row, :metadata_json)
173
+ parse_json_hash(metadata)
174
+ end
175
+ private_class_method :chapter_metadata
176
+
177
+ def chapter_raw_content(cache_root, book_sha, generation, idx)
178
+ Shoko::Adapters::Storage::LazyFileString.new(
179
+ chapter_raw_path(cache_root, book_sha, generation, idx),
180
+ sanitizer: method(:sanitize_content)
181
+ )
182
+ end
183
+ private_class_method :chapter_raw_content
184
+
185
+ def chapter_raw_path(cache_root, book_sha, generation, idx)
186
+ file = format("%0#{JsonCacheStore::CHAPTER_FILENAME_DIGITS}d.xhtml", idx)
187
+ File.join(cache_root.to_s, JsonCacheStore::CHAPTERS_DIRNAME, book_sha,
188
+ generation, JsonCacheStore::CHAPTERS_RAW_DIRNAME, file)
189
+ end
190
+ private_class_method :chapter_raw_path
191
+
192
+ def normalize_layouts_hash(layouts_hash)
193
+ layouts_hash.each_with_object({}) do |(key, payload), acc|
194
+ normalized = normalize_layout_payload(payload)
195
+ acc[key.to_s] = normalized if normalized
196
+ end
197
+ end
198
+ private_class_method :normalize_layouts_hash
199
+
200
+ def normalize_layouts_rows(raw_layouts)
201
+ Array(raw_layouts).each_with_object({}) do |row, acc|
202
+ normalized = normalize_row_layout(row)
203
+ acc[normalized[:key]] = normalized[:payload] if normalized
204
+ end
205
+ end
206
+ private_class_method :normalize_layouts_rows
207
+
208
+ def normalize_row_layout(row)
209
+ key = value_for(row, :key)
210
+ payload = value_for(row, :payload_json) || value_for(row, :payload)
211
+ normalized = normalize_layout_payload(payload)
212
+ return nil unless key && normalized
213
+
214
+ { key: key.to_s, payload: normalized }
215
+ end
216
+ private_class_method :normalize_row_layout
217
+
218
+ def toc_navigable?(entry)
219
+ value_for(entry, :navigable) != false
220
+ end
221
+ private_class_method :toc_navigable?
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ class EpubCache
6
+ # Shared helpers used by the cache payload serializer/deserializer.
7
+ module Serializer
8
+ module_function
9
+
10
+ def coerce_time(raw)
11
+ return raw if raw.is_a?(Time)
12
+ return nil unless raw
13
+
14
+ Time.at(raw.to_f).utc
15
+ rescue StandardError
16
+ nil
17
+ end
18
+
19
+ def value_for(obj, key)
20
+ if obj.respond_to?(key)
21
+ obj.public_send(key)
22
+ elsif obj.respond_to?(:[])
23
+ obj[key] || obj[key.to_s]
24
+ end
25
+ end
26
+
27
+ def sanitize_display(text)
28
+ string = text.to_s
29
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(string, preserve_newlines: false, preserve_tabs: false)
30
+ rescue StandardError
31
+ string.to_s
32
+ end
33
+ private_class_method :sanitize_display
34
+
35
+ def sanitize_content(text)
36
+ string = text.to_s
37
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(string, preserve_newlines: true, preserve_tabs: true)
38
+ rescue StandardError
39
+ string.to_s
40
+ end
41
+ private_class_method :sanitize_content
42
+
43
+ def parse_json(raw, fallback_json:)
44
+ return raw unless raw.is_a?(String)
45
+
46
+ JSON.parse(raw.empty? ? fallback_json : raw)
47
+ end
48
+ private_class_method :parse_json
49
+
50
+ def parse_json_array(raw)
51
+ Array(parse_json(raw, fallback_json: '[]'))
52
+ end
53
+ private_class_method :parse_json_array
54
+
55
+ def parse_json_hash(raw)
56
+ parsed = parse_json(raw, fallback_json: '{}')
57
+ parsed.is_a?(Hash) ? parsed : {}
58
+ end
59
+ private_class_method :parse_json_hash
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ class EpubCache
6
+ # Serialization helpers for persisting cache payloads.
7
+ module Serializer
8
+ module_function
9
+
10
+ def serialize(book_data, json: false)
11
+ {
12
+ book: serialize_book(book_data, json: json),
13
+ chapters: serialize_chapters(book_data.chapters, json: json),
14
+ resources: serialize_resources(book_data.resources),
15
+ }
16
+ end
17
+
18
+ def serialize_layouts(layouts_hash)
19
+ return {} unless layouts_hash.is_a?(Hash)
20
+
21
+ layouts_hash.transform_keys(&:to_s)
22
+ end
23
+
24
+ def serialize_book(book, json: true)
25
+ {
26
+ payload_version: CACHE_VERSION, cache_version: CACHE_VERSION,
27
+ title: book.title, language: book.language,
28
+ authors_json: json_field(Array(book.authors), json),
29
+ metadata_json: json_field(book.metadata || {}, json),
30
+ opf_path: book.opf_path,
31
+ spine_json: json_field(Array(book.spine), json),
32
+ chapter_hrefs_json: json_field(Array(book.chapter_hrefs), json),
33
+ toc_json: json_field(serialized_toc(book), json),
34
+ container_path: book.container_path, container_xml: book.container_xml.to_s
35
+ }
36
+ end
37
+
38
+ def serialize_chapters(chapters, json: true)
39
+ Array(chapters).each_with_index.map do |chapter, idx|
40
+ metadata = chapter.metadata || {}
41
+ {
42
+ position: idx,
43
+ number: chapter.number,
44
+ title: chapter.title,
45
+ metadata_json: json_field(metadata, json),
46
+ raw_content: chapter.raw_content,
47
+ }
48
+ end
49
+ end
50
+
51
+ def serialize_resources(resources)
52
+ return [] unless resources
53
+
54
+ resources.map do |path, data|
55
+ bytes = String(data).dup
56
+ bytes.force_encoding(Encoding::BINARY)
57
+ { path: path.to_s, data: bytes }
58
+ end
59
+ end
60
+
61
+ def serialize_toc_entry(entry)
62
+ {
63
+ title: value_for(entry, :title),
64
+ href: value_for(entry, :href),
65
+ level: value_for(entry, :level),
66
+ chapter_index: value_for(entry, :chapter_index),
67
+ navigable: value_for(entry, :navigable),
68
+ }
69
+ end
70
+
71
+ def json_field(value, json)
72
+ json ? JSON.generate(value) : value
73
+ end
74
+ private_class_method :json_field
75
+
76
+ def serialized_toc(book)
77
+ Array(book.toc_entries).map { |entry| serialize_toc_entry(entry) }
78
+ end
79
+ private_class_method :serialized_toc
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'serializer/serialize'
4
+ require_relative 'serializer/deserialize'
5
+ require_relative 'serializer/helpers'
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ # Source resolution helpers for `EpubCache` (EPUB source vs `.cache` pointer file).
6
+ class EpubCache
7
+ private
8
+
9
+ def setup_source_reference
10
+ if self.class.cache_file?(@raw_path)
11
+ setup_source_reference_from_pointer
12
+ else
13
+ setup_source_reference_from_epub
14
+ end
15
+ end
16
+
17
+ def setup_source_reference_from_pointer
18
+ @cache_path = @raw_path
19
+ @pointer_manager = CachePointerManager.new(@cache_path)
20
+ pointer = @pointer_manager.read
21
+ raise Shoko::CacheLoadError.new(@raw_path, 'invalid pointer file') unless pointer
22
+
23
+ @source_type = :cache_pointer
24
+ @pointer_metadata = pointer
25
+ @source_sha = pointer['sha256']
26
+ @source_path = pointer['source_path']
27
+ end
28
+
29
+ def setup_source_reference_from_epub
30
+ raise Shoko::FileNotFoundError, @raw_path unless File.file?(@raw_path)
31
+
32
+ @source_type = :epub
33
+ @source_path = @raw_path
34
+ @source_sha = Digest::SHA256.file(@source_path).hexdigest
35
+ @cache_path = cache_path_for_source_sha
36
+ @pointer_manager = CachePointerManager.new(@cache_path)
37
+ @pointer_metadata = @pointer_manager.read
38
+ end
39
+
40
+ def cache_path_for_source_sha
41
+ cache_path = self.class.cache_path_for_sha(@source_sha, cache_root: @cache_root)
42
+ raise Shoko::CacheLoadError.new(@raw_path, 'invalid sha256 digest') unless cache_path
43
+
44
+ cache_path
45
+ end
46
+
47
+ def ensure_sha!
48
+ return if @source_sha
49
+
50
+ if @source_type == :epub
51
+ @source_sha = Digest::SHA256.file(@source_path).hexdigest
52
+ elsif @pointer_metadata
53
+ @source_sha = @pointer_metadata['sha256']
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Storage
5
+ # Cache path helpers (XDG-compatible) for on-disk Shoko artifacts.
6
+ module CachePaths
7
+ module_function
8
+
9
+ # Root directory for Shoko cache: ${XDG_CACHE_HOME:-~/.cache}/shoko
10
+ def cache_root
11
+ env_home = ENV.fetch('XDG_CACHE_HOME', nil)
12
+ cache_root = if env_home && !env_home.empty?
13
+ env_home
14
+ else
15
+ File.join(Dir.home, '.cache')
16
+ end
17
+ File.join(cache_root, 'shoko')
18
+ end
19
+ end
20
+ end
21
+ end