fix(favorites): exclude bulk-backfill scenes from "+N new" (tube fake dates)
Some checks are pending
Backend tests / test (push) Waiting to run
Some checks are pending
Backend tests / test (push) Waiting to run
Browse scrapers backfilling old catalogs stamp the tube's import/post date as release_date, so old content (e.g. 83 MissaX classics via perverzija, ~3600/3 days across eporner/youporn/etc.) fake-ranked as newest and flooded the favorites "+N". NULLing the dates was a non-starter — the stub filter would hide 251k performer-less scenes. Instead: a Scene.backfill flag marks bulk catalog imports; they stay visible but never count as "new". - scenes.backfill column (+ index, migration 0026); resolve_scene/_process_scene thread it. - deep_crawl tags scenes from pages beyond the "latest" threshold (>2) as backfill; latest pages + TPDB/StashDB delta stay genuine. Cursor reset re-sweeps page 1 so real new content is always caught fresh. - favorites +N (performers + studios) excludes backfill within the top-200 window. - SceneOut exposes `backfill`; mobile NEW badge + NEW-first re-sort skip it (badge==count). - Retroactive: tagged 439k existing scenes in bulk (>10 non-canonical / studio / day) clusters. Device check: favorite-studios +N 11626 (naive) -> 236. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64aee54fb6
commit
8c7fa36437
13 changed files with 122 additions and 18 deletions
39
alembic/versions/20260702_0026_scene_backfill_flag.py
Normal file
39
alembic/versions/20260702_0026_scene_backfill_flag.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""scene backfill flag: exclude bulk catalog imports from "new"
|
||||||
|
|
||||||
|
Revision ID: 0026_scene_backfill_flag
|
||||||
|
Revises: 0025_source_ranking
|
||||||
|
Create Date: 2026-07-02
|
||||||
|
|
||||||
|
Tube'y podają datę importu jako release_date, więc masowy backfill starego katalogu
|
||||||
|
(deep-crawl głębokie strony) udawał świeżość i zawyżał licznik "+N nowych" w ulubionych.
|
||||||
|
`scenes.backfill` oznacza takie sceny — pozostają widoczne, ale nie liczą się jako nowe.
|
||||||
|
Ustawiane przy tworzeniu (deep_crawl page > próg); świeży latest-crawl + delta TPDB/
|
||||||
|
StashDB → False. Patrz app/scheduler/deep_crawl.py, app/resolve/scene_resolver.py.
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0026_scene_backfill_flag"
|
||||||
|
down_revision: str | None = "0025_source_ranking"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"scenes",
|
||||||
|
sa.Column(
|
||||||
|
"backfill",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("false"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index("ix_scenes_backfill", "scenes", ["backfill"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_scenes_backfill", table_name="scenes")
|
||||||
|
op.drop_column("scenes", "backfill")
|
||||||
|
|
@ -187,6 +187,7 @@ def list_favorites(
|
||||||
select(
|
select(
|
||||||
ScenePerformer.performer_id.label("gid"),
|
ScenePerformer.performer_id.label("gid"),
|
||||||
Scene.created_at.label("created_at"),
|
Scene.created_at.label("created_at"),
|
||||||
|
Scene.backfill.label("backfill"),
|
||||||
rn,
|
rn,
|
||||||
)
|
)
|
||||||
.join(Scene, Scene.id == ScenePerformer.scene_id)
|
.join(Scene, Scene.id == ScenePerformer.scene_id)
|
||||||
|
|
@ -194,11 +195,14 @@ def list_favorites(
|
||||||
.where(*clauses)
|
.where(*clauses)
|
||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
for gid, created_at in session.execute(
|
for gid, created_at, backfill in session.execute(
|
||||||
select(inner.c.gid, inner.c.created_at).where(inner.c.rn <= _FAVORITES_PAGE_CAP)
|
select(inner.c.gid, inner.c.created_at, inner.c.backfill).where(
|
||||||
|
inner.c.rn <= _FAVORITES_PAGE_CAP
|
||||||
|
)
|
||||||
):
|
):
|
||||||
ls = last_seen_by_perf.get(gid)
|
ls = last_seen_by_perf.get(gid)
|
||||||
if created_at is not None and ls is not None and created_at > ls:
|
# backfill (masowy import katalogu) NIE liczy się jako nowość — patrz Scene.backfill.
|
||||||
|
if not backfill and created_at is not None and ls is not None and created_at > ls:
|
||||||
new_counts[gid] = new_counts.get(gid, 0) + 1
|
new_counts[gid] = new_counts.get(gid, 0) + 1
|
||||||
|
|
||||||
items: list[FavoriteOut] = []
|
items: list[FavoriteOut] = []
|
||||||
|
|
@ -334,17 +338,21 @@ def list_favorite_studios(
|
||||||
select(
|
select(
|
||||||
Scene.studio_id.label("gid"),
|
Scene.studio_id.label("gid"),
|
||||||
Scene.created_at.label("created_at"),
|
Scene.created_at.label("created_at"),
|
||||||
|
Scene.backfill.label("backfill"),
|
||||||
rn,
|
rn,
|
||||||
)
|
)
|
||||||
.where(Scene.studio_id.in_(studio_ids))
|
.where(Scene.studio_id.in_(studio_ids))
|
||||||
.where(*clauses)
|
.where(*clauses)
|
||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
for gid, created_at in session.execute(
|
for gid, created_at, backfill in session.execute(
|
||||||
select(inner.c.gid, inner.c.created_at).where(inner.c.rn <= _FAVORITES_PAGE_CAP)
|
select(inner.c.gid, inner.c.created_at, inner.c.backfill).where(
|
||||||
|
inner.c.rn <= _FAVORITES_PAGE_CAP
|
||||||
|
)
|
||||||
):
|
):
|
||||||
ls = last_seen_by_studio.get(gid)
|
ls = last_seen_by_studio.get(gid)
|
||||||
if created_at is not None and ls is not None and created_at > ls:
|
# backfill (masowy import katalogu) NIE liczy się jako nowość — patrz Scene.backfill.
|
||||||
|
if not backfill and created_at is not None and ls is not None and created_at > ls:
|
||||||
new_counts[gid] = new_counts.get(gid, 0) + 1
|
new_counts[gid] = new_counts.get(gid, 0) + 1
|
||||||
|
|
||||||
items: list[FavoriteStudioOut] = []
|
items: list[FavoriteStudioOut] = []
|
||||||
|
|
|
||||||
|
|
@ -693,6 +693,7 @@ def _build_scenes_out_batch(
|
||||||
external_refs=refs_by_scene.get(scene.id, []),
|
external_refs=refs_by_scene.get(scene.id, []),
|
||||||
playback_sources=pb_by_scene.get(scene.id, []),
|
playback_sources=pb_by_scene.get(scene.id, []),
|
||||||
created_at=scene.created_at,
|
created_at=scene.created_at,
|
||||||
|
backfill=scene.backfill,
|
||||||
last_played_at=progress.last_played_at if progress else None,
|
last_played_at=progress.last_played_at if progress else None,
|
||||||
finished=progress.finished if progress else False,
|
finished=progress.finished if progress else False,
|
||||||
position_sec=progress.position_sec if progress else 0,
|
position_sec=progress.position_sec if progress else 0,
|
||||||
|
|
@ -828,6 +829,7 @@ def _build_scene_out(session: Session, scene: Scene, *, device_id: str = LEGACY_
|
||||||
external_refs=refs_out,
|
external_refs=refs_out,
|
||||||
playback_sources=playback_out,
|
playback_sources=playback_out,
|
||||||
created_at=scene.created_at,
|
created_at=scene.created_at,
|
||||||
|
backfill=scene.backfill,
|
||||||
last_played_at=progress.last_played_at if progress else None,
|
last_played_at=progress.last_played_at if progress else None,
|
||||||
finished=progress.finished if progress else False,
|
finished=progress.finished if progress else False,
|
||||||
position_sec=progress.position_sec if progress else 0,
|
position_sec=progress.position_sec if progress else 0,
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ class SceneOut(BaseModel):
|
||||||
# "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen — gdy
|
# "NEW" na karcie scen w PerformerScenesScreen / StudioScenesScreen — gdy
|
||||||
# `created_at > last_seen_at` (favorite) → badge.
|
# `created_at > last_seen_at` (favorite) → badge.
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
# True = scena z masowego backfillu katalogu (deep-crawl). Mobile NIE pokazuje na niej
|
||||||
|
# NEW badge nawet gdy created_at > last_seen — to stara treść, nie świeży release.
|
||||||
|
backfill: bool = False
|
||||||
# Watched indicator (z `scene_play_progress`): mobile dim'uje kafelek gdy
|
# Watched indicator (z `scene_play_progress`): mobile dim'uje kafelek gdy
|
||||||
# `finished=True`, pokazuje progress bar gdy `position_sec > 0`.
|
# `finished=True`, pokazuje progress bar gdy `position_sec > 0`.
|
||||||
last_played_at: datetime | None = None
|
last_played_at: datetime | None = None
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,13 @@ def ingest_from_connector(
|
||||||
return counters
|
return counters
|
||||||
|
|
||||||
|
|
||||||
def _process_scene(*, source_id: uuid.UUID, raw_scene: RawScene, counters: dict[str, int]) -> None:
|
def _process_scene(
|
||||||
|
*,
|
||||||
|
source_id: uuid.UUID,
|
||||||
|
raw_scene: RawScene,
|
||||||
|
counters: dict[str, int],
|
||||||
|
backfill: bool = False,
|
||||||
|
) -> None:
|
||||||
payload = raw_scene.raw or raw_scene.model_dump(mode="json")
|
payload = raw_scene.raw or raw_scene.model_dump(mode="json")
|
||||||
if _has_nul(payload):
|
if _has_nul(payload):
|
||||||
# Strip NUL z payloadu (→ external_records.raw JSONB) ORAZ ze structured fields
|
# Strip NUL z payloadu (→ external_records.raw JSONB) ORAZ ze structured fields
|
||||||
|
|
@ -304,7 +310,7 @@ def _process_scene(*, source_id: uuid.UUID, raw_scene: RawScene, counters: dict[
|
||||||
counters["skipped"] += 1
|
counters["skipped"] += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
result = resolve_scene(session, norm=norm, source_id=source_id)
|
result = resolve_scene(session, norm=norm, source_id=source_id, backfill=backfill)
|
||||||
|
|
||||||
if result.was_created:
|
if result.was_created:
|
||||||
counters["new"] += 1
|
counters["new"] += 1
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import uuid
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
Date,
|
Date,
|
||||||
DateTime,
|
DateTime,
|
||||||
Enum,
|
Enum,
|
||||||
|
|
@ -40,6 +41,14 @@ class Scene(UUIDPKMixin, TimestampMixin, Base):
|
||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
code: Mapped[str | None] = mapped_column(String(128), index=True)
|
code: Mapped[str | None] = mapped_column(String(128), index=True)
|
||||||
director: Mapped[str | None] = mapped_column(String(256))
|
director: Mapped[str | None] = mapped_column(String(256))
|
||||||
|
# True = scena wpadła masowym backfillem katalogu (deep-crawl, głębokie strony), NIE
|
||||||
|
# świeży ingest. Tube podają datę importu jako release_date, więc stary backfill
|
||||||
|
# udaje "nowość" (licznik +N w ulubionych). Ten flag pozwala go wykluczyć z "nowych"
|
||||||
|
# bez chowania sceny. Ustawiany przy TWORZENIU (deep_crawl page > próg); świeży
|
||||||
|
# latest-crawl + delta TPDB/StashDB → False. Patrz app/scheduler/deep_crawl.py.
|
||||||
|
backfill: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, server_default="false", index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SceneExternalRef(Base):
|
class SceneExternalRef(Base):
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,12 @@ def resolve_scene(
|
||||||
*,
|
*,
|
||||||
norm: NormalizedScene,
|
norm: NormalizedScene,
|
||||||
source_id: uuid.UUID,
|
source_id: uuid.UUID,
|
||||||
|
backfill: bool = False,
|
||||||
) -> SceneResolveResult:
|
) -> SceneResolveResult:
|
||||||
|
"""backfill=True → nowo utworzona scena to masowy import katalogu (deep-crawl głębokie
|
||||||
|
strony), NIE świeży release. Flag ląduje na Scene.backfill i wyklucza scenę z "nowych"
|
||||||
|
(licznik +N w ulubionych). Dotyczy tylko TWORZENIA — match do istniejącej sceny nie
|
||||||
|
zmienia jej flagi (istniejąca mogła być świeża)."""
|
||||||
studio = resolve_studio(session, norm=norm.studio, source_id=source_id) if norm.studio else None
|
studio = resolve_studio(session, norm=norm.studio, source_id=source_id) if norm.studio else None
|
||||||
studio_id = studio.id if studio else None
|
studio_id = studio.id if studio else None
|
||||||
|
|
||||||
|
|
@ -138,7 +143,7 @@ def resolve_scene(
|
||||||
scene_match.title_normalized, norm.title_normalized
|
scene_match.title_normalized, norm.title_normalized
|
||||||
)
|
)
|
||||||
if sp_strength >= 1.0:
|
if sp_strength >= 1.0:
|
||||||
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id)
|
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id, backfill=backfill)
|
||||||
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
||||||
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
||||||
return SceneResolveResult(
|
return SceneResolveResult(
|
||||||
|
|
@ -155,7 +160,7 @@ def resolve_scene(
|
||||||
penalised_score = raw_phash_score * max(dur_prox, 0.1)
|
penalised_score = raw_phash_score * max(dur_prox, 0.1)
|
||||||
if 0.0 < sp_strength < 1.0:
|
if 0.0 < sp_strength < 1.0:
|
||||||
penalised_score = min(penalised_score, 1.0 - sp_strength)
|
penalised_score = min(penalised_score, 1.0 - sp_strength)
|
||||||
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id)
|
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id, backfill=backfill)
|
||||||
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
||||||
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
||||||
session.add(
|
session.add(
|
||||||
|
|
@ -189,7 +194,7 @@ def resolve_scene(
|
||||||
# ale auto-merge zablokowane: tworzymy nową scenę + pending review.
|
# ale auto-merge zablokowane: tworzymy nową scenę + pending review.
|
||||||
if 0.0 < sp_strength < 1.0:
|
if 0.0 < sp_strength < 1.0:
|
||||||
penalised_score = min(raw_phash_score, 1.0 - sp_strength)
|
penalised_score = min(raw_phash_score, 1.0 - sp_strength)
|
||||||
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id)
|
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id, backfill=backfill)
|
||||||
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
||||||
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
_sync_attached_entities(session, scene=new_scene, norm=norm, source_id=source_id)
|
||||||
session.add(
|
session.add(
|
||||||
|
|
@ -342,7 +347,7 @@ def resolve_scene(
|
||||||
)
|
)
|
||||||
|
|
||||||
if decision == "review":
|
if decision == "review":
|
||||||
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id)
|
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id, backfill=backfill)
|
||||||
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
||||||
_sync_performers(session, scene_id=new_scene.id, resolved=resolved_performers)
|
_sync_performers(session, scene_id=new_scene.id, resolved=resolved_performers)
|
||||||
_sync_tags(session, scene_id=new_scene.id, norm=norm, source_id=source_id)
|
_sync_tags(session, scene_id=new_scene.id, norm=norm, source_id=source_id)
|
||||||
|
|
@ -369,7 +374,7 @@ def resolve_scene(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Brak żadnego sensownego dopasowania → nowa kanoniczna
|
# Brak żadnego sensownego dopasowania → nowa kanoniczna
|
||||||
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id)
|
new_scene = _create_canonical(session, norm=norm, studio_id=studio_id, backfill=backfill)
|
||||||
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
_attach_external_ref(session, scene_id=new_scene.id, source_id=source_id, norm=norm)
|
||||||
_sync_performers(session, scene_id=new_scene.id, resolved=resolved_performers)
|
_sync_performers(session, scene_id=new_scene.id, resolved=resolved_performers)
|
||||||
_sync_tags(session, scene_id=new_scene.id, norm=norm, source_id=source_id)
|
_sync_tags(session, scene_id=new_scene.id, norm=norm, source_id=source_id)
|
||||||
|
|
@ -403,7 +408,11 @@ def _cap(s: str | None, n: int) -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def _create_canonical(
|
def _create_canonical(
|
||||||
session: Session, *, norm: NormalizedScene, studio_id: uuid.UUID | None
|
session: Session,
|
||||||
|
*,
|
||||||
|
norm: NormalizedScene,
|
||||||
|
studio_id: uuid.UUID | None,
|
||||||
|
backfill: bool = False,
|
||||||
) -> Scene:
|
) -> Scene:
|
||||||
scene = Scene(
|
scene = Scene(
|
||||||
title=norm.title,
|
title=norm.title,
|
||||||
|
|
@ -415,6 +424,7 @@ def _create_canonical(
|
||||||
description=norm.description,
|
description=norm.description,
|
||||||
code=_cap(norm.code, 128),
|
code=_cap(norm.code, 128),
|
||||||
director=_cap(norm.director, 256),
|
director=_cap(norm.director, 256),
|
||||||
|
backfill=backfill,
|
||||||
)
|
)
|
||||||
session.add(scene)
|
session.add(scene)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,13 @@ _PAGE_CAP: dict[str, int] = {
|
||||||
# wracamy czysto — następny run kontynuuje. Margines 600s na dokończenie strony w toku.
|
# wracamy czysto — następny run kontynuuje. Margines 600s na dokończenie strony w toku.
|
||||||
_RUN_BUDGET_SEC = 3000
|
_RUN_BUDGET_SEC = 3000
|
||||||
|
|
||||||
|
# Strony <= tego progu = "latest" (świeże posty tube'a) → nowe sceny stąd to genuine
|
||||||
|
# nowości. Głębsze strony = backfill starego katalogu → nowe sceny stąd dostają
|
||||||
|
# Scene.backfill=True i NIE liczą się jako "nowe" w ulubionych (tube podaje datę importu
|
||||||
|
# jako release_date, więc stary backfill inaczej udaje świeżość). Reset kursora re-sweepuje
|
||||||
|
# od strony 1, więc genuinnie nowe zawsze łapią się na stronach 1-2.
|
||||||
|
_LATEST_PAGE_THRESHOLD = 2
|
||||||
|
|
||||||
|
|
||||||
def _state_path() -> Path:
|
def _state_path() -> Path:
|
||||||
return Path(getattr(get_settings(), "deepcrawl_state_path", None) or _DEFAULT_STATE)
|
return Path(getattr(get_settings(), "deepcrawl_state_path", None) or _DEFAULT_STATE)
|
||||||
|
|
@ -142,10 +149,16 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
|
||||||
exhausted = True
|
exhausted = True
|
||||||
last_done = page
|
last_done = page
|
||||||
break
|
break
|
||||||
|
page_is_backfill = page > _LATEST_PAGE_THRESHOLD
|
||||||
for raw in scenes:
|
for raw in scenes:
|
||||||
counters["seen"] += 1
|
counters["seen"] += 1
|
||||||
try:
|
try:
|
||||||
_process_scene(source_id=source_id, raw_scene=raw, counters=counters)
|
_process_scene(
|
||||||
|
source_id=source_id,
|
||||||
|
raw_scene=raw,
|
||||||
|
counters=counters,
|
||||||
|
backfill=page_is_backfill,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
counters["errors"] += 1
|
counters["errors"] += 1
|
||||||
last_done = page
|
last_done = page
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
id: '2026-07-02',
|
||||||
|
date: 'July 2026',
|
||||||
|
items: [
|
||||||
|
'Favorites "+N new" now counts genuinely new scenes only — bulk back-catalog imports (old content a site re-dated to today) no longer inflate the badge or show a NEW tag.',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '2026-07-01b',
|
id: '2026-07-01b',
|
||||||
date: 'July 2026',
|
date: 'July 2026',
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,9 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
||||||
};
|
};
|
||||||
|
|
||||||
const dim = scene.finished === true;
|
const dim = scene.finished === true;
|
||||||
const isNew = !!(seenSince && scene.created_at && scene.created_at > seenSince);
|
// NEW = dodane od ostatniej wizyty, ale NIE backfill (masowy import starego katalogu —
|
||||||
|
// tube podaje datę importu jako release_date, więc udawałby świeżość). Spójne z licznikiem +N.
|
||||||
|
const isNew = !!(seenSince && scene.created_at && scene.created_at > seenSince && !scene.backfill);
|
||||||
const dur = scene.duration_sec;
|
const dur = scene.duration_sec;
|
||||||
const durLabel =
|
const durLabel =
|
||||||
dur && dur > 0
|
dur && dur > 0
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,8 @@ export function PerformerScenesScreen() {
|
||||||
const newOnes: SceneOut[] = [];
|
const newOnes: SceneOut[] = [];
|
||||||
const rest: SceneOut[] = [];
|
const rest: SceneOut[] = [];
|
||||||
for (const s of items) {
|
for (const s of items) {
|
||||||
if (s.created_at && s.created_at > seenSince) {
|
// NEW-first pomija backfill (masowy import katalogu) — spójne z badge + licznikiem +N.
|
||||||
|
if (s.created_at && s.created_at > seenSince && !s.backfill) {
|
||||||
newOnes.push(s);
|
newOnes.push(s);
|
||||||
} else {
|
} else {
|
||||||
rest.push(s);
|
rest.push(s);
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,8 @@ export function StudioScenesScreen() {
|
||||||
const newOnes: SceneOut[] = [];
|
const newOnes: SceneOut[] = [];
|
||||||
const rest: SceneOut[] = [];
|
const rest: SceneOut[] = [];
|
||||||
for (const s of items) {
|
for (const s of items) {
|
||||||
if (s.created_at && s.created_at > seenSince) {
|
// NEW-first pomija backfill (masowy import katalogu) — spójne z badge + licznikiem +N.
|
||||||
|
if (s.created_at && s.created_at > seenSince && !s.backfill) {
|
||||||
newOnes.push(s);
|
newOnes.push(s);
|
||||||
} else {
|
} else {
|
||||||
rest.push(s);
|
rest.push(s);
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,9 @@ export interface SceneOut {
|
||||||
// Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW" — gdy
|
// Kiedy scena trafiła do bazy (ingest). Używane do oznaczenia "NEW" — gdy
|
||||||
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
// `created_at > favoriteSeenSince` (param przekazany z FavoritesScreen).
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
|
// True = scena z masowego backfillu katalogu (deep-crawl) — stara treść, nie świeży
|
||||||
|
// release. NIE pokazujemy na niej NEW badge nawet gdy created_at > seenSince.
|
||||||
|
backfill?: boolean;
|
||||||
// Watched indicator + favorite state. Backend dolicza z scene_play_progress + favorite_scenes.
|
// Watched indicator + favorite state. Backend dolicza z scene_play_progress + favorite_scenes.
|
||||||
last_played_at?: string | null;
|
last_played_at?: string | null;
|
||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue