Skip to content

Instantly share code, notes, and snippets.

@KristoforMaynard
Last active May 30, 2016 18:59
Show Gist options
  • Save KristoforMaynard/33cd486285c635fb6bc21b21c44dc7f9 to your computer and use it in GitHub Desktop.
Save KristoforMaynard/33cd486285c635fb6bc21b21c44dc7f9 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
"""Serve up an image from a music file
Install:
I highly recomend running this script with py3k since it has better
unicode support.
On a Raspbery Pi:
$ sudo apt-get install python3 python3-dev python3-pip
$ pip3 install twisted
$ pip3 install mutagen
Run:
$ /usr/bin/python3 ./mpod_art.py -r /var/lib/mpd/music -m Music -p 8888
"""
from __future__ import print_function, unicode_literals
import argparse
import os
import sys
import time
import datetime
import logging
import logging.handlers
from glob import glob
try:
from urllib.parse import urlparse, unquote
from urllib.request import urlopen
except ImportError:
from urlparse import urlparse
from urllib import unquote
from urllib2 import urlopen
import mutagen
# from twisted.web import server
from twisted.web.server import Site
from twisted.web.resource import Resource, NoResource
from twisted.internet import reactor
DEFAULT_LOG_FILENAME = os.path.expanduser("~/.mpodartwork.log")
DEFAULT_ROOT = "." # "/var/lib/mpd/music"
def _msg(s):
print(s, file=sys.stderr)
mylogger.info(s)
def img_from_path(root, path):
dirname, basename = os.path.split(path)
target_dir = os.path.join(root, dirname)
target_file = os.path.join(target_dir, basename)
# try to see if the path is an image on disk, and if so, return it
if any(target_file.endswith(_s) for _s in ['.jpeg', '.jpg', '.png']):
if os.path.exists(target_file):
with open(target_file, 'rb') as f:
if target_file[-3:].lower() == 'png':
fmt = "png"
else:
fmt = "jpeg"
return fmt, f.read()
# try to see if there are any audio files from which we can extract art
songs = glob(os.path.join(target_dir, "*.m4a"))
songs += glob(os.path.join(target_dir, "*.flac"))
songs += glob(os.path.join(target_dir, "*.mp3"))
songs += glob(os.path.join(target_dir, "*.acc"))
songs += glob(os.path.join(target_dir, "*.ogg"))
for song in sorted(songs):
fmt, img = get_cover_from_track(song)
if not img:
continue
return fmt, img
# try to do an iTunes album/artist lookup on the last folder in the path
dir_name = target_dir.rstrip('/').split('/')[-1]
_msg("-- Trying iTunes lookup: {0}".format(dirname))
if songs:
# if there are songs in the directory, it's probably an album,
# so check
fmt, img = get_img_from_itunes(dir_name, entity='album')
if img:
return fmt, img
# ok, maybe it's an artist
fmt, img = get_img_from_itunes(dir_name, entity='artist')
if img:
return fmt, img
# ok, now i give up
return None, None
def get_cover_from_track(track_fname):
f = mutagen.File(track_fname)
if not 'covr' in f or not f['covr']:
return None, None
img = f['covr'][0]
if img.imageformat == img.FORMAT_JPEG:
fmt = "jpeg"
elif img.imageformat == img.FORMAT_JPEG:
fmt = "png"
else:
fmt = "jpeg" # ?
# raise ValueError("Invalid image format")
return fmt, img
def get_img_from_itunes(name, res=100, entity='artist'):
try:
from iTunesStore import search_artist, search_album
if entity == 'artist':
results = search_artist(name)
elif entity == 'album':
results = search_album(name)
else:
raise ValueError("bad entity: {0}".format(entity))
for r in results:
r.get_artwork()
if r.artwork:
try:
img_url = r.artwork[str(res)]
except KeyError:
img_url = r.artwork[list(sorted(list(r.artwork.keys)))[-1]]
with urlopen(img_url) as f:
return 'jpeg', f.read()
except ImportError:
_msg("iTunes Not searched, iTunesStore.py not found")
return None, None
class InfoServer(Resource):
isLeaf = True
def __init__(self):
# super(InfoServer, self).__init__()
Resource.__init__(self)
@staticmethod
def render_GET(request):
"""
Handle a new request
"""
urlpath = urlparse(request.path.decode())
path = urlpath.path
_msg('= Proccessing request: {0}'.format(path))
try:
if not path.startswith(args.prefix):
_msg('>> Invalid Path Prefix: {0} != {1}'.format(path, args.prefix))
return NoResource().render(request)
path = unquote(path[len(args.prefix):])
_msg('-- Unquoted path: {0}'.format(path))
fmt, img = img_from_path(args.root, path)
if img:
cache_fname = os.path.join(args.root, path)
if args.cache and any(cache_fname.endswith(s) for s in ['.jpg', '.jpeg']):
if not os.path.exists(cache_fname):
with open(cache_fname, 'wb') as fout:
_msg("-- Caching image as: {0}".format(cache_fname))
fout.write(img)
request.setHeader('Content-Type', 'image/{0}'.format(fmt))
return img
else:
_msg(">> No artwork for path: {0}".format(path))
return NoResource().render(request)
except Exception:
raise
@staticmethod
def timestamp():
ts = time.time()
return datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
#############################################
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__,
conflict_handler='resolve')
parser.add_argument("--log", "-l", default=DEFAULT_LOG_FILENAME,
help="Name of the log file")
parser.add_argument("--root", "-r", default=DEFAULT_ROOT,
help="Root of where the music files live")
parser.add_argument('-m', dest="prefix", default="/Music/",
help="Request Prefix")
parser.add_argument('--port', '-p', default=8888, type=int,
help="Server Port")
parser.add_argument('--cache', '-c', action='store_true',
help="Cache the images")
args = parser.parse_args()
args.prefix = "/{0}/".format(args.prefix.strip("/"))
args.root = os.path.abspath(os.path.expanduser(os.path.expandvars(args.root)))
mylogger = logging.getLogger('mylogger')
mylogger.setLevel(logging.INFO)
# Add the log message handler to the logger
handler = logging.handlers.RotatingFileHandler(args.log, maxBytes=int(512e3),
backupCount=1)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
mylogger.addHandler(handler)
resource = InfoServer()
factory = Site(resource)
reactor.listenTCP(args.port, factory)
_msg('Server started. Listening on port: ' + str(args.port))
reactor.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment