├── MARTA ├── martapi │ ├── __init__.py │ ├── exceptions.py │ ├── vehicles.py │ └── api.py └── settings.py ├── media_server ├── deluge │ ├── __init__.py │ ├── settings.py │ └── deluge.py ├── booksonic │ ├── __init__.py │ ├── settings.py │ └── booksonic.py ├── rclone │ ├── settings.py │ └── rclone.py ├── discordConnector.db ├── emby │ ├── __init__.py │ ├── settings.py │ └── emby_api.py ├── olaris │ ├── settings.py │ └── olaris_manager.py ├── jellyfin │ ├── jellyfin_stats.py │ ├── settings.py │ ├── jellyfin_recs.py │ ├── jellyfin.py │ └── jellyfin_api.py ├── migrate.sql └── plex │ ├── settings.py │ ├── plex_recs.py │ └── plex_manager_nodb.py ├── sports ├── yahoofantasy │ ├── __init__.py │ ├── settings.py │ └── yahoofantasy.py └── espn │ ├── logo.png │ ├── __init__.py │ └── info.json ├── discord_cogs ├── __init__.py ├── vc_gaming_manager.py └── roles.py ├── smart_home ├── sengled_lights │ ├── requirements.txt │ └── sengled.py ├── google_home │ └── google_home.py └── wink │ └── wink.py ├── requirements.txt ├── helper ├── helper_functions.py ├── cog_list.py ├── dropbox_handler.py ├── discord_helper.py ├── encryption.py ├── cog_handler.py ├── pastebin.py └── db_commands.py ├── settings.py ├── .gitignore ├── bot.py ├── general ├── coronavirus.py └── speedtest.py ├── README.md └── news └── news.py /MARTA/martapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_server/deluge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sports/yahoofantasy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_server/booksonic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MARTA/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | MARTA_API_KEY = os.environ.get('MARTA_API_KEY') -------------------------------------------------------------------------------- /media_server/rclone/settings.py: -------------------------------------------------------------------------------- 1 | configPath = 'media_server/rclone/rclone.conf' 2 | -------------------------------------------------------------------------------- /sports/espn/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwithan8/Arca/HEAD/sports/espn/logo.png -------------------------------------------------------------------------------- /sports/espn/__init__.py: -------------------------------------------------------------------------------- 1 | from .espn import ESPN 2 | 3 | def setup(bot): 4 | bot.add_cog(ESPN(bot)) 5 | -------------------------------------------------------------------------------- /media_server/discordConnector.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwithan8/Arca/HEAD/media_server/discordConnector.db -------------------------------------------------------------------------------- /media_server/emby/__init__.py: -------------------------------------------------------------------------------- 1 | from .emby import Emby 2 | 3 | 4 | def setup(bot): 5 | bot.add_cog(Emby(bot)) 6 | -------------------------------------------------------------------------------- /media_server/olaris/settings.py: -------------------------------------------------------------------------------- 1 | OLARIS_URL = '' 2 | JWT = '' 3 | 4 | ADMIN_USERNAME = '' 5 | ADMIN_PASSWORD = '' 6 | -------------------------------------------------------------------------------- /discord_cogs/__init__.py: -------------------------------------------------------------------------------- 1 | import discord_cogs.vc_gaming_manager as VC 2 | 3 | def setup(bot): 4 | VC.setup(bot) 5 | -------------------------------------------------------------------------------- /smart_home/sengled_lights/requirements.txt: -------------------------------------------------------------------------------- 1 | discord 2 | requests 3 | imdbpie 4 | mysql-connector-python 5 | asyncio 6 | PlexAPI 7 | -------------------------------------------------------------------------------- /sports/yahoofantasy/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CLIENT_ID = os.environ.get('FANTASY_BOT_ID') 4 | CLIENT_SECRET = os.environ.get('FANTASY_BOT_SECRET') -------------------------------------------------------------------------------- /media_server/deluge/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | WEB_URL = os.environ.get('DELUGE_URL') # Leave off '/json' 4 | WEBUI_PASSWORD = os.environ.get('DELUGE_PASS') 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | GitPython 3 | urllib3 4 | discord 5 | requests 6 | requests_cache 7 | imdbpie 8 | asyncio 9 | bs4 10 | feedparser 11 | PlexAPI 12 | progress 13 | lxml 14 | cryptography 15 | dropbox 16 | -------------------------------------------------------------------------------- /MARTA/martapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class APIKeyError(Exception): 2 | """Exception thrown for a missing API key""" 3 | def __init__(self, message=None): 4 | 5 | if not message: 6 | message = 'API Key is missing. Please set MARTA_API_KEY or use api_key kwarg.' 7 | super(Exception, self).__init__(message) 8 | -------------------------------------------------------------------------------- /helper/helper_functions.py: -------------------------------------------------------------------------------- 1 | def filesize(size): 2 | """ 3 | Convert bytes to kilobytes, megabytes, etc. 4 | :param size: 5 | :return: 6 | """ 7 | pf = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 8 | i = 0 9 | while size > 1024: 10 | i += 1 11 | size /= 1024 12 | return "{:.2f}".format(size) + " " + pf[i] + ("s" if size != 1 else "") 13 | 14 | 15 | def is_positive_int(n): 16 | return n.isdigit() 17 | -------------------------------------------------------------------------------- /media_server/booksonic/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BOOKSONIC_URL = os.environ.get('BOOKSONIC_URL') 4 | BOOKSONIC_USER = os.environ.get('BOOKSONIC_USER') 5 | BOOKSONIC_PASS = os.environ.get('BOOKSONIC_PASS') 6 | BOOKSONIC_SERVER_NAME = os.environ.get('BOOKSONIC_SERVER_NAME') 7 | 8 | ADMIN_ROLE_NAME = os.environ.get('BOOKSONIC_ADMIN_ROLE') 9 | 10 | DEFAULT_EMAIL = '' 11 | USE_DEFAULT_PASSWORD = False # If FALSE, random password generated each time 12 | DEFAULT_PASSWORD = '' 13 | -------------------------------------------------------------------------------- /sports/espn/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "nwithan8" 4 | ], 5 | "bot_version": [ 6 | 3, 7 | 0, 8 | 0 9 | ], 10 | "description": "Get live sports scores from ESPN.com", 11 | "hidden": true, 12 | "install_msg": "Thank you for installing ESPN! Get started with '[p]load espn', then '[p]help espn'", 13 | "requirements": [], 14 | "short": "Scores and stats from ESPN", 15 | "tags": [ 16 | "nwithan8", 17 | "utils", 18 | "sports" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /media_server/olaris/olaris_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interact with a Olaris Media Server, manage users 3 | Copyright (C) 2020 Nathan Harris 4 | """ 5 | 6 | import discord 7 | from discord.ext import commands, tasks 8 | import json 9 | import random 10 | import string 11 | import csv 12 | from datetime import datetime 13 | from media_server.olaris import settings as settings 14 | from media_server.olaris import olaris_api as olaris 15 | from helper.db_commands import DB 16 | from helper.pastebin import hastebin, privatebin 17 | import helper.discord_helper as discord_helper 18 | 19 | 20 | class OlarisManager(commands.Cog): 21 | def __init__(self, bot): 22 | self.bot = bot 23 | print("Olaris Manager ready to go.") 24 | 25 | 26 | def setup(bot): 27 | bot.add_cog(OlarisManager(bot)) 28 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | extensions = [ 4 | # 'plex', 5 | # 'plex_manager', 6 | # 'plex_manager_nodb', 7 | 'jellyfin' 8 | # 'jellyfin_manager', 9 | # 'emby_manager', 10 | # 'olaris_manager', 11 | # 'booksonic', 12 | # 'rclone', 13 | # 'news', 14 | # 'marta', 15 | # 'roles', 16 | # 'espn', 17 | # 'yahoo_fantasy', 18 | # 'sengled', 19 | # 'google_home', 20 | # 'wink', 21 | # 'coronavirus', 22 | # 'speedtest', 23 | # 'voice_channel' 24 | ] 25 | 26 | PREFIX = "*" 27 | BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") 28 | 29 | USE_DROPBOX = False 30 | # True: Can download/upload cogs from Dropbox 31 | # False: Cogs have to be local 32 | DROPBOX_API_KEY = os.environ.get('DROPBOX_API_KEY') 33 | 34 | USE_REMOTE_CONFIG = False 35 | # True: Load/store cogs from a remote "cogs.txt" file in Dropbox (will need to know folder.file of each) 36 | # False: Load cogs from the ext list below. 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Private configs 60 | config.json 61 | config.txt 62 | *oauth2.json 63 | -------------------------------------------------------------------------------- /helper/cog_list.py: -------------------------------------------------------------------------------- 1 | nicks_to_paths = { 2 | 'plex': "media_server.plex.plex", 3 | 'plex_manager': "media_server.plex.plex_manager", 4 | 'plex_manager_nodb': "media_server.plex.plex_namanger_nodb", 5 | 'jellyfin': 'media_server.jellyfin.jellyfin', 6 | 'jellyfin_manager': "media_server.jellyfin.jellyfin_manager", 7 | 'emby_manager': "media_server.emby.emby_manager", 8 | 'olaris_manager': "media_server.olaris.olaris_manager", 9 | 'booksonic': "media_server.booksonic.booksonic", 10 | 'rclone': "media_server.rclone.rclone", 11 | 'news': "news.news", 12 | 'marta': "MARTA.marta", 13 | 'roles': "discord_cogs.roles", 14 | 'espn': "sports.espn.espn", 15 | 'yahoo_fantasy': "sports.yahoofantasy.yahoofantasy", 16 | 'sengled': "smart_home.sengled_lights.sengled", 17 | 'google_home': "smart_home.google_home.google_home", 18 | 'wink': "smart_home.wink.wink", 19 | 'coronavirus': "general.coronavirus", 20 | 'speedtest': "general.speedtest", 21 | 'voice_channel': "discord_cogs.__init__" 22 | } 23 | -------------------------------------------------------------------------------- /media_server/jellyfin/jellyfin_stats.py: -------------------------------------------------------------------------------- 1 | from media_server.jellyfin import settings as settings 2 | from media_server.jellyfin import jellyfin_api as jf 3 | 4 | 5 | class HistoryItem: # use results from * 6 | def __init__(self, data): 7 | self.date = data[0] 8 | self.userId = data[1] 9 | self.itemId = data[2] 10 | self.itemType = data[3] 11 | self.itemName = data[4] 12 | self.method = data[5] 13 | self.client = data[6] 14 | self.device = data[7] 15 | self.durationSeconds = data[8] 16 | 17 | 18 | def getUserHistory(user_id, past_x_days: int = 0, sum_watch_time: bool = False): 19 | sql_statement = f"SELECT {'SUM(PlayDuration)' if sum_watch_time else '*'} FROM PlaybackActivity WHERE UserId = '{user_id}'" 20 | if past_x_days: 21 | sql_statement += f" AND DateCreated >= date(julianday(date('now'))-14)" 22 | query = { 23 | "CustomQueryString": sql_statement, 24 | "ReplaceUserId": "false"} 25 | data = jf.statsCustomQuery(query) 26 | if not data: 27 | if sum_watch_time: 28 | return 0 29 | return None 30 | if sum_watch_time: 31 | return data['results'][0][0] 32 | history = [HistoryItem(item) for item in data['results']] 33 | return history 34 | -------------------------------------------------------------------------------- /MARTA/martapi/vehicles.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class Vehicle(): 5 | """Generic Vehicle object that exists to print vehicles as dicts""" 6 | def __str__(self): 7 | return str(self.__dict__) 8 | 9 | 10 | class Bus(Vehicle): 11 | def __init__(self, record): 12 | self.adherence = record.get('ADHERENCE') 13 | self.block_id = record.get('BLOCKID') 14 | self.block_abbr = record.get('BLOCK_ABBR') 15 | self.direction = record.get('DIRECTION') 16 | self.latitude = record.get('LATITUDE') 17 | self.longitude = record.get('LONGITUDE') 18 | self.last_updated = datetime.strptime(record.get('MSGTIME'), '%m/%d/%Y %H:%M:%S %p') 19 | self.route = int(record.get('ROUTE')) 20 | self.stop_id = record.get('STOPID') 21 | self.timepoint = record.get('TIMEPOINT') 22 | self.trip_id = record.get('TRIPID') 23 | self.vehicle = record.get('VEHICLE') 24 | 25 | 26 | class Train(Vehicle): 27 | def __init__(self, record): 28 | self.destination = record.get('DESTINATION') 29 | self.direction = record.get('DIRECTION') 30 | self.last_updated = datetime.strptime(record.get('EVENT_TIME'), '%m/%d/%Y %H:%M:%S %p') 31 | self.line = record.get('LINE') 32 | self.next_arrival = datetime.strptime(record.get('NEXT_ARR'), '%H:%M:%S %p').time() 33 | self.station = record.get('STATION') 34 | self.train_id = record.get('TRAIN_ID') 35 | self.waiting_seconds = record.get('WAITING_SECONDS') 36 | self.waiting_time = record.get('WAITING_TIME') 37 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # import helper.log as log 4 | import discord 5 | from discord.ext import commands 6 | import helper.cog_handler as cog_handler 7 | import settings as settings 8 | 9 | bot = commands.Bot(command_prefix=settings.PREFIX) 10 | 11 | formatter = commands.HelpCommand(show_check_failure=False) 12 | 13 | exts = settings.extensions 14 | 15 | if settings.USE_REMOTE_CONFIG: 16 | settings.USE_DROPBOX = True 17 | # You can only use cog_handler if you fill out the DROPBOX_API_KEY environmental variable 18 | cog_handler.USE_REMOTE_CONFIG = True 19 | exts = cog_handler.load_remote_config("cogs.txt") 20 | for ext in exts: 21 | path = cog_handler.find_cog_path_by_name(ext) 22 | if path: 23 | # log.info(f"Loading {path}...") 24 | print(f"Loading {path}...") 25 | bot.load_extension(path) 26 | if settings.USE_DROPBOX: 27 | cog_handler.USE_DROPBOX = True # USE_DROPBOX has to be enabled for remote config to work 28 | bot.load_extension("helper.cog_handler") # Always enabled, never disabled 29 | 30 | 31 | @bot.event 32 | async def on_ready(): 33 | print(f'\n\nLogged in as : {bot.user.name} - {bot.user.id}\nVersion: {discord.__version__}\n') 34 | await bot.change_presence(status=discord.Status.idle, activity=discord.Game(name=f'the waiting game | {settings.PREFIX}')) 35 | print(f'Successfully logged in and booted...!\n') 36 | 37 | 38 | print("Arca Copyright (C) 2020 Nathan Harris\nThis program comes with ABSOLUTELY NO WARRANTY\nThis is free " 39 | "software, and you are welcome to redistribute it\nunder certain conditions.") 40 | 41 | bot.run(settings.BOT_TOKEN) 42 | -------------------------------------------------------------------------------- /media_server/migrate.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | 3 | ATTACH DATABASE 'plex/PlexDiscord.db' AS old_plex; 4 | CREATE TABLE IF NOT EXISTS plex ( 5 | DiscordID VARCHAR(100) NOT NULL, 6 | PlexUsername VARCHAR(100) NOT NULL, 7 | Email VARCHAR(100), 8 | ExpirationStamp INT(11), 9 | WhichPlexServer INT(11), 10 | WhichTautServer INT(11), 11 | PayMethod VARCHAR(5), 12 | SubType VARCHAR(5) 13 | ); 14 | INSERT INTO plex(DiscordID, PlexUsername, Email, ExpirationStamp, WhichPlexServer, WhichTautServer, PayMethod, SubType) SELECT DiscordID, PlexUsername, email, ExpirationStamp, whichPlexServer, whichTautServer, method, Note FROM old_plex.users; 15 | 16 | ATTACH DATABASE 'jellyfin/JellyfinDiscord.db' AS old_jellyfin; 17 | CREATE TABLE IF NOT EXISTS jellyfin ( 18 | DiscordID VARCHAR(100) NOT NULL, 19 | JellyfinUsername VARCHAR(100), 20 | JellyfinID VARCHAR(200) NOT NULL, 21 | ExpirationStamp INT(11), 22 | PayMethod VARCHAR(5), 23 | SubType VARCHAR(5) 24 | ); 25 | INSERT INTO jellyfin(DiscordID, JellyfinUsername, JellyfinID, ExpirationStamp, SubType) SELECT DiscordID, JellyfinUsername, JellyfinID, ExpirationStamp, Note FROM old_jellyfin.users; 26 | 27 | ATTACH DATABASE 'emby/EmbyDiscord.db' AS old_emby; 28 | CREATE TABLE IF NOT EXISTS emby ( 29 | DiscordID VARCHAR(100) NOT NULL, 30 | EmbyUsername VARCHAR(100), 31 | EmbyID VARCHAR(200) NOT NULL, 32 | ExpirationStamp INT(11), 33 | PayMethod VARCHAR(5), 34 | SubType VARCHAR(5) 35 | ); 36 | INSERT INTO emby(DiscordID, EmbyUsername, EmbyID, ExpirationStamp, SubType) SELECT DiscordID, EmbyUsername, EmbyID, ExpirationStamp, Note FROM old_emby.users; 37 | 38 | ATTACH DATABASE 'blacklist.db' AS old_blacklist; 39 | CREATE TABLE IF NOT EXISTS blacklist(IDorUsername VARCHAR(200)); 40 | INSERT INTO blacklist(IDorUsername) SELECT id_or_username FROM old_blacklist.blacklist; 41 | 42 | COMMIT; 43 | -------------------------------------------------------------------------------- /helper/dropbox_handler.py: -------------------------------------------------------------------------------- 1 | import dropbox 2 | import os 3 | import ntpath 4 | import settings as settings 5 | 6 | DB_KEY = settings.DROPBOX_API_KEY 7 | if DB_KEY: 8 | dbx = dropbox.Dropbox(DB_KEY) 9 | 10 | 11 | def download_file(filePath, toWhere=None): 12 | try: 13 | if toWhere: 14 | dbx.files_download_to_file('/{}'.format(filePath), toWhere) 15 | else: 16 | dbx.files_download('/{}'.format(filePath)) 17 | return True 18 | except FileNotFoundError: 19 | return False 20 | 21 | 22 | def upload_file(filePath, rename=False): 23 | try: 24 | with open(filePath, 'rb') as f: 25 | print("Uploading {} to Dropbox...".format(filePath)) 26 | if not rename: 27 | filename = ntpath.basename(filePath) 28 | dbx.files_upload(f.read(), '/{}'.format(rename if rename else filename), 29 | mode=dropbox.files.WriteMode('overwrite')) 30 | print("File uploaded") 31 | return True 32 | except Exception as e: 33 | print(e) 34 | return False 35 | 36 | 37 | def check_if_folder_exits(folderPath): 38 | try: 39 | dbx.files_get_metadata('/{}'.format(folderPath)) 40 | return True 41 | except Exception as e: 42 | print(e) 43 | return False 44 | 45 | 46 | def create_folder_path(folderPath): 47 | """ 48 | Ex. Create /home/2020/Spring folders 49 | """ 50 | try: 51 | folders = folderPath.split('/') 52 | from_root = "" 53 | for folder in folders: 54 | if not check_if_folder_exits("{}{}".format(from_root, folder)): 55 | dbx.files_create_folder("/{}{}".format(from_root, folder)) 56 | from_root += folder + "/" 57 | return True 58 | except Exception as e: 59 | print(e) 60 | return False 61 | -------------------------------------------------------------------------------- /helper/discord_helper.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | import asyncio 4 | 5 | 6 | def get_users_with_roles(bot, roleNames=[], guild=None, guildID=None): 7 | filtered_members = [] 8 | filtered_roles = [] 9 | if not guild and not guildID: 10 | raise Exception("'Server' and 'Server ID' cannot both be empty.") 11 | return None 12 | if guild: 13 | guildID = guild.id 14 | allRoles = bot.get_guild(int(guildID)).roles 15 | for role in allRoles: 16 | if role.name in roleNames: 17 | filtered_roles.append(role) 18 | for member in bot.get_guild(int(guildID)).members: 19 | if any(x in member.roles for x in filtered_roles): 20 | filtered_members.append(member) 21 | return filtered_members 22 | 23 | 24 | def get_users_without_roles(bot, roleNames=[], guild=None, guildID=None): 25 | filtered_members = [] 26 | filtered_roles = [] 27 | if not guild and not guildID: 28 | raise Exception("'Server' and 'Server ID' cannot both be empty.") 29 | return None 30 | if guild: 31 | guildID = guild.id 32 | allRoles = bot.get_guild(int(guildID)).roles 33 | for role in allRoles: 34 | if role.name in roleNames: 35 | filtered_roles.append(role) 36 | for member in bot.get_guild(int(guildID)).members: 37 | if not any(x in member.roles for x in filtered_roles): 38 | filtered_members.append(member) 39 | return filtered_members 40 | 41 | 42 | def user_has_role(ctx, user, role_name): 43 | """ 44 | Check if user has a role 45 | :param ctx: commands.Context 46 | :param user: User object 47 | :param role_name: str 48 | :return: True/False 49 | """ 50 | role = discord.utils.get(ctx.message.guild.roles, name=role_name) 51 | if role in user.roles: 52 | return True 53 | return False -------------------------------------------------------------------------------- /smart_home/google_home/google_home.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse Plex Media Server statistics via Tautulli's API 3 | Copyright (C) 2019 Nathan Harris 4 | """ 5 | 6 | import discord 7 | from discord.ext import commands, tasks 8 | from discord.utils import get 9 | import json 10 | import requests 11 | import os 12 | import asyncio 13 | import time 14 | import pychromecast as pc 15 | from gtts import gTTS 16 | from googlehomepush import GoogleHome 17 | from tempfile import TemporaryFile 18 | import socket 19 | 20 | audio_file_dir = "/var/www/html/home_audio" 21 | 22 | class GoogleHome(commands.Cog): 23 | 24 | cast = None 25 | ip = None 26 | 27 | def setup(self, name): 28 | chromecasts = pc.get_chromecasts() 29 | for cc in chromecasts: 30 | print(cc.device.friendly_name) 31 | if cc.device.friendly_name == name: 32 | cc.wait() 33 | self.cast = cc 34 | break 35 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 36 | s.connect(("8.8.8.8", 80)) 37 | self.ip = s.getsockname()[0] 38 | s.close() 39 | 40 | def play_tts(self, text, lang='en', slow=False): 41 | tts = gTTS(text=text, lang=lang, slow=slow) 42 | #f = TemporaryFile() 43 | #tts.write_to_fp(f) 44 | with open(audio_file_dir + '/test.mp3', 'wb') as f: 45 | tts.write_to_fp(f) 46 | self.cast.wait() 47 | mc = self.cast.media_controller.play_media('http://"{ip}""{path}"/test.mp3'.format( 48 | ip=self.ip, 49 | path=audio_file_dir 50 | ), 'audio/mp3') 51 | mc.block_until_active() 52 | f.close() 53 | 54 | def speak(self, name, text, lang='en-US'): 55 | GoogleHome(name).say(text, lang) 56 | 57 | @commands.command(name="speak", pass_context=True) 58 | async def google_home_say(self, ctx: commands.Context, *, text: str): 59 | """ 60 | Speak through Google Home 61 | """ 62 | self.play_tts(text) 63 | 64 | def __init__(self, bot): 65 | self.bot = bot 66 | self.setup('Bedroom speaker') 67 | #self.speak('Bedroom speaker', 'Hello') 68 | print("Google Home ready to go.") 69 | 70 | 71 | def setup(bot): 72 | bot.add_cog(GoogleHome(bot)) -------------------------------------------------------------------------------- /general/coronavirus.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get coronavirus data 3 | Copyright (C) 2019 Nathan Harris 4 | """ 5 | import discord 6 | from discord.ext import commands, tasks 7 | import requests 8 | from datetime import datetime 9 | 10 | 11 | def get_data(): 12 | json = requests.get('https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ncov_cases/FeatureServer' 13 | '/2/query?f=json&where=1%3D1&returnGeometry=false&spatialRel=esriSpatialRelIntersects' 14 | '&outFields=*&orderByFields=Confirmed%20desc&resultOffset=0&resultRecordCount=250&cacheHint' 15 | '=true').json() 16 | return [country['attributes'] for country in json['features']] 17 | 18 | 19 | def convert_to_length(text, length): 20 | text = str(text) 21 | print(text) 22 | print(len(text)) 23 | if length > len(text): 24 | text = text.rjust(length - len(text)) 25 | print(text) 26 | return text 27 | 28 | 29 | def make_list(data): 30 | timestamp = int(str(data[0]['Last_Update'])[:-3]) 31 | timestamp = datetime.fromtimestamp(timestamp).strftime('%B %d, %Y %H:%M') 32 | response = "Conavirus numbers (Updated {})\n\n".format(timestamp) 33 | response += "%40s|%10s|%7s|%10s\n\n" % ("Country", "Confirmed", "Death", "Recovered") 34 | for country in data: 35 | response += "%40s|%10s|%7s|%10s\n" % ( 36 | country['Country_Region'], str(country['Confirmed']), str(country['Deaths']), str(country['Recovered'])) 37 | return response 38 | 39 | 40 | class Coronavirus(commands.Cog): 41 | 42 | @commands.command(name="coronavirus", aliases=['corona', 'covid', 'covid19'], pass_content=True) 43 | async def coronavirus(self, ctx: commands.Context): 44 | """ 45 | Get global data on the coronavirus (via ArcGIS) 46 | """ 47 | data = get_data() 48 | if data: 49 | list = make_list(data) 50 | temp_list = "" 51 | for line in list.splitlines(): 52 | if len(temp_list) < 1800: 53 | temp_list += "\n" + line 54 | else: 55 | await ctx.send("```" + temp_list + "```") 56 | temp_list = "" 57 | await ctx.send("```" + temp_list + "```") 58 | else: 59 | await ctx.send("Sorry, I couldn't grab the latest numbers") 60 | 61 | @coronavirus.error 62 | async def coronavirus_error(self, ctx, error): 63 | print(error) 64 | await ctx.send("Something went wrong.") 65 | 66 | def __init__(self, bot): 67 | self.bot = bot 68 | print("Coronavirus ready to go!") 69 | 70 | 71 | def setup(bot): 72 | bot.add_cog(Coronavirus(bot)) 73 | -------------------------------------------------------------------------------- /discord_cogs/vc_gaming_manager.py: -------------------------------------------------------------------------------- 1 | # Thanks to https://github.com/windfreaker for this script 2 | 3 | from discord import ActivityType 4 | from discord.ext import commands 5 | 6 | 7 | def name_generator(channel): 8 | game_names = [] 9 | for member in channel.members: 10 | for activity in member.activities: 11 | if activity.type is ActivityType.playing: 12 | game_names.append(activity.name) 13 | if len(game_names) == 0: 14 | return 'No Game Detected' 15 | elif len(game_names) == 1: 16 | return game_names[0] 17 | else: 18 | counter = 0 19 | prev_name = game_names[0] 20 | for name in game_names: 21 | if channel.name.startswith(name): 22 | prev_name = name 23 | else: 24 | counter += 1 25 | if counter != 0: 26 | return f'{prev_name} + {str(counter)}' 27 | else: 28 | return prev_name 29 | 30 | 31 | async def channel_joined(member, channel): 32 | if activator_checklist(channel): 33 | new_name = name_generator(channel) 34 | await channel.clone(reason=str(member) + ' joined activator channel') 35 | await channel.edit(name=new_name, user_limit=0) 36 | elif activated_checklist(channel): 37 | updated_name = name_generator(channel) 38 | await channel.edit(name=updated_name) 39 | 40 | 41 | async def channel_left(member, channel): 42 | if activated_checklist(channel): 43 | if len(channel.members) == 0: 44 | await channel.delete(reason=str(member) + ' left activated channel') 45 | else: 46 | updated_name = name_generator(channel) 47 | await channel.edit(name=updated_name) 48 | 49 | 50 | def activator_checklist(channel): 51 | if channel is None: 52 | return False 53 | afk_vc = channel.guild.afk_channel 54 | if channel.bitrate == 64000 and channel.user_limit == 1 and channel.name == 'Join to Talk': 55 | if len(channel.members) != 1: 56 | return False 57 | elif afk_vc is None: 58 | return True 59 | elif channel.id != afk_vc.id: 60 | return True 61 | return False 62 | 63 | 64 | def activated_checklist(channel): 65 | if channel is None: 66 | return False 67 | if channel.bitrate == 64000 and channel.user_limit != 1 and channel.name != 'Join to Talk' and channel.category_id == 588935560400338964: 68 | return True 69 | return False 70 | 71 | 72 | @commands.Cog.listener() 73 | async def on_voice_state_update(member, before, after): 74 | await channel_left(member, before.channel) 75 | await channel_joined(member, after.channel) 76 | 77 | 78 | @commands.Cog.listener() 79 | async def on_member_update(before, after): 80 | if before.activities != after.activities: 81 | if after.voice is not None: 82 | if activated_checklist(after.voice.channel): 83 | updated_name = name_generator(after.voice.channel) 84 | await after.voice.channel.edit(name=updated_name) 85 | 86 | 87 | def setup(bot): 88 | bot.add_listener(on_voice_state_update) 89 | bot.add_listener(on_member_update) 90 | -------------------------------------------------------------------------------- /MARTA/martapi/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import requests_cache 3 | from json import loads 4 | from os import getenv 5 | from functools import wraps 6 | 7 | from .exceptions import APIKeyError 8 | from .vehicles import Bus, Train 9 | 10 | import MARTA.settings as settings 11 | 12 | _API_KEY = settings.MARTA_API_KEY 13 | _CACHE_EXPIRE = 30 14 | _BASE_URL = 'http://developer.itsmarta.com' 15 | _TRAIN_PATH = '/RealtimeTrain/RestServiceNextTrain/GetRealtimeArrivals' 16 | _BUS_PATH = '/BRDRestService/RestBusRealTimeService/GetAllBus' 17 | _BUS_ROUTE_PATH = '/BRDRestService/RestBusRealTimeService/GetBusByRoute/' 18 | 19 | # requests_cache.install_cache('marta_api_cache', backend='sqlite', expire_after=_CACHE_EXPIRE) 20 | 21 | 22 | def require_api_key(func): 23 | """ 24 | Decorator to ensure an API key is present 25 | """ 26 | @wraps(func) 27 | def with_key(*args, **kwargs): 28 | api_key = kwargs.get('api_key') 29 | if not api_key and not _API_KEY: 30 | raise APIKeyError() 31 | kwargs['api_key'] = api_key if api_key else _API_KEY 32 | return func(*args, **kwargs) 33 | return with_key 34 | 35 | 36 | @require_api_key 37 | def get_trains(line=None, station=None, destination=None, api_key=None): 38 | """ 39 | Query API for train information 40 | :param line (str): train line identifier filter (red, gold, green, or blue) 41 | :param station (str): train station filter 42 | :param destination (str): destination filter 43 | :param api_key (str): API key to override environment variable 44 | :return: list of Train objects 45 | """ 46 | endpoint = '{}{}?apikey={}'.format(_BASE_URL, _TRAIN_PATH, api_key) 47 | response = requests.get(endpoint) 48 | 49 | if response.status_code == 401 or response.status_code == 403: 50 | raise APIKeyError('Your API key seems to be invalid. Try visiting {}.'.format(endpoint)) 51 | 52 | data = loads(response.text) 53 | trains = [Train(t) for t in data] 54 | 55 | # We only want results that match our criteria. This is done in a single list comprehension 56 | # to avoid iterating over the response multiple times. 57 | # Read as, for example, "if line parameter is specified, only include results for that line" 58 | # TODO: Make this filtering more intuitive. 59 | trains = [t for t in trains if 60 | (t.line.lower() == line.lower() if line is not None else True) and 61 | (t.station.lower() == station.lower() if station is not None else True) and 62 | (t.destination.lower() == destination.lower() if destination is not None else True)] 63 | 64 | return trains 65 | 66 | 67 | @require_api_key 68 | def get_buses(route=None, api_key=None): 69 | """ 70 | Query API for bus information 71 | :param route (int): route number 72 | :param api_key (str): API key to override environment variable 73 | :return: list of Bus objects 74 | """ 75 | if route is not None: 76 | url = '{}{}/{}?apikey={}'.format(_BASE_URL, _BUS_ROUTE_PATH, str(route), api_key) 77 | else: 78 | url = '{}{}?apikey={}'.format(_BASE_URL, _BUS_PATH, _API_KEY) 79 | 80 | response = requests.get(url) 81 | 82 | if response.status_code == 401 or response.status_code == 403: 83 | raise APIKeyError('Your API key seems to be invalid. Try visiting {}.'.format(url)) 84 | 85 | data = loads(response.text) 86 | return [Bus(b) for b in data] 87 | -------------------------------------------------------------------------------- /helper/encryption.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | import os 3 | from os.path import exists 4 | 5 | 6 | def getRawKey(key_file): 7 | return readFromFile(key_file) 8 | 9 | 10 | def getKey(key_file): 11 | """ 12 | WARNING: New key will be made (and potentially overwrite old file) if key cannot be loaded 13 | """ 14 | try: 15 | key = readFromFile(key_file) 16 | return Fernet(key) 17 | except Exception as e: 18 | print("Could not locate encryption key. Creating a new one...") 19 | key = makeKey() 20 | saveKey(key, key_file) 21 | return Fernet(key) 22 | 23 | 24 | def splitPath(file_path): 25 | return '/'.join(file_path.split('/')[:-1]) 26 | 27 | 28 | def makePath(file_path): 29 | working_path = splitPath(file_path) 30 | if not os.path.exists(working_path): 31 | os.makedirs(working_path) 32 | 33 | 34 | def makeKey(): 35 | return Fernet.generate_key() 36 | 37 | 38 | def saveKey(key, filename): 39 | writeToFile(text=key.decode('utf-8'), filename=filename) 40 | 41 | 42 | def writeToFile(text, filename): 43 | makePath(filename) 44 | f = open(filename, 'w+') 45 | f.write(text) 46 | f.close() 47 | 48 | 49 | def readFromFile(filename): 50 | with open(filename, 'r') as f: 51 | text = f.read() 52 | return text 53 | 54 | 55 | def backupFile(filename): 56 | copyFile(filename, '{}.bk'.format(filename)) 57 | 58 | 59 | def copyFile(filename, new_filename): 60 | text = readFromFile(filename) 61 | writeToFile(text=text, filename=new_filename) 62 | 63 | 64 | class Encryption: 65 | def __init__(self, key=None, key_file=None): 66 | if key: 67 | self.key = Fernet(key) 68 | self.key_file = key_file 69 | if self.key_file: 70 | self.key = getKey(self.key_file) # Fernet object 71 | 72 | def encryptText(self, text): 73 | token = self.key.encrypt(bytes(text, encoding='utf8')) 74 | # return token.encode('unicode_escape') 75 | return token.decode('utf-8') 76 | 77 | def decryptText(self, text): 78 | text = self.key.decrypt(bytes(text, encoding='utf8')) 79 | # return text.encode('unicode_escape') 80 | return text.decode('utf-8') 81 | 82 | def encryptFile(self, text, filename): 83 | text = self.encryptText(text) 84 | writeToFile(text=text, filename=filename) 85 | 86 | def encryptFileInPlace(self, filename): 87 | text = readFromFile(filename) 88 | os.remove(filename) 89 | self.encryptFile(text=text, filename=filename) 90 | 91 | def decryptFileInPlace(self, filename): 92 | text = self.decryptFile(filename=filename) 93 | os.remove(filename) 94 | writeToFile(text=text, filename=filename) 95 | 96 | def decryptFile(self, filename): 97 | text = readFromFile(filename=filename) 98 | return self.decryptText(text) 99 | 100 | def makeTemporaryFile(self, permFileName, tempFileName): 101 | text = readFromFile(permFileName) 102 | text = self.decryptText(text) 103 | writeToFile(text=text, filename=tempFileName) 104 | 105 | def backToPermFile(self, permFileName, tempFileName, deleteTempFile=False): 106 | text = readFromFile(tempFileName) 107 | text = self.encryptText(text) 108 | writeToFile(text=text, filename=permFileName) 109 | if deleteTempFile: 110 | os.remove(tempFileName) 111 | -------------------------------------------------------------------------------- /media_server/emby/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Emby settings 4 | EMBY_URL = '' 5 | EMBY_API_KEY = '' 6 | EMBY_ADMIN_USERNAME = '' 7 | EMBY_ADMIN_PASSWORD = '' 8 | EMBY_SERVER_NICKNAME = '' 9 | EMBY_USER_POLICY = { 10 | "IsAdministrator": "false", 11 | "IsHidden": "true", 12 | "IsHiddenRemotely": "true", 13 | "IsDisabled": "false", 14 | "EnableRemoteControlOfOtherUsers": "false", 15 | "EnableSharedDeviceControl": "false", 16 | "EnableRemoteAccess": "true", 17 | "EnableLiveTvManagement": "false", 18 | "EnableLiveTvAccess": "false", 19 | "EnableContentDeletion": "false", 20 | "EnableContentDownloading": "false", 21 | "EnableSyncTranscoding": "false", 22 | "EnableSubtitleManagement": "false", 23 | "EnableAllDevices": "true", 24 | "EnableAllChannels": "false", 25 | "EnablePublicSharing": "false", 26 | "InvalidLoginAttemptCount": 5, 27 | "BlockedChannels": [ 28 | "IPTV", 29 | "TVHeadEnd Recordings" 30 | ] 31 | } 32 | 33 | # Discord-to-Jellyfin database (SQLite3) 34 | SQLITE_FILE = 'media_server/discordConnector.db' # File path + name + extension (i.e. "/root/Arca/media_server/discordConnector.db" 35 | ''' 36 | Database schema: 37 | EmbyDiscord.users 38 | 0|DiscordID|VARCHAR(100)|1||0 39 | 1|EmbyUsername|VARCHAR(100)|0||0 40 | 2|EmbyID|VARCHAR(200)|1||0 41 | 3|ExpirationStamp|INT(11)|0||0 42 | 4|Note|VARCHAR(5)|0||0 43 | ''' 44 | ENABLE_BLACKLIST = True 45 | 46 | USE_DROPBOX = False # Store database in Dropbox, download and upload dynamically 47 | 48 | # Discord settings 49 | DISCORD_SERVER_ID = '' 50 | DISCORD_ADMIN_ID = '' # Presumably you, or whoever is the administrator of the Discord server 51 | DISCORD_ADMIN_ROLE_NAME = "Admin" # Only users with this role can call most administrative commands 52 | AFTER_APPROVED_ROLE_NAME = "Invited" # Role given after someone is added to Jellyfin 53 | 54 | SUB_ROLES = ["Monthly Subscriber", "Yearly Subscriber", "Winner", "Bot"] # Users with any of these roles is exempt from removal 55 | EXEMPT_SUBS = [DISCORD_ADMIN_ID] # Discord IDs for users exempt from subscriber checks/deletion, separated by commas 56 | SUB_CHECK_TIME = 7 # days 57 | 58 | # Trial settings 59 | TRIAL_ROLE_NAME = "Trial Member" # Role given to a trial user 60 | TRIAL_LENGTH = 24 # (hours) How long a trial lasts 61 | TRIAL_CHECK_FREQUENCY = 15 # (minutes) How often the bot checks for trial expirations 62 | TRIAL_END_NOTIFICATION = "Hello, your {}-hour trial of {} has ended.".format(str(TRIAL_LENGTH), 63 | str(EMBY_SERVER_NICKNAME)) 64 | 65 | # Winner settings 66 | WINNER_ROLE_NAME = "Winner" # Role given to a winner 67 | WINNER_THRESHOLD = 7200 # (seconds) How long a winner has to use every WEEK to keep access 68 | AUTO_WINNERS = False 69 | # True: Messages from the indicated GIVEAWAY_BOT_ID user will be scanned for mentioned Discord users (winners). The 70 | # winners will be auto-assigned the TEMP_WINNER_ROLE_NAME. That role gives them access to a specified WINNER_CHANNEL 71 | # channel Users then post their Jellyfin username (ONLY their Jellyfin username) in the channel, which is processed 72 | # by the bot. The bot invites the Jellyfin username, and associates the Discord user author of the message with the 73 | # Jellyfin username in the database. The user is then have the TEMP_WINNER_ROLE_NAME role removed (which removes them 74 | # from the WINNER_CHANNEL channel), and assigned the final WINNER_ROLE_NAME role. 75 | TEMP_WINNER_ROLE_NAME = "Uninvited Winner" # Role temporarily given to a winner (used if AUTO_WINNERS = True) 76 | WINNER_CHANNEL = 0 # Channel ID 77 | GIVEAWAY_BOT_ID = 0 # User ID for the Giveaway Bot that announces contest winners 78 | 79 | # Credentials settings 80 | CREATE_PASSWORD = True # Create a random password for new Jellyfin users (or else, keep a blank password) 81 | NO_PASSWORD_MESSAGE = "Leave password blank on first login, but please secure your account by setting a password." 82 | USE_PASTEBIN = None # 'privatebin', 'hastebin' or None 83 | PRIVATEBIN_URL = '' 84 | HASTEBIN_URL = '' 85 | 86 | # Migrate/mass import users 87 | MIGRATION_FILE = "/" # file path + name (leave off ".csv" extension) 88 | -------------------------------------------------------------------------------- /media_server/deluge/deluge.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse RSS feeds for major online media outlets 3 | Copyright (C) 2019 Nathan Harris 4 | """ 5 | 6 | from discord.ext import commands 7 | import discord 8 | import requests 9 | import json 10 | import media_server.deluge.settings as settings 11 | 12 | WEBUI_PASSWORD = settings.WEBUI_PASSWORD 13 | WEB_URL = settings.WEB_URL + '/json' 14 | 15 | header = {"Accept": "application/json", "Content-Type": "application/json"} 16 | 17 | 18 | class Deluge(commands.Cog): 19 | session = requests.Session() 20 | 21 | def post(self, method, params, use_cookies): 22 | """ 23 | Params: String method, List params, Bool use_cookies 24 | Returns: Requests.Response, Bool status_code 25 | """ 26 | data = {"method": method, "params": params, "id": 1} 27 | r = self.session.post(WEB_URL, headers=header, data=json.dumps(data), 28 | cookies=(self.session.cookies if use_cookies == True else None)) 29 | if str(r.status_code).startswith("2"): 30 | return r, True 31 | else: 32 | return None, False 33 | 34 | def login(self): 35 | # login 36 | login, passed = self.post("auth.login", [WEBUI_PASSWORD], False) 37 | if passed: 38 | self.session.cookies = login.cookies 39 | # verify connection 40 | connection, passed = self.post("web.connected", [], 'True') 41 | if passed and json.loads(connection.text)['result'] == 'True': 42 | return True 43 | return False 44 | 45 | def get_torrents(self): 46 | """ 47 | Params: None 48 | Returns: JSON-formatted Request.Response 49 | """ 50 | r, passed = self.post("core.get_torrents_status", [{}, ""], True) 51 | if passed: 52 | return json.loads(r.text) 53 | return None 54 | 55 | @commands.group(pass_context=True, case_insensitive=True) 56 | async def deluge(self, ctx: commands.Context): 57 | """ 58 | Interact with Deluge 59 | """ 60 | if ctx.invoked_subcommand is None: 61 | await ctx.send("What subcommand?") 62 | 63 | @deluge.command(name="torrents", pass_context=True, case_insensitive=True) 64 | async def deluge_torrents(self, ctx: commands.Context): 65 | """ 66 | Get Deluge torrents 67 | """ 68 | torrents = "```" 69 | data = self.get_torrents() 70 | for k, v in data['result'].items(): 71 | if v['ratio'] not in ['-1', '0']: 72 | if len(torrents) > 1800: 73 | await ctx.send(torrents + "```") 74 | torrents = "```" 75 | else: 76 | torrents += "#{queue} | {ratio} | {name} | {id})\n".format(queue=v['queue'], ratio=v['ratio'], 77 | name=v['name'], id=v['hash']) 78 | await ctx.send(torrents + "```") 79 | 80 | @deluge.command(name="active", pass_context=True) 81 | async def deluge_active(self, ctx: commands.Context): 82 | """ 83 | Get active Deluge torrents 84 | """ 85 | torrents = "```Queue\t | Progress\t | Name\n" 86 | data = self.get_torrents() 87 | for k, v in data['result'].items(): 88 | if float(v['progress']) > 0.0 and float(v['progress']) < 100.0: 89 | if len(torrents) > 1800: 90 | await ctx.send(torrents + "```") 91 | torrents = "```" 92 | else: 93 | torrents += "#{queue}\t | {progress}%\t | {name}\n".format(queue=v['queue'], 94 | progress=("%.2f" % v['progress']), 95 | name=v['name']) 96 | await ctx.send(torrents + "```") 97 | 98 | def __init__(self, bot): 99 | self.bot = bot 100 | self.login() 101 | print("Deluge ready.") 102 | 103 | 104 | def setup(bot): 105 | bot.add_cog(Deluge(bot)) 106 | -------------------------------------------------------------------------------- /general/speedtest.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | import asyncio 4 | import re 5 | from asyncio import ensure_future, gather, get_event_loop, sleep 6 | from collections import deque 7 | from statistics import mean 8 | from time import time 9 | 10 | from aiohttp import ClientSession 11 | 12 | MIN_DURATION = 7 13 | MAX_DURATION = 30 14 | STABILITY_DELTA = 2 15 | MIN_STABLE_MEASUREMENTS = 6 16 | 17 | total = 0 18 | done = 0 19 | sessions = [] 20 | 21 | """ Speedtest.net """ 22 | 23 | # Coming soon 24 | 25 | """ Fast.com """ 26 | 27 | 28 | async def run(msg): 29 | token = await get_token() 30 | urls = await get_urls(token) 31 | conns = await warmup(urls) 32 | future = ensure_future(measure(conns)) 33 | result = await progress(future, msg) 34 | await cleanup() 35 | return result 36 | 37 | 38 | async def get_token(): 39 | async with ClientSession() as s: 40 | resp = await s.get('https://fast.com/') 41 | text = await resp.text() 42 | script = re.search(r'