redirect when using non-canonical URL for podcasts, episodes
[mygpo.git] / mygpo / web / views / episode.py
blobf5cd62e8138d7125aa7f45da26f6c14aa3d1ac34
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/>.
18 from datetime import datetime
19 from functools import wraps
21 import dateutil.parser
23 from django.shortcuts import render
24 from django.http import HttpResponseRedirect, Http404
25 from django.contrib.auth.decorators import login_required
26 from django.contrib.sites.models import RequestSite
27 from django.views.decorators.vary import vary_on_cookie
28 from django.views.decorators.cache import never_cache, cache_control
29 from django.contrib import messages
30 from django.utils.translation import ugettext as _
32 from mygpo.api.constants import EPISODE_ACTION_TYPES
33 from mygpo.decorators import repeat_on_conflict
34 from mygpo.core.proxy import proxy_object
35 from mygpo.users.models import Chapter, HistoryEntry, EpisodeAction
36 from mygpo.utils import parse_time
37 from mygpo.web.heatmap import EpisodeHeatmap
38 from mygpo.web.utils import get_episode_link_target, fetch_episode_data
39 from mygpo.db.couchdb.episode import episode_for_slug_id, episode_for_oldid, \
40 favorite_episodes_for_user, chapters_for_episode
41 from mygpo.db.couchdb.podcast import podcast_by_id, podcast_for_url, \
42 podcasts_to_dict
43 from mygpo.db.couchdb.episode_state import episode_state_for_user_episode
44 from mygpo.db.couchdb.user import get_latest_episodes
45 from mygpo.userfeeds.feeds import FavoriteFeed
48 @vary_on_cookie
49 @cache_control(private=True)
50 def episode(request, episode):
52 podcast = podcast_by_id(episode.podcast)
54 if not podcast:
55 raise Http404
57 if request.user.is_authenticated():
59 episode_state = episode_state_for_user_episode(request.user, episode)
60 is_fav = episode_state.is_favorite()
63 # pre-populate data for fetch_data
64 podcasts_dict = {podcast.get_id(): podcast}
65 episodes_dict = {episode._id: episode}
67 history = list(episode_state.get_history_entries())
68 HistoryEntry.fetch_data(request.user, history,
69 podcasts=podcasts_dict, episodes=episodes_dict)
71 played_parts = EpisodeHeatmap(podcast.get_id(),
72 episode._id, request.user._id, duration=episode.duration)
74 devices = dict( (d.id, d.name) for d in request.user.devices )
76 else:
77 history = []
78 is_fav = False
79 played_parts = None
80 devices = {}
83 chapters = []
84 for user, chapter in chapters_for_episode(episode._id):
85 chapter.is_own = request.user.is_authenticated() and \
86 user == request.user._id
87 chapters.append(chapter)
90 prev = podcast.get_episode_before(episode)
91 next = podcast.get_episode_after(episode)
93 return render(request, 'episode.html', {
94 'episode': episode,
95 'podcast': podcast,
96 'prev': prev,
97 'next': next,
98 'history': history,
99 'chapters': chapters,
100 'is_favorite': is_fav,
101 'played_parts': played_parts,
102 'actions': EPISODE_ACTION_TYPES,
103 'devices': devices,
107 @never_cache
108 @login_required
109 def add_chapter(request, episode):
110 e_state = episode_state_for_user_episode(request.user, episode)
112 podcast = podcast_by_id(episode.podcast)
114 try:
115 start = parse_time(request.POST.get('start', '0'))
117 if request.POST.get('end', '0'):
118 end = parse_time(request.POST.get('end', '0'))
119 else:
120 end = start
122 adv = 'advertisement' in request.POST
123 label = request.POST.get('label')
125 except ValueError as e:
126 messages.error(request,
127 _('Could not add Chapter: {msg}'.format(msg=str(e))))
129 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
132 chapter = Chapter()
133 chapter.start = start
134 chapter.end = end
135 chapter.advertisement = adv
136 chapter.label = label
138 e_state.update_chapters(add=[chapter])
140 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
143 @never_cache
144 @login_required
145 def remove_chapter(request, episode, start, end):
146 e_state = episode_state_for_user_episode(request.user, episode)
148 remove = (int(start), int(end))
149 e_state.update_chapters(rem=[remove])
151 podcast = podcast_by_id(episode.podcast)
153 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
156 @never_cache
157 @login_required
158 def toggle_favorite(request, episode):
159 episode_state = episode_state_for_user_episode(request.user, episode)
161 @repeat_on_conflict(['episode_state'])
162 def _set_fav(episode_state, is_fav):
163 episode_state.set_favorite(is_fav)
164 episode_state.save()
166 is_fav = episode_state.is_favorite()
167 _set_fav(episode_state=episode_state, is_fav=not is_fav)
169 podcast = podcast_by_id(episode.podcast)
171 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
175 @vary_on_cookie
176 @cache_control(private=True)
177 @login_required
178 def list_favorites(request):
179 user = request.user
180 site = RequestSite(request)
182 episodes = favorite_episodes_for_user(user)
184 recently_listened = get_latest_episodes(user)
186 podcast_ids = [episode.podcast for episode in episodes + recently_listened]
187 podcasts = podcasts_to_dict(podcast_ids)
189 recently_listened = fetch_episode_data(recently_listened, podcasts=podcasts)
190 episodes = fetch_episode_data(episodes, podcasts=podcasts)
192 favfeed = FavoriteFeed(user)
193 feed_url = favfeed.get_public_url(site.domain)
195 podcast = podcast_for_url(feed_url)
197 token = request.user.favorite_feeds_token
199 return render(request, 'favorites.html', {
200 'episodes': episodes,
201 'feed_token': token,
202 'site': site,
203 'podcast': podcast,
204 'recently_listened': recently_listened,
208 @never_cache
209 def add_action(request, episode):
211 device = request.user.get_device(request.POST.get('device'))
213 action_str = request.POST.get('action')
214 timestamp = request.POST.get('timestamp', '')
216 if timestamp:
217 try:
218 timestamp = dateutil.parser.parse(timestamp)
219 except (ValueError, AttributeError):
220 timestamp = datetime.utcnow()
221 else:
222 timestamp = datetime.utcnow()
224 action = EpisodeAction()
225 action.timestamp = timestamp
226 action.device = device.id if device else None
227 action.action = action_str
229 state = episode_state_for_user_episode(request.user, episode)
231 @repeat_on_conflict(['state'])
232 def _add_action(state, action):
233 state.add_actions([action])
234 state.save()
236 _add_action(state, action)
238 podcast = podcast_by_id(episode.podcast)
240 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
242 # To make all view accessible via either CouchDB-ID for Slugs
243 # a decorator queries the episode and passes the Id on to the
244 # regular views
246 def slug_id_decorator(f):
247 @wraps(f)
248 def _decorator(request, p_slug_id, e_slug_id, *args, **kwargs):
249 episode = episode_for_slug_id(p_slug_id, e_slug_id)
251 if episode is None:
252 raise Http404
254 # redirect when Id or a merged (non-cannonical) slug is used
255 if episode.slug and episode.slug != e_slug_id:
256 podcast = podcast_by_id(episode.podcast)
257 return HttpResponseRedirect(
258 get_episode_link_target(episode, podcast))
260 return f(request, episode, *args, **kwargs)
262 return _decorator
265 def oldid_decorator(f):
266 @wraps(f)
267 def _decorator(request, id, *args, **kwargs):
268 episode = episode_for_oldid(id)
270 if episode is None:
271 raise Http404
273 # redirect to Id or slug URL
274 podcast = podcast_by_id(episode.podcast)
275 return HttpResponseRedirect(get_episode_link_target(episode, podcast))
277 return _decorator
279 show_slug_id = slug_id_decorator(episode)
280 add_chapter_slug_id = slug_id_decorator(add_chapter)
281 remove_chapter_slug_id = slug_id_decorator(remove_chapter)
282 toggle_favorite_slug_id = slug_id_decorator(toggle_favorite)
283 add_action_slug_id = slug_id_decorator(add_action)
285 show_oldid = oldid_decorator(episode)
286 add_chapter_oldid = oldid_decorator(add_chapter)
287 remove_chapter_oldid = oldid_decorator(remove_chapter)
288 toggle_favorite_oldid = oldid_decorator(toggle_favorite)
289 add_action_oldid = oldid_decorator(add_action)