source: lliurex-store/trunk/fuentes/python3-lliurex-store.install/usr/share/lliurexstore/plugins/appimage_catalogue.py @ 7118

Last change on this file since 7118 was 7118, checked in by Juanma, 3 years ago

WIP on bundles support

  • Property svn:executable set to *
File size: 14.8 KB
Line 
1#!/usr/bin/python3
2import urllib
3import re
4from urllib.request import Request
5from urllib.request import urlretrieve
6import shutil
7import json
8import os
9from subprocess import call
10import sys
11import threading
12from bs4 import BeautifulSoup
13import random
14import time
15import gi
16from gi.repository import Gio
17gi.require_version('AppStreamGlib', '1.0')
18from gi.repository import AppStreamGlib as appstream
19import queue
20import time
21
22class appimageToAppstream:
23
24        def __init__(self):
25                self.dbg=True
26                #To get the description of an app we must go to a specific url defined in url_info.
27                #$(appname) we'll be replaced with the appname so the url matches the right one.
28                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
29                self.repos={'appimagehub':{'type':'json','url':'https://appimage.github.io/feed.json','url_info':''}}
30                #Appimges not stored in a repo must be listed in this file, providing the download url and the info url (if there's any)
31                self.external_appimages="/usr/share/lliurex-store/files/external_appimages.json"
32                self.conf_dir=os.getenv("HOME")+"/.cache/lliurex-store"
33                self.bundles_dir=self.conf_dir+"/bundles"
34                self.queue=queue.Queue()
35        #def __init__
36
37        def _debug(self,msg=''):
38                if self.dbg:
39                        print ('DEBUG appimage: %s'%msg)
40        #def debug
41
42        def get_bundles_catalogue(self):
43                applist=[]
44                appdict={}
45                all_apps=[]
46                outdir=self.bundles_dir+'/appimg/'
47                #Load repos
48                for repo_name,repo_info in self.repos.items():
49                        if not os.path.isdir(self.bundles_dir):
50                                try:
51                                        os.makedirs(self.bundles_dir)
52                                except:
53                                        self._debug("appImage catalogue could not be fetched: Permission denied")
54                        self._debug("Fetching repo %s"%repo_info['url'])
55                        if repo_info['type']=='repo':
56                                applist=self._generate_applist(self._fetch_repo(repo_info['url']),repo_name)
57                                self._debug("Processing info...")
58                        elif repo_info['type']=='json':
59                                applist=self._process_appimage_json(self._fetch_repo(repo_info['url']),repo_name)
60
61                        self._debug("Fetched repo "+repo_info['url'])
62                        self._th_generate_xml_catalog(applist,outdir,repo_info['url_info'],repo_info['url'],repo_name)
63                        all_apps.extend(applist)
64                #Load external apps
65                for app_name,app_info in self._get_external_appimages().items():
66                        if os.path.isdir(self.bundles_dir):
67                                appinfo=self._init_appinfo()
68                                appinfo['name']=app_info['url'].split('/')[-1]
69                                appinfo['package']=app_info['url'].split('/')[-1]
70                                appinfo['homepage']='/'.join(app_info['url'].split('/')[0:-1])
71                                self._debug("Fetching external appimage %s"%app_info['url'])
72                                appinfo['bundle']='appimage'
73                                applist=[appinfo]
74                                self._th_generate_xml_catalog(applist,outdir,app_info['url_info'],app_info['url'],app_name)
75                                self._debug("Fetched appimage "+app_info['url'])
76                                all_apps.extend(applist)
77                        else:
78                                self._debug("External appImage could not be fetched: Permission denied")
79                self._debug("Removing old entries...")
80#               self._clean_bundle_catalogue(all_apps,outdir)
81        #def _download_bundles_catalogue
82
83        def _process_releases(self,applist):
84                appdict={}
85                for app in applist:
86                        version=''
87                        name_splitted=app.split('-')
88                        name=name_splitted[0]
89                        if len(name_splitted)>1:
90                                version='-'.join(name_splitted[1][-1])
91                        if name in appdict.keys():
92                                appdict[name].append(version)
93                        else:
94                                appdict[name]=[version]
95                self._debug("APPS:\n%s"%appdict)
96                return appdict
97        #def _process_releases
98
99        def _fetch_repo(self,repo):
100                req=Request(repo, headers={'User-Agent':'Mozilla/5.0'})
101                with urllib.request.urlopen(req) as f:
102                        content=(f.read().decode('utf-8'))
103               
104                return(content)
105        #def _fetch_repo
106
107        def _generate_applist(self,content,repo_name):
108                garbage_list=[]
109                applist=[]
110                #webscrapping for probono repo
111                if repo_name=='probono':
112                        garbage_list=content.split(' ')
113                        for garbage_line in garbage_list:
114                                if garbage_line.endswith('AppImage"'):
115                                        app=garbage_line.replace('href=":','')
116                                        applist.append(app.replace('"',''))
117                #Example of a webscrapping from another site
118#               if repo_name='other_repo_name':
119#                       for garbage_line in garbage_list:
120#                                       if garbage_line.startswith('file="'):
121#                                               if 'appimage' in garbage_line:
122#                                                       app=garbage_line.replace('file="','')
123#                                                       app=app.replace('\n','')
124#                                                       self._debug("Add %s"%app)
125#                                                       applist.append(app.replace('"',''))
126                        for garbage_line in garbage_list:
127                                if garbage_line.endswith('AppImage"'):
128                                        app=garbage_line.replace('href=":','')
129                                        applist.append(app.replace('"',''))
130                return(applist)
131        #def _generate_applist
132
133        def _get_external_appimages(self):
134                external_appimages={}
135                if os.path.isfile(self.external_appimages):
136                        try:
137                                with open(self.external_appimages) as appimages:
138                                        external_appimages=json.load(appimages)
139                        except:
140                                self._debug("Can't load %s"%self.external_appimages)
141                self._debug(external_appimages)
142                return external_appimages
143        #_get_external_appimages
144
145        def _process_appimage_json(self,data,repo_name):
146                maxconnections = 10
147                semaphore = threading.BoundedSemaphore(value=maxconnections)
148                applist=[]
149                json_data=json.loads(data)
150                if 'items' in json_data.keys():
151                        for appimage in json_data['items']:
152                                th=threading.Thread(target=self._th_process_appimage, args = (appimage,semaphore))
153                                th.start()
154
155                while threading.active_count()>1:
156                    time.sleep(0.1)
157
158                while not self.queue.empty():
159                    applist.append(self.queue.get())
160                self._debug("JSON: %s"%applist)
161                return (applist)
162        #_process_appimage_json
163
164        def _th_process_appimage(self,appimage,semaphore):
165                releases=[]
166                if 'links' in appimage.keys():
167                        releases=self._get_releases_from_json(appimage)
168                if releases:
169                        appinfo=self.load_json_appinfo(appimage)
170                        appinfo['releases']=releases
171#                       self.queue.put(appinfo)
172                        for release in releases:
173                                #Release has the direct download url
174                                tmp_release=release.split('/')
175                                tmp_appinfo=appinfo.copy()
176                                rel_number=tmp_release[-2]
177                                rel_name=tmp_release[-1].lower().replace('.appimage','')
178                                self._debug("Release: %s"%release)
179                                tmp_appinfo['name']=rel_name
180                                tmp_appinfo['package']=tmp_release[-1]
181                                tmp_appinfo['homepage']='/'.join(tmp_release[0:-1])
182                                self.queue.put(tmp_appinfo)
183        #def _th_process_appimage
184
185        def load_json_appinfo(self,appimage):
186                appinfo=self._init_appinfo()
187                appinfo['name']=appimage['name']
188                appinfo['package']=appimage['name']
189                if 'license' in appimage.keys():
190                        appinfo['license']=appimage['license']
191                appinfo['summary']=''
192                if 'description' in appimage.keys():
193                        appinfo['description']=appimage['description']
194                if 'categories' in appimage.keys():
195                        appinfo['categories']=appimage['categories']
196                if 'icon' in appimage.keys():
197                        appinfo['icon']=appimage['icon']
198                if 'screenshots' in appimage.keys():
199                        appinfo['thumbnails']=appimage['screenshots']
200                if 'links' in appimage.keys():
201                        if appimage['links']:
202                                for link in appimage['links']:
203                                        if 'url' in link.keys() and link['type']=='Download':
204                                                appinfo['homepage']=link['url']
205                elif 'authors' in appimage.keys():
206                        if appimage['authors']:
207                                for author in appimage['authors']:
208                                        if 'url' in author.keys():
209                                                appinfo['homepage']=author['url']
210                appinfo['bundle']='appimage'
211                return appinfo
212        #def load_json_appinfo
213
214        def _get_releases_from_json(self,appimage):
215                releases=[]
216                if appimage['links']:
217                        for link in appimage['links']:
218                                if 'type' in link.keys():
219                                        if link['type']=='Download':
220                                                self._debug("Info url: %s"%link['url'])
221                                                try:
222                                                        sw_git=False
223                                                        with urllib.request.urlopen(link['url']) as f:
224                                                                if 'github' in link['url']:
225                                                                        sw_git=True
226                                                                content=(f.read().decode('utf-8'))
227                                                                soup=BeautifulSoup(content,"html.parser")
228                                                                package_a=soup.findAll('a', attrs={ "href" : re.compile(r'.*[aA]pp[iI]mage$')})
229                                                                for package_data in package_a:
230                                                                        package_name=package_data.findAll('strong', attrs={ "class" : "pl-1"})
231                                                                        package_link=package_data['href']
232                                                                        if sw_git:
233                                                                                package_link="https://github.com"+package_link
234                                                                                releases.append(package_link)
235                                                                                self._debug("Link: %s"%package_link)
236                                                except Exception as e:
237                                                        print(e)
238                return releases
239        #def _get_releases_from_json
240
241        def _th_generate_xml_catalog(self,applist,outdir,info_url,repo,repo_name):
242                maxconnections = 10
243                semaphore = threading.BoundedSemaphore(value=maxconnections)
244                random_applist = list(applist)
245                random.shuffle(random_applist)
246                for app in applist:
247                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,info_url,repo,repo_name,semaphore))
248                        th.start()
249        #def _th_generate_xml_catalog
250
251        def _th_write_xml(self,appinfo,outdir,info_url,repo,repo_name,semaphore):
252                semaphore.acquire()
253                lock=threading.Lock()
254                self._debug("Populating %s"%appinfo)
255                package=appinfo['name'].lower().replace(".appimage","")
256                if len(package)>40:
257                        tmp_package=package.split('-')
258                        tam=0
259                        pos=1
260                        index=0
261                        banned=['linux32','linux64','i386','x86_64','ia32','amd64']
262                        for tmp_name in tmp_package:
263                                if (tam<len(tmp_name) and pos<index) and tmp_name not in banned:
264                                        tam=len(tmp_name)
265                                        pos=index
266                                index+=1
267                        print("Removed: %s"%tmp_package[pos])
268                        tmp_package[pos]=''
269                        package='-'.join(tmp_package)
270                        package=package.replace("--","-")
271                        package=package.replace("-.",".")
272
273                filename=outdir+package.lower().replace('appimage',"appdata")+".xml"
274                self._debug("checking if we need to download "+filename)
275                if not os.path.isfile(filename):
276                        repo_info={'info_url':info_url,'repo':repo,repo_name:'repo_name'}
277                        self._write_xml_file(filename,appinfo,repo_info,lock)
278                semaphore.release()
279        #def _th_write_xml
280
281        def _write_xml_file(self,filename,appinfo,repo_info,lock):
282                        name=appinfo['name'].lower().replace(".appimage","")
283                        self._debug("Generating %s xml"%appinfo['package'])
284                        f=open(filename,'w')
285                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
286                        f.write("<components version=\"0.10\">\n")
287                        f.write("<component  type=\"desktop-application\">\n")
288                        f.write("  <id>%s</id>\n"%appinfo['package'].lower())
289                        f.write("  <pkgname>%s</pkgname>\n"%appinfo['package'])
290                        f.write("  <name>%s</name>\n"%name)
291                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
292                        f.write("  <provides><binary>%s</binary></provides>\n"%appinfo['package'])
293                        if 'releases' in appinfo.keys():
294                                f.write("  <releases>\n")
295                                for release in appinfo['releases']:
296                                        tmp_release=release.split('/')
297                                        rel_number='/'.join(tmp_release[-2:])
298                                        f.write("    <release version=\"%s\" urgency=\"medium\">"%rel_number)
299                                        f.write("</release>\n")
300                                f.write("  </releases>\n")
301                        f.write("  <launchable type=\"desktop-id\">%s.desktop</launchable>\n"%name)
302                        if appinfo['description']=='':
303                                with lock:
304                                        try:
305                                                if appinfo['name'] in self.descriptions_dict.keys():
306                                                        (description,icon)=self.descriptions_dict[appinfo['name']]
307                                                else:
308                                                        (description,icon)=self._get_description_icon(appinfo['name'],repo_info)
309                                                        self.descriptions_dict.update({appinfo['name']:[description,icon]})
310                                        except:
311                                                description=''
312                                                icon=''
313                        else:
314                                description=appinfo['description']
315                        summary=' '.join(list(description.split(' ')[:8]))
316                        if len(description.split(' '))>8:
317                                summary+="... "
318                        description="This is an AppImage bundle of app %s. It hasn't been tested by our developers and comes from a 3rd party dev team. Please use it carefully.\n%s"%(name,description)
319                        if summary=='':
320                                summary=' '.join(list(description.split(' ')[:8]))
321                        f.write("  <description><p></p><p>%s</p></description>\n"%description)
322                        f.write("  <summary>%s</summary>\n"%summary)
323#                       f.write("  <icon type=\"local\">%s</icon>\n"%appinfo['icon'])
324                        f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
325                        f.write("  <url type=\"homepage\">%s</url>\n"%appinfo['homepage'])
326                        f.write("  <bundle type=\"appimage\">%s</bundle>\n"%appinfo['name'])
327                        f.write("  <keywords>\n")
328                        keywords=name.split("-")
329                        banned_keywords=["linux","x86_64","i386","ia32","amd64"]
330                        for keyword in keywords:
331                                #Better than isalpha method for this purpose
332                                if keyword.isidentifier() and keyword not in banned_keywords:
333                                        f.write("       <keyword>%s</keyword>\n"%keyword)
334                        f.write("       <keyword>appimage</keyword>\n")
335                        f.write("  </keywords>\n")
336                        f.write("  <categories>\n")
337                        f.write("       <category>AppImage</category>\n")
338                        if 'categories' in appinfo.keys():
339                                for category in appinfo['categories']:
340                                        f.write("       <category>%s</category>\n"%category)
341                        f.write("  </categories>\n")
342                        f.write("</component>\n")
343                        f.write("</components>\n")
344                        f.close()
345        #def _write_xml_file
346
347        def _get_description_icon(self,app_name,repo_info):
348                desc=''
349                icon=''
350                if repo_info['info_url']:
351                        if '$(appname)' in repo_info['info_url']:
352                                info_url=info_url.replace('$(appname)',app_name)
353                        self._debug("Getting description from repo/app %s - %s "%(repo_info['repo_name'],repo_info['info_url']))
354                        try:
355                                with urllib.request.urlopen(info_url) as f:
356                                        #Scrap target info page
357                                        if repo_name=='probono':
358                                                content=(f.read().decode('utf-8'))
359                                                soup=BeautifulSoup(content,"html.parser")
360                                                description_div=soup.findAll('div', attrs={ "class" : "description-text"})
361                                                icon_div=soup.findAll('div', attrs={ "class" : "avatar-icon avatar-large description-icon "})
362                                if len(description_div)>0:
363                                        desc=description_div[0].text
364                                        desc=desc.replace(':','.')
365                                        desc=desc.replace('&','&amp;')
366                                if len(icon_div)>0:
367                                        icon_str=str(icon_div[0])
368                                        icon=icon_str.split(' ')[9]
369                                        icon=icon.lstrip('url(')
370                                        if icon.startswith('http'):
371                                                icon=icon.rstrip(');"></div>')
372                                                icon=self._download_file(icon,app_name)
373                        except Exception as e:
374                                print("Can't get description from "+repo_info['info_url'])
375                                print(str(e))
376                                pass
377                return([desc,icon])
378        #def _get_description
379
380        def _download_file(self,url,app_name='app',target_file=''):
381                if target_file=='':
382                        target_file=self.icons_dir+'/'+app_name+".png"
383                if not os.path.isfile(target_file):
384                        if not os.path.isfile(target_file):
385                                self._debug("Downloading %s to %s"%(url,target_file))
386                                try:
387                                        with urllib.request.urlopen(url) as response, open(target_file, 'wb') as out_file:
388                                                bf=16*1024
389                                                acumbf=0
390                                                file_size=int(response.info()['Content-Length'])
391                                                while True:
392                                                        if acumbf>=file_size:
393                                                            break
394                                                        shutil.copyfileobj(response, out_file,bf)
395                                                        acumbf=acumbf+bf
396                                        st = os.stat(target_file)
397                                except Exception as e:
398                                        self._debug("Unable to download %s"%url)
399                                        self._debug("Reason: %s"%e)
400                                        target_file=url
401                return(target_file)
402        #def _download_file
403
404        def _init_appinfo(self):
405                appInfo={'appstream_id':'',\
406                'id':'',\
407                'name':'',\
408                'version':'',\
409                'releases':[],\
410                'package':'',\
411                'license':'',\
412                'summary':'',\
413                'description':'',\
414                'categories':[],\
415                'icon':'',\
416                'screenshot':'',\
417                'thumbnails':[],\
418                'video':'',\
419                'homepage':'',\
420                'installerUrl':'',\
421                'state':'',\
422                'depends':'',\
423                'kudos':'',\
424                'suggests':'',\
425                'extraInfo':'',\
426                'size':'',\
427                'bundle':'',\
428                'updatable':'',\
429                }
430                return(appInfo)
431
432
433catalogue=appimageToAppstream()
434catalogue.get_bundles_catalogue()
Note: See TracBrowser for help on using the repository browser.