Skip to content
This repository has been archived by the owner on Jun 8, 2023. It is now read-only.

Commit

Permalink
feat: use album covers
Browse files Browse the repository at this point in the history
  • Loading branch information
BobbyWibowo committed May 17, 2022
1 parent 2f7bb6c commit 907949b
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 10 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ name = "pypi"
[packages]
dbussy = "*"
pytoml = "*"
requests = "*"
requests_file = "*"

[dev-packages]

Expand Down
57 changes: 56 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 50 additions & 9 deletions discordrp_mpris/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

from ampris2 import Mpris2Dbussy, PlaybackStatus, PlayerInterfaces as Player, unwrap_metadata
import dbussy
import requests
from requests_file import FileAdapter
from discord_rpc.async_ import (AsyncDiscordRpc, DiscordRpcError, JSON,
exceptions as async_exceptions)

from .config import Config
from .cache import Cache

''' # master
CLIENT_ID = '435587535150907392'
Expand Down Expand Up @@ -88,11 +91,12 @@ class DiscordMpris:
active_player: Optional[Player] = None
last_activity: Optional[JSON] = None

def __init__(self, mpris: Mpris2Dbussy, discord: AsyncDiscordRpc, config: Config,
def __init__(self, mpris: Mpris2Dbussy, discord: AsyncDiscordRpc, config: Config, cache: Optional[Cache]
) -> None:
self.mpris = mpris
self.discord = discord
self.config = config
self.cache = cache

async def connect_discord(self) -> None:
if self.discord.connected:
Expand Down Expand Up @@ -175,9 +179,6 @@ async def tick(self) -> None:
# position should already be an int, but some players (smplayer) return a float
replacements = self.build_replacements(player, metadata, position, length, state)

# icons
large_image = PLAYER_ICONS[replacements['player']]

# TODO make format configurable
if replacements['artist']:
# details_fmt = "{artist} - {title}"
Expand All @@ -194,17 +195,29 @@ async def tick(self) -> None:
else:
large_text = replacements['player']

# modify large text if playing YouTube or Spotify on web browsers
# currently having interface issues with Chromium browsers
# ERROR:ampris2:Unable to fetch interfaces for player 'chrome.instanceXXXXX' - org.freedesktop.DBus.Error.UnknownInterface -- peer “org.mpris.MediaPlayer2.chrome.instanceXXXXX” object “/org/mpris/MediaPlayer2” does not understand interface “org.mpris.MediaPlayer2”
if player.bus_name == "plasma-browser-integration" and replacements['xesam_url']:
# large image
large_image = ""

if self.cache and replacements['mpris_artUrl']:
logger.debug("Attempting to use album covers")
large_image = self.cache.get(replacements['mpris_artUrl'])
if not large_image or (self.config.get('use_weserv_proxy') and 'images.weserv.nl' not in large_image):
thumbUrl = await self.upload_image(replacements['mpris_artUrl'])
self.cache.set(replacements['mpris_artUrl'], thumbUrl)
elif player.bus_name == "plasma-browser-integration" and replacements['xesam_url']:
# modify large text if playing YouTube or Spotify on web browsers
# currently having interface issues with Chromium browsers
# ERROR:ampris2:Unable to fetch interfaces for player 'chrome.instanceXXXXX' - org.freedesktop.DBus.Error.UnknownInterface -- peer “org.mpris.MediaPlayer2.chrome.instanceXXXXX” object “/org/mpris/MediaPlayer2” does not understand interface “org.mpris.MediaPlayer2”
if re.match(r'^https?://(www|music)\.youtube\.com/watch\?.*$', replacements['xesam_url'], re.M):
large_text = f"YouTube on {large_text}"
large_image = PLAYER_ICONS[large_text]
elif re.match(r'^https?://open\.spotify\.com/.*$', replacements['xesam_url'], re.M):
large_text = f"Spotify on {large_text}"
large_image = PLAYER_ICONS[large_text]

if not large_image:
large_image = PLAYER_ICONS[replacements['player']]

# set timestamps, small text (and state fallback)
activity['timestamps'] = {}
if length and position is not None:
Expand Down Expand Up @@ -331,6 +344,28 @@ async def find_active_player(self) -> Optional[Player]:
def _player_not_ignored(self, player: Player) -> bool:
return (not self.config.player_get(player, "ignore", False))

async def upload_image(self, url: str) -> Optional[str]:
try:
logger.debug("Fetching album cover")
s = requests.Session()
s.mount("file://", FileAdapter())
image = s.get(url).content
logger.debug("Uploading album cover")
data = requests.post(
"https://api.imgur.com/3/image",
headers = { "Authorization": f"Client-ID {self.config.get('imgur_client_id')}" },
files = { "image": image }
).json()
if not data["success"]:
raise Exception(data["data"]["error"])
if self.config.get('use_weserv_proxy', False):
link = data["data"]["link"].replace("https://", "")
return f"https://images.weserv.nl/?url={link}&w=128&h=128&fit=cover"
else:
return data["data"]["link"]
except:
logger.exception("An unexpected error occured while uploading an image")

@classmethod
def build_replacements(
cls,
Expand Down Expand Up @@ -421,9 +456,15 @@ async def main_async(loop: asyncio.AbstractEventLoop):
# TODO validate?
configure_logging(config)

cache = None
if config.get('show_album_covers') and config.get('imgur_client_id'):
cache = Cache.load()
if cache:
logger.debug(f"{cache.raw_cache}")

mpris = await Mpris2Dbussy.create(loop=loop)
async with AsyncDiscordRpc.for_platform(CLIENT_ID) as discord:
instance = DiscordMpris(mpris, discord, config)
instance = DiscordMpris(mpris, discord, config, cache)
return await instance.run()


Expand Down
52 changes: 52 additions & 0 deletions discordrp_mpris/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import json
import logging
import os
from pathlib import Path
from typing import Any, Dict, Optional, Tuple

logger = logging.getLogger(__name__)


class Cache:
def __init__(self, raw_cache: Dict[str, Any], cache_file: str) -> None:
self.raw_cache = raw_cache
self.cache_file = cache_file

def get(self, key: str, default: Any = None) -> Any:
if key in self.raw_cache:
return self.raw_cache.get(key)
else:
return default

def set(self, key: str, value: Any) -> None:
self.raw_cache[key] = value
try:
with open(self.cache_file, "w", encoding = "UTF-8") as file:
json.dump(self.raw_cache, file, separators = (",", ":"))
except:
logger.exception("Failed to write to the application's cache file.")

@classmethod
def load(cls) -> Optional['Cache']:
# TODO create an empty JSON file if not exist
user_cache, user_cache_file = cls._load_user_cache()
if user_cache != None:
return Cache(user_cache, user_cache_file)

return None

@staticmethod
def _load_user_cache() -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
user_patterns = ("$XDG_CONFIG_HOME", "$HOME/.config")
user_file = None

for pattern in user_patterns:
parent = Path(os.path.expandvars(pattern))
if parent.is_dir():
user_file = parent / "discordrp-mpris" / "cache.json"
if user_file.is_file():
logging.debug(f"Loading user cache: {user_file!s}")
with user_file.open() as f:
return json.load(f), user_file

return None, None
7 changes: 7 additions & 0 deletions discordrp_mpris/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ show_time = "elapsed"
ignore = false
# Maximum number of bytes in the title field
max_title_len = 64
# Show album covers.
# Must have an empty JSON file named cache.json in your local discordrp-mpris config directory.
show_album_covers = false
# Imgur Client ID (must be specified for album covers).
imgur_client_id = ""
# Use images.weserv.nl proxy to resize & crop.
use_weserv_proxy = false

# You can override any of the [options] options
# for each player individually.
Expand Down

0 comments on commit 907949b

Please sign in to comment.