case sensitive flag and other possible custom properties for filters

This commit is contained in:
dekzter 2025-08-03 08:40:00 -04:00
parent f1752cc720
commit f300da6eff
8 changed files with 143 additions and 36 deletions

View file

@ -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",)

View file

@ -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),
),
]

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -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',

View file

@ -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">

View file

@ -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;
};