tv_grab_fr_mafreebox_hts.py 12.7 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')

Jonas's avatar
Jonas committed
33 34 35 36 37 38 39 40 41 42
TZ = timezone('Europe/Paris')
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/"

CONFIG_FILE = os.path.join(os.environ['HOME'],'.xmltv','tv_grab_fr_mafreebox.conf')

43
parser = argparse.ArgumentParser(description = "Grabber xmltv via l'api de la freebox")
44 45
parser.add_argument("--description", action = 'store_true', help="Description")
parser.add_argument("--capabilities", action = 'store_true', help="Capactitée")
46
parser.add_argument("--configure", action = 'store_true', help="Configurer le grabber")
Jonas's avatar
Jonas committed
47
parser.add_argument("--config-file", default=CONFIG_FILE, help="Fichier de configuration (%s par défaut)"%CONFIG_FILE)
Jonas's avatar
Jonas committed
48 49 50
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")
51
args = parser.parse_args()
Jonas's avatar
Jonas committed
52

Jonas's avatar
Jonas committed
53
if args.debug :
Jonas's avatar
Jonas committed
54
    logLevel = logging.DEBUG
Jonas's avatar
Jonas committed
55
elif args.verbose :
Jonas's avatar
Jonas committed
56
    logLevel = logging.INFO
Jonas's avatar
Jonas committed
57 58
elif args.quiet :
    logLevel = logging.CRITICAL
Jonas's avatar
Jonas committed
59 60 61 62 63 64 65 66 67 68 69 70 71
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
72 73
    5:  "Documentary", # Documentaire
    9:  "Variety show", # Variétés
Jonas's avatar
Jonas committed
74 75 76 77 78 79 80 81 82
    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
83 84
    24: "Performing arts", # Spectacle
    31: "Religion" # Emission religieuse
Jonas's avatar
Jonas committed
85 86
}  #todo à compléter

87 88

def get_cfg_chan() :
Jonas's avatar
Jonas committed
89 90 91 92
    '''
        Recupérère la liste des chaines configurées dans le fichier de
        configuration.
    '''
93
    if not os.path.isfile(args.config_file) :
Jonas's avatar
Jonas committed
94
        logging.error("Non configuré, lancez %s --configure"%__file__)
95 96 97 98 99 100 101
        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))
102
    logging.debug("Liste des chaînes configurée :\n\t%s"%"\n\t".join(list_chan))
103 104 105 106
    return list_chan


def get_hts_chan():
Jonas's avatar
Jonas committed
107 108 109
    '''
        Recupérère la liste des chaines configurée sur hts.
    '''
110 111 112
    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
113
    list_chan = []
114
    reponse = requests.get('%s/api/channel/list'%url,auth=(user,pswd))
Jonas's avatar
Jonas committed
115 116 117 118 119 120 121
    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

122 123

def get_mythtv_chan():
Jonas's avatar
Jonas committed
124 125 126
    '''
        Recupérère la liste des chaines configurée sur mythtv.
    '''
127 128 129 130 131 132 133 134 135 136
    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
137
def do_request(my_url):
Jonas's avatar
Jonas committed
138 139 140
    '''
        Execute une requète http.
    '''
Jonas's avatar
Jonas committed
141 142 143 144 145
    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":
Jonas's avatar
Jonas committed
146
        logging.info("Oups : trop de requètes, nouvelle tentative dans 60''")
Jonas's avatar
Jonas committed
147 148 149 150 151 152 153 154 155 156
        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():
Jonas's avatar
Jonas committed
157 158 159
    '''
        L'heure actuelle.
    '''
Jonas's avatar
Jonas committed
160 161 162
    now=int(time.time())
    return now-now%7200

163

Jonas's avatar
Jonas committed
164
def format_time(dt):
Jonas's avatar
Jonas committed
165 166 167
    '''
        Format de l'heure selon spécification xmltv
    '''
Jonas's avatar
Jonas committed
168 169 170 171
    return datetime.strftime(
        datetime.fromtimestamp(dt, tz=TZ),
        "%Y%m%d%H%M%S %z")

172

Jonas's avatar
Jonas committed
173
def get_epg_data(root, channel_uid, channel_slug, start_time):
Jonas's avatar
Jonas committed
174 175 176 177
    '''
        Complète l'xmltv <root> pour la chaine <channel_uid> à partir de
        <start_time>
    '''
Jonas's avatar
Jonas committed
178 179 180 181 182 183 184 185 186 187
    # 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:
188
                    logging.debug("\"Fake result\", stop la récupération des données epg")
Jonas's avatar
Jonas committed
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 238 239 240 241 242 243 244 245 246 247 248 249 250 251
                    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:
Jonas's avatar
Jonas committed
252
                logging.debug("Pas de description trouvée")
Jonas's avatar
Jonas committed
253 254 255 256 257
        root.append(cur_prog)
    return True


def build_xml():
Jonas's avatar
Jonas committed
258 259 260
    '''
        Construit l'xmltv
    '''
Jonas's avatar
Jonas committed
261 262 263 264 265 266
    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))
267
    cfg_chan = get_cfg_chan()
Jonas's avatar
Jonas committed
268 269 270
    start = start_time()
    i=1
    for chan_name, channel in dic_channels.iteritems():
271
        if slugify(channel.get('name')) not in cfg_chan:
Jonas's avatar
Jonas committed
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
            logging.info("%s pas dans la liste, saut"%channel.get('name'))
            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,
287
                    start_time())
Jonas's avatar
Jonas committed
288 289 290 291 292 293
        if res:
            root.insert(1, cur_channel)

    return ET.tostring(root)


294
def configure () :
Jonas's avatar
Jonas committed
295 296 297
    '''
        Lance la configuration
    '''
298 299 300 301
    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"+\
302 303
        "  3 : Préconfiguration à partir de hts tvheadend\n"+\
        "  4 : Préconfiguration à partir de mythtv (beta, non testé)\n"
304 305 306 307 308 309 310 311 312
      )
    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 = []
Jonas's avatar
Jonas committed
313 314
    if choise == '3' :
        hts_chan = get_hts_chan()
315 316
    elif choise == '4' :
        mythtv_chan= get_mythtv_chan()
Jonas's avatar
Jonas committed
317 318 319 320 321 322 323 324 325

    for chan_name in channels_names:
        if choise == '1'\
         or (choise == '3' and slugify(chan_name) in hts_chan)\
         or (choise == '4' and slugify(chan_name) in mythtv_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"))

326 327 328 329 330 331 332 333
    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)
        )
Jonas's avatar
Jonas committed
334
    print("Vous pouvez à tout moment éditer la liste des chaines dans le fichier %s"%args.config_file)
335 336


Jonas's avatar
Jonas committed
337
if __name__ == '__main__':
338
    if args.description :
339
        sys.stdout.write("France (mafreebox.fr)\n")
340
    elif args.capabilities :
341
        sys.stdout.write("manualconfig\nbaseline\n")
342 343
    elif args.configure :
        configure()
344 345 346
    else :
        sys.stdout.write(build_xml())
    sys.exit(0)
Jonas's avatar
Jonas committed
347