tv_grab_fr_mafreebox_hts.py 12.3 KB
Newer Older
Jonas's avatar
Jonas committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#!/usr/bin/env python
# -*- coding: utf8 -*-

#  Copyright (c) 2016
#  Jonas Fourquier (http://jonas.cloud) based on tr4ck3ur work
#  https://mythtv-fr.org/forums/viewtopic.php?pid=25265#p25265
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#

24
import sys, os, requests, argparse, time, logging
Jonas's avatar
Jonas committed
25 26
import xml.etree.ElementTree as ET
from datetime import datetime
Jonas's avatar
Jonas committed
27
from pytz import timezone
Jonas's avatar
Jonas committed
28 29 30 31 32
from slugify import slugify

reload(sys)
sys.setdefaultencoding('utf8')

33
parser = argparse.ArgumentParser(description = "Grabber xmltv via l'api de la freebox")
34 35
parser.add_argument("--description", action = 'store_true', help="Description")
parser.add_argument("--capabilities", action = 'store_true', help="Capactitée")
36 37 38
parser.add_argument("--configure", action = 'store_true', help="Configurer le grabber")
config_file = os.path.join(os.environ['HOME'],'.xmltv','tv_grab_fr_mafreebox.conf')
parser.add_argument("--config-file", action = 'store_true', default=config_file, help="Fichier de configuration (%s par défaut)"%config_file)
Jonas's avatar
Jonas committed
39 40 41
parser.add_argument("--quiet", action = 'store_true', help="Supprime toutes les sorties de progressions")
parser.add_argument("--verbose", action = 'store_true', help="Bavard (affiche plus d'information de progression)")
parser.add_argument("--debug", action = 'store_true', help="Affiche des informations pour le debug")
42
args = parser.parse_args()
Jonas's avatar
Jonas committed
43 44

TZ = timezone('Europe/Paris')
Jonas's avatar
Jonas committed
45 46 47 48 49 50 51
B_URL = "http://mafreebox.freebox.fr"
BASE_URL = "http://mafreebox.freebox.fr/api/v3/"
CHANNEL_URL = BASE_URL + "tv/channels/"
ICON_URL = BASE_URL + "tv/img/channels/logos68x60/"
EPG_URL = BASE_URL + "tv/epg/by_channel/"
PROG_URL = BASE_URL + "tv/epg/programs/"

Jonas's avatar
Jonas committed
52
if args.debug :
Jonas's avatar
Jonas committed
53
    logLevel = logging.DEBUG
Jonas's avatar
Jonas committed
54
elif args.verbose :
Jonas's avatar
Jonas committed
55
    logLevel = logging.INFO
Jonas's avatar
Jonas committed
56 57
elif args.quiet :
    logLevel = logging.CRITICAL
Jonas's avatar
Jonas committed
58 59 60 61 62 63 64 65 66 67 68 69 70
else :
    logLevel = logging.WARNING

logging.basicConfig(
    format = "%(asctime)s %(levelname)s - %(message)s",
    level = logLevel
  )

categories_en = {
# Selon https://tvheadend.org/projects/tvheadend/repository/entry/src/epg.c
    1:  "Movie / Drama", # Film
    2:  "Movie / Drama", # Téléfilm
    3:  "Movie / Drama", # Série /Feuilleton
71 72
    5:  "Documentary", # Documentaire
    9:  "Variety show", # Variétés
Jonas's avatar
Jonas committed
73 74 75 76 77 78 79 80 81
    10: "Magazines / Reports / Documentary", # Magazine
    11: "Children's / Youth programmes", # Jeunesse
    12: "Game show / Quiz / Contes", # Jeu
    13: "Music / Ballet / Dance", # Musique
    14: "Variety show", # Divertissement
    16: "Cartoons / Puppets", # Dessins animés
    19: "Sports", # Sport
    20: "News / Current affairs", # Journal
    22: "Discussion / Interview / Debate", # Débats
82 83
    24: "Performing arts", # Spectacle
    31: "Religion" # Emission religieuse
Jonas's avatar
Jonas committed
84 85
}  #todo à compléter

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

def get_cfg_chan() :
    if not os.path.isfile(args.config_file) :
        logging.error("Non configuré, lancez %s --configure"%__file)
        sys.exit(1)
    list_chan = []
    with open(args.config_file) as fp:
        for line in fp :
            chan_name = line.split('#',1)[0].strip()
            if chan_name :
                list_chan.append(slugify(chan_name))
    return list_chan


def get_hts_chan():
    url = (raw_input ("URL de hts (\"http://localhost:9981/\" par défaut) : ") or "http://localhost:9981/")
    user = (raw_input ("Utilisateur hts (\"admin\" par défaut) : ") or "admin")
    pswd = (raw_input ("Mot de passe(\"admin\" par défaut) : ") or "admin")
