aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--README.md20
-rwxr-xr-xyoutube-podcaster159
-rw-r--r--youtube-podcaster.json.sample10
-rw-r--r--youtube/__init__.py6
-rw-r--r--youtube/downloader.py42
-rw-r--r--youtube/youtube.py44
7 files changed, 287 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1d7362c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
1downloads/
2
3feeds.save
4youtube-podcaster.json
5
6*.pyc
7*.xml
8*.sw?
diff --git a/README.md b/README.md
index ad7cc36..be8fbbd 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,18 @@
1# youtube-podcaster 1# YouTube Podcaster
2Converts youtube playlists to RSS-feeds 2Lets you covert YouTube-playlists to RSS podcast feeds and serve them.
3
4## Features
5* Check playlists on HTTP-request
6* Downloads videos and converts them to Ogg Vorbis files.
7* Serve RSS feed
8
9## What it doesn't do (at the moment)
10* Convert to mp3, mp4 or any other audio/video format
11* Serve the downloaded audio files (needs to be done by an other server)
12
13## To do
14* Add comments/documentation
15* Support multiple audio/video formats
16
17## LICENSE
18YouTube Podcaster is licensed under the MIT License, see the LICENSE file for more info.
diff --git a/youtube-podcaster b/youtube-podcaster
new file mode 100755
index 0000000..bb2db26
--- /dev/null
+++ b/youtube-podcaster
@@ -0,0 +1,159 @@
1#!/usr/bin/env python3
2import time
3import os
4import pickle
5import json
6
7from http.server import HTTPServer, BaseHTTPRequestHandler
8from hashlib import sha1
9
10from feedgen.feed import FeedGenerator
11
12import youtube
13
14
15class PodcastUpdater:
16 instance = None
17
18 def get_instance():
19 if PodcastUpdater.instance:
20 return PodcastUpdater.instance
21 else:
22 PodcastUpdater.instance = PodcastUpdater()
23 return PodcastUpdater.instance
24
25 def __init__(self):
26 self.podcasts = config["podcasts"]
27
28 self.feeds_file = "feeds.save"
29 if os.path.isfile(self.feeds_file):
30 with open(self.feeds_file, "rb") as feeds:
31 self.feeds = pickle.load(feeds)
32 else:
33 self.feeds = {}
34
35 def get_xml(self, channel, playlist):
36 channel = channel.replace('_', ' ')
37 playlist = playlist.replace('_', ' ')
38
39 for podcast in self.podcasts:
40 if podcast["username"] == channel:
41 break
42 else:
43 return None
44
45 if playlist not in podcast["playlists"]:
46 return None
47
48 xml = self.update_podcast(channel, playlist)
49
50 if xml:
51 return open(xml).read()
52
53 def update_podcast(self, channel, playlist):
54 feed_id = sha1(bytes("%s %s" % (channel, playlist), "UTF-8")).hexdigest()
55 yt_channel = yt.get_channel(channel)[0]
56 yt_playlists = yt.get_playlists(yt_channel, 50)
57
58 for yt_playlist in yt_playlists:
59 if yt_playlist["snippet"]["title"] == playlist:
60 break
61 else:
62 return None
63
64 if feed_id in self.feeds:
65 feed = self.feeds[feed_id]
66 else:
67 feed = self.add_feed(feed_id, yt_playlist)
68
69 if feed.last_updated < time.time() - 600:
70 self.populate_feed(feed, feed_id, yt_playlist)
71
72 feed_file = "%s.xml" % (feed_id)
73 self.feeds[feed_id].rss_file(feed_file)
74
75 with open(self.feeds_file, "wb") as feed:
76 pickle.dump(self.feeds, feed)
77
78 return "%s.xml" % (feed_id)
79
80 def add_feed(self, feed_id, yt_playlist):
81 feed = FeedGenerator()
82 feed.load_extension("podcast")
83 feed.id(feed_id)
84 feed.title(yt_playlist["snippet"]["title"])
85 feed.author({"name": yt_playlist["snippet"]["channelTitle"]})
86 feed.description(yt_playlist["snippet"]["description"])
87 feed.logo(yt_playlist["snippet"]["thumbnails"]["standard"]["url"])
88 feed.link(href="https://www.youtube.com/playlist?list=%s" % (yt_playlist["id"]))
89 feed.rss_str(pretty=True)
90 feed.last_updated = 0
91 self.feeds[feed_id] = feed
92 return feed
93
94 def populate_feed(self, feed, feed_id, yt_playlist, max_results=5):
95 videos = yt.get_playlist_items(yt_playlist, max_results)
96 downloader = youtube.Downloader.get_instance("vorbis", "downloads", "192.168.178.100")
97
98 entries = feed.entry()
99 for video in videos:
100 video_id = sha1(bytes(video["id"], "UTF-8")).hexdigest()
101 for entry in entries:
102 if entry.id() == video_id:
103 break
104 else:
105 url, size, mime = downloader.download(video, video_id, feed_id)
106 feed_entry = feed.add_entry()
107 feed_entry.id(video_id)
108 feed_entry.guid(video_id)
109 feed_entry.title(video["snippet"]["title"])
110 feed_entry.description(video["snippet"]["description"])
111 feed_entry.published(video["snippet"]["publishedAt"])
112 feed_entry.enclosure(url, size, mime)
113
114 feed.last_updated = time.time()
115
116
117class PodcastFeeder(BaseHTTPRequestHandler):
118 def do_GET(self):
119 updater = PodcastUpdater.get_instance()
120 path = self.path.split('/')
121
122 if len(path) == 3:
123 channel = path[1]
124 playlist = path[2]
125 else:
126 return self.return_error(404)
127
128 xml = updater.get_xml(channel, playlist)
129
130 if not xml:
131 return self.return_error(404)
132 else:
133 self.send_response(200)
134 self.send_header("Content-type", "text/xml")
135 self.end_headers()
136 self.wfile.write(bytes(xml, 'UTF-8'))
137
138 def return_error(self, code):
139 self.send_response(code)
140 self.send_header("Content-type", "text/html")
141 self.end_headers()
142
143 reponse = "<body>Error: %s</body>" % (code)
144 self.wfile.write(bytes(reponse, 'UTF-8'))
145
146
147def main():
148 try:
149 server = HTTPServer(("", 8888), PodcastFeeder)
150 server.serve_forever()
151 except KeyboardInterrupt:
152 server.socket.close()
153
154if __name__ == "__main__":
155 config = json.load(open("youtube-podcaster.json"))
156 yt = youtube.Youtube(config["youtube"]["api-key"])
157 main()
158
159# vim: set ts=8 sw=4 tw=0 et :
diff --git a/youtube-podcaster.json.sample b/youtube-podcaster.json.sample
new file mode 100644
index 0000000..d9d078f
--- /dev/null
+++ b/youtube-podcaster.json.sample
@@ -0,0 +1,10 @@
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/__init__.py b/youtube/__init__.py
new file mode 100644
index 0000000..d26ae61
--- /dev/null
+++ b/youtube/__init__.py
@@ -0,0 +1,6 @@
1#!/usr/bin/env python3
2
3from youtube.youtube import Youtube
4from youtube.downloader import Downloader
5
6# vim: set ts=8 sw=4 tw=0 et :
diff --git a/youtube/downloader.py b/youtube/downloader.py
new file mode 100644
index 0000000..ca1327b
--- /dev/null
+++ b/youtube/downloader.py
@@ -0,0 +1,42 @@
1#!/usr/bin/env python3
2
3import youtube_dl
4import os
5
6
7class Downloader:
8 instance = None
9
10 def get_instance(file_format, location, base_url):
11 if Downloader.instance:
12 return Downloader.instance
13 else:
14 Downloader.instance = Downloader(file_format, location, base_url)
15 return Downloader.instance
16
17 def __init__(self, file_format, location, base_url):
18 self.file_format = file_format
19 self.location = location
20 self.base_url = base_url
21
22 def download(self, video, video_id, feed_id):
23 output = "%s/%s/%s.ogg" % (self.location, feed_id, video_id)
24 options = {"format": "bestaudio/best",
25 "outtmpl": output,
26 "postprocessors": [{
27 "key": "FFmpegExtractAudio",
28 "preferredcodec": self.file_format