review: optionally queue save_media tasks after bulk actions

Adds SAVE_MEDIA_AFTER_BULK_ACTION (env:
TUBESYNC_SAVE_MEDIA_AFTER_BULK_ACTION, default False). When enabled,
each admin bulk action queues a save_media task per changed item so
flags are re-evaluated without waiting for the next source edit or
indexing run. When disabled (the default) behaviour is unchanged:
re-evaluation happens on the next source edit/index pass.

Requested by tcely in review.
This commit is contained in:
Agi-Asi
2026-08-27 14:56:58 +00:00
parent 81eec0da04
commit be4313ba1f
5 changed files with 54 additions and 1 deletions

View File

@@ -267,6 +267,7 @@ useful if you are manually installing TubeSync in some other environment. These
| TUBESYNC_DIRECTORY_PREFIX | Enable `video` and `audio` directory prefixes in `/downloads` | True |
| TUBESYNC_SHRINK_NEW | Filter unneeded information from newly retrieved metadata | True |
| TUBESYNC_SHRINK_OLD | Filter unneeded information from metadata loaded from the database | True |
| TUBESYNC_SAVE_MEDIA_AFTER_BULK_ACTION | Queue a `save_media` task per item changed by an admin bulk action, defaults to False | False |
| GUNICORN_WORKERS | Number of `gunicorn` (web request) workers to spawn | 3 |
| LISTEN_HOST | IP address for `gunicorn` to listen on | 127.0.0.1 |
| LISTEN_PORT | Port number for `gunicorn` to listen on | 8080 |

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib import admin
from .models import (
Source,
@@ -6,6 +7,7 @@ from .models import (
MetadataFormat,
MediaServer
)
from .tasks import save_media
@admin.register(Source)
@@ -31,25 +33,42 @@ class MediaAdmin(admin.ModelAdmin):
'enable_can_download', 'disable_can_download',
)
def _queue_save_media_tasks(self, queryset):
'''
Optionally queue a `save_media` task per changed item, so flags
are re-evaluated without waiting for the next source edit/index.
Disabled by default: SAVE_MEDIA_AFTER_BULK_ACTION = True enables it.
'''
if not getattr(settings, 'SAVE_MEDIA_AFTER_BULK_ACTION', False):
return
save_media.map(
str(media_uuid)
for media_uuid in queryset.values_list('uuid', flat=True)
)
@admin.action(description='Set "skip" for the selected media')
def enable_skip(self, request, queryset):
updated = queryset.update(skip=True, manual_skip=True)
self._queue_save_media_tasks(queryset)
self.message_user(request, f'Set "skip" on {updated} media item(s).')
@admin.action(description='Unset "skip" for the selected media')
def disable_skip(self, request, queryset):
updated = queryset.update(skip=False, manual_skip=False)
self._queue_save_media_tasks(queryset)
self.message_user(request, f'Unset "skip" on {updated} media item(s).')
@admin.action(description='Set "can download" for the selected media')
def enable_can_download(self, request, queryset):
updated = queryset.update(can_download=True)
self._queue_save_media_tasks(queryset)
self.message_user(
request, f'Set "can download" on {updated} media item(s).')
@admin.action(description='Unset "can download" for the selected media')
def disable_can_download(self, request, queryset):
updated = queryset.update(can_download=False)
self._queue_save_media_tasks(queryset)
self.message_user(
request, f'Unset "can download" on {updated} media item(s).')

View File

@@ -1,8 +1,9 @@
import logging
from unittest.mock import patch
from django.conf import settings
from django.contrib.admin.sites import AdminSite
from django.test import RequestFactory, TestCase
from django.test import RequestFactory, TestCase, override_settings
from sync.admin import MediaAdmin
from sync.models import Source, Media
@@ -84,3 +85,26 @@ class MediaAdminBulkActionsTestCase(TestCase):
'enable_can_download', 'disable_can_download',
):
self.assertIn(action, self.model_admin.actions)
def test_save_media_tasks_not_queued_by_default(self):
queryset = Media.objects.filter(source=self.source)
with patch('sync.admin.save_media') as mock_save_media:
self.model_admin.enable_skip(self.request, queryset)
mock_save_media.map.assert_not_called()
@override_settings(SAVE_MEDIA_AFTER_BULK_ACTION=True)
def test_save_media_tasks_queued_when_enabled(self):
queryset = Media.objects.filter(source=self.source)
with patch('sync.admin.save_media') as mock_save_media:
self.model_admin.enable_skip(self.request, queryset)
mock_save_media.map.assert_called_once()
queued = set(mock_save_media.map.call_args.args[0])
expected = {
str(media_uuid)
for media_uuid in queryset.values_list('uuid', flat=True)
}
self.assertEqual(queued, expected)

View File

@@ -108,6 +108,9 @@ SHRINK_OLD_MEDIA_METADATA = ( 'true' == SHRINK_OLD_MEDIA_METADATA_STR.strip().lo
# TUBESYNC_RENAME_ALL_SOURCES: True or False
RENAME_ALL_SOURCES_STR = getenv('TUBESYNC_RENAME_ALL_SOURCES', True)
RENAME_ALL_SOURCES = ( 'true' == RENAME_ALL_SOURCES_STR.strip().lower() )
# TUBESYNC_SAVE_MEDIA_AFTER_BULK_ACTION: True or False
SAVE_MEDIA_AFTER_BULK_ACTION_STR = getenv('TUBESYNC_SAVE_MEDIA_AFTER_BULK_ACTION', False)
SAVE_MEDIA_AFTER_BULK_ACTION = ( 'true' == SAVE_MEDIA_AFTER_BULK_ACTION_STR.strip().lower() )
# TUBESYNC_RENAME_SOURCES: A comma-separated list of Source directories
RENAME_SOURCES_STR = getenv('TUBESYNC_RENAME_SOURCES')
RENAME_SOURCES = RENAME_SOURCES_STR.split(',') if RENAME_SOURCES_STR else list()

View File

@@ -393,6 +393,12 @@ RENAME_ALL_SOURCES = True
RENAME_SOURCES = list()
# When True, admin bulk actions on Media queue a `save_media` task for each
# changed item so flags are re-evaluated without waiting for the next source
# edit or indexing run
SAVE_MEDIA_AFTER_BULK_ACTION = False
# An example for changing the ordering for audio tracks.
#ENGLISH_LANGUAGE_CODE_ORDER = (
# 'en-orig',