Jonas's avatar
Jonas committed
104
    list_chan = []
105
    reponse = requests.get('%s/api/channel/list'%url,auth=(user,pswd))
Jonas's avatar
Jonas committed
106 107 108 109 110 111 112
    if not reponse.ok :
        logging.error ("Erreur de connection au serveur hts (erreur http code %i)"%reponse.status_code)
        sys.exit(1)
    for entrie in reponse.json()['entries'] :
        list_chan.append(slugify(entrie['val']))
    return list_chan

113 114 115 116 117 118 119 120 121 122 123 124

def get_mythtv_chan():
    list_chan = []
    db = MythDB()
    SchemaVersion = db.settings.NULL.DBSchemaVer
    channels = Channel.getAllEntries()
    for chan in channels:
        if chan.sourceid==3:
            list_chan.append(slugify.slugify(chan.name))
    return list_chan


Jonas's avatar
Jonas committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
def do_request(my_url):
    logging.debug(my_url)
    time.sleep(1)
    response = requests.get(my_url)
    out_dic = response.json()
    while "msg" in out_dic and out_dic["msg"]=="Too many requests":
        logging.info("Oups : trop que requètes, nouvelle tentative dans 60''")
        time.sleep(60)
        logging.info("Nouvelle tentative ..")
        response = requests.get(my_url)
        out_dic = response.json()
    if "success" in out_dic and out_dic["success"]==False:
        logging.error("Oups : %s"%response.text)
    return out_dic.get('result',{})


def start_time():
    now=int(time.time())
    return now-now%7200

145

Jonas's avatar
Jonas committed
146 147 148 149 150
def format_time(dt):
    return datetime.strftime(
        datetime.fromtimestamp(dt, tz=TZ),
        "%Y%m%d%H%M%S %z")

151

Jonas's avatar
Jonas committed
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
def get_epg_data(root, channel_uid, channel_slug, start_time):
    # Scan EPG to get program data
    epg_progs = {}
    for time_slot in range(10):
        epgurl = EPG_URL + "%s/%s/" % (channel_uid, start_time+time_slot*7200)
        dic_epg = do_request(epgurl)
        logging.info("%d programes (%d au total)"%(len(dic_epg), len(epg_progs)))
        # don't handle fake results
        if len(dic_epg)==1:
            for k,v in dic_epg.items():
                if 'fake' in v:
                    return False
        epg_progs.update(dic_epg)

    for key, obj in epg_progs.iteritems():
        cur_prog = ET.Element('programme')
        cur_prog.attrib['start'] = format_time( obj.get('date'))
        cur_prog.attrib['stop'] = format_time( obj.get('date') + obj.get('duration') )
        cur_prog.attrib['channel'] = channel_slug
        # Add Title
        if 'title' in obj:
            title = ET.Element('title')
            title.text = obj.get('title')
            cur_prog.append(title)
        # Add Sub-Title
        if 'sub_title' in obj:
            subtitle = ET.Element('sub-title')
            subtitle.text = obj.get('sub_title')
            cur_prog.append(subtitle)
        # Add Description
        # desc = ET.Element('desc')
        # desc.text = str( obj.get('desc') )
        # Add category
        if 'picture' in obj:
            icon = ET.Element('icon')
            icon.attrib['src'] = B_URL+obj.get('picture')
            cur_prog.append(icon)
        if 'episode_number' in obj and 'season_number' in obj:
            episode = ET.Element('episode-num')
            episode.attrib['system'] = "xmltv_ns"
            episode.text="%d.%d.0" % (obj.get('season_number')-1,
                                    obj.get('episode_number')-1)
            cur_prog.append(episode)
        if 'category_name' in obj:
            if obj.get('category') in categories_en :
                #traduction de la catégorie en anglais pour le filtre de hts
                category_en = ET.Element('category')
                category_en.attrib['lang'] = "en"
                category_en.text = categories_en[obj.get('category')]
                cur_prog.append(category_en)
            else :
                logging.warning("Traduction de la catégorie inconnue merci de rapporter le problème %d:\"%s\""%(obj.get('category'), obj.get('category_name')))
            category_fr = ET.Element('category')
            category_fr.attrib['lang'] = "fr"
            category_fr.text = obj.get('category_name')
            cur_prog.append(category_fr)
        # Add length
        if 'duration' in obj:
            length = ET.Element('length')
            length.attrib['units'] = "seconds"
            length.text = str(obj.get('duration'))
        cur_prog.append(length)
        # check if need description depending category
        if 'category' in obj and obj.get('category') not in [0, 5, 6, 9, 10, 12, 13, 14, 19, 20, 22, 31]:
            # Ok let's check if we have a description
            epg_id = obj.get('id')
            prog_url = PROG_URL + "%s" % (epg_id)
            dic_prog = do_request(prog_url)
            if 'desc' in dic_prog:
                desc = ET.Element('desc')
                desc.attrib['lang'] = "fr"
                desc.text = dic_prog.get('desc')
                cur_prog.append(desc)
            else:
                logging.info("Pas de description trouvée")
        root.append(cur_prog)
    return True


