Browse Source

first commit

thezero 3 years ago
parent
commit
56509dcb0c
8 changed files with 296 additions and 1 deletions
  1. 132 0
      .gitignore
  2. 5 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 20 1
      README.md
  5. 10 0
      docker-compose.yml
  6. 2 0
      requirements.txt
  7. 47 0
      src/download_utils.py
  8. 59 0
      src/main.py

+ 132 - 0
.gitignore

@@ -0,0 +1,132 @@
+out/
+conf/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/

+ 5 - 0
Dockerfile

@@ -0,0 +1,5 @@
+FROM python:3.8-slim-buster
+WORKDIR /bot
+COPY requirements.txt .
+RUN pip3 install -r requirements.txt
+COPY src src

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 TheZero
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 20 - 1
README.md

@@ -1,2 +1,21 @@
-# telegram-apk-downloader
+# telegram-bot-youtube-downloader
 
+Set in your environment the following variables:
+  - `BOT_TOKEN` telegram token for your bot
+  - `CONF_FOLDER` conf folder with the `gplaycli.conf` file and the `token.cache` file
+
+Usage:
+  - Send link of android app or package name to the bot
+  - The bot will download the APK and send it
+      - If the APK is larger than 50MB, it is split into smaller parts, 
+        which then need to be concatenated (in linux: cat file.apk* > file.apk)
+
+This script require:
+  - Python3 interpreter
+  - telegram python api https://github.com/python-telegram-bot/python-telegram-bot
+  - gplaycli https://github.com/matlink/gplaycli/ (installed on the machine)
+
+Alternatively, use docker/docker-compose
+
+Tips:
+  - Use PythonAnyWhere for hosting the bot https://www.pythonanywhere.com

+ 10 - 0
docker-compose.yml

@@ -0,0 +1,10 @@
+version: "3.3"
+services:
+  bot:
+    build: .
+    command: python3 /bot/src/main.py
+    volumes:
+      - ./out:/bot/out
+      - ./conf:/bot/conf
+    environment:
+      - CONF_FOLDER=/bot/conf/

+ 2 - 0
requirements.txt

@@ -0,0 +1,2 @@
+gplaycli==3.29
+python-telegram-bot==13.0

+ 47 - 0
src/download_utils.py

@@ -0,0 +1,47 @@
+import os
+from glob import glob, escape
+from contextlib import contextmanager
+
+from gplaycli.gplaycli import GPlaycli
+
+class BadPackageNameException(Exception):
+    pass
+
+class dotdict(dict):
+    """dot.notation access to dictionary attributes"""
+    __getattr__ = dict.get
+    __setattr__ = dict.__setitem__
+    __delattr__ = dict.__delitem__
+
+class APK:
+    def __init__(self, apk, conf={}, conf_file=None):
+        self.conf = conf
+        self.conf_file = conf_file
+        if 'play.google.com/store/apps/details?id=' in apk:
+            self.package_name = apk.split('play.google.com/store/apps/details?id=')[1]
+        else:
+            self.package_name = apk
+
+    def download(self):
+        cli = GPlaycli(args=dotdict(self.conf), config_file=self.conf_file)
+        cli.download_folder = 'out'
+        d = cli.download([self.package_name])  # returns the number of downloaded packages
+        if len(d) == 0:
+            raise BadPackageNameException(f'error downloading {self.package_name}')
+
+        fname = os.path.join('out', self.package_name + '.apk')
+        if os.path.isfile(fname):
+            self.file_name = fname
+
+    def check_dimension(self):
+        if os.path.getsize(self.file_name) > 50 * 1024 * 1023:
+            os.system('split -b 49M "{0}" "{1}"'.format(self.file_name, self.file_name))
+            os.remove(self.file_name)
+        return glob(escape(self.file_name) + '*')
+
+    @contextmanager
+    def send(self):
+        files = self.check_dimension()  # split if size >= 50MB
+        yield files
+        for f in files:  # removing old files
+            os.remove(f)

+ 59 - 0
src/main.py

@@ -0,0 +1,59 @@
+import logging
+import os
+
+from telegram.ext import Updater, MessageHandler, Filters
+
+from download_utils import APK, BadPackageNameException
+
+logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def get_format(update, context):
+    logger.info("from {}: {}".format(update.message.chat_id, update.message.text))  # "history"
+
+    if update.message.text == '/start':
+        update.message.reply_text("To start downloading APK file send me a PlayStore link ok a package name. :)")
+        return
+
+    conf = {
+        'yes': True,
+        'verbose': False,
+        'tokencachefile': os.path.join(os.environ['CONF_FOLDER'], 'token.cache'),
+
+    }
+    apk = APK(update.message.text, conf=conf, conf_file=os.path.join(os.environ['CONF_FOLDER'], 'gplaycli.conf'))
+
+    msg = context.bot.send_message(text="Downloading...",
+                                   chat_id=update.message.chat_id)
+    try:
+        apk.download()
+    except BadPackageNameException as e:
+        # instead of editing the text message we delete and resend one so a new notification will trigger
+        context.bot.delete_message(chat_id=update.message.chat_id,
+                                   message_id=msg.message_id)
+        context.bot.send_message(text="Bad package name: {}".format(e),
+                                 chat_id=update.message.chat_id)
+        return
+
+    with apk.send() as files:
+        for f in files:
+            context.bot.send_document(chat_id=update.message.chat_id, document=open(f, 'rb'))
+
+    context.bot.delete_message(chat_id=update.message.chat_id,
+                               message_id=msg.message_id)
+
+
+if os.environ.get('CONF_FOLDER') is None:
+    logger.error("No conf folder available")
+    exit(1)
+TOKEN = None
+with open(os.path.join(os.environ['CONF_FOLDER'], 'bot.token')) as f:
+    TOKEN = f.read().strip()
+logger.error(TOKEN)
+updater = Updater(token=TOKEN)
+
+updater.dispatcher.add_handler(MessageHandler(Filters.text, get_format))
+
+updater.start_polling()
+updater.idle()