aboutsummaryrefslogtreecommitdiffstats
path: root/youtube_podcaster
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <t0m.vd.l33@gmail.com>2015-10-19 21:38:00 +0200
committerGravatar Tom van der Lee <t0m.vd.l33@gmail.com>2015-10-19 21:38:00 +0200
commit9cc311ca5376bfcbcacf7ac492f5958acfac0682 (patch)
tree451b076d21bbaa477c901b83d325d7360be2cd67 /youtube_podcaster
parenta7fe15b99d64904855ba2bd3649b1a3642824a75 (diff)
downloadyoutube-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__.py44
-rw-r--r--youtube_podcaster/__main__.py8
-rw-r--r--youtube_podcaster/config.py64
-rw-r--r--youtube_podcaster/podcastfeeder.py4
-rw-r--r--youtube_podcaster/podcastupdater.py34
-rw-r--r--youtube_podcaster/youtube-podcaster.json.sample10
-rw-r--r--youtube_podcaster/youtube/downloader.py23
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
3import json 3import argparse
4 4
5from http.server import ( 5from http.server import HTTPServer
6 HTTPServer,
7)
8 6
9from . import ( 7from .podcastfeeder import create_feeder
10 youtube, 8from .config import (
9 Config,
10 ConfigException
11) 11)
12 12
13from .podcastfeeder import ( 13"""
14 create_feeder 14Start the program
15) 15"""
16 16
17 17
18def main(): 18def 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
3import youtube_podcaster
4
5if __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 @@
1import sys
2import os
3import json
4
5
6class ConfigException(Exception):
7 def __init__(self, msg):
8 super(ConfigException, self).__init__("Config exception: %s" % (msg))
9
10
11class 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
8def create_feeder(youtube_config, podcast_config): 8def 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
2import os 2import os
3import time 3import time
4import hashlib 4import hashlib
5import sys
5 6
6from feedgen.feed import FeedGenerator 7from feedgen.feed import FeedGenerator
7 8
@@ -11,11 +12,19 @@ from . import (
11 12
12 13
13class PodcastUpdater: 14class 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
3import youtube_dl
4import os 3import os
4import mimetypes
5
6import youtube_dl
5 7
6 8
7class Downloader: 9class 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