def build_xml():
    root = ET.Element('tv')
    root.set('generator-info-name', 'snouf XMLTV generated')
    root.set('generator-info-url', 'https://gitlab.com/snouf/tvheadend_tools')

    dic_channels = do_request(CHANNEL_URL)
    logging.info("%d chaines trouvée"%len(dic_channels))
238
    cfg_chan = get_cfg_chan()
Jonas's avatar
Jonas committed
239 240 241
    start = start_time()
    i=1
    for chan_name, channel in dic_channels.iteritems():
242
        if slugify(channel.get('name')) not in cfg_chan:
Jonas's avatar
Jonas committed
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
            logging.info("%s pas dans la liste, saut"%channel.get('name'))
            continue
        if channel.get('available')==False:
            continue
        if channel.get('pub_service')==False:
            continue
        if channel.get('uuid').startswith('dvb'):
            continue
        if channel.get('name').startswith('C+'):
            continue
        channel_slug = slugify(channel.get('name')).lower()+'.mafreebox.fr'
        cur_channel = ET.Element('tv')
        cur_channel.attrib['id'] = channel_slug
        icon_channel = ET.Element('icon')
        icon_channel.attrib['src'] = B_URL + channel.get('logo_url')
        disp_channel = ET.Element('display-name')
        disp_channel.text = channel.get('name')
        cur_channel.append(disp_channel)
        cur_channel.append(icon_channel)
        logging.debug("Récupération epg pour %s"%channel.get('name'))
        res = get_epg_data(root,
                    channel.get('uuid'),
                    channel_slug,
266
                    start_time())
Jonas's avatar
Jonas committed
267 268 269 270 271 272
        if res:
            root.insert(1, cur_channel)

    return ET.tostring(root)


273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
def configure () :
    choise = raw_input(\
        "Configuration de la liste des chaines pour lequel récupérer l'epg :\n"+\
        "  1 : Toutes par défaut\n"+\
        "  2 : Aucune par défaut\n"+\
        "  3 : Préconfiguration à partir de hts tvheadend\n"
      )

    if choise not in ('1','2','3') :
        logging.error("Entrée invalide")
        sys.exit(1)

    dic_channels = do_request(CHANNEL_URL)
    channels_names = [chan.get('name') for chan in dic_channels.values()]
    channels_names.sort() #ordre alphanumérique plus pratique pour trouver une chaine
    list_chan = []
    if choise == '1' :
        for chan_name in channels_names:
            list_chan.append("%s # %s"%(chan_name,slugify(chan_name).lower()+".mafreebox.fr"))
    elif choise == '2' :
        for chan_name in channels_names:
            list_chan.append("# %s # %s"%(chan_name,slugify(chan_name).lower()+".mafreebox.fr"))
    elif choise == '3' :
        hts_chan= get_hts_chan()
        for chan_name in channels_names:
            if slugify(chan_name) in hts_chan:
                list_chan.append("%s # %s"%(chan_name,slugify(chan_name).lower()+".mafreebox.fr"))
            else :
                list_chan.append("#%s # %s"%(chan_name,slugify(chan_name).lower()+".mafreebox.fr"))
    if not os.path.isdir(os.path.dirname(args.config_file)) :
        os.makedirs(os.path.dirname(args.config_file))
    with open(args.config_file, 'w') as fp:
        fp.write(\
            "# Liste des chaines récupére dans le guide\n"+\
            "# Les lignes qui commencent par un # sont ignorées\n\n"+\
            "\n".join(list_chan)
        )
    print("Vous pouvez a tout moment éditer la liste des chaine dans le fichier %s"%args.config_file)


Jonas's avatar
Jonas committed
313
if __name__ == '__main__':
314
    if args.description :
315
        sys.stdout.write("France (mafreebox.fr)\n")
316
    elif args.capabilities :
317
        sys.stdout.write("manualconfig\nbaseline\n")
318 319
    elif args.configure :
        configure()
320 321 322
    else :
        sys.stdout.write(build_xml())
    sys.exit(0)
Jonas's avatar
Jonas committed
323