0.2.62 (unreleased) ------------------- - Set ``TERM=xterm-256color`` inside tox test environments so local runs launched from Ghostty do not print missing ``xterm-ghostty`` terminfo warnings. - Point the test settings' extra static files directory at the shipped ``src/cast/static`` tree so tox migrations no longer emit Django's ``staticfiles.W004`` warning. - Allow editor API post and episode draft ``PATCH`` requests to send the optimistic concurrency token as a strict ``If-Match: ""`` header instead of JSON ``base_revision_id``. - Avoid deep-copying Podlove and DOTe transcript JSON during public speaker sanitization when all speaker labels are already public or when the transcript contains no speaker metadata. - Fix ``Blog.last_build_date`` crashing with ``IndexError`` on blogs and podcasts without any published posts; it now falls back to the page's first publication date or the current time, so feeds for empty blogs render. - Fix the podcast feed's RSS ```` element: it now emits the first entry of the blog's ``keywords`` field when keywords are set (the previous guard checked a nonexistent ``categories`` attribute and always produced an empty category). - Declare explicit ``permission_classes = (AllowAny,)`` on the public API views (Podlove detail, player config, facet counts, theme list, theme update) so intended anonymous access no longer depends on the DRF project default and cannot be broken by a host site setting restrictive default permissions. - Remove dead tooling files (``bootstrap.md``, ``.travis.yml``, ``setup.cfg``, ``runtests.py``, ``javascript/jest.config.js``, ``command_lines.txt``); the ``commands.py`` ``test`` and ``mypy`` subcommands now invoke pytest and the configured mypy directly. - Stop pinning the ``pytest-randomly`` seed so the test order actually randomizes, and fix the two test-isolation bugs this surfaced (a shared API client leaking session cookies between tests, and a monkeypatched ``USE_THREADEDCOMMENTS`` baking the wrong comment form base class when the forms module was first imported under the patch). - Clean up packaging and type-check metadata: drop the contradictory MIT license classifier and a duplicated classifier, check types against Python 3.11 (the support floor), and remove the legacy mypy django-stubs plugin table. - Make ``Post.get_description`` side-effect free: the feed/social-card body template is now passed as a rendering argument instead of being stored on the instance, so a post or episode rendered after its description was generated no longer leaks the ``post_body.html`` partial into the full page render. - Wrap ``Video.save`` poster generation in a transaction like ``Audio.save``, so a failed poster derivation no longer leaves a half-saved video row. - Add ``sync_media`` and ``create_renditions`` keyword arguments to ``Post.save`` (both default ``True``) so bulk operations and migrations can skip media-derivation side effects explicitly. - Move the ``HtmxHttpRequest`` typing class to ``cast.http_types``; it stays importable from ``cast.views`` and ``cast.views.htmx_helpers`` for backwards compatibility. - Eliminate per-row queries when building blog index snapshots for mixed post/episode listings: page specialization and episode audio now load in bulk, so the query count no longer grows with the number of episodes, and a regression test guards the flat query count. - Deduplicate the audio/video/transcript Wagtail-admin media views: the previously triplicated ``index``/``add``/``edit``/``delete``/``chooser``/ ``chosen``/``chooser_upload`` views now share one parametrized implementation in ``cast.views.media``. URL names, templates, contexts, messages, and chooser modal workflows are unchanged. - Fix the video chooser upload view ignoring the ``CAST_CHOOSER_PAGINATION`` setting: it paginated with a hardcoded page size of 10 instead of ``CHOOSER_PAGINATION`` like every other chooser view (invisible under the default of 10, wrong for sites that override the setting). - Extract the transcript domain into a new ``cast.transcripts`` package: the WebVTT/Podlove/DOTe rewrite logic, speaker-sample selection, voice-reference derivation, and known-speaker handling now live in Django-free per-format modules plus a service layer, and the ``Transcript`` model keeps its public API as thin delegates. Public names such as ``time_to_seconds``, ``convert_dote_to_podcastindex_transcript``, and the ``KNOWN_SPEAKER_*`` constants remain importable from ``cast.models.transcript``. - Refactor the transcript admin edit view to dispatch its POST actions through a declarative action→handler map, and deduplicate the ``episode_from_latest_revision`` helper that was copied between the transcript and Voxhelm admin views. Messages, redirects, and form behavior are unchanged. - Centralize the static ``CAST_*`` setting defaults in a single registry in ``cast.appsettings``: the scattered inline ``getattr(settings, ...)`` defaults now resolve through one accessor (still read at call time, so ``override_settings`` and runtime changes keep working), and the ``cast.E001`` type check derives from the registry instead of a separately maintained table. Defaults and check coverage are unchanged. - Document all previously undocumented settings in the settings reference, including ``CAST_AUDIO_PLAYER``, ``CAST_EDITOR_SCOPES``, ``CAST_POST_BODY_BLOCKS``, the comment form CSS-class and moderator settings, and the remaining styleguide tunables. - Split the Voxhelm integration into a ``cast.voxhelm`` subpackage with dedicated ``settings`` (the site → Django setting → environment precedence chain), ``client`` (HTTP transport), ``task_refs``, and ``service`` (orchestration) modules. The transcript-generation status helpers move to ``cast.transcripts.generation_status``, which removes the circular import between ``cast.models`` and the Voxhelm code (a static test now guards that nothing imported during model initialisation depends on ``cast.voxhelm``). Every previously public name remains importable from ``cast.voxhelm``. - Show the "Generate transcript" action on the episode and audio edit pages only when Voxhelm is actually configured (API base and key resolvable via site settings, Django settings, or environment variables) for the request's site. Transcript generation status stays visible regardless, and direct POST requests keep reporting missing configuration as friendly error messages. - Deduplicate ``feeds.py``: the shared ``item_description``/``item_link``/ ``item_pubdate`` methods and the Atom stylesheet ``write()`` now live in one place, the iTunes/Podcast-Index generator mixins are statically typed (no more ``# type: ignore`` or defensive ``try/except``), and podcast feed XML output is unchanged. - Blog (non-podcast) RSS and Atom feeds now emit a stable per-entry identifier and publication dates: RSS items gain ```` and a ```` set to the post's UUID, and Atom entries gain ```` and a UUID ````. Feed readers may show existing blog entries as new once, after which the UUID identifier survives slug and URL changes. - Document ``cast.api.views`` as a frozen legacy API surface (including the bare-text ``POST /api/upload_video/`` response); new integrations should use the editor API for structured errors, scoped authorization, and ``If-Match`` revision handling. - Stop shipping test-only settings in the installed package. The former ``cast.settings`` module (committed ``SECRET_KEY``, ``tests.urls`` root URLconf, MD5 hasher) now lives entirely in ``tests/settings.py``, and ``dev_settings.py`` is renamed to ``dev_tools.py`` to reflect its role as a feature-flag resolver rather than a settings module. ``django-environ`` is dropped from the dependencies. This is an internal packaging change with no effect on configuring a real django-cast site. - Give the repository cache serialization an explicit ``type`` discriminator instead of guessing a cached object's class from which keys are present. Blog/podcast and post/episode entries now record their type and deserialize by it, with a fallback that keeps entries cached by earlier versions working.