class RichPressence: def __init__(self, client): self.rpc = Presence(client) # Initialize the client class self.rpc.connect() # Start the handshake loop def set_presence(self, pid, state, detail, large_image=None, starttime=None, limit=5): try: # while True: if large_image and starttime: self.rpc.update(state=state, large_image=large_image, start=starttime, details=detail, pid=pid) # Set the presence elif large_image: self.rpc.update(state=state, large_image=large_image, details=detail, pid=pid) # Set the presence elif starttime: self.rpc.update(state=state, start=starttime, details=detail, pid=pid) # Set the presence else: self.rpc.update(state=state, details=detail, pid=pid) # Set the presence # time.sleep(limit) except Exception as e: print(e) self.rpc.close() # close connection exit() def clear(self, pid): try: self.rpc.clear(pid) # close connection except Exception as e: print(e) pass
class Discord: def __init__(self, id): self.id = id self.rpc = Presence(id) self.cleared = True self.rpc.connect() def clear(self): if self.cleared == False: print("Clearing status") self.rpc.clear() self.cleared = True def setTrack(self, track): print("Changing track to: " + track.track + " by " + track.artist + "(" + track.artist.lower().replace(' ', '_') + ")") self.cleared = False self.rpc.update( state="By " + track.artist, details=track.track, start=int(track.start), large_image=track.artist.lower().replace(' ', '_'), large_text="Apple Music", small_image="logo", small_text="github.com/leahlundqvist")
class MyClass(): def __init__(self, client_id): self.client_id = client_id self.RPC = Presence(self.client_id) def connect(self): try: self.RPC.connect() return True except Exception as e: print(str(e)) return False def statusUpdate(self, state): try: self.RPC.update(details=state['details'], state=state["state"], start=state["startTimestamp"], large_image=state['largeImageKey'], large_text=state["largeImageText"], small_image=state['smallImageKey'], small_text=state["smallImageText"], buttons=state["buttons"]) except Exception as e: print(str(e)) def close_presence(self): try: self.RPC.clear(pid=os.getpid()) except Exception as e: print(str(e))
def connect_to_discord(): '''Connection code for Discord. Also clears the current presence in case the app is restarted and Discord still assumes the old state.''' global p #Use the global presence variable p = Presence(client_id=CLIENT_ID) logging.info("Connecting to Discord...") p.connect() logging.info("Connected.") logging.debug("Clearing presence...") p.clear() #Clear the presence logging.debug("Presence cleared.")
def main(): VERSION = "1.1.0"; config = GetConfig(); mpdc = MPDClient(); mpdc.timeout = int(config["General"]["MPDtimeout"]); mpdc.idletimeout = int(config["General"]["MPDtimeout_idle"]); try: mpdc.connect(config["General"]["MPDip"], port=int(config["General"]["MPDport"])); except ConnectionRefusedError: print(f"{TermColors.ERROR}ERROR!!! Either mpd isn't running or you have a mistake in the config. Fix ASAP! Exiting!{TermColors.END}"); exit(-1); RPC = Presence(710956455867580427); RPC.connect(); if (psys() == "Windows"): ossys("cls"); else: ossys("clear"); print(f"{TermColors.WORKS}MPDDRP v.{VERSION} - https://github.com/AKurushimi/mpddrp{TermColors.END}"); try: while True: statusout = mpdc.status(); csout = mpdc.currentsong(); if (statusout["state"] != "stop"): title = csout["title"]; artist = csout["artist"]; album = csout["album"]; timevar = statusout["time"].split(":"); timenow = str(timedelta(seconds=int(timevar[0]))); timeall = str(timedelta(seconds=int(timevar[1]))); if (statusout["state"] == "pause"): RPC.update(details=title + " | " + album, state="Paused | " + artist, large_image="mpdlogo", small_image="pause", small_text="Paused"); elif (statusout["state"] == "stop"): RPC.update(details="Stopped | MPD", state=" ", large_image="mpdlogo", small_image="stop", small_text="Stopped"); elif (statusout["state"] == "play"): RPC.update(details=title + " | " + album, state=timenow + "/" + timeall + " | " + artist, large_image="mpdlogo", small_image="play", small_text="Playing"); sleep(1); except (RuntimeError): pass; except (KeyboardInterrupt, SystemExit): RPC.clear(); RPC.close(); exit(0);
class Discord: def __init__(self, id): self.id = id self.rpc = Presence(id) self.cleared = True self.rpc.connect() def clear(self): if self.cleared == False: print("Clearing status") self.rpc.clear() self.cleared = True def setTrack(self, track): print("Changing track to: " + track.track + " by " + track.artist) self.cleared = False self.rpc.update(state="By " + track.artist, details=track.track, start=int(track.start), large_image="logo", large_text="Apple Music", small_image="github", small_text="github/memeone/amrpc")
class DiscordPresence(object): """Provide rich presence integration with Discord for games""" def __init__(self): self.available = bool(PyPresence) self.game_name = "" self.runner_name = "" self.last_rpc = 0 self.rpc_interval = 60 self.presence_connected = False self.rpc_client = None self.client_id = None self.custom_game_name = '' self.show_runner = True self.custom_runner_name = '' self.rpc_enabled = True def connect(self): """Make sure we are actually connected before trying to send requests""" logger.debug("Ensuring connected.") if self.presence_connected: logger.debug("Already connected!") else: logger.debug("Creating Presence object.") self.rpc_client = PyPresence(self.client_id) try: logger.debug("Attempting to connect.") self.rpc_client.connect() self.presence_connected = True except (ConnectionError, FileNotFoundError): logger.error("Could not connect to Discord") return self.presence_connected def disconnect(self): """Ensure we are definitely disconnected and fix broken event loop from pypresence That method is a huge mess of non-deterministic bs and should be nuked from orbit. """ logger.debug("Disconnecting from Discord") if self.rpc_client: try: self.rpc_client.close() except Exception as e: logger.exception("Unable to close Discord RPC connection: %s", e) if self.rpc_client.sock_writer is not None: try: logger.debug("Forcefully closing sock writer.") self.rpc_client.sock_writer.close() except Exception: logger.exception("Sock writer could not be closed.") try: logger.debug("Forcefully closing event loop.") self.rpc_client.loop.close() except Exception: logger.debug("Could not close event loop.") try: logger.debug("Forcefully replacing event loop.") self.rpc_client.loop = None asyncio.set_event_loop(asyncio.new_event_loop()) except Exception as e: logger.exception("Could not replace event loop: %s", e) try: logger.debug("Forcefully deleting RPC client.") self.rpc_client = None except Exception as ex: logger.exception(ex) self.rpc_client = None self.presence_connected = False def update_discord_rich_presence(self): """Dispatch a request to Discord to update presence""" if int(time.time()) - self.rpc_interval < self.last_rpc: logger.debug("Not enough time since last RPC") return if self.rpc_enabled: self.last_rpc = int(time.time()) if not self.connect(): return try: state_text = "via %s" % self.runner_name if self.show_runner else " " logger.info("Attempting to update Discord status: %s, %s", self.game_name, state_text) self.rpc_client.update(details="Playing %s" % self.game_name, state=state_text) except PyPresenceException as ex: logger.error("Unable to update Discord: %s", ex) def clear_discord_rich_presence(self): """Dispatch a request to Discord to clear presence""" if self.rpc_enabled: if self.connect(): try: logger.info('Attempting to clear Discord status.') self.rpc_client.clear() except PyPresenceException as ex: logger.error("Unable to clear Discord: %s", ex) self.disconnect()
def start_mainloop(): logging.info('Mainloop started') count = 0 rpc = Presence(CLIENT_ID) rpc.connect() lichess = Lichess(config.get_lichess_username()) chesscom = Chesscom(config.get_chesscom_username()) platforms = [] while states.running: updated = False lichess.update_username(config.get_lichess_username()) chesscom.update_username(config.get_chesscom_username()) if lichess.is_available(): platforms.append(lichess) if chesscom.is_available(): print('available', chesscom.username) platforms.append(chesscom) print(platforms) if len(platforms) == 0: logging.warning('Missing information -> update config') rpc.clear() utils.open_webpage() while config.get_temp_data() == {} or all( config.get_temp_data()[key] is None for key in config.get_temp_data()): time.sleep(1) else: continue count += 1 logging.info(f'Presence updated ({count})') for platform in platforms: platform.update_data() status = platform.get_status() if status is OFFLINE: continue if status is PLAYING: platform.display_playing(rpc) updated = True break if status is ONLINE: platform.display_online(rpc) updated = True break if not updated: rpc.clear() platforms.clear() time.sleep(15) rpc.clear() logging.info('Mainloop ended')
from pypresence import Presence import time import requests client_id = "" rpc = Presence(client_id) rpc.connect() url = "https://r-a-d.io/api" rpc.clear() while True: r = requests.get(url=url) data = r.json() np = data['main']['np'] dj = data['main']['dj']['djname'] dj_image = data['main']['dj']['djimage'] thread = data['main']['thread'] if dj == "Hanyuu-sama": large_image = "hanyuu" elif dj == "Claud": large_image = "claud" elif dj == "apt-get": large_image = "apt-get" elif dj == "kipukun": large_image = "kipukun" elif dj == "Suzubrah": large_image = "suzubrah" elif dj == "exci": large_image = "exci" elif dj == "Wessie":
small_text=small_image_text) sleep(5) elif playback_status == "Stopped": if already_stopped == 0: RPC_Client.update(state=RPC_DETAILS, details=RPC_STATE, large_image=large_image_key, large_text=large_image_text, small_image=small_image_text, small_text=small_image_text) already_stopped = 1 sleep(5) # This handles if discord closes or turns on while True: try: RPC_Client.clear() except (ConnectionRefusedError, pyexep.InvalidID): logger.error('Connection reset, retrying connection') while True: try: RPC_Client.connect() except (FileNotFoundError, ConnectionRefusedError): logger.error( "Could not connect to RPC. Do you have discord running?" ) sleep(5) else: break sleep(5) else: break
RPC.update(state="Description: " + des_display, details="" + sub_display, start=int(time.time()), large_text=tol_display, large_image="rmit-l") else: RPC.update(state="Description: " + des_display, details="" + sub_display, start=int(time.time()), large_image="rmit-l") print("Updated Successfully !!!") print("") command = console(command) elif (command == "/clear"): print("") RPC.clear(pid=os.getpid()) print("Cleared the status") print("") command = console(command) elif (command == "/quit"): print("") print("Thank you for using !") os._exit(1) else: print("") print("Invalid command!") print("") command = console(command)
elif (packet.sessionType == 7): showQualiPresence("Q3") elif (packet.sessionType == 8): showQualiPresence("SQ") elif (packet.sessionType == 9): showQualiPresence("OSQ") elif (packet.sessionType == 12): showTimeTrialPresence() elif (packet.sessionType == 1): showPracticePresence("P1") elif (packet.sessionType == 2): showTimeTrialPresence("P2") elif (packet.sessionType == 3): showTimeTrialPresence("P3") elif (packet.sessionType == 4): showTimeTrialPresence("Short Practice") else: RPC.update(large_image="f12020", details="Idling") except socket.timeout: RPC.update(large_image="f12020", details="Idling") while not "F1_2020_dx12.exe" in (p.name() for p in psutil.process_iter()): RPC.clear(os.getpid()) continue
class DiscordPresence(object): """This class provides rich presence integration with Discord for games. """ def __init__(self): self.available = bool(PyPresence) self.game_name = "" self.runner_name = "" self.last_rpc = 0 self.rpc_interval = 60 self.presence_connected = False self.rpc_client = None self.client_id = None self.custom_game_name = '' self.show_runner = True self.custom_runner_name = '' self.rpc_enabled = True def load_from_config(self, config): """Loads """ def ensure_discord_connected(self): """Make sure we are actually connected before trying to send requests""" logger.debug("Ensuring connected.") if self.presence_connected: logger.debug("Already connected!") else: logger.debug("Creating Presence object.") self.rpc_client = PyPresence(self.client_id) try: logger.debug("Attempting to connect.") self.rpc_client.connect() self.presence_connected = True except Exception as ex: logger.exception("Unable to reach Discord. Skipping update: %s", ex) self.ensure_discord_disconnected() return self.presence_connected def ensure_discord_disconnected(self): """Ensure we are definitely disconnected and fix broken event loop from pypresence""" logger.debug("Ensuring disconnected.") if self.rpc_client is not None: try: self.rpc_client.close() except Exception as e: logger.exception("Unable to close Discord RPC connection: %s", e) if self.rpc_client.sock_writer is not None: try: logger.debug("Forcefully closing sock writer.") self.rpc_client.sock_writer.close() except Exception: logger.exception("Sock writer could not be closed.") try: logger.debug("Forcefully closing event loop.") self.rpc_client.loop.close() except Exception: logger.debug("Could not close event loop.") try: logger.debug("Forcefully replacing event loop.") self.rpc_client.loop = None asyncio.set_event_loop(asyncio.new_event_loop()) except Exception as e: logger.exception("Could not replace event loop: %s", e) try: logger.debug("Forcefully deleting RPC client.") self.rpc_client = None except Exception as ex: logger.exception(ex) self.rpc_client = None self.presence_connected = False def update_discord_rich_presence(self): """Dispatch a request to Discord to update presence""" if int(time.time()) - self.rpc_interval < self.last_rpc: logger.debug("Not enough time since last RPC") return if self.rpc_enabled: self.last_rpc = int(time.time()) connected = self.ensure_discord_connected() if not connected: return try: state_text = "via %s" % self.runner_name if self.show_runner else " " logger.info("Attempting to update Discord status: %s, %s", self.game_name, state_text) self.rpc_client.update(details="Playing %s" % self.game_name, state=state_text) except PyPresenceException as ex: logger.error("Unable to update Discord: %s", ex) def clear_discord_rich_presence(self): """Dispatch a request to Discord to clear presence""" if self.rpc_enabled: connected = self.ensure_discord_connected() if connected: try: logger.info('Attempting to clear Discord status.') self.rpc_client.clear() except PyPresenceException as e: logger.error("Unable to clear Discord: %s", e) self.ensure_discord_disconnected()
class DiscordStatusMessage(EventPlugin): PLUGIN_ID = _("Discord status message") PLUGIN_NAME = _("Discord Status Message") PLUGIN_DESC = _("Change your Discord status message according to what " "you're currently listening to.") VERSION = VERSION def __init__(self): self.song = None self.discordrp = None def update_discordrp(self, details, state=None): if not self.discordrp: try: self.discordrp = Presence(QL_DISCORD_RP_ID, pipe=0) self.discordrp.connect() except (DiscordNotFound, ConnectionRefusedError): self.discordrp = None if self.discordrp: try: self.discordrp.update(details=details, state=state, large_image=QL_LARGE_IMAGE) except InvalidID: # XXX Discord was closed? self.discordrp = None def handle_play(self): if self.song: details = Pattern(discord_status_config.rp_line1) % self.song state = Pattern(discord_status_config.rp_line2) % self.song # The details and state fields must be atleast 2 characters. if len(details) < 2: details = None if len(state) < 2: state = None self.update_discordrp(details, state) def handle_paused(self): self.update_discordrp(details=_("Paused")) def handle_unpaused(self): if not self.song: self.song = app.player.song self.handle_play() def plugin_on_song_started(self, song): self.song = song if not app.player.paused: self.handle_play() def plugin_on_paused(self): self.handle_paused() def plugin_on_unpaused(self): self.handle_unpaused() def enabled(self): if app.player.paused: self.handle_paused() else: self.handle_unpaused() def disabled(self): if self.discordrp: self.discordrp.clear() self.discordrp.close() self.discordrp = None self.song = None def PluginPreferences(self, parent): vb = Gtk.VBox(spacing=6) def rp_line1_changed(entry): discord_status_config.rp_line1 = entry.get_text() if not app.player.paused: self.handle_play() def rp_line2_changed(entry): discord_status_config.rp_line2 = entry.get_text() if not app.player.paused: self.handle_play() status_line1_box = Gtk.HBox(spacing=6) status_line1_box.set_border_width(3) status_line1 = Gtk.Entry() status_line1.set_text(discord_status_config.rp_line1) status_line1.connect('changed', rp_line1_changed) status_line1_box.pack_start(Gtk.Label(label=_("Status Line #1")), False, True, 0) status_line1_box.pack_start(status_line1, True, True, 0) status_line2_box = Gtk.HBox(spacing=3) status_line2_box.set_border_width(3) status_line2 = Gtk.Entry() status_line2.set_text(discord_status_config.rp_line2) status_line2.connect('changed', rp_line2_changed) status_line2_box.pack_start(Gtk.Label(label=_('Status Line #2')), False, True, 0) status_line2_box.pack_start(status_line2, True, True, 0) vb.pack_start(status_line1_box, True, True, 0) vb.pack_start(status_line2_box, True, True, 0) return vb
class PoeRPC: def __init__(self, loop, account_name, cookies, logger): self.rpc = Presence(CLIENT_ID, pipe=0, loop=loop) self.cookies = cookies self.log_path = None self.on = True self.loop = loop self.logger = logger self.last_location = None self.last_latest_message = "" self.last_event = LogEvents.LOGOUT self.afk = False self.dnd = False self.afk_message = "" self.dnd_message = "" self.account_name = account_name self.current_rpc = {} self.locations = {} self.quit = False self.logger.debug("Loading areas") with open('areas.json') as f: areas = json.load(f) self.logger.debug("Loaded areas") self.locations.update(areas) self.logger.debug("Loading maps") with open('maps.json') as f: self.maps = json.load(f) self.logger.debug("Loaded maps") self.logger.debug("Loading icon refs") with open('available_icons.json') as f: self.icons = json.load(f) self.logger.debug("Loaded icon refs") self.logger.debug("Loading xp ref") with open('experience.json') as f: self.xp = json.load(f) self.logger.debug("Loaded xp ref") def do_quit(self): self.quit = True async def check_poe(self): """Tries to check if poe is running every 15sec, clears RPC if not. Waits for poe to start again, calls init again with restart as True""" while 1: if self.quit: break poe = get_path() if not poe: if self.on: self.logger.debug( "PoE no longer open, setting on to false") self.on = False self.rpc.clear() else: if not self.on: self.logger.debug( f"Found launched poe at {poe} setting on to True and restarting init" ) self.on = True self.loop.create_task(self.init(restart=True)) await asyncio.sleep(15) def update_rpc(self, field, value): """Basically just neater way to indicate updating of each field for rpc dict""" self.current_rpc[field] = value def submit_update(self): """rpc.update takes in everything as a kwarg, using splat operator the current_rpc dict is turned into kwargs and passed to update""" self.logger.debug( f"Submitting rpc update with content: {self.current_rpc}") self.rpc.update(**self.current_rpc) async def fetch_char(self): """Calls to get-characters at poe API, interprets response and updates rpc""" async with self.ses.get( f'https://www.pathofexile.com/character-window/get-characters?accountName={self.account_name}' ) as resp: js = await resp.json() for char in js: # js is still a valid json on error, if you try to iterate over the items, compared to a valid request # it is an str and not a dict, thus this works. if char is str(): self.logger.info( "Your character tab is set to be hidden or your profile is private\nchange private to true in config.json" "and set the value for sessid as your POESESSID\nAlternatively you can also make your character tab or profile public." ) exit() if 'lastActive' in char.keys(): break asc = char['class'] level = char['level'] name = char['name'] xp = char['experience'] # Calculate percent to next level # XP json contains only the total xp, so it must be subtracted from the xp of next level # and then calculated with difference of current xp max_cur_xp = int(self.xp[str(level)].replace(',', '')) needed_xp = int(self.xp[str(min(level + 1, 100))].replace(',', '')) diff1 = needed_xp - xp diff2 = needed_xp - max_cur_xp perc = round(((diff2 - diff1) / diff2) * 100) lg_text = f"{name} | Level {level} {asc}\n{perc}% to Level {'-' if level==100 else level+1}" self.update_rpc('large_image', asc.lower()) self.update_rpc('large_text', lg_text) self.update_rpc('state', f"in {char['league']} league") @staticmethod def fix_names(name): """Icon names on discord RPC follow a pretty standard pattern, no special chars, commas, apostrophes, spaces or unicode chars. Nothing other than maelstrom has a unicode char, so a chain of replaces just works as well.""" return name.replace(',', '').replace("'", "").replace('ö', 'o').replace(' ', '_').lower() async def fetch_area_data(self, name): """Runs Area name through a multitude of filters to conclude which area the player is in, then finds the appropriate icon and then update rpc dict""" loc = None if 'hideout' in name.lower(): loc = {'name': name} img = 'hideout' if not loc: for map in self.maps: if name in map['name']: loc = map fixed_name = self.fix_names(name) if fixed_name in self.icons: img = fixed_name elif int(map['tier']) < 6: img = 'white' elif 6 <= int(map['tier']) <= 10: img = 'yellow' elif int(map['tier']) > 10: img = 'red' elif name.lower() in self.locations['guardians']: if 'Phoenix' in name: img = 'phoenix' elif 'Minotaur' in name: img = 'minotaur' elif 'Hydra' in name: img = 'hydra' else: img = 'chimera' elif name in "Vaal Temple": img = 'vaal_temple' break if not loc: for town in self.locations['towns']: if name in town['name']: loc = town img = 'town' break if not loc: for part in self.locations['lab_rooms']: if part in name: loc = {'name': "The Lord's Labyrinth"} img = 'lab' break if not loc: if "Azurite Mine" in name: loc = {'name': "Azurite Mine"} img = 'mine' if not loc: if "Menagerie" in name or name in self.locations['bestiary_bosses']: loc = {'name': name} img = 'menagerie' if not loc: if "absence of value" in name.lower(): loc = {'name': name} img = 'elder' if not loc: for guardian in self.locations['elder_guardians']: if name in self.locations['elder_guardians'][guardian]: loc = {'name': f"{name} - {guardian}"} img = guardian.split()[1].lower() break if not loc: loc = {'name': name} img = 'waypoint' small_text = loc['name'] timestamp = round(time.time()) self.update_rpc('small_text', small_text) self.update_rpc('small_image', img) self.update_rpc('start', timestamp) if 'tier' in loc.keys(): self.update_rpc('details', f"{loc['name']} | Tier: {loc['tier']} | ") elif 'details' in self.current_rpc: if 'Tier' in self.current_rpc['details']: del self.current_rpc['details'] async def handle_log_event(self, log): """On the event of a log update, handle_log_event is called. The log is passed to it and it iters through messages to decide what methods to call for which event""" messages = reversed(log.split('\n')) event = None ping = None for ind, message in enumerate(messages): if message == self.last_latest_message: self.logger.debug( f"Reached last message from previous update: {message}") break elif "You have entered" in message: loc = message.split("You have entered ")[1].replace('.', '') self.logger.info(f"Entered {loc}") if self.last_location != loc and loc != "Arena": event = LogEvents.AREA self.last_location = loc else: return break elif "Async connecting" in message or "Abnormal disconnect" in message: self.logger.info("On character selection") self.last_location = None event = LogEvents.LOGOUT break elif "Connect time" in message: ping = message.split("was ")[1] self.logger.info(f"Ping to instance was: {ping}") elif "AFK mode is now" in message: if message.split("AFK mode is now O")[1][0] == "N": self.afk = True self.afk_message = message.split('Autoreply "')[1][:-1] self.logger.info(f"AFK: {self.afk_message}") else: self.afk = False self.afk_message = "" self.logger.info("AFK: Turned Off") elif "DND mode is now" in message: if message.split("DND mode is now O")[1][0] == "N": self.dnd = True self.dnd_message = message.split('Autoreply "')[1][:-1] self.logger.info(f"DND: {self.afk_message}") else: self.dnd = False self.dnd_message = "" self.logger.info("DND: Turned Off") self.last_latest_message = log.split('\n')[-1] or log.split('\n')[-2] if event != LogEvents.LOGOUT: await self.fetch_char() if event == LogEvents.AREA: await self.fetch_area_data(loc) elif event == LogEvents.LOGOUT: self.current_rpc = {} self.update_rpc('large_image', 'login_screen') self.update_rpc('state', 'On Character Selection') def update_dnd(): self.update_rpc('details', f"DND: {self.dnd_message}") def update_afk(): self.update_rpc('details', f"AFK: {self.afk_message}") if not self.afk and "AFK" in self.current_rpc.get('details', '') \ or not self.dnd and "DND" in self.current_rpc.get('details', ''): self.current_rpc.pop('details') if self.dnd and self.afk and "AFK" not in self.current_rpc.get( 'details', ''): update_afk() elif self.afk and "AFK" not in self.current_rpc.get('details', ''): update_afk() elif self.dnd and "DND" not in self.current_rpc.get('details', ''): update_dnd() if ping: state = self.current_rpc.get('state', '') if not 'Ping' in state: self.update_rpc( 'state', f'{state}{" | " if state else ""}Ping: {ping}') else: current_ping = state.split('Ping: ')[1] state = state.replace(current_ping, ping) self.update_rpc('state', state) self.submit_update() self.last_event = event async def monitor_log(self): """Monitors if log file has changed by checking last message, passes log to handler if yes, tries again in 5 seconds""" self.logger.info("Log monitor has started") while 1: if self.quit: break if not self.on: self.logger.info("Log monitor now sleeping") await self.ses.close() break with open(self.log_path, encoding='utf-8') as f: log = f.read() # log = log.encode('utf-8') new_last = log.split('\n')[-1] or log.split('\n')[-2] if self.last_latest_message != new_last: self.logger.debug(f"Log update observed: {new_last}") await self.handle_log_event(log) await asyncio.sleep(5) @staticmethod async def get_poe(): """When initializing, keeps trying to find a launched Path Of Exile before initializing the monitors""" while 1: poe = get_path() if not poe: await asyncio.sleep(10) else: return poe async def init(self, restart=False): """Standard method for initialization called by launcher, sets up the aiohttp session and starts monitor loops on confirmation from get_poe""" try: await self.rpc.connect() except: self.logger.info( "Discord not open, waiting for discord to launch...") while 1: if self.quit: break try: await self.rpc.connect() self.logger.info("Discord launched") break except: pass await asyncio.sleep(5) self.logger.info("Waiting for path of exile to launch...") poe = await self.get_poe() self.log_path = f"{poe}/logs/Client.txt" self.logger.info(f"Found path of exile log at {self.log_path}") self.ses = aiohttp.ClientSession(cookies=self.cookies) if not restart: self.loop.create_task(self.check_poe()) self.loop.create_task(self.monitor_log())
class Presence(object): """This class provides rich presence integration with Discord for games. """ def __init__(self): if PyPresence is not None: logger.debug('Discord Rich Presence not available due to lack of pypresence') self.rich_presence_available = True else: logger.debug('Discord Rich Presence enabled') self.rich_presence_available = False self.last_rpc = 0 self.rpc_interval = 60 self.presence_connected = False self.rpc_client = None self.client_id = None self.custom_game_name = '' self.show_runner = True self.custom_runner_name = '' self.rpc_enabled = True def available(self): """Confirm that we can run Rich Presence functions""" return self.rich_presence_available def ensure_discord_connected(self): """Make sure we are actually connected before trying to send requests""" if not self.available(): logger.debug("Discord Rich Presence not available due to lack of pypresence") return logger.debug("Ensuring connected.") if self.presence_connected: logger.debug("Already connected!") else: logger.debug("Creating Presence object.") self.rpc_client = PyPresence(self.client_id) try: logger.debug("Attempting to connect.") self.rpc_client.connect() self.presence_connected = True except Exception as e: logger.error("Unable to reach Discord. Skipping update: %s", e) self.ensure_discord_disconnected() return self.presence_connected def ensure_discord_disconnected(self): """Ensure we are definitely disconnected and fix broken event loop from pypresence""" if not self.available(): logger.debug("Discord Rich Presence not available due to lack of pypresence") return logger.debug("Ensuring disconnected.") if self.rpc_client is not None: try: self.rpc_client.close() except Exception as e: logger.error("Unable to close Discord RPC connection: %s", e) if self.rpc_client.sock_writer is not None: try: logger.debug("Forcefully closing sock writer.") self.rpc_client.sock_writer.close() except Exception: logger.debug("Sock writer could not be closed.") try: logger.debug("Forcefully closing event loop.") self.rpc_client.loop.close() except Exception: logger.debug("Could not close event loop.") try: logger.debug("Forcefully replacing event loop.") self.rpc_client.loop = None asyncio.set_event_loop(asyncio.new_event_loop()) except Exception as e: logger.debug("Could not replace event loop: %s", e) try: logger.debug("Forcefully deleting RPC client.") del self.rpc_client except Exception: pass self.rpc_client = None self.presence_connected = False def update_discord_rich_presence(self): """Dispatch a request to Discord to update presence""" if not self.available(): logger.debug("Discord Rich Presence not available due to lack of pypresence") return if int(time.time()) - self.rpc_interval < self.last_rpc: logger.debug("Not enough time since last RPC") return if self.rpc_enabled: logger.debug("RPC is enabled") self.last_rpc = int(time.time()) connected = self.ensure_discord_connected() if not connected: return try: state_text = "via {}".format(self.runner_name) if self.show_runner else " " logger.info("Attempting to update Discord status: %s, %s", self.game_name, state_text) self.rpc_client.update(details="Playing {}".format(self.game_name), state=state_text) except PyPresenceException as e: logger.error("Unable to update Discord: %s", e) else: logger.debug("RPC disabled") def clear_discord_rich_presence(self): """Dispatch a request to Discord to clear presence""" if not self.available(): logger.debug("Discord Rich Presence not available due to lack of pypresence") return if self.rpc_enabled: connected = self.ensure_discord_connected() if connected: try: logger.info('Attempting to clear Discord status.') self.rpc_client.clear() except PyPresenceException as e: logger.error("Unable to clear Discord: %s", e) self.ensure_discord_disconnected()
def main(): logger.info("Checking for updates...") os.system("git pull") logger.info( "If updates were done, restart this script by using CTRL-C to terminate it, and re run it." ) # Make connection to Discord try: RPC = Presence(client_id) # Initialize the Presence class RPC.connect() # Start the handshake loop except pypresence.exceptions.InvalidPipe: logger.error( "Could not connect to the discord pipe. Please ensure it's running." ) exit(1) except FileNotFoundError: logger.error( "Could not connect to the discord pipe. Please ensure it's running." ) exit(1) # Load our current config config = nso_functions.get_config_file() logger.info("Check discord!") # get friend code from config, and add config option if does not exist try: friend_code = config['friend_code'] except KeyError: config['friend_code'] = 'Unset' config_f = open("config.txt", "w") config_f.write(json.dumps( config, sort_keys=True, indent=4, )) config_f.close() friend_code = config['friend_code'] while True: # The presence will stay on as long as the program is running for i in range(0, 4): minutes_since, last_match = get_minutes_since() # int is here so we don't have funky floating point madness seconds_since = int(minutes_since * 60) hours_since = int(minutes_since / 60) if minutes_since >= 60: details = "Last match: {} hour(s) ago".format(hours_since) elif minutes_since > 1: details = "Last match: {} minute(s) ago".format( math.floor(minutes_since)) else: details = "Last match: {} second(s) ago".format(seconds_since) # job_result is only present in salmon run JSON if last_match.get('job_result') is not None: gamemode_key = "salmon_run" if last_match['job_result']['is_clear']: outcome = "winning" else: outcome = "losing" large_text = "Last match was Salmon Run, {} with {} eggs".format( outcome, last_match['job_score']) if i == 0: state = "Grade: {}".format( (last_match["grade"])["long_name"]) elif i == 1: state = "Friend code: {}".format(friend_code) elif i == 2: state = "Difficulty: {}".format( str(last_match["danger_rate"])) elif i == 3: state = "Played on {}".format( last_match['schedule']['stage']['name']) else: large_text = "Last match was {}, {} on {}".format( last_match["game_mode"]["name"], last_match["rule"]["name"], last_match["stage"]["name"]) gamemode_key = last_match["rule"]["key"] if i == 0: state = "Friend code: {}".format(friend_code) elif i == 1: state = "K/D: {}/{}".format( last_match["player_result"]["kill_count"], last_match["player_result"]["death_count"]) elif i == 2: details = last_match["my_team_result"]["name"] try: state = "{}% vs {}%".format( last_match["my_team_percentage"], last_match["other_team_percentage"]) except KeyError: try: state = "{} vs {}".format( last_match["my_team_count"], last_match["other_team_count"]) except KeyError: state = "Gamemode not yet supported" elif i == 3: state = "{}p".format( last_match["player_result"]["game_paint_point"]) if show_weapon: details = "{}".format(last_match["player_result"] ["player"]["weapon"]["name"]) else: pass if minutes_since < timeout_minutes: RPC.update(details=details, state=state, large_image=gamemode_key, small_image="default", large_text=large_text) else: RPC.clear() logger.debug("RPC cleared, not in game long enough") time.sleep(time_interval)
def main(): logger.info("Checking for updates...") os.system("git pull") logger.info( "If updates were done, restart this script by using CTRL-C to terminate it, and re run it." ) # Make connection to Discord try: RPC = Presence(client_id) # Initialize the Presence class RPC.connect() # Start the handshake loop except pypresence.exceptions.InvalidPipe: logger.error( "Could not connect to the discord pipe. Please ensure it's running." ) exit(1) except FileNotFoundError: logger.error( "Could not connect to the discord pipe. Please ensure it's running." ) exit(1) # Load our current config config = nso_functions.get_config_file() logger.info("Check discord!") # Get friend code from config, and add config option if does not exist try: friend_code = config['friend_code'] except KeyError: config['friend_code'] = 'Unset' config_f = open("config.txt", "w") config_f.write(json.dumps( config, sort_keys=True, indent=4, )) config_f.close() friend_code = config['friend_code'] while True: # The presence will stay on as long as the program is running for i in range(0, 5): minutes_since, last_match = get_minutes_since() # Calculating the secs/hours/days since Last Match/Run seconds_since = int(minutes_since * 60) hours_since = int(minutes_since / 60) days_since = int(minutes_since / 1440) # When Previous Match was Salmon Run # job_result is only present in salmon run JSON if last_match.get('job_result') is not None: # Sets Gamemode Key in order to change the Picture gamemode_key = "salmon_run" # Decides if last Run is shown in days, hours, minutes or seconds # In Days if minutes_since >= 1440: details = "Last Run: {} day{} ago".format( days_since, plural_logic(days_since)) # In Hours elif minutes_since >= 60: details = "Last Run: {} h{} ago".format( hours_since, plural_logic(hours_since)) # In Minutes elif minutes_since > 1: details = "Last Run: {} min{} ago".format( math.floor(minutes_since), plural_logic(math.floor(minutes_since))) # In Seconds else: details = "Last Run: {} sec{} ago".format( seconds_since, plural_logic(seconds_since)) # Deciding the Result if last_match['job_result']['is_clear']: outcome = "WON" else: outcome = "LOST" ### Checks how many waves were played on last Run # If all 3 Waves were played if last_match["wave_details"][2]: goldEgg = last_match["wave_details"][0]["golden_ikura_num"] + \ last_match["wave_details"][1]["golden_ikura_num"] + \ last_match["wave_details"][2]["golden_ikura_num"] powEgg = last_match["wave_details"][0]["ikura_num"] + \ last_match["wave_details"][1]["ikura_num"] + \ last_match["wave_details"][2]["ikura_num"] # If only 2 Waves were played elif not last_match["wave_details"][2] and last_match[ "wave_details"][1]: goldEgg = last_match["wave_details"][0][ "golden_ikura_num"] + last_match["wave_details"][1][ "golden_ikura_num"] powEgg = last_match["wave_details"][0][ "ikura_num"] + last_match["wave_details"][1][ "ikura_num"] # If only 1 Wave was played else: goldEgg = last_match["wave_details"][0]["golden_ikura_num"] powEgg = last_match["wave_details"][0]["ikura_num"] # When hovering on the Picture large_text = "Last match was Salmon Run on {}".format( last_match['schedule']['stage']['name']) # IGN and Salmon Run Rank if i == 0: details = "IGN: {}".format(last_match["my_result"]["name"]) state = "{} {}".format((last_match["grade"])["long_name"], last_match["grade_point"]) # Friend code elif i == 1: if not friend_code: state = "FC: Not Given" else: state = "FC: {}".format(friend_code) # Hazard Level elif i == 2: state = "Hazard Level: {}".format( str(last_match["danger_rate"]) + "%") # Result and Total Collected Golden Eggs / Power Eggs elif i == 3: details = "GoldEgg/PowEgg ({})".format(outcome) state = "{} / {}".format(goldEgg, powEgg) # Save / Death Ratio elif i == 4: state = "Save/Death Ratio: {}/{}".format( last_match["my_result"]["help_count"], last_match["my_result"]["dead_count"]) if minutes_since < timeout_minutes: RPC.update(details=details, state=state, large_image=gamemode_key, small_image="default", large_text=large_text) else: RPC.clear() logger.debug("RPC cleared, not in game long enough") time.sleep(time_interval) # When Previous Match was Turf, Ranked, League or Private else: # Decides if last Match is shown in days, hours, minutes or seconds # In Days if minutes_since >= 1440: details = "Last Match: {} day{} ago".format( days_since, plural_logic(days_since)) # In Hours elif minutes_since >= 60: details = "Last Match: {} h{} ago".format( hours_since, plural_logic(hours_since)) # In Minutes elif minutes_since > 1: details = "Last Match: {} min{} ago".format( math.floor(minutes_since), plural_logic(math.floor(minutes_since))) # In Seconds else: details = "Last Match: {} sec{} ago".format( seconds_since, plural_logic(seconds_since)) # When hovering on the Picture large_text = "Last match was {}, {} on {}".format( last_match["game_mode"]["name"], last_match["rule"]["name"], last_match["stage"]["name"]) # Gets Gamemode Key in order to change the Picture gamemode_key = last_match["rule"]["key"] # Gets Lobby Key lobby_key = last_match["game_mode"]["key"] # IGN and Level (+ Rank) if i == 0: details = "IGN: {}".format( last_match["player_result"]["player"]["nickname"]) # Checks if player has a Level Star # If player has no Level Star (yet XP) if not last_match["star_rank"]: # If last match was in a Regular Lobby (Turf War) or Private Lobby if lobby_key == "regular" or lobby_key == "private": state = "Level: {}".format( last_match["player_result"]["player"] ["player_rank"], ) # If last match was in a Ranked Solo Lobby elif lobby_key == "gachi": # If last match was Splat Zones if gamemode_key == "splat_zones": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}/R(SZ): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}/R(SZ): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], ) # If Player has X Power else: state = "Lvl: {}/R(SZ): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}/R(SZ): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"]) # If last match was Tower Control elif gamemode_key == "tower_control": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}/R(TC): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}/R(TC): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], ) # If Player has X Power else: state = "Lvl: {}/R(TC): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}/R(TC): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"]) # If last match was Rainmaker elif gamemode_key == "rainmaker": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}/R(RM): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}/R(RM): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], ) # If Player has X Power else: state = "Lvl: {}/R(RM): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}/R(RM): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"]) # If last match was Clam Blitz else: # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}/R(CB): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}/R(CB): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], ) # If Player has X Power else: state = "Lvl: {}/R(CB): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}/R(CB): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["udemae"]["name"]) # If last match was in a League Pair/Team Lobby elif lobby_key == "league_pair" or lobby_key == "league_team": # Checks if Player has League Power # If Player has no League Power (yet XP) if not last_match["league_point"]: state = "Lvl: {}/Power: TBD".format( last_match["player_result"]["player"] ["player_rank"]) # If Player has League Power else: state = "Lvl: {}/Power: {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["league_point"]) # If player has a Level Star else: # If last match was in a Regular Lobby (Turf War) or Private Lobby if lobby_key == "regular" or lobby_key == "private": state = "Level: {}☆{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], ) # If last match was in a Ranked Solo Lobby elif lobby_key == "gachi": # If last match was Splat Zones if gamemode_key == "splat_zones": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}☆{}/R(SZ): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}☆{}/R(SZ): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], ) # If Player has X Power else: state = "Lvl: {}☆{}/R(SZ): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}☆{}/R(SZ): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"]) # If last match was Tower Control elif gamemode_key == "tower_control": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}☆{}/R(TC): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}☆{}/R(TC): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], ) # If Player has X Power else: state = "Lvl: {}☆{}/R(TC): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}☆{}/R(TC): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"]) # If last match was Rainmaker elif gamemode_key == "rainmaker": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}☆{}/R(RM): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}☆{}/R(RM): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], ) # If Player has X Power else: state = "Lvl: {}☆{}/R(RM): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}☆{}/R(RM): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"]) # If last match was Clam Blitz elif gamemode_key == "clam_blitz": # If player has S+ Rank if last_match["udemae"]["name"] == "S+": state = "Lvl: {}☆{}/R(CZ): {}{}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"], last_match["udemae"]["s_plus_number"]) # If player has X Rank elif last_match["udemae"]["name"] == "X": # Checks if Player has any X Power # If Player has no X Power (yet XP) if not last_match["x_power"]: state = "Lvl: {}☆{}/R(CZ): X(TBD)".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], ) # If Player has X Power else: state = "Lvl: {}☆{}/R(CZ): X({})".format( last_match["player_result"] ["player"]["player_rank"], last_match["player_result"] ["player"]["star_rank"], last_match["x_power"]) # If player has other Ranks else: state = "Lvl: {}☆{}/R(CZ): {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["udemae"]["name"]) # If last match was in a League Pair/Team Lobby elif lobby_key == "league_pair" or lobby_key == "league_team": # Checks if Player has League Power # If Player has no League Power (yet XP) if not last_match["league_point"]: state = "Lvl: {}☆{}/Power: TBD".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], ) # If Player has League Power else: state = "Lvl: {}☆{}/Power: {}".format( last_match["player_result"]["player"] ["player_rank"], last_match["player_result"]["player"] ["star_rank"], last_match["league_point"]) # Friend Code elif i == 1: if not friend_code: state = "FC: Not Given" else: state = "FC: {}".format(friend_code) # Kill (Assist) / Death Ratio elif i == 2: state = "K(A)/D: {}({})/{}".format( last_match["player_result"]["kill_count"], last_match["player_result"]["assist_count"], last_match["player_result"]["death_count"]) # Result and Percentages elif i == 3: details = last_match["my_team_result"]["name"] try: state = "{}% vs {}%".format( last_match["my_team_percentage"], last_match["other_team_percentage"]) except KeyError: try: state = "{} vs {}".format( last_match["my_team_count"], last_match["other_team_count"]) except KeyError: state = "Gamemode not yet supported" # Used Weapon and Total Points elif i == 4: state = "{}p".format( last_match["player_result"]["game_paint_point"]) if show_weapon: details = "{}".format(last_match["player_result"] ["player"]["weapon"]["name"]) else: pass if minutes_since < timeout_minutes: RPC.update(details=details, state=state, large_image=gamemode_key, small_image="default", large_text=large_text) else: RPC.clear() logger.debug("RPC cleared, not in game long enough") time.sleep(time_interval)
class ghrp(): def __init__(self, discord_client_id: str, github_username: str, github_client_id: str = None, github_client_secret: str = None): """Init method for the GHRP class. Arguments: discord_client_id {str} -- Discord Client ID from the developer portal. github_username {str} -- GitHub username to monitor Keyword Arguments: github_client_id {str} -- GitHub OAuth application client id (default: {None}) github_client_secret {str} -- GitHub OAuth application client secret (default: {None}) """ self.dclient = discord_client_id self.gusername = github_username self.gclientid = github_client_id self.gclientsecret = github_client_secret self.show_status = False self.payload = None self.headers = {} self.events_url = "https://api.github.com/users/{}/events".format( config["github_username"]) self.session = requests.Session() self.timestamp = 0 # if github client id and secret are not provided we need to make sure # we don't go over the ratelimit. if self.gclientid and self.gclientsecret: self.interval = 30 payload = { "client_id": config["github_client_id"], "client_secret": config["github_client_secret"] } else: self.interval = 60 self.rpc = Presence(self.dclient) self.rpc.connect() def get_newest_push(self, events: dict) -> dict: """Helper method for getting the most recent commit. Returns the most recent event object that includes a commit (not new projects) Returns: dict -- Most recent event containing a commit. """ for event in events: if event["type"] == "PushEvent": return event return None def update(self) -> None: """Method to update the rich presence instance. Every 30 or 60 seconds (see __init__) it queries the GitHub events API. If there is new info, it checks if it is a commit. If it is, it parses it and updates the rich presence. After 1 hour, it clears the rich presence. """ events_rq = self.session.get(self.events_url, headers=self.headers, params=self.payload) if events_rq.status_code != 304: self.show_status = True self.headers["If-None-Match"] = events_rq.headers["ETag"] events = json.loads(events_rq.text) latest = self.get_newest_push(events) repo_name = latest["repo"]["name"].split("/")[1] commit_message = latest["payload"]["commits"][0]["message"].split( "\n")[0] self.timestamp = calendar.timegm( time.strptime(latest["created_at"], "%Y-%m-%dT%H:%M:%SZ")) self.rpc.update(details=repo_name, state=commit_message, large_image="github") else: if (time.time() - self.timestamp) > 60 * 60 and self.show_status: self.rpc.clear() self.show_status = False
class rich_presence: def __init__(self): super().__init__() self.vbox = virtualbox.VirtualBox() self.config = configparser.ConfigParser() self.config.read("config.ini") self.assets = json.load(open("assets.json", "r")) self.previous_formatdict = None client_id = self.config["Rich Presence"]["client_id"] self.RPC = Presence(client_id) self.RPC.connect() self.start_time = time.time() while True: if ("VirtualBox.exe" in (p.name() for p in psutil.process_iter()) or "VirtualBoxVM.exe" in (p.name() for p in psutil.process_iter()) ) and (sys.platform.startswith("win32")): pvars = self.presence_gen() # print("-------------------------\npresence_dict\n-------------------------") # pprint.pprint(pvars) self.RPC.update( large_image=pvars["large_image"], large_text=pvars["large_text"], small_image=pvars["small_image"], small_text=pvars["small_text"], details=pvars["details"], state=pvars["state"], start=pvars["start"], ) pprint.pprint(pvars) print("--------------------") elif sys.platform.startswith("win32"): print("VirtualBox is not running") self.RPC.clear() else: pvars = self.presence_gen() # print("-------------------------\npresence_dict\n-------------------------") # pprint.pprint(pvars) self.RPC.update( large_image=pvars["large_image"], large_text=pvars["large_text"], small_image=pvars["small_image"], small_text=pvars["small_text"], details=pvars["details"], state=pvars["state"], start=pvars["start"], ) pprint.pprint(pvars) print("--------------------") time.sleep(15) def vbox_to_dict(self): # Generate metadata for each machine self.machines = self.vbox.machines self.machine_names = [m.name for m in self.machines] self.machine_states = [m.state for m in self.machines] self.machine_operating_systems = [m.os_type_id for m in self.machines] # Create dictionary to store info self.vd_machine_dict = {} for i in range(len(self.machines)): # Extract OS version and architecture info self.vd_operating_system, self.vd_architecture = self.os_to_arch( self.machine_operating_systems[i]) self.vd_name = self.machine_names[i] # Get name of VM self.vd_machine_dict[self.vd_name] = {} # Initialize dictionaries self.vd_machine_dict[self.vd_name][ "os version"] = self.vd_operating_system # Add OS version self.vd_machine_dict[self.vd_name][ "architecture"] = self.vd_architecture # Add architecture self.vd_machine_dict[self.vd_name]["state"] = str( self.machine_states[i]) # Add state # Run through assets and find the correct OS for key in list(self.assets["operating systems"].keys()): if self.vd_operating_system in list( self.assets["operating systems"][key] ["versions"].keys()): self.vd_machine_dict[ self.vd_name]["operating system"] = key # print("-------------------------\nvbox_to_dict\n-------------------------") # pprint.pprint(self.vd_machine_dict) return self.vd_machine_dict def os_to_arch(self, oa_input): if "_" in oa_input: self.oa_operating_system, self.oa_architecture = oa_input.split( "_", 1) else: self.oa_operating_system = oa_input self.oa_architecture = "32" return self.oa_operating_system, self.oa_architecture def vdict_to_formatdict(self, vf_input): self.vf_active_vm = None self.format_dict = {} self.format_dict["active_vm"] = False for vf_key in list(vf_input.keys()): if vf_input[vf_key]["state"] == "FirstOnline": self.vf_active_vm = vf_key if self.vf_active_vm: self.format_dict["active_vm"] = True self.format_dict["os_hf"] = vf_input[ self.vf_active_vm]["operating system"] self.format_dict["version_hf"] = self.assets["operating systems"][ vf_input[self.vf_active_vm]["operating system"]]["versions"][ vf_input[self.vf_active_vm]["os version"]]["name"] self.format_dict["version_image"] = self.assets[ "operating systems"][vf_input[ self.vf_active_vm]["operating system"]]["versions"][ vf_input[self.vf_active_vm]["os version"]]["image"] self.format_dict["architecture"] = vf_input[ self.vf_active_vm]["architecture"] self.format_dict["architecture_image"] = vf_input[ self.vf_active_vm]["architecture"] self.format_dict["icon"] = "icon" # print("-------------------------\nformat_dict\n-------------------------") # pprint.pprint(self.format_dict) return self.format_dict def presence_gen(self): self.pg_formatdict = self.vdict_to_formatdict(self.vbox_to_dict()) if self.pg_formatdict != self.previous_formatdict: self.pg_presencedict = {} self.pg_presencedict["start"] = time.time() else: self.pg_pdtime = self.pg_presencedict["start"] self.pg_presencedict = {"start": self.pg_pdtime} if self.pg_formatdict["active_vm"] == True: for pg_field in list(self.config["In VM"].keys()): self.pg_presencedict[pg_field] = self.config["In VM"][ pg_field].format(**self.pg_formatdict) else: for pg_field in list(self.config["In Menu"].keys()): self.pg_presencedict[pg_field] = self.config["In Menu"][ pg_field].format(**self.pg_formatdict) self.pg_presencedict["start"] = None for pg_field in list(self.pg_presencedict.keys()): if (self.pg_presencedict[pg_field] == "" or self.pg_presencedict[pg_field] == []): self.pg_presencedict[pg_field] = None self.previous_formatdict = self.pg_formatdict return self.pg_presencedict
def do(): try: # 애러나면 로그는 해야죠 while True: # 죽을때까지 실행 검증 루프 a = True while a: # 실행 없으면 15초 쉬었다 루프 다시. --> 30초로 변경. try: pinfo = get_process_info() if not pinfo[0]: log("DEBUG", "Process NF", datetime) log("DEBUG", "Retry in 30s...", datetime) time.sleep(30) continue window_title = pinfo[3] a = False # 루프 종료 except IndexError as e: log("ERROR", "caught error.. Retry in 10s : %s" % (e), datetime) time.sleep(10) RPC = Presence(pinfo[len(pinfo) - 1]['appid']) try: RPC.connect() except Exception as e: log("ERROR", "Discord Connection Failed", datetime) log("DEBUG", "ERRINFO :: %s" % (e), datetime) plyer.notification.notify( title='Adobe Discord RPC', #message='디스코드와 연결하지 못하였습니다.\n디스코드가 켜져 있는지 다시 한번 확인 해 주세요.\n30초 후 연결을 다시 시도합니다.', message=lang['DiscordFailed'], app_name='Adobe Discord RPC', app_icon='icon_alpha.ico') goout(datetime) log("INFO", "Discord Connect Success", datetime) dt = pandas.to_datetime(datetime.datetime.now()) # -1은 미사용 # 그 이상은 splitindex 용도로 사용함 if pinfo[len(pinfo) - 1]['getver'] == -1: version = '' else: path = pinfo[5].split(pinfo[len(pinfo) - 1]['publicName'])[1] path = path.split('\\')[pinfo[len(pinfo) - 1]['getver']] version = path.replace(" ", '') filename = window_title.split( pinfo[len(pinfo) - 1]['splitBy'])[pinfo[len(pinfo) - 1]['splitIndex']] lmt = pinfo[len(pinfo) - 1][langcode]['largeText'].replace( '%Ver%', version) lmt = lmt.replace('%Filename%', filename) smt = pinfo[len(pinfo) - 1][langcode]['smallText'].replace( '%Filename%', filename) smt = smt.replace('%Ver%', version) b = True plyer.notification.notify( title='Adobe Discord RPC', #message='성공적으로 디스코드와 연결했습니다.\n%s를 플레이 하게 됩니다.' % (pinfo[len(pinfo)-1]['publicName']), message='%s\n%s %s' % (lang['Connected01'], pinfo[len(pinfo) - 1]['publicName'], lang['Connected02']), app_name='Adobe Discord RPC', app_icon='icon_alpha.ico') while b: # 20-04-25 코드 구조 대량으로 변경되어서 응답없음 여부 사용 안함 """if not isresponding(pinfo['processName'].replace('.exe', ''), datetime): # 응답없음은 10초 간격으로 체크해서 응답없음 풀리면 c = False 로 만듬 c = True while c: try: rtn = RPC.update( large_image='lg', large_text='프로그램 : %s' % (pinfo['publicName']), small_image='sm_temp', small_text="파일명 : %s" %(filename), details="응답없음", state="응답없음", start=int(time.mktime(dt.timetuple())) ) except Exception as e: log("ERROR", "pypresence 갱신 실패... 10초 대기 : %s" % (e), datetime) plyer.notification.notify( title='Adobe Discord RPC', message='RPC 갱신에 실패하였습니다.\n10초 후 다시 시도합니다.', app_name='Adobe Discord RPC', app_icon='icon_alpha.ico' ) b = False time.sleep(10) else: log("DEBUG", "pypresence 리턴 : %s" % (rtn), datetime) time.sleep(10) if not isresponding(pinfo['processName'].replace('.exe', ''), datetime): pass else: # 정상실행 경우 # RPC.clear(pid=os.getpid()) c = False pass else:""" try: rtn = RPC.update(large_image='lg', large_text='Program : %s' % (pinfo[len(pinfo) - 1]['publicName']), small_image='sm_temp', small_text="Adobe Discord RPC", details=lmt, state=smt, start=int(time.mktime( dt.timetuple()))) except Exception as e: log("ERROR", "pypresence 갱신 실패... 30초 대기 : %s" % (e), datetime) plyer.notification.notify( title='Adobe Discord RPC', #message='RPC 갱신에 실패하였습니다.\n30초 후 다시 시도합니다.', message=lang['UpdateFailed'], app_name='Adobe Discord RPC', app_icon='icon_alpha.ico') b = False time.sleep(30) else: log("DEBUG", "pypresence Return : %s" % (rtn), datetime) time.sleep(30) #time.sleep(5) # 테스트용인데 왜 주석 안하고 배포한거야 dur = True window_title = get_window_title(pinfo[2]) filename = window_title.split( pinfo[len(pinfo) - 1]['splitBy'])[pinfo[len(pinfo) - 1]['splitIndex']] lmt = pinfo[len(pinfo) - 1][langcode]['largeText'].replace( '%Ver%', version) lmt = lmt.replace('%Filename%', filename) smt = pinfo[len(pinfo) - 1][langcode]['smallText'].replace( '%Filename%', filename) smt = smt.replace('%Ver%', version) dur = False pass except KeyboardInterrupt: log("DEBUG", "KeyboardInterrupted by console", datetime) goout() except Exception as e: if dur: # PID 변경 OR 프로그램 종료. 같은 PID 금방 다시 할당할 일 없음. log("DEBUG", "PID 변동 OR 프로세스 종료. : %s -> %s" % (pinfo, e), datetime) RPC.clear(pid=os.getpid()) b = False do() pass else: log("ERROR", "Undefined : %s" % (e), datetime) goout()
def main(): consoleargs = parser.parse_args() switch_ip = consoleargs.ip client_id = consoleargs.client_id if not checkIP(switch_ip): print('Invalid IP') exit() rpc = Presence(str(client_id)) try: rpc.connect() rpc.clear() except: print('Unable to start RPC!') switch_server_address = (switch_ip, TCP_PORT) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(switch_server_address) print('Successfully connected to %s' % switch_ip + ':' + str(TCP_PORT)) except: print('Error connection to %s refused' % switch_ip + ':' + str(TCP_PORT)) exit() lastProgramName = '' startTimer = 0 while True: data = None try: data = sock.recv(628) except: print('Could not connect to Server! Retrying...') startTimer = 0 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(switch_server_address) print('Successfully reconnected to %s' % repr(switch_server_address)) except: print('Error reconnection to %s refused' % repr(switch_server_address)) exit() title = Title(data) if title.magic == PACKETMAGIC: if lastProgramName != title.name: startTimer = int(time.time()) if consoleargs.ignore_home_screen and title.name == 'Home Menu': rpc.clear() else: smallimagetext = '' largeimagekey = '' details = '' largeimagetext = title.name if int(title.pid) != PACKETMAGIC: smallimagetext = 'SwitchPresence-Rewritten' if title.name not in switchOverrides: largeimagekey = iconFromPid(title.pid) details = 'Playing ' + str(title.name) else: orinfo = switchOverrides[title.name] largeimagekey = orinfo['CustomKey'] or iconFromPid(title.pid) details = orinfo['CustomPrefix'] or 'Playing' details += ' ' + title.name else: smallimagetext = 'QuestPresence' if title.name not in questOverrides: largeimagekey = title.name.lower().replace(' ', '') details = 'Playing ' + title.name else: orinfo = questOverrides[title.name] largeimagekey = orinfo['CustomKey'] or title.name.lower().replace( ' ', '') details = orinfo['CustomPrefix'] or 'Playing' details += ' ' + title.name if not title.name: title.name = '' lastProgramName = title.name rpc.update(details=details, start=startTimer, large_image=largeimagekey, large_text=largeimagetext, small_text=smallimagetext) time.sleep(1) else: time.sleep(1) rpc.clear() rpc.close() sock.close() exit()
print('The episode title is:', data['episode']['title']) print('The show is:', data['show']['title']) print('The IMDB ID is:', data['show']['ids']['imdb']) newdetails = data['show']['title'] newstate = data['episode']['title'] # newstate='https://www.imdb.com/title/' + data['show']['ids']['imdb'] media = 'tv' elif (data['type'] == 'movie'): print('The name of the movie is:', data['movie']['title']) print('The IMDB ID is:', data['movie']['ids']['imdb']) newdetails = data['movie']['title'] newstate = 'https://www.imdb.com/title/' + data['movie']['ids'][ 'imdb'] media = 'movie' else: print('Ya f****d bud') starttime = data['started_at'] starttime = dp.parse(starttime) starttime = starttime.astimezone(est) starttime = starttime.timestamp() endtime = data['expires_at'] endtime = dp.parse(endtime) endtime = endtime.astimezone(est) endtime = endtime.timestamp() print(starttime, endtime) updateRPC(newstate, newdetails, starttime, endtime, media) except: print('Nothing is being played') RPC.clear() time.sleep(15)
class HistoryViewModel(QtCore.QObject): # Constants __INITIAL_SCROBBLE_HISTORY_COUNT = int( os.environ.get('INITIAL_HISTORY_ITEMS', 30)) # 30 is the default but can be configured __MEDIA_PLAYER_POLLING_INTERVAL = 100 if os.environ.get('MOCK') else 1000 __CURRENT_SCROBBLE_INDEX = -1 __NO_SELECTION_INDEX = -2 __DISCORD_RPC_MAX_RETRIES = 5 # Qt Property changed signals is_enabled_changed = QtCore.Signal() current_scrobble_data_changed = QtCore.Signal() scrobble_percentage_changed = QtCore.Signal() is_player_paused_changed = QtCore.Signal() is_spotify_plugin_available_changed = QtCore.Signal() is_using_mock_player_changed = QtCore.Signal() is_discord_rich_presence_enabled_changed = QtCore.Signal() selected_scrobble_changed = QtCore.Signal() selected_scrobble_index_changed = QtCore.Signal() is_loading_changed = QtCore.Signal() media_player_name_changed = QtCore.Signal() # Scrobble history list model signals pre_append_scrobble = QtCore.Signal() post_append_scrobble = QtCore.Signal() scrobble_album_image_changed = QtCore.Signal(int) scrobble_lastfm_is_loved_changed = QtCore.Signal(int) begin_refresh_history = QtCore.Signal() end_refresh_history = QtCore.Signal() # Signals handled from QML preloadProfileAndFriends = QtCore.Signal() def __init__(self) -> None: QtCore.QObject.__init__(self) # TODO: Figure out which things should be in reset_state self.__application_reference: ApplicationViewModel = None self.__is_enabled: bool = False self.__media_player: MediaPlayerPlugin = None self.__is_spotify_plugin_available = False # This is set to true if Spotify is installed # Set up Discord presence self.__is_discord_rpc_enabled = False self.__is_discord_rpc_connected = False self.__discord_rpc = Presence('799678908819439646') # Settings # TODO: Move these properties to an App view model self.__is_submission_enabled: bool = None self.is_player_paused = False self.__was_last_player_event_paused: bool = False if os.environ.get('MOCK'): self.__media_player = MockPlayerPlugin() self.__connect_media_player_signals() else: # Initialize media player plugins self.__spotify_plugin = SpotifyPlugin() self.__music_app_plugin = MusicAppPlugin() use_spotify = False spotify_app = SBApplication.applicationWithBundleIdentifier_( 'com.spotify.client') # Use Music app plugin by default since every Mac has it (this will be changed when the user preference is loaded from the database) # TODO: Make it possible for no media player to be set during onboarding so we don't need to have this temporary value? self.switchToMediaPlugin('musicApp', should_update_in_database=False) self.media_player_name_changed.emit() # Start polling interval to check for new media player position self.__timer = QtCore.QTimer(self) self.__timer.timeout.connect(self.__fetch_new_media_player_position) self.reset_state() def reset_state(self) -> None: # Store Scrobble objects that have been submitted self.scrobble_history: List[Scrobble] = [] # Keep track of whether the history view is loading data self.__is_loading = False # Hold a Scrobble object for currently playing track (will later be submitted) self.__current_scrobble: Scrobble = None # Hold the index of the selected scrobble in the sidebar self.__selected_scrobble_index: int = None # Hold the Scrobble object at the __selected_scrobble_index # This can either be a copy of the current scrobble or one in the history self.selected_scrobble: Scrobble = None # Keep track of whether the current scrobble has hit the threshold for submission self.__current_scrobble_percentage: float = None self.__should_submit_current_scrobble = False # Keep track of furthest position reached in a song self.__furthest_player_position_reached: float = None # Keep track of how many poll ticks have passed since the playback position changed self.__cached_playback_position: float = None self.__ticks_since_position_change: int = None # Store the current track's crop values since they aren't part of the scrobble object self.__current_track_crop: TrackCrop = None if (self.__is_enabled): self.__is_spotify_plugin_available = False # Load preferences from database media_player_preference = db_helper.get_preference('media_player') is_rich_presence_enabled = db_helper.get_preference( 'rich_presence_enabled') # Check if Spotify is installed; show option to switch media player in status bar menu if it's installed spotify_app = SBApplication.applicationWithBundleIdentifier_( 'com.spotify.client') if spotify_app: self.__is_spotify_plugin_available = True self.is_spotify_plugin_available_changed.emit() if media_player_preference == 'spotify': # If Spotify is not installed, reset media player preference back to Music.app # TODO: Use better method to figure out if Spotify is installed without logged error if self.__is_spotify_plugin_available: self.switchToMediaPlugin('spotify', should_update_in_database=False) else: logging.info( 'Spotify not detected on device; switching back to Music.app as media player' ) db_helper.set_preference('media_player', 'musicApp') media_player_preference = 'musicApp' if media_player_preference == 'musicApp': self.switchToMediaPlugin('musicApp', should_update_in_database=False) self.media_player_name_changed.emit() # Try connecting to Discord rich presence if enabled self.__is_discord_rpc_enabled = db_helper.get_preference( 'rich_presence_enabled') self.is_discord_rich_presence_enabled_changed.emit() if self.__is_discord_rpc_enabled and helpers.is_discord_open(): self.__connect_discord_rpc_if_open() # --- Qt Property Getters and Setters --- def set_is_discord_rich_presence_enabled(self, is_enabled): self.__is_discord_rpc_enabled = is_enabled if is_enabled: if self.__connect_discord_rpc_if_open(): # If there is a track playing when rich presence is enabled, update the rich presence immediately # This is nested because we want to the __connect_discord_rpc_if_open logic to run regardless if self.__current_scrobble: self.__update_discord_rpc( self.__current_scrobble.track_title, self.__current_scrobble.artist_name, self.__current_scrobble.album_title, self.__cached_playback_position) else: if self.__is_discord_rpc_connected: self.__is_discord_rpc_connected = False if helpers.is_discord_open(): self.__discord_rpc.close() self.is_discord_rich_presence_enabled_changed.emit() db_helper.set_preference('rich_presence_enabled', is_enabled) def set_application_reference(self, new_reference: ApplicationViewModel) -> None: if not new_reference: return self.__application_reference = new_reference self.__application_reference.is_logged_in_changed.connect( lambda: self.set_is_enabled(self.__application_reference. is_logged_in)) def get_selected_scrobble_index(self): '''Make the private selected scrobble index variable available to the UI''' if self.__selected_scrobble_index is None: # NO_SELECTION_INDEX represents no selection because Qt doesn't understand Python's None value return HistoryViewModel.__NO_SELECTION_INDEX return self.__selected_scrobble_index def set_selected_scrobble_index(self, new_index: int) -> None: if not self.__is_enabled: return # Prevent setting an illegal index with keyboard shortcuts if ( # Prevent navigating before current scrobble new_index < HistoryViewModel.__CURRENT_SCROBBLE_INDEX or ( # Prevent navigating to current scrobble when there isn't one new_index == HistoryViewModel.__CURRENT_SCROBBLE_INDEX and self.__current_scrobble is None) # Prevent navigating past scrobble history or new_index == len(self.scrobble_history)): return self.__selected_scrobble_index = new_index self.selected_scrobble_index_changed.emit() if new_index == HistoryViewModel.__CURRENT_SCROBBLE_INDEX: self.selected_scrobble = self.__current_scrobble else: self.selected_scrobble = self.scrobble_history[new_index] # Update details view self.selected_scrobble_changed.emit() def set_is_enabled(self, is_enabled: bool) -> None: self.__is_enabled = is_enabled self.is_enabled_changed.emit() if is_enabled: # Reset view model self.reset_state() self.__timer.start(self.__MEDIA_PLAYER_POLLING_INTERVAL) # Check for network connection on open self.__application_reference.update_is_offline() if not self.__application_reference.is_offline: self.reloadHistory() # Don't preload profile and friends in mock mode since it sends off tons of requests if not os.environ.get('MOCK'): self.preloadProfileAndFriends.emit() else: self.begin_refresh_history.emit() self.reset_state() self.end_refresh_history.emit() self.selected_scrobble_index_changed.emit() self.selected_scrobble_changed.emit( ) # This causes details pane to stop showing a scrobble self.current_scrobble_data_changed.emit() self.__timer.stop() self.is_loading_changed.emit() # --- Slots --- @QtCore.Slot() def reloadHistory(self): '''Reload recent scrobbles from Last.fm''' if (self.__INITIAL_SCROBBLE_HISTORY_COUNT == 0 # Prevent reloading during onboarding or not self.__is_enabled # Prevent reloading while the view is already loading or self.__is_loading): return # Update loading indicator self.__is_loading = True self.is_loading_changed.emit() # Reset scrobble history list self.begin_refresh_history.emit() self.scrobble_history = [] self.end_refresh_history.emit() # Fetch and load recent scrobbles fetch_recent_scrobbles_task = FetchRecentScrobbles( lastfm=self.__application_reference.lastfm, count=self.__INITIAL_SCROBBLE_HISTORY_COUNT) fetch_recent_scrobbles_task.finished.connect( self.__handle_recent_scrobbles_fetched) QtCore.QThreadPool.globalInstance().start(fetch_recent_scrobbles_task) @QtCore.Slot(int) def toggleLastfmIsLoved(self, scrobble_index: int) -> None: if not self.__is_enabled: return scrobble = None if scrobble_index == HistoryViewModel.__CURRENT_SCROBBLE_INDEX: scrobble = self.__current_scrobble else: scrobble = self.scrobble_history[scrobble_index] new_value = not scrobble.lastfm_track.is_loved scrobble.lastfm_track.is_loved = new_value # Update any matching scrobbles in history for history_scrobble in self.scrobble_history: if history_scrobble == scrobble: history_scrobble.lastfm_track.is_loved = new_value # Update UI to reflect changes self.__emit_scrobble_ui_update_signals(scrobble) # Submit new value to Last.fm QtCore.QThreadPool.globalInstance().start( UpdateTrackLoveOnLastfm(lastfm=self.__application_reference.lastfm, scrobble=scrobble, value=new_value)) @QtCore.Slot(str) def switchToMediaPlugin(self, media_plugin_name: str, should_update_in_database: bool = True) -> None: # Fake stopped event to un-load the current scrobble self.__handle_media_player_stopped() # Reset scrobble percentage self.__current_scrobble_percentage = 0 # Reset whether the last event was a pause self.__was_last_player_event_paused = False # Disconnect event signals if self.__media_player: self.__media_player.stopped.disconnect( self.__handle_media_player_stopped) self.__media_player.playing.disconnect( self.__handle_media_player_playing) self.__media_player.paused.disconnect( self.__handle_media_player_paused) if media_plugin_name == 'spotify': self.__media_player = self.__spotify_plugin elif media_plugin_name == 'musicApp': self.__media_player = self.__music_app_plugin if should_update_in_database: db_helper.set_preference('media_player', media_plugin_name) self.__set_is_scrobble_submission_enabled( self.__media_player.IS_SUBMISSION_ENABLED) logging.info( f'Switched media player to {self.__media_player.MEDIA_PLAYER_NAME}' ) self.__connect_media_player_signals() self.__media_player.request_initial_state() # Update 'Listening on X' text in history view for current scrobble self.media_player_name_changed.emit() def __connect_media_player_signals(self): # Reconnect event signals self.__media_player.stopped.connect(self.__handle_media_player_stopped) self.__media_player.playing.connect(self.__handle_media_player_playing) self.__media_player.paused.connect(self.__handle_media_player_paused) self.__media_player.showNotification.connect( lambda title, content: self.__application_reference. showNotification.emit(title, content)) @QtCore.Slot(str) def mock_event(self, event_name): '''Allow mock player events to be triggered from QML''' self.__media_player.mock_event(event_name) # --- Private Methods --- def __connect_discord_rpc_if_open(self) -> None: '''Check if Discord is open; if open, connect with a new client ID if not already connected. If closed, disconnect''' if helpers.is_discord_open(): if not self.__is_discord_rpc_connected: self.__discord_rpc.connect() self.__is_discord_rpc_connected = True logging.info(f'Discord RPC connected') else: if self.__is_discord_rpc_connected: self.__is_discord_rpc_connected = False self.__discord_rpc.close() logging.info(f'Discord RPC disconnected') return self.__is_discord_rpc_connected def __run_discord_rpc_request(self, request): '''Runs "request" lambda if Discord is open. If InvalidID occurs due to Discord generating a new client ID on relaunch, retry the request''' # Don't run any request if Discord isn't open if self.__connect_discord_rpc_if_open(): did_request_succeed = False number_of_retries = 0 while not did_request_succeed and number_of_retries < self.__DISCORD_RPC_MAX_RETRIES: try: request(self) did_request_succeed = True except InvalidID: self.__is_discord_rpc_connected = False if self.__connect_discord_rpc_if_open(): logging.info( f'Discord RPC reconnected to new client (at retry #{number_of_retries})' ) number_of_retries += 1 if number_of_retries == self.__DISCORD_RPC_MAX_RETRIES: logging.warning('Max retries exceeded on Discord RPC request') def __update_discord_rpc(self, track_title: str, artist_name: str, album_title: str, player_position: float): '''Update Discord RPC with new track details''' self.__discord_rpc.update( details=track_title, state=artist_name, large_image='music-logo', large_text='Playing on Music', small_image='lastredux-logo', small_text='Scrobbling on LastRedux', start=(datetime.now() - timedelta(seconds=player_position)). timestamp( ) # Don't include track start to accurately reflect timestamp in uncropped track ) def __handle_recent_scrobbles_fetched( self, recent_scrobbles: LastfmList[LastfmScrobble]): # Tell the history list model that we are going to change the data it relies on self.begin_refresh_history.emit() # User might not have any scrobbles (new account) if recent_scrobbles: # Convert scrobbles from history into scrobble objects for i, recent_scrobble in enumerate(recent_scrobbles.items): self.scrobble_history.append( Scrobble.from_lastfm_scrobble(recent_scrobble)) self.__load_external_scrobble_data(self.scrobble_history[i]) self.end_refresh_history.emit() self.__is_loading = False self.is_loading_changed.emit() def __load_external_scrobble_data(self, scrobble: Scrobble) -> None: if self.__application_reference.is_offline: return load_lastfm_track_info = LoadLastfmTrackInfo( self.__application_reference.lastfm, scrobble) load_lastfm_track_info.finished.connect( self.__handle_piece_of_external_scrobble_data_loaded) QtCore.QThreadPool.globalInstance().start(load_lastfm_track_info) load_lastfm_artist_info = LoadLastfmArtistInfo( self.__application_reference.lastfm, scrobble) load_lastfm_artist_info.finished.connect( self.__handle_piece_of_external_scrobble_data_loaded) QtCore.QThreadPool.globalInstance().start(load_lastfm_artist_info) load_lastfm_album_info = LoadLastfmAlbumInfo( self.__application_reference.lastfm, scrobble) load_lastfm_album_info.finished.connect( self.__handle_piece_of_external_scrobble_data_loaded) QtCore.QThreadPool.globalInstance().start(load_lastfm_album_info) load_track_images = LoadTrackImages( self.__application_reference.lastfm, self.__application_reference.art_provider, scrobble) load_track_images.finished.connect( self.__handle_piece_of_external_scrobble_data_loaded) QtCore.QThreadPool.globalInstance().start(load_track_images) def __handle_piece_of_external_scrobble_data_loaded( self, scrobble: Scrobble): if not self.__is_enabled: return # TODO: Find a better way to do this that's less hacky if not getattr(scrobble, 'TEMP_things_loaded', False): scrobble.TEMP_things_loaded = 0 scrobble.TEMP_things_loaded += 1 if scrobble.TEMP_things_loaded == 4: scrobble.is_loading = False self.__emit_scrobble_ui_update_signals(scrobble) def __emit_scrobble_ui_update_signals(self, scrobble: Scrobble) -> None: if not self.__is_enabled: return # Update details view if needed (all external scrobble data) if scrobble == self.selected_scrobble: self.selected_scrobble_changed.emit() # Update current scrobble view if needed (album art, is_loved) if scrobble == self.__current_scrobble: self.current_scrobble_data_changed.emit() # Update loved status and album art for all applicable history items if they match for i, history_scrobble in enumerate(self.scrobble_history): if scrobble == history_scrobble: self.scrobble_album_image_changed.emit(i) self.scrobble_lastfm_is_loved_changed.emit(i) def __submit_scrobble(self, scrobble: Scrobble) -> None: '''Add a scrobble object to the history array and submit it to Last.fm''' if not self.__is_enabled: return # Tell scrobble history list model that a change will be made self.pre_append_scrobble.emit() # Prepend the new scrobble to the scrobble_history array in the view model self.scrobble_history.insert(0, scrobble) # Tell scrobble history list model that a change was made in the view model # The list model will call the data function in the background to get the new data self.post_append_scrobble.emit() if not self.__selected_scrobble_index is None: # Shift down the selected scrobble index if new scrobble has been added to the top # This is because if the user has a scrobble in the history selected and a new scrobble is # submitted, it will display the wrong data if the index isn't updated # Change __selected_scrobble_index instead of calling set___selected_scrobble_index because the # selected scrobble shouldn't be redundantly set to itself and still emit selected_scrobble_changed if self.__selected_scrobble_index > HistoryViewModel.__CURRENT_SCROBBLE_INDEX: # Shift down the selected scrobble index by 1 self.__selected_scrobble_index += 1 # Tell the UI that the selected index changed, so it can update the selection highlight self.selected_scrobble_index_changed.emit() # Submit scrobble to Last.fm if self.__is_submission_enabled: # Use end time to submit to play more nicely with apps like Marvis # TODO: Make this a setting self.__current_scrobble.timestamp = datetime.now() submit_scrobble_task = SubmitScrobble( lastfm=self.__application_reference.lastfm, scrobble=self.__current_scrobble) QtCore.QThreadPool.globalInstance().start(submit_scrobble_task) # Update playcounts for scrobbles (including the one just added to history) for history_scrobble in self.scrobble_history: if history_scrobble.lastfm_track and scrobble.lastfm_track: if history_scrobble.lastfm_track == scrobble.lastfm_track: history_scrobble.lastfm_track.plays += 1 if history_scrobble.lastfm_artist and scrobble.lastfm_artist: if history_scrobble.lastfm_artist == scrobble.lastfm_artist: history_scrobble.lastfm_artist.plays += 1 # Reset flag so new scrobble can later be submitted self.__should_submit_current_scrobble = False def __update_current_scrobble( self, media_player_state: MediaPlayerState) -> None: '''Replace the current scrobble, update cached track start/finish, update the UI''' if not self.__is_enabled: return # Initialize a new Scrobble object with the updated media player state self.__current_scrobble = Scrobble( artist_name=media_player_state.artist_name, track_title=media_player_state.track_title, album_title=media_player_state.album_title, album_artist_name=media_player_state.album_artist_name, timestamp= None # TODO: Make this configurable, it should default to datetime.now() to match official app ) # Update UI content in current scrobble sidebar item self.current_scrobble_data_changed.emit() # Reset player position to temporary value until a new value can be recieved from the media player self.__furthest_player_position_reached = 0 # Refresh selected_scrobble with new __current_scrobble object if the current scrobble is selected if self.__selected_scrobble_index == HistoryViewModel.__CURRENT_SCROBBLE_INDEX: self.selected_scrobble = self.__current_scrobble # Update details pane view self.selected_scrobble_changed.emit() elif self.__selected_scrobble_index is None: self.__selected_scrobble_index = HistoryViewModel.__CURRENT_SCROBBLE_INDEX self.selected_scrobble = self.__current_scrobble # Update the current scrobble highlight and song details pane views self.selected_scrobble_index_changed.emit() # Update details pane view self.selected_scrobble_changed.emit() # Update cached media player track playback data self.__current_track_crop = media_player_state.track_crop # Load Last.fm data and album art self.__load_external_scrobble_data(self.__current_scrobble) # Reset scrobble meter self.__current_scrobble_percentage = 0 self.scrobble_percentage_changed.emit() def __fetch_new_media_player_position(self) -> None: '''Fetch the current player position timestamp from the selected media player''' if (not self.__is_enabled # Skip fetching if there isn't a track playing or not self.__current_scrobble): return fetch_player_position_task = FetchPlayerPosition(self.__media_player) fetch_player_position_task.finished.connect( self.__handle_player_position_fetched) QtCore.QThreadPool.globalInstance().start(fetch_player_position_task) def __handle_player_position_fetched(self, player_position: float) -> None: '''Update furthest player position reached if needed based on player position data''' if not self.__is_enabled: return # Only update the furthest reached position if it's further than the last recorded furthest position # This is because if the user scrubs backward in the track, the scrobble progress bar will stop # moving until they reach the previous furthest point reached in the track # TODO: Add support for different scrobble submission styles such as counting seconds of playback if player_position >= self.__furthest_player_position_reached: self.__furthest_player_position_reached = player_position # Count how many ticks have passed since the playback position changed (used for Discord RPC) if player_position == self.__cached_playback_position: self.__ticks_since_position_change += 1 # Clear discord status if paused for more than 60 seconds if self.__ticks_since_position_change == 60 and self.__is_discord_rpc_enabled: self.__run_discord_rpc_request( lambda self: self.__discord_rpc.clear()) else: self.__ticks_since_position_change = 0 self.__cached_playback_position = player_position self.__current_scrobble_percentage = self.__determine_current_scrobble_percentage( ) self.scrobble_percentage_changed.emit() def __determine_current_scrobble_percentage(self) -> int: '''Determine the percentage of the track that has played''' if not self.__is_enabled or not self.__current_scrobble: return 0 # Skip calculations if the track has already reached the scrobble threshold if self.__should_submit_current_scrobble: return 1 # Compensate for custom track start and end times # TODO: Only do this if the media player is the mac Music app relative_position = self.__furthest_player_position_reached - self.__current_track_crop.start relative_track_length = self.__current_track_crop.finish - self.__current_track_crop.start min_scrobble_length = relative_track_length * 0.75 # TODO: Grab the percentage from settings # Prevent scenarios where the relative position is negative relative_position = max(0, relative_position) scrobble_percentage = relative_position / min_scrobble_length # Prevent scenarios where the relative player position is greater than the relative track length # (don't let the percentage by greater than 1) scrobble_percentage = min(scrobble_percentage, 1) # Submit current scrobble if the progress towards the scrobble threshold is 100% if scrobble_percentage == 1: self.__should_submit_current_scrobble = True return scrobble_percentage def __set_is_scrobble_submission_enabled(self, value: bool) -> None: # Ignore any changes to is_submission_enabled if it's disabled globally if os.environ.get('DISABLE_SUBMISSION'): self.__is_submission_enabled = False else: self.__is_submission_enabled = value logging.info( f'Scrobble submission is set to {self.__is_submission_enabled}') def __handle_media_player_stopped(self) -> None: '''Handle media player stop event (no track is loaded)''' if (not self.__is_enabled # Don't do anything this case if there was nothing playing previously or not self.__current_scrobble): return if self.__is_discord_rpc_enabled: self.__run_discord_rpc_request( lambda self: self.__discord_rpc.clear()) # Submit if the music player stops as well, not just when a new track starts if self.__should_submit_current_scrobble: self.__submit_scrobble(self.__current_scrobble) # Reset current scrobble self.__current_scrobble = None # Update the UI in current scrobble sidebar item self.current_scrobble_data_changed.emit() # If the current scrobble is selected, deselect it if self.__selected_scrobble_index == HistoryViewModel.__CURRENT_SCROBBLE_INDEX: self.__selected_scrobble_index = None self.selected_scrobble = None # Update the current scrobble highlight and song details pane views self.selected_scrobble_index_changed.emit() self.selected_scrobble_changed.emit() def __handle_media_player_playing( self, new_media_player_state: MediaPlayerState) -> None: '''Handle media player play event''' if not self.__is_enabled: return # Update now playing on Last.fm regardless of whether it's a new play if self.__is_submission_enabled: QtCore.QThreadPool.globalInstance().start( UpdateNowPlaying( lastfm=self.__application_reference.lastfm, artist_name=new_media_player_state.artist_name, track_title=new_media_player_state.track_title, album_title=new_media_player_state.album_title, album_artist_name=new_media_player_state.album_artist_name, duration=new_media_player_state.track_crop.finish - new_media_player_state.track_crop.start)) # Update Discord rich presence regardless of whether it's a new play if self.__is_discord_rpc_enabled: self.__run_discord_rpc_request( lambda self: self.__update_discord_rpc( new_media_player_state.track_title, new_media_player_state. artist_name, new_media_player_state.album_title, new_media_player_state.position)) # Update playback indicator self.is_player_paused = False self.is_player_paused_changed.emit() # If we just resumed from paused state, we don't need to continue with checking for new tracks # if self.__was_last_player_event_paused: # self.__was_last_player_event_paused = False # logging.debug(f'Ignoring resume event for {new_media_player_state.track_title}') # return # Check if the track has changed or not is_same_track = None if not self.__current_scrobble: # This is the first track is_same_track = False else: is_same_track = (self.__current_scrobble.track_title == new_media_player_state.track_title and self.__current_scrobble.artist_name == new_media_player_state.artist_name and self.__current_scrobble.album_title == new_media_player_state.album_title) # Submit the current scrobble if it hit the scrobbling threshold if not is_same_track: # # Make sure that the current track has finished playing if it's being looped # if is_same_track and not self.__was_last_player_event_paused: # track_duration = self.__current_track_crop.finish - self.__current_track_crop.start # margin_of_error = self.__MEDIA_PLAYER_POLLING_INTERVAL + 1 # if (track_duration - self.__furthest_player_position_reached) > margin_of_error: # # Track didn't finish playing # return # Submit current scrobble to Last.fm if self.__should_submit_current_scrobble: self.__submit_scrobble(self.__current_scrobble) # Load new track data into current scrobble self.__update_current_scrobble(new_media_player_state) def __handle_media_player_paused(self) -> None: '''Handle media player pause event''' # Update playback indicator self.is_player_paused = True self.is_player_paused_changed.emit() self.__was_last_player_event_paused = True # --- Qt Properties --- applicationReference = QtCore.Property( type=ApplicationViewModel, fget=lambda self: self.__application_reference, fset=set_application_reference) isEnabled = QtCore.Property(type=bool, fget=lambda self: self.__is_enabled, fset=set_is_enabled, notify=is_enabled_changed) currentScrobble = QtCore.Property( type='QVariant', fget=lambda self: asdict(self.__current_scrobble) if self.__current_scrobble else None, notify=current_scrobble_data_changed) scrobblePercentage = QtCore.Property( type=float, fget=lambda self: self.__current_scrobble_percentage, notify=scrobble_percentage_changed) isSpotifyPluginAvailable = QtCore.Property( type=bool, fget=lambda self: self.__is_spotify_plugin_available, notify=is_spotify_plugin_available_changed) isUsingMockPlayer = QtCore.Property( type=bool, fget=lambda self: isinstance(self.__media_player, MockPlayerPlugin), notify=is_using_mock_player_changed) isDiscordRichPresenceEnabled = QtCore.Property( type=bool, fget=lambda self: self.__is_discord_rpc_enabled, fset=set_is_discord_rich_presence_enabled, notify=is_discord_rich_presence_enabled_changed) selectedScrobbleIndex = QtCore.Property( type=int, fget=get_selected_scrobble_index, fset=set_selected_scrobble_index, notify=selected_scrobble_index_changed) isLoading = QtCore.Property(type=bool, fget=lambda self: self.__is_loading, notify=is_loading_changed) mediaPlayerName = QtCore.Property( type=str, fget=lambda self: self.__media_player.MEDIA_PLAYER_NAME, notify=media_player_name_changed)