# -*- coding: utf-8 -*-
"""This module provides an easily scriptable interface to tagging an assortment
of media types including x264 video, AAC audio, and many others with iTunes
formatted metadata via the SublerCLI. By simply creating new metadata
:class:`Atom`'s and specifying any additionally desired variables in an
instance of :class:`Subler` you can quickly execute the tagging of metadata to
a specified file.
"""
import os
import logging
from collections import namedtuple
from .utils import subler_executable, get_output
__author__ = 'Jon Nappi'
__all__ = ['Atom', 'Subler']
_Atom = namedtuple('_Atom', ['tag', 'value'])
[docs]class Atom(_Atom):
"""A class representing a single metadata atom. It's important to note that
if you attempt to use an invalid tag for this Atom it will not be used when
actually tagging the media file via a :class:`Subler` instance.
"""
_valid_tags = ('Artist', 'Album Artist', 'Album', 'Grouping', 'Composer',
'Comments', 'Genre', 'Release Date', 'Track #', 'Disk #',
'Tempo', 'TV Show', 'TV Episode #', 'TV Network',
'TV Episode ID', 'TV Season', 'Description',
'Long Description', 'Series Description', 'HD Video',
'Rating Annotation', 'Studio', 'Cast', 'Director',
'Gapless', 'Codirector', 'Producers', 'Screenwriters',
'Lyrics', 'Copyright', 'Encoding Tool', 'Encoded By',
'Keywords', 'Category', 'contentID', 'artistID',
'playlistID', 'genreID', 'composerID', 'XID',
'iTunes Account', 'iTunes Account Type', 'iTunes Country',
'Track Sub-Title', 'Song Description', 'Art Director',
'Arranger', 'Lyricist', 'Acknowledgement', 'Conductor',
'Linear Notes', 'Record Company', 'Original Artist',
'Phonogram Rights', 'Producer', 'Performer', 'Publisher',
'Sound Engineer', 'Soloist', 'Credits', 'Thanks',
'Online Extras', 'Executive Producer', 'Sort Name',
'Sort Artist', 'Sort Album Artist', 'Sort Album',
'Sort Composer', 'Sort TV Show', 'Artwork', 'Name',
'Rating', 'Media Kind')
[docs] def is_valid(self):
"""Check that the data in this :class:`Atom` is valid"""
return self.tag in self._valid_tags
@property
def data(self):
"""The Subler argument formatted version of this :class:`Atom`"""
return '{%s:%s}' % (self.tag, self.value)
[docs]class Subler(object):
"""A Python interface to the SublerCLI that can be used to easily read
from and write to a specified source file.
"""
__executable = subler_executable()
[docs] def __init__(self, source, dest=None, chapters=None, delay=None,
chapters_preview=False, height=None, language='English',
remove=False, optimize=True, downmix=False, rating=None,
media_kind='Movie', explicit=None, metadata=None):
"""Create a Subler tagging instance
:param source: The source file to feed to Subler
:param dest: The destination file to save any changes to
:param chapters: A .txt file with chapter information
:param chapters_preview: Boolean option to create an additional video
track with the preview of the chapters. Used by iTunes and some
other media clients.
:param delay: The delay of the subtitle track in ms
:param height: The pixel height of the subtitle track
:param language: The language of the subtitle track (i.e. English)
:param remove: Boolean flag for remove all existing subtitles tracks
:param optimize: Boolean flag for optimizing the file by moving the
moov atom at the begining and interleaving the samples
:param downmix: downmix audio (mono, stereo, dolby, pl2) from the
source file
:param rating: A valid US, UK, or German content rating
:param media_kind: The type of media represented by the source file.
Valid values are Music, Audiobook, Music Video, Movie, TV Show,
Booklet, or Ringtone
:param explicit: The explicit-ness warning of the content in the source
file. Valid values are: None, "Clean", and "Explicit"
:param metadata: A list of :class:`Atom`'s to be applied to the source
file as metadata
"""
self.source = source
if dest is None:
dest = self._generate_dest()
self.dest = dest
self.chapters = chapters
self.chapters_preview = chapters_preview
self.delay = delay
self.height = height
self.language = language
self.remove = remove
self.optimize = optimize
self.downmix = downmix
self._rating = None
self._explicit = None
self._media_kind = None
self.rating = rating
self.media_kind = media_kind
self.explicit = explicit
self.metadata = metadata
self._logger = logging.getLogger('subler.Subler')
def _generate_dest(self):
"""Generate a destination file name for the provided source file. This
function will increment a number to append to the file name in order to
distinguish it from other copies of the same file name
:return: The absolute path for the destination file
"""
name, ext = os.path.splitext(self.source)
counter = 1
dest = name + ' ({})'.format(counter) + ext
while os.path.exists(dest):
counter += 1
dest = name + ' ({})'.format(counter) + ext
return dest
@property
def version(self):
"""The current version of your systems SublerCLI package"""
cmd = [self.__executable, '-version']
return get_output(cmd)
@property
def tracks(self):
"""A list of tracks found the source file"""
cmd = [self.__executable, '-source', self.source, '-listtracks']
return get_output(cmd).split('\n')
@property
def existing_metadata_raw(self):
"""The metadata currently contained in the source file"""
cmd = [self.__executable, '-source', self.source, '-listmetadata']
return get_output(cmd).split('\n')
@property
def existing_metadata(self):
"""Atom representations of the metadata currently contained in the
source file
"""
raw = self.existing_metadata_raw
atoms = []
for s in raw:
key, val = s[:s.find(':')].strip(), s[s.find(':') + 1:].strip()
atoms.append(Atom(key, val))
return atoms
@property
def existing_metadata_collection(self):
"""AtomCollection representation of the metadata currently contained in
the source file
"""
from .tools import AtomCollection
raw = self.existing_metadata_raw
atoms = AtomCollection()
for s in raw:
key, val = s[:s.find(':')].strip(), s[s.find(':') + 1:].strip()
atoms[key] = val
return atoms
@property
def rating(self):
"""The content rating of the source file. Valid US content ratings are:
Not Rated, G, PG, PG-13, R, NC-17, TV-Y, TV-Y7, TV-G, TV-PG, TV-14,
TV-MA, and Unrated. Valid UK content ratings are: Not Rated, U, Uc, PG,
12, 12A, 15, 18, R18, Exempt, Unrated, and Caution. Valid German
content ratings are FSK 0, FSK 6, FSK 12, FSK 16, and FSK 18.
"""
return self._rating
@rating.setter
def rating(self, new_rating):
"""We will only update the rating provided if *new_rating* is a valid,
Subler-supported rating.
"""
valid = ('Not Rated', 'G', 'PG', 'PG-13', 'R', 'NC-17', 'Unrated',
'TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA', 'U', 'Uc',
'12', '12A', '15', '18', 'R18', 'Exempt', 'Caution', 'FSK 0',
'FSK 6', 'FSK 12', 'FSK 16', 'FSK 18')
if new_rating in valid:
self._rating = new_rating
@property
def media_kind(self):
"""The type of media encapsulated in the source file. Valid values are:
Music, Audiobook, Music Video, Movie, TV Show, Booklet, or Ringtone
"""
return self._media_kind
@media_kind.setter
def media_kind(self, new_media_kind):
valid = ('Music', 'Audiobook', 'Music Video', 'Movie', 'TV Show',
'Booklet', 'Ringtone')
if new_media_kind in valid:
self._media_kind = new_media_kind
@property
def explicitness(self):
"""The explicit rating of the content in the source file. Valid explicit
ratings are: None, "Clean", and "Explicit"
"""
return self._explicit
@explicitness.setter
def explicitness(self, explicit_rating):
"""If we don't receive a valid Explicitness rating, we'll ignore it"""
valid = (None, 'Clean', 'Explicit')
if explicit_rating in valid:
self._explicit = explicit_rating
[docs] def tag(self):
"""Apply the specified metadata to the source file and output it to
the specified destination file
"""
cmd = [self.__executable, '-source', self.source]
if self.dest:
cmd += ['-dest', self.dest]
self.metadata.append(Atom('Media Kind', self.media_kind))
if self._rating is not None:
self.metadata.append(Atom('Rating', self.rating))
if self.explicitness:
self.metadata.append(Atom('Content Rating', self.explicitness))
tags = [atom.data for atom in self.metadata if atom.is_valid()]
cmd += ['-metadata', ''.join(tags)]
if self.optimize:
cmd += ['-optimize']
self._logger.debug(cmd)
return get_output(cmd)