diff --git a/server.js b/server.js index b9bd6a28..74fce167 100644 --- a/server.js +++ b/server.js @@ -1057,6 +1057,65 @@ ${excerpt} } catch (e) { console.error(e); res.status(500).json({ error: 'DB error' }); } }); + // ── Feed Media(カスタムRSSソース管理)──────────────────────────── + // テーブル: feed_media (id SERIAL PK, user_id TEXT, name TEXT, feed_url TEXT, + // site_url TEXT, category TEXT, is_active BOOLEAN DEFAULT true, + // created_at TIMESTAMPTZ DEFAULT NOW()) + + r.get('/feed/media', authMiddleware, async (req, res) => { + try { + const result = await pool.query( + 'SELECT id, name, feed_url, site_url, category, is_active, created_at FROM feed_media WHERE user_id=$1 ORDER BY created_at ASC', + [req.userId] + ); + res.json(result.rows); + } catch (e) { console.error(e); res.status(500).json({ error: 'DB error' }); } + }); + + r.post('/feed/media', authMiddleware, async (req, res) => { + const { name, feed_url, site_url = '', category = 'tech', is_active = true } = req.body || {}; + if (!name || !feed_url) return res.status(400).json({ error: 'name and feed_url required' }); + try { + const result = await pool.query( + 'INSERT INTO feed_media (user_id, name, feed_url, site_url, category, is_active) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id, name, feed_url, site_url, category, is_active, created_at', + [req.userId, name, feed_url, site_url, category, is_active] + ); + res.status(201).json(result.rows[0]); + } catch (e) { console.error(e); res.status(500).json({ error: 'DB error' }); } + }); + + r.patch('/feed/media/:id', authMiddleware, async (req, res) => { + const { name, feed_url, site_url, category, is_active } = req.body || {}; + try { + const fields = []; + const vals = []; + let idx = 1; + if (name !== undefined) { fields.push(`name=$${idx++}`); vals.push(name); } + if (feed_url !== undefined) { fields.push(`feed_url=$${idx++}`); vals.push(feed_url); } + if (site_url !== undefined) { fields.push(`site_url=$${idx++}`); vals.push(site_url); } + if (category !== undefined) { fields.push(`category=$${idx++}`); vals.push(category); } + if (is_active !== undefined) { fields.push(`is_active=$${idx++}`); vals.push(is_active); } + if (fields.length === 0) return res.status(400).json({ error: 'no fields to update' }); + vals.push(req.params.id, req.userId); + const result = await pool.query( + `UPDATE feed_media SET ${fields.join(',')} WHERE id=$${idx} AND user_id=$${idx + 1} RETURNING id, name, feed_url, site_url, category, is_active`, + vals + ); + if (result.rowCount === 0) return res.status(404).json({ error: 'not found' }); + res.json(result.rows[0]); + } catch (e) { console.error(e); res.status(500).json({ error: 'DB error' }); } + }); + + r.delete('/feed/media/:id', authMiddleware, async (req, res) => { + try { + await pool.query( + 'DELETE FROM feed_media WHERE id=$1 AND user_id=$2', + [req.params.id, req.userId] + ); + res.json({ ok: true }); + } catch (e) { console.error(e); res.status(500).json({ error: 'DB error' }); } + }); + // ── TTS (VOICEVOX) ───────────────────────────────────────────── // VOICEVOX_URL: docker-compose.yml で設定(同ネットワーク内なら http://voicevox:50021) // 未設定時は localhost フォールバック(NAS上で同じコンテナホストの場合) @@ -1295,7 +1354,7 @@ ${excerpt} const bodyStart = fullContent.search(/^#{1,2}\s/m); const excerpt = (bodyStart >= 0 ? fullContent.slice(bodyStart) : fullContent).slice(0, 4000); const model = genAITogether.getGenerativeModel({ model: 'gemini-2.5-flash' }); - const prompt = `以下の記事を分析して、JSONのみを返してください(コードブロック不要)。\n\n{"summary":"1〜2文の日本語要約","tags":["タグ1","タグ2","タグ3"]}\n\n- summary: 読者が読む価値があるかを判断できる1〜2文\n- tags: 内容を表す日本語タグを2〜4個(例: AI, テクノロジー, ビジネス, 健康, 旅行, 料理, スポーツ, 政治, 経済, エンタメ, ゲーム, 科学, デザイン)\n\n記事:\n${excerpt}`; + const prompt = `以下の記事を分析して、JSONのみを返してください(コードブロック不要)。\n\n{"summary":"1〜2文の日本語要約","tags":["タグ1","タグ2","タグ3"]}\n\n- summary: 読者が読む価値があるかを判断できる1〜2文\n- tags: 内容を表す具体的な日本語タグを2〜4個。「その他」は絶対に使わないこと。内容が不明な場合でも最も近いカテゴリを選ぶ(例: AI, テクノロジー, ビジネス, 健康, 旅行, 料理, スポーツ, 政治, 経済, エンタメ, ゲーム, 科学, デザイン, ライフスタイル, 教育, 環境, 医療, 法律, 文化, 歴史)\n\n記事:\n${excerpt}`; const timeoutP = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30000)); const result = await Promise.race([model.generateContent(prompt), timeoutP]); const raw = result.response.text().trim();