module Preview class ThumbnailFetcher class GenerationFailed < StandardError; end ALLOWED_IMAGE_CONTENT_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ].freeze HTML_MAX_BYTES = 1.megabyte NICONICO_XML_MAX_BYTES = 256.kilobytes def self.fetch(raw_url) uri, = UrlSafety.validate(raw_url) known_url = KnownSiteExtractor.thumbnail_url(uri) image = fetch_image_or_nil(known_url) if known_url return image if image niconico_url = niconico_thumbnail_url(uri) image = fetch_image_or_nil(niconico_url) if niconico_url return image if image page = HttpFetcher.fetch(uri.to_s, max_bytes: HTML_MAX_BYTES) metadata = HtmlMetadataExtractor.extract(page) raise GenerationFailed, 'サムネール画像が見つかりませんでした.' if metadata[:image_url].blank? fetch_image!(metadata[:image_url]) end def self.title(raw_url) uri, = UrlSafety.validate(raw_url) HtmlMetadataExtractor.extract( HttpFetcher.fetch(uri.to_s, max_bytes: HTML_MAX_BYTES))[:title] end def self.fetch_image_or_nil(url) return nil if url.blank? response = HttpFetcher.fetch(url) return nil unless allowed_image_content_type?(response.content_type) response.body rescue HttpFetcher::FetchTimeout raise rescue HttpFetcher::FetchFailed nil end def self.fetch_image!(url) response = HttpFetcher.fetch(url) unless allowed_image_content_type?(response.content_type) raise GenerationFailed, 'サムネール画像が見つかりませんでした.' end response.body rescue HttpFetcher::FetchTimeout raise rescue HttpFetcher::ResponseTooLarge raise rescue HttpFetcher::FetchFailed raise GenerationFailed, 'サムネール画像を取得できませんでした.' end def self.niconico_thumbnail_url(uri) video_id = KnownSiteExtractor.niconico_video_id(uri) return nil if video_id.blank? response = HttpFetcher.fetch("https://ext.nicovideo.jp/api/getthumbinfo/#{ video_id }", max_bytes: NICONICO_XML_MAX_BYTES) xml = Nokogiri::XML(response.body) return nil unless xml.at_xpath('/nicovideo_thumb_response/@status')&.value == 'ok' xml.at_xpath('//thumbnail_url')&.text&.strip.presence rescue HttpFetcher::FetchFailed, HttpFetcher::FetchTimeout => e Rails.logger.info("preview_niconico_getthumbinfo_fallback #{ { url: uri.to_s, video_id:, error: e.class.name, message: e.message }.to_json }") nil rescue Nokogiri::XML::SyntaxError => e Rails.logger.info("preview_niconico_getthumbinfo_fallback #{ { url: uri.to_s, video_id:, error: e.class.name, message: e.message }.to_json }") nil end def self.allowed_image_content_type?(content_type) mime_type = content_type.to_s.split(';', 2).first.downcase.strip ALLOWED_IMAGE_CONTENT_TYPES.include?(mime_type) end private_class_method :fetch_image_or_nil, :fetch_image!, :niconico_thumbnail_url, :allowed_image_content_type? end end