fix: migrate channel_tags boolean to empty list on startup

Channels without tags had channel_tags stored as boolean false rather
than an empty list. The strict ListField serializer introduced in later
versions raises TypeError: 'bool' object is not iterable when rendering
these records, causing HTTP 500 on /api/channel/ and /api/video/ and
the perpetual loading spinner in the UI.

The existing term-query approach used for artwork fields does not work
here because Elasticsearch does not index boolean false as a keyword,
making the affected documents invisible to term queries. Instead, use
match_all with a Painless instanceof check and ctx.op = 'noop' so only
genuinely broken documents are rewritten.

Fixes the root cause reported in #1042, #1063, #1067, #1097.
This commit is contained in:
Boyd Thomson 2026-06-07 12:49:45 -07:00
parent 9bf2050111
commit e3969197de
1 changed files with 31 additions and 0 deletions

View File

@ -64,6 +64,7 @@ class Command(BaseCommand):
self._mig_fix_playlist_description()
self._mig_fix_missing_stats()
self._mig_fix_channel_art_types()
self._mig_fix_channel_tags()
self._mig_fix_channel_description()
self._mig_fix_video_description()
@ -426,6 +427,36 @@ class Command(BaseCommand):
script={"source": source, "lang": "painless"},
)
def _mig_fix_channel_tags(self) -> None:
"""migrate: fix channel_tags stored as boolean instead of list"""
# Boolean false is not indexable as keyword, so term queries don't
# match it; use match_all + Painless instanceof check instead.
channel_script = (
"if (ctx._source.containsKey('channel_tags')"
" && !(ctx._source.channel_tags instanceof List))"
" { ctx._source.channel_tags = []; }"
" else { ctx.op = 'noop'; }"
)
self._run_migration(
index_name="ta_channel",
desc="fix channel_tags data type in channel index",
query={"match_all": {}},
script={"source": channel_script, "lang": "painless"},
)
video_script = (
"if (ctx._source.containsKey('channel')"
" && ctx._source.channel.containsKey('channel_tags')"
" && !(ctx._source.channel.channel_tags instanceof List))"
" { ctx._source.channel.channel_tags = []; }"
" else { ctx.op = 'noop'; }"
)
self._run_migration(
index_name="ta_video",
desc="fix channel_tags data type in video index",
query={"match_all": {}},
script={"source": video_script, "lang": "painless"},
)
def _mig_fix_channel_description(self) -> None:
"""migrate from 0.5.8 to 0.5.9, fix channel desc null value"""
desc = "fix channel description null value"