feat(social): add listening activity, shared rooms and modern chat
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
This commit is contained in:
1 parent
0a05bfe2cd
commit
11a16cd023
59 files changed
+5022
-542
No files matched your search
@@ -0,0 +1,48 @@
|
||||
package wave
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// Only currently visible activity is a social signal. A hidden profile never contributes
|
||||
// its listening history, and self feedback/recent plays retain precedence.
|
||||
func (h *Handler) friendCandidates(ctx context.Context, user uuid.UUID, exclude []string) ([]string, error) {
|
||||
rows, e := h.pool.Query(ctx, `SELECT a.track_id::text FROM listening_activity a
|
||||
JOIN users friend ON friend.id=a.user_id AND NOT friend.is_blocked AND friend.activity_visibility IN ('friends','everyone')
|
||||
JOIN users own ON own.id=$1 AND own.friends_recommendations
|
||||
JOIN friendships f ON f.low_id=LEAST($1::uuid,a.user_id) AND f.high_id=GREATEST($1::uuid,a.user_id) AND f.accepted
|
||||
JOIN tracks t ON t.id=a.track_id JOIN media_files mf ON mf.id=t.primary_file_id AND mf.missing_since IS NULL
|
||||
JOIN library_sources src ON src.id=mf.source_id AND src.status='mounted'
|
||||
WHERE a.updated_at>now()-interval '90 seconds' AND NOT(a.track_id::text=ANY($2::text[]))
|
||||
AND NOT EXISTS(SELECT 1 FROM social_blocks b WHERE (b.user_id=$1 AND b.peer_id=a.user_id) OR (b.user_id=a.user_id AND b.peer_id=$1))
|
||||
AND NOT EXISTS(SELECT 1 FROM track_reactions r WHERE r.user_id=$1 AND r.track_id=a.track_id AND r.reaction='dislike')
|
||||
AND NOT EXISTS(SELECT 1 FROM play_events e WHERE e.user_id=$1 AND e.track_id=a.track_id AND e.type='play_start' AND e.occurred_at>now()-interval '3 hours')
|
||||
GROUP BY a.track_id ORDER BY count(DISTINCT a.user_id) DESC,max(a.updated_at) DESC LIMIT 30`, user, exclude)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
defer rows.Close()
|
||||
return pgx.CollectRows(rows, pgx.RowTo[string])
|
||||
}
|
||||
func blendFriendPick(picks []Pick, ids []string) []Pick {
|
||||
if len(ids) == 0 || len(picks) < 3 {
|
||||
return picks
|
||||
}
|
||||
for _, id := range ids {
|
||||
exists := false
|
||||
for _, p := range picks {
|
||||
if p.ID == id {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
out := append([]Pick(nil), picks...)
|
||||
out[len(out)-1] = Pick{ID: id, Source: "friends", Score: 0.1}
|
||||
return out
|
||||
}
|
||||
}
|
||||
return picks
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package wave
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFriendMixRemainsMinorityAndDoesNotDuplicate(t *testing.T) {
|
||||
original := []Pick{{ID: "one"}, {ID: "two"}, {ID: "three"}, {ID: "four"}, {ID: "five"}}
|
||||
mixed := blendFriendPick(original, []string{"one", "friend"})
|
||||
require.Len(t, mixed, 5)
|
||||
require.Equal(t, "friends", mixed[4].Source)
|
||||
require.Equal(t, "friend", mixed[4].ID)
|
||||
require.Equal(t, "five", original[4].ID)
|
||||
require.Equal(t, original, blendFriendPick(original, []string{"one"}))
|
||||
}
|
||||
@@ -67,14 +67,15 @@ type Feedback struct {
|
||||
|
||||
// BatchRequest — запрос к рекомендателю.
|
||||
type BatchRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Activity *string `json:"activity"`
|
||||
Mood *string `json:"mood"`
|
||||
Seed *Seed `json:"seed,omitempty"`
|
||||
ExcludeIDs []string `json:"exclude_ids"`
|
||||
Feedback []Feedback `json:"feedback"`
|
||||
N int `json:"n"`
|
||||
FriendTrackIDs []string `json:"friend_track_ids,omitempty"`
|
||||
UserID string `json:"user_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Activity *string `json:"activity"`
|
||||
Mood *string `json:"mood"`
|
||||
Seed *Seed `json:"seed,omitempty"`
|
||||
ExcludeIDs []string `json:"exclude_ids"`
|
||||
Feedback []Feedback `json:"feedback"`
|
||||
N int `json:"n"`
|
||||
}
|
||||
|
||||
// Seed — от чего запущена волна.
|
||||
@@ -295,6 +296,11 @@ func (h *Handler) serve(ctx context.Context, s session, fb []Feedback) (gen.Wave
|
||||
UserID: s.UserID.String(), SessionID: s.ID.String(), Activity: s.Activity, Mood: s.Mood,
|
||||
ExcludeIDs: served, Feedback: fb, N: BatchSize,
|
||||
}
|
||||
friendIDs, friendErr := h.friendCandidates(ctx, s.UserID, served)
|
||||
if friendErr != nil {
|
||||
h.log.WarnContext(ctx, "friend music unavailable", "err", friendErr)
|
||||
}
|
||||
req.FriendTrackIDs = friendIDs
|
||||
if s.SeedType != nil && s.SeedID != nil {
|
||||
req.Seed = &Seed{Type: *s.SeedType, ID: s.SeedID.String()}
|
||||
}
|
||||
@@ -311,6 +317,9 @@ func (h *Handler) serve(ctx context.Context, s session, fb []Feedback) (gen.Wave
|
||||
if picks, err = h.fallback(ctx, s, served); err != nil {
|
||||
return gen.WaveBatch{}, err
|
||||
}
|
||||
if s.SeedType == nil && len(served)%10 >= 5 {
|
||||
picks = blendFriendPick(picks, friendIDs)
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]uuid.UUID, 0, len(picks))
|
||||
|
||||
Reference in new issue
Block a user