service.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package feed
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "hash/fnv"
  8. "math/rand"
  9. "strconv"
  10. "time"
  11. "viewer/internal/albums"
  12. "viewer/internal/models"
  13. )
  14. type Service struct {
  15. albums *albums.Service
  16. }
  17. type cursorState struct {
  18. Seed int64 `json:"seed"`
  19. Offset int `json:"offset"`
  20. }
  21. type photoRef struct {
  22. albumID string
  23. photo models.PhotoMeta
  24. }
  25. func NewService(albumsService *albums.Service) *Service {
  26. return &Service{albums: albumsService}
  27. }
  28. func (s *Service) Build(ctx context.Context, limit int, cursor string, seedParam string) (models.FeedResponse, error) {
  29. _ = ctx
  30. if limit <= 0 {
  31. limit = 80
  32. }
  33. if limit > 200 {
  34. limit = 200
  35. }
  36. albumsList := s.albums.AllAlbums()
  37. refs := make([]photoRef, 0)
  38. for _, a := range albumsList {
  39. for _, p := range a.Photos {
  40. refs = append(refs, photoRef{albumID: a.AlbumID, photo: p})
  41. }
  42. }
  43. if len(refs) == 0 {
  44. return models.FeedResponse{Items: []models.FeedItem{}}, nil
  45. }
  46. state, err := parseCursor(cursor)
  47. if err != nil {
  48. return models.FeedResponse{}, err
  49. }
  50. if state == nil {
  51. seed := parseSeed(seedParam)
  52. state = &cursorState{Seed: seed, Offset: 0}
  53. }
  54. r := rand.New(rand.NewSource(state.Seed))
  55. for i := 0; i < state.Offset; i++ {
  56. _ = r.Intn(len(refs))
  57. }
  58. items := make([]models.FeedItem, 0, limit)
  59. for i := 0; i < limit; i++ {
  60. idx := r.Intn(len(refs))
  61. ref := refs[idx]
  62. items = append(items, models.FeedItem{
  63. AlbumID: ref.albumID,
  64. I: ref.photo.I,
  65. W: ref.photo.W,
  66. H: ref.photo.H,
  67. Ratio: ref.photo.Ratio,
  68. Src: fmt.Sprintf("/api/image/%s/%d?mode=wall&w=480", ref.albumID, ref.photo.I),
  69. })
  70. }
  71. next := &cursorState{Seed: state.Seed, Offset: state.Offset + len(items)}
  72. nextCursor, err := encodeCursor(next)
  73. if err != nil {
  74. return models.FeedResponse{}, err
  75. }
  76. return models.FeedResponse{Items: items, NextCursor: nextCursor}, nil
  77. }
  78. func parseSeed(seed string) int64 {
  79. if seed == "" {
  80. return time.Now().UnixNano()
  81. }
  82. if n, err := strconv.ParseInt(seed, 10, 64); err == nil {
  83. return n
  84. }
  85. h := fnv.New64a()
  86. _, _ = h.Write([]byte(seed))
  87. return int64(h.Sum64())
  88. }
  89. func parseCursor(raw string) (*cursorState, error) {
  90. if raw == "" {
  91. return nil, nil
  92. }
  93. decoded, err := base64.RawURLEncoding.DecodeString(raw)
  94. if err != nil {
  95. return nil, fmt.Errorf("invalid cursor")
  96. }
  97. var st cursorState
  98. if err := json.Unmarshal(decoded, &st); err != nil {
  99. return nil, fmt.Errorf("invalid cursor")
  100. }
  101. if st.Offset < 0 {
  102. st.Offset = 0
  103. }
  104. return &st, nil
  105. }
  106. func encodeCursor(st *cursorState) (string, error) {
  107. b, err := json.Marshal(st)
  108. if err != nil {
  109. return "", err
  110. }
  111. return base64.RawURLEncoding.EncodeToString(b), nil
  112. }