feat(x-bot): YouTube 영상 추출 옵션 추가
X 봇 설정에서 트윗 내 YouTube 링크 자동 추출 기능을 온/오프 가능하게 함: - bot_x 테이블에 extract_youtube 컬럼 추가 (기본값: false) - 고급 설정에 "YouTube 영상 추출" 토글 추가 - extractYoutube가 true일 때만 YouTube 일정 자동 생성 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ecc4e6263
commit
ba7def935c
4 changed files with 48 additions and 6 deletions
|
|
@ -70,6 +70,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
: row.text_filters)
|
: row.text_filters)
|
||||||
: [],
|
: [],
|
||||||
includeRetweets: row.include_retweets === 1,
|
includeRetweets: row.include_retweets === 1,
|
||||||
|
extractYoutube: row.extract_youtube === 1,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const xBotResponse = {
|
||||||
avatar_url: { type: 'string' },
|
avatar_url: { type: 'string' },
|
||||||
text_filters: { type: 'array', items: { type: 'string' } },
|
text_filters: { type: 'array', items: { type: 'string' } },
|
||||||
include_retweets: { type: 'boolean' },
|
include_retweets: { type: 'boolean' },
|
||||||
|
extract_youtube: { type: 'boolean' },
|
||||||
cron_interval: { type: 'integer' },
|
cron_interval: { type: 'integer' },
|
||||||
enabled: { type: 'boolean' },
|
enabled: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
|
|
@ -42,6 +43,7 @@ function formatBotResponse(row) {
|
||||||
: row.text_filters)
|
: row.text_filters)
|
||||||
: [],
|
: [],
|
||||||
include_retweets: row.include_retweets === 1,
|
include_retweets: row.include_retweets === 1,
|
||||||
|
extract_youtube: row.extract_youtube === 1,
|
||||||
cron_interval: row.cron_interval,
|
cron_interval: row.cron_interval,
|
||||||
enabled: row.enabled === 1,
|
enabled: row.enabled === 1,
|
||||||
};
|
};
|
||||||
|
|
@ -160,6 +162,7 @@ export default async function xBotsRoutes(fastify) {
|
||||||
avatar_url: { type: ['string', 'null'] },
|
avatar_url: { type: ['string', 'null'] },
|
||||||
text_filters: { type: ['array', 'null'], items: { type: 'string' } },
|
text_filters: { type: ['array', 'null'], items: { type: 'string' } },
|
||||||
include_retweets: { type: 'boolean', default: false },
|
include_retweets: { type: 'boolean', default: false },
|
||||||
|
extract_youtube: { type: 'boolean', default: false },
|
||||||
cron_interval: { type: 'integer', default: 1 },
|
cron_interval: { type: 'integer', default: 1 },
|
||||||
},
|
},
|
||||||
required: ['username'],
|
required: ['username'],
|
||||||
|
|
@ -177,6 +180,7 @@ export default async function xBotsRoutes(fastify) {
|
||||||
avatar_url,
|
avatar_url,
|
||||||
text_filters,
|
text_filters,
|
||||||
include_retweets = false,
|
include_retweets = false,
|
||||||
|
extract_youtube = false,
|
||||||
cron_interval = 1,
|
cron_interval = 1,
|
||||||
} = request.body;
|
} = request.body;
|
||||||
|
|
||||||
|
|
@ -190,14 +194,15 @@ export default async function xBotsRoutes(fastify) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, cron_interval, enabled)
|
`INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, extract_youtube, cron_interval, enabled)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||||
[
|
[
|
||||||
username,
|
username,
|
||||||
display_name || null,
|
display_name || null,
|
||||||
avatar_url || null,
|
avatar_url || null,
|
||||||
text_filters && text_filters.length > 0 ? JSON.stringify(text_filters) : null,
|
text_filters && text_filters.length > 0 ? JSON.stringify(text_filters) : null,
|
||||||
include_retweets ? 1 : 0,
|
include_retweets ? 1 : 0,
|
||||||
|
extract_youtube ? 1 : 0,
|
||||||
cron_interval,
|
cron_interval,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -215,6 +220,7 @@ export default async function xBotsRoutes(fastify) {
|
||||||
nitterUrl: process.env.NITTER_URL || 'http://nitter:8080',
|
nitterUrl: process.env.NITTER_URL || 'http://nitter:8080',
|
||||||
textFilters: text_filters || [],
|
textFilters: text_filters || [],
|
||||||
includeRetweets: include_retweets,
|
includeRetweets: include_retweets,
|
||||||
|
extractYoutube: extract_youtube,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전체 동기화 (async, 응답 대기하지 않음)
|
// 전체 동기화 (async, 응답 대기하지 않음)
|
||||||
|
|
@ -255,6 +261,7 @@ export default async function xBotsRoutes(fastify) {
|
||||||
avatar_url: { type: ['string', 'null'] },
|
avatar_url: { type: ['string', 'null'] },
|
||||||
text_filters: { type: ['array', 'null'], items: { type: 'string' } },
|
text_filters: { type: ['array', 'null'], items: { type: 'string' } },
|
||||||
include_retweets: { type: 'boolean' },
|
include_retweets: { type: 'boolean' },
|
||||||
|
extract_youtube: { type: 'boolean' },
|
||||||
cron_interval: { type: 'integer' },
|
cron_interval: { type: 'integer' },
|
||||||
enabled: { type: 'boolean' },
|
enabled: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
|
|
@ -297,6 +304,10 @@ export default async function xBotsRoutes(fastify) {
|
||||||
fields.push('include_retweets = ?');
|
fields.push('include_retweets = ?');
|
||||||
values.push(updates.include_retweets ? 1 : 0);
|
values.push(updates.include_retweets ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
if (updates.extract_youtube !== undefined) {
|
||||||
|
fields.push('extract_youtube = ?');
|
||||||
|
values.push(updates.extract_youtube ? 1 : 0);
|
||||||
|
}
|
||||||
if (updates.cron_interval !== undefined) {
|
if (updates.cron_interval !== undefined) {
|
||||||
fields.push('cron_interval = ?');
|
fields.push('cron_interval = ?');
|
||||||
values.push(updates.cron_interval);
|
values.push(updates.cron_interval);
|
||||||
|
|
|
||||||
|
|
@ -186,10 +186,12 @@ async function xBotPlugin(fastify, opts) {
|
||||||
// Meilisearch 동기화
|
// Meilisearch 동기화
|
||||||
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
||||||
addedCount++;
|
addedCount++;
|
||||||
// YouTube 링크 처리
|
// YouTube 링크 처리 (옵션이 켜져 있을 때만)
|
||||||
|
if (bot.extractYoutube === true) {
|
||||||
ytAddedCount += await processYoutubeLinks(tweet);
|
ytAddedCount += await processYoutubeLinks(tweet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { addedCount: addedCount + ytAddedCount, total: tweets.length, tweetCount: addedCount, ytCount: ytAddedCount };
|
return { addedCount: addedCount + ytAddedCount, total: tweets.length, tweetCount: addedCount, ytCount: ytAddedCount };
|
||||||
}
|
}
|
||||||
|
|
@ -215,9 +217,12 @@ async function xBotPlugin(fastify, opts) {
|
||||||
// Meilisearch 동기화
|
// Meilisearch 동기화
|
||||||
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
||||||
addedCount++;
|
addedCount++;
|
||||||
|
// YouTube 링크 처리 (옵션이 켜져 있을 때만)
|
||||||
|
if (bot.extractYoutube === true) {
|
||||||
ytAddedCount += await processYoutubeLinks(tweet);
|
ytAddedCount += await processYoutubeLinks(tweet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { addedCount: addedCount + ytAddedCount, total: tweets.length, tweetCount: addedCount, ytCount: ytAddedCount };
|
return { addedCount: addedCount + ytAddedCount, total: tweets.length, tweetCount: addedCount, ytCount: ytAddedCount };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
const [textFilters, setTextFilters] = useState([]);
|
const [textFilters, setTextFilters] = useState([]);
|
||||||
const [filterInput, setFilterInput] = useState('');
|
const [filterInput, setFilterInput] = useState('');
|
||||||
const [includeRetweets, setIncludeRetweets] = useState(false);
|
const [includeRetweets, setIncludeRetweets] = useState(false);
|
||||||
|
const [extractYoutube, setExtractYoutube] = useState(false);
|
||||||
|
|
||||||
// X 봇 상세 조회 (수정 모드)
|
// X 봇 상세 조회 (수정 모드)
|
||||||
const { data: bot, isLoading: botLoading } = useQuery({
|
const { data: bot, isLoading: botLoading } = useQuery({
|
||||||
|
|
@ -157,7 +158,8 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
setInterval(bot.cron_interval || 1);
|
setInterval(bot.cron_interval || 1);
|
||||||
setTextFilters(bot.text_filters || []);
|
setTextFilters(bot.text_filters || []);
|
||||||
setIncludeRetweets(bot.include_retweets || false);
|
setIncludeRetweets(bot.include_retweets || false);
|
||||||
setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || bot.include_retweets || false);
|
setExtractYoutube(bot.extract_youtube || false);
|
||||||
|
setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || bot.include_retweets || bot.extract_youtube || false);
|
||||||
} else if (!botId) {
|
} else if (!botId) {
|
||||||
// 추가 모드
|
// 추가 모드
|
||||||
setUsername('');
|
setUsername('');
|
||||||
|
|
@ -166,6 +168,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
setTextFilters([]);
|
setTextFilters([]);
|
||||||
setFilterInput('');
|
setFilterInput('');
|
||||||
setIncludeRetweets(false);
|
setIncludeRetweets(false);
|
||||||
|
setExtractYoutube(false);
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
}
|
}
|
||||||
}, [isOpen, bot, botId]);
|
}, [isOpen, bot, botId]);
|
||||||
|
|
@ -202,6 +205,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
avatar_url: profileInfo.avatarUrl,
|
avatar_url: profileInfo.avatarUrl,
|
||||||
text_filters: textFilters.length > 0 ? textFilters : null,
|
text_filters: textFilters.length > 0 ? textFilters : null,
|
||||||
include_retweets: includeRetweets,
|
include_retweets: includeRetweets,
|
||||||
|
extract_youtube: extractYoutube,
|
||||||
cron_interval: interval,
|
cron_interval: interval,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -374,6 +378,27 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* YouTube 영상 추출 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">YouTube 영상 추출</label>
|
||||||
|
<p className="text-xs text-gray-400">트윗에 YouTube 링크가 있으면 유튜브 일정에 추가합니다</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExtractYoutube(!extractYoutube)}
|
||||||
|
className={`relative w-11 h-6 rounded-full transition-colors ${
|
||||||
|
extractYoutube ? 'bg-sky-500' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
|
||||||
|
extractYoutube ? 'translate-x-5' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 텍스트 필터 */}
|
{/* 텍스트 필터 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-600 mb-1">텍스트 필터</label>
|
<label className="block text-sm text-gray-600 mb-1">텍스트 필터</label>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue