initial commit

master
brent s. 6 years ago
commit 6426699056
  1. 65
      blank.schema.sql
  2. 153
      podloader.ini.dist
  3. 552
      podloader.py

@ -0,0 +1,65 @@
-- MySQL dump 10.16 Distrib 10.1.21-MariaDB, for Linux (x86_64)
--
-- Host: db.domain.tld Database: myDB
-- ------------------------------------------------------
-- Server version 10.1.21-MariaDB

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `myTBL`
--

DROP TABLE IF EXISTS `myTBL`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `myTBL` (
`episode` varchar(8) NOT NULL,
`file_prefix` varchar(255) NOT NULL,
`sha_mp3` char(64) NOT NULL,
`sha_ogg` char(64) NOT NULL,
`bytesize_mp3` int(16) NOT NULL,
`bytesize_ogg` int(16) NOT NULL,
`length` int(8) NOT NULL,
`editor` varchar(64) NOT NULL,
`intro_title` varchar(128) NOT NULL,
`intro_artist` varchar(128) NOT NULL,
`intro_link` varchar(256) NOT NULL,
`intro_copyright` varchar(45) NOT NULL,
`intro_copyrightlink` varchar(256) NOT NULL,
`outro_title` varchar(128) NOT NULL,
`outro_artist` varchar(128) NOT NULL,
`outro_link` varchar(256) NOT NULL,
`outro_copyright` varchar(45) NOT NULL,
`outro_copyrightlink` varchar(256) NOT NULL,
`recorded` datetime NOT NULL,
`released` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`episode`),
UNIQUE KEY `episode_UNIQUE` (`episode`),
UNIQUE KEY `file_prefix_UNIQUE` (`file_prefix`),
UNIQUE KEY `sha_mp3_UNIQUE` (`sha_mp3`),
UNIQUE KEY `sha_ogg_UNIQUE` (`sha_ogg`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2017-03-23 18:19:43

@ -0,0 +1,153 @@
## Podloader config ##

[rsync]
# The server the files go to
host = domain.tld

# The remote path root. This must be a full/absolute path!
path = /srv/http/myVhostDir

# The remote user
user = sshuser

[mysql]
# The mysql server. Note that this will be overridden if you
# use a .my.cnf and a host is specified in there.
host = db.${rsync:host}

# The mysql server's port. Note that this will be overridden
# if you use a .my.cnf and a port is specified in there.
port = 3306

# The mysql user. Note that this will be overridden if you
# use a .my.cnf and a user is specified in there.
user = mysqluser

# The mysql DB. Note that this will be overridden if you
# use a .my.cnf and a user is specified in there.
db = myDB

# The mysql table
table = myTBL

# The column names (separated by commas) for, in order:
# episode ID (e.g. "S1E2")
# file_prefix (the filename only WITHOUT .ogg/.mp3)
# sha_mp3 (the column to hold the SHA256 of the MP3 file)
# sha_ogg (the column to hold the SHA256 of the OGG file)
# bytesize_mp3 (size of the MP3 file in bytes)
# bytesize_ogg (size of the OGG file in bytes)
# length (the length of the track in seconds)
# editor (the name of the person that edited the audio track(s)
# intro_title (the title of the intro music track)
# intro_artist (the artist that composed the intro music track)
# intro_link (a URL to the intro track or artist's site/page)
# intro_copyright (the copyright license for the intro track, e.g. "CC-BY-SA 3.0")
# intro_copyrightlink (a URL to the full terms of the intro track's copyright)
# outro_title (the title of the outro music track)
# outro_artist (the artist that composed the outro music track)
# outro_link (a URL to the outro track or artist's site/page)
# outro_copyright (the copyright license for the outro track, e.g. "CC-BY-SA 3.0")
# outro_copyrightlink (a URL to the full terms of the outro track's copyright)
# recorded (when the episode was recorded)
# released (when the episode was released)
#
# Note that a dump of the *table* is included (blank.schema.sql). Feel free to use it:
# mysql -e "CREATE DATABASE myDB" && mysql myDB < blank.schema.sql
# This will create a database named "myDB" (you can skip that part if you already have a database),
# and create a table named "myTBL" according to the default spec outlined in here.
#
cols = episode,file_prefix,sha_mp3,sha_ogg,bytesize_mp3,bytesize_ogg,length,editor,intro_title,intro_artist,intro_link,intro_copyright,intro_copyrightlink,outro_title,outro_artist,outro_link,outro_copyright,outro_copyrightlink,recorded,released

# The remote mysql password - if this is set to False/no/0,
# we'll just use the my.cnf-formatted INI file (e.g. ~/.my.cnf) instead.
password = False

# If the above is False, path to the .my.cnf
conf = ~/.my.cnf

# If password is False, what [client] section suffix should we use?
# Note that this is going to look like e.g. [clientremote1] in the config
# file. (correlates to mysql's --defaults-group-suffix=)
confsec = remote1

[gpg]
# Should we actually sign episodes? True/yes/1 or False/no/0.
enabled = True

# The GPG key ID(s) (in a comma-separated list) to sign the episode with.
# You must have the private key in your *local* keyring!
keys = D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F

# The path to your GNUPG homedir.
homedir = ~/.gnupg

[local]
# The local path root to the edited FLAC files
path = ~/podcast

# A subdir for the episode-specific files. If it contains one of the following values,
# substitution will be done.
# Special values:
# - SEASONEPISODE = A special string that uses the -s/--season and -e/--episode strings together.
# i.e. if season is 1 and episode is 13, it'd be "s1e13".
# - SEASON = A special string that uses -s/--season.
# - EPISODE = A special string that uses -e/--episode.
subdir = SEASONEPISODE

# Where the transcoded media and GPG sigs (if enabled) should go
# (in a structure of <path>/<season>/<episode>/{mp3,ogg,gpg}/)
mediadir = ${path}/releases

[tags]
# What should the Artist string be?
artist = Podcastin' Joe

# What should the Album name be?
# If you set this to SEASON, it will set this to whatever's specified for -s/--season
album = SEASON

# How many digits should the season be padded to? (i.e. the minimum number of digits)
# A pad of three would have Season 3 be "003".
season_pad = 1

# How many digits should the episode be padded to? (i.e. the minimum number of digits)
# A pad of three would have Episode 1 be "001".
episode_pad = 1

# What should the Year be set to?
# If set as False/no/0, it will be automatically determined by the raw media file's metadata.
year = False

# What track number should be set?
# If set as EPISODE, it will set this to whatever's specified for -e/--episode
track = EPISODE

# What genre should be set?
genre = Podcast

# What should be set as the comment field?
comment = https://podcast.domain.tld

# What should be set as the Copyright notice?
copyright = CC-BY-SA 4.0

# What should be set for the URL field?
# Special values:
# - SEASONEPISODE = A special string that uses the -s/--season and -e/--episode strings together.
# i.e. if season is 1 and episode is 13, it'd be "S1E13".
# - SEASON = A special string that uses -s/--season.
# - EPISODE = A special string that uses -e/--episode.
url = ${comment}/episodes/SEASONEPISODE

# Who encoded the file? (e.g. what is your name)
encoded = Joe Schmoe

# Who edited the episode? (see -d/--editor)
# Note that this can contain (and should, if available)
# contain a link (e.g.:
# <a href="https://editorname.tld">Editor Name</a> )
editor = <a href="${comment}/editor">Some Editor</a>

# A local path to the image to embed.
img = ${local:path}/images/podcast_logo.jpg

@ -0,0 +1,552 @@
#!/usr/bin/env python3


import configparser
import argparse
import os
import re
import base64
import subprocess
import hashlib
import datetime
import pymysql
import magic
import gpgme
from mutagen.id3 import ID3, APIC, TALB, TDRC, TENC, TRCK, COMM, WXXX, TCON, TIT2, TPE1, TCOP
from mutagen.oggvorbis import OggVorbis
from mutagen.flac import Picture

dflt_config_paths = ['~/.podloader.ini',
'~/.podloader/podloader.ini',
'podloader.ini',
'podloader.ini.dist']

def configParse(configfile = dflt_config_paths[-1]):
# Here we find and parse the config, then return a dict of the values.
# We COULD return a configparser object, but that's a PITA to reference.
conf = configfile
olddef = dflt_config_paths[-1]
for i, item in enumerate(dflt_config_paths):
dflt_config_paths[i] = os.path.expanduser(item)
for i, item in enumerate(dflt_config_paths):
if not dflt_config_paths[i].startswith('/'):
dflt_config_paths[i] = '{0}/{1}'.format(os.path.dirname(os.path.realpath(__file__)), item)
if configfile != olddef:
conf = configfile
else:
for p in dflt_config_paths:
if os.path.isfile(p):
conf = p
break
defconf = dflt_config_paths[-1]
config = configparser.ConfigParser()
config._interpolation = configparser.ExtendedInterpolation()
config.read([defconf, conf])
config_dict = {s:dict(config.items(s)) for s in config.sections()}
# Convert the booleans to pythonic booleans in the dict, convert to ints, etc.
if config['mysql']['password'] == 'False':
config_dict['mysql']['password'] = config['mysql'].getboolean('password')
config_dict['gpg']['enabled'] = config['gpg'].getboolean('enabled')
config_dict['mysql']['port'] = config['mysql'].getint('port')
config_dict['tags']['season_pad'] = config['tags'].getint('season_pad')
config_dict['tags']['episode_pad'] = config['tags'].getint('episode_pad')
# Set some "magic" interpolation
if not config_dict['mysql']['password']:
config_dict['mysql']['conf'] = os.path.expanduser(config_dict['mysql']['conf'])
mysqlconf = configparser.ConfigParser(allow_no_value = True)
if os.path.isfile(config_dict['mysql']['conf']):
mysqlconf.read(config_dict['mysql']['conf'])
mysqlcnf_dict = {s:dict(mysqlconf.items(s)) for s in mysqlconf.sections()}
mysqlcnf = mysqlcnf_dict['client' + config_dict['mysql']['confsec']]
if 'host' in mysqlcnf:
config_dict['mysql']['host'] = mysqlcnf['host']
else:
config_dict['mysql']['host'] = 'localhost'
if 'ssl' in mysqlcnf:
config_dict['mysql']['ssl'] = {}
for c in ('ssl-ca','ssl-cert', 'ssl-key', 'ssl-cipher'):
if c in mysqlcnf:
newkey = c.replace('ssl-', '')
config_dict['mysql']['ssl'][newkey] = mysqlcnf[c]
config_dict['mysql']['user'] = mysqlcnf['user']
config_dict['mysql']['password'] = mysqlcnf['password']
if 'port' in mysqlcnf:
config_dict['mysql']['port'] = int(mysqlcnf['port'])
else:
config_dict['mysql']['port'] = 3306
del config_dict['mysql']['confsec']
mysqlcnf.clear()
mysqlcnf_dict.clear()
for s in mysqlconf.sections():
mysqlconf.remove_section(s)
else:
exit('ERROR: You specified [mysql]password as False but did not provide a valid .my.cnf path!')
config_dict['gpg']['keys'] = config_dict['gpg']['keys'].split(',')
if len(config_dict['gpg']['keys']) >= 1:
config_dict['gpg']['keys'][:] = [re.sub('^\s*(0x)?([0-9A-F]*)\s*',
'\g<2>', x).upper() for x in config_dict['gpg']['keys']]
if config_dict['gpg']['enabled'] == True:
if config_dict['gpg']['homedir'] != '':
config_dict['gpg']['homedir'] = os.path.expanduser(config_dict['gpg']['homedir'])
config_dict['local']['path'] = os.path.expanduser(config_dict['local']['path'])
config_dict['local']['mediadir'] = os.path.expanduser(config_dict['local']['mediadir'])
os.makedirs(config_dict['local']['mediadir'], exist_ok = True)
if config_dict['tags']['year'] == 'False':
config_dict['tags']['year'] = config['tags'].getboolean('year')
config_dict['tags']['img'] = os.path.expanduser(config_dict['tags']['img'])
return(config_dict)

def confArgs(conf, args):
conf['episode'] = {}
conf['episode']['title'] = args.title
conf['episode']['file_title'] = re.sub('[^A-Za-z0-9-]', '.', conf['episode']['title']).lower()
conf['episode']['season'] = str(args.season).zfill(conf['tags']['season_pad'])
conf['episode']['serial'] = str(args.episode).zfill(conf['tags']['episode_pad'])
for i in ('season', 'episode'):
del conf['tags'][i + '_pad']
conf['episode']['id'] = 'S{0}E{1}'.format(str(conf['episode']['season']),
str(conf['episode']['serial']))
conf['episode']['pretty_title'] = '{0}: {1}'.format(conf['episode']['id'], conf['episode']['title'])
if conf['tags']['track'] == 'EPISODE':
conf['tags']['track'] = conf['episode']['serial']
conf['tags']['url'] = re.sub('^(.*)SEASONEPISODE(.*)$',
'\g<1>' + conf['episode']['id'] + '\g<2>',
conf['tags']['url'])
conf['tags']['url'] = re.sub('^(.*)SEASON(.*)$',
'\g<1>' + conf['episode']['season'] + '\g<2>',
conf['tags']['url'])
conf['tags']['url'] = re.sub('^(.*)EPISODE(.*)$',
'\g<1>' + conf['episode']['serial'] + '\g<2>',
conf['tags']['url'])
conf['local']['subdir'] = re.sub('^(.*)SEASONEPISODE(.*)$',
'\g<1>' + conf['episode']['id'] + '\g<2>',
conf['local']['subdir'])
conf['local']['subdir'] = re.sub('^(.*)SEASON(.*)$',
'\g<1>' + conf['episode']['season'] + '\g<2>',
conf['local']['subdir'])
conf['local']['subdir'] = re.sub('^(.*)EPISODE(.*)$',
'\g<1>' + conf['episode']['serial'] + '\g<2>',
conf['local']['subdir'])
conf['local']['path'] = '{0}/{1}'.format(conf['local']['path'],
conf['local']['subdir'].lower())
if not conf['tags']['year']:
conf['tags']['year'] = datetime.datetime.now().year
conf['tags']['year'] = str(conf['tags']['year'])
if not os.path.isdir(conf['local']['path']):
os.makedirs(conf['local']['path'], exist_ok = True)
del conf['local']['subdir']
conf['episode']['raw'] = args.flacfile
if args.flacfile:
newpath = os.path.abspath(os.path.expanduser(args.flacfile))
if os.path.isfile(newpath):
conf['episode']['raw'] = newpath
else:
exit('ERROR: The FLAC file you specified does not seem to exist({0}). Check your path.'.format(newpath))
else:
dflt_flac_names = ['{0}.edited.flac'.format(conf['episode']['id'].lower()),
'{0}.final.flac'.format(conf['episode']['id'].lower()),
'{0}.flac'.format(conf['episode']['id'].lower())]
for f in dflt_flac_names:
if os.path.isfile('{0}/{1}'.format(conf['local']['path'], f)):
conf['episode']['raw'] = '{0}/{1}'.format(conf['local']['path'], f)
break
if not conf['episode']['raw']:
exit('ERROR: We cannot seem to locate a FLAC to convert. Try using the -f/--file argument.')
magic_file = magic.open(magic.MAGIC_MIME)
magic_file.load()
if not magic_file.file(conf['episode']['raw']) == 'audio/x-flac; charset=binary':
exit('ERROR: Your FLAC file does not seem to actually be FLAC.')
conf['flac'] = {}
conf['flac']['samples'] = subprocess.check_output(['metaflac',
'--show-total-samples',
'{0}'.format(conf['episode']['raw'])]).decode('utf-8').strip()
conf['flac']['rate'] = subprocess.check_output(['metaflac',
'--show-sample-rate',
'{0}'.format(conf['episode']['raw'])]).decode('utf-8').strip()
conf['flac']['rate'] = '{0:.2f}'.format(float(conf['flac']['rate']))
rawfilepath = os.path.abspath(os.path.expanduser(args.raw_recording))
if not os.path.isfile(rawfilepath):
exit('ERROR: the raw recording evaluated to {0} but it does not seem to exist!'.format(rawfilepath))
conf['episode']['recorded'] = (str(datetime.datetime.utcfromtimestamp(os.path.getmtime(rawfilepath)))).split('.')[0]
conf['episode']['length'] = float(conf['flac']['samples'])/float(conf['flac']['rate'])
conf['episode']['length'] = str(int(conf['episode']['length']))
if args.now:
timestamp = datetime.datetime.timestamp(datetime.datetime.now())
else:
timestamp = os.path.getmtime(conf['episode']['raw'])
conf['episode']['sha'] = {}
conf['episode']['size'] = {}
conf['episode']['released'] = (str(datetime.datetime.utcnow())).split('.')[0]
conf['episode']['month'] = datetime.datetime.fromtimestamp(timestamp).strftime('%m')
conf['episode']['day'] = datetime.datetime.fromtimestamp(timestamp).strftime('%d')
conf['episode']['file_title'] = re.sub('\.+', '.', conf['episode']['file_title'])
conf['episode']['file_title'] = '{0}.{1}'.format(conf['episode']['id'].lower(),
re.sub('\.$',
'',
conf['episode']['file_title']))
del conf['flac']
if args.editor:
del conf['tags']['editor']
conf['episode']['editor'] = args.editor
else:
conf['episode']['editor'] = conf['tags']['editor']
if conf['tags']['album'] == 'SEASON':
conf['tags']['album'] = 'Season {0}'.format(conf['episode']['season'])
conf['local']['mediadir'] = '{0}/S{1}/E{2}'.format(conf['local']['mediadir'],
conf['episode']['season'],
conf['episode']['serial'])
os.makedirs(conf['local']['mediadir'], exist_ok = True)
cc_base_url = 'https://creativecommons.org/licenses'
conf['music'] = {}
conf['music']['intro'] = {}
conf['music']['intro']['artist'] = args.intro_artist
conf['music']['intro']['title'] = args.intro_title
conf['music']['intro']['copyright'] = args.intro_copyright
conf['music']['intro']['link'] = args.intro_link
if args.intro_copyrightlink:
conf['music']['intro']['copyrightlink'] = args.intro_copyrightlink
else:
strp_cr = (re.sub('CC-?', '', args.intro_copyright, flags = re.I)).split()
if len(strp_cr) != 2:
exit('ERROR: You did not specify a copyright link and this does not seem to be a CC license!')
conf['music']['intro']['copyrightlink'] = '{0}/{1}/{2}/'.format(
cc_base_url,
strp_cr[0].lower(),
strp_cr[1])
conf['music']['outro'] = {}
conf['music']['outro']['artist'] = args.outro_artist
conf['music']['outro']['title'] = args.outro_title
conf['music']['outro']['copyright'] = args.outro_copyright
conf['music']['outro']['link'] = args.outro_link
if args.intro_copyrightlink:
conf['music']['outro']['copyrightlink'] = args.outro_copyrightlink
else:
strp_cr = (re.sub('CC-?', '', args.outro_copyright, flags = re.I)).split()
conf['music']['outro']['copyrightlink'] = '{0}/{1}/{2}/'.format(
cc_base_url,
strp_cr[0].lower(),
strp_cr[1])
return(conf)

def transcodeMP3(conf):
mediatype = 'mp3'
mediadir = '{0}/{1}'.format(conf['local']['mediadir'], mediatype)
mediafile = '{0}/{1}.{2}'.format(mediadir,
conf['episode']['file_title'],
mediatype)
if os.path.isfile(mediafile):
os.remove(mediafile)
os.makedirs(mediadir, exist_ok = True)
print('{0}: Transcoding to {1}...'.format(datetime.datetime.now(), mediatype))
subprocess.call(['ffmpeg', '-stats', '-loglevel', '0', '-i',
conf['episode']['raw'], '-b:a', '128k', '-ac','1', '-joint_stereo', '1',
mediafile])
return(mediafile)

def transcodeOGG(conf):
mediatype = 'ogg'
mediadir = '{0}/{1}'.format(conf['local']['mediadir'], mediatype)
mediafile = '{0}/{1}.{2}'.format(mediadir,
conf['episode']['file_title'],
mediatype)
if os.path.isfile(mediafile):
os.remove(mediafile)
os.makedirs(mediadir, exist_ok = True)
print('{0}: Transcoding to {1}...'.format(datetime.datetime.now(), mediatype))
subprocess.call(['ffmpeg', '-stats', '-loglevel', '0', '-i',
conf['episode']['raw'], '-qscale:a', '8', '-ac','1', '-joint_stereo', '1',
mediafile])
return(mediafile)

def tagMP3(conf, mediafile):
# This appears to not work.
# http://id3.org/id3v2.3.0#Attached_picture
# http://id3.org/id3v2.4.0-frames (section 4.14)
# https://stackoverflow.com/questions/7275710/mutagen-how-to-detect-and-embed-album-art-in-mp3-flac-and-mp4
# https://stackoverflow.com/questions/409949/how-do-you-embed-album-art-into-an-mp3-using-python
magic_file = magic.open(magic.MAGIC_MIME)
magic_file.load()
imgmime = magic_file.file(conf['tags']['img']).split(';')[0]
with open(conf['tags']['img'], 'rb') as f:
img_data = f.read()
print('{0}: Now adding tags to {1}...'.format(datetime.datetime.now(), mediafile))
tag = ID3(mediafile)
tag.add(TALB(encoding = 0, text = [conf['tags']['album']]))
tag.add(APIC(encoding = 0, mime = imgmime, type = 3,
desc = conf['tags']['artist'], data = img_data))
tag.add(TDRC(encoding = 0, text = ['{0}.{1}.{2}'.format(conf['tags']['year'],
conf['episode']['month'],
conf['episode']['day'])]))
tag.add(TENC(encoding = 0, text = [conf['tags']['encoded']]))
tag.add(TRCK(encoding = 0, text = [conf['tags']['track']]))
tag.add(COMM(encoding = 0, lang = '\x00\x00\x00', desc = '',
text = [conf['tags']['comment']]))
tag.add(WXXX(encoding = 0, desc = '', url = conf['tags']['url']))
tag.add(TCON(encoding = 0, text = [conf['tags']['genre']]))
tag.add(TIT2(encoding = 0, text = [conf['episode']['pretty_title']]))
tag.add(TPE1(encoding = 0, text = [conf['tags']['artist']]))
tag.add(TCOP(encoding = 0, text = [conf['tags']['copyright']]))
tag.save()

def tagOGG(conf, mediafile):
# It seems we can't use this method.
# https://mutagen.readthedocs.io/en/latest/user/vcomment.html
# https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
# https://xiph.org/flac/format.html#metadata_block_picture
# https://github.com/quodlibet/mutagen/issues/200
magic_file = magic.open(magic.MAGIC_MIME)
magic_file.load()
imgmime = magic_file.file(conf['tags']['img']).split(';')[0]
with open(conf['tags']['img'], 'rb') as f:
img_b64 = base64.b64encode(f.read())
print('{0}: Now adding tags to {1}...'.format(datetime.datetime.now(), mediafile))
tag = OggVorbis(mediafile)
tag['TITLE'] = conf['episode']['pretty_title']
tag['ARTIST'] = conf['tags']['artist']
tag['ALBUM'] = conf['tags']['album']
tag['DATE'] = '{0}.{1}.{2}'.format(conf['tags']['year'],
conf['episode']['month'],
conf['episode']['day'])
tag['TRACKNUMBER'] = conf['tags']['track']
tag['GENRE'] = conf['tags']['genre']
tag['DESCRIPTION'] = conf['tags']['comment']
tag['COPYRIGHT'] = conf['tags']['copyright']
tag['CONTACT'] = conf['tags']['url']
tag['ENCODED-BY'] = conf['tags']['encoded']
tag['ENCODER'] = conf['tags']['encoded']
tag['METADATA_BLOCK_PICTURE'] = img_b64.decode('utf-8')
tag.save()

def getSHA256(mediafile):
print('{0}: Generating SHA256 for {1}...'.format(datetime.datetime.now(),
mediafile))
filehash = hashlib.sha256()
with open(mediafile, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
filehash.update(chunk)
return(filehash.hexdigest())

def getSize(mediafile):
filesize = os.path.getsize(mediafile)
return(filesize)

def dbEntry(conf):
print('{0}: Inserting into the {1}.{2}@{3} table...'.format(datetime.datetime.now(),
conf['mysql']['db'],
conf['mysql']['table'],
conf['mysql']['host']))
ssl = False
if 'ssl' in conf['mysql']:
ssl = conf['mysql']['ssl']
vals = "'{0}','{1}','{2}','{3}','{4}','{5}','{6}','{7}','{8}','{9}','{10}','{11}','{12}','{13}','{14}','{15}','{16}','{17}','{18}','{19}'".format(conf['episode']['id'],
conf['episode']['file_title'],
conf['episode']['sha']['mp3'],
conf['episode']['sha']['ogg'],
conf['episode']['size']['mp3'],
conf['episode']['size']['ogg'],
conf['episode']['length'],
re.sub("'", "\\'", conf['episode']['editor']),
re.sub("'", "\\'", conf['music']['intro']['title']),
re.sub("'", "\\'", conf['music']['intro']['artist']),
conf['music']['intro']['link'],
re.sub("'", "\\'", conf['music']['intro']['copyright']),
conf['music']['intro']['copyrightlink'],
re.sub("'", "\\'", conf['music']['outro']['title']),
re.sub("'", "\\'", conf['music']['outro']['artist']),
conf['music']['outro']['link'],
re.sub("'", "\\'", conf['music']['outro']['copyright']),
conf['music']['outro']['copyrightlink'],
conf['episode']['recorded'],
conf['episode']['released'])


conn = pymysql.connect(host = conf['mysql']['host'],
port = conf['mysql']['port'],
user = conf['mysql']['user'],
passwd = conf['mysql']['password'],
db = conf['mysql']['db'],
ssl = ssl,
autocommit = True)
cur = conn.cursor()
query = 'INSERT INTO {0} ({1}) VALUES ({2})'.format(conf['mysql']['table'],
conf['mysql']['cols'],
vals)
try:
cur.execute(query)
cur.close()
conn.close()
except:
print('{0}: There seems to have been some error when inserting into the DB. Check access (or it is a dupe).'.format(
datetime.datetime.now()))

def signEp(mediatype):
os.makedirs('{0}/gpg'.format(conf['local']['mediadir']), exist_ok = True)
sigfile = '{0}/gpg/{1}.{2}.asc'.format(conf['local']['mediadir'],
conf['episode']['file_title'],
mediatype)
os.environ['GNUPGHOME'] = conf['gpg']['homedir']
vrfykeys = []
sigs = {}
gpg = gpgme.Context()
gpg.armor = True
for k in conf['gpg']['keys']:
if gpg.get_key(k, True).can_sign:
# it seems pygpgme does not allow signing with subkeys. sad day. gpg.signkeys complains if you pass it Subkey objects.
#subkeys = []
#for i in gpg.get_key(k, True).subkeys:
# subkeys.append(i.fpr)
#indexnum = [x for x, s in enumerate(subkeys) if k in s][0]
#vrfykeys.append(gpg.get_key(k, True).subkeys[indexnum].fpr)
if gpg.get_key(k, True).subkeys[0].fpr not in vrfykeys:
vrfykeys.append(gpg.get_key(k, True).subkeys[0].fpr)
data_in = '{0}/{1}/{2}.{3}'.format(conf['local']['mediadir'],
mediatype,
conf['episode']['file_title'],
mediatype)
print('{0}: Checking for existing GPG signatures (and skipping if we signed)...'.format(datetime.datetime.now()))
if os.path.isfile(sigfile):
with open(sigfile, 'rb') as s:
with open(data_in, 'rb') as f:
for k in gpg.verify(s, f, None):
try:
sigs[gpg.get_key(k.fpr, True).subkeys[0].fpr] = True
except:
pass
for k in vrfykeys:
if k not in sigs:
sigkeys = []
if gpg.get_key(k, True).can_sign:
print('{0}: Signing with key {1}...'.format(datetime.datetime.now(),
k))
sigkeys.append(gpg.get_key(k, True))
gpg.signers = sigkeys
with open(sigfile, 'ab') as s:
with open(data_in, 'rb') as f:
gpg.sign(f, s, gpgme.SIG_MODE_DETACH)
return(sigfile)

def uploadFile():
print('{0}: Syncing files to server...'.format(datetime.datetime.now()))
subprocess.call(['rsync',
'-a',
'{0}'.format(conf['local']['mediadir']),
'{0}@{1}:{2}S{3}/.'.format(conf['rsync']['user'],
conf['rsync']['host'],
conf['rsync']['path'],
conf['episode']['season'])])

def argParse():
parser = argparse.ArgumentParser(
description = 'PodLoader - a script to assist in Textpattern-powered podcasts',
prog = 'podloader v1.0')
requiredArgs = parser.add_argument_group('REQUIRED arguments')
requiredArgs.add_argument('-t',
'--title',
dest = 'title',
required = True,
help = "The episode's title (as it will appear in meta information).")
requiredArgs.add_argument('-e',
'--episode',
dest = 'episode',
required = True,
type = int,
help = "The episode number for this episode.")
requiredArgs.add_argument('-s',
'--season',
dest = 'season',
required = True,
type = int,
help = "The season number this episode is in.")
requiredArgs.add_argument('-r',
'--raw-recording',
dest = 'raw_recording',
required = True,
help = "The path to a single-track *raw* recording. This file is used to get the timestamp of recording.")
requiredArgs.add_argument('-i:a',
'--intro-artist',
dest = 'intro_artist',
required = True,
help = "The artist for the intro music.")
requiredArgs.add_argument('-i:t',
'--intro-title',
dest = 'intro_title',
required = True,
help = "The title for the intro music.")
requiredArgs.add_argument('-i:l',
'--intro-link',
dest = 'intro_link',
required = True,
help = "The link to the intro track (i.e. page to more information about the track).")
requiredArgs.add_argument('-i:c',
'--intro-copyright',
dest = 'intro_copyright',
required = True,
help = "The copyright for the intro music. If it's a Creative Commons type, you do not need to include a copyright link. e.g. '-i:c \"CC-BY-SA 3.0\"'")
requiredArgs.add_argument('-o:a',
'--outro-artist',
dest = 'outro_artist',
required = True,
help = "The artist for the outro music.")
requiredArgs.add_argument('-o:t',
'--outro-title',
dest = 'outro_title',
required = True,
help = "The title for the outro music.")
requiredArgs.add_argument('-o:l',
'--outro-link',
dest = 'outro_link',
required = True,
help = "The link to the outro track (i.e. page to more information about the track).")
requiredArgs.add_argument('-o:c',
'--outro-copyright',
dest = 'outro_copyright',
required = True,
help = "The copyright for the outro music. If it's a Creative Commons type, you do not need to include a copyright link. e.g. '-i:c \"CC-BY-SA 3.0\"'")
parser.add_argument('-i:cl',
'--intro-copyrightlink',
dest = 'intro_copyrightlink',
default = False,
help = "The link to the copyright terms for the intro. Optional if it's a CC license.")
parser.add_argument('-o:cl',
'--outro-copyrightlink',
default = False,
dest = 'outro_copyrightlink',
help = "The link to the copyright terms for the outro. Optional if it's a CC license.")
parser.add_argument('-d',
'--editor',
dest = 'editor',
help = 'The audio editor for the episode. Can (should) contain HTML link to editor (e.g. \'<a href="https://editorname.tld">Editor Name</a>\'')
parser.add_argument('-f',
'--file',
dest = 'flacfile',
default = False,
help = "The (final edit) FLAC file to be used for the episode. If not specified, we'll try to guess.")
parser.add_argument('-n',
'--now',
dest = 'now',
default = False,
action = 'store_true',
help = "Instead of getting the date based on the time of the file, use today's date (for media tags).")
try:
args = parser.parse_args()
print('{0}: Starting.'.format(datetime.datetime.now()))
except (NameError, TypeError):
parser.print_help()
exit(1)
return(args)

if __name__ == "__main__":
conf = confArgs(configParse(), argParse())
mp3 = transcodeMP3(conf)
tagMP3(conf, mp3)
ogg = transcodeOGG(conf)
tagOGG(conf, ogg)
conf['episode']['sha']['mp3'] = getSHA256(mp3)
conf['episode']['sha']['ogg'] = getSHA256(ogg)
conf['episode']['size']['mp3'] = getSize(mp3)
conf['episode']['size']['ogg'] = getSize(ogg)
dbEntry(conf)
signEp('mp3')
signEp('ogg')
uploadFile()
print('{0}: Finished.'.format(datetime.datetime.now()))