2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
19 from itertools
import islice
21 from couchdbkit
.exceptions
import ResourceNotFound
23 from django
.shortcuts
import render_to_response
24 from django
.template
import RequestContext
25 from django
.core
.cache
import cache
27 from mygpo
.api
.basic_auth
import require_valid_user
, check_username
28 from django
.http
import HttpResponse
, HttpResponseBadRequest
29 from django
.views
.decorators
.cache
import cache_page
30 from mygpo
.core
import models
31 from mygpo
.users
.models
import Suggestions
32 from mygpo
.api
.models
import Device
, Podcast
33 from mygpo
.api
.opml
import Exporter
, Importer
34 from mygpo
.api
.httpresponse
import JsonResponse
35 from mygpo
.api
.sanitizing
import sanitize_urls
36 from mygpo
.directory
.toplist
import PodcastToplist
37 from mygpo
.directory
.models
import ExamplePodcasts
38 from mygpo
.api
.advanced
.directory
import podcast_data
39 from django
.views
.decorators
.csrf
import csrf_exempt
40 from django
.shortcuts
import get_object_or_404
41 from django
.contrib
.sites
.models
import RequestSite
42 from mygpo
.directory
.search
import search_podcasts
43 from mygpo
.log
import log
44 from django
.utils
.translation
import ugettext
as _
45 from mygpo
.decorators
import allowed_methods
46 from mygpo
.utils
import parse_range
47 from mygpo
import migrate
51 import simplejson
as json
56 ALLOWED_FORMATS
= ('txt', 'opml', 'json', 'jsonp', 'xml')
59 def tmp(request
, format
, *args
, **kwargs
):
60 if not format
in ALLOWED_FORMATS
:
61 return HttpResponseBadRequest('Invalid format')
63 return fn(request
, *args
, format
=format
, **kwargs
)
71 @allowed_methods(['GET', 'PUT', 'POST'])
72 def subscriptions(request
, username
, device_uid
, format
):
74 if request
.method
== 'GET':
75 title
= _('%(username)s\'s Subscription List') % {'username': username
}
76 subscriptions
= get_subscriptions(request
.user
, device_uid
)
77 return format_podcast_list(subscriptions
, format
, title
, jsonp_padding
=request
.GET
.get('jsonp'))
79 elif request
.method
in ('PUT', 'POST'):
80 subscriptions
= parse_subscription(request
.raw_post_data
, format
)
81 return set_subscriptions(subscriptions
, request
.user
, device_uid
)
88 @allowed_methods(['GET'])
89 def all_subscriptions(request
, username
, format
):
90 user
= migrate
.get_or_migrate_user(request
.user
)
91 subscriptions
= user
.get_subscribed_podcasts()
92 title
= _('%(username)s\'s Subscription List') % {'username': username
}
93 return format_podcast_list(subscriptions
, format
, title
)
96 def format_podcast_list(obj_list
, format
, title
, get_podcast
=None,
97 json_map
=lambda x
: x
.url
, jsonp_padding
=None,
98 xml_template
=None, request
=None, template_args
={}):
100 Formats a list of podcasts for use in a API response
102 obj_list is a list of podcasts or objects that contain podcasts
103 format is one if txt, opml or json
104 title is a label of the list
105 if obj_list is a list of objects containing podcasts, get_podcast is the
106 function used to get the podcast out of the each of these objects
107 json_map is a function returning the contents of an object (from obj_list)
108 that should be contained in the result (only used for format='json')
111 def default_get_podcast(p
):
112 return p
.get_podcast()
114 get_podcast
= get_podcast
or default_get_podcast
117 podcasts
= map(get_podcast
, obj_list
)
118 s
= '\n'.join([p
.url
for p
in podcasts
] + [''])
119 return HttpResponse(s
, mimetype
='text/plain')
121 elif format
== 'opml':
122 podcasts
= map(get_podcast
, obj_list
)
123 exporter
= Exporter(title
)
124 opml
= exporter
.generate(podcasts
)
125 return HttpResponse(opml
, mimetype
='text/xml')
127 elif format
== 'json':
128 objs
= map(json_map
, obj_list
)
129 return JsonResponse(objs
)
131 elif format
== 'jsonp':
132 ALLOWED_FUNCNAME
= string
.letters
+ string
.digits
+ '_'
134 if not jsonp_padding
:
135 return HttpResponseBadRequest('For a JSONP response, specify the name of the callback function in the jsonp parameter')
137 if any(x
not in ALLOWED_FUNCNAME
for x
in jsonp_padding
):
138 return HttpResponseBadRequest('JSONP padding can only contain the characters %(char)s' % {'char': ALLOWED_FUNCNAME
})
140 objs
= map(json_map
, obj_list
)
141 return JsonResponse(objs
, jsonp_padding
=jsonp_padding
)
143 elif format
== 'xml':
144 if None in (xml_template
, request
):
145 return HttpResponseBadRequest('XML is not a valid format for this request')
147 podcasts
= map(json_map
, obj_list
)
148 template_args
.update({'podcasts': podcasts
})
150 return render_to_response(xml_template
, template_args
, context_instance
=RequestContext(request
),
151 mimetype
='application/xml')
157 def get_subscriptions(user
, device_uid
):
158 device
= get_object_or_404(Device
, uid
=device_uid
, user
=user
, deleted
=False)
159 device
= migrate
.get_or_migrate_device(device
)
161 return device
.get_subscribed_podcasts()
164 def parse_subscription(raw_post_data
, format
):
166 urls
= raw_post_data
.split('\n')
168 elif format
== 'opml':
169 begin
= raw_post_data
.find('<?xml')
170 end
= raw_post_data
.find('</opml>') + 7
171 i
= Importer(content
=raw_post_data
[begin
:end
])
172 urls
= [p
['url'] for p
in i
.items
]
174 elif format
== 'json':
175 begin
= raw_post_data
.find('[')
176 end
= raw_post_data
.find(']') + 1
177 urls
= json
.loads(raw_post_data
[begin
:end
])
183 urls
= sanitize_urls(urls
)
184 urls
= filter(None, urls
)
189 def set_subscriptions(urls
, user
, device_uid
):
190 device
, created
= Device
.objects
.get_or_create(user
=user
, uid
=device_uid
)
192 # undelete a previously deleted device
194 device
.deleted
= False
197 dev
= migrate
.get_or_migrate_device(device
)
198 old
= [p
.url
for p
in dev
.get_subscribed_podcasts()]
199 new
= [p
for p
in urls
if p
not in old
]
200 rem
= [p
for p
in old
if p
not in urls
]
203 p
, created
= Podcast
.objects
.get_or_create(url
=r
)
204 p
= migrate
.get_or_migrate_podcast(p
)
206 p
.unsubscribe(device
)
207 except Exception as e
:
208 log('Simple API: %(username)s: Could not remove subscription for podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
209 {'username': user
.username
, 'podcast_url': r
, 'device_id': device
.id, 'exception': e
})
212 p
, created
= Podcast
.objects
.get_or_create(url
=n
)
213 p
= migrate
.get_or_migrate_podcast(p
)
216 except Exception as e
:
217 log('Simple API: %(username)s: Could not add subscription for podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
218 {'username': user
.username
, 'podcast_url': n
, 'device_id': device
.id, 'exception': e
})
220 # Only an empty response is a successful response
221 return HttpResponse('', mimetype
='text/plain')
225 @allowed_methods(['GET'])
227 def toplist(request
, count
, format
):
228 count
= parse_range(count
, 1, 100, 100)
230 toplist
= PodcastToplist()
231 entries
= toplist
[:count
]
232 domain
= RequestSite(request
).domain
235 scale
= int(request
.GET
.get('scale_logo', 64))
236 except (TypeError, ValueError):
237 return HttpResponseBadRequest('scale_logo has to be a numeric value')
239 if scale
not in range(1, 257):
240 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
245 return podcast
.get_podcast()
249 podcast
.old_pos
= old_pos
251 p
= podcast_data(podcast
, domain
, scale
)
253 subscribers
= podcast
.subscriber_count(),
254 subscribers_last_week
= podcast
.prev_subscriber_count(),
255 position_last_week
= podcast
.old_pos
,
259 title
= _('gpodder.net - Top %(count)d') % {'count': len(entries
)}
260 return format_podcast_list(entries
,
263 get_podcast
=get_podcast
,
265 jsonp_padding
=request
.GET
.get('jsonp', ''),
266 xml_template
='podcasts.xml',
272 @allowed_methods(['GET'])
273 def search(request
, format
):
277 query
= request
.GET
.get('q', '').encode('utf-8')
280 scale
= int(request
.GET
.get('scale_logo', 64))
281 except (TypeError, ValueError):
282 return HttpResponseBadRequest('scale_logo has to be a numeric value')
284 if scale
not in range(1, 257):
285 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
288 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
290 results
, total
= search_podcasts(q
=query
, limit
=NUM_RESULTS
)
292 title
= _('gpodder.net - Search')
293 domain
= RequestSite(request
).domain
294 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
295 return format_podcast_list(results
, format
, title
, json_map
=p_data
, jsonp_padding
=request
.GET
.get('jsonp', ''), xml_template
='podcasts.xml', request
=request
)
300 @allowed_methods(['GET'])
301 def suggestions(request
, count
, format
):
302 count
= parse_range(count
, 1, 100, 100)
304 suggestion_obj
= Suggestions
.for_user_oldid(request
.user
.id)
305 suggestions
= suggestion_obj
.get_podcasts(count
)
306 title
= _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions
)}
307 domain
= RequestSite(request
).domain
308 p_data
= lambda p
: podcast_data(p
, domain
)
309 return format_podcast_list(suggestions
, format
, title
, json_map
=p_data
, jsonp_padding
=request
.GET
.get('jsonp'))
313 @allowed_methods(['GET'])
315 def example_podcasts(request
, format
):
317 podcasts
= cache
.get('example-podcasts', None)
320 scale
= int(request
.GET
.get('scale_logo', 64))
321 except (TypeError, ValueError):
322 return HttpResponseBadRequest('scale_logo has to be a numeric value')
324 if scale
not in range(1, 257):
325 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
331 examples
= ExamplePodcasts
.get('example_podcasts')
332 ids
= examples
.podcast_ids
333 podcasts
= list(models
.Podcast
.get_multi(ids
))
334 cache
.set('example-podcasts', podcasts
)
336 except ResourceNotFound
:
339 title
= 'gPodder Podcast Directory'
340 domain
= RequestSite(request
).domain
341 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
342 return format_podcast_list(
347 xml_template
='podcasts.xml',