From 402e8375511470c61021956edad72e1080f51c8e Mon Sep 17 00:00:00 2001 From: Dirk Alders Date: Thu, 12 Sep 2024 20:19:38 +0200 Subject: [PATCH] Added som features for audio handling (cddb, rip, encode) --- CDDB.py | 138 ++++++++++++++++++++++++++++ __init__.py | 233 +++++++++-------------------------------------- common.py | 36 ++++++++ convert.py | 102 ++++++++++++++++++++- image.py | 150 ++++++++++++++++++++++++++++++ metadata.py | 186 ++++++++++++++++++++++++++----------- requirements.txt | 2 + 7 files changed, 603 insertions(+), 244 deletions(-) create mode 100644 CDDB.py create mode 100644 image.py create mode 100644 requirements.txt diff --git a/CDDB.py b/CDDB.py new file mode 100644 index 0000000..7694314 --- /dev/null +++ b/CDDB.py @@ -0,0 +1,138 @@ +import urllib +import socket +import os +import urllib.parse +import urllib.request +import subprocess +import logging +from .common import KEY_ALBUM, KEY_ARTIST, KEY_GENRE, KEY_TITLE, KEY_TRACK, KEY_YEAR + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + + +version = 2.0 +if 'EMAIL' in os.environ: + (default_user, hostname) = os.environ['EMAIL'].split('@') +else: + default_user = os.environ['USER'] or os.geteuid() or 'user' + hostname = socket.gethostname() or 'host' + +proto = 6 +default_server = 'http://gnudb.gnudb.org/~cddb/cddb.cgi' + + +def my_disc_metadata(**kwargs): + """Generate my disc metadata + + kwargs needs to include the following data: + * KEY_ARTIST (str) + * KEY_ALBUM (str) + * KEY_YEAR (int) - will be converted here + * KEY_GENRE (str) + * "track_xx" (str) - where xx is the track number which will be converted to int here + """ + main_dict = {} + for key in [KEY_ARTIST, KEY_ALBUM, KEY_YEAR, KEY_GENRE]: + try: + value = kwargs.pop(key) + except KeyError: + logger.error("Information is missing in kwargs - key=%s", key) + return None + if key in [KEY_YEAR]: + try: + main_dict[key] = int(value) + except ValueError: + logger.error("Can't convert %s (key=%s) to integer value", value, key) + return None + else: + main_dict[key] = value + rv = dict(main_dict) + rv["tracks"] = [] + for key in list(kwargs): + value = kwargs.pop(key) + if key.startswith("track_"): + track = dict(main_dict) + try: + track[KEY_TRACK] = int(key[6:]) + except ValueError: + logger.warning("Useless information kwargs - kwargs[%s] = %s", key, repr(value)) + track[KEY_TITLE] = value + rv["tracks"].append(track) + else: + logger.warning("Useless information kwargs - key=%s", key) + return rv + + +def query(data_str, server_url=default_server, user=default_user, host=hostname, client_name=ROOT_LOGGER_NAME, client_version=version): + url = f"{server_url}?cmd=cddb+query+{data_str}&hello={user}+{host}+{client_name}+{client_version}&proto={proto}" + response = urllib.request.urlopen(url) + header = response.readline().decode("utf-8").rstrip().split(" ", 3) + header[0] = int(header[0]) + + if header[0] not in (210, ): + logger.error("Error while querying cddb entry: \"%d - %s\"", header[0], header[3]) + return None + + rv = {} + for line in response.readlines(): + line = line.decode("utf-8").rstrip() + if line == '.': # end of matches + break + dummy, did, txt = line.split(" ", 2) + rv[did] = txt + return rv + + +def cddb(disc_id, server_url=default_server, user=default_user, host=hostname, client_name=ROOT_LOGGER_NAME, client_version=version): + KEY_TRANSLATOR = { + "DGENRE": KEY_GENRE, + "DYEAR": KEY_YEAR + } + # + url = f"{server_url}?cmd=cddb+read+data+{disc_id}&hello={default_server}+{hostname}+{client_name}+{client_version}&proto={proto}" + response = urllib.request.urlopen(url) + + header = response.readline().decode("utf-8").rstrip().split(" ", 3) + header[0] = int(header[0]) + + if header[0] not in (210, ): + logger.error("Error while reading cddb entry: \"%d - %s\"", header[1], header[3]) + return None + data = {} + for line in response.readlines(): + line = line.decode("utf-8").rstrip() + if line == '.': # end of matches + break + if not line.startswith("#"): + match = line.split('=', 2) + key = KEY_TRANSLATOR.get(match[0]) + value = match[1].strip() + if key: + if key == KEY_YEAR: + value = int(value) + data[key] = value + elif match[0] == "DTITLE": + art_tit = value.split("/", 2) + data[KEY_ARTIST] = art_tit[0].strip() + data[KEY_ALBUM] = art_tit[1].strip() + elif match[0].startswith("TTITLE"): + data["track_%02d" % (int(match[0][6:]) + 1)] = value + else: + logger.debug("cddb line ignored: \"%s\"", line) + return my_disc_metadata(**data) + + +def discid(): + discid_cmd = subprocess.getoutput("which cd-discid") + if not discid_cmd: + logger.error("cd-discid is required for encoding. You need to install it to your system.") + return None + else: + try: + return subprocess.check_output(discid_cmd).decode("utf-8").strip().replace(" ", "+") + except subprocess.CalledProcessError as e: + return None diff --git a/__init__.py b/__init__.py index af18a87..6ba6641 100644 --- a/__init__.py +++ b/__init__.py @@ -22,203 +22,52 @@ media (Media Tools) See also the :download:`unittest <../../media/_testresults_/unittest.pdf>` documentation. """ +from .common import CALLBACK_CDDB_CHOICE +from .common import CALLBACK_MAN_INPUT +from .common import get_disc_device +from .common import KEY_ALBUM +from .common import KEY_APERTURE +from .common import KEY_ARTIST +from .common import KEY_BITRATE +from .common import KEY_CAMERA +from .common import KEY_DURATION +from .common import KEY_EXPOSURE_PROGRAM +from .common import KEY_EXPOSURE_TIME +from .common import KEY_FLASH +from .common import KEY_FOCAL_LENGTH +from .common import KEY_GENRE +from .common import KEY_GPS +from .common import KEY_HEIGHT +from .common import KEY_ISO +from .common import KEY_ORIENTATION +from .common import KEY_RATIO +from .common import KEY_SIZE +from .common import KEY_TIME +from .common import KEY_TIME_IS_SUBSTITUTION +from .common import KEY_TITLE +from .common import KEY_TRACK +from .common import KEY_WIDTH +from .common import KEY_YEAR +from .convert import disc_track_rip +from .convert import wav_to_mp3 +from .convert import track_to_targetpath +from .image import image +from .image import ORIENTATION_HALF_ROTATED +from .image import ORIENTATION_HORIZONTAL_MIRRORED +from .image import ORIENTATION_LEFT_ROTATED +from .image import ORIENTATION_NORMAL +from .image import ORIENTATION_RIGHT_ROTATED +from .image import ORIENTATION_VERTICAL_MIRRORED +from .image import JOIN_BOT_LEFT, JOIN_BOT_RIGHT +from .image import JOIN_CENTER +from .image import JOIN_TOP_LEFT +from .image import JOIN_TOP_RIGHT +from .metadata import get_media_data __DEPENDENCIES__ = [] -import io -import logging -from PIL import Image, ImageEnhance, ExifTags - -try: - from config import APP_NAME as ROOT_LOGGER_NAME -except ImportError: - ROOT_LOGGER_NAME = 'root' -logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) - __DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with media files, like tags (e.g. exif, id3) and transformations. For more Information read the documentation.""" % __name__.replace('_', '\_') """The Module Description""" __INTERPRETER__ = (3, ) """The Tested Interpreter-Versions""" - - -KEY_ALBUM = 'album' -KEY_APERTURE = 'aperture' -KEY_ARTIST = 'artist' -KEY_BITRATE = 'bitrate' -KEY_CAMERA = 'camera' -KEY_DURATION = 'duration' -KEY_EXPOSURE_PROGRAM = 'exposure_program' -KEY_EXPOSURE_TIME = 'exposure_time' -KEY_FLASH = 'flash' -KEY_FOCAL_LENGTH = 'focal_length' -KEY_GENRE = 'genre' -KEY_GPS = 'gps' -KEY_HEIGHT = 'height' -KEY_ISO = 'iso' -KEY_ORIENTATION = 'orientation' -KEY_RATIO = 'ratio' -KEY_SIZE = 'size' -KEY_TIME = 'time' # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp -KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst' -KEY_TITLE = 'title' -KEY_TRACK = 'track' -KEY_WIDTH = 'width' -KEY_YEAR = 'year' - - -def get_media_data(full_path): - from media.metadata import get_audio_data, get_image_data, get_video_data - from media.common import get_filetype, FILETYPE_AUDIO, FILETYPE_IMAGE, FILETYPE_VIDEO - # - ft = get_filetype(full_path) - # - if ft == FILETYPE_AUDIO: - return get_audio_data(full_path) - elif ft == FILETYPE_IMAGE: - return get_image_data(full_path) - elif ft == FILETYPE_VIDEO: - return get_video_data(full_path) - else: - logger.warning('Filetype not known: %s', full_path) - - -ORIENTATION_NORMAL = 1 -ORIENTATION_VERTICAL_MIRRORED = 2 -ORIENTATION_HALF_ROTATED = 3 -ORIENTATION_HORIZONTAL_MIRRORED = 4 -ORIENTATION_LEFT_ROTATED = 6 -ORIENTATION_RIGHT_ROTATED = 8 - -JOIN_TOP_LEFT = 1 -JOIN_TOP_RIGHT = 2 -JOIN_BOT_LEFT = 3 -JOIN_BOT_RIGHT = 4 -JOIN_CENTER = 5 - - -class image(object): - def __init__(self, media_instance=None): - if media_instance is not None: - self.load_from_file(media_instance) - else: - self._im = None - - def load_from_file(self, media_instance): - from media.convert import get_pil_image - # - self._im = get_pil_image(media_instance) - if self._im is None: - return False - try: - self._exif = dict(self._im._getexif().items()) - except AttributeError: - self._exif = {} - if type(self._im) is not Image.Image: - self._im = self._im.copy() - logger.debug('loading image from %s', repr(media_instance)) - return True - - def save(self, full_path): - if self._im is None: - logger.warning('No image available to be saved (%s)', repr(full_path)) - return False - else: - logger.debug('Saving image to %s', repr(full_path)) - with open(full_path, 'w') as fh: - im = self._im.convert('RGB') - im.save(fh, 'JPEG') - return True - - def image_data(self): - im = self._im.copy().convert('RGB') - output = io.BytesIO() - im.save(output, format='JPEG') - return output.getvalue() - - def resize(self, max_size): - if self._im is None: - logger.warning('No image available to be resized') - return False - else: - logger.debug('Resizing picture to max %d pixel in whatever direction', max_size) - x, y = self._im.size - xy_max = max(x, y) - self._im = self._im.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0) - return True - - def rotate_by_orientation(self, orientation=None): - if self._im is None: - logger.warning('No image available, rotation not possible') - return False - - if orientation is None: - exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items()) - try: - orientation = self._exif[exif_tags['Orientation']] - logger.debug("No orientation given, orientation %s extract from exif data", repr(orientation)) - except KeyError: - return False - - if orientation == ORIENTATION_HALF_ROTATED: - angle = 180 - elif orientation == ORIENTATION_LEFT_ROTATED: - angle = 270 - elif orientation == ORIENTATION_RIGHT_ROTATED: - angle = 90 - else: - if type(orientation) == int and orientation > 8: - logger.warning('Orientation %s unknown for rotation', repr(orientation)) - return False - logger.debug('Rotating picture by %d (deg)', angle) - self._im = self._im.rotate(angle, expand=True) - return True - - def join(self, join_image, join_pos=JOIN_TOP_RIGHT, opacity=0.7): - from media.convert import get_pil_image - - def rgba_copy(im): - if im.mode != 'RGBA': - return im.convert('RGBA') - else: - return im.copy() - - if self._im is None: - logger.warning('No image available, joining not possible') - return False - - # ensure type of join_image is PIL.Image - join_image = get_pil_image(join_image) - if join_image is None: - logger.warning('Image to be joined is not supported %s', repr(join_image)) - return False - - im2 = rgba_copy(join_image) - # change opacity of im2 - alpha = im2.split()[3] - alpha = ImageEnhance.Brightness(alpha).enhance(opacity) - im2.putalpha(alpha) - - self._im = rgba_copy(self._im) - - # create a transparent layer - layer = Image.new('RGBA', self._im.size, (0, 0, 0, 0)) - # draw im2 in layer - if join_pos == JOIN_TOP_LEFT: - layer.paste(im2, (0, 0)) - elif join_pos == JOIN_TOP_RIGHT: - layer.paste(im2, ((self._im.size[0] - im2.size[0]), 0)) - elif join_pos == JOIN_BOT_LEFT: - layer.paste(im2, (0, (self._im.size[1] - im2.size[1]))) - elif join_pos == JOIN_BOT_RIGHT: - layer.paste(im2, ((self._im.size[0] - im2.size[0]), (self._im.size[1] - im2.size[1]))) - elif join_pos == JOIN_CENTER: - layer.paste(im2, (int((self._im.size[0] - im2.size[0]) / 2), int((self._im.size[1] - im2.size[1]) / 2))) - else: - logger.warning("Join position value %s is not supported", join_pos) - return False - - logger.debug('Joining two images') - self._im = Image.composite(layer, self._im, layer) - - return True diff --git a/common.py b/common.py index 75feda4..985006b 100644 --- a/common.py +++ b/common.py @@ -1,12 +1,42 @@ import os +import discid + +KEY_ALBUM = 'album' +KEY_APERTURE = 'aperture' +KEY_ARTIST = 'artist' +KEY_BITRATE = 'bitrate' +KEY_CAMERA = 'camera' +KEY_DURATION = 'duration' +KEY_EXPOSURE_PROGRAM = 'exposure_program' +KEY_EXPOSURE_TIME = 'exposure_time' +KEY_FLASH = 'flash' +KEY_FOCAL_LENGTH = 'focal_length' +KEY_GENRE = 'genre' +KEY_GPS = 'gps' +KEY_HEIGHT = 'height' +KEY_ISO = 'iso' +KEY_ORIENTATION = 'orientation' +KEY_RATIO = 'ratio' +KEY_SIZE = 'size' +KEY_TIME = 'time' # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp +KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst' +KEY_TITLE = 'title' +KEY_TRACK = 'track' +KEY_WIDTH = 'width' +KEY_YEAR = 'year' FILETYPE_AUDIO = 'audio' FILETYPE_IMAGE = 'image' FILETYPE_VIDEO = 'video' +FILETYPE_DISC = 'disc' + +CALLBACK_CDDB_CHOICE = 0 +CALLBACK_MAN_INPUT = 1 EXTENTIONS_AUDIO = ['.mp3', ] EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ] EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ] +PREFIX_DISC = '/dev/' def get_filetype(full_path): @@ -17,3 +47,9 @@ def get_filetype(full_path): return FILETYPE_IMAGE elif ext in EXTENTIONS_VIDEO: return FILETYPE_VIDEO + elif full_path.startswith(PREFIX_DISC): + return FILETYPE_DISC + + +def get_disc_device(): + return discid.get_default_device() diff --git a/convert.py b/convert.py index 59b0e80..ccdb3ba 100644 --- a/convert.py +++ b/convert.py @@ -1,8 +1,17 @@ import io -from media import common, logger +from media import common from PIL import Image import subprocess import platform +import logging +import os +import subprocess + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) def get_pil_image(media_instance): @@ -33,3 +42,94 @@ def get_pil_image(media_instance): return media_instance.copy() else: logger.warning('Instance type is not supported: %s' % type(media_instance)) + + +def FilenameFilter(filename: str) -> str: + # WHITELIST = [os.path.sep, os.path.extsep] + WHITELIST = [chr(x) for x in range(ord('0'), ord('9') + 1)] + WHITELIST += [chr(x) for x in range(ord('a'), ord('z') + 1)] + WHITELIST += ["ä", "ö", "ü", "ß"] + # + rv = "" + for c in filename.lower(): + rv += c if c in WHITELIST else '_' + return rv + + +def track_to_targetpath(basepath: str, track: dict, ext: str): + return os.path.join( + basepath, + FilenameFilter(track[common.KEY_ARTIST]), + "%04d_" % track[common.KEY_YEAR] + FilenameFilter(track[common.KEY_ALBUM]), + "%02d_" % track[common.KEY_TRACK] + FilenameFilter(track[common.KEY_TITLE]) + "." + ext + ) + + +def disc_track_rip(track_num: int, target_file: str, progress_callback): + FAC_SEC_VAL = 1224 + # + cdp_cmd = subprocess.getoutput("which cdparanoia") + if not cdp_cmd: + logger.error("cdparanoia is required for ripping. You need to install it to your system.") + else: + cmd = [cdp_cmd, "-e", "-X", "%d" % track_num, target_file] + cdp = subprocess.Popen(cmd, text=True, stderr=subprocess.PIPE) + # + rval = 0 + min_sec = None + max_sec = None + min_read = None + while (out := cdp.stderr.readline()) != "": + out = out.strip() + # identify minimum sector + if ("Ripping from sector" in out): + min_sec = int(list(filter(None, out.split(" ")))[3]) + # identify maximum sector + if ("to sector" in out): + max_sec = int(list(filter(None, out.split(" ")))[2]) + # identify progress + if "[read]" in out: + val = int(out.split(" ")[-1]) + if not min_read: + min_read = val + rval = max(val, rval) + try: + dsec = max_sec - min_sec + except TypeError: + logger.exception("Error while parsing cdparanoia. Start and End sector could not be detrmined.") + else: + p = (rval - min_read) / FAC_SEC_VAL / dsec + p = min(p, 1) + progress_callback(p) + progress_callback(1) + return cdp.wait() + + +def wav_to_mp3(infile: str, basepath: str, track_information, progress_callback, bitrate=256, vbr=0, quaulity=0): + lame_parameter = { + common.KEY_ARTIST: '--ta', + common.KEY_ALBUM: '--tl', + common.KEY_YEAR: '--ty', + common.KEY_GENRE: '--tg', + common.KEY_TRACK: '--tn', + common.KEY_TITLE: '--tt' + } + lame_cmd = subprocess.getoutput("which lame") + if not lame_cmd: + logger.error("lame is required for encoding. You need to install it to your system.") + else: + outfile = track_to_targetpath(basepath, track_information, 'mp3') + cmd = [lame_cmd, "-b", str(bitrate), "-V", str(vbr), "--vbr-old", "-q", str(quaulity), infile, outfile] + cmd.extend(["--tc", "Encoded by lame"]) + for key in track_information: + cmd.extend([lame_parameter[key], str(track_information[key])]) + lame = subprocess.Popen(cmd, text=True, stderr=subprocess.PIPE) + while (out := lame.stderr.readline()) != "": + out = out.strip() + posb = out.find("(") + posp = out.find("%") + if posb >= 0 and posp >= 0: + p = int(out[posb+1:posp]) / 100 + progress_callback(p) + progress_callback(1) + return lame.wait() diff --git a/image.py b/image.py new file mode 100644 index 0000000..fbbb43e --- /dev/null +++ b/image.py @@ -0,0 +1,150 @@ +import io +import logging +from PIL import Image, ImageEnhance, ExifTags + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + + +ORIENTATION_NORMAL = 1 +ORIENTATION_VERTICAL_MIRRORED = 2 +ORIENTATION_HALF_ROTATED = 3 +ORIENTATION_HORIZONTAL_MIRRORED = 4 +ORIENTATION_LEFT_ROTATED = 6 +ORIENTATION_RIGHT_ROTATED = 8 + +JOIN_TOP_LEFT = 1 +JOIN_TOP_RIGHT = 2 +JOIN_BOT_LEFT = 3 +JOIN_BOT_RIGHT = 4 +JOIN_CENTER = 5 + + +class image(object): + def __init__(self, media_instance=None): + if media_instance is not None: + self.load_from_file(media_instance) + else: + self._im = None + + def load_from_file(self, media_instance): + from media.convert import get_pil_image + # + self._im = get_pil_image(media_instance) + if self._im is None: + return False + try: + self._exif = dict(self._im._getexif().items()) + except AttributeError: + self._exif = {} + if type(self._im) is not Image.Image: + self._im = self._im.copy() + logger.debug('loading image from %s', repr(media_instance)) + return True + + def save(self, full_path): + if self._im is None: + logger.warning('No image available to be saved (%s)', repr(full_path)) + return False + else: + logger.debug('Saving image to %s', repr(full_path)) + with open(full_path, 'w') as fh: + im = self._im.convert('RGB') + im.save(fh, 'JPEG') + return True + + def image_data(self): + im = self._im.copy().convert('RGB') + output = io.BytesIO() + im.save(output, format='JPEG') + return output.getvalue() + + def resize(self, max_size): + if self._im is None: + logger.warning('No image available to be resized') + return False + else: + logger.debug('Resizing picture to max %d pixel in whatever direction', max_size) + x, y = self._im.size + xy_max = max(x, y) + self._im = self._im.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0) + return True + + def rotate_by_orientation(self, orientation=None): + if self._im is None: + logger.warning('No image available, rotation not possible') + return False + + if orientation is None: + exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items()) + try: + orientation = self._exif[exif_tags['Orientation']] + logger.debug("No orientation given, orientation %s extract from exif data", repr(orientation)) + except KeyError: + return False + + if orientation == ORIENTATION_HALF_ROTATED: + angle = 180 + elif orientation == ORIENTATION_LEFT_ROTATED: + angle = 270 + elif orientation == ORIENTATION_RIGHT_ROTATED: + angle = 90 + else: + if type(orientation) == int and orientation > 8: + logger.warning('Orientation %s unknown for rotation', repr(orientation)) + return False + logger.debug('Rotating picture by %d (deg)', angle) + self._im = self._im.rotate(angle, expand=True) + return True + + def join(self, join_image, join_pos=JOIN_TOP_RIGHT, opacity=0.7): + from media.convert import get_pil_image + + def rgba_copy(im): + if im.mode != 'RGBA': + return im.convert('RGBA') + else: + return im.copy() + + if self._im is None: + logger.warning('No image available, joining not possible') + return False + + # ensure type of join_image is PIL.Image + join_image = get_pil_image(join_image) + if join_image is None: + logger.warning('Image to be joined is not supported %s', repr(join_image)) + return False + + im2 = rgba_copy(join_image) + # change opacity of im2 + alpha = im2.split()[3] + alpha = ImageEnhance.Brightness(alpha).enhance(opacity) + im2.putalpha(alpha) + + self._im = rgba_copy(self._im) + + # create a transparent layer + layer = Image.new('RGBA', self._im.size, (0, 0, 0, 0)) + # draw im2 in layer + if join_pos == JOIN_TOP_LEFT: + layer.paste(im2, (0, 0)) + elif join_pos == JOIN_TOP_RIGHT: + layer.paste(im2, ((self._im.size[0] - im2.size[0]), 0)) + elif join_pos == JOIN_BOT_LEFT: + layer.paste(im2, (0, (self._im.size[1] - im2.size[1]))) + elif join_pos == JOIN_BOT_RIGHT: + layer.paste(im2, ((self._im.size[0] - im2.size[0]), (self._im.size[1] - im2.size[1]))) + elif join_pos == JOIN_CENTER: + layer.paste(im2, (int((self._im.size[0] - im2.size[0]) / 2), int((self._im.size[1] - im2.size[1]) / 2))) + else: + logger.warning("Join position value %s is not supported", join_pos) + return False + + logger.debug('Joining two images') + self._im = Image.composite(layer, self._im, layer) + + return True diff --git a/metadata.py b/metadata.py index d8207be..a15254a 100644 --- a/metadata.py +++ b/metadata.py @@ -1,47 +1,131 @@ -import logging -import os -from PIL import Image -import math -import media -import subprocess +import media.CDDB import time +import subprocess +from media import common +import math +from PIL import Image +import os +import logging +import sys -logger = media.logger - +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) +try: + import discid +except OSError: + logger.exception("You might install python3-libdiscid") __KEY_CAMERA_VENDOR__ = 'camera_vendor' __KEY_CAMERA_MODEL__ = 'camera_model' +def get_media_data(full_path, user_callback=None): + # + ft = common.get_filetype(full_path) + # + if ft == common.FILETYPE_AUDIO: + return get_audio_data(full_path) + elif ft == common.FILETYPE_IMAGE: + return get_image_data(full_path) + elif ft == common.FILETYPE_VIDEO: + return get_video_data(full_path) + elif ft == common.FILETYPE_DISC: + return get_disc_data(full_path, user_callback) + else: + logger.warning('Filetype not known: %s', full_path) + + def get_audio_data(full_path): conv_key_dict = {} - conv_key_dict['album'] = (str, media.KEY_ALBUM) - conv_key_dict['TAG:album'] = (str, media.KEY_ALBUM) - conv_key_dict['TAG:artist'] = (str, media.KEY_ARTIST) - conv_key_dict['artist'] = (str, media.KEY_ARTIST) - conv_key_dict['bit_rate'] = (__int_conv__, media.KEY_BITRATE) - conv_key_dict['duration'] = (float, media.KEY_DURATION) - conv_key_dict['TAG:genre'] = (str, media.KEY_GENRE) - conv_key_dict['genre'] = (str, media.KEY_GENRE) - conv_key_dict['TAG:title'] = (str, media.KEY_TITLE) - conv_key_dict['title'] = (str, media.KEY_TITLE) - conv_key_dict['TAG:track'] = (__int_conv__, media.KEY_TRACK) - conv_key_dict['track'] = (__int_conv__, media.KEY_TRACK) - conv_key_dict['TAG:date'] = (__int_conv__, media.KEY_YEAR) - conv_key_dict['date'] = (__int_conv__, media.KEY_YEAR) + conv_key_dict['album'] = (str, common.KEY_ALBUM) + conv_key_dict['TAG:album'] = (str, common.KEY_ALBUM) + conv_key_dict['TAG:artist'] = (str, common.KEY_ARTIST) + conv_key_dict['artist'] = (str, common.KEY_ARTIST) + conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE) + conv_key_dict['duration'] = (float, common.KEY_DURATION) + conv_key_dict['TAG:genre'] = (str, common.KEY_GENRE) + conv_key_dict['genre'] = (str, common.KEY_GENRE) + conv_key_dict['TAG:title'] = (str, common.KEY_TITLE) + conv_key_dict['title'] = (str, common.KEY_TITLE) + conv_key_dict['TAG:track'] = (__int_conv__, common.KEY_TRACK) + conv_key_dict['track'] = (__int_conv__, common.KEY_TRACK) + conv_key_dict['TAG:date'] = (__int_conv__, common.KEY_YEAR) + conv_key_dict['date'] = (__int_conv__, common.KEY_YEAR) return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path) +def get_disc_data(full_path, user_callback): + # + # Read Information from CDDB database + # + did = media.CDDB.discid() + if did is None: + logger.error("Could not determine disc id...") + sys.exit(1) + q = media.CDDB.query(did) + if q is None: + data = { + common.KEY_ARTIST: None, + common.KEY_ALBUM: None, + common.KEY_YEAR: None, + common.KEY_GENRE: None + } + for i in range(0, int(did.split('+')[1])): + data["track_%02d" % (i + 1)] = None + data = user_callback(common.CALLBACK_MAN_INPUT, data) + return media.CDDB.my_disc_metadata(**data) + + if len(q) == 1: + # Only one entry + did = tuple(q.keys())[0] + else: + # multiple entries (choose) + if user_callback is None: + logger.warning("No usercallback to handle multiple cddb choices...") + sys.exit(1) + did = user_callback(common.CALLBACK_CDDB_CHOICE, q) + + return media.CDDB.cddb(did) + """ + musicbrainzngs.set_useragent("pyrip", "0.1", "your@mail") + disc = discid.read() + disc_id = disc.id + disc_data = {} + try: + result = musicbrainzngs.get_releases_by_discid(disc_id, includes=["artists", "recordings"]) + except musicbrainzngs.ResponseError: + logger.exception("disc not found or bad response") + sys.exit(1) + else: + disc_data[common.KEY_ARTIST] = result["disc"]["release-list"][0]["artist-credit-phrase"] + disc_data[common.KEY_ALBUM] = result["disc"]["release-list"][0]["title"] + disc_data[common.KEY_YEAR] = int(result["disc"]["release-list"][0]["date"][:4]) + data_copy = dict(disc_data) + disc_data["id"] = result["disc"]["release-list"][0]["id"] + disc_data["tracks"] = [] + # get tracklist + for entry in result["disc"]["release-list"][0]["medium-list"][0]["track-list"]: + track = dict(data_copy) + track[common.KEY_TITLE] = entry["recording"]["title"] + track[common.KEY_TRACK] = int(entry['number']) + disc_data["tracks"].append(track) + return disc_data + """ + + def get_video_data(full_path): conv_key_dict = {} - conv_key_dict['creation_time'] = (__vid_datetime_conv__, media.KEY_TIME) - conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, media.KEY_TIME) - conv_key_dict['bit_rate'] = (__int_conv__, media.KEY_BITRATE) - conv_key_dict['duration'] = (float, media.KEY_DURATION) - conv_key_dict['height'] = (__int_conv__, media.KEY_HEIGHT) - conv_key_dict['width'] = (__int_conv__, media.KEY_WIDTH) - conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, media.KEY_RATIO) + conv_key_dict['creation_time'] = (__vid_datetime_conv__, common.KEY_TIME) + conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, common.KEY_TIME) + conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE) + conv_key_dict['duration'] = (float, common.KEY_DURATION) + conv_key_dict['height'] = (__int_conv__, common.KEY_HEIGHT) + conv_key_dict['width'] = (__int_conv__, common.KEY_WIDTH) + conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, common.KEY_RATIO) return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path) @@ -50,25 +134,25 @@ def get_image_data(full_path): def __adapt__data__(data, full_path): - data[media.KEY_SIZE] = os.path.getsize(full_path) + data[common.KEY_SIZE] = os.path.getsize(full_path) # Join Camera Vendor and Camera Model if __KEY_CAMERA_MODEL__ in data and __KEY_CAMERA_VENDOR__ in data: model = data.pop(__KEY_CAMERA_MODEL__) vendor = data.pop(__KEY_CAMERA_VENDOR__) - data[media.KEY_CAMERA] = '%s: %s' % (vendor, model) + data[common.KEY_CAMERA] = '%s: %s' % (vendor, model) # Add time if not exists - if media.KEY_TIME not in data: - if media.KEY_YEAR in data and media.KEY_TRACK in data: - if data[media.KEY_YEAR] != 0: # ignore year 0 - must be wrong + if common.KEY_TIME not in data: + if common.KEY_YEAR in data and common.KEY_TRACK in data: + if data[common.KEY_YEAR] != 0: # ignore year 0 - must be wrong # Use a date where track 1 is the newest in the given year - minute = int(data[media.KEY_TRACK] / 60) - second = (data[media.KEY_TRACK] - 60 * minute) % 60 + minute = int(data[common.KEY_TRACK] / 60) + second = (data[common.KEY_TRACK] - 60 * minute) % 60 # - data[media.KEY_TIME] = int(time.mktime((data[media.KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0))) - data[media.KEY_TIME_IS_SUBSTITUTION] = True + data[common.KEY_TIME] = int(time.mktime((data[common.KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0))) + data[common.KEY_TIME_IS_SUBSTITUTION] = True else: - data[media.KEY_TIME] = int(os.path.getmtime(full_path)) - data[media.KEY_TIME_IS_SUBSTITUTION] = True + data[common.KEY_TIME] = int(os.path.getmtime(full_path)) + data[common.KEY_TIME_IS_SUBSTITUTION] = True return data @@ -114,19 +198,19 @@ def __get_exif_data__(full_path): else: conv_key_dict = {} # IMAGE - conv_key_dict[0x9003] = (__datetime_conv__, media.KEY_TIME) - conv_key_dict[0x8822] = (__exposure_program_conv__, media.KEY_EXPOSURE_PROGRAM) - conv_key_dict[0x829A] = (__num_denum_conv__, media.KEY_EXPOSURE_TIME) - conv_key_dict[0x9209] = (__flash_conv__, media.KEY_FLASH) - conv_key_dict[0x829D] = (__num_denum_conv__, media.KEY_APERTURE) - conv_key_dict[0x920A] = (__num_denum_conv__, media.KEY_FOCAL_LENGTH) - conv_key_dict[0x8825] = (__gps_conv__, media.KEY_GPS) - conv_key_dict[0xA003] = (__int_conv__, media.KEY_HEIGHT) - conv_key_dict[0x8827] = (__int_conv__, media.KEY_ISO) + conv_key_dict[0x9003] = (__datetime_conv__, common.KEY_TIME) + conv_key_dict[0x8822] = (__exposure_program_conv__, common.KEY_EXPOSURE_PROGRAM) + conv_key_dict[0x829A] = (__num_denum_conv__, common.KEY_EXPOSURE_TIME) + conv_key_dict[0x9209] = (__flash_conv__, common.KEY_FLASH) + conv_key_dict[0x829D] = (__num_denum_conv__, common.KEY_APERTURE) + conv_key_dict[0x920A] = (__num_denum_conv__, common.KEY_FOCAL_LENGTH) + conv_key_dict[0x8825] = (__gps_conv__, common.KEY_GPS) + conv_key_dict[0xA003] = (__int_conv__, common.KEY_HEIGHT) + conv_key_dict[0x8827] = (__int_conv__, common.KEY_ISO) conv_key_dict[0x010F] = (str, __KEY_CAMERA_VENDOR__) conv_key_dict[0x0110] = (str, __KEY_CAMERA_MODEL__) - conv_key_dict[0x0112] = (__int_conv__, media.KEY_ORIENTATION) - conv_key_dict[0xA002] = (__int_conv__, media.KEY_WIDTH) + conv_key_dict[0x0112] = (__int_conv__, common.KEY_ORIENTATION) + conv_key_dict[0xA002] = (__int_conv__, common.KEY_WIDTH) for key in conv_key_dict: if key in exif: tp, name = conv_key_dict[key] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ecd1894 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pillow +discid