Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions apps/server/internal/platform/peertube.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,31 +867,37 @@ func (p *PeerTubeAdapter) DiscoverAccountContent(ctx context.Context, accessToke
Status: AccountContentDiscoveryPartial,
Description: "Only videos visible through the authenticated PeerTube instance are included.",
}}
reachedLowerBound := false
for _, video := range result.Data {
item, ok := p.normalizeAccountContentVideo(video, input.PublishedAfter)
item, ok := p.normalizeAccountContentVideo(video)
if !ok {
continue
}
// Videos come newest first, so the first one published before the
// window means the rest of the channel is older too.
if item.PublishedAt.Before(input.PublishedAfter) {
reachedLowerBound = true
break
}
page.Items = append(page.Items, item)
if page.BackfillWatermark.IsZero() || item.PublishedAt.Before(page.BackfillWatermark) {
page.BackfillWatermark = item.PublishedAt
}
}
if next := int64(start + len(page.Items)); next < result.Total {
// The cursor is an offset into the listing, so it advances by every
// video read, including those left out of the page.
if next := int64(start + len(result.Data)); !reachedLowerBound && next < result.Total {
page.NextCursor = strconv.FormatInt(next, 10)
}
return page, nil
}

func (p *PeerTubeAdapter) normalizeAccountContentVideo(video peertubeChannelVideo, publishedAfter time.Time) (AccountContentItem, bool) {
func (p *PeerTubeAdapter) normalizeAccountContentVideo(video peertubeChannelVideo) (AccountContentItem, bool) {
publishedAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(video.PublishedAt))
if err != nil || publishedAt.IsZero() {
return AccountContentItem{}, false
}
publishedAt = publishedAt.UTC()
if !publishedAfter.IsZero() && publishedAt.Before(publishedAfter) {
return AccountContentItem{}, false
}
item := AccountContentItem{
ProviderContentID: p.instanceURL + "/videos/watch/" + firstNonEmptyString(video.UUID, video.ShortUUID),
ContentProfile: "long_video",
Expand Down
56 changes: 56 additions & 0 deletions apps/server/internal/platform/peertube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -429,6 +430,61 @@ func TestPeerTubeAnalytics(t *testing.T) {
require.False(t, hasDislikes, "dislikes must not be relabelled")
}

func TestPeerTubeAccountContentDiscoveryCursorReachesTheEnd(t *testing.T) {
// GET /api/v1/video-channels/{handle}/videos pages by start and count and
// sorts by -publishedAt. The last three of 30 videos predate the window,
// and one video in the window has no usable publish time.
originalClient := httpClient
defer func() { httpClient = originalClient }()
now := time.Date(2026, 9, 5, 12, 0, 0, 0, time.UTC)
const videoCount = 30
var starts []string
httpClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
require.Equal(t, "/api/v1/video-channels/demos/videos", req.URL.Path)
require.Equal(t, "-publishedAt", req.URL.Query().Get("sort"))
start, _ := strconv.Atoi(req.URL.Query().Get("start"))
count, _ := strconv.Atoi(req.URL.Query().Get("count"))
starts = append(starts, strconv.Itoa(start))
videos := make([]peertubeChannelVideo, 0, count)
for index := start; index < min(start+count, videoCount); index++ {
publishedAt := now.Add(-time.Duration(index) * time.Hour)
if index >= videoCount-3 {
publishedAt = now.Add(-100 * 24 * time.Hour)
}
video := peertubeChannelVideo{UUID: "uuid-" + strconv.Itoa(index), Name: "Video " + strconv.Itoa(index), PublishedAt: publishedAt.Format(time.RFC3339)}
if index == 3 {
video.PublishedAt = ""
}
videos = append(videos, video)
}
body, err := json.Marshal(struct {
Total int64 `json:"total"`
Data []peertubeChannelVideo `json:"data"`
}{Total: videoCount, Data: videos})
require.NoError(t, err)
return jsonResponse(req, string(body)), nil
})}

adapter := NewPeerTubeAdapter("https://tube.example")
request := AccountContentDiscoveryRequest{AccountID: "demos", PageSize: 25, PublishedAfter: now.Add(-90 * 24 * time.Hour)}
var items []AccountContentItem
for calls := 0; ; calls++ {
require.Less(t, calls, 5, "discovery must reach the end of the channel instead of repeating a cursor; requested starts %v", starts)
page, err := adapter.DiscoverAccountContent(t.Context(), "token", request)
require.NoError(t, err)
items = append(items, page.Items...)
if page.NextCursor == "" {
break
}
request.Cursor = page.NextCursor
}
require.Equal(t, []string{"0", "25"}, starts, "the offset counts every video read, including one left out")
require.Len(t, items, videoCount-4)
for _, item := range items {
require.False(t, item.PublishedAt.Before(request.PublishedAfter))
}
}

func TestPeerTubeValidation(t *testing.T) {
issues := validatePeerTubeMedia([]MediaItem{{ID: "m1", MimeType: "image/jpeg"}})
require.Len(t, issues, 1)
Expand Down
3 changes: 3 additions & 0 deletions changes/peertube-discovery-cursor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- PeerTube account content discovery finishes its cycle and picks up new videos again. It used to get stuck repeating the same page once a channel had a video older than the discovery window, spending the daily read budget on that page and never finding newer uploads.
Loading