fix: feedback round 2 — language, subscriptions, feed scope, UX
- Language detection: classify one cleaned+concatenated blob (strip emoji, @mentions, #tags, numbers, punctuation); fixes caps/emoji-heavy channels (e.g. Nessaj -> Hungarian, no more bogus Chinese/Korean) - Feed now joins the user's subscriptions, so unsubscribing on YouTube removes a channel from the feed; periodic subscription re-sync job picks up changes - Watched/Saved/Hidden views ignore the Shorts/live default-hiding so the full set is visible (fixes hidden videos missing from the Hidden view) - Persist feed filters + search across reloads (localStorage) - 3D polish: cards lift with shadow on hover; chips/buttons get depth and a press effect; undo toast lasts longer
This commit is contained in:
parent
8c245e986f
commit
e07a37622d
9 changed files with 97 additions and 27 deletions
|
|
@ -5,7 +5,7 @@ detection over a sample of recent video titles. Topics are mapped from YouTube's
|
|||
topicDetails categories and the channel's dominant video category. System tags are
|
||||
regenerated freely; user tags are never touched here.
|
||||
"""
|
||||
from collections import Counter
|
||||
import re
|
||||
|
||||
from sqlalchemy import and_, exists, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
|
@ -145,9 +145,18 @@ def map_topic_slug(slug: str) -> str | None:
|
|||
return _TOPIC_SLUGS.get(slug)
|
||||
|
||||
|
||||
def _clean_title(title: str | None) -> str:
|
||||
"""Strip emojis, @mentions, #tags, URLs, numbers and punctuation so the language
|
||||
detector sees actual words, not caps/emoji-heavy noise."""
|
||||
text = title or ""
|
||||
text = re.sub(r"http\S+", " ", text)
|
||||
text = re.sub(r"[@#]\w+", " ", text)
|
||||
text = re.sub(r"\d+", " ", text)
|
||||
text = re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE).replace("_", " ")
|
||||
return " ".join(w for w in text.split() if len(w) > 1)
|
||||
|
||||
|
||||
def detect_channel_language(db: Session, channel: Channel) -> tuple[str | None, float]:
|
||||
if channel.default_language:
|
||||
return channel.default_language.split("-")[0].lower(), 0.99
|
||||
titles = (
|
||||
db.execute(
|
||||
select(Video.title)
|
||||
|
|
@ -158,20 +167,16 @@ def detect_channel_language(db: Session, channel: Channel) -> tuple[str | None,
|
|||
.scalars()
|
||||
.all()
|
||||
)
|
||||
# Majority vote over individual titles is more robust than one concatenated blob
|
||||
# (short/technical titles otherwise skew the detector).
|
||||
votes: Counter[str] = Counter()
|
||||
for title in titles:
|
||||
cleaned = (title or "").strip()
|
||||
if len(cleaned) < 8:
|
||||
continue
|
||||
lang, _conf = _classify(cleaned)
|
||||
votes[lang] += 1
|
||||
if not votes:
|
||||
return None, 0.0
|
||||
lang, count = votes.most_common(1)[0]
|
||||
total = sum(votes.values())
|
||||
return lang, count / total
|
||||
# Detect over one cleaned, concatenated blob — more context and far less skew from
|
||||
# short, emoji/caps-heavy titles than per-title voting.
|
||||
blob = " ".join(_clean_title(t) for t in titles).strip()
|
||||
if len(blob) >= 15:
|
||||
lang, confidence = _classify(blob)
|
||||
return lang, float(confidence)
|
||||
# Sparse text: fall back to the channel's declared language if any.
|
||||
if channel.default_language:
|
||||
return channel.default_language.split("-")[0].lower(), 0.6
|
||||
return None, 0.0
|
||||
|
||||
|
||||
def compute_channel_topics(db: Session, channel: Channel) -> set[str]:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
|||
from app import quota
|
||||
from app.config import settings
|
||||
from app.models import Channel, OAuthToken, User
|
||||
from app.sync.subscriptions import import_subscriptions
|
||||
from app.sync.videos import (
|
||||
backfill_channel_deep,
|
||||
backfill_channel_recent,
|
||||
|
|
@ -65,6 +66,26 @@ def run_shorts(db: Session) -> dict:
|
|||
return run_shorts_classification(db)
|
||||
|
||||
|
||||
def run_subscription_resync(db: Session) -> dict:
|
||||
"""Re-import every user's subscriptions so unsubscribes and new subscriptions on
|
||||
YouTube are reflected automatically."""
|
||||
users = (
|
||||
db.execute(
|
||||
select(User)
|
||||
.join(OAuthToken)
|
||||
.where(OAuthToken.refresh_token_enc.is_not(None))
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
for user in users:
|
||||
try:
|
||||
import_subscriptions(db, user)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
return {"users": len(users)}
|
||||
|
||||
|
||||
def run_recent_backfill(
|
||||
db: Session, channels: list[Channel] | None = None, max_channels: int | None = None
|
||||
) -> dict:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue