├── requirements.txt ├── .gitignore ├── logging.ini ├── serve.py ├── README.md ├── gen_feed.py └── parse_dir.py /requirements.txt: -------------------------------------------------------------------------------- 1 | feedgen 2 | requests 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | # Project-specific 10 | gen/ 11 | audiobooks/ 12 | -------------------------------------------------------------------------------- /logging.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=DEBUG 17 | formatter=simpleFormatter 18 | args=(sys.stdout,) 19 | 20 | [formatter_simpleFormatter] 21 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 22 | datefmt= -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | from http.server import HTTPServer,ThreadingHTTPServer, SimpleHTTPRequestHandler 2 | from gen_feed import gen_feed 3 | import logging.config , os 4 | 5 | 6 | LOG_CONF='logging.ini' 7 | if os.path.exists(LOG_CONF): logging.config.fileConfig(LOG_CONF) 8 | 9 | 10 | PORT = int(os.getenv('PORT')) or 8000 11 | HOST = os.getenv('HOST') or 'localhost' 12 | ENDPOINT = os.getenv('ENDPOINT') or 'http://%s:%s' %(HOST, PORT) 13 | 14 | gen_feed(ENDPOINT) 15 | 16 | 17 | # Creates a really basic server the serves this entire directory 18 | def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler): 19 | server_address = ('', PORT) 20 | httpd = server_class(server_address, handler_class) 21 | print('Starting server at ' + ENDPOINT) 22 | httpd.serve_forever() 23 | 24 | 25 | run() 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # audiobook-podcaster 2 | 3 | I've found myself in the situation where I have an audiobook as a folder on my PC, but want to listen to it on the go. 4 | 5 | This is a audiobook server, using podcast technology. You can listen to the audiobooks on your phone/whatever using any podcast app. No need to download glitchy apps! 6 | 7 | ## Setup 8 | 9 | Clone the repository. 10 | 11 | ``` 12 | git clone https://github.com/SirSpoony/audiobook-podcaster.git 13 | ``` 14 | 15 | To make sure it works, add an audiobook to `./audiobooks/` (just put the folder in there, for example `./audiobooks/book/chapter01.mp3` is a file that would be found) 16 | 17 | Start the server using `python serve.py`. Using a browser, navigate to `localhost:8000/gen` and you will see a list of available podcast xml files. 18 | 19 | If this all works, you can edit `serve.py` and change `PORT` and `HOST` to what you want. 20 | 21 | ### Dependencies 22 | - feedgen >0.6.1 23 | -------------------------------------------------------------------------------- /gen_feed.py: -------------------------------------------------------------------------------- 1 | import os 2 | from feedgen.feed import FeedGenerator 3 | import parse_dir 4 | from requests.utils import requote_uri 5 | import logging 6 | 7 | # Generates the .xml files in the ./gen directory 8 | def gen_feed(endpoint): 9 | # Make sure we have somewhere to save the files 10 | if not os.path.isdir('./gen'): 11 | print('There is no gen directory. Create ./gen') 12 | 13 | # Uses parse_dir.py to get the books and files 14 | books = parse_dir.getbooks_r('./audiobooks') 15 | 16 | for (book, files) in books: 17 | # Creates a new feed for each book 18 | fg = FeedGenerator() 19 | fg.load_extension('podcast') 20 | 21 | fg.podcast.itunes_category('Audiobook') 22 | 23 | for (file_name, file_path) in files: 24 | # the 1: removes the period because the base dir is ./audiobooks 25 | url = endpoint + file_path[1:] 26 | 27 | fe = fg.add_entry() 28 | fe.id(url) 29 | fe.title(file_name) 30 | fe.description('Segment of the book') 31 | fe.enclosure(requote_uri(url), str(os.path.getsize(file_path)), 'audio/mpeg') 32 | 33 | fg.title('Audiobook: ' + book) 34 | fg.link(href=endpoint, rel='self') 35 | fg.description('A book') 36 | fg.rss_str(pretty=True) 37 | 38 | # Saves the file 39 | rss_file_path = os.path.join('./gen/', book + '.xml') 40 | ensure_dir(rss_file_path) 41 | logging.info("generate feed: %s" % rss_file_path) 42 | fg.rss_file(rss_file_path) 43 | 44 | 45 | def ensure_dir(file_path): 46 | dir_path = os.path.dirname(file_path) 47 | if not os.path.exists(dir_path): 48 | os.makedirs(dir_path) 49 | return dir_path 50 | 51 | 52 | -------------------------------------------------------------------------------- /parse_dir.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # Returns a list of the files in a path 5 | def listfiles(path): 6 | return [ 7 | f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) 8 | ] 9 | 10 | 11 | # Returns a list of the directories in a path 12 | def listdirs(path): 13 | return [ 14 | d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d)) 15 | ] 16 | 17 | 18 | 19 | 20 | def acceptable_files(top , files): 21 | acceptable_extensions = { 22 | '.mp3', '.wav', '.aiff', '.m4a', '.m4b', '.m4p' 23 | } 24 | files = [(os.path.splitext(f)[0], os.path.join(top, f)) 25 | for f in files 26 | if (os.path.splitext(f)[1].lower() in acceptable_extensions)] 27 | return files 28 | 29 | def getbooks(root): 30 | ''' Returns a list of tuples (file_name, file_path). File name doesn't have an extension''' 31 | if not os.path.isdir(root): 32 | print('There is no audiobooks directory. Create ./audiobooks') 33 | 34 | books = [] 35 | for book_dir in listdirs(root): 36 | book_dir_path = os.path.join(root, book_dir) 37 | 38 | # List the files in the directory 39 | files_in_dir = listfiles(book_dir_path) 40 | # Sorts the files alphabetically 41 | files_in_dir.sort() 42 | # List of tuples (file_name, file_path), also making sure they are one of the accepted audio file formats 43 | files = acceptable_files(book_dir_path, files_in_dir) 44 | books.append((book_dir, files)) 45 | return books 46 | 47 | 48 | def getbooks_r(root): 49 | ''' get books tupple (file_name, file_path) recursively, dive into subdirectories''' 50 | if not os.path.isdir(root): 51 | print('There is no %s directory. Create %s' %(root, root)) 52 | 53 | books = [] 54 | for top, dirs, files in os.walk(root, followlinks=True): 55 | files.sort() 56 | # create records if any suitable files exists 57 | files = acceptable_files(top, files) 58 | if files: 59 | books.append((top, files)) 60 | 61 | return books 62 | 63 | 64 | # For debug purposes 65 | if __name__ == "__main__": 66 | root = './audiobooks' 67 | books = getbooks(root) 68 | print(books) 69 | --------------------------------------------------------------------------------