openvibers/openvibe.live Commits
Discover: 'streamers to check out' ranks real recent activity (hours + sessions in 14 days, live first, idle days penalized), category only as a tiebreak
Changes
@@@ -379,13 +379,26 @@ router.get('/discover', optionalAuth, async (req, res) => {try {const cat = me ? (db.getChannelByUserId(me.id) || {}) : {};const category = cat.ai_category || cat.category || null;-- similar = (db.all(`SELECT u.id, u.username, u.display_name, u.avatar_url, u.profile_color, MAX(s.started_at) AS last_live_at, COUNT(s.id) AS sessions,++ // Who is actually active: ranked by hours + sessions in the window (followers only as a++ // nudge, category only as a tiebreak), live people first. 14 days; widen to 30 only if++ // that leaves fewer than 3 names — nobody wants "live 29d ago" as a recommendation.++ const pick = (days) => (db.all(`SELECT u.id, u.username, u.display_name, u.avatar_url, u.profile_color, MAX(s.started_at) AS last_live_at, COUNT(s.id) AS sessions,++ ROUND(SUM(COALESCE(s.duration_seconds, CASE WHEN s.ended_at IS NOT NULL THEN (julianday(s.ended_at) - julianday(s.started_at)) * 86400 ELSE 0 END)) / 3600.0, 1) AS hours,(SELECT COUNT(*) FROM follows f WHERE f.streamer_id = u.id) AS followers,(SELECT COALESCE(ch.ai_category, ch.category) FROM channels ch WHERE ch.user_id = u.id) AS categoryFROM streams s JOIN users u ON u.id = s.user_id-- WHERE s.started_at >= datetime('now', '-30 days') AND COALESCE(u.is_banned, 0) = 0 AND u.id != COALESCE(?, -1)-- GROUP BY u.id ORDER BY (category = ?) DESC, followers DESC, last_live_at DESC LIMIT 8`, [exclude, category]) || [])-- .map(r => ({ ...r, live: live.some(l => l.user_id === r.id), same_category: !!(category && r.category === category) }));++ WHERE s.started_at >= datetime('now', ?) AND COALESCE(u.is_banned, 0) = 0 AND u.id != COALESCE(?, -1)++ GROUP BY u.id HAVING hours >= 0.25 OR sessions >= 2++ ORDER BY last_live_at DESC LIMIT 40`, [`-${days} days`, exclude]) || []);++ let rows = pick(14);++ if (rows.length < 3) rows = pick(30);++ const score = (r) => (live.some(l => l.user_id === r.id) ? 1000 : 0)++ + Number(r.hours || 0) * 2 + Math.min(Number(r.sessions || 0), 10)++ + Math.min(Number(r.followers || 0), 50) * 0.2++ + (category && r.category === category ? 2 : 0)++ - Math.max(0, (Date.now() - Date.parse(String(r.last_live_at).replace(' ', 'T') + 'Z')) / 86400000) * 0.6; // every idle day costs++ similar = rows.map(r => ({ ...r, live: live.some(l => l.user_id === r.id), same_category: !!(category && r.category === category), score: score(r) }))++ .sort((a, b) => b.score - a.score).slice(0, 8);} catch { similar = []; }const data = { channel: me ? { username: me.username, display_name: me.display_name || me.username } : null, live, clips, recaps, star, similar };_discoverCache.set(uname, { at: Date.now(), data });
@@ -379,13 +379,26 @@ router.get('/discover', optionalAuth, async (req, res) => {
@@ -379,13 +379,26 @@ router.get('/discover', optionalAuth, async (req, res) => {
try {
try {
const cat = me ? (db.getChannelByUserId(me.id) || {}) : {};
const cat = me ? (db.getChannelByUserId(me.id) || {}) : {};
const category = cat.ai_category || cat.category || null;
const category = cat.ai_category || cat.category || null;
- similar = (db.all(`SELECT u.id, u.username, u.display_name, u.avatar_url, u.profile_color, MAX(s.started_at) AS last_live_at, COUNT(s.id) AS sessions,
+ // Who is actually active: ranked by hours + sessions in the window (followers only as a
+ // nudge, category only as a tiebreak), live people first. 14 days; widen to 30 only if
+ // that leaves fewer than 3 names — nobody wants "live 29d ago" as a recommendation.
+ const pick = (days) => (db.all(`SELECT u.id, u.username, u.display_name, u.avatar_url, u.profile_color, MAX(s.started_at) AS last_live_at, COUNT(s.id) AS sessions,
+ ROUND(SUM(COALESCE(s.duration_seconds, CASE WHEN s.ended_at IS NOT NULL THEN (julianday(s.ended_at) - julianday(s.started_at)) * 86400 ELSE 0 END)) / 3600.0, 1) AS hours,
(SELECT COUNT(*) FROM follows f WHERE f.streamer_id = u.id) AS followers,
(SELECT COUNT(*) FROM follows f WHERE f.streamer_id = u.id) AS followers,
(SELECT COALESCE(ch.ai_category, ch.category) FROM channels ch WHERE ch.user_id = u.id) AS category
(SELECT COALESCE(ch.ai_category, ch.category) FROM channels ch WHERE ch.user_id = u.id) AS category
FROM streams s JOIN users u ON u.id = s.user_id
FROM streams s JOIN users u ON u.id = s.user_id
- WHERE s.started_at >= datetime('now', '-30 days') AND COALESCE(u.is_banned, 0) = 0 AND u.id != COALESCE(?, -1)
- GROUP BY u.id ORDER BY (category = ?) DESC, followers DESC, last_live_at DESC LIMIT 8`, [exclude, category]) || [])
- .map(r => ({ ...r, live: live.some(l => l.user_id === r.id), same_category: !!(category && r.category === category) }));
+ WHERE s.started_at >= datetime('now', ?) AND COALESCE(u.is_banned, 0) = 0 AND u.id != COALESCE(?, -1)
+ GROUP BY u.id HAVING hours >= 0.25 OR sessions >= 2
+ ORDER BY last_live_at DESC LIMIT 40`, [`-${days} days`, exclude]) || []);
+ let rows = pick(14);
+ if (rows.length < 3) rows = pick(30);
+ const score = (r) => (live.some(l => l.user_id === r.id) ? 1000 : 0)
+ + Number(r.hours || 0) * 2 + Math.min(Number(r.sessions || 0), 10)
+ + Math.min(Number(r.followers || 0), 50) * 0.2
+ + (category && r.category === category ? 2 : 0)
+ - Math.max(0, (Date.now() - Date.parse(String(r.last_live_at).replace(' ', 'T') + 'Z')) / 86400000) * 0.6; // every idle day costs
+ similar = rows.map(r => ({ ...r, live: live.some(l => l.user_id === r.id), same_category: !!(category && r.category === category), score: score(r) }))
+ .sort((a, b) => b.score - a.score).slice(0, 8);
} catch { similar = []; }
} catch { similar = []; }
const data = { channel: me ? { username: me.username, display_name: me.display_name || me.username } : null, live, clips, recaps, star, similar };
const data = { channel: me ? { username: me.username, display_name: me.display_name || me.username } : null, live, clips, recaps, star, similar };
_discoverCache.set(uname, { at: Date.now(), data });
_discoverCache.set(uname, { at: Date.now(), data });