Ocena wątku:
  • 0 głosów - średnia: 0
  • 1
  • 2
  • 3
  • 4
  • 5
parser EPG Telewizja Biznesowa
#1
Witam,

zrobiłem przy pomocy AI parser do Telewizja Biznesowa, edytowałem i dodałem jeszcze opis i grafikę.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Telewizja Biznesowa (tbpl) -> XMLTV EPG

Pobiera harmonogram z API MediaTool i zapisuje program w formacie XMLTV.
Domyślnie zapisuje dzisiejszy dzień oraz dwa kolejne dni.

Wymagania:
    python3 -m pip install requests

Uruchomienie:
    python3 telewizjabiznesowa_epg.py

Opcje:
    python3 telewizjabiznesowa_epg.py --days 2
    python3 telewizjabiznesowa_epg.py -o /ścieżka/telewizjabiznesowa.xml
    python3 telewizjabiznesowa_epg.py --channel-id telewizjabiznesowa.pl

Uwaga:
- Parametr --days oznacza liczbę dni naprzód oprócz dzisiejszego dnia.
  --days 2 obejmuje dziś, jutro i pojutrze.
- Ustaw CHANNEL_TOKEN 
"""

import argparse
import html
import logging
import os
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, time as dt_time, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

import requests


API_URL = "https://app.mediatool.tv/api/epg-api/information/channel/tbpl/weeks/1"
TIMEZONE =  ZoneInfo("Europe/Warsaw") #timezone.utc
DEFAULT_OUTPUT = "telewizjabiznesowa.xml"
#USER_AGENT = "Mozilla/5.0 (compatible; XMLTV-EPG/1.0)"
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"
CHANNEL_TOKEN='86e89a49-9e7c-48da-8c29-7db6807e8f8b'

def parse_datetime(value):
    """Zachowuje godzinę z API i interpretuje ją jako czas polski."""
    if not value:
        return None

    try:
        parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))

        # Usuwamy źródłową strefę bez przeliczania godziny.
        # Następnie przypisujemy strefę docelową.
        parsed = parsed.replace(tzinfo=None)
        return parsed.replace(tzinfo=TIMEZONE)

    except (ValueError, TypeError):
        logging.warning("Nie można odczytać daty: %r", value)
        return None

def localized_text(value, language="AB"):
    """Wydobywa tekst z pola, które może być zwykłym stringiem lub mapą języków."""
    if isinstance(value, dict):
        text = value.get(language)
        if text is None:
            # Awaryjnie wybierz pierwszą niepustą wartość tekstową.
            text = next((v for v in value.values() if isinstance(v, str) and v.strip()), "")
        return str(text).strip() if text is not None else ""
    if isinstance(value, str):
        return value.strip()
    return ""


def add_text(parent, tag, value, **attrs):
    node = ET.SubElement(parent, tag, attrs)
    node.text = value
    return node


def xmltv_timestamp(value):
    """XMLTV zapisuje czas lokalny wraz z przesunięciem względem UTC."""
#    value = value.astimezone(timezone.utc)
    return value.strftime("%Y%m%d%H%M%S %z")

#def xmltv_timestamp(value):
#    """Zapisuje czas w UTC w formacie XMLTV."""
#    value = value.astimezone(timezone.utc)
#    return value.strftime("%Y%m%d%H%M%S +0000")


def fetch_schedule(token, timeout):
    url = API_URL
    started = time.monotonic()
    try:
        response = requests.get(
            url,
            params={"token": token},
            headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
            timeout=(15, timeout),
        )
        elapsed = time.monotonic() - started
        response.raise_for_status()
        try:
            data = response.json()
        except requests.exceptions.JSONDecodeError as exc:
            raise RuntimeError("API nie zwróciło poprawnego JSON-a.") from exc
        logging.info(
            "Pobrano odpowiedź HTTP %s w %.2f s (%s bajtów).",
            response.status_code, elapsed, len(response.content)
        )
    except requests.RequestException as exc:
        raise RuntimeError(f"Błąd pobierania danych z API: {exc}") from exc

    if not isinstance(data, dict):
        raise RuntimeError("Nieoczekiwany format odpowiedzi API: oczekiwano obiektu JSON.")
    schedule = data.get("schedule")
    if not isinstance(schedule, list):
        keys = ", ".join(data.keys())
        raise RuntimeError(f"Brak listy 'schedule' w odpowiedzi API. Dostępne pola: {keys}")
    return schedule


def programme_bounds(now, days_ahead):
    start = now.replace(hour=0, minute=0, second=0, microsecond=0)
    # Koniec jest wyłączny: przy --days 2 obejmujemy dziś, jutro i pojutrze.
    end_date = start.date() + timedelta(days=days_ahead + 1)
    end = datetime.combine(end_date, dt_time.min, tzinfo=TIMEZONE)
    return start, end


def build_xml(schedule, output, days_ahead, channel_id, channel_name):
    now = datetime.now(TIMEZONE)
    start_bound, end_bound = programme_bounds(now, days_ahead)

    root = ET.Element("tv", {"source-info-name": "MediaTool", "generator-info-name": "telewizjabiznesowa_epg.py"})
    channel = ET.SubElement(root, "channel", {"id": channel_id})
    add_text(channel, "display-name", channel_name, lang="pl")

    programmes = []
    skipped = 0
    for item in schedule:
        if not isinstance(item, dict):
            skipped += 1
            continue

        start = parse_datetime(item.get("startTime"))
        end = parse_datetime(item.get("endTime"))
        if not start or not end or end <= start:
            skipped += 1
            continue

        # Zachowujemy audycje przecinające wybrany zakres.
        if end <= start_bound or start >= end_bound:
            continue

        title = localized_text(item.get("title"))
        episode_title = localized_text(item.get("episodeTitle"))
        if not title:
            title = episode_title or "Program"

        programme = ET.Element(
            "programme",
            {
                "start": xmltv_timestamp(start),
                "stop": xmltv_timestamp(end),
                "channel": channel_id,
            },
        )
        add_text(programme, "title", title, lang="pl")
        if episode_title and episode_title.casefold() != title.casefold():
            add_text(programme, "sub-title", episode_title, lang="pl")

        # Dodatkowe pola dodajemy tylko wtedy, gdy faktycznie są obecne w JSON-ie.
   # Pełny opis audycji: synopsis.PL.longSynopsis
        synopsis = item.get("synopsis")
        description = ""

        if isinstance(synopsis, dict):
            synopsis_pl = synopsis.get("PL")
            if isinstance(synopsis_pl, dict):
                long_synopsis = synopsis_pl.get("longSynopsis")
                if isinstance(long_synopsis, str):
                    description = long_synopsis.strip()

        # Awaryjnie wykorzystaj dotychczasowe pole description.
        if not description:
            description = localized_text(item.get("description"))

        if description:
            add_text(programme, "desc", description, lang="pl")

        # Obraz audycji: images.stillScene
        images = item.get("images")
        if isinstance(images, dict):
            image_url = images.get("stillScene")
            if isinstance(image_url, str) and image_url.strip():
                ET.SubElement(
                    programme,
                    "icon",
                    {"src": image_url.strip()}
                )

        # Data produkcji: productionDate, np. 2024-12-05.
        production_date = item.get("productionDate")
        if isinstance(production_date, str) and production_date.strip():
            try:
                date_value = datetime.strptime(
                    production_date.strip()[:10], "%Y-%m-%d"
                ).strftime("%Y%m%d")
                add_text(programme, "date", date_value)
            except ValueError:
                logging.warning(
                    "Nieprawidłowa data produkcji: %r", production_date
                )

        # Kategoria, jeśli występuje w odpowiedzi API.
        category = localized_text(item.get("category"))
        if category:
            add_text(programme, "category", category, lang="pl")

        programmes.append((start, programme))

    programmes.sort(key=lambda pair: pair[0])
    for _, programme in programmes:
        root.append(programme)

    tree = ET.ElementTree(root)
    try:
        ET.indent(tree, space="  ")
    except AttributeError:
        pass

    output_path = Path(output).expanduser()
    output_path.parent.mkdir(parents=True, exist_ok=True)
    tree.write(output_path, encoding="utf-8", xml_declaration=True)

    return output_path, len(programmes), skipped, start_bound, end_bound


def main():
    parser = argparse.ArgumentParser(description="Pobiera EPG Telewizji Biznesowej z MediaTool i zapisuje XMLTV.")
    parser.add_argument("-o", "--output", default=DEFAULT_OUTPUT, help=f"Plik wyjściowy (domyślnie: {DEFAULT_OUTPUT})")
    parser.add_argument("--days", type=int, default=2, help="Ile dni naprzód poza dzisiejszym dniem; domyślnie 2")
    parser.add_argument("--channel-id", default="Telewizja Biznesowa", help="Identyfikator kanału XMLTV")
    parser.add_argument("--channel-name", default="Telewizja Biznesowa", help="Nazwa kanału XMLTV")
    parser.add_argument("--timeout", type=int, default=120, help="Limit czasu odpowiedzi API w sekundach")
    parser.add_argument("-v", "--verbose", action="store_true", help="Więcej informacji diagnostycznych")
    args = parser.parse_args()

    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(asctime)s %(levelname)s: %(message)s",
    )

    if args.days < 0 or args.days > 6:
        parser.error("--days musi być w zakresie 0–6.")

    token = CHANNEL_TOKEN
    if not token:
        logging.error("Brak tokenu. Ustaw zmienną środowiskową CHANNEL_TOKEN.")
        logging.error("Przykład: export CHANNEL_TOKEN='TWÓJ_TOKEN'")
        return 2

    try:
        schedule = fetch_schedule(token, args.timeout)
        output, count, skipped, start, end = build_xml(
            schedule, args.output, args.days, args.channel_id, args.channel_name
        )
    except (RuntimeError, OSError) as exc:
        logging.error("%s", exc)
        return 1

    logging.info("Zakres EPG: %s – %s (koniec wyłączny).", start.isoformat(), end.isoformat())
    logging.info("Zapisano %d audycji do: %s", count, output.resolve())
    if skipped:
        logging.warning("Pominięto %d nieprawidłowych rekordów.", skipped)
    if count == 0:
        logging.warning("Nie znaleziono audycji w wybranym zakresie. Sprawdź zakres API i format dat.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Odpowiedz
REKLAMA:





Skocz do:


Użytkownicy przeglądający ten wątek: memek86, 1 gości