Merge remote-tracking branch 'origin/dev' into xtream

This commit is contained in:
dekzter 2025-05-01 10:15:42 -04:00
commit c5dd351bf1
17 changed files with 130 additions and 55 deletions

View file

@ -265,8 +265,8 @@ class ChannelViewSet(viewsets.ModelViewSet):
stream_custom_props = json.loads(stream.custom_properties) if stream.custom_properties else {}
channel_number = None
if 'tv-chno' in stream_custom_props:
channel_number = int(stream_custom_props['tv-chno'])
if 'tvg-chno' in stream_custom_props:
channel_number = int(stream_custom_props['tvg-chno'])
elif 'channel-number' in stream_custom_props:
channel_number = int(stream_custom_props['channel-number'])
@ -386,8 +386,8 @@ class ChannelViewSet(viewsets.ModelViewSet):
stream_custom_props = json.loads(stream.custom_properties) if stream.custom_properties else {}
channel_number = None
if 'tv-chno' in stream_custom_props:
channel_number = int(stream_custom_props['tv-chno'])
if 'tvg-chno' in stream_custom_props:
channel_number = int(stream_custom_props['tvg-chno'])
elif 'channel-number' in stream_custom_props:
channel_number = int(stream_custom_props['channel-number'])
@ -600,6 +600,8 @@ class ChannelViewSet(viewsets.ModelViewSet):
parse_programs_for_tvg_id.delay(epg_id)
programs_refreshed += 1
return Response({
'success': True,
'channels_updated': channels_updated,

View file

@ -38,8 +38,14 @@ class EPGSource(models.Model):
# Build full path in MEDIA_ROOT/cached_epg
cache_dir = os.path.join(settings.MEDIA_ROOT, "cached_epg")
# Create directory if it doesn't exist
os.makedirs(cache_dir, exist_ok=True)
cache = os.path.join(cache_dir, filename)
return cache
class EPGData(models.Model):
# Removed the Channel foreign key. We now just store the original tvg_id
# and a name (which might simply be the tvg_id if no real channel exists).

View file

@ -24,6 +24,7 @@ from .services.channel_service import ChannelService
from .url_utils import generate_stream_url, transform_url, get_stream_info_for_switch, get_stream_object, get_alternate_streams
from .utils import get_logger
from uuid import UUID
import gevent
logger = get_logger()
@ -119,7 +120,7 @@ def stream_ts(request, channel_id):
# Wait before retrying (using exponential backoff with a cap)
wait_time = min(0.5 * (2 ** attempt), 2.0) # Caps at 2 seconds
logger.info(f"[{client_id}] Waiting {wait_time:.1f}s for a connection to become available (attempt {attempt+1}/{max_retries})")
time.sleep(wait_time)
gevent.sleep(wait_time) # FIXED: Using gevent.sleep instead of time.sleep
if stream_url is None:
# Make sure to release any stream locks that might have been acquired
@ -258,7 +259,7 @@ def stream_ts(request, channel_id):
proxy_server.stop_channel(channel_id)
return JsonResponse({'error': 'Failed to connect'}, status=502)
time.sleep(0.1)
gevent.sleep(0.1) # FIXED: Using gevent.sleep instead of time.sleep
logger.info(f"[{client_id}] Successfully initialized channel {channel_id}")
channel_initializing = True

View file

@ -41,11 +41,12 @@ RUN cd /app && \
pip install --no-cache-dir -r requirements.txt
# Use a dedicated Node.js stage for frontend building
FROM node:20-slim AS frontend-builder
FROM node:20 AS frontend-builder
WORKDIR /app/frontend
COPY --from=builder /app /app
RUN npm install --legacy-peer-deps && \
npm run build && \
RUN corepack enable && corepack prepare yarn@stable --activate && \
yarn install && \
yarn build && \
find . -maxdepth 1 ! -name '.' ! -name 'dist' -exec rm -rf '{}' \;
FROM python:3.13-slim

View file

@ -40,8 +40,14 @@ export DISPATCHARR_PORT=${DISPATCHARR_PORT:-9191}
# Extract version information from version.py
export DISPATCHARR_VERSION=$(python -c "import sys; sys.path.append('/app'); import version; print(version.__version__)")
export DISPATCHARR_BUILD=$(python -c "import sys; sys.path.append('/app'); import version; print(version.__build__)")
echo "📦 Dispatcharr version: ${DISPATCHARR_VERSION}-${DISPATCHARR_BUILD}"
export DISPATCHARR_TIMESTAMP=$(python -c "import sys; sys.path.append('/app'); import version; print(version.__timestamp__ or '')")
# Display version information with timestamp if available
if [ -n "$DISPATCHARR_TIMESTAMP" ]; then
echo "📦 Dispatcharr version: ${DISPATCHARR_VERSION} (build: ${DISPATCHARR_TIMESTAMP})"
else
echo "📦 Dispatcharr version: ${DISPATCHARR_VERSION}"
fi
# READ-ONLY - don't let users change these
export POSTGRES_DIR=/data/db
@ -64,7 +70,7 @@ if [[ ! -f /etc/profile.d/dispatcharr.sh ]]; then
echo "export POSTGRES_DIR=$POSTGRES_DIR" >> /etc/profile.d/dispatcharr.sh
echo "export DISPATCHARR_PORT=$DISPATCHARR_PORT" >> /etc/profile.d/dispatcharr.sh
echo "export DISPATCHARR_VERSION=$DISPATCHARR_VERSION" >> /etc/profile.d/dispatcharr.sh
echo "export DISPATCHARR_BUILD=$DISPATCHARR_BUILD" >> /etc/profile.d/dispatcharr.sh
echo "export DISPATCHARR_TIMESTAMP=$DISPATCHARR_TIMESTAMP" >> /etc/profile.d/dispatcharr.sh
fi
chmod +x /etc/profile.d/dispatcharr.sh

View file

@ -41,6 +41,7 @@ lazy-apps = true # Improve memory efficiency
# Async mode (use gevent for high concurrency)
gevent = 100
async = 100
gevent-monkey-patch = true ; Ensure all blocking operations are patched (especially important for Ryzen CPUs)
# Performance tuning
thunder-lock = true

View file

@ -128,9 +128,6 @@ export const WebsocketProvider = ({ children }) => {
// Check if we have associations data and use the more efficient batch API
if (event.data.associations && event.data.associations.length > 0) {
API.batchSetEPG(event.data.associations);
} else {
// Fall back to legacy full refresh method
API.requeryChannels();
}
break;

View file

@ -1305,6 +1305,9 @@ export default class API {
color: 'blue',
});
// First fetch the complete channel data
await useChannelsStore.getState().fetchChannels();
// Then refresh the current table view
this.requeryChannels();
}

View file

@ -161,6 +161,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
console.error('Error saving channel:', error);
}
formik.resetForm();
API.requeryChannels();
setSubmitting(false);
setTvgFilter('');

