gpodder/share/gpodder/extensions/tagging.py

301 lines
9.5 KiB
Python

# -*- coding: utf-8 -*-
####
# 01/2011 Bernd Schlapsi <brot@gmx.info>
#
# This script is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Dependencies:
# * python-mutagen (Mutagen is a Python module to handle audio metadata)
#
# This extension script adds episode title and podcast title to the audio file
# The episode title is written into the title tag
# The podcast title is written into the album tag
import base64
import datetime
import logging
import mimetypes
import os
from mutagen import File
from mutagen.easyid3 import EasyID3
from mutagen.easymp4 import EasyMP4Tags
from mutagen.flac import Picture
from mutagen.id3 import APIC, ID3
from mutagen.mp3 import MP3, EasyMP3
from mutagen.mp4 import MP4Cover, MP4Tags
import gpodder
from gpodder import coverart
logger = logging.getLogger(__name__)
# workaround for https://github.com/quodlibet/mutagen/issues/334
# can't add_tags to MP4 when file has no tag
MP4Tags._padding = 0
_ = gpodder.gettext
__title__ = _('Tag downloaded files using Mutagen')
__description__ = _('Add episode and podcast titles to MP3/OGG tags')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/tagging.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/Tagging'
__category__ = 'post-download'
DefaultConfig = {
'strip_album_from_title': True,
'genre_tag': 'Podcast',
'always_remove_tags': False,
'auto_embed_coverart': False,
'set_artist_to_album': False,
'set_version': 4,
'modify_tags': True
}
class AudioFile(object):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
self.filename = filename
self.album = album
self.title = title
self.subtitle = subtitle
self.genre = genre
self.pubDate = pubDate
self.cover = cover
def remove_tags(self):
audio = File(self.filename, easy=True)
if audio.tags is not None:
audio.delete()
audio.save()
def write_basic_tags(self, modify_tags, set_artist_to_album, set_version):
audio = File(self.filename, easy=True)
if audio is None:
logger.warning("Unable to add tags to file '%s'", self.filename)
return
if audio.tags is None:
audio.add_tags()
if modify_tags:
if self.album is not None:
audio.tags['album'] = self.album
if self.title is not None:
audio.tags['title'] = self.title
if self.subtitle is not None:
audio.tags['subtitle'] = self.subtitle
if self.subtitle is not None:
audio.tags['comments'] = self.subtitle
if self.genre is not None:
audio.tags['genre'] = self.genre
if self.pubDate is not None:
audio.tags['date'] = self.pubDate
if set_artist_to_album:
audio.tags['artist'] = self.album
if type(audio) is EasyMP3:
audio.save(v2_version=set_version)
else:
# Not actually audio
audio.save()
def insert_coverart(self):
""" implement the cover art logic in the subclass
"""
None
def get_cover_picture(self, cover):
""" Returns mutagen Picture class for the cover image
Usefull for OGG and FLAC format
Picture type = cover image
see http://flac.sourceforge.net/documentation_tools_flac.html#encoding_options
"""
f = file(cover)
p = Picture()
p.type = 3
p.data = f.read()
p.mime = mimetypes.guess_type(cover)[0]
f.close()
return p
class OggFile(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(OggFile, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = File(self.filename, easy=True)
p = self.get_cover_picture(self.cover)
audio['METADATA_BLOCK_PICTURE'] = base64.b64encode(p.write())
audio.save()
class Mp4File(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(Mp4File, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = File(self.filename)
if self.cover.endswith('png'):
cover_format = MP4Cover.FORMAT_PNG
else:
cover_format = MP4Cover.FORMAT_JPEG
data = open(self.cover, 'rb').read()
audio.tags['covr'] = [MP4Cover(data, cover_format)]
audio.save()
class Mp3File(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(Mp3File, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = MP3(self.filename, ID3=ID3)
if audio.tags is None:
audio.add_tags()
audio.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime=mimetypes.guess_type(self.cover)[0],
type=3,
desc='Cover',
data=open(self.cover, 'rb').read()
)
)
audio.save()
class gPodderExtension:
def __init__(self, container):
self.container = container
# fix #737 EasyID3 doesn't recognize subtitle and comment tags
EasyID3.RegisterTextKey("comments", "COMM")
EasyID3.RegisterTextKey("subtitle", "TIT3")
EasyMP4Tags.RegisterTextKey("comments", "desc")
EasyMP4Tags.RegisterFreeformKey("subtitle", "SUBTITLE")
def on_episode_downloaded(self, episode):
info = self.read_episode_info(episode)
if info['filename'] is None:
return
self.write_info2file(info, episode)
def get_audio(self, info, episode):
audio = None
cover = None
audioClass = None
if self.container.config.auto_embed_coverart:
cover = self.get_cover(episode.channel)
if info['filename'].endswith('.mp3'):
audioClass = Mp3File
elif info['filename'].endswith('.ogg'):
audioClass = OggFile
elif info['filename'].endswith('.m4a') or info['filename'].endswith('.mp4'):
audioClass = Mp4File
elif File(info['filename'], easy=True):
# mutagen can work with it: at least add basic tags
audioClass = AudioFile
if audioClass:
audio = audioClass(info['filename'],
info['album'],
info['title'],
info['subtitle'],
info['genre'],
info['pubDate'],
cover)
return audio
def read_episode_info(self, episode):
info = {
'filename': None,
'album': None,
'title': None,
'subtitle': None,
'genre': None,
'pubDate': None
}
# read filename (incl. file path) from gPodder database
info['filename'] = episode.local_filename(create=False, check_only=True)
if info['filename'] is None:
return
# read title+album from gPodder database
info['album'] = episode.channel.title
title = episode.title
if (self.container.config.strip_album_from_title and title and info['album'] and title.startswith(info['album'])):
info['title'] = title[len(info['album']):].lstrip()
else:
info['title'] = title
info['subtitle'] = episode._text_description
if self.container.config.genre_tag is not None:
info['genre'] = self.container.config.genre_tag
# convert pubDate to string
try:
pubDate = datetime.datetime.fromtimestamp(episode.pubDate)
info['pubDate'] = pubDate.strftime('%Y-%m-%d %H:%M')
except:
try:
# since version 3 the published date has a new/other name
pubDate = datetime.datetime.fromtimestamp(episode.published)
info['pubDate'] = pubDate.strftime('%Y-%m-%d %H:%M')
except:
info['pubDate'] = None
return info
def write_info2file(self, info, episode):
audio = self.get_audio(info, episode)
if self.container.config.always_remove_tags:
audio.remove_tags()
else:
audio.write_basic_tags(self.container.config.modify_tags,
self.container.config.set_artist_to_album,
self.container.config.set_version)
if self.container.config.auto_embed_coverart:
audio.insert_coverart()
logger.info('tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title)
def get_cover(self, podcast):
downloader = coverart.CoverDownloader()
return downloader.get_cover(podcast.cover_file, podcast.cover_url,
podcast.url, podcast.title, None, None, True)