diff options
| author | 2015-10-19 21:38:00 +0200 | |
|---|---|---|
| committer | 2015-10-19 21:38:00 +0200 | |
| commit | 9cc311ca5376bfcbcacf7ac492f5958acfac0682 (patch) | |
| tree | 451b076d21bbaa477c901b83d325d7360be2cd67 /youtube_podcaster | |
| parent | a7fe15b99d64904855ba2bd3649b1a3642824a75 (diff) | |
| download | youtube-podcaster-9cc311ca5376bfcbcacf7ac492f5958acfac0682.tar.gz youtube-podcaster-9cc311ca5376bfcbcacf7ac492f5958acfac0682.tar.bz2 youtube-podcaster-9cc311ca5376bfcbcacf7ac492f5958acfac0682.zip | |
Added commandline options and config file
Diffstat (limited to 'youtube_podcaster')
| -rw-r--r-- | youtube_podcaster/__init__.py | 44 | ||||
| -rw-r--r-- | youtube_podcaster/__main__.py | 8 | ||||
| -rw-r--r-- | youtube_podcaster/config.py | 64 | ||||
| -rw-r--r-- | youtube_podcaster/podcastfeeder.py | 4 | ||||
| -rw-r--r-- | youtube_podcaster/podcastupdater.py | 34 | ||||
| -rw-r--r-- | youtube_podcaster/youtube-podcaster.json.sample | 10 | ||||
| -rw-r--r-- | youtube_podcaster/youtube/downloader.py | 23 |
7 files changed, 135 insertions, 52 deletions
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 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | import json | 3 | import argparse |
| 4 | 4 | ||
| 5 | from http.server import ( | 5 | from http.server import HTTPServer |
| 6 | HTTPServer, | ||
| 7 | ) | ||
| 8 | 6 | ||
| 9 | from . import ( | 7 | from .podcastfeeder import create_feeder |
| 10 | youtube, | 8 | from .config import ( |
| 9 | Config, | ||
| 10 | ConfigException | ||
| 11 | ) | 11 | ) |
| 12 | 12 | ||
| 13 | from .podcastfeeder import ( | 13 | """ |
| 14 | create_feeder | 14 | Start the program |
| 15 | ) | 15 | """ |
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | def main(): | 18 | def main(): |
| 19 | config = json.load(open("youtube-podcaster.json")) | 19 | arg_parser = argparse.ArgumentParser(prog="youtube-podcaster", |
| 20 | description="Converts youtube \ | ||
| 21 | playlists to RSS-feeds") | ||
| 22 | arg_parser.add_argument("-c", "--config", | ||
| 23 | dest="config", | ||
| 24 | help="Use CONFIG as the config file") | ||
| 25 | arg_parser.add_argument("-i", "--interface", | ||
| 26 | dest="interface", | ||
| 27 | help="The interface the http server will listen on") | ||
| 28 | arg_parser.add_argument("-p", "--port", | ||
| 29 | dest="port", | ||
| 30 | help="The port the http server will listen on") | ||
| 31 | arg_parser.add_argument("--api-key", | ||
| 32 | dest="apikey", | ||
| 33 | help="The YouTube API v3 key") | ||
| 34 | args = arg_parser.parse_args() | ||
| 20 | 35 | ||
| 21 | try: | 36 | try: |
| 22 | PodcastFeeder = create_feeder(config["youtube"], config["podcasts"]) | 37 | config = Config.parse_config(args) |
| 23 | server = HTTPServer(("", 8888), PodcastFeeder) | 38 | |
| 39 | PodcastFeeder = create_feeder(config) | ||
| 40 | |||
| 41 | server = HTTPServer(config.get_server_address(), PodcastFeeder) | ||
| 24 | server.serve_forever() | 42 | server.serve_forever() |
| 43 | except ConfigException as e: | ||
| 44 | print(e) | ||
| 25 | except KeyboardInterrupt: | 45 | except KeyboardInterrupt: |
| 26 | server.socket.close() | 46 | server.socket.close() |
| 27 | 47 | ||
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 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | |||
| 3 | import youtube_podcaster | ||
| 4 | |||
| 5 | if __name__ == "__main__": | ||
| 6 | youtube_podcaster.main() | ||
| 7 | |||
| 8 | # 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 @@ | |||
| 1 | import sys | ||
| 2 | import os | ||
| 3 | import json | ||
| 4 | |||
| 5 | |||
| 6 | class ConfigException(Exception): | ||
| 7 | def __init__(self, msg): | ||
| 8 | super(ConfigException, self).__init__("Config exception: %s" % (msg)) | ||
| 9 | |||
| 10 | |||
| 11 | class Config: | ||
| 12 | instance = None | ||
| 13 | |||
| 14 | def parse_config(config_file=None): | ||
| 15 | if not Config.instance: | ||
| 16 | Config.instance = Config(config_file) | ||
| 17 | |||
| 18 | return Config.instance | ||
| 19 | |||
| 20 | def __init__(self, args): | ||
| 21 | if args.config: | ||
| 22 | config = args.config | ||
| 23 | else: | ||
| 24 | config_file = "youtube-podcaster.json" | ||
| 25 | |||
| 26 | if sys.platform == "linux" and not hasattr(sys, "real_prefix"): | ||
| 27 | config = "/etc/%s" % (config_file) | ||
| 28 | else: | ||
| 29 | config = "%s/etc/%s" % (sys.prefix, config_file) | ||
| 30 | |||
| 31 | if not os.path.isfile(config): | ||
| 32 | raise ConfigException("%s not found" % (config)) | ||
| 33 | |||
| 34 | try: | ||
| 35 | config = json.load(open(config)) | ||
| 36 | except json.decoder.JSONDecodeError as e: | ||
| 37 | raise ConfigException("%s is not valid json: %s" % ( | ||
| 38 | config, str(e))) | ||
| 39 | |||
| 40 | try: | ||
| 41 | self.server = config["server"] | ||
| 42 | self.youtube = config["youtube"] | ||
| 43 | self.podcasts = config["podcasts"] | ||
| 44 | except KeyError as e: | ||
| 45 | raise ConfigException("Missing %s-section in %s" % ( | ||
| 46 | str(e), config)) | ||
| 47 | |||
| 48 | for arg, value in vars(args).items(): | ||
| 49 | if not value: | ||
| 50 | continue | ||
| 51 | |||
| 52 | if arg == "interface": | ||
| 53 | self.server["interface"] = value | ||
| 54 | elif arg == "port": | ||
| 55 | self.server["port"] = value | ||
| 56 | elif arg == "apikey": | ||
| 57 | self.youtube["api-key"] = value | ||
| 58 | |||
| 59 | def get_server_address(self): | ||
| 60 | interface = str(self.server["interface"]) | ||
| 61 | port = int(self.server["port"]) | ||
| 62 | return interface, port | ||
| 63 | |||
| 64 | # 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 ( | |||
| 5 | ) | 5 | ) |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | def create_feeder(youtube_config, podcast_config): | 8 | def create_feeder(config): |
| 9 | class PodcastFeeder(BaseHTTPRequestHandler): | 9 | class PodcastFeeder(BaseHTTPRequestHandler): |
| 10 | def __init__(self, request, client_address, server): | 10 | def __init__(self, request, client_address, server): |
| 11 | self.updater = PodcastUpdater(youtube_config, podcast_config) | 11 | self.updater = PodcastUpdater(config) |
| 12 | super(PodcastFeeder, self).__init__(request, client_address, server) | 12 | super(PodcastFeeder, self).__init__(request, client_address, server) |
| 13 | 13 | ||
| 14 | def do_GET(self): | 14 | 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 | |||
| 2 | import os | 2 | import os |
| 3 | import time | 3 | import time |
| 4 | import hashlib | 4 | import hashlib |
| 5 | import sys | ||
| 5 | 6 | ||
| 6 | from feedgen.feed import FeedGenerator | 7 | from feedgen.feed import FeedGenerator |
| 7 | 8 | ||
| @@ -11,11 +12,19 @@ from . import ( | |||
| 11 | 12 | ||
| 12 | 13 | ||
| 13 | class PodcastUpdater: | 14 | class PodcastUpdater: |
| 14 | def __init__(self, youtube_config, podcast_config): | 15 | def __init__(self, config): |
| 15 | self.podcasts = podcast_config | 16 | self.podcasts = config.podcasts |
| 16 | self.youtube = youtube.Youtube(youtube_config["api-key"]) | 17 | self.youtube = youtube.Youtube(config.youtube["api-key"]) |
| 18 | |||
| 19 | if sys.platform == "linux" and not hasattr(sys, "real_prefix"): | ||
| 20 | self.data_dir = "/var/lib/youtube-podcaster" | ||
| 21 | else: | ||
| 22 | self.data_dir = "%s/var/lib/youtube-podcaster" % (sys.prefix) | ||
| 23 | |||
| 24 | os.makedirs(self.data_dir, 0o755, True) | ||
| 25 | |||
| 26 | self.feeds_file = "%s/feeds.dump" % (self.data_dir) | ||
| 17 | 27 | ||
| 18 | self.feeds_file = "feeds.save" | ||
| 19 | if os.path.isfile(self.feeds_file): | 28 | if os.path.isfile(self.feeds_file): |
| 20 | with open(self.feeds_file, "rb") as feeds: | 29 | with open(self.feeds_file, "rb") as feeds: |
| 21 | self.feeds = pickle.load(feeds) | 30 | self.feeds = pickle.load(feeds) |
| @@ -42,6 +51,8 @@ class PodcastUpdater: | |||
| 42 | 51 | ||
| 43 | def update_podcast(self, channel, playlist): | 52 | def update_podcast(self, channel, playlist): |
| 44 | feed_id = hashlib.sha1(bytes("%s %s" % (channel, playlist), "UTF-8")).hexdigest() | 53 | feed_id = hashlib.sha1(bytes("%s %s" % (channel, playlist), "UTF-8")).hexdigest() |
| 54 | feed_file = "%s/%s.xml" % (self.data_dir, feed_id) | ||
| 55 | |||
| 45 | yt_channel = self.youtube.get_channel(channel)[0] | 56 | yt_channel = self.youtube.get_channel(channel)[0] |
| 46 | yt_playlists = self.youtube.get_playlists(yt_channel, 50) | 57 | yt_playlists = self.youtube.get_playlists(yt_channel, 50) |
| 47 | 58 | ||
| @@ -58,17 +69,16 @@ class PodcastUpdater: | |||
| 58 | 69 | ||
| 59 | if feed.last_updated < time.time() - 600: | 70 | if feed.last_updated < time.time() - 600: |
| 60 | self.populate_feed(feed, feed_id, yt_playlist) | 71 | self.populate_feed(feed, feed_id, yt_playlist) |
| 72 | feed.rss_file(feed_file) | ||
| 61 | 73 | ||
| 62 | feed_file = "%s.xml" % (feed_id) | 74 | with open(self.feeds_file, "wb") as feeds: |
| 63 | self.feeds[feed_id].rss_file(feed_file) | 75 | pickle.dump(self.feeds, feeds) |
| 64 | |||
| 65 | with open(self.feeds_file, "wb") as feed: | ||
| 66 | pickle.dump(self.feeds, feed) | ||
| 67 | 76 | ||
| 68 | return "%s.xml" % (feed_id) | 77 | return feed_file |
| 69 | 78 | ||
| 70 | def add_feed(self, feed_id, yt_playlist): | 79 | def add_feed(self, feed_id, yt_playlist): |
| 71 | feed = FeedGenerator() | 80 | feed = FeedGenerator() |
| 81 | |||
| 72 | feed.load_extension("podcast") | 82 | feed.load_extension("podcast") |
| 73 | feed.id(feed_id) | 83 | feed.id(feed_id) |
| 74 | feed.title(yt_playlist["snippet"]["title"]) | 84 | feed.title(yt_playlist["snippet"]["title"]) |
| @@ -78,7 +88,9 @@ class PodcastUpdater: | |||
| 78 | feed.link(href="https://www.youtube.com/playlist?list=%s" % (yt_playlist["id"])) | 88 | feed.link(href="https://www.youtube.com/playlist?list=%s" % (yt_playlist["id"])) |
| 79 | feed.rss_str(pretty=True) | 89 | feed.rss_str(pretty=True) |
| 80 | feed.last_updated = 0 | 90 | feed.last_updated = 0 |
| 91 | |||
| 81 | self.feeds[feed_id] = feed | 92 | self.feeds[feed_id] = feed |
| 93 | |||
| 82 | return feed | 94 | return feed |
| 83 | 95 | ||
| 84 | def populate_feed(self, feed, feed_id, yt_playlist, max_results=5): | 96 | def populate_feed(self, feed, feed_id, yt_playlist, max_results=5): |
| @@ -93,7 +105,9 @@ class PodcastUpdater: | |||
| 93 | break | 105 | break |
| 94 | else: | 106 | else: |
| 95 | url, size, mime = downloader.download(video, video_id, feed_id) | 107 | url, size, mime = downloader.download(video, video_id, feed_id) |
| 108 | |||
| 96 | feed_entry = feed.add_entry() | 109 | feed_entry = feed.add_entry() |
| 110 | |||
| 97 | feed_entry.id(video_id) | 111 | feed_entry.id(video_id) |
| 98 | feed_entry.guid(video_id) | 112 | feed_entry.guid(video_id) |
| 99 | feed_entry.title(video["snippet"]["title"]) | 113 | 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 @@ | |||
| 1 | { | ||
| 2 | "youtube": { | ||
| 3 | "api-key": "youtube-api-v3-key-here" | ||
| 4 | }, | ||
| 5 | |||
| 6 | "podcasts": [{ | ||
| 7 | "username": "youtube-username-here", | ||
| 8 | "playlists": ["playlist-name-here"] | ||
| 9 | }] | ||
| 10 | } | ||
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 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | import youtube_dl | ||
| 4 | import os | 3 | import os |
| 4 | import mimetypes | ||
| 5 | |||
| 6 | import youtube_dl | ||
| 5 | 7 | ||
| 6 | 8 | ||
| 7 | class Downloader: | 9 | class Downloader: |
| 8 | instance = None | 10 | instance = None |
| 9 | 11 | ||
| 10 | def get_instance(file_format, location, base_url): | 12 | def get_instance(file_format, location, base_url): |
| 11 | if Downloader.instance: | 13 | if not Downloader.instance: |
| 12 | return Downloader.instance | ||
| 13 | else: | ||
| 14 | Downloader.instance = Downloader(file_format, location, base_url) | 14 | Downloader.instance = Downloader(file_format, location, base_url) |
| 15 | return Downloader.instance | 15 | return Downloader.instance |
| 16 | 16 | ||
| 17 | def __init__(self, file_format, location, base_url): | 17 | def __init__(self, file_format, location, base_url): |
| 18 | self.file_format = file_format | 18 | self.file_format = file_format |
| 19 | self.location = location | 19 | self.location = location |
| 20 | self.base_url = base_url | 20 | self.base_url = base_url |
| 21 | 21 | ||
| 22 | if file_format == "vorbis": | ||
| 23 | self.extension = "ogg" | ||
| 24 | |||
| 22 | def download(self, video, video_id, feed_id): | 25 | def download(self, video, video_id, feed_id): |
| 23 | output = "%s/%s/%s.ogg" % (self.location, feed_id, video_id) | 26 | output = "%s/%s/%s.%s" % (self.location, feed_id, video_id, self.extension) |
| 24 | options = {"format": "bestaudio/best", | 27 | options = {"format": "bestaudio/best", |
| 25 | "outtmpl": output, | 28 | "outtmpl": output, |
| 26 | "postprocessors": [{ | 29 | "postprocessors": [{ |
| 27 | "key": "FFmpegExtractAudio", | 30 | "key": "FFmpegExtractAudio", |
| 28 | "preferredcodec": self.file_format | 31 | "preferredcodec": self.file_format |
| 29 | }], | 32 | }], |
| 30 | "nooverwrites": True | 33 | "nooverwrites": True} |
| 31 | } | 34 | |
| 32 | 35 | ||
| 33 | video_url = "https://www.youtube.com/watch?v=%s" % (video["snippet"]["resourceId"]["videoId"]) | 36 | video_url = "https://www.youtube.com/watch?v=%s" % (video["snippet"]["resourceId"]["videoId"]) |
| 34 | youtube_dl.YoutubeDL(options).download([video_url]) | 37 | youtube_dl.YoutubeDL(options).download([video_url]) |
| 35 | 38 | ||
| 36 | url = "%s/%s/%s.ogg" % (self.base_url, feed_id, video_id) | 39 | url = "%s/%s/%s.%s" % (self.base_url, feed_id, video_id, self.extension) |
| 37 | size = str(os.path.getsize(output)) | 40 | size = str(os.path.getsize(output)) |
| 38 | mime = "audio/ogg" | 41 | mime = mimetypes.guess_type(output)[0] |
| 39 | 42 | ||
| 40 | return (url, size, mime) | 43 | return (url, size, mime) |
| 41 | 44 | ||
