Expand on rendering, server component
This commit is contained in:
parent
2e44bf7c10
commit
0cf85cb6dc
13 changed files with 223 additions and 84 deletions
|
|
@ -6,7 +6,7 @@ require 'optparse'
|
||||||
require 'ostruct'
|
require 'ostruct'
|
||||||
|
|
||||||
options = OpenStruct.new
|
options = OpenStruct.new
|
||||||
OptParse.new do |opts|
|
optparse = OptParse.new do |opts|
|
||||||
opts.banner = 'Usage: fic_tracker [OPTIONS...]'
|
opts.banner = 'Usage: fic_tracker [OPTIONS...]'
|
||||||
|
|
||||||
opts.on '-b', '--backend=BACKEND', 'The backend to use' do |backend|
|
opts.on '-b', '--backend=BACKEND', 'The backend to use' do |backend|
|
||||||
|
|
@ -56,13 +56,24 @@ OptParse.new do |opts|
|
||||||
puts FicTracker::VERSION
|
puts FicTracker::VERSION
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
end.parse!
|
end
|
||||||
|
optparse.parse!
|
||||||
|
|
||||||
|
unless options.backend && options.format && options.story
|
||||||
|
puts "Backend, format, and sotry must be provided\n"
|
||||||
|
puts optparse
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
FicTracker.logger.level = options.log_level || :warn
|
FicTracker.logger.level = options.log_level || :warn
|
||||||
FicTracker.configure!
|
FicTracker.configure!
|
||||||
|
|
||||||
backend = FicTracker::Backends.get(options.backend)
|
backend = FicTracker::Backends.get(options.backend)
|
||||||
story = FicTracker::Models::Story.find(backend_name: backend.name, slug: backend.parse_slug(options.story)) || FicTracker::Models::Story.new(backend: backend, slug: backend.parse_slug(options.story))
|
|
||||||
|
slug = options.story
|
||||||
|
slug = backend.parse_slug(slug) if backend.respond_to? :parse_slug
|
||||||
|
|
||||||
|
story = FicTracker::Models::Story.find(backend_name: backend.name, slug:) || FicTracker::Models::Story.new(backend:, slug:)
|
||||||
story.ensure_fully_loaded
|
story.ensure_fully_loaded
|
||||||
data = nil
|
data = nil
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,52 +51,9 @@ module FicTracker
|
||||||
@global_logger ||= false
|
@global_logger ||= false
|
||||||
end
|
end
|
||||||
|
|
||||||
module Models
|
autoload :Models, 'fic_tracker/models'
|
||||||
def self.const_missing(const)
|
autoload :Renderers, 'fic_tracker/renderers'
|
||||||
raise 'No database connected' unless FicTracker.database
|
autoload :Tasks, 'fic_tracker/tasks'
|
||||||
|
|
||||||
model = const.to_s.downcase
|
|
||||||
require_relative "fic_tracker/models/#{model}"
|
|
||||||
|
|
||||||
mod = const_get(const) if const_defined? const
|
|
||||||
return mod if mod
|
|
||||||
|
|
||||||
raise "Model not found: #{const}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module Tasks
|
|
||||||
autoload :Cleanup, 'fic_tracker/tasks/cleanup'
|
|
||||||
end
|
|
||||||
|
|
||||||
module Renderers
|
|
||||||
autoload :Epub, 'fic_tracker/renderers/epub'
|
|
||||||
autoload :HTML, 'fic_tracker/renderers/html'
|
|
||||||
autoload :Markdown, 'fic_tracker/renderers/markdown'
|
|
||||||
|
|
||||||
def self.render(type, story, **attrs)
|
|
||||||
klass = case type
|
|
||||||
when :Markdown, :markdown, :md
|
|
||||||
Markdown
|
|
||||||
when :HTML, :html
|
|
||||||
HTML
|
|
||||||
when :Epub, :epub
|
|
||||||
Epub
|
|
||||||
end
|
|
||||||
|
|
||||||
stringio = nil
|
|
||||||
unless attrs[:io]
|
|
||||||
require 'stringio'
|
|
||||||
attrs[:io] = StringIO.new
|
|
||||||
stringio = true
|
|
||||||
end
|
|
||||||
|
|
||||||
result = klass.new(story, **attrs).render
|
|
||||||
return result unless stringio
|
|
||||||
|
|
||||||
attrs[:io].string
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
require_relative 'fic_tracker/backend'
|
require_relative 'fic_tracker/backend'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'client'
|
require_relative 'client'
|
||||||
|
require_relative 'search_info'
|
||||||
|
|
||||||
module FicTracker::Backends::Ao3
|
module FicTracker::Backends::Ao3
|
||||||
class Backend < FicTracker::Backend
|
class Backend < FicTracker::Backend
|
||||||
|
|
@ -53,7 +54,7 @@ module FicTracker::Backends::Ao3
|
||||||
story = FicTracker::Models::Story.new(slug: parse_slug(story), backend: self) unless story.is_a? FicTracker::Models::Story
|
story = FicTracker::Models::Story.new(slug: parse_slug(story), backend: self) unless story.is_a? FicTracker::Models::Story
|
||||||
|
|
||||||
logger.info "Loading story #{story.slug}"
|
logger.info "Loading story #{story.slug}"
|
||||||
doc = client.request("/works/#{story.slug}")
|
doc = client.request("/works/#{story.slug}", query: { view_adult: true })
|
||||||
|
|
||||||
attrs = extract_story doc
|
attrs = extract_story doc
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ module FicTracker::Backends::Ao3
|
||||||
story = FicTracker::Models::Story.new(slug: parse_slug(story), backend: self) unless story.is_a? FicTracker::Models::Story
|
story = FicTracker::Models::Story.new(slug: parse_slug(story), backend: self) unless story.is_a? FicTracker::Models::Story
|
||||||
|
|
||||||
logger.info "Loading all chapters for #{story.slug}"
|
logger.info "Loading all chapters for #{story.slug}"
|
||||||
doc = client.request("/works/#{story.slug}", query: { view_full_work: true })
|
doc = client.request("/works/#{story.slug}", query: { view_full_work: true, view_adult: true })
|
||||||
|
|
||||||
attrs = extract_story(doc)
|
attrs = extract_story(doc)
|
||||||
chapters = doc.css('#chapters > div.chapter').map { |chapter| extract_chapter(chapter) }
|
chapters = doc.css('#chapters > div.chapter').map { |chapter| extract_chapter(chapter) }
|
||||||
|
|
@ -116,13 +117,23 @@ module FicTracker::Backends::Ao3
|
||||||
chapter = FicTracker::Models::Chapter.new(slug: parse_slug(chapter), story: story) unless chapter.is_a? FicTracker::Models::Chapter
|
chapter = FicTracker::Models::Chapter.new(slug: parse_slug(chapter), story: story) unless chapter.is_a? FicTracker::Models::Chapter
|
||||||
|
|
||||||
logger.info "Loading chapter #{chapter.slug} for #{story.slug}"
|
logger.info "Loading chapter #{chapter.slug} for #{story.slug}"
|
||||||
doc = client.request("/works/#{story.slug}/chapters/#{chapter.slug}")
|
doc = client.request("/works/#{story.slug}/chapters/#{chapter.slug}", query: { view_adult: true })
|
||||||
|
|
||||||
attrs = extract_chapter(doc.at_css('#chapters > div.chapter'))
|
attrs = extract_chapter(doc.at_css('#chapters > div.chapter'))
|
||||||
|
|
||||||
chapter.set(**attrs)
|
chapter.set(**attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_search_info
|
||||||
|
SearchInfo.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search(search_info)
|
||||||
|
info = SearchInfo.from_search(search_info)
|
||||||
|
|
||||||
|
info
|
||||||
|
end
|
||||||
|
|
||||||
def parse_slug(slug)
|
def parse_slug(slug)
|
||||||
return URI(slug).path.split('/').last if slug.is_a?(String) && slug.start_with?('http')
|
return URI(slug).path.split('/').last if slug.is_a?(String) && slug.start_with?('http')
|
||||||
|
|
||||||
|
|
|
||||||
34
lib/fic_tracker/backends/ao3/search_info.rb
Normal file
34
lib/fic_tracker/backends/ao3/search_info.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fic_tracker/models/light/search_info'
|
||||||
|
|
||||||
|
module FicTracker::Backends::Ao3
|
||||||
|
class SearchInfo < FicTracker::Models::Light::SearchInfo
|
||||||
|
# Meta tags
|
||||||
|
tag_category :category, list: [
|
||||||
|
{name: 'Gen'},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
tag_category :rating, list: [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
|
||||||
|
tag_category :character, freeform: true
|
||||||
|
tag_category :fandom, search: ->(search) { autocomplete_fandom(search) }
|
||||||
|
tag_category :relationship, freeform: true
|
||||||
|
tag_category :freeform, freeform: true
|
||||||
|
|
||||||
|
def initialize(backend)
|
||||||
|
@backend = backend
|
||||||
|
@client = backend.client
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def autocomplete_fandom(name)
|
||||||
|
@client.request("/autocomplete").map do |tag|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
15
lib/fic_tracker/models.rb
Normal file
15
lib/fic_tracker/models.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FicTracker::Models
|
||||||
|
def self.const_missing(const)
|
||||||
|
raise 'No database connected' unless FicTracker.database
|
||||||
|
|
||||||
|
model = const.to_s.downcase
|
||||||
|
require_relative "models/#{model}"
|
||||||
|
|
||||||
|
mod = const_get(const) if const_defined? const
|
||||||
|
return mod if mod
|
||||||
|
|
||||||
|
raise "Model not found: #{const}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
|
||||||
module FicTracker::Models::Light
|
module FicTracker::Models::Light
|
||||||
class SearchInfo
|
class SearchInfo
|
||||||
|
def initialize(**data)
|
||||||
|
data.each do |k, v|
|
||||||
|
send(:"#{k}=", v) if respond_to?(:"#{k}=")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_language(lang)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
def with_tag(tag)
|
def with_tag(tag)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
@ -15,10 +24,56 @@ module FicTracker::Models::Light
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_cursor(cursor)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_tags(category, search = nil)
|
||||||
|
raise ArgumentError, "No such category #{category.inspect}" unless @tags.key? category
|
||||||
|
|
||||||
|
tag_cat = @tags[category]
|
||||||
|
return if tag_cat[:freeform]
|
||||||
|
|
||||||
|
list = begin
|
||||||
|
if tag_cat[:list]
|
||||||
|
return tag_cat[:list] if search.nil?
|
||||||
|
return tag_cat[:list].select do |t|
|
||||||
|
case search
|
||||||
|
when Regex
|
||||||
|
t[:name] =~ search
|
||||||
|
when String
|
||||||
|
t[:name].downcase.include? search.downcase
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tag_cat[:search].call(search) if tag_cat[:search]
|
||||||
|
end
|
||||||
|
list.each do |t|
|
||||||
|
t[:id] ||= t[:name]
|
||||||
|
t[:category] ||= category
|
||||||
|
end
|
||||||
|
FicTracker::Models::Light::Tag.load list
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_info_json
|
||||||
|
self.class.to_info_json
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def from_search(data)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_info_json
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def tag_category(category, **params)
|
def tag_category(category, **params)
|
||||||
|
(@tags ||= {})[category] = params
|
||||||
end
|
end
|
||||||
|
|
||||||
def word_limits(*limits)
|
def word_limits(*limits)
|
||||||
|
|
|
||||||
|
|
@ -148,16 +148,17 @@ module FicTracker::Models
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_fully_loaded
|
def ensure_fully_loaded
|
||||||
ensure_chapters
|
# FIXME: Should check for a reasonable set of parameters - full load unless XX% (75%?) of chapters have content
|
||||||
|
if chapters && chapters.any? && chapters.all? { |c| c.content? && c.content_type? }
|
||||||
refresh_content
|
refresh_content
|
||||||
refresh_metadata
|
refresh_metadata
|
||||||
|
else
|
||||||
|
backend.load_full_story(self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_chapters
|
def ensure_chapters
|
||||||
# FIXME: Should check for a reasonable set of parameters - full load unless XX% (75%?) of chapters have content
|
ensure_fully_loaded
|
||||||
return if chapters && chapters.any? && chapters.all? { |c| c.content? && c.content_type? }
|
|
||||||
|
|
||||||
backend.load_full_story(self)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def backend
|
def backend
|
||||||
|
|
|
||||||
30
lib/fic_tracker/renderers.rb
Normal file
30
lib/fic_tracker/renderers.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FicTracker::Renderers
|
||||||
|
autoload :Epub, 'fic_tracker/renderers/epub'
|
||||||
|
autoload :HTML, 'fic_tracker/renderers/html'
|
||||||
|
autoload :Markdown, 'fic_tracker/renderers/markdown'
|
||||||
|
|
||||||
|
def self.render(type, story, **attrs)
|
||||||
|
klass = case type
|
||||||
|
when :Markdown, :markdown, :md
|
||||||
|
Markdown
|
||||||
|
when :HTML, :html
|
||||||
|
HTML
|
||||||
|
when :Epub, :epub
|
||||||
|
Epub
|
||||||
|
end
|
||||||
|
|
||||||
|
stringio = nil
|
||||||
|
unless attrs[:io]
|
||||||
|
require 'stringio'
|
||||||
|
attrs[:io] = StringIO.new
|
||||||
|
stringio = true
|
||||||
|
end
|
||||||
|
|
||||||
|
result = klass.new(story, **attrs).render
|
||||||
|
return result unless stringio
|
||||||
|
|
||||||
|
attrs[:io].string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -69,6 +69,8 @@ module FicTracker::Renderers
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_content_opf
|
def build_content_opf
|
||||||
|
require_relative '../util/time_extensions'
|
||||||
|
|
||||||
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
||||||
xml.package version: '2.0', xmlns: 'http://www.idpf.org/2007/opf', 'unique-identifier': 'story-url' do
|
xml.package version: '2.0', xmlns: 'http://www.idpf.org/2007/opf', 'unique-identifier': 'story-url' do
|
||||||
xml.metadata 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', 'xmlns:opf': 'http://www.idpf.org/2007/opf' do
|
xml.metadata 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', 'xmlns:opf': 'http://www.idpf.org/2007/opf' do
|
||||||
|
|
@ -95,6 +97,7 @@ module FicTracker::Renderers
|
||||||
xml.meta name: 'ft-backend', content: story.backend_name
|
xml.meta name: 'ft-backend', content: story.backend_name
|
||||||
xml.meta name: 'ft-story', content: story.slug
|
xml.meta name: 'ft-story', content: story.slug
|
||||||
xml.meta name: 'ft-etag', content: story.etag
|
xml.meta name: 'ft-etag', content: story.etag
|
||||||
|
xml.meta name: 'ft-modified', content: (story.updated_at || story.published_at || Time.now).to_header
|
||||||
xml.meta name: 'ft-complete', content: story.completed?
|
xml.meta name: 'ft-complete', content: story.completed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,12 @@ module FicTracker::Renderers
|
||||||
def build_html(html)
|
def build_html(html)
|
||||||
return yield if body_only
|
return yield if body_only
|
||||||
|
|
||||||
|
require_relative '../util/time_extensions'
|
||||||
|
|
||||||
html.html(lang: story.language || 'en') do
|
html.html(lang: story.language || 'en') do
|
||||||
html.head do
|
html.head do
|
||||||
html.meta charset: 'utf-8'
|
html.meta charset: 'utf-8'
|
||||||
html.meta viewport: 'width=device-width, initial-scale=1'
|
html.meta name: 'viewport', content: 'width=device-width, initial-scale=1'
|
||||||
story.authors.each do |aut|
|
story.authors.each do |aut|
|
||||||
html.meta name: 'author', content: aut.to_s
|
html.meta name: 'author', content: aut.to_s
|
||||||
end
|
end
|
||||||
|
|
@ -135,6 +137,8 @@ module FicTracker::Renderers
|
||||||
html.meta name: 'ft-backend', content: story.backend_name
|
html.meta name: 'ft-backend', content: story.backend_name
|
||||||
html.meta name: 'ft-story', content: story.slug
|
html.meta name: 'ft-story', content: story.slug
|
||||||
html.meta name: 'ft-etag', content: story.etag
|
html.meta name: 'ft-etag', content: story.etag
|
||||||
|
html.meta name: 'ft-modified', content: (story.updated_at || story.published_at || Time.now).to_header
|
||||||
|
html.meta name: 'ft-complete', content: story.completed?
|
||||||
|
|
||||||
html.title story.to_s
|
html.title story.to_s
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,13 @@ module FicTracker
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/search/:backend', provides: :json do |_backend_name|
|
get '/search/:backend', provides: :json do |_backend_name|
|
||||||
backend.get_search_info.to_json
|
search = backend.get_search_info
|
||||||
|
if params['tag'] || params['name']
|
||||||
|
raise "Missing params, must specify both tag and name" unless params['tag'] && params['name']
|
||||||
|
return search.class.find_tags(params['tag'], params['name']).to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
search.to_info_json
|
||||||
end
|
end
|
||||||
|
|
||||||
post '/search/:backend', provides: :html do |_backend_name|
|
post '/search/:backend', provides: :html do |_backend_name|
|
||||||
|
|
@ -108,14 +114,16 @@ module FicTracker
|
||||||
halt 400, "Unknown format #{format}"
|
halt 400, "Unknown format #{format}"
|
||||||
end
|
end
|
||||||
|
|
||||||
story = Models::Story.find(backend_name: backend.name, slug:)
|
|
||||||
story ||= Models::Story.new(backend_name: backend.name, slug:)
|
|
||||||
|
|
||||||
content_type mime
|
content_type mime
|
||||||
attachment "#{story.safe_name}.#{format}"
|
attachment "#{story.safe_name}.#{format}"
|
||||||
|
|
||||||
|
story = Models::Story.find(backend_name: backend.name, slug:)
|
||||||
|
if story
|
||||||
|
story.set(last_accessed: Time.now)
|
||||||
|
|
||||||
last_modified story.updated_at || story.published_at
|
last_modified story.updated_at || story.published_at
|
||||||
etag story.etag
|
etag story.etag
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
story&.save_changes
|
story&.save_changes
|
||||||
end
|
end
|
||||||
|
|
@ -140,8 +148,7 @@ module FicTracker
|
||||||
story = Models::Story.find(backend_name: backend.name, slug:)
|
story = Models::Story.find(backend_name: backend.name, slug:)
|
||||||
story ||= Models::Story.new(backend_name: backend.name, slug:)
|
story ||= Models::Story.new(backend_name: backend.name, slug:)
|
||||||
|
|
||||||
story.refresh_content
|
story.ensure_fully_loaded unless story.chapters&.any?
|
||||||
story.refresh_metadata
|
|
||||||
story.set(last_accessed: Time.now)
|
story.set(last_accessed: Time.now)
|
||||||
|
|
||||||
content_type mime
|
content_type mime
|
||||||
|
|
@ -176,8 +183,7 @@ module FicTracker
|
||||||
story = Models::Story.find(backend_name: backend.name, slug:)
|
story = Models::Story.find(backend_name: backend.name, slug:)
|
||||||
story ||= Models::Story.new(backend_name: backend.name, slug:)
|
story ||= Models::Story.new(backend_name: backend.name, slug:)
|
||||||
|
|
||||||
story.refresh_content
|
story.ensure_fully_loaded
|
||||||
story.refresh_metadata
|
|
||||||
story.set(last_accessed: Time.now)
|
story.set(last_accessed: Time.now)
|
||||||
|
|
||||||
chapter = story.chapters[index.to_i]
|
chapter = story.chapters[index.to_i]
|
||||||
|
|
|
||||||
5
lib/fic_tracker/tasks.rb
Normal file
5
lib/fic_tracker/tasks.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FicTracker::Tasks
|
||||||
|
autoload :Cleanup, 'fic_tracker/tasks/cleanup'
|
||||||
|
end
|
||||||
7
lib/fic_tracker/util/time_extensions.rb
Normal file
7
lib/fic_tracker/util/time_extensions.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Time
|
||||||
|
def to_header
|
||||||
|
getgm.strftime("%a, %d %b %Y %T GMT")
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue