From 9cc311ca5376bfcbcacf7ac492f5958acfac0682 Mon Sep 17 00:00:00 2001 From: Tom van der Lee Date: Mon, 19 Oct 2015 21:38:00 +0200 Subject: Added commandline options and config file --- .gitignore | 1 - README.rst | 1 + docs/index.rst | 27 +---------- setup.cfg | 50 +++++-------------- youtube-podcaster.json.example | 21 ++++++++ youtube_podcaster/__init__.py | 44 ++++++++++++----- youtube_podcaster/__main__.py | 8 ---- youtube_podcaster/config.py | 64 +++++++++++++++++++++++++ youtube_podcaster/podcastfeeder.py | 4 +- youtube_podcaster/podcastupdater.py | 34 +++++++++---- youtube_podcaster/youtube-podcaster.json.sample | 10 ---- youtube_podcaster/youtube/downloader.py | 23 +++++---- 12 files changed, 171 insertions(+), 116 deletions(-) create mode 100644 youtube-podcaster.json.example delete mode 100644 youtube_podcaster/__main__.py create mode 100644 youtube_podcaster/config.py delete mode 100644 youtube_podcaster/youtube-podcaster.json.sample diff --git a/.gitignore b/.gitignore index 55cf740..6296ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,3 @@ MANIFEST *.xml *.save downloads/ -youtube-podcaster.json diff --git a/README.rst b/README.rst index b485fbb..9b66292 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,4 @@ +================= YouTube Podcaster ================= diff --git a/docs/index.rst b/docs/index.rst index 62a305d..73e63f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,29 +1,4 @@ -================= -youtube-podcaster -================= - -This is the documentation of **youtube-podcaster**. - -.. note:: - - This is the main page of your project's `Sphinx `_ - documentation. It is formatted in `reStructuredText - `__. Add additional pages by creating - rst-files in ``docs`` and adding them to the `toctree - `_ below. Use then - `references `__ in order to link - them from this page, e.g. :ref:`authors ` and :ref:`changes`. - It is also possible to refer to the documentation of other Python packages - with the `Python domain syntax - `__. By default you - can reference the documentation of `Sphinx `__, - `Python `__, `matplotlib - `__, `NumPy - `__, `Scikit-Learn - `__, `Pandas - `__, `SciPy - `__. You can add more by - extending the ``intersphinx_mapping`` in your Sphinx's ``conf.py``. +.. include:: ../README.rst Contents ======== diff --git a/setup.cfg b/setup.cfg index f1b8f8c..e559f20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,53 +6,35 @@ author-email = t0m.vd.l33@gmail.com license = MIT License home-page = http://github.com/tomvanderlee/youtube-podcaster description-file = README.rst -# Add here all kinds of additional classifiers as defined under -# https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers = Development Status :: 3 - Alpha, - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 :: Only + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 :: Only [entry_points] console_scripts = youtube-podcaster = youtube_podcaster:main -# Add here console scripts like: -# console_scripts = -# hello_world = youtube_podcaster.module:function -# as well as other entry_points. - [files] -# Add here 'data_files', 'packages' or 'namespace_packages'. -# Additional data files are defined as key value pairs of source and target: -packages = - youtube_podcaster -# data_files = -# share/youtube_podcaster_docs = docs/* +packages = + youtube_podcaster +data_files = + share/man/man1/ = docs/_build/man/* + share/youtube-podcaster/ = youtube-podcaster.json.example [extras] -# Add here additional requirements for extra features, like: -# PDF = -# ReportLab>=1.2 -# RXP [test] -# py.test options when running `python setup.py test` addopts = tests [pytest] -# Options for py.test: -# Specify command line options as you would do when invoking py.test directly. -# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml -# in order to write a coverage file that can be read by Jenkins. -addopts = - --cov youtube_podcaster --cov-report term-missing - --verbose +addopts = + --cov youtube_podcaster --cov-report term-missing + --verbose [aliases] docs = build_sphinx [bdist_wheel] -# Use this option if your package is pure-python universal = 1 [build_sphinx] @@ -60,17 +42,11 @@ source_dir = docs build_dir = docs/_build [pbr] -# Let pbr run sphinx-apidoc autodoc_tree_index_modules = True -# autodoc_tree_excludes = ... -# Let pbr itself generate the apidoc -# autodoc_index_modules = True -# autodoc_exclude_modules = ... -# Convert warnings to errors -# warnerrors = True [devpi:upload] -# Options for the devpi: PyPI serer and packaging tool -# VCS export must be deactivated since we are using setuptools-scm no-vcs = 1 format = bdist_wheel + +[easy_install] + diff --git a/youtube-podcaster.json.example b/youtube-podcaster.json.example new file mode 100644 index 0000000..d9b48af --- /dev/null +++ b/youtube-podcaster.json.example @@ -0,0 +1,21 @@ +{ + "server" : { + "interface": "0.0.0.0", + "port": 8888 + }, + + "youtube": { + "api-key": "" + }, + + "podcasts": [{ + "username": "", + "playlists": [""] + }], + + "downloads": { + "format": "vorbis", + "path": "/tmp/downloads/", + "url": "localhost" + } +} diff --git a/youtube_podcaster/__init__.py b/youtube_podcaster/__init__.py index 32aee7c..77ab9cc 100644 --- a/youtube_podcaster/__init__.py +++ b/youtube_podcaster/__init__.py @@ -1,27 +1,47 @@ #!/usr/bin/env python3 -import json +import argparse -from http.server import ( - HTTPServer, -) +from http.server import HTTPServer -from . import ( - youtube, +from .podcastfeeder import create_feeder +from .config import ( + Config, + ConfigException ) -from .podcastfeeder import ( - create_feeder -) +""" +Start the program +""" def main(): - config = json.load(open("youtube-podcaster.json")) + arg_parser = argparse.ArgumentParser(prog="youtube-podcaster", + description="Converts youtube \ + playlists to RSS-feeds") + arg_parser.add_argument("-c", "--config", + dest="config", + help="Use CONFIG as the config file") + arg_parser.add_argument("-i", "--interface", + dest="interface", + help="The interface the http server will listen on") + arg_parser.add_argument("-p", "--port", + dest="port", + help="The port the http server will listen on") + arg_parser.add_argument("--api-key", + dest="apikey", + help="The YouTube API v3 key") + args = arg_parser.parse_args() try: - PodcastFeeder = create_feeder(config["youtube"], config["podcasts"]) - server = HTTPServer(("", 8888), PodcastFeeder) + config = Config.parse_config(args) + + PodcastFeeder = create_feeder(config) + + server = HTTPServer(config.get_server_address(), PodcastFeeder) server.serve_forever() + except ConfigException as e: + print(e) except KeyboardInterrupt: server.socket.close() diff --git a/youtube_podcaster/__main__.py b/youtube_podcaster/__main__.py deleted file mode 100644 index 941a79a..0000000 --- a/youtube_podcaster/__main__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 - -import youtube_podcaster - -if __name__ == "__main__": - youtube_podcaster.main() - -# vim: set ts=8 sw=4 tw=0 et : diff --git a/youtube_podcaster/config.py b/youtube_podcaster/config.py new file mode 100644 index 0000000..bfce902 --- /dev/null +++ b/youtube_podcaster/config.py @@ -0,0 +1,64 @@ +import sys +import os +import json + + +class ConfigException(Exception): + def __init__(self, msg): + super(ConfigException, self).__init__("Config exception: %s" % (msg)) + + +class Config: + instance = None + + def parse_config(config_file=None): + if not Config.instance: + Config.instance = Config(config_file) + + return Config.instance + + def __init__(self, args): + if args.config: + config = args.config + else: + config_file = "youtube-podcaster.json" + + if sys.platform == "linux" and not hasattr(sys, "real_prefix"): + config = "/etc/%s" % (config_file) + else: + config = "%s/etc/%s" % (sys.prefix, config_file) + + if not os.path.isfile(config): + raise ConfigException("%s not found" % (config)) + + try: + config = json.load(open(config)) + except json.decoder.JSONDecodeError as e: + raise ConfigException("%s is not valid json: %s" % ( + config, str(e))) + + try: + self.server = config["server"] + self.youtube = config["youtube"] + self.podcasts = config["podcasts"] + except KeyError as e: + raise ConfigException("Missing %s-section in %s" % ( + str(e), config)) + + for arg, value in vars(args).items(): + if not value: + continue + + if arg == "interface": + self.server["interface"] = value + elif arg == "port": + self.server["port"] = value + elif arg == "apikey": + self.youtube["api-key"] = value + + def get_server_address(self): + interface = str(self.server["interface"]) + port = int(self.server["port"]) + return interface, port + +# vim: set ts=8 sw=4 tw=0 et : diff --git a/youtube_podcaster/podcastfeeder.py b/youtube_podcaster/podcastfeeder.py index 3f752d2..4181d6c 100644 --- a/youtube_podcaster/podcastfeeder.py +++ b/youtube_podcaster/podcastfeeder.py @@ -5,10 +5,10 @@ from .podcastupdater import ( ) -def create_feeder(youtube_config, podcast_config): +def create_feeder(config): class PodcastFeeder(BaseHTTPRequestHandler): def __init__(self, request, client_address, server): - self.updater = PodcastUpdater(youtube_config, podcast_config) + self.updater = PodcastUpdater(config) super(PodcastFeeder, self).__init__(request, client_address, server) def do_GET(self): diff --git a/youtube_podcaster/podcastupdater.py b/youtube_podcaster/podcastupdater.py index 4a0a017..75b8b10 100644 --- a/youtube_podcaster/podcastupdater.py +++ b/youtube_podcaster/podcastupdater.py @@ -2,6 +2,7 @@ import pickle import os import time import hashlib +import sys from feedgen.feed import FeedGenerator @@ -11,11 +12,19 @@ from . import ( class PodcastUpdater: - def __init__(self, youtube_config, podcast_config): - self.podcasts = podcast_config - self.youtube = youtube.Youtube(youtube_config["api-key"]) + def __init__(self, config): + self.podcasts = config.podcasts + self.youtube = youtube.Youtube(config.youtube["api-key"]) + + if sys.platform == "linux" and not hasattr(sys, "real_prefix"): + self.data_dir = "/var/lib/youtube-podcaster" + else: + self.data_dir = "%s/var/lib/youtube-podcaster" % (sys.prefix) + + os.makedirs(self.data_dir, 0o755, True) + + self.feeds_file = "%s/feeds.dump" % (self.data_dir) - self.feeds_file = "feeds.save" if os.path.isfile(self.feeds_file): with open(self.feeds_file, "rb") as feeds: self.feeds = pickle.load(feeds) @@ -42,6 +51,8 @@ class PodcastUpdater: def update_podcast(self, channel, playlist): feed_id = hashlib.sha1(bytes("%s %s" % (channel, playlist), "UTF-8")).hexdigest() + feed_file = "%s/%s.xml" % (self.data_dir, feed_id) + yt_channel = self.youtube.get_channel(channel)[0] yt_playlists = self.youtube.get_playlists(yt_channel, 50) @@ -58,17 +69,16 @@ class PodcastUpdater: if feed.last_updated < time.time() - 600: self.populate_feed(feed, feed_id, yt_playlist) + feed.rss_file(feed_file) - feed_file = "%s.xml" % (feed_id) - self.feeds[feed_id].rss_file(feed_file) - - with open(self.feeds_file, "wb") as feed: - pickle.dump(self.feeds, feed) + with open(self.feeds_file, "wb") as feeds: + pickle.dump(self.feeds, feeds) - return "%s.xml" % (feed_id) + return feed_file def add_feed(self, feed_id, yt_playlist): feed = FeedGenerator() + feed.load_extension("podcast") feed.id(feed_id) feed.title(yt_playlist["snippet"]["title"]) @@ -78,7 +88,9 @@ class PodcastUpdater: feed.link(href="https://www.youtube.com/playlist?list=%s" % (yt_playlist["id"])) feed.rss_str(pretty=True) feed.last_updated = 0 + self.feeds[feed_id] = feed + return feed def populate_feed(self, feed, feed_id, yt_playlist, max_results=5): @@ -93,7 +105,9 @@ class PodcastUpdater: break else: url, size, mime = downloader.download(video, video_id, feed_id) + feed_entry = feed.add_entry() + feed_entry.id(video_id) feed_entry.guid(video_id) feed_entry.title(video["snippet"]["title"]) diff --git a/youtube_podcaster/youtube-podcaster.json.sample b/youtube_podcaster/youtube-podcaster.json.sample deleted file mode 100644 index d9d078f..0000000 --- a/youtube_podcaster/youtube-podcaster.json.sample +++ /dev/null @@ -1,10 +0,0 @@ -{ - "youtube": { - "api-key": "youtube-api-v3-key-here" - }, - - "podcasts": [{ - "username": "youtube-username-here", - "playlists": ["playlist-name-here"] - }] -} diff --git a/youtube_podcaster/youtube/downloader.py b/youtube_podcaster/youtube/downloader.py index ca1327b..529902a 100644 --- a/youtube_podcaster/youtube/downloader.py +++ b/youtube_podcaster/youtube/downloader.py @@ -1,41 +1,44 @@ #!/usr/bin/env python3 -import youtube_dl import os +import mimetypes + +import youtube_dl class Downloader: instance = None def get_instance(file_format, location, base_url): - if Downloader.instance: - return Downloader.instance - else: + if not Downloader.instance: Downloader.instance = Downloader(file_format, location, base_url) - return Downloader.instance + return Downloader.instance def __init__(self, file_format, location, base_url): self.file_format = file_format self.location = location self.base_url = base_url + if file_format == "vorbis": + self.extension = "ogg" + def download(self, video, video_id, feed_id): - output = "%s/%s/%s.ogg" % (self.location, feed_id, video_id) + output = "%s/%s/%s.%s" % (self.location, feed_id, video_id, self.extension) options = {"format": "bestaudio/best", "outtmpl": output, "postprocessors": [{ "key": "FFmpegExtractAudio", "preferredcodec": self.file_format }], - "nooverwrites": True - } + "nooverwrites": True} + video_url = "https://www.youtube.com/watch?v=%s" % (video["snippet"]["resourceId"]["videoId"]) youtube_dl.YoutubeDL(options).download([video_url]) - url = "%s/%s/%s.ogg" % (self.base_url, feed_id, video_id) + url = "%s/%s/%s.%s" % (self.base_url, feed_id, video_id, self.extension) size = str(os.path.getsize(output)) - mime = "audio/ogg" + mime = mimetypes.guess_type(output)[0] return (url, size, mime) -- cgit v1.2.3