GHP publish

This commit is contained in:
ace
2021-01-09 21:06:20 +03:00
commit 380dbf855f
68 changed files with 10903 additions and 0 deletions

15
playmaker/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.pyc
**/__pycache__/
*.swp
*.apk
.tern-project
.ycm_extra_conf.py
dist/
build/
node_modules/
.ropeproject/
eggs/
.eggs/
*.egg-info/
*.egg

67
playmaker/Dockerfile Normal file
View File

@ -0,0 +1,67 @@
FROM python:3-buster
RUN apt-get update && \
apt-get install -y git \
lib32stdc++6 \
lib32gcc1 \
lib32z1 \
lib32ncurses6 \
libffi-dev \
libssl-dev \
libjpeg-dev \
libxml2-dev \
libxslt1-dev \
openjdk-11-jdk-headless \
virtualenv \
wget \
unzip \
zlib1g-dev \
less \
mc \
nano
RUN mkdir -p /data/fdroid/repo && \
mkdir -p /opt/playmaker
RUN wget https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip \
&& echo "F10F9D5BCA53CC27E2D210BE2CBC7C0F1EE906AD9B868748D74D62E10F2C8275 commandlinetools-linux-6200805_latest.zip" | sha256sum -c \
&& unzip commandlinetools-linux-6200805_latest.zip \
&& rm commandlinetools-linux-6200805_latest.zip
RUN mkdir /opt/android-sdk-linux \
&& mv tools /opt/android-sdk-linux/tools
ENV ANDROID_HOME=/opt/android-sdk-linux
ENV PATH=$PATH:$ANDROID_HOME/tools
RUN echo 'y' | /opt/android-sdk-linux/tools/bin/sdkmanager --sdk_root=/opt/android-sdk-linux --verbose --install "platforms;android-28" "build-tools;28.0.3"
RUN echo 'y' | rm -rf tools
COPY README.md setup.py pm-server /opt/playmaker/
COPY playmaker /opt/playmaker/playmaker
WORKDIR /opt/playmaker
RUN pip3 install fdroidserver
RUN pip3 install .
RUN rm -rf /opt/playmaker
RUN groupadd -g 999 pmuser && \
useradd -m -u 999 -g pmuser pmuser
RUN chown -R pmuser:pmuser /data/fdroid && \
chown -R pmuser:pmuser /opt/playmaker
RUN mkdir -p /usr/local/share/doc/fdroidserver/examples && \
cp -r /usr/local/lib/python3.9/site-packages/usr/local/share/doc/fdroidserver/examples/* /usr/local/share/doc/fdroidserver/examples
USER pmuser
VOLUME /data/fdroid
WORKDIR /data/fdroid
EXPOSE 5000
ENTRYPOINT python3 -u /usr/local/bin/pm-server --fdroid --debug

13
playmaker/Makefile Normal file
View File

@ -0,0 +1,13 @@
VERSION=0.6.4
IMAGE=playmaker
REGISTRY=registry.0xace.cc
.PHONY: build push all
build:
docker build -t $(REGISTRY)/$(IMAGE):$(VERSION) .
push:
docker push $(REGISTRY)/$(IMAGE):$(VERSION)
all: build push

93
playmaker/README.md Normal file
View File

@ -0,0 +1,93 @@
# Playmaker
![screenshot](https://github.com/NoMore201/playmaker/raw/master/example.png)
## Description & Features
Playmaker is a fdroid repository manager, which lets you download/update apps from the play store using your google account
and configure repository with app you download. After you setup the server, repository will be available at the address
`http[s]://<playmaker_host>/fdroid`, and you can start downloading apps from play store.
Server uses [googleplay-api](https://github.com/NoMore201/googleplay-api) library, which is the python equivalent of the Java [play-store-api](https://github.com/yeriomin/play-store-api) library used by YalpStore.
Features:
* Download apks from google play store to your collection
* Generate a fdroid repository serving apks downloaded, directly from `<pm_url>/fdroid`
* Configure automatic updates of app+repo through a Crontab string
* Non-blocking UI, you can browse the collection or search for an app while the server is updating the fdroid
repository.
* Responsive UI, usable also from a mobile device
## Configuration
### Authentication
To avoid authentication problems, like captcha requests, it's recommended to setup app specific password, and securing your account with 2-factor auth. There are two ways to login to Play Store:
- Providing credentials in a configuration file
- Through a login page.
The default behaviour is to ask credentials with a login page, when accessing playmaker on first launch. In order to skip login page, it is possible to provide google credentials through a configuration file. Just put `credentials.conf` inside the playmaker directory, with this structure:
```
[google]
email = myemail@gmail.com
password = mypassword
```
To restrict access to that file, ensure it is readable only by user running playmaker.
### HTTPS
It's recommended to configure playmaker with HTTPS, especially with the login page authentication, since playmaker needs to send to the server credentials in plaintext. You can setup it in conjunction with a proxy like nginx, or provide certificate directly to playmaker.
## Running
Since this app requires a lot of heavy dependencies, like Android SDK and fdroidserver, it is recommended to use the docker image.
You can use a pre-built image on [docker hub](https://hub.docker.com/r/nomore201/playmaker/builds/) or build by yourself using provided `Dockerfile`.
There are some environment variables you'll want to use:
- `HTTPS_CERTFILE`: path of the https certificate file
- `HTTPS_KEYFILE`: path of the https key file
- `LANG_LOCALE`: set a specific locale. Defaults to the system one if not set
- `LANG_TIMEZONE`: set a specific timezone. Defaults to `Europe/Berlin` if not set
- `CRONTAB_STRING`: crontab string to configure automatic updates. Defaults to every night at 2AM (`0 2 * * *`)
- `DEVICE_CODE`: specify a device to be used by playmaker, defaults to `bacon` (OnePlus One) if not specified. For
a list of supported devices see [this file](https://raw.githubusercontent.com/NoMore201/googleplay-api/master/gpapi/device.properties)
To enable HTTPS through playmaker, without an external tool, just define `HTTPS_CERTFILE`
and `HTTPS_KEYFILE` with paths to those file. If these variables are not set, tornado will default to http.
If you want to browse apps for a specific country, you need to specify the variables `LANG_LOCALE` and `LANG_TIMEZONE`.
Before creating an issue "cannot find app X", make sure the app is available it that country.
The docker run command will look like this:
```
docker run -d --name playmaker \
-p 5000:5000 \
-v /srv/fdroid:/data/fdroid \
-e HTTPS_CERTFILE="/srv/https.crt" \
-e HTTPS_KEYFILE="/srv/https.key" \
-e LANG_LOCALE="de_DE" \
-e LANG_TIMEZONE="Europe/Berlin" \
-e DEVICE_CODE="hammerhead" \
fellek/playmaker:fellek
```
If you want to run it in a virtualenv rather than using docker, remember that you need to install fdroidserver,
android SDK and define the ANDROID\_HOME env variable (see the Dockerfile as a reference).
Instruction on how to install fdroidserver [here](https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools/)
## Alternatives
### YalpStore
[YalpStore](https://github.com/yeriomin/YalpStore) is an open source alternative to the play store.
It works very well and it requires you to install only the app, but it requires one of the
following thing to be able to install/update apks:
- enable **Unknown Sources**
- have **root** privileges
If you use playmaker and the fdroid [privileged extension](https://gitlab.com/fdroid/privileged-extension),
fdroid will be able to install/update app without root privileges or enabling unknown sources.

BIN
playmaker/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en" ng-app="playmaker">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Playmaker</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css"
integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU"
crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.5/angular.min.js"></script>
<link href="/static/app.css" rel="stylesheet">
</head>
<body>
<div ng-controller="navbar" class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target=".navbar-responsive-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button> <a class="navbar-brand">Playmaker</a>
</div>
<div class="navbar-collapse collapse navbar-responsive-collapse">
<ul class="nav navbar-nav">
<li ng-class="{active: path === '/'}">
<a href="/#!/" id="app-page-link">
<i class="fab fa-android" aria-hidden="true"></i> Apps
</a>
</li>
<li ng-class="{active: path === '/search'}">
<a href="/#!/search" id="search-page-link">
<i class="fas fa-search" aria-hidden="true"></i> Search
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a href=
"https://github.com/NoMore201/playmaker/releases/tag/v0.6.4">v0.6.4</a>
</li>
<li>
<a href="https://github.com/NoMore201/playmaker">
<i class="fab fa-github" aria-hidden="true"></i> Source
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="notification-panel" ng-controller="notify">
<script type="text/ng-template" id="alert.html">
<div ng-transclude></div>
</script>
<div uib-alert="" ng-repeat="alert in alerts" ng-class=
"'alert-' + (alert.type || 'warning')" close="closeAlert($index)"
dismiss-on-timeout="3000">
{{alert.msg}}
</div>
</div>
<div ng-view=""></div>
<!-- angular libraries -->
<script src="/static/js/angular-route.min.js"></script>
<script src="/static/js/angular-animate.min.js"></script>
<script src="/static/js/angular-touch.min.js"></script>
<script src="/static/js/ui-bootstrap.min.js"></script>
<script src="/static/js/additional.js"></script>
<!-- crypto library -->
<script src="/static/js/crypto-js.js"></script>
<script src="/static/app.module.js"></script>
<script src="/static/app.service.js"></script>
<script src="/static/app.controller.js"></script>
</body>
</html>

View File

@ -0,0 +1,151 @@
import os
import tornado
from tornado import web
from tornado.concurrent import run_on_executor
from tornado.web import MissingArgumentError
from concurrent.futures import ThreadPoolExecutor
app_dir = os.path.dirname(os.path.realpath(__file__))
static_dir = os.path.join(app_dir, 'static')
fdroid_instance = {}
def createServer(service):
class HomeHandler(web.RequestHandler):
def get(self):
with open(app_dir + '/index.html', 'r') as f:
self.write(f.read())
class ApiHandler(web.RequestHandler):
executor = ThreadPoolExecutor()
@run_on_executor
def get_apps(self):
return service.get_apps()
@run_on_executor
def get_last_fdroid_update(self):
return service.get_last_fdroid_update()
@run_on_executor
def search(self):
try:
keyword = self.get_argument('search')
except MissingArgumentError:
return None
return service.search(keyword)
@run_on_executor
def login(self):
data = tornado.escape.json_decode(self.request.body)
service.set_encoded_credentials(data.get('email'), data.get('password'))
return service.login()
@run_on_executor
def download(self):
data = tornado.escape.json_decode(self.request.body)
if data.get('download') is None:
return None
return service.download_selection(data['download'])
@run_on_executor
def check(self):
return service.check_local_apks()
@run_on_executor
def update_state(self):
service.update_state()
@run_on_executor
def remove_app(self, app):
return service.remove_local_app(app)
@run_on_executor
def update_fdroid(self):
return service.fdroid_update()
@tornado.gen.coroutine
def get(self, path):
if path == 'apps':
apps = yield self.get_apps()
self.write(apps)
elif path == 'search':
apps = yield self.search()
if apps is not None:
self.write(apps)
else:
self.clear()
self.set_status(400, 'You should supply a valid search query')
elif path == 'fdroid':
result = yield self.get_last_fdroid_update()
self.write(result)
else:
self.set_status(404)
self.finish()
@tornado.gen.coroutine
def post(self, path):
if path == 'download':
result = yield self.download()
if result is None:
self.clear()
self.set_status(400)
else:
self.write(result)
elif path == 'check':
result = yield self.check()
self.write(result)
elif path == 'login':
result = yield self.login()
self.write(result)
if result['status'] == 'SUCCESS' and result['message'] == 'OK':
self.update_state()
elif path == 'fdroid':
global fdroid_instance
if fdroid_instance != {}:
self.write({'status': 'PENDING'})
else:
fdroid_instance = self
result = yield self.update_fdroid()
self.write(result)
fdroid_instance = {}
else:
self.set_status(404)
self.finish()
@tornado.gen.coroutine
def delete(self, path):
if path == 'delete':
data = tornado.escape.json_decode(self.request.body)
if data.get('delete') is None:
self.clear()
self.set_status(400)
else:
result = yield self.remove_app(data['delete'])
self.write(result)
else:
self.set_status(404)
self.finish()
if service.fdroid:
app = web.Application([
(r'/', HomeHandler),
(r'/api/(.*?)/?', ApiHandler),
(r'/fdroid/(.*)', web.StaticFileHandler, {'path': service.download_path}),
(r'/static/(.*)', web.StaticFileHandler, {'path': static_dir}),
(r'/views/(.*)', web.StaticFileHandler, {'path': app_dir + '/views'}),
], debug=False)
else:
app = web.Application([
(r'/', HomeHandler),
(r'/api/(.*?)/?', ApiHandler),
(r'/static/(.*)', web.StaticFileHandler, {'path': static_dir}),
(r'/views/(.*)', web.StaticFileHandler, {'path': app_dir + '/views'}),
], debug=False)
# overwrite settings
app.settings['static_path'] = ''
return app

View File

@ -0,0 +1,385 @@
from gpapi.googleplay import GooglePlayAPI, LoginError, RequestError, SecurityCheckError
from pyaxmlparser import APK
from subprocess import Popen, PIPE
import base64
import os
import sys
import concurrent.futures
import locale as locale_service
from datetime import datetime as dt
NOT_LOGGED_IN_ERR = 'Not logged in'
WRONG_CREDENTIALS_ERR = 'Wrong credentials'
SESSION_EXPIRED_ERR = 'Session tokens expired, re-login needed'
FDROID_ERR = 'Error while executing fdroidserver tool'
def makeError(message):
return {'status': 'ERROR',
'message': message}
def get_details_from_apk(apk, downloadPath, service):
if apk is not None:
filepath = os.path.join(downloadPath, apk)
try:
a = APK(filepath)
except Exception as e:
print(e)
return None
print('Fetching details for %s' % a.package)
try:
details = service.details(a.package)
details['filename'] = apk
details['versionCode'] = int(a.version_code)
except RequestError as e:
print('Cannot fetch information for %s' % a.package)
print('Extracting basic information from package...')
return {'docid': a.package,
'filename': apk,
'versionCode': int(a.version_code),
'title': a.application}
print('Added %s to cache' % details['docid'])
return details
class Play(object):
def __init__(self, debug=True, fdroid=False):
self.currentSet = []
self.totalNumOfApps = 0
self.debug = debug
self.fdroid = fdroid
self.firstRun = True
self.loggedIn = False
self._email = None
self._passwd = None
self._gsfId = None
self._token = None
self._last_fdroid_update = None
# configuring download folder
if self.fdroid:
self.download_path = os.path.join(os.getcwd(), 'repo')
else:
self.download_path = os.getcwd()
# configuring fdroid data
if self.fdroid:
self.fdroid_exe = 'fdroid'
self.fdroid_path = os.getcwd()
self.fdroid_init()
# language settings
locale = os.environ.get('LANG_LOCALE')
if locale is None:
locale = locale_service.getdefaultlocale()[0]
timezone = os.environ.get('LANG_TIMEZONE')
if timezone is None:
timezone = 'Europe/Berlin'
device = os.environ.get('DEVICE_CODE')
if device is None:
self.service = GooglePlayAPI(locale, timezone)
else:
self.service = GooglePlayAPI(locale, timezone,
device_codename=device)
def fdroid_init(self):
found = False
for path in os.environ['PATH'].split(':'):
exe = os.path.join(path, self.fdroid_exe)
if os.path.isfile(exe):
found = True
break
if not found:
print('Please install fdroid')
sys.exit(1)
elif os.path.isfile('config.py'):
print('Repo already initalized, skipping init')
else:
p = Popen([self.fdroid_exe, 'init', '-v'], stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
sys.stderr.write("error initializing fdroid repository " +
stderr.decode('utf-8'))
sys.exit(1)
# backup config.py
if self.debug:
print('Checking config.py file')
with open('config.py', 'r') as config_file:
content = config_file.readlines()
with open('config.py', 'w') as config_file:
# copy all the original content of config.py
# if the file was not modified with custom values, do it
modified = False
for line in content:
if '# playmaker' in line:
modified = True
config_file.write(line)
if not modified:
if self.debug:
print('Appending playmaker data to config.py')
config_file.write('\n# playmaker\nrepo_name = "playmaker"\n'
'repo_description = "repository managed with '
'playmaker https://github.com/NoMore201/playmaker"\n')
# ensure all folder and files are setup
p = Popen([self.fdroid_exe, 'update', '--create-key', '-v'], stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
print('Skipping fdroid update')
else:
print('Fdroid repo initialized successfully')
def get_last_fdroid_update(self):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
return {'status': 'SUCCESS',
'message': str(self._last_fdroid_update)}
def fdroid_update(self):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
if self.fdroid:
try:
p = Popen([self.fdroid_exe, 'update', '-c', '--clean'],
stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
sys.stderr.write("error updating fdroid repository " +
stderr.decode('utf-8'))
return makeError(FDROID_ERR)
else:
print('Fdroid repo updated successfully')
self._last_fdroid_update = dt.today().replace(microsecond=0)
return {'status': 'SUCCESS'}
except Exception as e:
return makeError(FDROID_ERR)
else:
return {'status': 'SUCCESS'}
def get_apps(self):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
if self.firstRun:
return {'status': 'PENDING',
'total': self.totalNumOfApps,
'current': len(self.currentSet)}
return {'status': 'SUCCESS',
'message': sorted(self.currentSet, key=lambda k: k['title'])}
def set_encoded_credentials(self, email, password):
self._email = base64.b64decode(email).decode('utf-8')
self._passwd = base64.b64decode(password).decode('utf-8')
def set_credentials(self, email, password):
self._email = email
self._passwd = password
def set_token_credentials(self, gsfId, token):
self._gsfId = int(gsfId, 16)
self._token = token
def has_credentials(self):
passwd_credentials = self._email is not None and self._passwd is not None
token_credentials = self._gsfId is not None and self._token is not None
return passwd_credentials or token_credentials
def login(self):
if self.loggedIn:
return {'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK'}
try:
if not self.has_credentials():
raise LoginError("missing credentials")
self.service.login(self._email,
self._passwd,
self._gsfId,
self._token)
self.loggedIn = True
return {'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK'}
except LoginError as e:
print('LoginError: {0}'.format(e))
self.loggedIn = False
return {'status': 'ERROR',
'securityCheck': False,
'message': 'Wrong credentials'}
except SecurityCheckError as e:
print('SecurityCheckError: {0}'.format(e))
self.loggedIn = False
return {'status': 'ERROR',
'securityCheck': True,
'message': 'Need security check'}
except RequestError as e:
# probably tokens are invalid, so it is better to
# invalidate them
print('RequestError: {0}'.format(e))
self.loggedIn = False
return {'status': 'ERROR',
'securityCheck': False,
'message': 'Request error, probably invalid token'}
def update_state(self):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
print('Updating cache')
with concurrent.futures.ProcessPoolExecutor() as executor:
# get application ids from apk files
apkFiles = [apk for apk in os.listdir(self.download_path)
if os.path.splitext(apk)[1] == '.apk']
self.totalNumOfApps = len(apkFiles)
if self.totalNumOfApps != 0:
future_to_app = [executor.submit(get_details_from_apk,
a,
self.download_path,
self.service)
for a in apkFiles]
for future in concurrent.futures.as_completed(future_to_app):
app = future.result()
if app is not None:
self.currentSet.append(app)
print('Cache correctly initialized')
self.firstRun = False
def insert_app_into_state(self, newApp):
found = False
result = list(filter(lambda x: x['docid'] == newApp['docid'],
self.currentSet))
if len(result) > 0:
found = True
if self.debug:
print('%s is already cached, updating..' % newApp['docid'])
i = self.currentSet.index(result[0])
self.currentSet[i] = newApp
if not found:
if self.debug:
print('Adding %s into cache..' % newApp['docid'])
self.currentSet.append(newApp)
def search(self, appName, numItems=15):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
try:
apps = self.service.search(appName)
except RequestError as e:
print(e)
self.loggedIn = False
return {'status': 'ERROR',
'message': SESSION_EXPIRED_ERR}
except LoginError as e:
print(SESSION_EXPIRED_ERR)
self.loggedIn = False
except IndexError as e:
print(SESSION_EXPIRED_ERR)
self.loggedIn = False
return {'status': 'SUCCESS',
'message': apps}
def details(self, app):
try:
details = self.service.details(app)
except RequestError:
details = None
return details
def get_bulk_details(self, apksList):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
try:
apps = [self.details(a) for a in apksList]
except LoginError as e:
print(e)
self.loggedIn = False
return apps
def download_selection(self, apps):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
success = []
failed = []
unavail = []
for app in apps:
docid = app.get('docid')
details = self.details(docid)
filename = app.get('filename')
if filename is None:
filename = details.get('docid') + '.apk'
if details is None:
print('Package %s does not exits' % docid)
unavail.append(docid)
continue
print('Downloading %s' % docid)
try:
if details.get('offer')[0].get('micros') == 0:
data_gen = self.service.download(docid, details.get('details').get('appDetails')['versionCode'])
else:
data_gen = self.service.delivery(docid, details.get('details').get('appDetails')['versionCode'])
data_gen = data_gen.get('file').get('data')
except IndexError as exc:
print(exc)
print('Package %s does not exists' % docid)
unavail.append(docid)
except Exception as exc:
print(exc)
print('Failed to download %s' % docid)
failed.append(docid)
else:
filepath = os.path.join(self.download_path, filename)
try:
with open(filepath, 'wb') as apk_file:
for chunk in data_gen:
apk_file.write(chunk)
except IOError as exc:
print('Error while writing %s: %s' % (filename, exc))
failed.append(docid)
details['filename'] = filename
success.append(details)
for x in success:
self.insert_app_into_state(x)
return {'status': 'SUCCESS',
'message': {'success': success,
'failed': failed,
'unavail': unavail}}
def check_local_apks(self):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
if len(self.currentSet) == 0:
print('There is no package')
return {'status': 'SUCCESS',
'message': []}
else:
toUpdate = []
for app in self.currentSet:
details = self.details(app.get('docid'))
#print(details)
if details is None:
print('%s not available in Play Store' % app['docid'])
continue
details['filename'] = app.get('filename')
if self.debug:
print('Checking %s' % app['docid'])
print('%d == %d ?' % (app.get('details').get('appDetails')['versionCode'], details.get('details').get('appDetails')['versionCode']))
if app.get('details').get('appDetails')['versionCode'] != details.get('details').get('appDetails')['versionCode']:
toUpdate.append(details)
return {'status': 'SUCCESS',
'message': toUpdate}
def remove_local_app(self, docid):
if not self.loggedIn:
return {'status': 'UNAUTHORIZED'}
# get app from cache
app = list(filter(lambda x: x['docid'] == docid, self.currentSet))
if len(app) < 1:
return {'status': 'ERROR'}
apkPath = os.path.join(self.download_path, app[0]['filename'])
if os.path.isfile(apkPath):
os.remove(apkPath)
self.currentSet.remove(app[0])
return {'status': 'SUCCESS'}
return {'status': 'ERROR'}

View File

@ -0,0 +1,28 @@
angular.module('playmaker').controller('notify', [
'$scope',
'global',
function($scope, global) {
$scope.alerts = [];
$scope.closeAlert = function(index) {
$scope.alerts.splice(index, 1);
};
global.addAlert = function(type, msg) {
newAlert = {
type: type,
msg: msg
};
$scope.alerts.push(newAlert);
};
}]);
angular.module('playmaker').controller('navbar', [
'$location',
'$scope',
'$rootScope',
function($location, $scope, $rootScope) {
$rootScope.$on('$routeChangeSuccess', function() {
$scope.path = $location.path();
});
}]);

View File

@ -0,0 +1,177 @@
/*
* index.html
*/
.navbar-fixed-top {
top:0;
}
.notification-panel {
position: fixed;
right: 30px;
top: 75px;
width: 300px;
z-index: 10;
}
/*
* Login view
*/
.login-body {
margin:0 auto;
max-width: 500px;
}
.loading-login {
max-width: 500px;
margin: 0 auto;
margin-top: 50px;
}
.loading-login-text {
font-size: 20px;
margin: 10px auto;
}
/*
* Apps view
*/
.sidebar {
margin: 0 auto;
margin-top: 71px;
width: 300px;
}
.sidebar-list {
display: flex;
flex-direction: column;
}
.sidebar-list-el {
align-items: center;
display: flex;
flex-direction: row;
height:50px;
justify-content: space-between;
padding-left:15px;
padding-right:15px;
}
.app-container {
margin-top: 30px;
}
.panel-body {
align-items: center;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
white-space: nowrap;
}
.apk-item-title {
display: block;
font-size: 22px;
margin-bottom: 12px;
margin-top:10px;
white-space: nowrap;
}
.apk-info {
margin-left: 20px;
}
.apk-buttons {
display: block;
padding:15px;
text-align:right;
}
.apk-buttons > a {
margin-left: 30px;
}
.apk-progress {
margin:24px 15px 18px 15px;
}
/*
* Search view
*/
.view-container {
max-width: 1000px;
margin:0 auto;
margin-top: 81px;
padding:0 15px;
}
.dl-button {
cursor: pointer;
color: #333333;
}
.dl-button:hover {
color: #333333;
}
.dl-button-disabled {
cursor: default;
pointer-event: none;
color: #BABABA;
}
.dl-button-disabled:hover {
color: #BABABA;
}
#dl-button-td {
width:80px;
}
#search-progress {
margin-top:40px;
margin-left:30px;
margin-right:30px;
}
#search-area {
padding:0 10px;
}
.table {
margin-top:40px;
}
#table-body > tr {
line-height: 70px;
vertical-align: middle;
}
#table-body > tr > td {
vertical-align: middle;
}
@media (min-width: 768px) {
.sidebar {
background-color:#f7f7f7;
bottom:0;
left:0;
margin:0;
padding-top:30px;
position: fixed;
top: 51px;
width: 300px;
}
.app-container {
margin-left:300px;
margin-top: 81px;
max-width: 1200px;
overflow: auto;
}
}

