class PostBulkCreator def initialize actor:, posts:, thumbnails: @actor = actor @posts = posts @thumbnails = thumbnails end def run results = @posts.each_with_index.map { |attributes, index| create_row(attributes, index) } { results: } end private def create_row attributes, index preflight = PostCreatePreflight.new( attributes: attributes, thumbnail: thumbnail_for(index, attributes)).run if preflight[:existing_post].present? return { status: 'skipped', existing_post: preflight[:existing_post] } end post = PostCreator.new( actor: @actor, attributes: normalised_attributes(attributes, preflight, index)).create! result = { status: 'created', post: { id: post.id } } result[:field_warnings] = preflight[:field_warnings] if preflight[:field_warnings].present? result[:base_warnings] = preflight[:base_warnings] if preflight[:base_warnings].present? result rescue PostCreatePreflight::ValidationFailed => e { status: 'failed', recoverable: true, errors: e.fields, base_errors: e.base_errors } rescue ActiveRecord::RecordInvalid => e existing_post = existing_post_for_race(attributes, e.record) if existing_post.present? return { status: 'skipped', existing_post: existing_post } end { status: 'failed', recoverable: true, errors: e.record.errors.to_hash, base_errors: e.record.errors[:base] } rescue ActiveRecord::RecordNotUnique => e if e.message.include?('index_posts_on_url') existing_post = existing_post_for_race(attributes) return { status: 'skipped', existing_post: existing_post } if existing_post.present? end Rails.logger.error( "post_bulk_creator_record_not_unique #{ { error: e.class.name, message: e.message }.to_json }") { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } rescue Tag::NicoTagNormalisationError { status: 'failed', recoverable: true, errors: { tags: ['ニコニコ・タグは直接指定できません.'] }, base_errors: [] } rescue Tag::DeprecatedTagNormalisationError { status: 'failed', recoverable: true, errors: { tags: ['廃止済みタグは付与できません.'] }, base_errors: [] } rescue PostCreator::VideoMsParseError { status: 'failed', recoverable: true, errors: { video_ms: ['動画時間の記法が不正です.'] }, base_errors: [] } rescue Post::RemoteThumbnailFetchFailed { status: 'failed', recoverable: true, errors: { thumbnail_base: ['サムネイル画像の取得に失敗しました.'] }, base_errors: [] } rescue ArgumentError => e { status: 'failed', recoverable: true, errors: { base: [e.message] }, base_errors: [] } rescue StandardError => e Rails.logger.error( "post_bulk_creator_failure #{ { error: e.class.name, message: e.message }.to_json }") { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } end def normalised_attributes attributes, preflight, index { url: preflight[:url], title: preflight[:title], thumbnail_base: preflight[:thumbnail_base], thumbnail: thumbnail_for(index, attributes), tags: preflight[:tags], parent_post_ids: preflight[:parent_post_ids], original_created_from: preflight[:original_created_from], original_created_before: preflight[:original_created_before], duration: preflight[:duration], video_ms: preflight[:video_ms] } end def thumbnail_for index, attributes return nil if attributes['thumbnail_base'].present? || attributes[:thumbnail_base].present? @thumbnails[index] end def existing_post_for_race attributes, record = nil return nil if record.present? && !(record.errors.of_kind?(:url, :taken)) normal_url = PostUrlNormaliser.normalise(attributes['url'] || attributes[:url]) return nil if normal_url.blank? compact_existing_post(Post.with_attached_thumbnail.find_by(url: normal_url)) end def compact_existing_post post PostCompactRepr.base(post) end end