mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into pr/sethwv/1336
This commit is contained in:
commit
37cc72a040
11 changed files with 130 additions and 52 deletions
|
|
@ -2800,8 +2800,9 @@ class LogoViewSet(viewsets.ModelViewSet):
|
|||
user_agent_obj = UserAgent.objects.get(id=int(default_user_agent_id))
|
||||
user_agent = user_agent_obj.user_agent
|
||||
except (CoreSettings.DoesNotExist, UserAgent.DoesNotExist, ValueError):
|
||||
# Fallback to hardcoded if default not found
|
||||
user_agent = 'Dispatcharr/1.0'
|
||||
# Fallback if default not found
|
||||
from core.utils import dispatcharr_user_agent
|
||||
user_agent = dispatcharr_user_agent()
|
||||
|
||||
# Hard total timeout (connect + full download) prevents a slow
|
||||
# server dripping bytes from holding a greenlet indefinitely.
|
||||
|
|
|
|||
|
|
@ -1181,12 +1181,13 @@ def _dvr_ffmpeg_retry_backoff_seconds(retry_index):
|
|||
|
||||
def _dvr_build_ffmpeg_cmd(stream_url, recording_id, hls_m3u8, hls_seg_pattern, hls_start_number):
|
||||
"""Build the FFmpeg command for DVR HLS segment recording."""
|
||||
from core.utils import dispatcharr_dvr_user_agent
|
||||
return [
|
||||
"ffmpeg", "-y",
|
||||
"-reconnect", "1",
|
||||
"-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", "5",
|
||||
"-user_agent", f"Dispatcharr-DVR/recording-{recording_id}",
|
||||
"-user_agent", dispatcharr_dvr_user_agent(recording_id),
|
||||
# Regenerate monotonic PTS to handle erratic/discontinuous timestamps
|
||||
# from IPTV sources.
|
||||
"-fflags", "+genpts",
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
import hashlib
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from version import __version__ as dispatcharr_version
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
username = (source.username or '').strip()
|
||||
password = (source.password or '').strip()
|
||||
|
|
@ -143,7 +143,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
auth_response = http_requests.post(
|
||||
f"{SD_BASE_URL}/token",
|
||||
json={'username': username, 'password': sha1_password},
|
||||
headers={'Content-Type': 'application/json', 'User-Agent': f'Dispatcharr/{dispatcharr_version}'},
|
||||
headers=dispatcharr_http_headers(),
|
||||
timeout=15,
|
||||
)
|
||||
auth_response.raise_for_status()
|
||||
|
|
@ -227,6 +227,24 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
f"Lockout set until {reset_at.isoformat()}."
|
||||
)
|
||||
|
||||
def _fetch_sd_countries(self):
|
||||
"""Fetch the SD country list (token not required; User-Agent is)."""
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
f"{SD_BASE_URL}/available/countries",
|
||||
headers=dispatcharr_http_headers(content_type=None),
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except http_requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Failed to fetch SD countries: {e}")
|
||||
return None
|
||||
|
||||
@action(detail=True, methods=["get", "post", "delete"], url_path="sd-lineups")
|
||||
def sd_lineups(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -236,7 +254,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from version import __version__ as dispatcharr_version
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
source = self.get_object()
|
||||
if source.source_type != 'schedules_direct':
|
||||
|
|
@ -249,13 +267,10 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
if error:
|
||||
return error
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'Dispatcharr/{dispatcharr_version}',
|
||||
'token': token,
|
||||
}
|
||||
headers = dispatcharr_http_headers(token=token)
|
||||
|
||||
if request.method == "GET":
|
||||
countries = self._fetch_sd_countries()
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
f"{SD_BASE_URL}/lineups",
|
||||
|
|
@ -272,6 +287,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"changes_remaining": self._get_sd_changes_remaining(source),
|
||||
"changes_reset_at": self._get_sd_reset_at(source),
|
||||
"notice": "No lineups are currently configured on this Schedules Direct account. Use the search below to add one.",
|
||||
"countries": countries,
|
||||
})
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
|
@ -281,6 +297,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"max_lineups": 4,
|
||||
"changes_remaining": self._get_sd_changes_remaining(source),
|
||||
"changes_reset_at": self._get_sd_reset_at(source),
|
||||
"countries": countries,
|
||||
})
|
||||
except http_requests.exceptions.RequestException as e:
|
||||
return Response(
|
||||
|
|
@ -399,7 +416,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from version import __version__ as dispatcharr_version
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
source = self.get_object()
|
||||
if source.source_type != 'schedules_direct':
|
||||
|
|
@ -420,11 +437,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
if error:
|
||||
return error
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'Dispatcharr/{dispatcharr_version}',
|
||||
'token': token,
|
||||
}
|
||||
headers = dispatcharr_http_headers(token=token)
|
||||
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
|
|
@ -493,6 +506,7 @@ class ProgramViewSet(viewsets.ModelViewSet):
|
|||
"""Proxy endpoint for SD program poster images. Nginx caches the response."""
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
program = self.get_object()
|
||||
poster_sd_url = (program.custom_properties or {}).get('sd_icon')
|
||||
|
|
@ -516,14 +530,10 @@ class ProgramViewSet(viewsets.ModelViewSet):
|
|||
if not token:
|
||||
sha1_password = hashlib.sha1(source.password.encode('utf-8')).hexdigest()
|
||||
try:
|
||||
from version import __version__ as dispatcharr_version
|
||||
auth_resp = http_requests.post(
|
||||
f"{SD_BASE_URL}/token",
|
||||
json={'username': source.username, 'password': sha1_password},
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'Dispatcharr/{dispatcharr_version}',
|
||||
},
|
||||
headers=dispatcharr_http_headers(),
|
||||
timeout=10,
|
||||
)
|
||||
auth_data = auth_resp.json()
|
||||
|
|
@ -549,7 +559,7 @@ class ProgramViewSet(viewsets.ModelViewSet):
|
|||
try:
|
||||
img_resp = http_requests.get(
|
||||
poster_sd_url,
|
||||
headers={'token': token},
|
||||
headers=dispatcharr_http_headers(token=token, content_type=None),
|
||||
timeout=15,
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2242,17 +2242,10 @@ def fetch_schedules_direct(source, stations_only=False, force=False):
|
|||
# SD API spec requires the User-Agent to identify the application and version.
|
||||
# SergeantPanda confirmed Dispatcharr should identify itself properly.
|
||||
# -------------------------------------------------------------------------
|
||||
from version import __version__ as dispatcharr_version
|
||||
sd_user_agent = f"Dispatcharr/{dispatcharr_version}"
|
||||
from core.utils import dispatcharr_http_headers
|
||||
|
||||
def _sd_headers(token=None):
|
||||
h = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': sd_user_agent,
|
||||
}
|
||||
if token:
|
||||
h['token'] = token
|
||||
return h
|
||||
return dispatcharr_http_headers(token=token)
|
||||
|
||||
def _sd_post_refresh_tasks(mapped_epg_ids, program_metadata, today):
|
||||
"""Poster fetch, logo auto-apply, and pruning — runs even when schedules are unchanged."""
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from rest_framework_simplejwt.authentication import JWTAuthentication
|
|||
from apps.accounts.authentication import ApiKeyAuthentication, QueryParamJWTAuthentication
|
||||
from apps.proxy.utils import check_user_stream_limits
|
||||
from dispatcharr.utils import network_access_allowed
|
||||
from core.utils import dispatcharr_user_agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -704,7 +705,7 @@ def head_vod(request, content_type, content_id, session_id=None, profile_id=None
|
|||
# Use M3U account's user agent as primary, client user agent as fallback
|
||||
m3u_user_agent = m3u_account.get_user_agent().user_agent if m3u_account.get_user_agent() else None
|
||||
headers = {
|
||||
'User-Agent': m3u_user_agent or client_user_agent or 'Dispatcharr/1.0',
|
||||
'User-Agent': m3u_user_agent or client_user_agent or dispatcharr_user_agent(),
|
||||
'Accept': '*/*',
|
||||
'Range': 'bytes=0-1' # Request only first 2 bytes
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue