mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
case sensitive flag and other possible custom properties for filters
This commit is contained in:
parent
f1752cc720
commit
f300da6eff
8 changed files with 143 additions and 36 deletions
|
|
@ -2,56 +2,73 @@ from django.contrib import admin
|
|||
from django.utils.html import format_html
|
||||
from .models import M3UAccount, M3UFilter, ServerGroup, UserAgent
|
||||
|
||||
|
||||
class M3UFilterInline(admin.TabularInline):
|
||||
model = M3UFilter
|
||||
extra = 1
|
||||
verbose_name = "M3U Filter"
|
||||
verbose_name_plural = "M3U Filters"
|
||||
|
||||
|
||||
@admin.register(M3UAccount)
|
||||
class M3UAccountAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'server_url', 'server_group', 'max_streams', 'is_active', 'user_agent_display', 'uploaded_file_link', 'created_at', 'updated_at')
|
||||
list_filter = ('is_active', 'server_group')
|
||||
search_fields = ('name', 'server_url', 'server_group__name')
|
||||
list_display = (
|
||||
"name",
|
||||
"server_url",
|
||||
"server_group",
|
||||
"max_streams",
|
||||
"is_active",
|
||||
"user_agent_display",
|
||||
"uploaded_file_link",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_filter = ("is_active", "server_group")
|
||||
search_fields = ("name", "server_url", "server_group__name")
|
||||
inlines = [M3UFilterInline]
|
||||
actions = ['activate_accounts', 'deactivate_accounts']
|
||||
actions = ["activate_accounts", "deactivate_accounts"]
|
||||
|
||||
# Handle both ForeignKey and ManyToManyField cases for UserAgent
|
||||
def user_agent_display(self, obj):
|
||||
if hasattr(obj, 'user_agent'): # ForeignKey case
|
||||
if hasattr(obj, "user_agent"): # ForeignKey case
|
||||
return obj.user_agent.user_agent if obj.user_agent else "None"
|
||||
elif hasattr(obj, 'user_agents'): # ManyToManyField case
|
||||
elif hasattr(obj, "user_agents"): # ManyToManyField case
|
||||
return ", ".join([ua.user_agent for ua in obj.user_agents.all()]) or "None"
|
||||
return "None"
|
||||
|
||||
user_agent_display.short_description = "User Agent(s)"
|
||||
|
||||
def uploaded_file_link(self, obj):
|
||||
if obj.uploaded_file:
|
||||
return format_html("<a href='{}' target='_blank'>Download M3U</a>", obj.uploaded_file.url)
|
||||
return format_html(
|
||||
"<a href='{}' target='_blank'>Download M3U</a>", obj.uploaded_file.url
|
||||
)
|
||||
return "No file uploaded"
|
||||
|
||||
uploaded_file_link.short_description = "Uploaded File"
|
||||
|
||||
@admin.action(description='Activate selected accounts')
|
||||
@admin.action(description="Activate selected accounts")
|
||||
def activate_accounts(self, request, queryset):
|
||||
queryset.update(is_active=True)
|
||||
|
||||
@admin.action(description='Deactivate selected accounts')
|
||||
@admin.action(description="Deactivate selected accounts")
|
||||
def deactivate_accounts(self, request, queryset):
|
||||
queryset.update(is_active=False)
|
||||
|
||||
# Add ManyToManyField for Django Admin (if applicable)
|
||||
if hasattr(M3UAccount, 'user_agents'):
|
||||
filter_horizontal = ('user_agents',) # Only for ManyToManyField
|
||||
if hasattr(M3UAccount, "user_agents"):
|
||||
filter_horizontal = ("user_agents",) # Only for ManyToManyField
|
||||
|
||||
|
||||
@admin.register(M3UFilter)
|
||||
class M3UFilterAdmin(admin.ModelAdmin):
|
||||
list_display = ('m3u_account', 'filter_type', 'regex_pattern', 'exclude')
|
||||
list_filter = ('filter_type', 'exclude')
|
||||
search_fields = ('regex_pattern',)
|
||||
ordering = ('m3u_account',)
|
||||
list_display = ("m3u_account", "filter_type", "regex_pattern", "exclude")
|
||||
list_filter = ("filter_type", "exclude")
|
||||
search_fields = ("regex_pattern",)
|
||||
ordering = ("m3u_account",)
|
||||
|
||||
|
||||
@admin.register(ServerGroup)
|
||||
class ServerGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-02 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('m3u', '0014_alter_m3ufilter_options_m3ufilter_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='m3ufilter',
|
||||
options={},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='m3ufilter',
|
||||
name='custom_properties',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -180,6 +180,7 @@ class M3UFilter(models.Model):
|
|||
help_text="If True, matching items are excluded; if False, only matches are included.",
|
||||
)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
custom_properties = models.TextField(null=True, blank=True)
|
||||
|
||||
def applies_to(self, stream_name, group_name):
|
||||
target = group_name if self.filter_type == "group" else stream_name
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from core.models import UserAgent
|
|||
from apps.channels.models import ChannelGroup, ChannelGroupM3UAccount
|
||||
from apps.channels.serializers import (
|
||||
ChannelGroupM3UAccountSerializer,
|
||||
ChannelGroupSerializer,
|
||||
)
|
||||
import logging
|
||||
|
||||
|
|
@ -18,7 +17,14 @@ class M3UFilterSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = M3UFilter
|
||||
fields = ["id", "filter_type", "regex_pattern", "exclude", "order"]
|
||||
fields = [
|
||||
"id",
|
||||
"filter_type",
|
||||
"regex_pattern",
|
||||
"exclude",
|
||||
"order",
|
||||
"custom_properties",
|
||||
]
|
||||
|
||||
|
||||
class M3UAccountProfileSerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -279,7 +279,17 @@ def process_groups(account, groups):
|
|||
logger.info(f"Currently {len(existing_groups)} existing groups")
|
||||
|
||||
compiled_filters = [
|
||||
(re.compile(f.regex_pattern), f)
|
||||
(
|
||||
re.compile(
|
||||
f.regex_pattern,
|
||||
(
|
||||
re.IGNORECASE
|
||||
if json.loads(f.custom_properties or "{}").get("case_sensitive", True) == False
|
||||
else 0
|
||||
),
|
||||
),
|
||||
f,
|
||||
)
|
||||
for f in account.filters.order_by("order")
|
||||
if f.filter_type == "group"
|
||||
]
|
||||
|
|
@ -510,7 +520,17 @@ def process_m3u_batch(account_id, batch, groups, hash_keys):
|
|||
account = M3UAccount.objects.get(id=account_id)
|
||||
|
||||
compiled_filters = [
|
||||
(re.compile(f.regex_pattern), f)
|
||||
(
|
||||
re.compile(
|
||||
f.regex_pattern,
|
||||
(
|
||||
re.IGNORECASE
|
||||
if json.loads(f.custom_properties or "{}").get("case_sensitive", True) == False
|
||||
else 0
|
||||
),
|
||||
),
|
||||
f,
|
||||
)
|
||||
for f in account.filters.order_by("order")
|
||||
if f.filter_type != "group"
|
||||
]
|
||||
|
|
@ -519,7 +539,6 @@ def process_m3u_batch(account_id, batch, groups, hash_keys):
|
|||
streams_to_update = []
|
||||
stream_hashes = {}
|
||||
|
||||
# compiled_filters = [(f.filter_type, re.compile(f.regex_pattern, re.IGNORECASE)) for f in filters]
|
||||
logger.debug(f"Processing batch of {len(batch)} for M3U account {account_id}")
|
||||
for stream_info in batch:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -857,7 +857,6 @@ export default class API {
|
|||
body = { ...payload };
|
||||
delete body.file;
|
||||
}
|
||||
console.log(body);
|
||||
|
||||
const response = await request(`${host}/api/m3u/accounts/${id}/`, {
|
||||
method: 'PATCH',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import API from '../../api';
|
||||
import {
|
||||
TextInput,
|
||||
|
|
@ -7,24 +7,15 @@ import {
|
|||
Modal,
|
||||
Flex,
|
||||
Select,
|
||||
PasswordInput,
|
||||
Group,
|
||||
Stack,
|
||||
MultiSelect,
|
||||
ActionIcon,
|
||||
Switch,
|
||||
Box,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcwKey, X } from 'lucide-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import {
|
||||
M3U_FILTER_TYPES,
|
||||
USER_LEVELS,
|
||||
USER_LEVEL_LABELS,
|
||||
} from '../../constants';
|
||||
import useAuthStore from '../../store/auth';
|
||||
import { M3U_FILTER_TYPES } from '../../constants';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import { setCustomProperty } from '../../utils';
|
||||
|
||||
const M3UFilter = ({ filter = null, m3u, isOpen, onClose }) => {
|
||||
const fetchPlaylist = usePlaylistsStore((s) => s.fetchPlaylist);
|
||||
|
|
@ -35,6 +26,7 @@ const M3UFilter = ({ filter = null, m3u, isOpen, onClose }) => {
|
|||
filter_type: 'group',
|
||||
regex_pattern: '',
|
||||
exclude: true,
|
||||
case_sensitive: true,
|
||||
},
|
||||
|
||||
validate: (values) => ({}),
|
||||
|
|
@ -46,6 +38,8 @@ const M3UFilter = ({ filter = null, m3u, isOpen, onClose }) => {
|
|||
filter_type: filter.filter_type,
|
||||
regex_pattern: filter.regex_pattern,
|
||||
exclude: filter.exclude,
|
||||
case_sensitive:
|
||||
JSON.parse(filter.custom_properties || '{}').case_sensitive ?? true,
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
|
|
@ -55,6 +49,15 @@ const M3UFilter = ({ filter = null, m3u, isOpen, onClose }) => {
|
|||
const onSubmit = async () => {
|
||||
const values = form.getValues();
|
||||
|
||||
values.custom_properties = setCustomProperty(
|
||||
filter ? filter.custom_properties : {},
|
||||
'case_sensitive',
|
||||
values.case_sensitive,
|
||||
true
|
||||
);
|
||||
|
||||
delete values.case_sensitive;
|
||||
|
||||
if (!filter) {
|
||||
// By default, new rule will go at the end
|
||||
values.order = m3u.filters.length;
|
||||
|
|
@ -106,6 +109,19 @@ const M3UFilter = ({ filter = null, m3u, isOpen, onClose }) => {
|
|||
})}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Box>Case Sensitive</Box>
|
||||
<Switch
|
||||
id="case_sensitive"
|
||||
name="case_sensitive"
|
||||
description="If the regex should be case sensitive or not"
|
||||
key={form.key('case_sensitive')}
|
||||
{...form.getInputProps('case_sensitive', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
|
|
|
|||
|
|
@ -89,3 +89,30 @@ export const copyToClipboard = async (value) => {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setCustomProperty = (input, key, value, serialize = false) => {
|
||||
let obj;
|
||||
|
||||
if (input == null) {
|
||||
// matches null or undefined
|
||||
obj = {};
|
||||
} else if (typeof input === 'string') {
|
||||
try {
|
||||
obj = JSON.parse(input);
|
||||
} catch (e) {
|
||||
obj = {};
|
||||
}
|
||||
} else if (typeof input === 'object' && !Array.isArray(input)) {
|
||||
obj = { ...input }; // shallow copy
|
||||
} else {
|
||||
obj = {};
|
||||
}
|
||||
|
||||
obj[key] = value;
|
||||
|
||||
if (serialize === true) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue