User:Estemi/History Of WineAppDB Generator

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

I created and maintain a script-generated file, History Of WineAppDB.gif. It takes a long time and is very cumbersome. Given that I could lose interest in the file or die horribly, I offer the script here.

Run it with an internet connection in a directory with a font file 'font.ttf' and it should work perfectly. Should.

#!/usr/bin/env python
# WineStats 0.5.1 -- Graphs WineAppDB test results from .9 to the present
# TODO: more graphing techniques; less like spaghetti
 
"""
It takes forever to do the whole thing at once. You might want to do the
first section and the rest seperately. Run twice with the --nograph and the
--nodownload flags each to accomplish this. If your download fails, use the
--resume <int> flag with the appID where the script broke off.

If Imagemagick is installed on your computer, the script will compose an
animated gif automatically. Otherwise, used GIMP to open the imgs folder
as layers and save as an animated gif.
 
See http://commons.wikimedia.org/wiki/File:History_Of_WineAppDB.gif for more
information, or its uploader's commons page if you have questions.
"""
 
#-----------------------------------------------------------------------------
# Assemble resources
#-----------------------------------------------------------------------------
 
# Modules
import os
import sys
import glob
import Image
import ImageDraw
import ImageFont
import time
import urllib
 
# Global variables
appdb = 'http://appdb.winehq.org/objectManager.php?'
app_url = appdb + 'sClass=application&iId='
show_all_macro = 'bShowAll=true&bIsQueue=false&bIsRejected=false&'
last_app_macro = 'sClass=application&iPage=1&sOrderBy=appId&bAscending=false'
download_data = draw_graph = True
start_time = end_time = time.ctime()
start_app = end_app = 0
apps_dir = 'apps'
imgs_dir = 'imgs'
font_file = 'font.ttf'
resume = False
make_gif = True
failed = []
 
# Command line arguments
args = sys.argv
args.remove(__file__)
 
if '-h' in args or '--help' in args:
    print('Usage: python ' + __file__ + ' [ options ... ]')
    print('Collects data from Wine\'s AppDB (http://appdb.winehq.org/) and '
            + 'graphs them.')
    print('\nOptions:')
    print('\t--nodownload\t\tDo not download new data.')
    print('\t--nograph\t\tDo not draw graphs after download.')
    print('\t--resume <int>\t\tStart download at certain app (default: 0).')
    print('\t--stopat <int>\t\tStop download at certain app (default: last).')
    print('\t-h or --help\t\tPrint this message.')
    sys.exit()
 
if '--resume' in args:
    arg_index = args.index('--resume')
    int_index = arg_index + 1
    resume = True
    try:
        start_app = int(args[int_index])
        del args[int_index]
        del args[arg_index]
    except:
        print(__file__ + ': use --resume <int> where <int> is an application '
                + 'id in AppDB.')
        print('Try python ' + __file__ + ' --help for more information.')
        sys.exit()
 
if '--stopat' in args:
    arg_index = args.index('--stopat')
    int_index = arg_index + 1
    try:
        end_app = int(args[int_index])
        del args[int_index]
        del args[arg_index]
    except:
        print(__file__ + ': use --stopat <int> where <int> is an '
                + 'application id in AppDB.')
        print('Try python ' + __file__ + ' --help for more information.')
        sys.exit()
 
if '--nodownload' in args:
    arg_index = args.index('--nodownload')
    del args[arg_index]
    download_data = False
 
if '--nograph' in args:
    arg_index = args.index('--nograph')
    del args[arg_index]
    draw_graph = False
 
if len(args) > 0:
    print(__file__ + ': Unkown or malformatted option ' + args[0])
    print('Try python ' + __file__ + ' --help for more information.')
    sys.exit()
 
#-----------------------------------------------------------------------------
# Assert that the script is running in a valid environment
#-----------------------------------------------------------------------------
 
if draw_graph:
    assert os.path.isfile(font_file), 'Must have %s in directory.' % font_file
    assert not os.path.exists(imgs_dir), '%s directory exists.' % imgs_dir
if download_data:
    if not resume:
        assert not os.path.exists(apps_dir), '%s directory exists.' % apps_dir
    assert end_app == 0 or end_app > start_app, 'No app data to fetch.'
assert download_data or draw_graph, 'Neither getting data or rendering it.'

# Test if imagemagic available for GIF animation
try:
    if draw_graph and os.system('if which convert > /dev/null; then return 1; fi') == 0:
        raise Exception
except:
    print 'Imagemagick not available. Will not compile animated GIF file.'
    make_gif = False
    
 
#-----------------------------------------------------------------------------
# Fetch app data from server, consolidate, and save in apps directory
#-----------------------------------------------------------------------------
 
def analyse_result(result):
    tags = result.split('><')
    del tags[:7] #first seven are trash
    if len(tags) < 5:
        return ['null', 'null', 'null']
    date = tags[0].replace('td>', '').replace('</td', '')
    version = tags[1].replace('td>', '').replace('.&nbsp;</td',
            '').replace('&nbsp;</td', '')
    rating = tags[4].replace('td>', '').replace('&nbsp;</td', '')
    return [date, version, rating]
 
def get_test_results(url):
    html = get_html(url)
 
    for line in html:
        if line.find('<table class="historyTable"') > -1:
            break
 
    if line.find('</thead><tbody>') > -1:
        data = line.split('</thead><tbody>')[1]
    elif line.find('</thead>') > -1:
        data = line.split('</thead>')[1]
    else:
        return None
 
    data = data.split('</tr>')
    del data[len(data)-1] #useless
 
    testResults = []
    for datum in data:
        testResults.append(analyse_result(datum))
    return testResults
 
def get_versions(html):
    for line in html:
        if line.find('Class=version') > -1:
            break
 
    parts = line.split('onclick="DoNav(')
    del parts[0]
    urls = []
    for version in range(len(parts)):
        urlEnd = parts[version].find(')')
        url = parts[version][0:urlEnd].replace("'", "").replace('amp;',
                '').replace('objectManager.php?', 'objectManager.php?'
                + show_all_macro)
        urls.append(url)
    return urls
 
def make_apps(startApp, endApp):
    if not resume:
        os.mkdir(apps_dir)
 
    print('This is gonna take awhile. Go out for a movie or something.')
    for appID in range(startApp, endApp):
        if appID % 5 == 0:
            print('%s out of %s data retrieved' % (str(appID), str(endApp)))
        try:
            make_app(appID)
        except:
            print('Error loading application ' + str(appID))
            failed.append(appID)
    print('Download and write complete.')
    end_time = time.ctime()
    if len(failed) > 0:
        print('The following applications failed:')
        for appID in failed:
            print(appID)
 
def make_app(appID):
    html = get_html(app_url, appID)
    name = get_name(html)
    if is_valid_app(name):
        versionURLs = get_versions(html)
        versionsData = get_version_data(versionURLs)
        make_app_file(name, versionsData)
 
 
def make_app_file(name, versionsData):
    savePath = os.path.join(apps_dir, name+'.txt')
    file = open(savePath, 'w')
    file.write(name + '\n\n')
    for version in versionsData:
        file.write('version[\n')
        write_version(version, file)
        file.write(']\n\n')
    file.close()
 
def write_version(version, file):
    for testResult in version:
        file.write(testResult[0] + ',' + testResult[1] + ','
                    + testResult[2] + '\n')
 
def get_version_data(versionURLs):
    data = []
    for url in versionURLs:
        testResults = get_test_results(url)
        if testResults != None:
            data.append(testResults)
    return data
 
def get_html(url, tailID=None):
    if tailID is not None:
        url = url + str(tailID+1)
    return urllib.urlopen(url)
 
def get_name(html):
    for line in html:
        if line.find('<title>') > -1:
            title = line.replace('<title>WineHQ', '').replace('</title>\n',
                    '').replace('-', '').replace('/', '').strip()
            return title
 
def is_valid_app(name):
    if name == '':
        return False
    return True
 
def get_last_app():
    url = appdb + last_app_macro
    html = get_html(url)
    for line in html:
        if line.find('<td>Description</td></tr>') > -1:
            appid = line.split('<td>Description</td></tr>')[1].split(
                    '</td>')[1].replace('<td>', '')
            return int(appid)
 
if download_data:
    if end_app == 0: end_app = get_last_app()
    make_apps(start_app, end_app)
else:
    print('Skipping download of application data....')
 
#-----------------------------------------------------------------------------
# Create an object hierachy with the data
#-----------------------------------------------------------------------------
 
class AppDB():
    def __init__(self):
        self.apps = []
        for infile in glob.glob(os.path.join(apps_dir, '*.txt')):
            app = App(infile)
            self.apps.append(app)
        self.wineVersions = self.get_wine_versions()
 
    def get_wine_versions(self):
        wineVersions = []
        for app in self.apps:
            for wineVersion in app.wineVersions:
                if wineVersion not in wineVersions:
                    wineVersions.append(wineVersion)
        wineVersions.sort()
        return wineVersions
 
    def create_visual(self):
        os.mkdir(imgs_dir)
 
        self.logPath = self.make_report_log()
        visuals = []
        for version in self.wineVersions:
            garbage = 0
            bronze = 0
            silver = 0
            gold = 0
            platinum = 0
            for app in self.apps:
                quality = app.get_quality_at(version, self.wineVersions)
                if quality == "Garbage": garbage+=1
                elif quality == "Bronze": bronze+=1
                elif quality == "Silver": silver+=1
                elif quality == "Gold": gold+=1
                elif quality == "Platinum": platinum+=1
 
            filename = os.path.join(imgs_dir, version + '.png')
            create_visual(garbage, bronze, silver, gold, platinum, filename)
            visuals.append(filename)
            self.print_data(version, str(garbage), str(bronze), str(silver),
                            str(gold), str(platinum))

        if make_gif:
            visuals_string = ' '.join(visuals)
            command = ('convert -delay 10 -loop 0 -dispose Background ' +
                       visuals_string + ' wine_animation.gif')
            os.system(command)
 
    def make_report_log(self):
        logPath = 'appdb_retrieve_log.txt'
        log = open(logPath, 'w')
        if start_time == end_time:
            log.write('Data retrieval time unknown. Graphs drawn at %s\n\n'
                        % start_time)
        else:
            log.write('Data retrieved from %s to %s\n\n'
                        % (start_time, end_time))
        log.close()
        return logPath
 
    def print_data(self, ver, gar, bro, sil, gol, pla):
        verString = '%s -- Pl:%s Go:%s Si:%s Br:%s Ga:%s' \
                    % (ver, pla, gol, sil, bro, gar)
        print verString
        log = open(self.logPath, 'a')
        log.write(verString + '\n')
        log.close()
 
 
class App():
    def __init__(self, appFile):
        self.name = os.path.split(appFile)[1].replace('.txt', '')
        self.versions = self.make_versions(appFile)
        self.wineVersions = self.get_wine_versions()
 
    def make_versions(self, filename):
        file = open(filename, 'r')
        data = file.read()
        file.seek(0)
 
        versions = []
        numVersions = data.count('version[\n')
        versionsRead = 0
 
        while versionsRead < numVersions:
            line = file.readline()
            if line == 'version[\n':
                versionResults = []
                while line != ']\n':
                    line = file.readline()
                    results = line.replace('\n', '').split(',')
                    if len(results) == 3:
                        versionResults.append(results)
                versions.append(Version(versionResults))
                versionsRead+=1
        file.close()
        return versions
 
    def get_wine_versions(self):
        wineVersions = []
        for version in self.versions:
            for wineVersion in version.wineVersions:
                if wineVersion not in wineVersions:
                    wineVersions.append(wineVersion)
        return wineVersions
 
    def get_quality_at(self, wineVersion, allVersions):
        reports = []
        for version in self.versions:
            quality = version.get_quality_at(wineVersion, allVersions)
            if quality is not None:
                reports.append(quality)
        if len(reports) > 0:
            score = self.average_qualities(reports)
            return score
 
    def average_qualities(self, qualities):
        score = 0.0
        for quality in qualities:
            if quality == 'Garbage':  score+=0
            elif quality == 'Bronze': score+=1
            elif quality == 'Silver': score+=2
            elif quality == 'Gold': score+=3
            elif quality == 'Platinum': score+=4
        average = score / len(qualities)
 
        if average < 0.5:
            return "Garbage"
        elif 0.5 <= average < 1.5:
            return "Bronze"
        elif 1.5 <= average < 2.5:
            return "Silver"
        elif 2.5 <= average < 3.5:
            return "Gold"
        elif average >= 3.5:
            return "Platinum"
 
class Version():
    def __init__(self, versionResults):
        self.results = self.make_reports(versionResults)
        self.wineVersions = self.get_wine_versions()
 
    def make_reports(self, results):
        testResults = []
        for result in results:
            #fix wine for sorting
            subvers = result[1].split('.')
            if len(subvers) > 2:
                if int(subvers[2]) < 10:
                    subvers[2] = '0' + subvers[2]
                    result[1] = '%s.%s.%s' % tuple(subvers)
            else:
                # and remove non-release builds (eg 2384594) from test results
                if len(subvers) < 2:
                    continue
 
            testResults.append(Result(result[0], result[1], result[2]))
        return testResults
 
    def get_wine_versions(self):
        wineVersions = []
        for report in self.results:
            wineVersion = report.wine
            if wineVersion not in wineVersions:
                wineVersions.append(wineVersion)
        return wineVersions
 
    def get_quality_at(self, wineVersion, allVersions):
        searchIndex = allVersions.index(wineVersion)
        for report in self.results:
            reportIndex = allVersions.index(report.wine)
            if reportIndex <= searchIndex and searchIndex - reportIndex < 16:
                return report.score
 
class Result():
    def __init__(self, date, wine, score):
        self.date = date
        self.wine = wine
        self.score = score
 
appdb = AppDB()
 
#-----------------------------------------------------------------------------
# Create an image with data from object hierarchy
#-----------------------------------------------------------------------------
 
def create_visual(garbageApps, bronzeApps, silverApps, goldApps, platinumApps, saveFile):
    imHeight = 500
    imWidth = 750
    garbageColor = '#999966'
    bronzeColor = '#fcba0a'
    silverColor = '#c0c0c0'
    goldColor = '#fff600'
    platinumColor = '#ececec'
    myfont = ImageFont.truetype(font_file, 50)
 
    im = Image.new('RGB', (imWidth, imHeight), color='#ffffff')
    rectHeight = 75
    draw = ImageDraw.Draw(im)
    text = os.path.split(saveFile)[1].replace('.png', '')
    draw.text([25,20], text, fill='#000000', font=myfont)
 
    garbagex1 = imWidth/2 + garbageApps/3
    garbagey1 = imHeight
    garbagex2 = imWidth/2 - garbageApps/3
    garbagey2 = imHeight-rectHeight
    draw.rectangle([garbagex1, garbagey1, garbagex2, garbagey2],
                    fill=garbageColor, outline='#000000')
 
    bronzex1 = imWidth/2 + bronzeApps/3
    bronzey1 = imHeight - rectHeight
    bronzex2 = imWidth/2 - bronzeApps/3
    bronzey2 = imHeight - rectHeight*2
    draw.rectangle([bronzex1, bronzey1, bronzex2, bronzey2],
                    fill=bronzeColor, outline='#000000')
 
    silverx1 = imWidth/2 + silverApps/3
    silvery1 = imHeight - rectHeight*2
    silverx2 = imWidth/2 - silverApps/3
    silvery2 = imHeight - rectHeight*3
    draw.rectangle([silverx1, silvery1, silverx2, silvery2],
                    fill=silverColor, outline='#000000')
 
    goldx1 = imWidth/2 + goldApps/3
    goldy1 = imHeight - rectHeight*3
    goldx2 = imWidth/2 - goldApps/3
    goldy2 = imHeight - rectHeight*4
    draw.rectangle([goldx1, goldy1, goldx2, goldy2],
                    fill=goldColor, outline='#000000')
 
    platinumx1 = imWidth/2 + platinumApps/3
    platinumy1 = imHeight - rectHeight*4
    platinumx2 = imWidth/2 - platinumApps/3
    platinumy2 = imHeight - rectHeight*5
    draw.rectangle([platinumx1, platinumy1, platinumx2, platinumy2],
                    fill=platinumColor, outline='#000000')
 
    del draw
    im.save(saveFile)

 
appdb.create_visual()