├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── assets ├── music_black.png └── music_white.png ├── requirements.txt ├── showcase ├── lyrics.png ├── notification.png ├── progress_bar.gif └── tooltip.png └── src ├── config.json ├── mediaplayer ├── mkstyle ├── style-catppuccin.css └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .syncedlyrics/ 2 | .python_version 3 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | waybar 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Raffaele Mancuso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # waybar-mediaplayer 2 | 3 | ## Retirement notice 4 | 5 | I don't use this project anymore and therefore I'm archiving it. 6 | 7 | Feel free to fork it. 8 | 9 | ## Introduction 10 | 11 | This is a mediaplayer for [waybar](https://github.com/Alexays/Waybar). 12 | 13 | Widget with album art and progress bar: 14 | 15 | ![progress_bar](./showcase/progress_bar.gif) 16 | 17 | Notification with album art, track ticle and track artist 18 | 19 | ![notification](./showcase/notification.png) 20 | 21 | Tooltip with track title, track artist and track album: 22 | 23 | ![tooltip](./showcase/tooltip.png) 24 | 25 | Synced lyrics in tooltip: 26 | 27 | ![lyrics](./showcase/lyrics.png) 28 | 29 | It features: 30 | 1. Progress bar 31 | 2. Tooltip that displays title, author and album 32 | 3. Synced lyrics in tooltip 33 | 4. Album cover art (click to zoom) 34 | 5. Optional notification on song change 35 | 6. Click to play/pause, scroll up/down to scroll up/down on the playlist 36 | 37 | ## Requirements 38 | 39 | [playerctl](https://github.com/altdesktop/playerctl) must be installed. 40 | 41 | The default configuration use Nerd Fonts, so it requires waybar to use a Nerd Font. 42 | 43 | The default configuration use [feh](https://github.com/derf/feh) to open the album art image. 44 | 45 | ## Install 46 | 47 | 1. Create a virtual environment within which to run waybar. For example, using pyenv this would be: 48 | 49 | ``` 50 | pyenv virtualenv 3.12.0 waybar 51 | pyenv activate waybar 52 | ``` 53 | 54 | 1. Create a wrapper script to run waybar inside this virtual environment. 55 | 56 | For example, always using pyenv, create the file `start_waybar` and put it in your PATH: 57 | ``` 58 | #!/usr/bin/env bash 59 | . ~/.pyenv/versions/waybar/bin/activate 60 | waybar 61 | ``` 62 | 63 | 1. Clone this repo: 64 | 65 | ``` 66 | git clone https://github.com/raffaem/waybar-mediaplayer "$HOME/.config/waybar/waybar-mediaplayer" 67 | cd "$HOME/.config/waybar/waybar-mediaplayer" 68 | ``` 69 | 70 | 1. Install the required python packages inside your virtual environment: 71 | 72 | ``` 73 | pip3 install -r requirements.txt 74 | ``` 75 | 76 | 1. You need to find the name of your player. To do so, run `playerctl --list-all` while your player is running. 77 | 78 | 1. Open `"$HOME/.config/waybar/waybar-mediaplayer/src/config.json` and change `player_name` with the name of your player. 79 | 80 | 1. Put the following in `$HOME/.config/waybar/config`: 81 | 82 | ``` 83 | "modules-left": ["image", "custom/mediaplayer"], 84 | ``` 85 | 86 | ``` 87 | "image": { 88 | "path": "/tmp/waybar-mediaplayer-art", 89 | "size": 32, 90 | "signal": 4, 91 | "on-click": "feh --auto-zoom --borderless --title 'feh-float' /tmp/waybar-mediaplayer-art" 92 | }, 93 | 94 | "custom/mediaplayer": { 95 | "exec": "$HOME/.config/waybar/waybar-mediaplayer/src/mediaplayer monitor", 96 | "return-type": "json", 97 | "format": "{}", 98 | "on-click": "$HOME/.config/waybar/waybar-mediaplayer/src/mediaplayer play-pause", 99 | "on-scroll-up": "$HOME/.config/waybar/waybar-mediaplayer/src/mediaplayer next", 100 | "on-scroll-down": "$HOME/.config/waybar/waybar-mediaplayer/src/mediaplayer previous", 101 | "min-length": 20, 102 | "max-length": 20 103 | }, 104 | ``` 105 | 106 | Put the following in `$HOME/.config/waybar/style.css`: 107 | 108 | ``` 109 | #custom-mediaplayer 110 | { 111 | font-size: 16px; 112 | border-radius: 2%; 113 | } 114 | @import "./waybar-mediaplayer/src/style.css"; 115 | ``` 116 | 117 | 1. Start waybar using the wrapper script `start_waybar`. 118 | 119 | The mediaplayer should work. Click on the progress bar to start/stop playing, and scroll on it to change song. 120 | 121 | ## Update 122 | 123 | ``` 124 | cd "$HOME/.config/waybar/waybar-mediaplayer" 125 | git pull 126 | ``` 127 | 128 | ## Personalization 129 | 130 | To disable notifications, put `is_notification=false` in `config.json`. 131 | 132 | To change widget's length, set `min-length` and `max-length` in `$HOME/.config/waybar/config`, and set `widget_length` in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json`. These 3 variables MUST be set to the same value. 133 | 134 | In order for the album art to automatically update on song change, it's important that the `signal` variable of the `image` module in `$HOME/.config/waybar/config` matches the `image_signal` variable in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json`. 135 | 136 | If you change the colors of the bar in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json`, make sure to apply the changes by running `$HOME/.config/waybar/waybar-mediaplayer/src/mkstyle` and restart waybar. 137 | 138 | ## Troubleshooting 139 | 140 | ### Firefox users 141 | 142 | Although Firefox reports MPRIS metadata, the metadata it reports is not sufficient, as it doesn't report song's length. Please install the [Plasma Integration](https://addons.mozilla.org/en-US/firefox/addon/plasma-integration) add-on and use `plasma-browser-integration` as `player_name` in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json`. 143 | 144 | To have album art, make sure to set `convert_to_jpeg` to `true` in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json` (this option decreases performance, don't use it if not necessary). 145 | 146 | ### Me progress bar doesn't work 147 | 148 | It's likely cause by the player not reporting song length or position back to us. Run `$HOME/.config/waybar/waybar-mediaplayer/src/mediaplayer -vvv` to debug. 149 | 150 | ### Player reports its name with instance number 151 | 152 | If the player reports an instance after its name, please provide only the player name without the instance number. For example, [kew](https://github.com/ravachol/kew) may report itself as `kew123456`, where `123456` is an instance number which will change with different runs of `kew`. In this case, we report only `kew` without the instance number. This software will check whether the reported player name _starts_ with the name you provide to bind the correct player. 153 | 154 | ### Me title and tooltip are empty 155 | 156 | It's likely your song file doesn't contain metadata. 157 | 158 | Run `exiftool SONG.mp3` and check the `Title`, `Album` and `Artist` fields. 159 | 160 | ### Me album art doesn't change on song change 161 | 162 | Make sure that the `signal` in the `image` module in `$HOME/.config/waybar/config` matches the number provided by `image_signal` in `$HOME/.config/waybar/waybar-mediaplayer/src/config.json`. 163 | 164 | -------------------------------------------------------------------------------- /assets/music_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/assets/music_black.png -------------------------------------------------------------------------------- /assets/music_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/assets/music_white.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | pycairo 3 | PyGObject 4 | syncedlyrics 5 | -------------------------------------------------------------------------------- /showcase/lyrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/showcase/lyrics.png -------------------------------------------------------------------------------- /showcase/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/showcase/notification.png -------------------------------------------------------------------------------- /showcase/progress_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/showcase/progress_bar.gif -------------------------------------------------------------------------------- /showcase/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffaem/waybar-mediaplayer/ff5f9ad675f3726461d1d55e348c320201aa5d6b/showcase/tooltip.png -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "refresh_interval": 500, 3 | "is_notification": false, 4 | "notification_min_interval": 2, 5 | "widget_length": 20, 6 | "sepchar": "", 7 | "surface_color": "gray", 8 | "overlay_color": "cyan", 9 | "interval": 1, 10 | "text_rot_int": 1000, 11 | "image_signal": 4, 12 | "length_factor": 1, 13 | "player_name": "spotify", 14 | "convert_to_jpeg": false, 15 | "album_art_placeholder": "no", 16 | "lyrics_providers": [ 17 | "Lrclib", 18 | "Musixmatch", 19 | "NetEase", 20 | "Megalobiz" 21 | ], 22 | "lyrics_span_before": 2, 23 | "lyrics_span_after": 2 24 | } 25 | -------------------------------------------------------------------------------- /src/mediaplayer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2024 Raffaele Mancuso 3 | # SPDX-License-Identifier: MIT 4 | # Inspired by: https://github.com/Alexays/Waybar/blob/ 5 | # master/resources/custom_modules/mediaplayer.py 6 | import argparse 7 | import io 8 | import json 9 | import logging 10 | import math 11 | import re 12 | import shutil 13 | import subprocess 14 | import sys 15 | import time 16 | import urllib 17 | from collections import namedtuple 18 | from functools import partial 19 | from pathlib import Path 20 | 21 | import gi 22 | import requests 23 | import syncedlyrics 24 | from PIL import Image 25 | 26 | gi.require_version("Playerctl", "2.0") 27 | from gi.repository import GLib, Playerctl # noqa: E402 28 | 29 | # Internal global variables 30 | config = None 31 | logger = logging.getLogger(__name__) 32 | artfp = Path("/tmp/waybar-mediaplayer-art") 33 | last_metadata = None 34 | last_art_url = None 35 | last_rot = 0 36 | last_rot_time = 0 37 | last_notification_time = 0 38 | # Whether refresh_interval callback is registered 39 | is_refint = False 40 | format_class = None 41 | icons = {"play": " ", "stop": " "} 42 | is_text_rotating = None 43 | lyrics = dict() 44 | 45 | 46 | def on_player_appeared(manager, player, requested_player): 47 | logger.debug("I was called") 48 | if player and (player.name == requested_player): 49 | init_player(manager, player) 50 | else: 51 | logger.debug( 52 | "New player appeared, but it's not the selected player, skipping" 53 | ) 54 | 55 | logger.debug("Returning True") 56 | return True 57 | 58 | 59 | def delete_album_art(): 60 | p = Path(__file__) 61 | if config["album_art_placeholder"] == "dark": 62 | phfp = p.parents[1] / "assets" / "music_black.png" 63 | elif config["album_art_placeholder"] == "light": 64 | phfp = p.parents[1] / "assets" / "music_white.png" 65 | elif config["album_art_placeholder"] == "no": 66 | phfp = None 67 | else: 68 | raise Exception( 69 | f"ERROR: Invalid value {config['album_art_placeholder']} " 70 | "for `album_art_placeholder` option" 71 | ) 72 | if phfp: 73 | shutil.copy(phfp, artfp) 74 | else: 75 | if artfp.is_file(): 76 | artfp.unlink() 77 | signal_album_art_change() 78 | 79 | 80 | def on_player_vanished(manager, player): 81 | logger.info("I was called") 82 | write_output() 83 | delete_album_art() 84 | logger.info("Returning True") 85 | return True 86 | 87 | 88 | def on_metadata(player, metadata, manager): 89 | logger.debug("I was called") 90 | update_metadata(player, metadata, manager) 91 | logger.debug("Returning True") 92 | return True 93 | 94 | 95 | def on_playback_status(player, status, manager): 96 | logger.info("I was called") 97 | logger.debug(f"status={status}") 98 | # Update the icon in the progressbar (play/pause) 99 | logger.debug("Calling register_refresh_interval_callback") 100 | register_refresh_interval_callback(player, manager) 101 | logger.debug("Calling update_progressbar") 102 | update_progressbar(manager, player) 103 | logger.debug("Returning True") 104 | return True 105 | 106 | 107 | def on_refresh_interval(manager): 108 | global is_refint 109 | logger.debug("I was called") 110 | # If there are no players, stop calling this handler 111 | if len(manager.props.players) == 0: 112 | logger.debug("Returning False") 113 | is_refint = False 114 | return False 115 | player = manager.props.players[0] 116 | # If we are not playing, stop calling this handler 117 | is_playing = player.props.status == "Playing" 118 | if not is_playing: 119 | logger.debug("Returning False") 120 | is_refint = False 121 | return False 122 | # Update progressbar and return 123 | update_progressbar(manager, player) 124 | logger.debug("Returning True") 125 | return True 126 | 127 | 128 | def write_output(text=None, class_=None, tooltip=""): 129 | global config 130 | # Default values if not explicitly set 131 | if not text: 132 | text = "" + (" " * (config["widget_length"] - 1)) 133 | if not class_: 134 | class_ = gen_class(0.0) 135 | if not tooltip: 136 | tooltip = "" 137 | # HTML entities need to be escaped 138 | text = text.replace("&", "&") 139 | tooltip = tooltip.replace("&", "&") 140 | # Write to stdout 141 | output = { 142 | "text": text, 143 | "class": class_, 144 | "tooltip": "" + tooltip + "", 145 | } 146 | sys.stdout.write(json.dumps(output) + "\n") 147 | sys.stdout.flush() 148 | 149 | 150 | def register_refresh_interval_callback(player, manager): 151 | global is_refint 152 | logger.info("I was called") 153 | is_playing = player.props.status == "Playing" 154 | if is_playing and (not is_refint): 155 | logger.debug("Registering `refresh_interval` callback") 156 | GLib.timeout_add( 157 | config["refresh_interval"], on_refresh_interval, manager 158 | ) 159 | is_refint = True 160 | 161 | 162 | def rotate_str_left(s, rot): 163 | return s[rot:] + s[:rot] 164 | 165 | 166 | def gen_widget_text(player): 167 | global last_rot, last_rot_time, is_text_rotating 168 | 169 | metadata = player.props.metadata 170 | # artist = player.get_artist() 171 | # artist = metadata["xesam:artist"][0] 172 | title = player.get_title() or "" 173 | # title = metadata["xesam:title"] 174 | 175 | icon = icons["play"] if player.props.status != "Playing" else icons["stop"] 176 | 177 | if ( 178 | player.props.player_name == "spotify" 179 | and "mpris:trackid" in metadata.keys() 180 | and ":ad:" in metadata["mpris:trackid"] 181 | ): 182 | song_info = "AD PLAYING" 183 | else: 184 | song_info = title.strip() 185 | 186 | if not is_text_rotating: 187 | text = left_text(icon + song_info) 188 | else: 189 | song_info += " " + config["sepchar"] + " " 190 | elapsed = (time.time() - last_rot_time) * 1000 191 | rot_needs_update = elapsed > config["text_rot_int"] 192 | if rot_needs_update: 193 | to_add = math.floor(elapsed / config["text_rot_int"]) 194 | l_rot = (last_rot + to_add) % len(song_info) 195 | last_rot = l_rot 196 | last_rot_time = time.time() 197 | else: 198 | l_rot = last_rot 199 | song_info = rotate_str_left(song_info, l_rot) 200 | text = icon + song_info 201 | 202 | return text 203 | 204 | 205 | def signal_album_art_change(): 206 | cmd = ["pkill", "-RTMIN+" + str(config["image_signal"]), "waybar"] 207 | res = subprocess.run(cmd) 208 | logger.debug(f"pkill returned: {res}") 209 | 210 | 211 | def download_art(art_url): 212 | logger.debug("Updating album art") 213 | if art_url.startswith("http"): 214 | resp = requests.get(art_url) 215 | with open(artfp, "wb") as fh: 216 | fh.write(resp.content) 217 | logger.debug( 218 | f"Album art '{art_url}' downloaded and saved into '{artfp}'" 219 | ) 220 | elif art_url.startswith("file://"): 221 | art_url = art_url[7:] 222 | art_url = urllib.parse.unquote(art_url) 223 | # symlinking the file doesn't work 224 | # apparently symlinks are not followed 225 | # artfp.symlink_to(art_url) 226 | shutil.copy(art_url, artfp) 227 | logger.debug(f"Album art '{art_url}' copied to '{artfp}'") 228 | elif Path(art_url).is_file(): 229 | shutil.copy(art_url, artfp) 230 | logger.debug(f"Album art '{art_url}' copied to '{artfp}'") 231 | else: 232 | logger.debug( 233 | f"Invalid schema for {art_url}. Please report this as a bug." 234 | ) 235 | return 236 | # Convert image to JPEG 237 | if config["convert_to_jpeg"]: 238 | artfp2 = artfp.parent / (artfp.stem + ".jpeg") 239 | with Image.open(artfp) as im: 240 | im.save(artfp2) 241 | shutil.move(artfp2, artfp) 242 | # Update album art on bar 243 | signal_album_art_change() 244 | 245 | 246 | def send_notification(summary, body): 247 | global last_notification_time 248 | cmd = [ 249 | "notify-send", 250 | "--app-name=waybar-mediaplayer", 251 | "--icon=" + artfp, 252 | summary, 253 | body, 254 | ] 255 | logger.error(f"Running {cmd}") 256 | subprocess.run(cmd) 257 | last_notification_time = time.time() 258 | 259 | 260 | def update_metadata(player, metadata, manager): 261 | global last_notification_time, last_art_url, is_text_rotating 262 | logger.info("I was called") 263 | metadata = dict(metadata) 264 | # Update progressbar 265 | updated = update_progressbar(manager, player) 266 | if not updated: 267 | logger.debug("Progress bar not updated. Returning") 268 | return False 269 | # Download album art 270 | art_url = metadata.get("mpris:artUrl", None) 271 | if art_url and art_url != last_art_url: 272 | download_art(art_url) 273 | last_art_url = art_url 274 | # Notification 275 | notexp = time.time() - last_notification_time 276 | if config["is_notification"] and ( 277 | notexp >= config["notification_min_interval"] 278 | ): 279 | send_notification(player.get_title(), player.get_artist()) 280 | # Does text need rotation? 281 | title = player.get_title() or "" 282 | is_text_rotating = ( 283 | len(title) + len(icons["stop"]) + 1 > config["widget_length"] 284 | ) 285 | # Lyrics 286 | artist = player.get_artist() or "" 287 | get_new_lyrics(artist, title) 288 | logger.debug("Returning") 289 | 290 | 291 | def download_lyrics(artist, title): 292 | stream = io.StringIO() 293 | lyrlogger = syncedlyrics.logger 294 | lyrlogger.propagate = False 295 | lyrlogger.handlers.clear() 296 | lyrlogger.addHandler(logging.StreamHandler(stream)) 297 | lyrlogger.setLevel(logging.INFO) 298 | try: 299 | lt = syncedlyrics.search( 300 | f"{artist} {title}", 301 | synced_only=True, 302 | providers=config["lyrics_providers"], 303 | ) 304 | except Exception: 305 | logger.debug( 306 | "ERROR: syncedlyrics.search threw an exception. Returning None.") 307 | return None 308 | if lt is None: 309 | logger.debug( 310 | "ERROR: syncedlyrics.search returned None. Returning None.") 311 | return None 312 | lt = lt.replace("\n", "") 313 | lt = lt.split("[") 314 | lt = ["["+x for x in lt if x] 315 | provider = stream.getvalue().split("on ")[1].strip() 316 | stream.close() 317 | SyncedLyrics = namedtuple("SyncedLyrics", ["lt", "provider"]) 318 | return SyncedLyrics(lt, provider) 319 | 320 | 321 | def get_new_lyrics(artist, title): 322 | global lyrics 323 | lyrics = { 324 | "secs": [0], 325 | "text": ["[INTRO]"], 326 | "last_ix": -1, 327 | "max_len": 0, 328 | "provider": None, 329 | } 330 | lyr = download_lyrics(artist, title) 331 | if lyr is None: 332 | logger.debug("ERROR: download_lyrics returned None. Returning.") 333 | lyrics = None 334 | return 335 | lyrics["provider"] = lyr.provider 336 | for i, line in enumerate(lyr.lt): 337 | m = re.match(r"\[(.*)\](.*)", line) 338 | if not m: 339 | continue 340 | # convert time from min:sec into sec 341 | timing = m.group(1) 342 | timing2 = timing.split(":") 343 | if len(timing2) != 2: 344 | logger.debug(f"ERROR: timing2={timing2} is not of length 2") 345 | continue 346 | try: 347 | secs = int(timing2[0]) * 60.0 + float(timing2[1]) 348 | except ValueError: 349 | logger.debug(f"ERROR: ValueError while converting timing2={ 350 | timing2} to seconds") 351 | continue 352 | # get lyrics text 353 | text = m.group(2).strip() 354 | if text == "": 355 | text = "[INTERMISSION]" 356 | lyrics["secs"].append(secs) 357 | lyrics["text"].append(text) 358 | lyrics["max_len"] = max(lyrics["max_len"], len(text)) 359 | # DEBUG 360 | logger.debug(str(i) + str(line) + "->" + "[" + str(secs) + "] " + text) 361 | lyrics["text"].append("[OUTRO]") 362 | lyrics["secs"].append(lyrics["secs"][-1] + 1) 363 | 364 | 365 | def left_text(s): 366 | le = len(s) 367 | if le > config["widget_length"]: 368 | return s 369 | missing = config["widget_length"] - le 370 | s = s + " " * (missing) 371 | return s 372 | 373 | 374 | def center_text(s, ml=None): 375 | if not ml: 376 | ml = max([len(x) for x in s]) 377 | for i in range(0, len(s)): 378 | delta = max(math.floor((ml - len(s[i])) / 2), 0) 379 | s[i] = (" " * delta) + s[i] + (" " * delta) 380 | return s 381 | 382 | 383 | def gen_class(perc): 384 | if not isinstance(perc, float): 385 | raise Exception(f"{perc} ({type(perc)}) is not a float") 386 | return "perc" + format_class(perc).replace(".", "-") 387 | 388 | 389 | # Called 390 | # 1. By `on_refresh_interval` 391 | # 2. By `on_metadata` 392 | # 3. By `on_playback_status` 393 | def update_progressbar(manager, player): 394 | global last_metadata, lyrics 395 | logger.debug("I was called") 396 | # Get percentage in the track 397 | pos = player.get_position() 398 | logger.debug(f"pos={pos}") 399 | # If it doesn't work, use CLI utility 400 | if pos == 0: 401 | cmd = ["playerctl", "--player=" + player.props.player_name, "position"] 402 | pos = subprocess.run(cmd, capture_output=True, text=True).stdout 403 | pos = float(pos) * (10**6) 404 | logger.debug(f"pos from CLI={pos}") 405 | pmetadata = player.props.metadata 406 | logger.debug(f"pmetadata={pmetadata}") 407 | try: 408 | # cast to int is important as some softwares, 409 | # like Amberol, are reporting a string 410 | # If the length of the track is known, 411 | # it should be provided in the metadata property 412 | # with the "mpris:length" key. 413 | # The length must be given in microseconds, 414 | # and be represented as a signed 64-bit integer 415 | # see: https://specifications.freedesktop.org/mpris-spec/2.2/Track_List_Interface.html # noqa: E501 416 | length = int(pmetadata["mpris:length"]) 417 | except KeyError: 418 | logger.debug( 419 | "ERROR: The player is not reporting the length of the song to us. " 420 | "Progress bar won't work." 421 | ) 422 | length = 100 423 | pos = 0 424 | else: 425 | logger.debug(f"length={length}") 426 | if length == 0: 427 | logger.debug( 428 | "ERROR: Length of song is 0. Progress bar won't work." 429 | ) 430 | length = 100 431 | pos = 0 432 | # Compute song percentage 433 | perc = (pos / (length / config["length_factor"])) * 100 434 | perc = min(perc, 100.0) 435 | metadata = { 436 | "title": player.get_title() or "", 437 | "artist": player.get_artist() or "", 438 | "album": player.get_album() or "", 439 | "pos": pos, 440 | "length": length, 441 | "perc": perc, 442 | } 443 | # Check if a rotating text needs to be updated 444 | elapsed = (time.time() - last_rot_time) * 1000 445 | rot_needs_update = elapsed > config["text_rot_int"] 446 | # Check if progress bar needs to be updated 447 | diff = metadata["perc"] - last_metadata["perc"] if last_metadata else 99999 448 | pbar_needs_update = diff > config["interval"] 449 | # Check if lyrics needs an update 450 | if lyrics: 451 | # pos is in microseconds 452 | lyr_pos = float(pos / (10**6)) 453 | lyrics_ix = sum(lyr_pos >= x for x in lyrics["secs"]) - 1 454 | lyrics_same = lyrics_ix == lyrics["last_ix"] 455 | else: 456 | lyrics_same = True 457 | # Check if song is the same (otherwise text needs to be updated) 458 | same_song = ( 459 | last_metadata 460 | and metadata["title"] == last_metadata["title"] 461 | and metadata["artist"] == last_metadata["artist"] 462 | and metadata["album"] == last_metadata["album"] 463 | and metadata["length"] == last_metadata["length"] 464 | and lyrics_same 465 | ) 466 | # Check if we need to update 467 | if same_song and \ 468 | not pbar_needs_update and \ 469 | not rot_needs_update: 470 | logger.debug( 471 | "Song is the same and neither the text " 472 | "nor the progressbar nor the lyrics needs an update. Returning" 473 | ) 474 | return False 475 | # Write output 476 | # - Widget text 477 | widget_text = gen_widget_text(player) 478 | # - Tooltip text 479 | s = [metadata["title"], metadata["artist"], metadata["album"]] 480 | # -- Lyrics 481 | curr_line = None 482 | text_len = None 483 | if lyrics: 484 | cand = [len(x) for x in s] 485 | cand.append(lyrics.get("max_len", 3)) 486 | text_len = max(cand) 487 | s.append("-" * text_len) 488 | provider = lyrics.get("provider", "") 489 | if provider is None: 490 | provider = "NO PROVIDER" 491 | s.append("["+provider+"]") 492 | 493 | # DEBUG 494 | # print(f"lyrics['secs']={lyrics['secs']}") 495 | # print(f"pos={pos}; length={length}") 496 | # print(f"lyr_pos={lyr_pos}; lyrics_ix={lyrics_ix}") 497 | 498 | lyrics["last_ix"] = lyrics_ix 499 | span_before = config["lyrics_span_before"] 500 | span_after = config["lyrics_span_after"] 501 | # Make sure we always have the same number of lines 502 | # also at start and end of the song 503 | span_before += max(lyrics_ix + span_after - len(lyrics["text"]) + 1, 0) 504 | span_after += max(span_before - lyrics_ix, 0) 505 | # Loop over lyrics window 506 | to_put = range( 507 | max(lyrics_ix - span_before, 0), 508 | min(lyrics_ix + span_after, len(lyrics["text"]) - 1) + 1, 509 | ) 510 | for t in to_put: 511 | s.append(lyrics["text"][t]) 512 | if t == lyrics_ix: 513 | curr_line = len(s) - 1 514 | # -- Tooltip 515 | center_text(s, text_len) 516 | if curr_line: 517 | s[curr_line] = "" + s[curr_line] + "" 518 | tooltip = f"{s[0]}" 519 | tooltip += f"\n{s[1]}" 520 | # Skip album in tooltip if we don't have it 521 | s[2] = s[2].strip() 522 | if s[2]: 523 | tooltip += f"\n{s[2]}" 524 | if len(s) >= 4 and s[3].strip(): 525 | tooltip += "\n" 526 | tooltip += "\n".join(s[3:]) 527 | write_output(widget_text, gen_class(perc), tooltip) 528 | # It's important to return True to keep the handler going 529 | last_metadata = metadata 530 | logger.debug("Returning True") 531 | return True 532 | 533 | 534 | def init_player(manager, name): 535 | logger.debug("I was called") 536 | logger.debug(f"name.name={name.name}") 537 | # Register handlers 538 | player = Playerctl.Player.new_from_name(name) 539 | player.connect("playback-status", on_playback_status, manager) 540 | player.connect("metadata", on_metadata, manager) 541 | manager.manage_player(player) 542 | # Call update_metadata to update progress bar and send notification 543 | logger.debug("Calling update_metadata") 544 | update_metadata(player, player.props.metadata, manager) 545 | # If we are playing, register refresh interval callback 546 | register_refresh_interval_callback(player, manager) 547 | logger.debug("Returning None") 548 | 549 | 550 | def signal_handler(sig, frame): 551 | logger.debug("I was called") 552 | sys.stdout.write("\n") 553 | sys.stdout.flush() 554 | # loop.quit() 555 | sys.exit(0) 556 | 557 | 558 | def parse_arguments(): 559 | parser = argparse.ArgumentParser() 560 | # Increase verbosity with every occurrence of -v 561 | parser.add_argument("command", action="store", default="error") 562 | parser.add_argument("-v", "--verbose", action="count", default=0) 563 | return parser.parse_args() 564 | 565 | 566 | def main(): 567 | arguments = parse_arguments() 568 | 569 | # Initialize logging 570 | logging.basicConfig( 571 | stream=sys.stderr, 572 | level=logging.DEBUG, 573 | format="[%(name)s] [%(funcName)s] [%(levelname)s] %(message)s", 574 | ) 575 | 576 | # Logging is set by default to WARN and higher. 577 | # With every occurrence of -v it's lowered by one 578 | # until it reaches 0 579 | loglevel = max((3 - arguments.verbose) * 10, 0) 580 | logger.setLevel(loglevel) 581 | logging.getLogger("urllib3").setLevel(loglevel) 582 | logging.getLogger("syncedlyrics").setLevel(loglevel) 583 | 584 | logging.getLogger("syncedlyrics").setLevel(loglevel) 585 | 586 | # Log the sent command line arguments 587 | logger.info("I was called") 588 | logger.debug("Arguments received {}".format(vars(arguments))) 589 | 590 | # Read configuration from file 591 | fp = Path(sys.argv[0]).parent.resolve() / "config.json" 592 | if not fp.is_file(): 593 | logging.critical(f"ERROR: Configuration file {fp} not found") 594 | sys.exit(1) 595 | with open(fp, "r") as fh: 596 | global config 597 | config = json.load(fh) 598 | logger.debug(f"config={config}") 599 | assert isinstance(config, dict) 600 | 601 | # Remove album art if present 602 | delete_album_art() 603 | 604 | # Function to generate CSS classes 605 | global format_class 606 | interval = config["interval"] 607 | digits = math.floor(math.log10(interval)) 608 | digits = max(0, math.fabs(digits)) 609 | digits = int(digits) 610 | format_class = partial(lambda x, y: str(round(x, y)), y=digits) 611 | 612 | manager = Playerctl.PlayerManager() 613 | 614 | requested_player = config["player_name"].split(".")[0] 615 | logger.debug( 616 | f"Splitting player_name on `.`. requested_player={requested_player}" 617 | ) 618 | 619 | ini = False 620 | for player in manager.props.player_names: 621 | logger.debug(f"Found player '{player.name}'") 622 | if not player.name.startswith(requested_player): 623 | logger.debug("This is not the filtered player, skipping it") 624 | continue 625 | if arguments.command == "play-pause": 626 | Playerctl.Player.new(player.name).play_pause() 627 | sys.exit(0) 628 | elif arguments.command == "next": 629 | Playerctl.Player.new(player.name).next() 630 | sys.exit(0) 631 | elif arguments.command == "previous": 632 | Playerctl.Player.new(player.name).previous() 633 | sys.exit(0) 634 | elif arguments.command == "monitor": 635 | logger.debug("Initializing player") 636 | init_player(manager, player) 637 | ini = True 638 | else: 639 | logger.critical(f"Invalid argument {sys.argv[1]}") 640 | sys.exit(1) 641 | 642 | if not ini: 643 | logger.debug("No player found. Printing empty") 644 | write_output() 645 | 646 | loop = GLib.MainLoop() 647 | 648 | on_player_appeared_inst = partial( 649 | on_player_appeared, requested_player=requested_player 650 | ) 651 | # manager.connect("player-appeared", on_player_appeared) 652 | manager.connect("name-appeared", on_player_appeared_inst) 653 | manager.connect("player-vanished", on_player_vanished) 654 | 655 | # signal.signal(signal.SIGINT, signal_handler) 656 | # signal.signal(signal.SIGTERM, signal_handler) 657 | # signal.signal(signal.SIGPIPE, signal.SIG_DFL) 658 | 659 | loop.run() 660 | 661 | 662 | if __name__ == "__main__": 663 | main() 664 | -------------------------------------------------------------------------------- /src/mkstyle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import json 5 | import math 6 | import sys 7 | from functools import partial 8 | from pathlib import Path 9 | 10 | import numpy as np 11 | 12 | # Read config file 13 | fp = Path(sys.argv[0]).parent.resolve() / "config.json" 14 | if not fp.is_file(): 15 | print(f"ERROR: Configuration file {fp} not found") 16 | sys.exit(1) 17 | with open(fp, "r") as fh: 18 | global config 19 | config = json.load(fh) 20 | assert isinstance(config, dict) 21 | 22 | overlay_color = config["overlay_color"] 23 | surface_color = config["surface_color"] 24 | interval = config["interval"] 25 | digits = math.floor(math.log10(interval)) 26 | digits = max(0, math.fabs(digits)) 27 | digits = int(digits) 28 | print(f"Decimal points: {digits}") 29 | 30 | 31 | def myround(x, y): 32 | return str(round(x, y)) 33 | 34 | 35 | # Internals 36 | format_pc = partial(myround, y=digits + 1) 37 | format_class = partial(myround, y=1) 38 | 39 | 40 | def parse_arguments(): 41 | parser = argparse.ArgumentParser() 42 | parser.add_argument("--output-file", action="store", default="style.css") 43 | return parser.parse_args() 44 | 45 | 46 | args = parse_arguments() 47 | outfp = args.output_file 48 | print(f"Writing output to {outfp}") 49 | 50 | outfh = open(outfp, "w", encoding="utf8") 51 | 52 | for i in np.arange(0, 100 + interval, interval): 53 | i = float(i) 54 | class_ = "perc" + format_class(i).replace(".", "-") 55 | i2 = i + (interval / 10) 56 | outfh.write( 57 | f"""#custom-mediaplayer.{class_} {{ 58 | background-image: linear-gradient( 59 | to right, 60 | {overlay_color} {format_pc(i)}%, 61 | {surface_color} {format_pc(i2)}% 62 | ); 63 | }}\n""" 64 | ) 65 | 66 | outfh.close() 67 | -------------------------------------------------------------------------------- /src/style-catppuccin.css: -------------------------------------------------------------------------------- 1 | #custom-mediaplayer.perc0-0 { 2 | background-image: linear-gradient( 3 | to right, 4 | @overlay0 0.0%, 5 | @surface0 0.1% 6 | ); 7 | } 8 | #custom-mediaplayer.perc1-0 { 9 | background-image: linear-gradient( 10 | to right, 11 | @overlay0 1.0%, 12 | @surface0 1.1% 13 | ); 14 | } 15 | #custom-mediaplayer.perc2-0 { 16 | background-image: linear-gradient( 17 | to right, 18 | @overlay0 2.0%, 19 | @surface0 2.1% 20 | ); 21 | } 22 | #custom-mediaplayer.perc3-0 { 23 | background-image: linear-gradient( 24 | to right, 25 | @overlay0 3.0%, 26 | @surface0 3.1% 27 | ); 28 | } 29 | #custom-mediaplayer.perc4-0 { 30 | background-image: linear-gradient( 31 | to right, 32 | @overlay0 4.0%, 33 | @surface0 4.1% 34 | ); 35 | } 36 | #custom-mediaplayer.perc5-0 { 37 | background-image: linear-gradient( 38 | to right, 39 | @overlay0 5.0%, 40 | @surface0 5.1% 41 | ); 42 | } 43 | #custom-mediaplayer.perc6-0 { 44 | background-image: linear-gradient( 45 | to right, 46 | @overlay0 6.0%, 47 | @surface0 6.1% 48 | ); 49 | } 50 | #custom-mediaplayer.perc7-0 { 51 | background-image: linear-gradient( 52 | to right, 53 | @overlay0 7.0%, 54 | @surface0 7.1% 55 | ); 56 | } 57 | #custom-mediaplayer.perc8-0 { 58 | background-image: linear-gradient( 59 | to right, 60 | @overlay0 8.0%, 61 | @surface0 8.1% 62 | ); 63 | } 64 | #custom-mediaplayer.perc9-0 { 65 | background-image: linear-gradient( 66 | to right, 67 | @overlay0 9.0%, 68 | @surface0 9.1% 69 | ); 70 | } 71 | #custom-mediaplayer.perc10-0 { 72 | background-image: linear-gradient( 73 | to right, 74 | @overlay0 10.0%, 75 | @surface0 10.1% 76 | ); 77 | } 78 | #custom-mediaplayer.perc11-0 { 79 | background-image: linear-gradient( 80 | to right, 81 | @overlay0 11.0%, 82 | @surface0 11.1% 83 | ); 84 | } 85 | #custom-mediaplayer.perc12-0 { 86 | background-image: linear-gradient( 87 | to right, 88 | @overlay0 12.0%, 89 | @surface0 12.1% 90 | ); 91 | } 92 | #custom-mediaplayer.perc13-0 { 93 | background-image: linear-gradient( 94 | to right, 95 | @overlay0 13.0%, 96 | @surface0 13.1% 97 | ); 98 | } 99 | #custom-mediaplayer.perc14-0 { 100 | background-image: linear-gradient( 101 | to right, 102 | @overlay0 14.0%, 103 | @surface0 14.1% 104 | ); 105 | } 106 | #custom-mediaplayer.perc15-0 { 107 | background-image: linear-gradient( 108 | to right, 109 | @overlay0 15.0%, 110 | @surface0 15.1% 111 | ); 112 | } 113 | #custom-mediaplayer.perc16-0 { 114 | background-image: linear-gradient( 115 | to right, 116 | @overlay0 16.0%, 117 | @surface0 16.1% 118 | ); 119 | } 120 | #custom-mediaplayer.perc17-0 { 121 | background-image: linear-gradient( 122 | to right, 123 | @overlay0 17.0%, 124 | @surface0 17.1% 125 | ); 126 | } 127 | #custom-mediaplayer.perc18-0 { 128 | background-image: linear-gradient( 129 | to right, 130 | @overlay0 18.0%, 131 | @surface0 18.1% 132 | ); 133 | } 134 | #custom-mediaplayer.perc19-0 { 135 | background-image: linear-gradient( 136 | to right, 137 | @overlay0 19.0%, 138 | @surface0 19.1% 139 | ); 140 | } 141 | #custom-mediaplayer.perc20-0 { 142 | background-image: linear-gradient( 143 | to right, 144 | @overlay0 20.0%, 145 | @surface0 20.1% 146 | ); 147 | } 148 | #custom-mediaplayer.perc21-0 { 149 | background-image: linear-gradient( 150 | to right, 151 | @overlay0 21.0%, 152 | @surface0 21.1% 153 | ); 154 | } 155 | #custom-mediaplayer.perc22-0 { 156 | background-image: linear-gradient( 157 | to right, 158 | @overlay0 22.0%, 159 | @surface0 22.1% 160 | ); 161 | } 162 | #custom-mediaplayer.perc23-0 { 163 | background-image: linear-gradient( 164 | to right, 165 | @overlay0 23.0%, 166 | @surface0 23.1% 167 | ); 168 | } 169 | #custom-mediaplayer.perc24-0 { 170 | background-image: linear-gradient( 171 | to right, 172 | @overlay0 24.0%, 173 | @surface0 24.1% 174 | ); 175 | } 176 | #custom-mediaplayer.perc25-0 { 177 | background-image: linear-gradient( 178 | to right, 179 | @overlay0 25.0%, 180 | @surface0 25.1% 181 | ); 182 | } 183 | #custom-mediaplayer.perc26-0 { 184 | background-image: linear-gradient( 185 | to right, 186 | @overlay0 26.0%, 187 | @surface0 26.1% 188 | ); 189 | } 190 | #custom-mediaplayer.perc27-0 { 191 | background-image: linear-gradient( 192 | to right, 193 | @overlay0 27.0%, 194 | @surface0 27.1% 195 | ); 196 | } 197 | #custom-mediaplayer.perc28-0 { 198 | background-image: linear-gradient( 199 | to right, 200 | @overlay0 28.0%, 201 | @surface0 28.1% 202 | ); 203 | } 204 | #custom-mediaplayer.perc29-0 { 205 | background-image: linear-gradient( 206 | to right, 207 | @overlay0 29.0%, 208 | @surface0 29.1% 209 | ); 210 | } 211 | #custom-mediaplayer.perc30-0 { 212 | background-image: linear-gradient( 213 | to right, 214 | @overlay0 30.0%, 215 | @surface0 30.1% 216 | ); 217 | } 218 | #custom-mediaplayer.perc31-0 { 219 | background-image: linear-gradient( 220 | to right, 221 | @overlay0 31.0%, 222 | @surface0 31.1% 223 | ); 224 | } 225 | #custom-mediaplayer.perc32-0 { 226 | background-image: linear-gradient( 227 | to right, 228 | @overlay0 32.0%, 229 | @surface0 32.1% 230 | ); 231 | } 232 | #custom-mediaplayer.perc33-0 { 233 | background-image: linear-gradient( 234 | to right, 235 | @overlay0 33.0%, 236 | @surface0 33.1% 237 | ); 238 | } 239 | #custom-mediaplayer.perc34-0 { 240 | background-image: linear-gradient( 241 | to right, 242 | @overlay0 34.0%, 243 | @surface0 34.1% 244 | ); 245 | } 246 | #custom-mediaplayer.perc35-0 { 247 | background-image: linear-gradient( 248 | to right, 249 | @overlay0 35.0%, 250 | @surface0 35.1% 251 | ); 252 | } 253 | #custom-mediaplayer.perc36-0 { 254 | background-image: linear-gradient( 255 | to right, 256 | @overlay0 36.0%, 257 | @surface0 36.1% 258 | ); 259 | } 260 | #custom-mediaplayer.perc37-0 { 261 | background-image: linear-gradient( 262 | to right, 263 | @overlay0 37.0%, 264 | @surface0 37.1% 265 | ); 266 | } 267 | #custom-mediaplayer.perc38-0 { 268 | background-image: linear-gradient( 269 | to right, 270 | @overlay0 38.0%, 271 | @surface0 38.1% 272 | ); 273 | } 274 | #custom-mediaplayer.perc39-0 { 275 | background-image: linear-gradient( 276 | to right, 277 | @overlay0 39.0%, 278 | @surface0 39.1% 279 | ); 280 | } 281 | #custom-mediaplayer.perc40-0 { 282 | background-image: linear-gradient( 283 | to right, 284 | @overlay0 40.0%, 285 | @surface0 40.1% 286 | ); 287 | } 288 | #custom-mediaplayer.perc41-0 { 289 | background-image: linear-gradient( 290 | to right, 291 | @overlay0 41.0%, 292 | @surface0 41.1% 293 | ); 294 | } 295 | #custom-mediaplayer.perc42-0 { 296 | background-image: linear-gradient( 297 | to right, 298 | @overlay0 42.0%, 299 | @surface0 42.1% 300 | ); 301 | } 302 | #custom-mediaplayer.perc43-0 { 303 | background-image: linear-gradient( 304 | to right, 305 | @overlay0 43.0%, 306 | @surface0 43.1% 307 | ); 308 | } 309 | #custom-mediaplayer.perc44-0 { 310 | background-image: linear-gradient( 311 | to right, 312 | @overlay0 44.0%, 313 | @surface0 44.1% 314 | ); 315 | } 316 | #custom-mediaplayer.perc45-0 { 317 | background-image: linear-gradient( 318 | to right, 319 | @overlay0 45.0%, 320 | @surface0 45.1% 321 | ); 322 | } 323 | #custom-mediaplayer.perc46-0 { 324 | background-image: linear-gradient( 325 | to right, 326 | @overlay0 46.0%, 327 | @surface0 46.1% 328 | ); 329 | } 330 | #custom-mediaplayer.perc47-0 { 331 | background-image: linear-gradient( 332 | to right, 333 | @overlay0 47.0%, 334 | @surface0 47.1% 335 | ); 336 | } 337 | #custom-mediaplayer.perc48-0 { 338 | background-image: linear-gradient( 339 | to right, 340 | @overlay0 48.0%, 341 | @surface0 48.1% 342 | ); 343 | } 344 | #custom-mediaplayer.perc49-0 { 345 | background-image: linear-gradient( 346 | to right, 347 | @overlay0 49.0%, 348 | @surface0 49.1% 349 | ); 350 | } 351 | #custom-mediaplayer.perc50-0 { 352 | background-image: linear-gradient( 353 | to right, 354 | @overlay0 50.0%, 355 | @surface0 50.1% 356 | ); 357 | } 358 | #custom-mediaplayer.perc51-0 { 359 | background-image: linear-gradient( 360 | to right, 361 | @overlay0 51.0%, 362 | @surface0 51.1% 363 | ); 364 | } 365 | #custom-mediaplayer.perc52-0 { 366 | background-image: linear-gradient( 367 | to right, 368 | @overlay0 52.0%, 369 | @surface0 52.1% 370 | ); 371 | } 372 | #custom-mediaplayer.perc53-0 { 373 | background-image: linear-gradient( 374 | to right, 375 | @overlay0 53.0%, 376 | @surface0 53.1% 377 | ); 378 | } 379 | #custom-mediaplayer.perc54-0 { 380 | background-image: linear-gradient( 381 | to right, 382 | @overlay0 54.0%, 383 | @surface0 54.1% 384 | ); 385 | } 386 | #custom-mediaplayer.perc55-0 { 387 | background-image: linear-gradient( 388 | to right, 389 | @overlay0 55.0%, 390 | @surface0 55.1% 391 | ); 392 | } 393 | #custom-mediaplayer.perc56-0 { 394 | background-image: linear-gradient( 395 | to right, 396 | @overlay0 56.0%, 397 | @surface0 56.1% 398 | ); 399 | } 400 | #custom-mediaplayer.perc57-0 { 401 | background-image: linear-gradient( 402 | to right, 403 | @overlay0 57.0%, 404 | @surface0 57.1% 405 | ); 406 | } 407 | #custom-mediaplayer.perc58-0 { 408 | background-image: linear-gradient( 409 | to right, 410 | @overlay0 58.0%, 411 | @surface0 58.1% 412 | ); 413 | } 414 | #custom-mediaplayer.perc59-0 { 415 | background-image: linear-gradient( 416 | to right, 417 | @overlay0 59.0%, 418 | @surface0 59.1% 419 | ); 420 | } 421 | #custom-mediaplayer.perc60-0 { 422 | background-image: linear-gradient( 423 | to right, 424 | @overlay0 60.0%, 425 | @surface0 60.1% 426 | ); 427 | } 428 | #custom-mediaplayer.perc61-0 { 429 | background-image: linear-gradient( 430 | to right, 431 | @overlay0 61.0%, 432 | @surface0 61.1% 433 | ); 434 | } 435 | #custom-mediaplayer.perc62-0 { 436 | background-image: linear-gradient( 437 | to right, 438 | @overlay0 62.0%, 439 | @surface0 62.1% 440 | ); 441 | } 442 | #custom-mediaplayer.perc63-0 { 443 | background-image: linear-gradient( 444 | to right, 445 | @overlay0 63.0%, 446 | @surface0 63.1% 447 | ); 448 | } 449 | #custom-mediaplayer.perc64-0 { 450 | background-image: linear-gradient( 451 | to right, 452 | @overlay0 64.0%, 453 | @surface0 64.1% 454 | ); 455 | } 456 | #custom-mediaplayer.perc65-0 { 457 | background-image: linear-gradient( 458 | to right, 459 | @overlay0 65.0%, 460 | @surface0 65.1% 461 | ); 462 | } 463 | #custom-mediaplayer.perc66-0 { 464 | background-image: linear-gradient( 465 | to right, 466 | @overlay0 66.0%, 467 | @surface0 66.1% 468 | ); 469 | } 470 | #custom-mediaplayer.perc67-0 { 471 | background-image: linear-gradient( 472 | to right, 473 | @overlay0 67.0%, 474 | @surface0 67.1% 475 | ); 476 | } 477 | #custom-mediaplayer.perc68-0 { 478 | background-image: linear-gradient( 479 | to right, 480 | @overlay0 68.0%, 481 | @surface0 68.1% 482 | ); 483 | } 484 | #custom-mediaplayer.perc69-0 { 485 | background-image: linear-gradient( 486 | to right, 487 | @overlay0 69.0%, 488 | @surface0 69.1% 489 | ); 490 | } 491 | #custom-mediaplayer.perc70-0 { 492 | background-image: linear-gradient( 493 | to right, 494 | @overlay0 70.0%, 495 | @surface0 70.1% 496 | ); 497 | } 498 | #custom-mediaplayer.perc71-0 { 499 | background-image: linear-gradient( 500 | to right, 501 | @overlay0 71.0%, 502 | @surface0 71.1% 503 | ); 504 | } 505 | #custom-mediaplayer.perc72-0 { 506 | background-image: linear-gradient( 507 | to right, 508 | @overlay0 72.0%, 509 | @surface0 72.1% 510 | ); 511 | } 512 | #custom-mediaplayer.perc73-0 { 513 | background-image: linear-gradient( 514 | to right, 515 | @overlay0 73.0%, 516 | @surface0 73.1% 517 | ); 518 | } 519 | #custom-mediaplayer.perc74-0 { 520 | background-image: linear-gradient( 521 | to right, 522 | @overlay0 74.0%, 523 | @surface0 74.1% 524 | ); 525 | } 526 | #custom-mediaplayer.perc75-0 { 527 | background-image: linear-gradient( 528 | to right, 529 | @overlay0 75.0%, 530 | @surface0 75.1% 531 | ); 532 | } 533 | #custom-mediaplayer.perc76-0 { 534 | background-image: linear-gradient( 535 | to right, 536 | @overlay0 76.0%, 537 | @surface0 76.1% 538 | ); 539 | } 540 | #custom-mediaplayer.perc77-0 { 541 | background-image: linear-gradient( 542 | to right, 543 | @overlay0 77.0%, 544 | @surface0 77.1% 545 | ); 546 | } 547 | #custom-mediaplayer.perc78-0 { 548 | background-image: linear-gradient( 549 | to right, 550 | @overlay0 78.0%, 551 | @surface0 78.1% 552 | ); 553 | } 554 | #custom-mediaplayer.perc79-0 { 555 | background-image: linear-gradient( 556 | to right, 557 | @overlay0 79.0%, 558 | @surface0 79.1% 559 | ); 560 | } 561 | #custom-mediaplayer.perc80-0 { 562 | background-image: linear-gradient( 563 | to right, 564 | @overlay0 80.0%, 565 | @surface0 80.1% 566 | ); 567 | } 568 | #custom-mediaplayer.perc81-0 { 569 | background-image: linear-gradient( 570 | to right, 571 | @overlay0 81.0%, 572 | @surface0 81.1% 573 | ); 574 | } 575 | #custom-mediaplayer.perc82-0 { 576 | background-image: linear-gradient( 577 | to right, 578 | @overlay0 82.0%, 579 | @surface0 82.1% 580 | ); 581 | } 582 | #custom-mediaplayer.perc83-0 { 583 | background-image: linear-gradient( 584 | to right, 585 | @overlay0 83.0%, 586 | @surface0 83.1% 587 | ); 588 | } 589 | #custom-mediaplayer.perc84-0 { 590 | background-image: linear-gradient( 591 | to right, 592 | @overlay0 84.0%, 593 | @surface0 84.1% 594 | ); 595 | } 596 | #custom-mediaplayer.perc85-0 { 597 | background-image: linear-gradient( 598 | to right, 599 | @overlay0 85.0%, 600 | @surface0 85.1% 601 | ); 602 | } 603 | #custom-mediaplayer.perc86-0 { 604 | background-image: linear-gradient( 605 | to right, 606 | @overlay0 86.0%, 607 | @surface0 86.1% 608 | ); 609 | } 610 | #custom-mediaplayer.perc87-0 { 611 | background-image: linear-gradient( 612 | to right, 613 | @overlay0 87.0%, 614 | @surface0 87.1% 615 | ); 616 | } 617 | #custom-mediaplayer.perc88-0 { 618 | background-image: linear-gradient( 619 | to right, 620 | @overlay0 88.0%, 621 | @surface0 88.1% 622 | ); 623 | } 624 | #custom-mediaplayer.perc89-0 { 625 | background-image: linear-gradient( 626 | to right, 627 | @overlay0 89.0%, 628 | @surface0 89.1% 629 | ); 630 | } 631 | #custom-mediaplayer.perc90-0 { 632 | background-image: linear-gradient( 633 | to right, 634 | @overlay0 90.0%, 635 | @surface0 90.1% 636 | ); 637 | } 638 | #custom-mediaplayer.perc91-0 { 639 | background-image: linear-gradient( 640 | to right, 641 | @overlay0 91.0%, 642 | @surface0 91.1% 643 | ); 644 | } 645 | #custom-mediaplayer.perc92-0 { 646 | background-image: linear-gradient( 647 | to right, 648 | @overlay0 92.0%, 649 | @surface0 92.1% 650 | ); 651 | } 652 | #custom-mediaplayer.perc93-0 { 653 | background-image: linear-gradient( 654 | to right, 655 | @overlay0 93.0%, 656 | @surface0 93.1% 657 | ); 658 | } 659 | #custom-mediaplayer.perc94-0 { 660 | background-image: linear-gradient( 661 | to right, 662 | @overlay0 94.0%, 663 | @surface0 94.1% 664 | ); 665 | } 666 | #custom-mediaplayer.perc95-0 { 667 | background-image: linear-gradient( 668 | to right, 669 | @overlay0 95.0%, 670 | @surface0 95.1% 671 | ); 672 | } 673 | #custom-mediaplayer.perc96-0 { 674 | background-image: linear-gradient( 675 | to right, 676 | @overlay0 96.0%, 677 | @surface0 96.1% 678 | ); 679 | } 680 | #custom-mediaplayer.perc97-0 { 681 | background-image: linear-gradient( 682 | to right, 683 | @overlay0 97.0%, 684 | @surface0 97.1% 685 | ); 686 | } 687 | #custom-mediaplayer.perc98-0 { 688 | background-image: linear-gradient( 689 | to right, 690 | @overlay0 98.0%, 691 | @surface0 98.1% 692 | ); 693 | } 694 | #custom-mediaplayer.perc99-0 { 695 | background-image: linear-gradient( 696 | to right, 697 | @overlay0 99.0%, 698 | @surface0 99.1% 699 | ); 700 | } 701 | #custom-mediaplayer.perc100-0 { 702 | background-image: linear-gradient( 703 | to right, 704 | @overlay0 100.0%, 705 | @surface0 100.1% 706 | ); 707 | } 708 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | #custom-mediaplayer.perc0-0 { 2 | background-image: linear-gradient( 3 | to right, 4 | cyan 0.0%, 5 | gray 0.1% 6 | ); 7 | } 8 | #custom-mediaplayer.perc1-0 { 9 | background-image: linear-gradient( 10 | to right, 11 | cyan 1.0%, 12 | gray 1.1% 13 | ); 14 | } 15 | #custom-mediaplayer.perc2-0 { 16 | background-image: linear-gradient( 17 | to right, 18 | cyan 2.0%, 19 | gray 2.1% 20 | ); 21 | } 22 | #custom-mediaplayer.perc3-0 { 23 | background-image: linear-gradient( 24 | to right, 25 | cyan 3.0%, 26 | gray 3.1% 27 | ); 28 | } 29 | #custom-mediaplayer.perc4-0 { 30 | background-image: linear-gradient( 31 | to right, 32 | cyan 4.0%, 33 | gray 4.1% 34 | ); 35 | } 36 | #custom-mediaplayer.perc5-0 { 37 | background-image: linear-gradient( 38 | to right, 39 | cyan 5.0%, 40 | gray 5.1% 41 | ); 42 | } 43 | #custom-mediaplayer.perc6-0 { 44 | background-image: linear-gradient( 45 | to right, 46 | cyan 6.0%, 47 | gray 6.1% 48 | ); 49 | } 50 | #custom-mediaplayer.perc7-0 { 51 | background-image: linear-gradient( 52 | to right, 53 | cyan 7.0%, 54 | gray 7.1% 55 | ); 56 | } 57 | #custom-mediaplayer.perc8-0 { 58 | background-image: linear-gradient( 59 | to right, 60 | cyan 8.0%, 61 | gray 8.1% 62 | ); 63 | } 64 | #custom-mediaplayer.perc9-0 { 65 | background-image: linear-gradient( 66 | to right, 67 | cyan 9.0%, 68 | gray 9.1% 69 | ); 70 | } 71 | #custom-mediaplayer.perc10-0 { 72 | background-image: linear-gradient( 73 | to right, 74 | cyan 10.0%, 75 | gray 10.1% 76 | ); 77 | } 78 | #custom-mediaplayer.perc11-0 { 79 | background-image: linear-gradient( 80 | to right, 81 | cyan 11.0%, 82 | gray 11.1% 83 | ); 84 | } 85 | #custom-mediaplayer.perc12-0 { 86 | background-image: linear-gradient( 87 | to right, 88 | cyan 12.0%, 89 | gray 12.1% 90 | ); 91 | } 92 | #custom-mediaplayer.perc13-0 { 93 | background-image: linear-gradient( 94 | to right, 95 | cyan 13.0%, 96 | gray 13.1% 97 | ); 98 | } 99 | #custom-mediaplayer.perc14-0 { 100 | background-image: linear-gradient( 101 | to right, 102 | cyan 14.0%, 103 | gray 14.1% 104 | ); 105 | } 106 | #custom-mediaplayer.perc15-0 { 107 | background-image: linear-gradient( 108 | to right, 109 | cyan 15.0%, 110 | gray 15.1% 111 | ); 112 | } 113 | #custom-mediaplayer.perc16-0 { 114 | background-image: linear-gradient( 115 | to right, 116 | cyan 16.0%, 117 | gray 16.1% 118 | ); 119 | } 120 | #custom-mediaplayer.perc17-0 { 121 | background-image: linear-gradient( 122 | to right, 123 | cyan 17.0%, 124 | gray 17.1% 125 | ); 126 | } 127 | #custom-mediaplayer.perc18-0 { 128 | background-image: linear-gradient( 129 | to right, 130 | cyan 18.0%, 131 | gray 18.1% 132 | ); 133 | } 134 | #custom-mediaplayer.perc19-0 { 135 | background-image: linear-gradient( 136 | to right, 137 | cyan 19.0%, 138 | gray 19.1% 139 | ); 140 | } 141 | #custom-mediaplayer.perc20-0 { 142 | background-image: linear-gradient( 143 | to right, 144 | cyan 20.0%, 145 | gray 20.1% 146 | ); 147 | } 148 | #custom-mediaplayer.perc21-0 { 149 | background-image: linear-gradient( 150 | to right, 151 | cyan 21.0%, 152 | gray 21.1% 153 | ); 154 | } 155 | #custom-mediaplayer.perc22-0 { 156 | background-image: linear-gradient( 157 | to right, 158 | cyan 22.0%, 159 | gray 22.1% 160 | ); 161 | } 162 | #custom-mediaplayer.perc23-0 { 163 | background-image: linear-gradient( 164 | to right, 165 | cyan 23.0%, 166 | gray 23.1% 167 | ); 168 | } 169 | #custom-mediaplayer.perc24-0 { 170 | background-image: linear-gradient( 171 | to right, 172 | cyan 24.0%, 173 | gray 24.1% 174 | ); 175 | } 176 | #custom-mediaplayer.perc25-0 { 177 | background-image: linear-gradient( 178 | to right, 179 | cyan 25.0%, 180 | gray 25.1% 181 | ); 182 | } 183 | #custom-mediaplayer.perc26-0 { 184 | background-image: linear-gradient( 185 | to right, 186 | cyan 26.0%, 187 | gray 26.1% 188 | ); 189 | } 190 | #custom-mediaplayer.perc27-0 { 191 | background-image: linear-gradient( 192 | to right, 193 | cyan 27.0%, 194 | gray 27.1% 195 | ); 196 | } 197 | #custom-mediaplayer.perc28-0 { 198 | background-image: linear-gradient( 199 | to right, 200 | cyan 28.0%, 201 | gray 28.1% 202 | ); 203 | } 204 | #custom-mediaplayer.perc29-0 { 205 | background-image: linear-gradient( 206 | to right, 207 | cyan 29.0%, 208 | gray 29.1% 209 | ); 210 | } 211 | #custom-mediaplayer.perc30-0 { 212 | background-image: linear-gradient( 213 | to right, 214 | cyan 30.0%, 215 | gray 30.1% 216 | ); 217 | } 218 | #custom-mediaplayer.perc31-0 { 219 | background-image: linear-gradient( 220 | to right, 221 | cyan 31.0%, 222 | gray 31.1% 223 | ); 224 | } 225 | #custom-mediaplayer.perc32-0 { 226 | background-image: linear-gradient( 227 | to right, 228 | cyan 32.0%, 229 | gray 32.1% 230 | ); 231 | } 232 | #custom-mediaplayer.perc33-0 { 233 | background-image: linear-gradient( 234 | to right, 235 | cyan 33.0%, 236 | gray 33.1% 237 | ); 238 | } 239 | #custom-mediaplayer.perc34-0 { 240 | background-image: linear-gradient( 241 | to right, 242 | cyan 34.0%, 243 | gray 34.1% 244 | ); 245 | } 246 | #custom-mediaplayer.perc35-0 { 247 | background-image: linear-gradient( 248 | to right, 249 | cyan 35.0%, 250 | gray 35.1% 251 | ); 252 | } 253 | #custom-mediaplayer.perc36-0 { 254 | background-image: linear-gradient( 255 | to right, 256 | cyan 36.0%, 257 | gray 36.1% 258 | ); 259 | } 260 | #custom-mediaplayer.perc37-0 { 261 | background-image: linear-gradient( 262 | to right, 263 | cyan 37.0%, 264 | gray 37.1% 265 | ); 266 | } 267 | #custom-mediaplayer.perc38-0 { 268 | background-image: linear-gradient( 269 | to right, 270 | cyan 38.0%, 271 | gray 38.1% 272 | ); 273 | } 274 | #custom-mediaplayer.perc39-0 { 275 | background-image: linear-gradient( 276 | to right, 277 | cyan 39.0%, 278 | gray 39.1% 279 | ); 280 | } 281 | #custom-mediaplayer.perc40-0 { 282 | background-image: linear-gradient( 283 | to right, 284 | cyan 40.0%, 285 | gray 40.1% 286 | ); 287 | } 288 | #custom-mediaplayer.perc41-0 { 289 | background-image: linear-gradient( 290 | to right, 291 | cyan 41.0%, 292 | gray 41.1% 293 | ); 294 | } 295 | #custom-mediaplayer.perc42-0 { 296 | background-image: linear-gradient( 297 | to right, 298 | cyan 42.0%, 299 | gray 42.1% 300 | ); 301 | } 302 | #custom-mediaplayer.perc43-0 { 303 | background-image: linear-gradient( 304 | to right, 305 | cyan 43.0%, 306 | gray 43.1% 307 | ); 308 | } 309 | #custom-mediaplayer.perc44-0 { 310 | background-image: linear-gradient( 311 | to right, 312 | cyan 44.0%, 313 | gray 44.1% 314 | ); 315 | } 316 | #custom-mediaplayer.perc45-0 { 317 | background-image: linear-gradient( 318 | to right, 319 | cyan 45.0%, 320 | gray 45.1% 321 | ); 322 | } 323 | #custom-mediaplayer.perc46-0 { 324 | background-image: linear-gradient( 325 | to right, 326 | cyan 46.0%, 327 | gray 46.1% 328 | ); 329 | } 330 | #custom-mediaplayer.perc47-0 { 331 | background-image: linear-gradient( 332 | to right, 333 | cyan 47.0%, 334 | gray 47.1% 335 | ); 336 | } 337 | #custom-mediaplayer.perc48-0 { 338 | background-image: linear-gradient( 339 | to right, 340 | cyan 48.0%, 341 | gray 48.1% 342 | ); 343 | } 344 | #custom-mediaplayer.perc49-0 { 345 | background-image: linear-gradient( 346 | to right, 347 | cyan 49.0%, 348 | gray 49.1% 349 | ); 350 | } 351 | #custom-mediaplayer.perc50-0 { 352 | background-image: linear-gradient( 353 | to right, 354 | cyan 50.0%, 355 | gray 50.1% 356 | ); 357 | } 358 | #custom-mediaplayer.perc51-0 { 359 | background-image: linear-gradient( 360 | to right, 361 | cyan 51.0%, 362 | gray 51.1% 363 | ); 364 | } 365 | #custom-mediaplayer.perc52-0 { 366 | background-image: linear-gradient( 367 | to right, 368 | cyan 52.0%, 369 | gray 52.1% 370 | ); 371 | } 372 | #custom-mediaplayer.perc53-0 { 373 | background-image: linear-gradient( 374 | to right, 375 | cyan 53.0%, 376 | gray 53.1% 377 | ); 378 | } 379 | #custom-mediaplayer.perc54-0 { 380 | background-image: linear-gradient( 381 | to right, 382 | cyan 54.0%, 383 | gray 54.1% 384 | ); 385 | } 386 | #custom-mediaplayer.perc55-0 { 387 | background-image: linear-gradient( 388 | to right, 389 | cyan 55.0%, 390 | gray 55.1% 391 | ); 392 | } 393 | #custom-mediaplayer.perc56-0 { 394 | background-image: linear-gradient( 395 | to right, 396 | cyan 56.0%, 397 | gray 56.1% 398 | ); 399 | } 400 | #custom-mediaplayer.perc57-0 { 401 | background-image: linear-gradient( 402 | to right, 403 | cyan 57.0%, 404 | gray 57.1% 405 | ); 406 | } 407 | #custom-mediaplayer.perc58-0 { 408 | background-image: linear-gradient( 409 | to right, 410 | cyan 58.0%, 411 | gray 58.1% 412 | ); 413 | } 414 | #custom-mediaplayer.perc59-0 { 415 | background-image: linear-gradient( 416 | to right, 417 | cyan 59.0%, 418 | gray 59.1% 419 | ); 420 | } 421 | #custom-mediaplayer.perc60-0 { 422 | background-image: linear-gradient( 423 | to right, 424 | cyan 60.0%, 425 | gray 60.1% 426 | ); 427 | } 428 | #custom-mediaplayer.perc61-0 { 429 | background-image: linear-gradient( 430 | to right, 431 | cyan 61.0%, 432 | gray 61.1% 433 | ); 434 | } 435 | #custom-mediaplayer.perc62-0 { 436 | background-image: linear-gradient( 437 | to right, 438 | cyan 62.0%, 439 | gray 62.1% 440 | ); 441 | } 442 | #custom-mediaplayer.perc63-0 { 443 | background-image: linear-gradient( 444 | to right, 445 | cyan 63.0%, 446 | gray 63.1% 447 | ); 448 | } 449 | #custom-mediaplayer.perc64-0 { 450 | background-image: linear-gradient( 451 | to right, 452 | cyan 64.0%, 453 | gray 64.1% 454 | ); 455 | } 456 | #custom-mediaplayer.perc65-0 { 457 | background-image: linear-gradient( 458 | to right, 459 | cyan 65.0%, 460 | gray 65.1% 461 | ); 462 | } 463 | #custom-mediaplayer.perc66-0 { 464 | background-image: linear-gradient( 465 | to right, 466 | cyan 66.0%, 467 | gray 66.1% 468 | ); 469 | } 470 | #custom-mediaplayer.perc67-0 { 471 | background-image: linear-gradient( 472 | to right, 473 | cyan 67.0%, 474 | gray 67.1% 475 | ); 476 | } 477 | #custom-mediaplayer.perc68-0 { 478 | background-image: linear-gradient( 479 | to right, 480 | cyan 68.0%, 481 | gray 68.1% 482 | ); 483 | } 484 | #custom-mediaplayer.perc69-0 { 485 | background-image: linear-gradient( 486 | to right, 487 | cyan 69.0%, 488 | gray 69.1% 489 | ); 490 | } 491 | #custom-mediaplayer.perc70-0 { 492 | background-image: linear-gradient( 493 | to right, 494 | cyan 70.0%, 495 | gray 70.1% 496 | ); 497 | } 498 | #custom-mediaplayer.perc71-0 { 499 | background-image: linear-gradient( 500 | to right, 501 | cyan 71.0%, 502 | gray 71.1% 503 | ); 504 | } 505 | #custom-mediaplayer.perc72-0 { 506 | background-image: linear-gradient( 507 | to right, 508 | cyan 72.0%, 509 | gray 72.1% 510 | ); 511 | } 512 | #custom-mediaplayer.perc73-0 { 513 | background-image: linear-gradient( 514 | to right, 515 | cyan 73.0%, 516 | gray 73.1% 517 | ); 518 | } 519 | #custom-mediaplayer.perc74-0 { 520 | background-image: linear-gradient( 521 | to right, 522 | cyan 74.0%, 523 | gray 74.1% 524 | ); 525 | } 526 | #custom-mediaplayer.perc75-0 { 527 | background-image: linear-gradient( 528 | to right, 529 | cyan 75.0%, 530 | gray 75.1% 531 | ); 532 | } 533 | #custom-mediaplayer.perc76-0 { 534 | background-image: linear-gradient( 535 | to right, 536 | cyan 76.0%, 537 | gray 76.1% 538 | ); 539 | } 540 | #custom-mediaplayer.perc77-0 { 541 | background-image: linear-gradient( 542 | to right, 543 | cyan 77.0%, 544 | gray 77.1% 545 | ); 546 | } 547 | #custom-mediaplayer.perc78-0 { 548 | background-image: linear-gradient( 549 | to right, 550 | cyan 78.0%, 551 | gray 78.1% 552 | ); 553 | } 554 | #custom-mediaplayer.perc79-0 { 555 | background-image: linear-gradient( 556 | to right, 557 | cyan 79.0%, 558 | gray 79.1% 559 | ); 560 | } 561 | #custom-mediaplayer.perc80-0 { 562 | background-image: linear-gradient( 563 | to right, 564 | cyan 80.0%, 565 | gray 80.1% 566 | ); 567 | } 568 | #custom-mediaplayer.perc81-0 { 569 | background-image: linear-gradient( 570 | to right, 571 | cyan 81.0%, 572 | gray 81.1% 573 | ); 574 | } 575 | #custom-mediaplayer.perc82-0 { 576 | background-image: linear-gradient( 577 | to right, 578 | cyan 82.0%, 579 | gray 82.1% 580 | ); 581 | } 582 | #custom-mediaplayer.perc83-0 { 583 | background-image: linear-gradient( 584 | to right, 585 | cyan 83.0%, 586 | gray 83.1% 587 | ); 588 | } 589 | #custom-mediaplayer.perc84-0 { 590 | background-image: linear-gradient( 591 | to right, 592 | cyan 84.0%, 593 | gray 84.1% 594 | ); 595 | } 596 | #custom-mediaplayer.perc85-0 { 597 | background-image: linear-gradient( 598 | to right, 599 | cyan 85.0%, 600 | gray 85.1% 601 | ); 602 | } 603 | #custom-mediaplayer.perc86-0 { 604 | background-image: linear-gradient( 605 | to right, 606 | cyan 86.0%, 607 | gray 86.1% 608 | ); 609 | } 610 | #custom-mediaplayer.perc87-0 { 611 | background-image: linear-gradient( 612 | to right, 613 | cyan 87.0%, 614 | gray 87.1% 615 | ); 616 | } 617 | #custom-mediaplayer.perc88-0 { 618 | background-image: linear-gradient( 619 | to right, 620 | cyan 88.0%, 621 | gray 88.1% 622 | ); 623 | } 624 | #custom-mediaplayer.perc89-0 { 625 | background-image: linear-gradient( 626 | to right, 627 | cyan 89.0%, 628 | gray 89.1% 629 | ); 630 | } 631 | #custom-mediaplayer.perc90-0 { 632 | background-image: linear-gradient( 633 | to right, 634 | cyan 90.0%, 635 | gray 90.1% 636 | ); 637 | } 638 | #custom-mediaplayer.perc91-0 { 639 | background-image: linear-gradient( 640 | to right, 641 | cyan 91.0%, 642 | gray 91.1% 643 | ); 644 | } 645 | #custom-mediaplayer.perc92-0 { 646 | background-image: linear-gradient( 647 | to right, 648 | cyan 92.0%, 649 | gray 92.1% 650 | ); 651 | } 652 | #custom-mediaplayer.perc93-0 { 653 | background-image: linear-gradient( 654 | to right, 655 | cyan 93.0%, 656 | gray 93.1% 657 | ); 658 | } 659 | #custom-mediaplayer.perc94-0 { 660 | background-image: linear-gradient( 661 | to right, 662 | cyan 94.0%, 663 | gray 94.1% 664 | ); 665 | } 666 | #custom-mediaplayer.perc95-0 { 667 | background-image: linear-gradient( 668 | to right, 669 | cyan 95.0%, 670 | gray 95.1% 671 | ); 672 | } 673 | #custom-mediaplayer.perc96-0 { 674 | background-image: linear-gradient( 675 | to right, 676 | cyan 96.0%, 677 | gray 96.1% 678 | ); 679 | } 680 | #custom-mediaplayer.perc97-0 { 681 | background-image: linear-gradient( 682 | to right, 683 | cyan 97.0%, 684 | gray 97.1% 685 | ); 686 | } 687 | #custom-mediaplayer.perc98-0 { 688 | background-image: linear-gradient( 689 | to right, 690 | cyan 98.0%, 691 | gray 98.1% 692 | ); 693 | } 694 | #custom-mediaplayer.perc99-0 { 695 | background-image: linear-gradient( 696 | to right, 697 | cyan 99.0%, 698 | gray 99.1% 699 | ); 700 | } 701 | #custom-mediaplayer.perc100-0 { 702 | background-image: linear-gradient( 703 | to right, 704 | cyan 100.0%, 705 | gray 100.1% 706 | ); 707 | } 708 | --------------------------------------------------------------------------------