Update for Sailgoal

Hello dear community,
I just made an update for the app Sailgoal which can be found in https://openrepos.net/content/emanymton/sailgoal.
The app uses Germanys first TV channel (ARD) Teletext for displaying football results of European Leagues.
I updates the page links and removed leagues that are no longer supported.
The file to be changed can be found in


and it’s called


Here is the code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from urllib.request import urlopen
from lxml import html
import time
import datetime
import calendar
import locale

URL = 'https://www.ard-text.de/mobil/{}'

LEAGUE_ID = {'1.Bundesliga': 252, 
             '2.Bundesliga': 276, 
             '3.Bundesliga': 502,
             'Premier League': 522, 
             'Serie A': 526, 
             'Primera Division': 524, 
             'Ligue 1': 528, 
             'Eredivisie': 531, 
             'Jupiler League':532,
             'SPORTOTO Süper Lig': 536, 
             'Super League': 537, 
             'Liga NOS':535,
             'Raiffeisen Super League': 534, 
             'tipico - Bundesliga':533,
             'ALKA Superligaen': 539, 
             'Scottish Premiership': 538, 
             'T-Mobile Ekstraklasa': 540, 
             'SYNOT Liga': 541, #'1. HNL': 546, 'Jelen SuperLiga': 547,
             #'Liga I': 548, 'Premier Liga': 549,
             #'CL Group A-B': 678, 'CL Group C-D': 679, 'CL Group E-F': 680, 
             #'CL Group G-H': 681,
             #'EL Group A-D': 683, 'EL Group E-H': 684, 'EL Group I-L': 685,
             #'Group A-C': 686, 'Group D-F':687, 'Group G-I':688,
             #'UEFA EURO 2016 A': 817,
             #'1 Champions League': 553,
             #'2 Europa League': 554,
             #'3 Conference League': 555,
             #'DFB-Pokal': 551

LEAGUE_COUNTRY = {'1.Bundesliga': 'Germany', 
                  '2.Bundesliga': 'Germany', 
                  '3.Bundesliga': 'Germany', 
                  'Premier League': 'England', 
                  'Serie A': 'Italy', 
                  'Primera Division': 'Spain',
                  'Eredivisie': 'Netherlands', 
                  'Jupiler League': 'Belgium',
                  'Ligue 1': 'France', 
                  'SPORTOTO Süper Lig':'Turkey',
                  'Super League': 'Greece', 
                  'Liga NOS': 'Portugal',
                  'Raiffeisen Super League': 'Switzerland',
                  'tipico - Bundesliga': 'Austria',
                  'ALKA Superligaen': 'Denmark', 
                  #'Allsvenskan': 'Sweden', 
                  'Scottish Premiership': 'Scotland',
                  'T-Mobile Ekstraklasa': 'Poland',
                  'SYNOT Liga':'Czechia', #'1. HNL': 'Croatia',
                  #'Jelen SuperLiga': 'Serbia',
                  #'Liga I': 'Romania',
                  #'Premier Liga': 'Russia',
                  #'CL Group A-B': 'Champions League',
                  #'CL Group C-D': 'Champions League',
                  #'CL Group E-F': 'Champions League',
                  #'CL Group G-H': 'Champions League', 
                  #'EL Group A-D': 'Europa League',
                  #'EL Group E-H': 'Europa League', 
                  #'EL Group I-L': 'Europa League',
                  #'Group A': 'UEFA Euro 2016', 
                  #'Group D-F':'UEFA Euro', 
                  #'Group G-I': 'UEFA Euro',
                  #'1 Champions League': 'EuroCups',
                  #'2 Europa League': 'EuroCups',
                  #'3 Conference League': 'EuroCups',
                  #'DFB-Pokal': 'Germany',

def utc_to_local(utc_dt):
    # get integer timestamp to avoid precision lost
    timestamp = calendar.timegm(utc_dt.timetuple())
    local_dt = datetime.datetime.fromtimestamp(timestamp)
    assert utc_dt.resolution >= datetime.timedelta(microseconds=1)
    return local_dt.replace(microsecond=utc_dt.microsecond)

class LeagueAPI(object):
    def __init__(self, league_id):
        self.league_id = league_id

    def get_matchday(self, matchday=''):
        url = URL.format(self.league_id)
        # for testing: ###########
        #displayed_games = 9
        results = []
        tree = html.parse(urlopen(url))
        #if self.league_id in [252, 276]:
        #    xpath_search_string = "//div[@class='std']"
        xpath_search_string = "//div[@class='std']" 
        for item in tree.xpath(xpath_search_string):
            match = item.xpath("./div[@class='matchPair']")
            result = item.xpath("./div[@class='matchResult']//text()")
            print(match, result)
            if match:
                match_pairing = match[0].xpath(".//text()")
                if self.league_id in [252, 276]:
                    home, guest = match[0].xpath(".//text()")[1].split(" - ")
                    home, guest = match[0].xpath(".//text()")[0].split(" - ")
                home = home.strip()
                guest = guest.strip()
                game_id = match[0].xpath("./a/@href")
                if game_id:
                    game_id = int(game_id[0].split('/')[-1])
                    game_id = 0
                print(home, guest)
                print('GAME_ID', game_id)
                #game_id = 0
                if result:
                    if len(result)>1:
                        status = 'live'
                        result = ''.join(result)
                    elif result[0] == '-:- (-:-)': 
                        status = 'vorschau'
                        result = result[0]
                        status = 'beendet'
                        result = result[0]
                    status = 'vorschau'
                    result = ''
                ### for testing ###########
                #if displayed_games > 0:
                #    status = 'live'
                #    displayed_games -= 1
                #print(home, guest, status, result, game_id, time)
                results.append({'home': home, 'result': result, 
                                'guest': guest.strip(),
                                'game_id': game_id, 'status': status, 
                                'time': time_str})
                time_str = item.xpath(".//b/text()")
                if time_str:
                    time_str = time_str[0] 
                    time_str = ' '.join([item for item in time_str.split(' ') if item])
                    time_str = time_str.replace(',','')
                    time_str = self.convert_to_datetime(time_str)
                    time_str =''
        return results

    def get_ranking(self):
        if self.league_id in [252, 276, 502, 522, 524, 526, 528]:
            league_id = self.league_id + 1
            league_id = self.league_id
        url = URL.format(league_id)
        tree = html.parse(urlopen(url))
        ranking = []
        for entry in tree.xpath("//div[@class='std']//tr")[1:]:
            row = entry.xpath("./td/text()")
            row = [item.strip() for item in row]
            if row:
                if len(row) > 5:
                    column_goal_diff = 6
                    column_goal_diff = 3
                #print(column_goal_diff, row[column_goal_diff].split(':'))
                    goals_shot, goals_get = row[column_goal_diff].split(':')
                    goal_diff = str(int(goals_shot) - int(goals_get))
                    goal_diff = ''
                if len(row) > 5:
                    rank, team, games_played, _ , _ , _, goals, points = row
                    rank, team, games_played, goals, points = row
                ranking.append({'rank': rank, 'team': team, 
                            'games_played': games_played, 'goals': goals, 
                            'goal_diff': goal_diff, 'points': points})
                ranking.append({'rank': '', 'team': '', 
                            'games_played': '', 'goals': '', 
                            'goal_diff': '', 'points': ''})
        #for row in ranking:
        return ranking

    def get_ticker(self):
        league_ticker_id = {252:272, 276:287}
        url = URL.format(league_ticker_id[self.league_id])
        tree = html.parse(urlopen(url))
        ticker_text = []
        for item in tree.xpath("//div[@class='std']"):
            for entry in item.xpath("./h3"):
                time = entry.xpath("./text()")[0].strip()
                text = entry.xpath("./following-sibling::p[1]/text()")[0].replace('\n', ' ').strip()
                ticker_text.append({'time': time, 'ticker_text': text})
        return ticker_text

    def get_match_info(self, game_id):
        matchinfo = []
        url = URL.format(game_id)
        tree = html.parse(urlopen(url))
        game = tree.xpath("//h1/text()")[0].strip()
        data = tree.xpath("//div[@class='std']")
        result = data[0].xpath('.//text()')[0]
        #print(game, result)
        for item in data[1:]:
            data1 = item.xpath('.//text()')
            yellow_cards = item.xpath('.//i/text()')
            #print(data1, yellow_cards)
            for yellow_card in yellow_cards:
                for data in data1:
                    #print(yellow_card, data)
                    if yellow_card in data:
                        data1[data1.index(data)] = data.replace(yellow_card,
                                             '<font color="#FFFF00">{}</font>'.format(yellow_card))
            data1 = ''.join([data.replace('\n\n', '<br>') for data in data1])
            data1 = data1.replace('\n','<br>')
        if self.league_id == 252:
            url = URL.format(int(game_id)+1)
            tree = html.parse(urlopen(url))
            data2 = tree.xpath("//div[@class='std']//text()")
            data2 = ''.join([data.replace('\n\n', '<br>') for data in data2])
            data1 = data1.replace('\n','<br>')
            data2 = ''
        matchinfo.append({'game': game, 'result': result, 'data1': data1, 'data2': data2})
        return matchinfo

    def convert_to_datetime(self, time_str):
        #time_str = time_str.replace('29','28')
        this_year = datetime.date.today().year
        UTC_OFFSET = 1
        if time_str.endswith("Uhr"):
            time_representation_in = "%A %d.%B %H.%M Uhr"
            time_representation_out = "%A, %d.%B %H.%M"
            time_representation_in = "%A %d.%B"
            time_representation_out = "%A, %d.%B"
        print(time_str, locale.getlocale())
            system_locale = ".".join(locale.getlocale())
        except TypeError:
            system_locale = "de_DE.utf8"
        locale.setlocale(locale.LC_TIME, "de_DE.utf8")
        # quick and dirty fix for different datetimerepresentation, TODO!
            datetime_obj = datetime.datetime.strptime(time_str, time_representation_in)
        except ValueError:
            time_representation_in = "%A %d.%B %H.%M Uhr"
            datetime_obj = datetime.datetime.strptime(time_str, time_representation_in)
        datetime_obj = datetime_obj.replace(year = this_year)
        datetime_obj = datetime_obj - datetime.timedelta(hours=UTC_OFFSET)
        locale.setlocale(locale.LC_TIME, system_locale)
        return "<b>  " + utc_to_local(datetime_obj).strftime(time_representation_out) +"</b>"

if __name__ == '__main__':
    #for league_id in LEAGUE_ID.values():
    for league_id in [252]:
        league = LeagueAPI(league_id)

just copy and paste everything and overwrite the existing code, you need root rights for it.
unfortunately i didn’t find the sourcecode on any hub pages so I post it here in hope somebody else wants to use it.


Doyou mean you just fixed it for yourself, as per the code above, or did you make a PR against some repository?

I looked at the openrepos page and the code itself, there seems to be no repository.
And the user does not seem to exist here.
I have a TMO account, if you want I can send them a PM.

As I said I didn’t find a repo that’s why I posted the code here. So I first did it for myself and then wanted to share it.
I also have an TMO account, but last time of the developer was in 2020, so I imagine he moved on.

1 Like

There is no repo…

It is open source but I have no github-repository. You can see the source code on your device under ‘/usr/share/harbour-sailgoal’. It’s just python and qml. But be warned, atm I’m not very proud of it and I think a lot things could be done better."
still valid! :wink:
SailGoal | OpenRepos.net — Community Repository System

1 Like

I think from your background picture I see why you did this…

What background picture? Where?

When you follow the link to open repos at the top you see screenshots of the app where the background shows a logo of Bayer Leverkusen, who just won the German national championship and are about to enter UEFA Champions League.
However, I just realized this is nonsense, because the screenshots are old and it’s not PatsJolla’s repo… So just ignore me please.

Ah, Leverkusen. what, 48 games unbeaten this season? It’s a bit of a fairy tale in Germany right now.

Thinking about the changes to the original python script, I wonder why the entries for Champions league and DFB Pokal should be non-functional.

If I go to ARD Text - Der Teletext im Ersten with my desktop browser and select i.e. page 553 for Championsleague, there are current results displayed (I am rather bad with Python, maybe there is more to it than missing data source).

Another thing that came to my mind in this context: wouldn’t it be relatively easy to implement an app for the complete ARD-text service? This already looks relatively pleasant and wouldn’t need much of an interface

Thank you very much. I love this app. I have one question though. On my phone, start time for Bundesliga games is always one hour behind (eg 16:30 Uhr instead of 15:30). Is this somehow related to my phone? Edit: Thanks to pmelas I just changed UTC_OFFSET = 2 and have the correct time now.

1 Like

I think the code can be improved, in dev convert_to_datetime method there is UTC_OFFSET = 1 which can be ok for CET but causes issues in later conversions for different timezones.


I tried to make the parts for CL and Cups work but the page is built differently:
Ususally the page is using div classes like “matchpair” and “matchresult” but for the tournament pages the only use a span with the class “formatted”.
I guess they change the content frequently from group stages to knock-out mode, so the code has to be quite flexible.

I tried some methods to catch it but I’m soo rusty in “programming” these days and a bit too tired that I couldn’t figure it out fast. Also I never managed to get the sdk working so I can not really debug only copy to phone and retry which is really slow…

maybe somebody else can give it a shot??

1 Like