From 31fc32b37721f298b93b2bac92031e55f5374a47 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 22 Sep 2026 19:22:16 +0900 Subject: [PATCH] #419 --- .../app/controllers/nico_tags_controller.rb | 31 +++----- .../controllers/post_versions_controller.rb | 25 +++--- backend/app/controllers/posts_controller.rb | 17 ++-- .../controllers/tag_children_controller.rb | 2 - backend/app/controllers/tags_controller.rb | 79 +++++++++++-------- backend/app/models/external_tag.rb | 15 ++++ backend/app/models/nico_tag_version.rb | 2 +- backend/app/models/post.rb | 37 ++++++--- backend/app/models/post_external_tag.rb | 4 + backend/app/models/tag.rb | 36 ++------- .../app/services/nico_tag_version_recorder.rb | 3 +- backend/app/services/post_create_plan.rb | 44 +++++------ backend/app/services/tag_versioning.rb | 24 +----- backend/db/schema.rb | 23 +++++- backend/lib/tasks/sync_nico.rake | 10 +-- 15 files changed, 181 insertions(+), 171 deletions(-) create mode 100644 backend/app/models/external_tag.rb create mode 100644 backend/app/models/post_external_tag.rb diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index ce63840..68c35f5 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -15,23 +15,22 @@ class NicoTagsController < ApplicationController limit = 1 if limit < 1 post_tag_max_sql = - PostTag - .select('tag_id, MAX(created_at) AS max_created_at') - .group('tag_id') + PostExternalTag + .select('external_tag_id, MAX(created_at) AS max_created_at') + .group('external_tag_id') .to_sql - q = Tag.nico_tags - .joins(:tag_name) - .joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \ - 'ON post_tag_max.tag_id = tags.id') - .includes(:tag_name, tag_name: :wiki_page, linked_tags: { tag_name: :wiki_page }) - q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name + q = + ExternalTag + .joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \ + 'ON post_tag_max.external_tag_id = external_tags.id') + q = q.where('external_tags.name LIKE ?', "%#{ name }%") if name if linked_tag linked_tag_ids = - Tag - .joins(:tag_name) - .where('tag_names.name LIKE ?', "%#{ linked_tag }%") - .pluck(:id) + Tag + .joins(:tag_name) + .where('tag_names.name LIKE ?', "%#{ linked_tag }%") + .pluck(:id) linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id) q = q.where(id: linked_nico_tag_ids) end @@ -72,8 +71,7 @@ class NicoTagsController < ApplicationController id = params[:id].to_i - tag = Tag.find(id) - return render_bad_request('ニコニコ・タグを指定してください.') unless tag.nico? + tag = ExternalTag.find(id) linked_tag_names = params[:tags].to_s.split linked_tags = nil @@ -81,9 +79,6 @@ class NicoTagsController < ApplicationController ApplicationRecord.transaction do linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false, with_no_deerjikist: false) - if linked_tags.any? { |t| t.nico? } - raise Tag::NicoTagNormalisationError - end TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user) diff --git a/backend/app/controllers/post_versions_controller.rb b/backend/app/controllers/post_versions_controller.rb index e938a11..d337fe1 100644 --- a/backend/app/controllers/post_versions_controller.rb +++ b/backend/app/controllers/post_versions_controller.rb @@ -27,8 +27,8 @@ class PostVersionsController < ApplicationController 'prev.original_created_before AS prev_original_created_before') q = q.where('post_versions.post_id = ?', post_id) if post_id if tag_id - q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('id', #{ tag_id })) " + - "OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))") + q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('tag_id', #{ tag_id })) " + + "OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('tag_id', #{ tag_id }))") end count = q.except(:select, :order, :limit, :offset).count @@ -113,21 +113,18 @@ class PostVersionsController < ApplicationController end end - def build_version_tags(cur_tags, prev_tags) + def build_version_tags cur_tags, prev_tags (cur_tags | prev_tags).map do |name| type = - if cur_tags.include?(name) && prev_tags.include?(name) - 'context' - elsif cur_tags.include?(name) - 'added' - else - 'removed' - end + if cur_tags.include?(name) && prev_tags.include?(name) + 'context' + elsif cur_tags.include?(name) + 'added' + else + 'removed' + end - { - name:, - type: - } + { name:, type: } end end end diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index edbc392..0239644 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -671,7 +671,6 @@ class PostsController < ApplicationController post .post_tags .joins(tag: :tag_name) - .merge(Tag.not_nico) .merge(Tag.where(deprecated_at: nil)) .includes(:sections, tag: :tag_name) .order('tag_names.name') @@ -689,8 +688,7 @@ class PostsController < ApplicationController tag_names:, video_ms_param:, duration_param:, parent_post_ids: validate_original_created_values!(original_created_from, original_created_before) Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true, - with_sections: true) => - { tags:, sections: } + with_sections: true) => { tags:, sections: } tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:) @@ -833,15 +831,12 @@ class PostsController < ApplicationController original_created_from: snapshot[:original_created_from], original_created_before: snapshot[:original_created_before]) - Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false, - deny_deprecated: true, - with_sections: true) => - { tags: editable_tags, sections: } - TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user) + Tag.normalise_tags!(snapshot[:tag_names], + with_tagme: false, + deny_deprecated: true, + with_sections: true) => { tags:, sections: } + TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user) - readonly_tags = post.tags.nico.to_a - - tags = readonly_tags + editable_tags tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil diff --git a/backend/app/controllers/tag_children_controller.rb b/backend/app/controllers/tag_children_controller.rb index cfdde30..ebffad7 100644 --- a/backend/app/controllers/tag_children_controller.rb +++ b/backend/app/controllers/tag_children_controller.rb @@ -10,7 +10,6 @@ class TagChildrenController < ApplicationController parent = Tag.find(parent_id) child = Tag.find(child_id) - return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico? ApplicationRecord.transaction do TagVersioning.ensure_snapshot!(child, created_by_user: current_user) @@ -33,7 +32,6 @@ class TagChildrenController < ApplicationController parent = Tag.find(parent_id) child = Tag.find(child_id) - return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico? ApplicationRecord.transaction do TagVersioning.ensure_snapshot!(child, created_by_user: current_user) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index f152d19..85ef6d2 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -100,13 +100,14 @@ class TagsController < ApplicationController def autocomplete q = params[:q].to_s.strip.sub(/\Anot:/i, '') + prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%" with_nico = bool?(:nico, default: true) present_only = bool?(:present, default: true) alias_rows = TagName - .where('name LIKE ?', "#{ q }%") + .where('name LIKE ?', prefix) .where.not(canonical_id: nil) .pluck(:canonical_id, :name) @@ -118,29 +119,57 @@ class TagsController < ApplicationController matched_alias_by_tag_name_id[canonical_id] ||= alias_name end - base = Tag.joins(:tag_name) - .includes(:tag_name, :materials, tag_name: :wiki_page) - .where(deprecated_at: nil) + base = + Tag + .joins(:tag_name) + .includes(:tag_name, :materials, tag_name: :wiki_page) + .where(deprecated_at: nil) base = base.where('tags.post_count > 0') if present_only - canonical_hit = - base - .where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') + - 'tag_names.name LIKE ?'), - *(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%") + canonical_hit = base.where('tag_names.name LIKE ?', prefix) - tags = - if canonical_ids.present? + internal_tags = + if with_nico canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq)) else canonical_hit end - tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a + internal_rows = + internal_tags + .order(Arel.sql('tags.post_count DESC, tag_names.name')) + .limit(20) + .map { |tag| + TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id]) + } - render json: tags.map { |tag| - TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id]) - } + return render json: internal_rows unless with_nico + + external_base = ExternalTag.all + external_base = external_base.where('post_count > 0') if present_only + + external_rows = + external_base + .where("CONCAT(platform, ':', name) LIKE ? OR name LIKE ?", prefix, prefix) + .order(post_count: :desc, name: :asc) + .limit(20) + .map { |tag| + { 'id' => tag.id, + 'name' => "#{ tag.platform }:#{ tag.name }", + 'category' => 'nico', + 'deprecated_at' => nil, + 'created_at' => tag.created_at, + 'updated_at' => tag.created_at, + 'post_count' => tag.post_count, + 'matched_alias' => nil } + } + + rows = + (internal_rows + external_rows) + .sort_by { |row| [-row[:post_count], row[:name]] } + .first(20) + + render json: rows end def show @@ -282,11 +311,7 @@ class TagsController < ApplicationController parent_names = params[:parent_tags].to_s.split.uniq deprecated = bool?(:deprecated) - if tag.nico? && deprecated - return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated - end - - if tag.nico? || category == 'nico' + if category == 'nico' return render_unprocessable_entity 'ニコタグは変更できません.', field: :category end @@ -334,13 +359,9 @@ class TagsController < ApplicationController tag = Tag.find(params[:id]) - if tag.nico? && deprecated_given && deprecated - return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated - end - return unless validate_tag_rename(tag, name) - if tag.nico? || (category.present? && category == 'nico') + if category.present? && category == 'nico' return render_unprocessable_entity 'ニコタグは変更できません.', field: :category end @@ -547,11 +568,6 @@ class TagsController < ApplicationController end def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil - if tag.nico? - NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) - return - end - TagVersionRecorder.record!(tag:, event_type:, created_by_user:) return unless name_changed @@ -661,8 +677,7 @@ class TagsController < ApplicationController def update_parent_tags! tag, parent_names parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false, - with_no_deerjikist: false, - deny_nico: true) + with_no_deerjikist: false) old_parent_tags = tag.parents.to_a diff --git a/backend/app/models/external_tag.rb b/backend/app/models/external_tag.rb new file mode 100644 index 0000000..305c641 --- /dev/null +++ b/backend/app/models/external_tag.rb @@ -0,0 +1,15 @@ +class ExternalTag < ApplicationRecord + enum :platform, nico: 'nico' + validates :platform, presence: true, inclusion: { in: ExternalTag.platforms.keys } + + has_many :post_external_tags, dependent: :delete_all + has_many :posts, through: :post_external_tags + + has_many :nico_tag_versions, foreign_key: :tag_id, inverse_of: :external_tag + has_many :nico_tag_relations, foreign_key: :tag_id, dependent: :destroy + has_many :linked_tags, through: :nico_tag_relations, source: :nico_tag + + def snapshot_linked_tag_names + linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') + end +end diff --git a/backend/app/models/nico_tag_version.rb b/backend/app/models/nico_tag_version.rb index b57252a..e83ecff 100644 --- a/backend/app/models/nico_tag_version.rb +++ b/backend/app/models/nico_tag_version.rb @@ -1,7 +1,7 @@ class NicoTagVersion < ApplicationRecord include VersionRecord - belongs_to :tag + belongs_to :external_tag, foreign_key: :tag_id, inverse_of: :nico_tag_versions validates :name, presence: true end diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index e4fd44c..f10eb07 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -92,6 +92,9 @@ class Post < ApplicationRecord inverse_of: :parent_post has_many :children, through: :child_post_implications, source: :post + has_many :post_external_tags, dependent: :delete_all + has_many :external_tags, through: :post_external_tags + has_one_attached :thumbnail attribute :version_no, :integer, default: 1 @@ -150,19 +153,27 @@ class Post < ApplicationRecord end def snapshot_tags_json - post_tags - .joins(tag: :tag_name) - .includes(:sections, tag: :tag_name) - .order('tags.id') - .map do |pt| - { 'id' => pt.tag.id, - 'version_no' => pt.tag.version_no, - 'name' => pt.tag.name, - 'category' => pt.tag.category, - 'sections' => pt.sections.sort_by(&:begin_ms).map { - { 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms } - } } - end + tag_snapshots = + post_tags + .joins(tag: :tag_name) + .includes(:sections, tag: :tag_name) + .order('tags.id') + .map { |pt| + { 'tag_id' => pt.tag.id, + 'version_no' => pt.tag.version_no, + 'name' => pt.tag.name, + 'category' => pt.tag.category, + 'sections' => pt.sections.sort_by(&:begin_ms).map { + { 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms } + } } + } + + external_tag_snapshots = + post_external_tags.order(:external_tag_id).map { + { 'external_tag_id' => _1.external_tag_id } + } + + tag_snapshots + external_tag_snapshots end def self.section_literal section diff --git a/backend/app/models/post_external_tag.rb b/backend/app/models/post_external_tag.rb new file mode 100644 index 0000000..38cb528 --- /dev/null +++ b/backend/app/models/post_external_tag.rb @@ -0,0 +1,4 @@ +class PostExternalTag < ApplicationRecord + belongs_to :post + belongs_to :external_tag +end diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 8a09ef4..1e37685 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -31,10 +31,6 @@ class Tag < ApplicationRecord has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy has_many :linked_tags, through: :nico_tag_relations, source: :tag - has_many :reversed_nico_tag_relations, - class_name: 'NicoTagRelation', foreign_key: :tag_id, dependent: :destroy - has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag - has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy has_many :children, through: :tag_implications, source: :tag @@ -50,7 +46,6 @@ class Tag < ApplicationRecord has_many :materials has_many :tag_versions - has_many :nico_tag_versions belongs_to :tag_name delegate :wiki_page, to: :tag_name @@ -65,17 +60,15 @@ class Tag < ApplicationRecord character: 'character', general: 'general', material: 'material', - nico: 'nico', meta: 'meta' validates :category, presence: true, inclusion: { in: Tag.categories.keys } - validate :nico_tag_name_must_start_with_nico + validate :tag_name_mustnt_start_with_nico validate :tag_name_must_be_canonical validate :category_must_be_deerjikist_with_deerjikists - validate :nico_tags_cannot_be_deprecated - scope :nico_tags, -> { nico } + def self.nico_tags = ExternalTag.where(platform: :nico) CATEGORY_PREFIXES = { 'general:' => :general, @@ -110,10 +103,9 @@ class Tag < ApplicationRecord def self.normalise_tags! tag_names, with_tagme: true, with_no_deerjikist: true, - deny_nico: true, deny_deprecated: false, with_sections: false - if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') } + if tag_names.any? { |n| n.downcase.start_with?('nico:') } raise NicoTagNormalisationError end @@ -273,12 +265,8 @@ class Tag < ApplicationRecord TagVersioning.record!(source_tag, event_type: :discard, created_by_user:) source_tag.destroy! - if source_tag.nico? - source_tag_name.destroy! - else - source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, - updated_at: Time.current) - end + source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, + updated_at: Time.current) TagVersioning.record!(target_tag, event_type: :update, created_by_user:) end @@ -305,11 +293,9 @@ class Tag < ApplicationRecord private - def nico_tag_name_must_start_with_nico - n = name.to_s - if ((nico? && !(n.downcase.start_with?('nico:'))) || - (!(nico?) && n.downcase.start_with?('nico:'))) - errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.' + def tag_name_mustnt_start_with_nico + if name.to_s.downcase.start_with?('nico:') + errors.add :name, 'タグの命名規則に反してゐます.' end end @@ -350,10 +336,4 @@ class Tag < ApplicationRecord total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i end - - def nico_tags_cannot_be_deprecated - if nico? && deprecated_at.present? - errors.add :deprecated_at, 'ニコタグは廃止できません.' - end - end end diff --git a/backend/app/services/nico_tag_version_recorder.rb b/backend/app/services/nico_tag_version_recorder.rb index 8f1be7f..34dfbde 100644 --- a/backend/app/services/nico_tag_version_recorder.rb +++ b/backend/app/services/nico_tag_version_recorder.rb @@ -14,6 +14,7 @@ class NicoTagVersionRecorder < VersionRecorder def record_key = :tag def snapshot_attributes - { name: @record.name, linked_tags: @record.snapshot_linked_tag_names.join(' ') } + { name: "#{ @record.platform }:#{ @record.name }", + linked_tags: @record.snapshot_linked_tag_names.join(' ') } end end diff --git a/backend/app/services/post_create_plan.rb b/backend/app/services/post_create_plan.rb index a6f38b7..fedd574 100644 --- a/backend/app/services/post_create_plan.rb +++ b/backend/app/services/post_create_plan.rb @@ -20,23 +20,22 @@ class PostCreatePlan parent_post_ids = normalise_parent_post_ids validate_parent_post_ids!(parent_post_ids) - { - url: @attributes[:url], - title: @attributes[:title].to_s, - thumbnail_base: @attributes[:thumbnail_base].presence, - original_created_from: @attributes[:original_created_from].presence, - original_created_before: @attributes[:original_created_before].presence, - tags: serialised_tags(direct_tag_specs, tag_sections), - display_tags: display_tags(direct_tag_specs, tag_sections), - duration: @attributes[:duration].to_s, - video_ms: video_ms, - parent_post_ids: parent_post_ids.join(' '), - direct_tag_specs: direct_tag_specs, - default_tag_specs: default_tag_specs, - snapshot_tag_specs: snapshot_tag_specs, - post_tag_specs: post_tag_specs, - tag_sections: tag_sections, - normalised_parent_post_ids: parent_post_ids } + { url: @attributes[:url], + title: @attributes[:title].to_s, + thumbnail_base: @attributes[:thumbnail_base].presence, + original_created_from: @attributes[:original_created_from].presence, + original_created_before: @attributes[:original_created_before].presence, + tags: serialised_tags(direct_tag_specs, tag_sections), + display_tags: display_tags(direct_tag_specs, tag_sections), + duration: @attributes[:duration].to_s, + video_ms: video_ms, + parent_post_ids: parent_post_ids.join(' '), + direct_tag_specs: direct_tag_specs, + default_tag_specs: default_tag_specs, + snapshot_tag_specs: snapshot_tag_specs, + post_tag_specs: post_tag_specs, + tag_sections: tag_sections, + normalised_parent_post_ids: parent_post_ids } end private @@ -50,7 +49,6 @@ class PostCreatePlan tag_names.each do |raw_name| tag_name, category, sections = parse_raw_tag_name(raw_name) existing_tag = existing_tags_by_name[tag_name] - raise Tag::NicoTagNormalisationError if existing_tag&.nico? raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated? direct_tag_specs << { @@ -139,14 +137,14 @@ class PostCreatePlan Tag.expand_parent_tags(existing_snapshot_tags) .reject(&:deprecated?) .map { |tag| - { - name: tag.name, - category: tag.category.to_sym } } + { name: tag.name, + category: tag.category.to_sym } + } merge_tag_specs(snapshot_tag_specs + expanded_parent_specs) end def merge_tag_specs specs - specs.each_with_object({ }) do |spec, merged| + specs.each_with_object({ }) { |spec, merged| merged[spec[:name]] = if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general merged[spec[:name]] @@ -155,7 +153,7 @@ class PostCreatePlan name: spec[:name], category: spec[:category] } end - end.values.sort_by { _1[:name] } + }.values.sort_by { _1[:name] } end def existing_tags_by_name diff --git a/backend/app/services/tag_versioning.rb b/backend/app/services/tag_versioning.rb index ae5b3dd..d874a3e 100644 --- a/backend/app/services/tag_versioning.rb +++ b/backend/app/services/tag_versioning.rb @@ -1,32 +1,16 @@ class TagVersioning def self.record! tag, event_type:, created_by_user: - if tag.nico? - NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) - else - TagVersionRecorder.record!(tag:, event_type:, created_by_user:) - end + TagVersionRecorder.record!(tag:, event_type:, created_by_user:) end def self.ensure_snapshot! tag, created_by_user: - if tag.nico? - return if tag.nico_tag_versions.exists? + return if tag.tag_versions.exists? - NicoTagVersionRecorder.record!(tag:, event_type: :create, created_by_user:) - else - return if tag.tag_versions.exists? - - TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:) - end + TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:) end def self.record_tag_snapshot! tag, created_by_user: - event_type = - if tag.nico? - tag.nico_tag_versions.exists? ? :update : :create - else - tag.tag_versions.exists? ? :update : :create - end - + event_type = tag.tag_versions.exists? ? :update : :create record!(tag, event_type:, created_by_user:) end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 1cb6423..c4ce56a 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do +ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -48,6 +48,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do t.index ["tag_id"], name: "index_deerjikists_on_tag_id" end + create_table "external_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "platform", limit: 16, null: false + t.string "name", null: false + t.integer "post_count", default: 0, null: false + t.datetime "created_at", null: false + t.index ["platform", "name"], name: "index_external_tags_on_platform_and_name", unique: true + end + create_table "gekanator_ai_runs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "model", null: false t.integer "input_tokens", default: 0, null: false @@ -281,6 +289,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive" end + create_table "post_external_tags", primary_key: ["post_id", "external_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "post_id", null: false + t.bigint "external_tag_id", null: false + t.datetime "created_at", null: false + t.index ["external_tag_id"], name: "index_post_external_tags_on_external_tag_id" + end + create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "post_id", null: false t.bigint "parent_post_id", null: false @@ -352,7 +367,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive" t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid" t.check_constraint "`version_no` > 0", name: "post_versions_version_no_positive" - t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema" + t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"tag_id\",\"version_no\",\"sections\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"external_tag_id\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"external_tag_id\"],\"additionalProperties\":false}]}}',`tags_json`)", name: "chk_post_versions_tags_json_schema" end create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -700,9 +715,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do add_foreign_key "materials", "tags" add_foreign_key "materials", "users", column: "created_by_user_id" add_foreign_key "materials", "users", column: "updated_by_user_id" + add_foreign_key "nico_tag_relations", "external_tags", column: "nico_tag_id" add_foreign_key "nico_tag_relations", "tags" - add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id" add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id" + add_foreign_key "post_external_tags", "external_tags" + add_foreign_key "post_external_tags", "posts" add_foreign_key "post_implications", "posts" add_foreign_key "post_implications", "posts", column: "parent_post_id" add_foreign_key "post_similarities", "posts" diff --git a/backend/lib/tasks/sync_nico.rake b/backend/lib/tasks/sync_nico.rake index f95fba1..2331f5b 100644 --- a/backend/lib/tasks/sync_nico.rake +++ b/backend/lib/tasks/sync_nico.rake @@ -115,7 +115,7 @@ namespace :nico do # 既存のタグ Id. 集合 kept_tag_ids = tags.pluck(:id).to_set # うち内部タグ Id. 集合 - kept_non_nico_tag_ids = tags.not_nico.pluck(:id).to_set + kept_non_nico_tag_ids = tags.pluck(:id).to_set # 記載すべき外部タグ Id. および連携される内部タグ Id. のリスト desired_nico_tag_based_ids = [] @@ -123,11 +123,11 @@ namespace :nico do desired_non_nico_tag_ids = [] datum['tags'].each do |raw| - name = TagNameSanitisationRule.sanitise("nico:#{ raw }") - tag = Tag.find_or_create_by_tag_name!(name, category: :nico) + tag = ExternalTag.find_or_create_by!(platform: :nico, name: raw) - event_type = tag.nico_tag_versions.exists? ? :update : :create - NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil) + unless tag.nico_tag_versions.exists? + NicoTagVersionRecorder.record!(tag:, event_type: :create, created_by_user: nil) + end desired_nico_tag_based_ids << tag.id