3 from itertools
import islice
4 from functools
import wraps
6 from django
.shortcuts
import render
7 from django
.core
.cache
import cache
8 from django
.http
import HttpResponse
, HttpResponseBadRequest
9 from django
.views
.decorators
.cache
import cache_page
10 from django
.views
.decorators
.csrf
import csrf_exempt
11 from django
.views
.decorators
.cache
import never_cache
12 from django
.contrib
.sites
.requests
import RequestSite
13 from django
.utils
.translation
import ugettext
as _
15 from mygpo
.api
.basic_auth
import require_valid_user
, check_username
16 from mygpo
.api
.backend
import get_device
17 from mygpo
.podcasts
.models
import Podcast
18 from mygpo
.api
.opml
import Exporter
, Importer
19 from mygpo
.api
.httpresponse
import JsonResponse
20 from mygpo
.directory
.models
import ExamplePodcast
21 from mygpo
.api
.advanced
.directory
import podcast_data
22 from mygpo
.subscriptions
import get_subscribed_podcasts
, subscribe
, unsubscribe
23 from mygpo
.directory
.search
import search_podcasts
24 from mygpo
.decorators
import allowed_methods
, cors_origin
25 from mygpo
.utils
import parse_range
, normalize_feed_url
28 logger
= logging
.getLogger(__name__
)
31 ALLOWED_FORMATS
= ('txt', 'opml', 'json', 'jsonp', 'xml')
35 def tmp(request
, format
, *args
, **kwargs
):
36 if not format
in ALLOWED_FORMATS
:
37 return HttpResponseBadRequest('Invalid format')
39 return fn(request
, *args
, format
=format
, **kwargs
)
48 @allowed_methods(['GET', 'PUT', 'POST'])
50 def subscriptions(request
, username
, device_uid
, format
):
52 user_agent
= request
.META
.get('HTTP_USER_AGENT', '')
54 if request
.method
== 'GET':
55 title
= _('%(username)s\'s Subscription List') % {'username': username
}
56 subscriptions
= get_subscriptions(request
.user
, device_uid
, user_agent
)
57 return format_podcast_list(subscriptions
, format
, title
, jsonp_padding
=request
.GET
.get('jsonp'))
59 elif request
.method
in ('PUT', 'POST'):
61 body
= request
.body
.decode('utf-8')
62 subscriptions
= parse_subscription(body
, format
)
64 except ValueError as e
:
65 return HttpResponseBadRequest('Unable to parse POST data: %s' % str(e
))
67 return set_subscriptions(subscriptions
, request
.user
, device_uid
,
76 @allowed_methods(['GET'])
78 def all_subscriptions(request
, username
, format
):
81 scale
= int(request
.GET
.get('scale_logo', 64))
82 except (TypeError, ValueError):
83 return HttpResponseBadRequest('scale_logo has to be a numeric value')
85 if scale
not in range(1, 257):
86 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
89 subscriptions
= get_subscribed_podcasts(request
.user
)
90 title
= _('%(username)s\'s Subscription List') % {'username': username
}
91 domain
= RequestSite(request
).domain
92 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
93 return format_podcast_list(subscriptions
, format
, title
,
94 json_map
=p_data
, xml_template
='podcasts.xml', request
=request
)
97 def format_podcast_list(obj_list
, format
, title
, get_podcast
=None,
98 json_map
=lambda x
: x
.url
, jsonp_padding
=None,
99 xml_template
=None, request
=None, template_args
={}):
101 Formats a list of podcasts for use in a API response
103 obj_list is a list of podcasts or objects that contain podcasts
104 format is one if txt, opml or json
105 title is a label of the list
106 if obj_list is a list of objects containing podcasts, get_podcast is the
107 function used to get the podcast out of the each of these objects
108 json_map is a function returning the contents of an object (from obj_list)
109 that should be contained in the result (only used for format='json')
112 def default_get_podcast(p
):
115 get_podcast
= get_podcast
or default_get_podcast
118 podcasts
= map(get_podcast
, obj_list
)
119 s
= '\n'.join([p
.url
for p
in podcasts
] + [''])
120 return HttpResponse(s
, content_type
='text/plain')
122 elif format
== 'opml':
123 podcasts
= map(get_podcast
, obj_list
)
124 exporter
= Exporter(title
)
125 opml
= exporter
.generate(podcasts
)
126 return HttpResponse(opml
, content_type
='text/xml')
128 elif format
== 'json':
129 objs
= list(map(json_map
, obj_list
))
130 return JsonResponse(objs
)
132 elif format
== 'jsonp':
133 ALLOWED_FUNCNAME
= string
.ascii_letters
+ string
.digits
+ '_'
135 if not jsonp_padding
:
136 return HttpResponseBadRequest('For a JSONP response, specify the name of the callback function in the jsonp parameter')
138 if any(x
not in ALLOWED_FUNCNAME
for x
in jsonp_padding
):
139 return HttpResponseBadRequest('JSONP padding can only contain the characters %(char)s' % {'char': ALLOWED_FUNCNAME
})
141 objs
= map(json_map
, obj_list
)
142 return JsonResponse(objs
, jsonp_padding
=jsonp_padding
)
144 elif format
== 'xml':
145 if None in (xml_template
, request
):
146 return HttpResponseBadRequest('XML is not a valid format for this request')
148 podcasts
= map(json_map
, obj_list
)
149 template_args
.update({'podcasts': podcasts
})
151 return render(request
, xml_template
, template_args
,
152 content_type
='application/xml')
158 def get_subscriptions(user
, device_uid
, user_agent
=None):
159 device
= get_device(user
, device_uid
, user_agent
)
160 return device
.get_subscribed_podcasts()
163 def parse_subscription(raw_post_data
, format
):
164 """ Parses the data according to the 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
])
182 urls
= filter(None, urls
)
183 urls
= list(map(normalize_feed_url
, urls
))
187 def set_subscriptions(urls
, user
, device_uid
, user_agent
):
190 urls
= list(filter(None, (u
.strip() for u
in urls
)))
192 device
= get_device(user
, device_uid
, user_agent
, undelete
=True)
194 subscriptions
= dict( (p
.url
, p
) for p
in device
.get_subscribed_podcasts())
195 new
= [p
for p
in urls
if p
not in subscriptions
.keys()]
196 rem
= [p
for p
in subscriptions
.keys() if p
not in urls
]
198 remove_podcasts
= Podcast
.objects
.filter(urls__url__in
=rem
)
199 for podcast
in remove_podcasts
:
200 unsubscribe(podcast
, user
, device
)
203 podcast
= Podcast
.objects
.get_or_create_for_url(url
)
204 subscribe(podcast
, user
, device
, url
)
206 # Only an empty response is a successful response
207 return HttpResponse('', content_type
='text/plain')
211 @allowed_methods(['GET'])
214 def toplist(request
, count
, format
):
215 count
= parse_range(count
, 1, 100, 100)
217 entries
= Podcast
.objects
.all().toplist()[:count
]
218 domain
= RequestSite(request
).domain
221 scale
= int(request
.GET
.get('scale_logo', 64))
222 except (TypeError, ValueError):
223 return HttpResponseBadRequest('scale_logo has to be a numeric value')
225 if scale
not in range(1, 257):
226 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
234 p
= podcast_data(podcast
, domain
, scale
)
237 title
= _('gpodder.net - Top %(count)d') % {'count': len(entries
)}
238 return format_podcast_list(entries
,
241 get_podcast
=get_podcast
,
243 jsonp_padding
=request
.GET
.get('jsonp', ''),
244 xml_template
='podcasts.xml',
251 @allowed_methods(['GET'])
253 def search(request
, format
):
257 query
= request
.GET
.get('q', '')
260 scale
= int(request
.GET
.get('scale_logo', 64))
261 except (TypeError, ValueError):
262 return HttpResponseBadRequest('scale_logo has to be a numeric value')
264 if scale
not in range(1, 257):
265 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
268 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
270 results
= search_podcasts(query
)[:NUM_RESULTS
]
272 title
= _('gpodder.net - Search')
273 domain
= RequestSite(request
).domain
274 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
275 return format_podcast_list(results
, format
, title
, json_map
=p_data
, jsonp_padding
=request
.GET
.get('jsonp', ''), xml_template
='podcasts.xml', request
=request
)
281 @allowed_methods(['GET'])
283 def suggestions(request
, count
, format
):
284 count
= parse_range(count
, 1, 100, 100)
287 suggestions
= Podcast
.objects
.filter(podcastsuggestion__suggested_to
=user
,
288 podcastsuggestion__deleted
=False)
289 title
= _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions
)}
290 domain
= RequestSite(request
).domain
291 p_data
= lambda p
: podcast_data(p
, domain
)
292 return format_podcast_list(suggestions
, format
, title
, json_map
=p_data
, jsonp_padding
=request
.GET
.get('jsonp'))
296 @allowed_methods(['GET'])
299 def example_podcasts(request
, format
):
301 podcasts
= cache
.get('example-podcasts', None)
304 scale
= int(request
.GET
.get('scale_logo', 64))
305 except (TypeError, ValueError):
306 return HttpResponseBadRequest('scale_logo has to be a numeric value')
308 if scale
not in range(1, 257):
309 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
312 podcasts
= list(ExamplePodcast
.objects
.get_podcasts())
313 cache
.set('example-podcasts', podcasts
)
315 podcast_ad
= Podcast
.objects
.get_advertised_podcast()
317 podcasts
= [podcast_ad
] + podcasts
319 title
= 'gPodder Podcast Directory'
320 domain
= RequestSite(request
).domain
321 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
322 return format_podcast_list(
327 xml_template
='podcasts.xml',