diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 08331381..0cdd716a 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -1857,7 +1857,7 @@ class RecordingViewSet(viewsets.ModelViewSet): except Exception: pass - library_dir = '/app/data' + library_dir = '/data' allowed_roots = ['/data/', library_dir.rstrip('/') + '/'] def _safe_remove(path: str): diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py index d4faa7bb..5135ddaf 100755 --- a/apps/channels/tasks.py +++ b/apps/channels/tasks.py @@ -578,8 +578,8 @@ def _build_output_paths(channel, program, start_time, end_time): Build (final_path, temp_ts_path, final_filename) using DVR templates. """ from core.models import CoreSettings - # Root for DVR recordings: fixed to /app/data inside the container - library_root = '/app/data' + # Root for DVR recordings: fixed to /data/recordings inside the container + library_root = '/data/recordings' is_movie, season, episode, year, sub_title = _parse_epg_tv_movie_info(program) show = _safe_name(program.get('title') if isinstance(program, dict) else channel.name) @@ -632,7 +632,7 @@ def _build_output_paths(channel, program, start_time, end_time): if not is_movie and not rel_path: rel_path = f"TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv" # Keep any leading folder like 'Recordings/' from the template so users can - # structure their library under /app/data as desired. + # structure their library under /data as desired. if not rel_path.lower().endswith('.mkv'): rel_path = f"{rel_path}.mkv" diff --git a/apps/plugins/loader.py b/apps/plugins/loader.py index 2999e869..5422ae7e 100644 --- a/apps/plugins/loader.py +++ b/apps/plugins/loader.py @@ -26,7 +26,7 @@ class LoadedPlugin: class PluginManager: - """Singleton manager that discovers and runs plugins from /app/data/plugins.""" + """Singleton manager that discovers and runs plugins from /data/plugins.""" _instance: Optional["PluginManager"] = None @@ -37,7 +37,7 @@ class PluginManager: return cls._instance def __init__(self) -> None: - self.plugins_dir = os.environ.get("DISPATCHARR_PLUGINS_DIR", "/app/data/plugins") + self.plugins_dir = os.environ.get("DISPATCHARR_PLUGINS_DIR", "/data/plugins") self._registry: Dict[str, LoadedPlugin] = {} # Ensure plugins directory exists diff --git a/apps/plugins/migrations/0001_initial.py b/apps/plugins/migrations/0001_initial.py index dd1bdc76..6de1490a 100644 --- a/apps/plugins/migrations/0001_initial.py +++ b/apps/plugins/migrations/0001_initial.py @@ -1,25 +1,29 @@ +# Generated by Django 5.2.4 on 2025-09-13 13:51 + from django.db import migrations, models class Migration(migrations.Migration): + initial = True - dependencies = [] + dependencies = [ + ] operations = [ migrations.CreateModel( - name="PluginConfig", + name='PluginConfig', fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("key", models.CharField(max_length=128, unique=True)), - ("name", models.CharField(max_length=255)), - ("version", models.CharField(blank=True, default="", max_length=64)), - ("description", models.TextField(blank=True, default="")), - ("enabled", models.BooleanField(default=False)), # merged change - ("ever_enabled", models.BooleanField(default=False)), # merged addition - ("settings", models.JSONField(blank=True, default=dict)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=128, unique=True)), + ('name', models.CharField(max_length=255)), + ('version', models.CharField(blank=True, default='', max_length=64)), + ('description', models.TextField(blank=True, default='')), + ('enabled', models.BooleanField(default=False)), + ('ever_enabled', models.BooleanField(default=False)), + ('settings', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), ], - ) + ), ] diff --git a/apps/plugins/migrations/__init__.py b/apps/plugins/migrations/__init__.py new file mode 100644 index 00000000..ade076bd --- /dev/null +++ b/apps/plugins/migrations/__init__.py @@ -0,0 +1 @@ +# This file marks the migrations package for the plugins app. diff --git a/core/migrations/0016_update_dvr_template_paths.py b/core/migrations/0016_update_dvr_template_paths.py new file mode 100644 index 00000000..5e729c47 --- /dev/null +++ b/core/migrations/0016_update_dvr_template_paths.py @@ -0,0 +1,61 @@ +# Generated manually to update DVR template paths + +from django.db import migrations +from django.utils.text import slugify + + +def update_dvr_template_paths(apps, schema_editor): + """Remove 'Recordings/' prefix from DVR template paths""" + CoreSettings = apps.get_model("core", "CoreSettings") + + # Define the updates needed + updates = [ + (slugify("DVR TV Template"), "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"), + (slugify("DVR Movie Template"), "Movies/{title} ({year}).mkv"), + (slugify("DVR TV Fallback Template"), "TV_Shows/{show}/{start}.mkv"), + (slugify("DVR Movie Fallback Template"), "Movies/{start}.mkv"), + ] + + # Update each setting + for key, new_value in updates: + try: + setting = CoreSettings.objects.get(key=key) + setting.value = new_value + setting.save() + print(f"Updated {setting.name}: {new_value}") + except CoreSettings.DoesNotExist: + print(f"Setting with key '{key}' not found - skipping") + + +def reverse_dvr_template_paths(apps, schema_editor): + """Add back 'Recordings/' prefix to DVR template paths""" + CoreSettings = apps.get_model("core", "CoreSettings") + + # Define the reverse updates (add back Recordings/ prefix) + updates = [ + (slugify("DVR TV Template"), "Recordings/TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"), + (slugify("DVR Movie Template"), "Recordings/Movies/{title} ({year}).mkv"), + (slugify("DVR TV Fallback Template"), "Recordings/TV_Shows/{show}/{start}.mkv"), + (slugify("DVR Movie Fallback Template"), "Recordings/Movies/{start}.mkv"), + ] + + # Update each setting back to original + for key, original_value in updates: + try: + setting = CoreSettings.objects.get(key=key) + setting.value = original_value + setting.save() + print(f"Reverted {setting.name}: {original_value}") + except CoreSettings.DoesNotExist: + print(f"Setting with key '{key}' not found - skipping") + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0015_dvr_templates"), + ] + + operations = [ + migrations.RunPython(update_dvr_template_paths, reverse_dvr_template_paths), + ] \ No newline at end of file diff --git a/core/models.py b/core/models.py index f9d49f7b..ba040666 100644 --- a/core/models.py +++ b/core/models.py @@ -255,7 +255,7 @@ class CoreSettings(models.Model): return cls.objects.get(key=DVR_TV_FALLBACK_TEMPLATE_KEY).value except cls.DoesNotExist: # default requested by user - return "Recordings/TV_Shows/{show}/{start}.mkv" + return "TV_Shows/{show}/{start}.mkv" @classmethod def get_dvr_movie_fallback_template(cls): @@ -263,7 +263,7 @@ class CoreSettings(models.Model): try: return cls.objects.get(key=DVR_MOVIE_FALLBACK_TEMPLATE_KEY).value except cls.DoesNotExist: - return "Recordings/Movies/{start}.mkv" + return "Movies/{start}.mkv" @classmethod def get_dvr_comskip_enabled(cls): diff --git a/docker/init/03-init-dispatcharr.sh b/docker/init/03-init-dispatcharr.sh index b9c3c63b..629c5a51 100644 --- a/docker/init/03-init-dispatcharr.sh +++ b/docker/init/03-init-dispatcharr.sh @@ -6,6 +6,7 @@ mkdir -p /data/uploads/m3us mkdir -p /data/uploads/epgs mkdir -p /data/m3us mkdir -p /data/epgs +mkdir -p /data/plugins mkdir -p /app/logo_cache mkdir -p /app/media diff --git a/frontend/src/pages/Plugins.jsx b/frontend/src/pages/Plugins.jsx index 9494e301..f2902523 100644 --- a/frontend/src/pages/Plugins.jsx +++ b/frontend/src/pages/Plugins.jsx @@ -49,7 +49,10 @@ const Field = ({ field, value, onChange }) => { return (