Added som features for audio handling (cddb, rip, encode)
This commit is contained in:
parent
abcf63d02e
commit
402e837551
138
CDDB.py
Normal file
138
CDDB.py
Normal file
@ -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
|
233
__init__.py
233
__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
|
||||
|
36
common.py
36
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()
|
||||
|
102
convert.py
102
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()
|
||||
|
150
image.py
Normal file
150
image.py
Normal file
@ -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
|
186
metadata.py
186
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]
|
||||
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
pillow
|
||||
discid
|
Loading…
x
Reference in New Issue
Block a user