View File

@ -0,0 +1,347 @@
var app = angular.module('playmaker', [
'ngRoute',
'ui.bootstrap'
]);
app.config(['$locationProvider', '$routeProvider',
function config($locationProvider, $routeProvider) {
$routeProvider.
when('/', {
template: '<app-list></app-list>'
}).
when('/search', {
template: '<search-view></search-view>'
}).
when('/login', {
template: '<login-view></login-view>'
}).
otherwise('/');
}
]).run(['$rootScope', '$location', 'api', 'global',
function ($rootScope, $location, api, global) {
api.getApps(function(response) {
if (response.status === 'SUCCESS') {
global.auth.login();
}
if (response === 'err' || !global.auth.isLoggedIn()) {
$location.path('/login');
} else {
// redirect home
$location.path('/');
}
$rootScope.$on('$routeChangeStart', function (event, next, current) {
if (!global.auth.isLoggedIn() && $location.path() !== '/login') {
event.preventDefault();
$location.path('/login');
} else if (global.auth.isLoggedIn() && $location.path() === '/login') {
// redirect home
event.preventDefault();
$location.path('/');
}
});
});
}
]);
app.component('appList', {
templateUrl: '/views/app.html',
controller: function AppController(api, global, $location) {
var ctrl = this;
ctrl.apps = [];
ctrl.lastFdroidUpdate = 'None';
ctrl.desktop = global.desktop;
ctrl.mobile = global.mobile;
var port = $location.port();
ctrl.baseUrl = $location.protocol() + '://' + $location.host();
if (port !== 80 && port !== 443) {
ctrl.baseUrl += ":" + port.toString();
}
var updateApp = function(app) {
app.updating = true;
api.download(app, function(data) {
if (data === 'err' || data.status === 'ERROR') {
global.addAlert('danger', 'Unable to update ' + app.docid);
app.updating = false;
return;
}
if (data.message.success.length === 0) {
global.addAlert('danger', 'Unable to update ' + app.docid);
app.updating = false;
return;
}
app.versionCode = data.message.success[0].versionCode;
app.updating = false;
});
};
ctrl.check = function() {
global.addAlert('info', 'Checking for updates');
api.check(function(data) {
if (data === 'err') {
global.addAlert('danger', 'Cannot check for updates');
return;
}
if (data.status === 'SUCCESS' && data.message.length === 0) {
global.addAlert('success', 'All apps are up-to-date!');
}
if (data.status === 'SUCCESS' && data.message.length > 0) {
global.addAlert('success', 'Updating ' + data.message.length.toString() + ' apps');
data.message.forEach(function(newApp) {
var oldAppIndex = ctrl.apps.findIndex(function(elem) {
return elem.docid === newApp.docid
});
if (oldAppIndex === -1) return;
updateApp(ctrl.apps[oldAppIndex]);
});
}
});
};
ctrl.delete = function(app) {
api.remove(app.docid, function(data) {
if (data.status === 'SUCCESS') {
var i = ctrl.apps.findIndex(function(elem) {
return elem.docid === app.docid;
});
ctrl.apps.splice(i, 1);
} else {
global.addAlert('danger', 'Unable to delete ' + app.docid);
}
});
};
ctrl.fdroid = function() {
var oldUpdate = ctrl.lastFdroidUpdate;
ctrl.lastFdroidUpdate = 'Pending';
api.fdroidUpdate(function (data) {
if (data === 'err') {
global.addAlert('danger', 'Error updating repository');
ctrl.lastFdroidUpdate = oldUpdate;
return;
}
if (data.status === 'PENDING') {
return;
}
if (data.status === 'SUCCESS') {
api.fdroid(function(data) {
if (data.status !== 'SUCCESS') {
return;
}
ctrl.lastFdroidUpdate = data.message;
});
}
});
};
api.getApps(function(data) {
if (data.status === 'UNAUTHORIZED') {
return;
}
ctrl.apps = data.message.map(function(a) {
if (a.aggregateRating !== undefined) {
roundedStars = Math.floor(a.aggregateRating.starRating);
a.formattedStars = a.aggregateRating.starRating.toFixed(1);
a.starList = [];
for (i = 0; i < 5; i++) {
if (i+1 <= roundedStars){
a.starList.push({index: i, full: true});
} else {
a.starList.push({index: i, full: false});
}
}
}
if (a.image !== undefined) {
a.previewImage = a.image.filter(function(img) {
return img.imageType === 4;
});
}
if (a.details.appDetails.installationSize !== undefined) {
a.formattedSize = a.details.appDetails.installationSize / (1024*1024);
a.formattedSize = a.formattedSize.toFixed(2);
}
if (a.author === undefined) {
a.author = "unknown";
}
if (a.files === undefined) {
a.files = ["unknown"];
}
a.updating = false;
return a;
});
});
api.fdroid(function(data) {
if (data.status !== 'SUCCESS') {
return;
}
ctrl.lastFdroidUpdate = data.message;
});
}
});
app.directive('onEnter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
scope.$apply(function (){
scope.$eval(attrs.onEnter);
});
event.preventDefault();
}
});
};
});
app.component('searchView', {
templateUrl: '/views/search.html',
controller: function SearchController($uibModal, api, global) {
var ctrl = this;
ctrl.desktop = global.desktop;
ctrl.mobile = global.mobile;
ctrl.results = [];
ctrl.searching = false;
ctrl.modalOpen = function (item) {
$uibModal.open({
animation: true,
ariaLabelledBy: 'modal-title',
ariaDescribedBy: 'modal-body',
templateUrl: 'myModalContent.html',
controller: function($scope) {
$scope.app = item;
}
});
};
ctrl.search = function(app) {
// no input by the user
if (app === undefined || app === '') return;
ctrl.results = [];
ctrl.searching = true;
api.search(app, function(data) {
if (data === 'err') {
global.addAlert('danger', 'Error while searching');
ctrl.searching = false;
return;
}
if (data.status === 'SUCCESS' && data.message.length === 0) {
global.addAlert('warning', 'No result for "' + app + '"');
ctrl.searching = false;
return;
}
data.message.forEach(function(d) {
d.downloading = false;
d.disabled = false;
});
ctrl.results = data.message[0].child[0].child;
ctrl.searching = false;
});
};
ctrl.download = function(app) {
if (app.disabled) {
return;
}
app.downloading = true;
api.download(app, function(data) {
if (data === 'err') {
app.downloading = false;
global.addAlert('danger', 'Error downloading app');
return;
}
if (data.status === 'SUCCESS') {
if (data.message.success.length === 0) {
app.downloading = false;
global.addAlert('warning', app.docid + ' can\'t be downloaded');
return;
}
}
app.downloading = false;
app.disabled = true;
});
};
}
});
app.component('loginView', {
templateUrl: '/views/login.html',
controller: function LoginController($location, api, global) {
var ctrl = this;
ctrl.current = 0;
ctrl.max = -1;
ctrl.formattedPercent = 0;
ctrl.securityCheck = false;
var polling = function() {
api.getApps(function(response) {
if (response === 'err') {
ctrl.loggingIn = false;
return;
}
if (response.status === 'UNAUTHORIZED') {
return;
}
if (response.status === 'PENDING') {
ctrl.loggingIn = true;
if (response.total !== 0) {
ctrl.max = response.total;
ctrl.current = response.current;
ctrl.formattedPercent = (ctrl.current / ctrl.max)*100;
ctrl.formattedPercent = ctrl.formattedPercent.toFixed(1);
}
}
if (response.status === 'SUCCESS') {
global.auth.login();
$location.path('/');
ctrl.loggingIn = false;
clearInterval(interval);
}
});
};
ctrl.loggingIn = false;
ctrl.badUsername = false;
ctrl.badPassword = false;
polling();
var interval = setInterval(polling, 3000);
ctrl.login = function(user) {
ctrl.badUsername = false;
ctrl.badPassword = false;
if (user.email === '' || user.email === undefined) {
ctrl.badUsername = true;
return;
}
if (user.password === '' || user.password === undefined) {
ctrl.badPassword = true;
return;
}
ctrl.loggingIn = true;
var email = CryptoJS.enc.Utf8.parse(user.email);
var passwd = CryptoJS.enc.Utf8.parse(user.password);
var emailB64 = CryptoJS.enc.Base64.stringify(email);
var passwdB64 = CryptoJS.enc.Base64.stringify(passwd);
api.login(emailB64, passwdB64, function(data) {
if (data.status === 'ERROR') {
global.addAlert('danger', data.message);
ctrl.loggingIn = false;
ctrl.securityCheck = data.securityCheck;
return;
}
});
};
}
});

