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

Last change on this file since 7115 was 7115, checked in by Juanma, 2 years ago

appimage json

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