gpodder/share/gpodder/extensions/tagging.py

300 lines
9.5 KiB
Python
Raw Normal View History

# -*- 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
from mutagen import File
from mutagen.easyid3 import EasyID3
2020-04-15 09:44:18 +02:00
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
2018-07-29 14:46:29 +02:00
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')
2012-03-27 01:24:42 +02:00
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
2020-05-25 18:39:00 +02:00
__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',
2018-10-08 19:18:21 +02:00
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")
2020-04-15 09:44:18 +02:00
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)