├── .gitignore ├── .pylintrc ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── bum ├── __init__.py ├── __main__.py ├── brainz.py ├── display.py ├── song.py └── util.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | bum.egg-info/* 3 | *.pyc 4 | subprocess 5 | re 6 | build/* 7 | .coverage 8 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | generated-members=send_idle,fetch_idle 3 | 4 | [MESSAGES CONTROL] 5 | disable=WO212 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: python 3 | python: 4 | - "3.6" 5 | 6 | install: 7 | - pip install flake8 pylint python-mpv python-mpd2 musicbrainzngs 8 | 9 | script: 10 | - flake8 bum setup.py 11 | - pylint bum setup.py 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dylan Araps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎵 bum 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/bum.svg)](https://pypi.python.org/pypi/bum/) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.md) 5 | [![Build Status](https://travis-ci.org/dylanaraps/bum.svg?branch=master)](https://travis-ci.org/dylanaraps/bum) 6 | [![Donate](https://img.shields.io/badge/donate-patreon-yellow.svg)](https://www.patreon.com/dyla) 7 | 8 | `bum` is a daemon that downloads album art for songs playing in `mpd`/`mopidy` and displays them in a little window. `bum` doesn't loop on a timer, instead it waits for `mpd`/`mopidy` to send a `player` event. When it receives a `player` event it wakes up and downloads album art for the current playing track. This makes `bum` lightweight and makes it idle at `~0%` CPU usage. 9 | 10 | `bum` uses [musicbrainz](https://musicbrainz.org/) to source and download cover art, if an album is missing it's cover art you can easily create an account and fill in the data yourself. `bum` outputs a `release-id` which you can use to find the exact entry on musicbrainz. 11 | 12 | Note: `bum` is meant to be used with files that don't have embedded album art (`mopidy-spotify`). 13 | 14 | 15 | ![showcase](http://i.imgur.com/uKomDoL.gif) 16 | 17 | 18 | ## Dependencies 19 | 20 | - `python 3.6+` 21 | - `python-mpv` 22 | - `python-mpd2` 23 | - `musicbrainzngs` 24 | 25 | 26 | ## Installation 27 | 28 | ```sh 29 | pip3 install --user bum 30 | ``` 31 | 32 | 33 | ## Usage 34 | 35 | ```sh 36 | usage: bum [-h] [--size "px"] [--cache_dir "/path/to/dir"] [--version] 37 | 38 | bum - Download and display album art for mpd tracks. 39 | 40 | optional arguments: 41 | -h, --help show this help message and exit 42 | --size "px" what size to display the album art in. 43 | --cache_dir "/path/to/dir" 44 | Where to store the downloaded cover art. 45 | --version Print "bum" version. 46 | --port Use a custom mpd port. 47 | ``` 48 | 49 | 50 | ## Donate 51 | 52 | Donations will allow me to spend more time working on `bum`. 53 | 54 | If you like `bum` and want to give back in some way you can donate here: 55 | 56 | **https://patreon.com/dyla** 57 | -------------------------------------------------------------------------------- /bum/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | '|| 3 | || ... ... ... .. .. .. 4 | ||' || || || || || || 5 | || | || || || || || 6 | '|...' '|..'|. .|| || ||. 7 | 8 | Created by Dylan Araps 9 | """ 10 | __version__ = "0.1.3" 11 | -------------------------------------------------------------------------------- /bum/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | '|| 3 | || ... ... ... .. .. .. 4 | ||' || || || || || || 5 | || | || || || || || 6 | '|...' '|..'|. .|| || ||. 7 | 8 | Created by Dylan Araps 9 | """ 10 | import argparse 11 | import pathlib 12 | import sys 13 | 14 | from . import display 15 | from . import song 16 | 17 | from .__init__ import __version__ 18 | 19 | 20 | def get_args(): 21 | """Get the script arguments.""" 22 | description = "bum - Download and display album art \ 23 | for mpd tracks." 24 | arg = argparse.ArgumentParser(description=description) 25 | 26 | arg.add_argument("--size", metavar="\"px\"", 27 | help="what size to display the album art in.", 28 | default=250) 29 | 30 | arg.add_argument("--cache_dir", metavar="\"/path/to/dir\"", 31 | help="Where to store the downloaded cover art.", 32 | default=pathlib.Path.home() / ".cache/bum", 33 | type=pathlib.Path) 34 | 35 | arg.add_argument("--version", action="store_true", 36 | help="Print \"bum\" version.") 37 | 38 | arg.add_argument("--port", 39 | help="Use a custom mpd port.", 40 | default=6600) 41 | 42 | arg.add_argument("--server", 43 | help="Use a remote server instead of localhost.", 44 | default="localhost") 45 | 46 | arg.add_argument("--no_display", 47 | action="store_true", 48 | help="Only download album art, don't display.") 49 | arg.add_argument("--default_cover", 50 | help="Use a custom image for the default cover.") 51 | 52 | return arg.parse_args() 53 | 54 | 55 | def process_args(args): 56 | """Process the arguments.""" 57 | if args.version: 58 | print(f"bum {__version__}") 59 | sys.exit(0) 60 | 61 | 62 | def main(): 63 | """Main script function.""" 64 | args = get_args() 65 | process_args(args) 66 | 67 | if not args.no_display: 68 | disp = display.init(args.size) 69 | 70 | client = song.init(args.port, args.server) 71 | 72 | while True: 73 | song.get_art(args.cache_dir, args.size, args.default_cover, client) 74 | if not args.no_display: 75 | display.launch(disp, args.cache_dir / "current.jpg") 76 | 77 | client.idle("player") 78 | 79 | print("album: Received player event from mpd. Swapping cover art.") 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /bum/brainz.py: -------------------------------------------------------------------------------- 1 | """ 2 | Musicbrainz related functions. 3 | """ 4 | import time 5 | import musicbrainzngs as mus 6 | 7 | from .__init__ import __version__ 8 | 9 | 10 | def init(): 11 | """Initialize musicbrainz.""" 12 | mus.set_useragent("python-bum: A cover art daemon.", 13 | __version__, 14 | "https://github.com/dylanaraps/bum") 15 | 16 | 17 | def get_cover(song, size=250, retry_delay=5, retries=5): 18 | """Download the cover art.""" 19 | try: 20 | data = mus.search_releases(artist=song["artist"], 21 | release=song["album"], 22 | limit=1) 23 | release_id = data["release-list"][0]["release-group"]["id"] 24 | print(f"album: Using release-id: {data['release-list'][0]['id']}") 25 | 26 | return mus.get_release_group_image_front(release_id, size=size) 27 | 28 | except mus.NetworkError: 29 | if retries == 0: 30 | raise mus.NetworkError("Failure connecting to MusicBrainz.org") 31 | print(f"warning: Retrying download. {retries} retries left!") 32 | time.sleep(retry_delay) 33 | get_cover(song, size, retries=retries - 1) 34 | 35 | except mus.ResponseError: 36 | print("error: Couldn't find album art for", 37 | f"{song['artist']} - {song['album']}") 38 | -------------------------------------------------------------------------------- /bum/display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Display related functions. 3 | """ 4 | import mpv 5 | 6 | 7 | def init(size=250): 8 | """Initialize mpv.""" 9 | player = mpv.MPV(start_event_thread=False) 10 | player["force-window"] = "immediate" 11 | player["keep-open"] = "yes" 12 | player["geometry"] = f"{size}x{size}" 13 | player["autofit"] = f"{size}x{size}" 14 | player["title"] = "bum" 15 | 16 | return player 17 | 18 | 19 | def launch(player, input_file): 20 | """Open mpv.""" 21 | player.play(str(input_file)) 22 | -------------------------------------------------------------------------------- /bum/song.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get song info. 3 | """ 4 | import shutil 5 | import os 6 | import mpd 7 | 8 | from . import brainz 9 | from . import util 10 | 11 | 12 | def init(port=6600, server="localhost"): 13 | """Initialize mpd.""" 14 | client = mpd.MPDClient() 15 | 16 | try: 17 | client.connect(server, port) 18 | return client 19 | 20 | except ConnectionRefusedError: 21 | print("error: Connection refused to mpd/mopidy.") 22 | os._exit(1) # pylint: disable=W0212 23 | 24 | 25 | def get_art(cache_dir, size, default_cover, client): 26 | """Get the album art.""" 27 | song = client.currentsong() 28 | 29 | if len(song) < 2: 30 | print("album: Nothing currently playing.") 31 | if default_cover: 32 | shutil.copy(default_cover, cache_dir / "current.jpg") 33 | return 34 | 35 | util.bytes_to_file(util.default_album_art(), cache_dir / "current.jpg") 36 | return 37 | 38 | file_name = f"{song['artist']}_{song['album']}_{size}.jpg".replace("/", "") 39 | file_name = cache_dir / file_name 40 | 41 | if file_name.is_file(): 42 | shutil.copy(file_name, cache_dir / "current.jpg") 43 | print("album: Found cached art.") 44 | 45 | else: 46 | print("album: Downloading album art...") 47 | 48 | brainz.init() 49 | album_art = brainz.get_cover(song, size) 50 | 51 | if not album_art and default_cover: 52 | shutil.copy(default_cover, cache_dir / "current.jpg") 53 | elif not album_art and not default_cover: 54 | album_art = util.default_album_art() 55 | 56 | if album_art: 57 | util.bytes_to_file(album_art, cache_dir / "current.jpg") 58 | util.bytes_to_file(album_art, file_name) 59 | 60 | print(f"album: Swapped art to {song['artist']}, {song['album']}.") 61 | -------------------------------------------------------------------------------- /bum/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Util functions. 3 | """ 4 | import pathlib 5 | import base64 6 | 7 | 8 | def bytes_to_file(input_data, output_file): 9 | """Save bytes to a file.""" 10 | pathlib.Path(output_file.parent).mkdir(parents=True, exist_ok=True) 11 | 12 | with open(output_file, "wb") as file: 13 | file.write(input_data) 14 | 15 | 16 | def default_album_art(): 17 | """Return default album art as bytes.""" 18 | return base64.b64decode(""" 19 | iVBORw0KGgoAAAANSUhEUgAAAOYAAADmAQMAAAD7pGv4AAAABlBMVEX///8AAABVwtN+AAAChUlE 20 | QVRYw6XZsW0jMRCFYRoXTMYKWIRClsXQ4WXX1lVwNbAQB5YXgiBpZ77gRNiAtZS5/HfJmTePrb3R 21 | Luyb6F1toHe3Xnd+/G1R9/76/fNTtTj+vWr9uHXVxjHtqs3bb4XbALxv965wWw18sJbAcR+gwq2B 22 | x33iFW4NvB5GyHFL4NtsA7glcDwNkeNWwONp6jluBbxexshwC+D7XAO4BXCcBslwc+BxmnyGmwOv 23 | ZJSW3K0DNwV+oEyAIx0mu9kGbgY8i7/P3x/ATYCf5hnATYCjHOh8qw3cM/DEp9dvD+CegF9mGcA9 24 | fQwO1TmNQYRJ/MVHt/XYTy8lml7o04XgUulcZoNLdHJ5L26NrW2VbLpo2rAPl4KhoDOMDIagyfC1 25 | GPq2wmYaVKMpIN8vBkN9Z5oYTDGT6WkxtW2lxSJpRlPCvV0OpvJOGTAoISblx6J02ZI9pSiKJkF1 26 | dASlWqfMG5SIk/JyUZpuyVqI3mgSzNeuoBTvlPGDJYAKhAncH+CN3g7cKzBwr8B/VPB8/GM99MXe 27 | zzd6v/5/Viby0/CT9FvwG/Tb58rxqvOK9Wr3TvEu8w717mZkcFRxRHI0cyR0FHUEdvRm5HfWcMZx 28 | tnKmc5Z0hnV2Zma3KrCisBqxkrEKsoKy+qJys+qzYrTatFK1yrVCtrqmMreqd0XgasKViKsYV0Cu 29 | nlh5uWpzxedq0ZWmq1RXuK6OWVm7KndFbzfAToJdCDsYdj/onNh1sWNjt8dOkV0mO1R2t+iM2VWz 30 | I2c3z06gXUQ7kIvJayvx2TW142q31k6vXWI7zIviZEvY2BW3o2433k6+TwF8grAoPreEq089fGLi 31 | 0xaf1PiUxydEF/Re2hvtG6k8p7n4F+LQAAAAAElFTkSuQmCC 32 | """) 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """bum - setup.py""" 2 | import sys 3 | import setuptools 4 | 5 | try: 6 | import bum 7 | except (ImportError, SyntaxError): 8 | print("error: bum requires Python 3.6 or greater.") 9 | sys.exit(1) 10 | 11 | 12 | try: 13 | import pypandoc 14 | LONG_DESC = pypandoc.convert("README.md", "rst") 15 | except(IOError, ImportError, RuntimeError): 16 | LONG_DESC = open('README.md').read() 17 | 18 | 19 | VERSION = bum.__version__ 20 | DOWNLOAD = "https://github.com/dylanaraps/bum/archive/%s.tar.gz" % VERSION 21 | 22 | 23 | setuptools.setup( 24 | name="bum", 25 | version=VERSION, 26 | author="Dylan Araps", 27 | author_email="dylan.araps@gmail.com", 28 | description="Download and display album art for mpd tracks.", 29 | long_description=LONG_DESC, 30 | license="MIT", 31 | url="https://github.com/dylanaraps/bum", 32 | download_url=DOWNLOAD, 33 | classifiers=[ 34 | "Environment :: X11 Applications", 35 | "License :: OSI Approved :: MIT License", 36 | "Operating System :: POSIX :: Linux", 37 | "Programming Language :: Python :: 3.6", 38 | ], 39 | packages=["bum"], 40 | entry_points={ 41 | "console_scripts": ["bum=bum.__main__:main"] 42 | }, 43 | install_requires=[ 44 | "musicbrainzngs", 45 | "python-mpv", 46 | "python-mpd2", 47 | ], 48 | python_requires=">=3.6", 49 | test_suite="tests", 50 | include_package_data=True 51 | ) 52 | --------------------------------------------------------------------------------