3 from functools
import wraps
5 from django
.shortcuts
import render
6 from django
.core
.cache
import cache
7 from django
.http
import HttpResponse
, HttpResponseBadRequest
8 from django
.views
.decorators
.cache
import cache_page
9 from django
.views
.decorators
.csrf
import csrf_exempt
10 from django
.views
.decorators
.cache
import never_cache
11 from django
.contrib
.sites
.requests
import RequestSite
12 from django
.utils
.translation
import gettext
as _
14 from mygpo
.api
.basic_auth
import require_valid_user
, check_username
15 from mygpo
.api
.backend
import get_device
16 from mygpo
.podcasts
.models
import Podcast
17 from mygpo
.api
.opml
import Exporter
, Importer
18 from mygpo
.api
.httpresponse
import JsonResponse
19 from mygpo
.directory
.models
import ExamplePodcast
20 from mygpo
.api
.advanced
.directory
import podcast_data
21 from mygpo
.subscriptions
import get_subscribed_podcasts
22 from mygpo
.subscriptions
.tasks
import 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
29 logger
= logging
.getLogger(__name__
)
32 ALLOWED_FORMATS
= ("txt", "opml", "json", "jsonp", "xml")
37 def tmp(request
, format
, *args
, **kwargs
):
38 if format
not in ALLOWED_FORMATS
:
39 return HttpResponseBadRequest("Invalid format")
41 return fn(request
, *args
, format
=format
, **kwargs
)
51 @allowed_methods(["GET", "PUT", "POST"])
53 def subscriptions(request
, username
, device_uid
, format
):
55 user_agent
= request
.META
.get("HTTP_USER_AGENT", "")
57 if request
.method
== "GET":
58 title
= _("%(username)s's Subscription List") % {"username": username
}
59 subscriptions
= get_subscriptions(request
.user
, device_uid
, user_agent
)
60 return format_podcast_list(
61 subscriptions
, format
, title
, jsonp_padding
=request
.GET
.get("jsonp")
64 elif request
.method
in ("PUT", "POST"):
66 body
= request
.body
.decode("utf-8")
67 subscriptions
= parse_subscription(body
, format
)
69 except ValueError as e
:
70 return HttpResponseBadRequest("Unable to parse POST data: %s" % str(e
))
72 return set_subscriptions(subscriptions
, request
.user
, device_uid
, user_agent
)
80 @allowed_methods(["GET"])
82 def all_subscriptions(request
, username
, format
):
85 scale
= int(request
.GET
.get("scale_logo", 64))
86 except (TypeError, ValueError):
87 return HttpResponseBadRequest("scale_logo has to be a numeric value")
89 if scale
not in range(1, 257):
90 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
92 subscriptions
= get_subscribed_podcasts(request
.user
)
93 title
= _("%(username)s's Subscription List") % {"username": username
}
94 domain
= RequestSite(request
).domain
95 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
96 return format_podcast_list(
101 xml_template
="podcasts.xml",
106 def format_podcast_list(
111 json_map
=lambda x
: x
.url
,
118 Formats a list of podcasts for use in a API response
120 obj_list is a list of podcasts or objects that contain podcasts
121 format is one if txt, opml or json
122 title is a label of the list
123 if obj_list is a list of objects containing podcasts, get_podcast is the
124 function used to get the podcast out of the each of these objects
125 json_map is a function returning the contents of an object (from obj_list)
126 that should be contained in the result (only used for format='json')
129 def default_get_podcast(p
):
132 get_podcast
= get_podcast
or default_get_podcast
135 podcasts
= map(get_podcast
, obj_list
)
136 s
= "\n".join([p
.url
for p
in podcasts
] + [""])
137 return HttpResponse(s
, content_type
="text/plain")
139 elif format
== "opml":
140 podcasts
= map(get_podcast
, obj_list
)
141 exporter
= Exporter(title
)
142 opml
= exporter
.generate(podcasts
)
143 return HttpResponse(opml
, content_type
="text/xml")
145 elif format
== "json":
146 objs
= list(map(json_map
, obj_list
))
147 return JsonResponse(objs
)
149 elif format
== "jsonp":
150 ALLOWED_FUNCNAME
= string
.ascii_letters
+ string
.digits
+ "_"
152 if not jsonp_padding
:
153 return HttpResponseBadRequest(
154 "For a JSONP response, specify the name of the callback function in the jsonp parameter"
157 if any(x
not in ALLOWED_FUNCNAME
for x
in jsonp_padding
):
158 return HttpResponseBadRequest(
159 "JSONP padding can only contain the characters %(char)s"
160 % {"char": ALLOWED_FUNCNAME
}
163 objs
= list(map(json_map
, obj_list
))
164 return JsonResponse(objs
, jsonp_padding
=jsonp_padding
)
166 elif format
== "xml":
167 if None in (xml_template
, request
):
168 return HttpResponseBadRequest("XML is not a valid format for this request")
170 podcasts
= map(json_map
, obj_list
)
171 template_args
.update({"podcasts": podcasts
})
174 request
, xml_template
, template_args
, content_type
="application/xml"
181 def get_subscriptions(user
, device_uid
, user_agent
=None):
182 device
= get_device(user
, device_uid
, user_agent
)
183 return device
.get_subscribed_podcasts()
186 def parse_subscription(raw_post_data
, format
):
187 """Parses the data according to the format"""
189 urls
= raw_post_data
.split("\n")
191 elif format
== "opml":
192 begin
= raw_post_data
.find("<?xml")
193 end
= raw_post_data
.find("</opml>") + 7
194 i
= Importer(content
=raw_post_data
[begin
:end
])
195 urls
= [p
["url"] for p
in i
.items
]
197 elif format
== "json":
198 begin
= raw_post_data
.find("[")
199 end
= raw_post_data
.find("]") + 1
200 urls
= json
.loads(raw_post_data
[begin
:end
])
202 if not isinstance(urls
, list):
203 raise ValueError("A list of feed URLs was expected")
208 urls
= filter(None, urls
)
209 urls
= list(map(normalize_feed_url
, urls
))
213 def set_subscriptions(urls
, user
, device_uid
, user_agent
):
216 urls
= list(filter(None, (u
.strip() for u
in urls
)))
218 device
= get_device(user
, device_uid
, user_agent
, undelete
=True)
220 subscriptions
= dict((p
.url
, p
) for p
in device
.get_subscribed_podcasts())
221 new
= [p
for p
in urls
if p
not in subscriptions
.keys()]
222 rem
= [p
for p
in subscriptions
.keys() if p
not in urls
]
224 remove_podcasts
= Podcast
.objects
.filter(urls__url__in
=rem
)
225 for podcast
in remove_podcasts
:
226 unsubscribe(podcast
.pk
, user
.pk
, device
.uid
)
229 podcast
= Podcast
.objects
.get_or_create_for_url(url
).object
230 subscribe(podcast
.pk
, user
.pk
, device
.uid
, url
)
232 # Only an empty response is a successful response
233 return HttpResponse("", content_type
="text/plain")
237 @allowed_methods(["GET"])
240 def toplist(request
, count
, format
):
241 count
= parse_range(count
, 1, 100, 100)
243 entries
= Podcast
.objects
.all().toplist()[:count
]
244 domain
= RequestSite(request
).domain
247 scale
= int(request
.GET
.get("scale_logo", 64))
248 except (TypeError, ValueError):
249 return HttpResponseBadRequest("scale_logo has to be a numeric value")
251 if scale
not in range(1, 257):
252 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
256 p
= podcast_data(podcast
, domain
, scale
)
259 title
= _("gpodder.net - Top %(count)d") % {"count": len(entries
)}
260 return format_podcast_list(
264 get_podcast
=lambda t
: t
,
266 jsonp_padding
=request
.GET
.get("jsonp", ""),
267 xml_template
="podcasts.xml",
274 @allowed_methods(["GET"])
276 def search(request
, format
):
280 query
= request
.GET
.get("q", "")
283 scale
= int(request
.GET
.get("scale_logo", 64))
284 except (TypeError, ValueError):
285 return HttpResponseBadRequest("scale_logo has to be a numeric value")
287 if scale
not in range(1, 257):
288 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
291 return HttpResponseBadRequest("/search.opml|txt|json?q={query}")
293 results
= search_podcasts(query
)[:NUM_RESULTS
]
295 title
= _("gpodder.net - Search")
296 domain
= RequestSite(request
).domain
297 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
298 return format_podcast_list(
303 jsonp_padding
=request
.GET
.get("jsonp", ""),
304 xml_template
="podcasts.xml",
312 @allowed_methods(["GET"])
314 def suggestions(request
, count
, format
):
315 count
= parse_range(count
, 1, 100, 100)
318 suggestions
= Podcast
.objects
.filter(
319 podcastsuggestion__suggested_to
=user
, podcastsuggestion__deleted
=False
321 title
= _("gpodder.net - %(count)d Suggestions") % {"count": len(suggestions
)}
322 domain
= RequestSite(request
).domain
323 p_data
= lambda p
: podcast_data(p
, domain
)
324 return format_podcast_list(
329 jsonp_padding
=request
.GET
.get("jsonp"),
334 @allowed_methods(["GET"])
337 def example_podcasts(request
, format
):
339 podcasts
= cache
.get("example-podcasts", None)
342 scale
= int(request
.GET
.get("scale_logo", 64))
343 except (TypeError, ValueError):
344 return HttpResponseBadRequest("scale_logo has to be a numeric value")
346 if scale
not in range(1, 257):
347 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
350 podcasts
= list(ExamplePodcast
.objects
.get_podcasts())
351 cache
.set("example-podcasts", podcasts
)
353 podcast_ad
= Podcast
.objects
.get_advertised_podcast()
355 podcasts
= [podcast_ad
] + podcasts
357 title
= "gPodder Podcast Directory"
358 domain
= RequestSite(request
).domain
359 p_data
= lambda p
: podcast_data(p
, domain
, scale
)
360 return format_podcast_list(
365 xml_template
="podcasts.xml",