import configparser from requests import get from feedgen.feed import FeedGenerator import datetime import os import re import json import sys # osSafeName() convertes a string to an OS safe name. def osSafeName(name): return re.sub(r'[^a-zA-Z0-9 -]', '', name).lower().replace(' ', '-').replace('--', '-') class AudibleBook(object): def __init__(self, filePath, activationBytes, targetDir): self.filePath = filePath self.activationBytes = activationBytes self.targetDir = targetDir self.convertedFilePath='' self.metadata = json.loads(os.popen('ffprobe -i {} -show_format \ -print_format json'.format(filePath)).read())['format']['tags'] self.chapters = json.loads(os.popen('ffprobe -i {} -print_format json \ -show_chapters -loglevel error -sexagesimal'.format(self.filePath)) .read())['chapters'] # getPath() returns path to origonal audible file. def getPath(self): return self.filePath # getMetadata() returns audio metadata for a given filepath. def getMetadata(self): return self.metadata # getChapters() returns chapter metadata for a given filepath. def getChapters(self): return self.chapters # getTitle() returns audiobook title. def getTitle(self): return self.getMetadata()['title'] def getAuthor(self): return self.getMetadata()['artist'] def setTargetDir(self, targetDir): self.targetDir = targetDir def removeDRM(self, fileName): filePath = '{}/{}.m4b'.format(self.targetDir, fileName) if not os.path.exists(filePath): os.system('ffmpeg -activation_bytes {} -i {} -c copy "{}"' .format(self.activationBytes, self.filePath, filePath)) self.convertedFilePath = filePath def splitChapters(self): for c in self.getChapters(): start=c['start_time'] end=c['end_time'] title=c['tags']['title'] outFilePath = '{}-{}.m4a'.format(os.path.splitext(self.convertedFilePath)[0], osSafeName(title)) if not os.path.exists(outFilePath): os.system('ffmpeg -i {} -acodec copy -vcodec copy -ss {} -t {} {}'. format(self.convertedFilePath, start, end, outFilePath)) def getCover(self): outFilePath = '{}/cover.jpg'.format(self.targetDir) if not os.path.exists(outFilePath): os.system('ffmpeg -i {} -an -vcodec copy {}'. format(self.convertedFilePath, outFilePath)) def deleteConverted(self): if self.convertedFilePath is not '' and os.path.exists(self.configFilePath): os.remove(self.convertedFilePath) # getHash() returns the hash of a given file. def getHash(filePath): with open(filePath, 'rb') as f: f.seek(653) data = f.read(20) f.close() return data.hex() # getActivationBytes returns the bytes needed to decrypt a given hash. def getActivationBytes(filehash): headers = {'User-Agent': 'audible-converter'} response = get('https://aax.api.j-kit.me/api/v2/activation/{}'.format(filehash), headers=headers) return json.loads(response.text)['activationBytes'] # createConfig() creates a config file. def createConfig(configFilePath, filePath): activationBytes = getActivationBytes(getHash(filePath)) f = open(configFilePath, 'w') f.write('activationBytes={}'.format(activationBytes)) f.close() configFilePath='./settings.conf' audibleBookPath=sys.argv[1] serverURL=sys.argv[2] targetDir='./audiobooks' # Check if config file exists and creates one if needed. if not os.path.exists(configFilePath): print('Creating config file') createConfig(configFilePath, audibleBookPath) configString = '[Settings]\n' + open(configFilePath).read() configParser = configparser.RawConfigParser() configParser.read_string(configString) activationBytes=configParser.get('Settings', 'activationBytes') book = AudibleBook(audibleBookPath, activationBytes, targetDir) name = osSafeName(book.getTitle()) outputDir = '{}/{}'.format(targetDir, name) if not os.path.exists(outputDir): os.makedirs(outputDir) book.setTargetDir(outputDir) rssFilePath='{}/rss.xml'.format(outputDir) if not os.path.exists(rssFilePath): book.removeDRM(name) book.splitChapters() book.getCover() fg = FeedGenerator() fg.id(name) fg.title(book.getTitle()) fg.author( {'name': book.getAuthor()} ) fg.link( href='https://audible.com', rel='alternate' ) fg.logo('{}/{}/cover.jpg'.format(serverURL, name)) fg.subtitle(book.getMetadata()['copyright']) fg.language('en') count = 0 for c in book.getChapters(): key = osSafeName(c['tags']['title']) fe = fg.add_entry() fe.id(key) fe.title(c['tags']['title']) fe.link(href='{0}/{1}/{1}-{2}.m4a'.format(serverURL, name, key)) fe.enclosure('{0}/{1}/{1}-{2}.m4a'.format(serverURL, name, key), 0, 'audio') fe.published(datetime.datetime(int(book.getMetadata()['date']), 6, 1, 0, count, tzinfo=datetime.timezone.utc)) count += 1 rssfeed = fg.rss_str(pretty=True) fg.rss_file(rssFilePath) book.deleteConverted()