feat(player): a video's related videos, on demand beside it
The player shows what YouTube lists beside the video, without what the user has watched or hidden. Wide, a Related tab beside the description; stacked, a button beside the info opens them in a drawer. A row plays in the same player (Back returns) and the list stays the opened video's. A switch picks on-topic or Hungarian-trending lists, kept on the account. Each related row says whether the user follows its channel; the row is the video card's narrow form with its whole menu. VideoMenu moves to features/video, where the player can reach it.
This commit is contained in:
@@ -158,6 +158,19 @@ def blocked_ids(db: Session, user: User) -> set[str]:
|
||||
)
|
||||
|
||||
|
||||
def followed_among(db: Session, user: User, channel_ids: set[str]) -> set[str]:
|
||||
"""Which of these channels the user subscribes to."""
|
||||
if not channel_ids:
|
||||
return set()
|
||||
return set(
|
||||
db.scalars(
|
||||
select(Subscription.channel_id).where(
|
||||
owned_by(Subscription.user_id, user), Subscription.channel_id.in_(channel_ids)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def blocked_channels(db: Session, user: User, lang: str) -> list[Channel]:
|
||||
return list(
|
||||
db.execute(
|
||||
|
||||
@@ -284,8 +284,11 @@ def related_videos(
|
||||
# (a cursor) is the same list.
|
||||
if cursor is None:
|
||||
quota.log_action(user.id, quota.QuotaAction.VIDEOS_RELATED)
|
||||
rows = reader_queries.feed_rows_by_ids(db, user, collected)
|
||||
# A related list mixes channels the user follows with ones they never met: each row says which.
|
||||
followed = queries.followed_among(db, user, {r["channel_id"] for r in rows})
|
||||
return {
|
||||
"items": reader_queries.feed_rows_by_ids(db, user, collected),
|
||||
"items": [{**r, "followed": r["channel_id"] in followed} for r in rows],
|
||||
"next_cursor": next_cursor,
|
||||
"has_more": bool(next_cursor),
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app import quota, sysconfig
|
||||
from app.catalog.models import BlockedChannel, Channel, Video
|
||||
from app.catalog.models import BlockedChannel, Channel, Subscription, Video
|
||||
from app.models import QuotaEvent
|
||||
from app.reader.models import TakeoutItem, TakeoutKind, VideoState, VideoUnmark
|
||||
from app.routes import search as search_routes
|
||||
@@ -296,3 +296,22 @@ def test_reading_further_is_the_same_list_not_another_one(db, make_user, as_user
|
||||
assert client.get(f"/api/search/related/{SOURCE}").status_code == 200
|
||||
assert client.get(f"/api/search/related/{SOURCE}?cursor=more").status_code == 200
|
||||
assert quota.actions_today(db, user.id, quota.QuotaAction.VIDEOS_RELATED) == 1
|
||||
|
||||
|
||||
def test_each_row_says_whether_the_user_follows_its_channel(db, make_user, as_user, pages):
|
||||
user = make_user()
|
||||
stranger = make_user()
|
||||
_known(db, 1, channel="UCfollowed1")
|
||||
_known(db, 2, channel="UCstranger1")
|
||||
db.add(Subscription(user_id=user.id, channel_id="UCfollowed1"))
|
||||
db.add(Subscription(user_id=stranger.id, channel_id="UCstranger1"))
|
||||
db.commit()
|
||||
pages["pages"] = [
|
||||
_page([_item(1, "UCfollowed1"), _item(2, "UCstranger1"), _item(3, "UCnobody01")], None)
|
||||
]
|
||||
rows = as_user(user).get(f"/api/search/related/{SOURCE}").json()["items"]
|
||||
assert {r["id"]: r["followed"] for r in rows} == {
|
||||
_vid(1): True,
|
||||
_vid(2): False, # another user's follow is not this one's
|
||||
_vid(3): False,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user