View file

@ -21,20 +21,33 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
const profileSearchPreview = usePlaylistsStore((s) => s.profileSearchPreview);
const profileResult = usePlaylistsStore((s) => s.profileResult);
const [streamUrl, setStreamUrl] = useState('');
const [searchPattern, setSearchPattern] = useState('');
const [replacePattern, setReplacePattern] = useState('');
const [debouncedPatterns, setDebouncedPatterns] = useState({});
useEffect(() => {
async function fetchStreamUrl() {
const params = new URLSearchParams();
params.append('page', 1);
params.append('page_size', 1);
params.append('m3u_account', m3u.id);
const response = await API.queryStreams(params);
setStreamUrl(response.results[0].url);
}
fetchStreamUrl();
}, []);
useEffect(() => {
sendMessage(
JSON.stringify({
type: 'm3u_profile_test',
url: m3u.server_url,
url: streamUrl,
search: debouncedPatterns['search'] || '',
replace: debouncedPatterns['replace'] || '',
})
);
}, [m3u, debouncedPatterns]);
}, [m3u, debouncedPatterns, streamUrl]);
useEffect(() => {
const handler = setTimeout(() => {
@ -155,7 +168,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
<Text>Search</Text>
<Text
dangerouslySetInnerHTML={{
__html: profileSearchPreview || m3u.server_url,
__html: profileSearchPreview || streamUrl,
}}
sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
/>
@ -163,7 +176,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
<Paper p="md" radius="md" withBorder>
<Text>Replace</Text>
<Text>{profileResult || m3u.server_url}</Text>
<Text>{profileResult || streamUrl}</Text>
</Paper>
</Modal>
);

View file

@ -95,19 +95,19 @@ const ChannelRowActions = React.memo(
}) => {
const onEdit = useCallback(() => {
editChannel(row.original);
}, []);
}, [row.original]);
const onDelete = useCallback(() => {
deleteChannel(row.original.id);
}, []);
}, [row.original]);
const onPreview = useCallback(() => {
handleWatchStream(row.original);
}, []);
}, [row.original]);
const onRecord = useCallback(() => {
createRecording(row.original);
}, []);
}, [row.original]);
return (
<Box style={{ width: '100%', justifyContent: 'left' }}>
@ -523,9 +523,9 @@ const ChannelsTable = ({}) => {
},
},
{
id: 'channel_number',
accessorKey: 'channel_number',
size: 30,
header: () => <Flex justify="flex-end">#</Flex>,
size: 40,
cell: ({ getValue }) => (
<Flex justify="flex-end" style={{ width: '100%' }}>
<Text size="sm">{getValue()}</Text>
@ -706,6 +706,7 @@ const ChannelsTable = ({}) => {
},
headerCellRenderFns: {
name: renderHeaderCell,
channel_number: renderHeaderCell,
channel_group: renderHeaderCell,
enabled: renderHeaderCell,
},

View file

@ -42,20 +42,39 @@ const EPGsTable = () => {
{
header: 'Name',
accessorKey: 'name',
size: 150,
minSize: 100,
},
{
header: 'Source Type',
accessorKey: 'source_type',
size: 120,
minSize: 100,
},
{
header: 'URL / API Key',
accessorKey: 'url',
size: 200,
minSize: 120,
enableSorting: false,
Cell: ({ cell }) => (
<div
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
}}
>
{cell.getValue()}
</div>
),
},
{
header: 'Active',
accessorKey: 'is_active',
size: 100,
size: 80,
minSize: 60,
sortingFn: 'basic',
mantineTableBodyCellProps: {
align: 'left',
@ -73,6 +92,8 @@ const EPGsTable = () => {
{
header: 'Updated',
accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'),
size: 180,
minSize: 100,
enableSorting: false,
},
],
@ -144,6 +165,13 @@ const EPGsTable = () => {
density: 'compact',
},
enableRowActions: true,
positionActionsColumn: 'last',
displayColumnDefOptions: {
'mrt-row-actions': {
size: 120, // Make action column wider
minSize: 120, // Ensure minimum width for action buttons
},
},
renderRowActions: ({ row }) => (
<>
<ActionIcon
@ -176,11 +204,7 @@ const EPGsTable = () => {
mantineTableContainerProps: {
style: {
height: 'calc(40vh - 10px)',
},
},
displayColumnDefOptions: {
'mrt-row-actions': {
size: 10,
overflowX: 'auto', // Ensure horizontal scrolling works
},
},
});

View file

@ -99,16 +99,21 @@ const M3UTable = () => {
{
header: 'Name',
accessorKey: 'name',
size: 150,
minSize: 100, // Minimum width
},
{
header: 'URL / File',
accessorKey: 'server_url',
size: 200,
minSize: 120,
Cell: ({ cell }) => (
<div
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
}}
>
{cell.getValue()}
@ -118,7 +123,8 @@ const M3UTable = () => {
{
header: 'Max Streams',
accessorKey: 'max_streams',
size: 200,
size: 120,
minSize: 80,
},
{
header: 'Status',
@ -132,12 +138,14 @@ const M3UTable = () => {
return generateStatusString(refreshProgress[row.id]);
},
size: 200,
size: 150,
minSize: 80,
},
{
header: 'Active',
accessorKey: 'is_active',
size: 100,
size: 80,
minSize: 60,
sortingFn: 'basic',
mantineTableBodyCellProps: {
align: 'left',
@ -155,6 +163,8 @@ const M3UTable = () => {
{
header: 'Updated',
accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'),
size: 180,
minSize: 100,
enableSorting: false,
},
],
@ -239,6 +249,13 @@ const M3UTable = () => {
density: 'compact',
},
enableRowActions: true,
positionActionsColumn: 'last',
displayColumnDefOptions: {
'mrt-row-actions': {
size: 120, // Make action column wider
minSize: 120, // Ensure minimum width for action buttons
},
},
renderRowActions: ({ row }) => (
<>
<ActionIcon
@ -273,6 +290,7 @@ const M3UTable = () => {
mantineTableContainerProps: {
style: {
height: 'calc(40vh - 10px)',
overflowX: 'auto', // Ensure horizontal scrolling works
},
},
});

View file

@ -156,7 +156,7 @@ const StreamRowActions = ({
);
};
const StreamsTable = ({}) => {
const StreamsTable = ({ }) => {
const theme = useMantineTheme();
/**
@ -177,7 +177,7 @@ const StreamsTable = ({}) => {
// const [allRowsSelected, setAllRowsSelected] = useState(false);
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 250,
pageSize: 50,
});
const [filters, setFilters] = useState({
name: '',
@ -606,23 +606,22 @@ const StreamsTable = ({}) => {
{/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
<Group justify="space-between" style={{ paddingLeft: 10 }}>
<Box>
{selectedStreamIds.length > 0 && (
<Button
leftSection={<IconSquarePlus size={18} />}
variant="light"
size="xs"
onClick={addStreamsToChannel}
p={5}
color={theme.tailwind.green[5]}
style={{
borderWidth: '1px',
borderColor: theme.tailwind.green[5],
color: 'white',
}}
>
Add Streams to Channel
</Button>
)}
<Button
leftSection={<IconSquarePlus size={18} />}
variant={selectedStreamIds.length > 0 && selectedChannelIds.length === 1 ? "light" : "default"}
size="xs"
onClick={addStreamsToChannel}
p={5}
color={selectedStreamIds.length > 0 && selectedChannelIds.length === 1 ? theme.tailwind.green[5] : undefined}
style={selectedStreamIds.length > 0 && selectedChannelIds.length === 1 ? {
borderWidth: '1px',
borderColor: theme.tailwind.green[5],
color: 'white',
} : undefined}
disabled={!(selectedStreamIds.length > 0 && selectedChannelIds.length === 1)}
>
Add Streams to Channel
</Button>
</Box>
<Box

View file

@ -55,7 +55,7 @@ const UserAgentsTable = () => {
),
},
{
header: 'Desecription',
header: 'Description',
accessorKey: 'description',
enableSorting: false,
Cell: ({ cell }) => (

View file

@ -15,7 +15,9 @@ const M3UPage = () => {
<Stack
style={{
padding: 10,
height: 'calc(100vh - 60px)', // Set a specific height to ensure proper display
}}
spacing="xs" // Reduce spacing to give tables more room
>
<Box sx={{ flex: '1 1 50%', overflow: 'hidden' }}>
<M3UsTable />

View file

@ -1,5 +1,5 @@
"""
Dispatcharr version information.
"""
__version__ = '0.3.3' # Follow semantic versioning (MAJOR.MINOR.PATCH)
__version__ = '0.4.0' # Follow semantic versioning (MAJOR.MINOR.PATCH)
__timestamp__ = None # Set during CI/CD build process