fix(playlists): "+N without length" counts every video the sum left out

The label said "without length" but the count held only live videos, so
a video not fetched yet, or gone before it was, was left out of the sum
and named nowhere. The server's `Video.without_length` and the client's
`isWithoutLength` are now one rule (no length, whatever the live
status), and the field, the key and the helpers are named for what they
count. The comments that still quoted "+N live" say the current copy.
This commit is contained in:
2026-11-08 00:49:21 +01:00
parent 55cbbfb6e4
commit e8b9bdd7c1
17 changed files with 97 additions and 97 deletions
+9 -9
View File
@@ -17,7 +17,6 @@ from sqlalchemy import (
String,
Text,
UniqueConstraint,
and_,
case,
func,
literal_column,
@@ -125,7 +124,7 @@ LIVE_OR_UPCOMING = ("live", "upcoming")
# the stream has run (measured on prod, 2026-11-06: 122,962,914 s), and one such video made a Watch
# later total read 44,073 hours. YouTube caps an upload at 12 hours, so nothing real is lost. Such a
# length is unknown wherever a length is used: the API sends none (`Video.length_seconds`), sums leave
# it out (a playlist's counts it as `live_without_length`), and the duration filter, sort and Home budget never
# it out (a playlist's counts it as `without_length`), and the duration filter, sort and Home budget never
# match it. The raw `duration_seconds` stays for what it does prove — that the video is not a Short —
# and for the jobs that ask whether YouTube has answered yet (test_video_length holds who reads it).
LENGTH_MAX_S = 24 * 3600
@@ -239,15 +238,16 @@ class Video(Base, TimestampMixin):
)
@hybrid_property
def live_without_length(self) -> bool:
"""A live video (on air, upcoming, a premiere or an archive) whose length is unknown: what a
total leaves out and names as "+N live"."""
return self.live_status != "none" and self.length_seconds is None
def without_length(self) -> bool:
"""A video whose length is unknown — live, an archive past a day, or not fetched yet: what a
sum of lengths leaves out and names as "+N without length". Every such video, so the count
and the sum never disagree about what was left out."""
return self.length_seconds is None
@live_without_length.inplace.expression
@without_length.inplace.expression
@classmethod
def _live_without_length_expression(cls) -> ColumnElement[bool]:
return and_(cls.live_status != "none", cls.length_seconds.is_(None))
def _without_length_expression(cls) -> ColumnElement[bool]:
return cls.length_seconds.is_(None)
# Weighted full-text search document (DB-generated, never written by the app): title (A) >
# creator keywords + search queries (B) > truncated description (C). ts_rank honours the