View File

@ -0,0 +1,150 @@
angular.module('playmaker').service('global', ['$http', function($http) {
function AuthManager() {
this.loggedIn = false;
this.isLoggedIn = function () {
return this.loggedIn;
};
this.login = function () {
this.loggedIn = !this.loggedIn;
};
}
this.addAlert = {};
this.desktop = false;
this.mobile = false;
this.auth = new AuthManager();
var screenWidth = window.innerWidth;
if (screenWidth < 700) {
this.mobile = true;
} else {
this.desktop = true;
}
}]);
angular.module('playmaker').service('api', ['$http', '$location', 'global', function($http, $location, global) {
function loginHandler(result) {
if (result.data.status === 'ERROR') {
if (result.data.message !== undefined) {
global.addAlert('danger', result.data.message);
} else {
global.addAlert('danger', 'Application error');
}
global.auth.loggedIn = false;
$location.path('/login');
}
}
this.getApps = function(callback) {
$http({
method: 'GET',
url: '/api/apps'
}).then(function success(response) {
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.search = function(app, callback) {
$http({
method: 'GET',
url: '/api/search?search=' + app
}).then(function success(response) {
loginHandler(response);
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.check = function(callback) {
$http.post('/api/check')
.then(function success(response) {
loginHandler(response);
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.download = function(app, callback) {
var requestData = {
download: [app]
};
$http({
method: 'POST',
url: '/api/download',
data: JSON.stringify(requestData)
}).then(function success(response) {
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.remove = function(app, callback) {
var requestData = {
delete: app
};
$http({
method: 'DELETE',
url: '/api/delete',
data: JSON.stringify(requestData)
}).then(function success(response) {
loginHandler(response);
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.fdroid = function(callback) {
$http({
method: 'GET',
url: '/api/fdroid'
}).then(function success(response) {
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.fdroidUpdate = function(callback) {
$http({
method: 'POST',
url: '/api/fdroid'
}).then(function success(response) {
loginHandler(response);
callback(response.data);
}, function error(response) {
callback('err');
});
};
this.login = function(email, password, callback) {
$http({
method: 'POST',
url: '/api/login',
data: JSON.stringify({
email: email,
password: password
})
}).then(function success(response) {
callback(response.data);
}, function error(response) {
callback('err');
});
};
}]);

View File

@ -0,0 +1,5 @@
$(document).on('click', '.navbar-collapse.in', function(e) {
if($(e.target).is('a')) {
$(this).collapse('hide');
}
});

View File

@ -0,0 +1,58 @@
/*
AngularJS v1.7.5
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(Y,z){'use strict';function Fa(a,b,c){if(!a)throw Pa("areq",b||"?",c||"required");return a}function Ga(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;Z(a)&&(a=a.join(" "));Z(b)&&(b=b.join(" "));return a+" "+b}function Qa(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function $(a,b,c){var d="";a=Z(a)?a:a&&G(a)&&a.length?a.split(/\s+/):[];s(a,function(a,k){a&&0<a.length&&(d+=0<k?" ":"",d+=c?b+a:a+b)});return d}function Ha(a){if(a instanceof A)switch(a.length){case 0:return a;
case 1:if(1===a[0].nodeType)return a;break;default:return A(va(a))}if(1===a.nodeType)return A(a)}function va(a){if(!a[0])return a;for(var b=0;b<a.length;b++){var c=a[b];if(1===c.nodeType)return c}}function Ra(a,b,c){s(b,function(b){a.addClass(b,c)})}function Sa(a,b,c){s(b,function(b){a.removeClass(b,c)})}function aa(a){return function(b,c){c.addClass&&(Ra(a,b,c.addClass),c.addClass=null);c.removeClass&&(Sa(a,b,c.removeClass),c.removeClass=null)}}function pa(a){a=a||{};if(!a.$$prepared){var b=a.domOperation||
N;a.domOperation=function(){a.$$domOperationFired=!0;b();b=N};a.$$prepared=!0}return a}function ha(a,b){Ia(a,b);Ja(a,b)}function Ia(a,b){b.from&&(a.css(b.from),b.from=null)}function Ja(a,b){b.to&&(a.css(b.to),b.to=null)}function T(a,b,c){var d=b.options||{};c=c.options||{};var f=(d.addClass||"")+" "+(c.addClass||""),k=(d.removeClass||"")+" "+(c.removeClass||"");a=Ta(a.attr("class"),f,k);c.preparationClasses&&(d.preparationClasses=ba(c.preparationClasses,d.preparationClasses),delete c.preparationClasses);
f=d.domOperation!==N?d.domOperation:null;wa(d,c);f&&(d.domOperation=f);d.addClass=a.addClass?a.addClass:null;d.removeClass=a.removeClass?a.removeClass:null;b.addClass=d.addClass;b.removeClass=d.removeClass;return d}function Ta(a,b,c){function d(a){G(a)&&(a=a.split(" "));var c={};s(a,function(a){a.length&&(c[a]=!0)});return c}var f={};a=d(a);b=d(b);s(b,function(a,c){f[c]=1});c=d(c);s(c,function(a,c){f[c]=1===f[c]?null:-1});var k={addClass:"",removeClass:""};s(f,function(c,b){var d,f;1===c?(d="addClass",
f=!a[b]||a[b+"-remove"]):-1===c&&(d="removeClass",f=a[b]||a[b+"-add"]);f&&(k[d].length&&(k[d]+=" "),k[d]+=b)});return k}function K(a){return a instanceof A?a[0]:a}function Ua(a,b,c,d){a="";c&&(a=$(c,"ng-",!0));d.addClass&&(a=ba(a,$(d.addClass,"-add")));d.removeClass&&(a=ba(a,$(d.removeClass,"-remove")));a.length&&(d.preparationClasses=a,b.addClass(a))}function qa(a,b){var c=b?"-"+b+"s":"";ma(a,[na,c]);return[na,c]}function xa(a,b){var c=b?"paused":"",d=ca+"PlayState";ma(a,[d,c]);return[d,c]}function ma(a,
b){a.style[b[0]]=b[1]}function ba(a,b){return a?b?a+" "+b:a:b}function Ka(a,b,c){var d=Object.create(null),f=a.getComputedStyle(b)||{};s(c,function(a,c){var b=f[a];if(b){var L=b.charAt(0);if("-"===L||"+"===L||0<=L)b=Va(b);0===b&&(b=null);d[c]=b}});return d}function Va(a){var b=0;a=a.split(/\s*,\s*/);s(a,function(a){"s"===a.charAt(a.length-1)&&(a=a.substring(0,a.length-1));a=parseFloat(a)||0;b=b?Math.max(a,b):a});return b}function ya(a){return 0===a||null!=a}function La(a,b){var c=M,d=a+"s";b?c+="Duration":
d+=" linear all";return[c,d]}function Ma(a,b,c){s(c,function(c){a[c]=za(a[c])?a[c]:b.style.getPropertyValue(c)})}var M,Aa,ca,Ba;void 0===Y.ontransitionend&&void 0!==Y.onwebkittransitionend?(M="WebkitTransition",Aa="webkitTransitionEnd transitionend"):(M="transition",Aa="transitionend");void 0===Y.onanimationend&&void 0!==Y.onwebkitanimationend?(ca="WebkitAnimation",Ba="webkitAnimationEnd animationend"):(ca="animation",Ba="animationend");var ra=ca+"Delay",Ca=ca+"Duration",na=M+"Delay",Na=M+"Duration",
Pa=z.$$minErr("ng"),Wa={transitionDuration:Na,transitionDelay:na,transitionProperty:M+"Property",animationDuration:Ca,animationDelay:ra,animationIterationCount:ca+"IterationCount"},Xa={transitionDuration:Na,transitionDelay:na,animationDuration:Ca,animationDelay:ra},Da,wa,s,Z,za,sa,Ea,ta,G,R,A,N;z.module("ngAnimate",[],function(){N=z.noop;Da=z.copy;wa=z.extend;A=z.element;s=z.forEach;Z=z.isArray;G=z.isString;ta=z.isObject;R=z.isUndefined;za=z.isDefined;Ea=z.isFunction;sa=z.isElement}).info({angularVersion:"1.7.5"}).directive("ngAnimateSwap",
["$animate",function(a){return{restrict:"A",transclude:"element",terminal:!0,priority:600,link:function(b,c,d,f,k){var e,Q;b.$watchCollection(d.ngAnimateSwap||d["for"],function(b){e&&a.leave(e);Q&&(Q.$destroy(),Q=null);(b||0===b)&&k(function(b,d){e=b;Q=d;a.enter(b,null,c)})})}}}]).directive("ngAnimateChildren",["$interpolate",function(a){return{link:function(b,c,d){function f(a){c.data("$$ngAnimateChildren","on"===a||"true"===a)}var k=d.ngAnimateChildren;G(k)&&0===k.length?c.data("$$ngAnimateChildren",
!0):(f(a(k)(b)),d.$observe("ngAnimateChildren",f))}}}]).factory("$$rAFScheduler",["$$rAF",function(a){function b(a){d=d.concat(a);c()}function c(){if(d.length){for(var b=d.shift(),e=0;e<b.length;e++)b[e]();f||a(function(){f||c()})}}var d,f;d=b.queue=[];b.waitUntilQuiet=function(b){f&&f();f=a(function(){f=null;b();c()})};return b}]).provider("$$animateQueue",["$animateProvider",function(a){function b(a){return{addClass:a.addClass,removeClass:a.removeClass,from:a.from,to:a.to}}function c(a){if(!a)return null;
a=a.split(" ");var b=Object.create(null);s(a,function(a){b[a]=!0});return b}function d(a,b){if(a&&b){var d=c(b);return a.split(" ").some(function(a){return d[a]})}}function f(a,b,c){return e[a].some(function(a){return a(b,c)})}function k(a,b){var c=0<(a.addClass||"").length,d=0<(a.removeClass||"").length;return b?c&&d:c||d}var e=this.rules={skip:[],cancel:[],join:[]};e.join.push(function(a,b){return!a.structural&&k(a)});e.skip.push(function(a,b){return!a.structural&&!k(a)});e.skip.push(function(a,
b){return"leave"===b.event&&a.structural});e.skip.push(function(a,b){return b.structural&&2===b.state&&!a.structural});e.cancel.push(function(a,b){return b.structural&&a.structural});e.cancel.push(function(a,b){return 2===b.state&&a.structural});e.cancel.push(function(a,b){if(b.structural)return!1;var c=a.addClass,f=a.removeClass,k=b.addClass,e=b.removeClass;return R(c)&&R(f)||R(k)&&R(e)?!1:d(c,e)||d(f,k)});this.$get=["$$rAF","$rootScope","$rootElement","$document","$$Map","$$animation","$$AnimateRunner",
"$templateRequest","$$jqLite","$$forceReflow","$$isDocumentHidden",function(c,d,e,C,U,oa,H,u,t,I,da){function ia(a){O.delete(a.target)}function v(){var a=!1;return function(b){a?b():d.$$postDigest(function(){a=!0;b()})}}function ua(a,b,c){var g=[],l=m[c];l&&s(l,function(l){Oa.call(l.node,b)?g.push(l.callback):"leave"===c&&Oa.call(l.node,a)&&g.push(l.callback)});return g}function h(a,b,c){var l=va(b);return a.filter(function(a){return!(a.node===l&&(!c||a.callback===c))})}function q(a,J,w){function e(a,
b,l,g){u(function(){var a=ua(ia,m,b);a.length?c(function(){s(a,function(a){a(h,l,g)});"close"!==l||m.parentNode||D.off(m)}):"close"!==l||m.parentNode||D.off(m)});a.progress(b,l,g)}function I(a){var b=h,c=n;c.preparationClasses&&(b.removeClass(c.preparationClasses),c.preparationClasses=null);c.activeClasses&&(b.removeClass(c.activeClasses),c.activeClasses=null);W(h,n);ha(h,n);n.domOperation();q.complete(!a)}var n=Da(w),h=Ha(a),m=K(h),ia=m&&m.parentNode,n=pa(n),q=new H,u=v();Z(n.addClass)&&(n.addClass=
n.addClass.join(" "));n.addClass&&!G(n.addClass)&&(n.addClass=null);Z(n.removeClass)&&(n.removeClass=n.removeClass.join(" "));n.removeClass&&!G(n.removeClass)&&(n.removeClass=null);n.from&&!ta(n.from)&&(n.from=null);n.to&&!ta(n.to)&&(n.to=null);if(!(B&&m&&fa(m,J,w)&&Ya(m,n)))return I(),q;var x=0<=["enter","move","leave"].indexOf(J),r=da(),P=r||O.get(m);w=!P&&y.get(m)||{};var p=!!w.state;P||p&&1===w.state||(P=!E(m,ia,J));if(P)return r&&e(q,J,"start",b(n)),I(),r&&e(q,J,"close",b(n)),q;x&&F(m);r={structural:x,
element:h,event:J,addClass:n.addClass,removeClass:n.removeClass,close:I,options:n,runner:q};if(p){if(f("skip",r,w)){if(2===w.state)return I(),q;T(h,w,r);return w.runner}if(f("cancel",r,w))if(2===w.state)w.runner.end();else if(w.structural)w.close();else return T(h,w,r),w.runner;else if(f("join",r,w))if(2===w.state)T(h,r,{});else return Ua(t,h,x?J:null,n),J=r.event=w.event,n=T(h,w,r),w.runner}else T(h,r,{});(p=r.structural)||(p="animate"===r.event&&0<Object.keys(r.options.to||{}).length||k(r));if(!p)return I(),
g(m),q;var C=(w.counter||0)+1;r.counter=C;l(m,1,r);d.$$postDigest(function(){h=Ha(a);var c=y.get(m),d=!c,c=c||{},t=0<(h.parent()||[]).length&&("animate"===c.event||c.structural||k(c));if(d||c.counter!==C||!t){d&&(W(h,n),ha(h,n));if(d||x&&c.event!==J)n.domOperation(),q.end();t||g(m)}else J=!c.structural&&k(c,!0)?"setClass":c.event,l(m,2),c=oa(h,J,c.options),q.setHost(c),e(q,J,"start",b(n)),c.done(function(a){I(!a);(a=y.get(m))&&a.counter===C&&g(m);e(q,J,"close",b(n))})});return q}function F(a){a=a.querySelectorAll("[data-ng-animate]");
s(a,function(a){var b=parseInt(a.getAttribute("data-ng-animate"),10),c=y.get(a);if(c)switch(b){case 2:c.runner.end();case 1:y.delete(a)}})}function g(a){a.removeAttribute("data-ng-animate");y.delete(a)}function E(a,b,c){c=C[0].body;var l=K(e),g=a===c||"HTML"===a.nodeName,d=a===l,t=!1,m=O.get(a),h;for((a=A.data(a,"$ngAnimatePin"))&&(b=K(a));b;){d||(d=b===l);if(1!==b.nodeType)break;a=y.get(b)||{};if(!t){var f=O.get(b);if(!0===f&&!1!==m){m=!0;break}else!1===f&&(m=!1);t=a.structural}if(R(h)||!0===h)a=
A.data(b,"$$ngAnimateChildren"),za(a)&&(h=a);if(t&&!1===h)break;g||(g=b===c);if(g&&d)break;if(!d&&(a=A.data(b,"$ngAnimatePin"))){b=K(a);continue}b=b.parentNode}return(!t||h)&&!0!==m&&d&&g}function l(a,b,c){c=c||{};c.state=b;a.setAttribute("data-ng-animate",b);c=(b=y.get(a))?wa(b,c):c;y.set(a,c)}var y=new U,O=new U,B=null,P=d.$watch(function(){return 0===u.totalPendingRequests},function(a){a&&(P(),d.$$postDigest(function(){d.$$postDigest(function(){null===B&&(B=!0)})}))}),m=Object.create(null);U=a.customFilter();
var la=a.classNameFilter();I=function(){return!0};var fa=U||I,Ya=la?function(a,b){var c=[a.getAttribute("class"),b.addClass,b.removeClass].join(" ");return la.test(c)}:I,W=aa(t),Oa=Y.Node.prototype.contains||function(a){return this===a||!!(this.compareDocumentPosition(a)&16)},D={on:function(a,b,c){var l=va(b);m[a]=m[a]||[];m[a].push({node:l,callback:c});A(b).on("$destroy",function(){y.get(l)||D.off(a,b,c)})},off:function(a,b,c){if(1!==arguments.length||G(arguments[0])){var l=m[a];l&&(m[a]=1===arguments.length?
null:h(l,b,c))}else for(l in b=arguments[0],m)m[l]=h(m[l],b)},pin:function(a,b){Fa(sa(a),"element","not an element");Fa(sa(b),"parentElement","not an element");a.data("$ngAnimatePin",b)},push:function(a,b,c,l){c=c||{};c.domOperation=l;return q(a,b,c)},enabled:function(a,b){var c=arguments.length;if(0===c)b=!!B;else if(sa(a)){var l=K(a);if(1===c)b=!O.get(l);else{if(!O.has(l))A(a).on("$destroy",ia);O.set(l,!b)}}else b=B=!!a;return b}};return D}]}]).provider("$$animateCache",function(){var a=0,b=Object.create(null);
this.$get=[function(){return{cacheKey:function(b,d,f,k){var e=b.parentNode;b=[e.$$ngAnimateParentKey||(e.$$ngAnimateParentKey=++a),d,b.getAttribute("class")];f&&b.push(f);k&&b.push(k);return b.join(" ")},containsCachedAnimationWithoutDuration:function(a){return(a=b[a])&&!a.isValid||!1},flush:function(){b=Object.create(null)},count:function(a){return(a=b[a])?a.total:0},get:function(a){return(a=b[a])&&a.value},put:function(a,d,f){b[a]?(b[a].total++,b[a].value=d):b[a]={total:1,value:d,isValid:f}}}}]}).provider("$$animation",
["$animateProvider",function(a){var b=this.drivers=[];this.$get=["$$jqLite","$rootScope","$injector","$$AnimateRunner","$$Map","$$rAFScheduler","$$animateCache",function(a,d,f,k,e,Q,L){function x(a){function b(a){if(a.processed)return a;a.processed=!0;var d=a.domNode,t=d.parentNode;f.set(d,a);for(var h;t;){if(h=f.get(t)){h.processed||(h=b(h));break}t=t.parentNode}(h||c).children.push(a);return a}var c={children:[]},d,f=new e;for(d=0;d<a.length;d++){var da=a[d];f.set(da.domNode,a[d]={domNode:da.domNode,
element:da.element,fn:da.fn,children:[]})}for(d=0;d<a.length;d++)b(a[d]);return function(a){var b=[],c=[],d;for(d=0;d<a.children.length;d++)c.push(a.children[d]);a=c.length;var t=0,f=[];for(d=0;d<c.length;d++){var g=c[d];0>=a&&(a=t,t=0,b.push(f),f=[]);f.push(g);g.children.forEach(function(a){t++;c.push(a)});a--}f.length&&b.push(f);return b}(c)}var C=[],U=aa(a);return function(e,H,u){function t(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=
a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function I(a){var b=[],c={};s(a,function(a,d){var l=K(a.element),g=0<=["enter","move"].indexOf(a.event),l=a.structural?t(l):[];if(l.length){var f=g?"to":"from";s(l,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]||{};c[b][f]={animationID:d,element:A(a)}})}else b.push(a)});var d={},g={};s(c,function(c,t){var f=c.from,e=c.to;if(f&&e){var h=a[f.animationID],k=a[e.animationID],E=f.animationID.toString();if(!g[E]){var I=g[E]=
{structural:!0,beforeStart:function(){h.beforeStart();k.beforeStart()},close:function(){h.close();k.close()},classes:da(h.classes,k.classes),from:h,to:k,anchors:[]};I.classes.length?b.push(I):(b.push(h),b.push(k))}g[E].anchors.push({out:f.element,"in":e.element})}else f=f?f.animationID:e.animationID,e=f.toString(),d[e]||(d[e]=!0,b.push(a[f]))});return b}function da(a,b){a=a.split(" ");b=b.split(" ");for(var c=[],d=0;d<a.length;d++){var g=a[d];if("ng-"!==g.substring(0,3))for(var t=0;t<b.length;t++)if(g===
b[t]){c.push(g);break}}return c.join(" ")}function ia(a){for(var c=b.length-1;0<=c;c--){var d=f.get(b[c])(a);if(d)return d}}function v(a,b){function c(a){(a=a.data("$$animationRunner"))&&a.setHost(b)}a.from&&a.to?(c(a.from.element),c(a.to.element)):c(a.element)}function ua(){var a=e.data("$$animationRunner");!a||"leave"===H&&u.$$domOperationFired||a.end()}function h(b){e.off("$destroy",ua);e.removeData("$$animationRunner");U(e,u);ha(e,u);u.domOperation();E&&a.removeClass(e,E);F.complete(!b)}u=pa(u);
var q=0<=["enter","move","leave"].indexOf(H),F=new k({end:function(){h()},cancel:function(){h(!0)}});if(!b.length)return h(),F;var g=Ga(e.attr("class"),Ga(u.addClass,u.removeClass)),E=u.tempClasses;E&&(g+=" "+E,u.tempClasses=null);q&&e.data("$$animatePrepareClasses","ng-"+H+"-prepare");e.data("$$animationRunner",F);C.push({element:e,classes:g,event:H,structural:q,options:u,beforeStart:function(){E=(E?E+" ":"")+"ng-animate";a.addClass(e,E);var b=e.data("$$animatePrepareClasses");b&&a.removeClass(e,
b)},close:h});e.on("$destroy",ua);if(1<C.length)return F;d.$$postDigest(function(){var b=[];s(C,function(a){a.element.data("$$animationRunner")?b.push(a):a.close()});C.length=0;var d=I(b),g=[];s(d,function(a){var b=a.from?a.from.element:a.element,c=u.addClass,d=L.cacheKey(b[0],a.event,(c?c+" ":"")+"ng-animate",u.removeClass);g.push({element:b,domNode:K(b),fn:function(){var b,c=a.close;if(L.containsCachedAnimationWithoutDuration(d))c();else{a.beforeStart();if((a.anchors?a.from.element||a.to.element:
a.element).data("$$animationRunner")){var g=ia(a);g&&(b=g.start)}b?(b=b(),b.done(function(a){c(!a)}),v(a,b)):c()}}})});for(var d=x(g),t=0;t<d.length;t++)for(var f=d[t],e=0;e<f.length;e++){var h=f[e],k=h.element;d[t][e]=h.fn;0===t?k.removeData("$$animatePrepareClasses"):(h=k.data("$$animatePrepareClasses"))&&a.addClass(k,h)}Q(d)});return F}}]}]).provider("$animateCss",["$animateProvider",function(a){this.$get=["$window","$$jqLite","$$AnimateRunner","$timeout","$$animateCache","$$forceReflow","$sniffer",
"$$rAFScheduler","$$animateQueue",function(a,c,d,f,k,e,Q,L,x){function C(d,f,e,x){var v,s="stagger-"+e;0<k.count(e)&&(v=k.get(s),v||(f=$(f,"-stagger"),c.addClass(d,f),v=Ka(a,d,x),v.animationDuration=Math.max(v.animationDuration,0),v.transitionDuration=Math.max(v.transitionDuration,0),c.removeClass(d,f),k.put(s,v,!0)));return v||{}}function U(a){u.push(a);L.waitUntilQuiet(function(){k.flush();for(var a=e(),b=0;b<u.length;b++)u[b](a);u.length=0})}function z(c,d,f,e){d=k.get(f);d||(d=Ka(a,c,Wa),"infinite"===
d.animationIterationCount&&(d.animationIterationCount=1));k.put(f,d,e||0<d.transitionDuration||0<d.animationDuration);c=d;f=c.animationDelay;e=c.transitionDelay;c.maxDelay=f&&e?Math.max(f,e):f||e;c.maxDuration=Math.max(c.animationDuration*c.animationIterationCount,c.transitionDuration);return c}var H=aa(c),u=[];return function(a,b){function e(){v()}function L(){v(!0)}function v(b){if(!(P||la&&m)){P=!0;m=!1;V&&!g.$$skipPreparationClasses&&c.removeClass(a,V);ba&&c.removeClass(a,ba);xa(l,!1);qa(l,!1);
s(y,function(a){l.style[a[0]]=""});H(a,g);ha(a,g);Object.keys(E).length&&s(E,function(a,b){a?l.style.setProperty(b,a):l.style.removeProperty(b)});if(g.onDone)g.onDone();w&&w.length&&a.off(w.join(" "),q);var d=a.data("$$animateCss");d&&(f.cancel(d[0].timer),a.removeData("$$animateCss"));fa&&fa.complete(!b)}}function u(a){p.blockTransition&&qa(l,a);p.blockKeyframeAnimation&&xa(l,!!a)}function h(){fa=new d({end:e,cancel:L});U(N);v();return{$$willAnimate:!1,start:function(){return fa},end:e}}function q(a){a.stopPropagation();
var b=a.originalEvent||a;b.target===l&&(a=b.$manualTimeStamp||Date.now(),b=parseFloat(b.elapsedTime.toFixed(3)),Math.max(a-J,0)>=G&&b>=D&&(la=!0,v()))}function F(){function b(){if(!P){u(!1);s(y,function(a){l.style[a[0]]=a[1]});H(a,g);c.addClass(a,ba);if(p.recalculateTimingStyles){T=l.getAttribute("class")+" "+V;ka=k.cacheKey(l,ja,g.addClass,g.removeClass);r=z(l,T,ka,!1);ga=r.maxDelay;W=Math.max(ga,0);D=r.maxDuration;if(0===D){v();return}p.hasTransitions=0<r.transitionDuration;p.hasAnimations=0<r.animationDuration}p.applyAnimationDelay&&
(ga="boolean"!==typeof g.delay&&ya(g.delay)?parseFloat(g.delay):ga,W=Math.max(ga,0),r.animationDelay=ga,ea=[ra,ga+"s"],y.push(ea),l.style[ea[0]]=ea[1]);G=1E3*W;R=1E3*D;if(g.easing){var e,h=g.easing;p.hasTransitions&&(e=M+"TimingFunction",y.push([e,h]),l.style[e]=h);p.hasAnimations&&(e=ca+"TimingFunction",y.push([e,h]),l.style[e]=h)}r.transitionDuration&&w.push(Aa);r.animationDuration&&w.push(Ba);J=Date.now();var m=G+1.5*R;e=J+m;var h=a.data("$$animateCss")||[],F=!0;if(h.length){var n=h[0];(F=e>n.expectedEndTime)?
f.cancel(n.timer):h.push(v)}F&&(m=f(d,m,!1),h[0]={timer:m,expectedEndTime:e},h.push(v),a.data("$$animateCss",h));if(w.length)a.on(w.join(" "),q);g.to&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.to)),Ja(a,g))}}function d(){var b=a.data("$$animateCss");if(b){for(var c=1;c<b.length;c++)b[c]();a.removeData("$$animateCss")}}if(!P)if(l.parentNode){var e=function(a){if(la)m&&a&&(m=!1,v());else if(m=!a,r.animationDuration)if(a=xa(l,m),m)y.push(a);else{var b=y,c=b.indexOf(a);0<=a&&b.splice(c,1)}},h=0<aa&&(r.transitionDuration&&
0===X.transitionDuration||r.animationDuration&&0===X.animationDuration)&&Math.max(X.animationDelay,X.transitionDelay);h?f(b,Math.floor(h*aa*1E3),!1):b();A.resume=function(){e(!0)};A.pause=function(){e(!1)}}else v()}var g=b||{};g.$$prepared||(g=pa(Da(g)));var E={},l=K(a);if(!l||!l.parentNode||!x.enabled())return h();var y=[],O=a.attr("class"),B=Qa(g),P,m,la,fa,A,W,G,D,R,J,w=[];if(0===g.duration||!Q.animations&&!Q.transitions)return h();var ja=g.event&&Z(g.event)?g.event.join(" "):g.event,Y=ja&&g.structural,
n="",S="";Y?n=$(ja,"ng-",!0):ja&&(n=ja);g.addClass&&(S+=$(g.addClass,"-add"));g.removeClass&&(S.length&&(S+=" "),S+=$(g.removeClass,"-remove"));g.applyClassesEarly&&S.length&&H(a,g);var V=[n,S].join(" ").trim(),T=O+" "+V,O=B.to&&0<Object.keys(B.to).length;if(!(0<(g.keyframeStyle||"").length||O||V))return h();var X,ka=k.cacheKey(l,ja,g.addClass,g.removeClass);if(k.containsCachedAnimationWithoutDuration(ka))return V=null,h();0<g.stagger?(B=parseFloat(g.stagger),X={transitionDelay:B,animationDelay:B,
transitionDuration:0,animationDuration:0}):X=C(l,V,ka,Xa);g.$$skipPreparationClasses||c.addClass(a,V);g.transitionStyle&&(B=[M,g.transitionStyle],ma(l,B),y.push(B));0<=g.duration&&(B=0<l.style[M].length,B=La(g.duration,B),ma(l,B),y.push(B));g.keyframeStyle&&(B=[ca,g.keyframeStyle],ma(l,B),y.push(B));var aa=X?0<=g.staggerIndex?g.staggerIndex:k.count(ka):0;(n=0===aa)&&!g.skipBlocking&&qa(l,9999);var r=z(l,T,ka,!Y),ga=r.maxDelay;W=Math.max(ga,0);D=r.maxDuration;var p={};p.hasTransitions=0<r.transitionDuration;
p.hasAnimations=0<r.animationDuration;p.hasTransitionAll=p.hasTransitions&&"all"===r.transitionProperty;p.applyTransitionDuration=O&&(p.hasTransitions&&!p.hasTransitionAll||p.hasAnimations&&!p.hasTransitions);p.applyAnimationDuration=g.duration&&p.hasAnimations;p.applyTransitionDelay=ya(g.delay)&&(p.applyTransitionDuration||p.hasTransitions);p.applyAnimationDelay=ya(g.delay)&&p.hasAnimations;p.recalculateTimingStyles=0<S.length;if(p.applyTransitionDuration||p.applyAnimationDuration)D=g.duration?parseFloat(g.duration):
D,p.applyTransitionDuration&&(p.hasTransitions=!0,r.transitionDuration=D,B=0<l.style[M+"Property"].length,y.push(La(D,B))),p.applyAnimationDuration&&(p.hasAnimations=!0,r.animationDuration=D,y.push([Ca,D+"s"]));if(0===D&&!p.recalculateTimingStyles)return h();var ba=$(V,"-active");if(null!=g.delay){var ea;"boolean"!==typeof g.delay&&(ea=parseFloat(g.delay),W=Math.max(ea,0));p.applyTransitionDelay&&y.push([na,ea+"s"]);p.applyAnimationDelay&&y.push([ra,ea+"s"])}null==g.duration&&0<r.transitionDuration&&
(p.recalculateTimingStyles=p.recalculateTimingStyles||n);G=1E3*W;R=1E3*D;g.skipBlocking||(p.blockTransition=0<r.transitionDuration,p.blockKeyframeAnimation=0<r.animationDuration&&0<X.animationDelay&&0===X.animationDuration);g.from&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.from)),Ia(a,g));p.blockTransition||p.blockKeyframeAnimation?u(D):g.skipBlocking||qa(l,!1);return{$$willAnimate:!0,end:e,start:function(){if(!P)return A={end:e,cancel:L,resume:null,pause:null},fa=new d(A),U(F),fa}}}}]}]).provider("$$animateCssDriver",
["$$animationProvider",function(a){a.drivers.push("$$animateCssDriver");this.$get=["$animateCss","$rootScope","$$AnimateRunner","$rootElement","$sniffer","$$jqLite","$document",function(a,c,d,f,k,e,Q){function L(a){return a.replace(/\bng-\S+\b/g,"")}function x(a,b){G(a)&&(a=a.split(" "));G(b)&&(b=b.split(" "));return a.filter(function(a){return-1===b.indexOf(a)}).join(" ")}function C(c,e,f){function k(a){var b={},c=K(a).getBoundingClientRect();s(["width","height","top","left"],function(a){var d=c[a];
switch(a){case "top":d+=H.scrollTop;break;case "left":d+=H.scrollLeft}b[a]=Math.floor(d)+"px"});return b}function v(){var c=L(f.attr("class")||""),d=x(c,q),c=x(q,c),d=a(h,{to:k(f),addClass:"ng-anchor-in "+d,removeClass:"ng-anchor-out "+c,delay:!0});return d.$$willAnimate?d:null}function C(){h.remove();e.removeClass("ng-animate-shim");f.removeClass("ng-animate-shim")}var h=A(K(e).cloneNode(!0)),q=L(h.attr("class")||"");e.addClass("ng-animate-shim");f.addClass("ng-animate-shim");h.addClass("ng-anchor");
u.append(h);var F;c=function(){var c=a(h,{addClass:"ng-anchor-out",delay:!0,from:k(e)});return c.$$willAnimate?c:null}();if(!c&&(F=v(),!F))return C();var g=c||F;return{start:function(){function a(){c&&c.end()}var b,c=g.start();c.done(function(){c=null;if(!F&&(F=v()))return c=F.start(),c.done(function(){c=null;C();b.complete()}),c;C();b.complete()});return b=new d({end:a,cancel:a})}}}function z(a,b,c,e){var f=oa(a,N),k=oa(b,N),h=[];s(e,function(a){(a=C(c,a.out,a["in"]))&&h.push(a)});if(f||k||0!==h.length)return{start:function(){function a(){s(b,
function(a){a.end()})}var b=[];f&&b.push(f.start());k&&b.push(k.start());s(h,function(a){b.push(a.start())});var c=new d({end:a,cancel:a});d.all(b,function(a){c.complete(a)});return c}}}function oa(c){var d=c.element,e=c.options||{};c.structural&&(e.event=c.event,e.structural=!0,e.applyClassesEarly=!0,"leave"===c.event&&(e.onDone=e.domOperation));e.preparationClasses&&(e.event=ba(e.event,e.preparationClasses));c=a(d,e);return c.$$willAnimate?c:null}if(!k.animations&&!k.transitions)return N;var H=
Q[0].body;c=K(f);var u=A(c.parentNode&&11===c.parentNode.nodeType||H.contains(c)?c:H);return function(a){return a.from&&a.to?z(a.from,a.to,a.classes,a.anchors):oa(a)}}]}]).provider("$$animateJs",["$animateProvider",function(a){this.$get=["$injector","$$AnimateRunner","$$jqLite",function(b,c,d){function f(c){c=Z(c)?c:c.split(" ");for(var d=[],f={},k=0;k<c.length;k++){var s=c[k],z=a.$$registeredAnimations[s];z&&!f[s]&&(d.push(b.get(z)),f[s]=!0)}return d}var k=aa(d);return function(a,b,d,x){function C(){x.domOperation();
k(a,x)}function z(a,b,d,f,e){switch(d){case "animate":b=[b,f.from,f.to,e];break;case "setClass":b=[b,t,I,e];break;case "addClass":b=[b,t,e];break;case "removeClass":b=[b,I,e];break;default:b=[b,e]}b.push(f);if(a=a.apply(a,b))if(Ea(a.start)&&(a=a.start()),a instanceof c)a.done(e);else if(Ea(a))return a;return N}function A(a,b,d,e,f){var h=[];s(e,function(e){var l=e[f];l&&h.push(function(){var e,f,h=!1,k=function(a){h||(h=!0,(f||N)(a),e.complete(!a))};e=new c({end:function(){k()},cancel:function(){k(!0)}});
f=z(l,a,b,d,function(a){k(!1===a)});return e})});return h}function H(a,b,d,e,f){var h=A(a,b,d,e,f);if(0===h.length){var k,q;"beforeSetClass"===f?(k=A(a,"removeClass",d,e,"beforeRemoveClass"),q=A(a,"addClass",d,e,"beforeAddClass")):"setClass"===f&&(k=A(a,"removeClass",d,e,"removeClass"),q=A(a,"addClass",d,e,"addClass"));k&&(h=h.concat(k));q&&(h=h.concat(q))}if(0!==h.length)return function(a){var b=[];h.length&&s(h,function(a){b.push(a())});b.length?c.all(b,a):a();return function(a){s(b,function(b){a?
b.cancel():b.end()})}}}var u=!1;3===arguments.length&&ta(d)&&(x=d,d=null);x=pa(x);d||(d=a.attr("class")||"",x.addClass&&(d+=" "+x.addClass),x.removeClass&&(d+=" "+x.removeClass));var t=x.addClass,I=x.removeClass,G=f(d),K,v;if(G.length){var M,h;"leave"===b?(h="leave",M="afterLeave"):(h="before"+b.charAt(0).toUpperCase()+b.substr(1),M=b);"enter"!==b&&"move"!==b&&(K=H(a,b,x,G,h));v=H(a,b,x,G,M)}if(K||v){var q;return{$$willAnimate:!0,end:function(){q?q.end():(u=!0,C(),ha(a,x),q=new c,q.complete(!0));
return q},start:function(){function b(c){u=!0;C();ha(a,x);q.complete(c)}if(q)return q;q=new c;var d,f=[];K&&f.push(function(a){d=K(a)});f.length?f.push(function(a){C();a(!0)}):C();v&&f.push(function(a){d=v(a)});q.setHost({end:function(){u||((d||N)(void 0),b(void 0))},cancel:function(){u||((d||N)(!0),b(!0))}});c.chain(f,b);return q}}}}}]}]).provider("$$animateJsDriver",["$$animationProvider",function(a){a.drivers.push("$$animateJsDriver");this.$get=["$$animateJs","$$AnimateRunner",function(a,c){function d(c){return a(c.element,
c.event,c.classes,c.options)}return function(a){if(a.from&&a.to){var b=d(a.from),e=d(a.to);if(b||e)return{start:function(){function a(){return function(){s(d,function(a){a.end()})}}var d=[];b&&d.push(b.start());e&&d.push(e.start());c.all(d,function(a){f.complete(a)});var f=new c({end:a(),cancel:a()});return f}}}else return d(a)}}]}])})(window,window.angular);
//# sourceMappingURL=angular-animate.min.js.map

View File

@ -0,0 +1,17 @@
/*
AngularJS v1.7.5
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(I,b){'use strict';function z(b,h){var d=[],c=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[?*])?/g,function(b,c,h,k){b="?"===k||"*?"===k;k="*"===k||"*?"===k;d.push({name:h,optional:b});c=c||"";return(b?"(?:"+c:c+"(?:")+(k?"(.+?)":"([^/]+)")+(b?"?)?":")")}).replace(/([/$*])/g,"\\$1");h.ignoreTrailingSlashes&&(c=c.replace(/\/+$/,"")+"/*");return{keys:d,regexp:new RegExp("^"+c+"(?:[?#]|$)",h.caseInsensitiveMatch?"i":"")}}function A(b){p&&b.get("$route")}function v(u,h,d){return{restrict:"ECA",
terminal:!0,priority:400,transclude:"element",link:function(c,f,g,l,k){function q(){r&&(d.cancel(r),r=null);m&&(m.$destroy(),m=null);s&&(r=d.leave(s),r.done(function(b){!1!==b&&(r=null)}),s=null)}function C(){var g=u.current&&u.current.locals;if(b.isDefined(g&&g.$template)){var g=c.$new(),l=u.current;s=k(g,function(g){d.enter(g,null,s||f).done(function(d){!1===d||!b.isDefined(w)||w&&!c.$eval(w)||h()});q()});m=l.scope=g;m.$emit("$viewContentLoaded");m.$eval(p)}else q()}var m,s,r,w=g.autoscroll,p=g.onload||
"";c.$on("$routeChangeSuccess",C);C()}}}function x(b,h,d){return{restrict:"ECA",priority:-400,link:function(c,f){var g=d.current,l=g.locals;f.html(l.$template);var k=b(f.contents());if(g.controller){l.$scope=c;var q=h(g.controller,l);g.controllerAs&&(c[g.controllerAs]=q);f.data("$ngControllerController",q);f.children().data("$ngControllerController",q)}c[g.resolveAs||"$resolve"]=l;k(c)}}}var D,E,F,G,y=b.module("ngRoute",[]).info({angularVersion:"1.7.5"}).provider("$route",function(){function u(d,
c){return b.extend(Object.create(d),c)}D=b.isArray;E=b.isObject;F=b.isDefined;G=b.noop;var h={};this.when=function(d,c){var f;f=void 0;if(D(c)){f=f||[];for(var g=0,l=c.length;g<l;g++)f[g]=c[g]}else if(E(c))for(g in f=f||{},c)if("$"!==g.charAt(0)||"$"!==g.charAt(1))f[g]=c[g];f=f||c;b.isUndefined(f.reloadOnUrl)&&(f.reloadOnUrl=!0);b.isUndefined(f.reloadOnSearch)&&(f.reloadOnSearch=!0);b.isUndefined(f.caseInsensitiveMatch)&&(f.caseInsensitiveMatch=this.caseInsensitiveMatch);h[d]=b.extend(f,{originalPath:d},
d&&z(d,f));d&&(g="/"===d[d.length-1]?d.substr(0,d.length-1):d+"/",h[g]=b.extend({originalPath:d,redirectTo:d},z(g,f)));return this};this.caseInsensitiveMatch=!1;this.otherwise=function(b){"string"===typeof b&&(b={redirectTo:b});this.when(null,b);return this};p=!0;this.eagerInstantiationEnabled=function(b){return F(b)?(p=b,this):p};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce","$browser",function(d,c,f,g,l,k,q,p){function m(a){var e=t.current;n=A();(x=
!B&&n&&e&&n.$$route===e.$$route&&(!n.reloadOnUrl||!n.reloadOnSearch&&b.equals(n.pathParams,e.pathParams)))||!e&&!n||d.$broadcast("$routeChangeStart",n,e).defaultPrevented&&a&&a.preventDefault()}function s(){var a=t.current,e=n;if(x)a.params=e.params,b.copy(a.params,f),d.$broadcast("$routeUpdate",a);else if(e||a){B=!1;t.current=e;var c=g.resolve(e);p.$$incOutstandingRequestCount("$route");c.then(r).then(w).then(function(g){return g&&c.then(y).then(function(c){e===t.current&&(e&&(e.locals=c,b.copy(e.params,
f)),d.$broadcast("$routeChangeSuccess",e,a))})}).catch(function(b){e===t.current&&d.$broadcast("$routeChangeError",e,a,b)}).finally(function(){p.$$completeOutstandingRequest(G,"$route")})}}function r(a){var e={route:a,hasRedirection:!1};if(a)if(a.redirectTo)if(b.isString(a.redirectTo))e.path=v(a.redirectTo,a.params),e.search=a.params,e.hasRedirection=!0;else{var d=c.path(),f=c.search();a=a.redirectTo(a.pathParams,d,f);b.isDefined(a)&&(e.url=a,e.hasRedirection=!0)}else if(a.resolveRedirectTo)return g.resolve(l.invoke(a.resolveRedirectTo)).then(function(a){b.isDefined(a)&&
(e.url=a,e.hasRedirection=!0);return e});return e}function w(a){var b=!0;if(a.route!==t.current)b=!1;else if(a.hasRedirection){var g=c.url(),d=a.url;d?c.url(d).replace():d=c.path(a.path).search(a.search).replace().url();d!==g&&(b=!1)}return b}function y(a){if(a){var e=b.extend({},a.resolve);b.forEach(e,function(a,c){e[c]=b.isString(a)?l.get(a):l.invoke(a,null,null,c)});a=z(a);b.isDefined(a)&&(e.$template=a);return g.all(e)}}function z(a){var e,c;b.isDefined(e=a.template)?b.isFunction(e)&&(e=e(a.params)):
b.isDefined(c=a.templateUrl)&&(b.isFunction(c)&&(c=c(a.params)),b.isDefined(c)&&(a.loadedTemplateUrl=q.valueOf(c),e=k(c)));return e}function A(){var a,e;b.forEach(h,function(d,g){var f;if(f=!e){var h=c.path();f=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,p=h.length;k<p;++k){var m=f[k-1],n=h[k];m&&n&&(l[m.name]=n)}f=l}else f=null;else f=null;f=a=f}f&&(e=u(d,{params:b.extend({},c.search(),a),pathParams:a}),e.$$route=d)});return e||h[null]&&u(h[null],{params:{},pathParams:{}})}function v(a,
c){var d=[];b.forEach((a||"").split(":"),function(a,b){if(0===b)d.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),g=f[1];d.push(c[g]);d.push(f[2]||"");delete c[g]}});return d.join("")}var B=!1,n,x,t={routes:h,reload:function(){B=!0;var a={defaultPrevented:!1,preventDefault:function(){this.defaultPrevented=!0;B=!1}};d.$evalAsync(function(){m(a);a.defaultPrevented||s()})},updateParams:function(a){if(this.current&&this.current.$$route)a=b.extend({},this.current.params,a),c.path(v(this.current.$$route.originalPath,
a)),c.search(a);else throw H("norout");}};d.$on("$locationChangeStart",m);d.$on("$locationChangeSuccess",s);return t}]}).run(A),H=b.$$minErr("ngRoute"),p;A.$inject=["$injector"];y.provider("$routeParams",function(){this.$get=function(){return{}}});y.directive("ngView",v);y.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular);
//# sourceMappingURL=angular-route.min.js.map

View File

@ -0,0 +1,10 @@
/*
AngularJS v1.7.5
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(t,p){'use strict';function q(g,h,s){n.directive(g,["$parse","$swipe",function(a,b){return function(c,e,f){function k(a){if(!d)return!1;var b=Math.abs(a.y-d.y);a=(a.x-d.x)*h;return l&&75>b&&0<a&&30<a&&.3>b/a}var m=a(f[g]),d,l,r=["touch"];p.isDefined(f.ngSwipeDisableMouse)||r.push("mouse");b.bind(e,{start:function(a,b){d=a;l=!0},cancel:function(a){l=!1},end:function(a,b){k(a)&&c.$apply(function(){e.triggerHandler(s);m(c,{$event:b})})}},r)}}])}var n=p.module("ngTouch",[]);n.info({angularVersion:"1.7.5"});
n.factory("$swipe",[function(){function g(a){a=a.originalEvent||a;var b=a.touches&&a.touches.length?a.touches:[a];a=a.changedTouches&&a.changedTouches[0]||b[0];return{x:a.clientX,y:a.clientY}}function h(a,b){var c=[];p.forEach(a,function(a){(a=n[a][b])&&c.push(a)});return c.join(" ")}var n={mouse:{start:"mousedown",move:"mousemove",end:"mouseup"},touch:{start:"touchstart",move:"touchmove",end:"touchend",cancel:"touchcancel"},pointer:{start:"pointerdown",move:"pointermove",end:"pointerup",cancel:"pointercancel"}};
return{bind:function(a,b,c){var e,f,k,m,d=!1;c=c||["mouse","touch","pointer"];a.on(h(c,"start"),function(a){k=g(a);d=!0;f=e=0;m=k;b.start&&b.start(k,a)});var l=h(c,"cancel");if(l)a.on(l,function(a){d=!1;b.cancel&&b.cancel(a)});a.on(h(c,"move"),function(a){if(d&&k){var c=g(a);e+=Math.abs(c.x-m.x);f+=Math.abs(c.y-m.y);m=c;10>e&&10>f||(f>e?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(c,a)))}});a.on(h(c,"end"),function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q("ngSwipeLeft",-1,"swipeleft");
q("ngSwipeRight",1,"swiperight")})(window,window.angular);
//# sourceMappingURL=angular-touch.min.js.map

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
<div>
<div class="sidebar">
<div class="sidebar-list">
<div class="sidebar-list-el">
Update applications <a ng-click="$ctrl.check()" class=
"btn btn-default sidebar-button">
<i class="fas fa-sync-alt" aria-hidden="true"></i>
</a>
</div>
<div class="sidebar-list-el">
Update fdroid repo <a ng-click="$ctrl.fdroid()" class=
"btn btn-default">
<i class="fab fa-android" aria-hidden="true"></i>
</a>
</div>
<div class="sidebar-list-el">
URL: <code>{{$ctrl.baseUrl}}/fdroid</code>
</div>
<div class="sidebar-list-el">
<strong>Last update:</strong> {{$ctrl.lastFdroidUpdate}}
</div>
</div>
</div>
<div class="app-container">
<div ng-repeat="app in $ctrl.apps" class="col-xs-12 col-md-12 col-lg-6">
<div class="panel panel-default">
<div class="panel-body">
<img ng-if="$ctrl.desktop" width="100" height="100" ng-src=
"{{app.previewImage[0].imageUrl}}">
<div class="apk-info">
<strong><span class=
"apk-item-title">{{app.title}}</span></strong>
<i ng-repeat="val in app.starList" ng-class=
"val.full ? 'fas fa-star' : 'far fa-star'"
aria-hidden="true"></i>
<strong>{{app.formattedStars}}</strong><br>
<span><strong>Developer:</strong> {{app.creator}}</span><br>
<span><strong>Version:</strong> {{app.details.appDetails.versionCode}}</span><br>
<span><strong>Files:</strong> {{app.details.appDetails.file.length}}</span><br>
<span ng-if="app.formattedSize !== undefined"><strong>Size:</strong> {{app.formattedSize}} MB</span>
<span ng-if="app.formattedSize === undefined"><strong>Size:</strong> unknown</span><br>
<span><strong>PackageId:</strong> {{app.docid}}</span><br>
</div>
</div><!-- panel-body -->
<div ng-hide="!app.updating" class="apk-progress">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role=
"progressbar" aria-valuenow="100" aria-valuemin="0"
aria-valuemax="100" style="width:100%">
<span class="sr-only">Updating</span>
</div><!-- progress-bar -->
</div><!-- progress -->
</div><!-- apk-progress -->
<div ng-hide="app.updating" class="apk-buttons">
<a ng-click="$ctrl.delete(app)" class=
"btn btn-raised btn-danger"> delete</a>
</div>
</div><!-- panel -->
</div><!-- main -->
</div>
</div>

View File

@ -0,0 +1,42 @@
<div class="view-container">
<form>
<div class="login-body">
<h2>First time initialization</h2>
<p>In order to initalize the server, you need to enter your google
credentials. They are sent to the server, which will perform the login
procedure and fetch an authorization token, and then they are
discarded. To secure communucation between client and server you should
configure playmaker with https, like explained <a href=
"https://github.com/NoMore201/playmaker#usage">here</a></p>
<div class="form-group">
<label for="emailInput">Email address</label> <input type="text"
class="form-control" id="emailInput" placeholder="Email" ng-model=
"user.email" uib-tooltip=
"Enter a valid username (with or without '@gmail.com')"
tooltip-enable="$ctrl.badUsername" tooltip-placement="top"
tooltip-is-open="$ctrl.badUsername" ng-disabled="$ctrl.loggingIn">
</div>
<div class="form-group">
<label for="passwordInput">Password</label> <input type="password"
class="form-control" id="passwordInput" placeholder="Password"
ng-model="user.password" uib-tooltip="Enter a valid password"
tooltip-enable="$ctrl.badPassword" tooltip-placement="bottom"
tooltip-is-open="$ctrl.badPassword" ng-disabled="$ctrl.loggingIn">
</div><button type="submit" class="btn btn-default" ng-click=
"$ctrl.login(user)" ng-disabled="$ctrl.loggingIn">Submit</button>
<a class="btn btn-warning"
href="https://accounts.google.com/b/0/DisplayUnlockCaptcha"
target="_blank"
ng-disabled="!$ctrl.securityCheck">Security check</a>
</div>
</form>
<div ng-hide="!$ctrl.loggingIn" class="row loading-login">
<div class="loading-login-text">
Loading apps into cache, please wait
</div>
<uib-progressbar class="progress-striped active" max="$ctrl.max" value=
"$ctrl.current"><span style=
"color:white; white-space:nowrap;">{{$ctrl.formattedPercent}}%</span>
</uib-progressbar>
</div>
</div>

View File

@ -0,0 +1,101 @@
<div class="view-container">
<div id="search-area" class="input-group input-group-lg">
<span class="input-group-addon" id="search-input-label">
<i class="fas fa-search"></i>
</span>
<input ng-model="searchString" on-enter="$ctrl.search(searchString)"
type="text" class="form-control" id="search-input" placeholder="Search"
aria-describedby="search-input-label">
</div>
<div ng-hide="!$ctrl.searching" id="search-progress" class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width:100%">
<span class="sr-only">Loading results</span>
</div>
</div><!-- progress -->
<div ng-if="$ctrl.desktop" class="row" id="table-box">
<table class="table" ng-hide="$ctrl.results.length === 0">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Developer</th>
<th>Id</th>
<th>Version</th>
<th>Size</th>
<th>Stars</th>
</tr>
</thead>
<tbody id="table-body">
<tr ng-repeat="app in $ctrl.results">
<td id="dl-button-td">
<a ng-click="$ctrl.download(app)" ng-class=
"{'dl-button-disabled': app.disabled, 'dl-button': !app.disabled}">
<div ng-hide="app.downloading" class="fa-2x">
<i class="fas fa-download"></i>
</div>
<div ng-hide="!app.downloading" class="fa-2x">
<i class="fas fa-cog fa-spin"></i>
</div>
</a>
</td>
<td>{{app.title}}</td>
<td>{{app.creator}}</td>
<td>{{app.docid}}</td>
<td>{{app.details.appDetails.versionCode}}</td>
<td>{{ ((app.details.appDetails.file[0].size)/(1024*1024)).toFixed(2) }} MB</td>
<td>{{app.aggregateRating.starRating.toFixed(2)}} Stars</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="$ctrl.mobile" class="row" id="table-box">
<script type="text/ng-template" id="myModalContent.html">
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{app.title}}</h3>
</div>
<div class="modal-body" id="modal-body">
<strong>Id:</strong> {{app.docid}}<br>
<strong>Developer:</strong> {{app.creator}}<br>
<strong>Version:</strong> {{app.details.appDetails.versionCode}}<br>
<strong>Size:</strong> {{ ((app.details.appDetails.file[0].size)/(1024*1024)).toFixed(2) }} MB<br>
<strong>Stars:</strong> {{app.aggregateRating.starRating.toFixed(2)}}
</div>
<div class="modal-footer">
<button class="btn btn-default" type="button" ng-click="$close()">Close</button>
</div>
</script>
<table class="table" ng-hide="$ctrl.results.length === 0">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Info</th>
</tr>
</thead>
<tbody id="table-body">
<tr ng-repeat="app in $ctrl.results">
<td id="dl-button-td">
<a ng-click="$ctrl.download(app)" ng-class=
"{'dl-button-disabled': app.disabled, 'dl-button': !app.disabled}">
<div ng-hide="app.downloading" class="fa-2x">
<i class="fas fa-download"></i>
</div>
<div ng-hide="!app.downloading" class="fa-2x">
<i class="fas fa-cog fa-spin"></i>
</div>
</a>
</td>
<td>{{app.title}}</td>
<td>
<a ng-click="$ctrl.modalOpen(app)" class="dl-button">
<i class="fas fa-info-circle fa-2x" data-toggle="popover"
data-html="true" title="" data-placement="left"
data-content="" aria-hidden="true"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div><!-- row -->
</div><!-- container -->

70
playmaker/pm-server Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
import argparse
from tornado import httpserver
from tornado import ioloop as io
import functools
import tornado_crontab
import os
import configparser
from playmaker.server import createServer
from playmaker.service import Play
def auto_update(service):
if service.loggedIn:
print('Executing auto update cron task')
apps = service.check_local_apks().get('message')
if len(apps) > 0:
service.download_selection(apps)
service.fdroid_update()
# tornado setup
if __name__ == '__main__':
# arguments parsing
ap = argparse.ArgumentParser(description='Apk and fdroid repository ' +
'manager with a web interface.')
ap.add_argument('-f', '--fdroid', dest='fdroid',
action='store_true', default=False,
help='Enable fdroid integration')
ap.add_argument('-d', '--debug', dest='debug',
action='store_true', default=False,
help='Enable debug output')
args = ap.parse_args()
service = Play(debug=args.debug, fdroid=args.fdroid)
app = createServer(service)
# server setup
certfile = os.environ.get('HTTPS_CERTFILE')
keyfile = os.environ.get('HTTPS_KEYFILE')
server = (httpserver.HTTPServer(app)
if certfile is None or keyfile is None else
httpserver.HTTPServer(app,
ssl_options={'certfile': certfile,
'keyfile': keyfile}))
server.listen(5000, address='0.0.0.0')
# credentials setup
auth_file_parser = configparser.ConfigParser()
auth_file_parser.read('credentials.conf')
if 'google' in auth_file_parser:
google_section = auth_file_parser['google']
if 'email' in google_section and 'password' in google_section:
service.set_credentials(google_section['email'], google_section['password'])
elif 'gsfId' in google_section and 'token' in google_section:
service.set_token_credentials(google_section['gsfId'], google_section['token'])
if service.has_credentials():
service.login()
service.update_state()
# cron task settings
cron_string = os.environ.get('CRONTAB_STRING')
if cron_string is None:
# default is every night at 2AM
cron_string = '0 2 * * *'
_func = functools.partial(auto_update, *[service])
tornado_crontab.CronTabCallback(_func, cron_string).start()
io.IOLoop.instance().start()

29
playmaker/setup.py Normal file
View File

@ -0,0 +1,29 @@
from setuptools import setup
setup(name='playmaker',
version='0.6.4',
description='Apk manager with web interface based on googleplay-api',
url='https://github.com/NoMore201/playmaker',
author='NoMore201',
author_email='domenico.iezzi.201@gmail.com',
license='MIT',
packages=['playmaker'],
package_data={
'playmaker': [
'index.html',
'static/*',
'static/css/*',
'static/fonts/*',
'static/js/*',
'views/*'
],
},
install_requires=[
'pyaxmlparser',
'pycryptodome',
'tornado<5',
'gpapi>=0.4.4',
'tornado-crontab'
],
scripts=['pm-server']
)