mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 10:45:27 +00:00
Merge remote-tracking branch 'origin/dev' into xtream
This commit is contained in:
commit
c5dd351bf1
17 changed files with 130 additions and 55 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
console.error('Error saving channel:', error);
|
||||
}
|
||||
|
||||
formik.resetForm();
|
||||
API.requeryChannels();
|
||||
setSubmitting(false);
|
||||
setTvgFilter('');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const UserAgentsTable = () => {
|
|||
),
|
||||
},
|
||||
{
|
||||
header: 'Desecription',
|
||||
header: 'Description',
|
||||
accessorKey: 'description',
|
||||
enableSorting: false,
|
||||
Cell: ({ cell }) => (
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue