fix(autotag): the orphan cleanup keeps a system tag a saved view filters on

Feed views store tag ids. A system tag no channel carried for a while was deleted and, once a
channel earned it again, recreated under a new id — the view's filter then pointed at nothing, the
feed dropped the unknown id, and the view showed the whole feed under its old name. The cleanup now
skips tags a saved feed view references (test seen red without it).
This commit is contained in:
2026-10-03 19:37:24 +02:00
parent 328d415cfe
commit 4fc307c426
2 changed files with 48 additions and 2 deletions
+12 -2
View File
@@ -12,7 +12,7 @@ from sqlalchemy import and_, exists, func, select
from sqlalchemy.orm import Session
from app import logs, progress, sysconfig
from app.models import Channel, ChannelTag, Tag, Video
from app.models import Channel, ChannelTag, SavedView, Tag, Video
log = logs.get_logger(__name__)
@@ -289,12 +289,22 @@ def run_autotag_all(db: Session, only_missing: bool = False) -> dict:
def _cleanup_orphan_system_tags(db: Session) -> int:
"""Delete system tags that no longer link to any channel (e.g. after a mapping change)."""
"""Delete system tags that no longer link to any channel (e.g. after a mapping change) — but not
one a saved feed view filters on. Views hold tag IDS, and a deleted tag comes back under a new id
when a channel earns it again, so the view's filter would point at nothing; the feed drops an
unknown id, and the view showed the whole feed under its old name (E32 S2)."""
in_views = {
t
for (filters,) in db.execute(select(SavedView.filters).where(SavedView.source == "feed"))
for t in ((filters or {}).get("tags") or [])
if isinstance(t, int)
}
orphans = (
db.execute(
select(Tag).where(
Tag.user_id.is_(None),
~exists().where(ChannelTag.tag_id == Tag.id),
Tag.id.notin_(in_views),
)
)
.scalars()
+36
View File
@@ -0,0 +1,36 @@
"""sync/autotag's orphan cleanup (E32 S2: the module was 32% covered).
A system tag no channel carries any more is deleted — unless a saved feed view filters on it. The
views hold tag ids; a deleted tag returns under a NEW id when a channel earns it again, and the feed
drops an unknown id, so the view silently showed the whole feed under its old name.
"""
from sqlalchemy import select
from app.models import Channel, ChannelTag, SavedView, Tag
from app.sync import autotag
def test_an_orphan_system_tag_goes_but_one_a_saved_view_filters_on_stays(db, make_user):
user = make_user()
used = Tag(user_id=None, name="Gaming", category="topic")
orphan = Tag(user_id=None, name="Cooking", category="topic")
in_view = Tag(user_id=None, name="Music", category="topic")
own = Tag(user_id=user.id, name="Mine", category="other")
ch = Channel(id="UC_tags", title="T")
db.add_all([used, orphan, in_view, own, ch])
db.flush()
db.add(ChannelTag(channel_id=ch.id, tag_id=used.id, user_id=None, source="auto"))
db.add(
SavedView(
user_id=user.id, source="feed", name="Music", filters={"tags": [in_view.id], "q": ""}
)
)
# A Plex view's filters are another vocabulary; they must not break the scan.
db.add(SavedView(user_id=user.id, source="plex", name="P", filters={"genres": ["Drama"]}))
db.commit()
assert autotag._cleanup_orphan_system_tags(db) == 1
db.expire_all()
names = set(db.execute(select(Tag.name)).scalars())
assert names == {"Gaming", "Music", "Mine"} # only the unreferenced system orphan went