mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-21 01:05:30 +00:00
198 lines
6.2 KiB
Python
198 lines
6.2 KiB
Python
# apps/channels/tasks.py
|
|
import logging
|
|
import os
|
|
import re
|
|
import requests
|
|
import time
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime
|
|
|
|
from celery import shared_task
|
|
from django.utils.text import slugify
|
|
|
|
from apps.channels.models import Channel
|
|
from apps.epg.models import EPGData
|
|
from core.models import CoreSettings
|
|
|
|
from channels.layers import get_channel_layer
|
|
from asgiref.sync import async_to_sync
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from channels.layers import get_channel_layer
|
|
import tempfile
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Words we remove to help with fuzzy + embedding matching
|
|
COMMON_EXTRANEOUS_WORDS = [
|
|
"tv", "channel", "network", "television",
|
|
"east", "west", "hd", "uhd", "24/7",
|
|
"1080p", "720p", "540p", "480p",
|
|
"film", "movie", "movies"
|
|
]
|
|
|
|
def normalize_name(name: str) -> str:
|
|
"""
|
|
A more aggressive normalization that:
|
|
- Lowercases
|
|
- Removes bracketed/parenthesized text
|
|
- Removes punctuation
|
|
- Strips extraneous words
|
|
- Collapses extra spaces
|
|
"""
|
|
if not name:
|
|
return ""
|
|
|
|
norm = name.lower()
|
|
norm = re.sub(r"\[.*?\]", "", norm)
|
|
norm = re.sub(r"\(.*?\)", "", norm)
|
|
norm = re.sub(r"[^\w\s]", "", norm)
|
|
tokens = norm.split()
|
|
tokens = [t for t in tokens if t not in COMMON_EXTRANEOUS_WORDS]
|
|
norm = " ".join(tokens).strip()
|
|
return norm
|
|
|
|
@shared_task
|
|
def match_epg_channels():
|
|
"""
|
|
Goes through all Channels and tries to find a matching EPGData row by:
|
|
1) If channel.tvg_id is valid in EPGData, skip.
|
|
2) If channel has a tvg_id but not found in EPGData, attempt direct EPGData lookup.
|
|
3) Otherwise, perform name-based fuzzy matching with optional region-based bonus.
|
|
4) If a match is found, we set channel.tvg_id
|
|
5) Summarize and log results.
|
|
"""
|
|
logger.info("Starting EPG matching logic...")
|
|
|
|
# Attempt to retrieve a "preferred-region" if configured
|
|
try:
|
|
region_obj = CoreSettings.objects.get(key="preferred-region")
|
|
region_code = region_obj.value.strip().lower()
|
|
except CoreSettings.DoesNotExist:
|
|
region_code = None
|
|
|
|
matched_channels = []
|
|
channels_to_update = []
|
|
|
|
channels_json = [{
|
|
"id": channel.id,
|
|
"name": channel.name,
|
|
"tvg_id": channel.tvg_id,
|
|
"fallback_name": channel.tvg_id.strip() if channel.tvg_id else channel.name,
|
|
"norm_chan": normalize_name(channel.tvg_id.strip() if channel.tvg_id else channel.name)
|
|
} for channel in Channel.objects.all() if not channel.epg_data]
|
|
|
|
epg_json = [{
|
|
'id': epg.id,
|
|
'tvg_id': epg.tvg_id,
|
|
'name': epg.name,
|
|
'norm_name': normalize_name(epg.name),
|
|
'epg_source_id': epg.epg_source.id,
|
|
} for epg in EPGData.objects.all()]
|
|
|
|
payload = {
|
|
"channels": channels_json,
|
|
"epg_data": epg_json,
|
|
"region_code": region_code,
|
|
}
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
|
temp_file.write(json.dumps(payload).encode('utf-8'))
|
|
temp_file_path = temp_file.name
|
|
|
|
process = subprocess.Popen(
|
|
['python', '/app/scripts/epg_match.py', temp_file_path],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
# Log stderr in real-time
|
|
for line in iter(process.stderr.readline, ''):
|
|
if line:
|
|
logger.info(line.strip())
|
|
|
|
process.stderr.close()
|
|
stdout, stderr = process.communicate()
|
|
|
|
os.remove(temp_file_path)
|
|
|
|
if process.returncode != 0:
|
|
return f"Failed to process EPG matching: {stderr}"
|
|
|
|
result = json.loads(stdout)
|
|
channels_to_update = result["channels_to_update"]
|
|
matched_channels = result["matched_channels"]
|
|
|
|
if channels_to_update:
|
|
Channel.objects.bulk_update(channels_to_update, ['epg_data'])
|
|
|
|
total_matched = len(matched_channels)
|
|
if total_matched:
|
|
logger.info(f"Match Summary: {total_matched} channel(s) matched.")
|
|
for (cid, cname, tvg) in matched_channels:
|
|
logger.info(f" - Channel ID={cid}, Name='{cname}' => tvg_id='{tvg}'")
|
|
else:
|
|
logger.info("No new channels were matched.")
|
|
|
|
logger.info("Finished EPG matching logic.")
|
|
|
|
channel_layer = get_channel_layer()
|
|
async_to_sync(channel_layer.group_send)(
|
|
'updates',
|
|
{
|
|
'type': 'update',
|
|
"data": {"success": True, "type": "epg_match"}
|
|
}
|
|
)
|
|
|
|
return f"Done. Matched {total_matched} channel(s)."
|
|
|
|
@shared_task
|
|
def run_recording(channel_id, start_time_str, end_time_str):
|
|
channel = Channel.objects.get(id=channel_id)
|
|
|
|
start_time = datetime.fromisoformat(start_time_str)
|
|
end_time = datetime.fromisoformat(end_time_str)
|
|
|
|
duration_seconds = int((end_time - start_time).total_seconds())
|
|
filename = f'{slugify(channel.name)}-{start_time.strftime("%Y-%m-%d_%H-%M-%S")}.mp4'
|
|
|
|
channel_layer = get_channel_layer()
|
|
|
|
async_to_sync(channel_layer.group_send)(
|
|
"updates",
|
|
{
|
|
"type": "update",
|
|
"data": {"success": True, "type": "recording_started", "channel": channel.name}
|
|
},
|
|
)
|
|
|
|
logger.info(f"Starting recording for channel {channel.name}")
|
|
with requests.get(f"http://localhost:5656/proxy/ts/stream/{channel.uuid}", headers={
|
|
'User-Agent': 'Dispatcharr-DVR',
|
|
}, stream=True) as response:
|
|
# Raise an exception for bad responses (4xx, 5xx)
|
|
response.raise_for_status()
|
|
|
|
# Open the file in write-binary mode
|
|
with open(f"/data/recordings/{filename}", 'wb') as file:
|
|
start_time = time.time() # Start the timer
|
|
for chunk in response.iter_content(chunk_size=8192): # 8KB chunks
|
|
if time.time() - start_time > duration_seconds:
|
|
print(f"Timeout reached: {duration_seconds} seconds")
|
|
break
|
|
# Write the chunk to the file
|
|
file.write(chunk)
|
|
|
|
async_to_sync(channel_layer.group_send)(
|
|
"updates",
|
|
{
|
|
"type": "update",
|
|
"data": {"success": True, "type": "recording_ended", "channel": channel.name}
|
|
},
|
|
)
|
|
|
|
# After the loop, the file and response are closed automatically.
|
|
logger.info(f"Finished recording for channel {channel.name}")
|