module Preview class ThumbnailFetcher class GenerationFailed < StandardError; end 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 response.content_type.downcase.start_with?('image/') response.body rescue HttpFetcher::FetchTimeout raise rescue HttpFetcher::FetchFailed nil end def self.fetch_image!(url) response = HttpFetcher.fetch(url) unless response.content_type.downcase.start_with?('image/') 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 private_class_method :fetch_image_or_nil, :fetch_image!, :niconico_thumbnail_url end end