"""Stöbern (podcasts/discover.py): the full-text index, similar episodes, people, an episode from outside onto the Merkliste.""" import json from datetime import datetime, timedelta, timezone from unittest.mock import patch from django.contrib.auth.models import User from django.core.cache import cache from django.test import TestCase from . import discover, people from .models import EpisodeProgress, PodcastEpisode, PodcastFeed, PodcastLater class PeopleTests(TestCase): def test_names_from_titles(self): eps = [ (1, 1, 'Episode 651: Die Brandmauer mit Peter Decker'), (2, 1, 'Episode 568: Trumps Zollkrieg mit Peter Decker'), (3, 2, 'Imperialismus heute - Peter Decker - 2006'), (4, 3, 'Gespräch mit Peter Schadt über Arbeit'), (5, 3, 'Ulrike Eifler im Gespräch mit Peter Thiel'), (6, 4, 'Was tun? Mit Ulrike Herrmann'), (9, 4, 'Streik! Diskussion mit Ulrike Eifler'), (7, 5, 'Gespräch mit Ulrike Herrmann über das Ende des Kapitalismus'), (8, 6, 'Live aus Berlin: Die Linke und Friedrich Merz'), ] found = {p['name']: p for p in people.people(eps)} self.assertEqual(found['Peter Decker']['episodes'], 3) self.assertEqual(found['Peter Decker']['feeds'], 2) self.assertIn('Ulrike Herrmann', found) self.assertNotIn('Die Brandmauer', found) self.assertNotIn('Die Linke', found) class IndexTests(TestCase): def setUp(self): cache.clear() self.user = User.objects.create_user(username='finder', password='pw12345678') self.client.force_login(self.user) self.other = User.objects.create_user(username='other', password='pw12345678') self.f1 = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/1', title='Eins') self.f2 = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/2', title='Zwei') self.f3 = PodcastFeed.objects.create(user=self.user, rss_url='https://example.org/3', title='Drei', new_episodes=PodcastFeed.NEW_IGNORE) theirs = PodcastFeed.objects.create(user=self.other, rss_url='https://example.org/1', title='Eins') def ep(feed, guid, title, desc=''): return PodcastEpisode.objects.create(feed=feed, guid=guid, title=title, description=desc, audio_url=f'https://example.org/{guid}.mp3', pub_date=datetime(2026, 1, 1, tzinfo=timezone.utc)) self.a = ep(self.f1, 'a', 'Über die Ökonomie der Plattformen', '
Mit Gästen über Plattformkapitalismus und Arbeit.
') self.b = ep(self.f2, 'b', 'Plattformkapitalismus erklärt', 'Wie Plattformen Arbeit organisieren.') self.c = ep(self.f2, 'c', 'Gartenarbeit im Herbst', 'Tomaten, Kürbisse und Laub.') self.d = ep(self.f3, 'd', 'Noch mehr Plattformkapitalismus', 'Auf Abruf.') for i in range(30): # words everywhere say nothing ep(self.f1, f'x{i}', f'Folge {i}', 'Arbeit und Gesellschaft') ep(theirs, 't', 'Plattformkapitalismus bei jemand anderem', '') self.assertGreaterEqual(discover.index_new(), 35) self.assertEqual(discover.index_new(), 0, 'nothing twice') def test_search_titles_shownotes_prefix_folding(self): r = self.client.get('/podcasts/discover/search/?q=plattformkap').json() self.assertEqual({e['id'] for e in r['episodes']}, {self.a.id, self.b.id, self.d.id}) self.assertEqual(r['episodes'][0]['id'] in (self.b.id, self.d.id), True, 'a title match first') r = self.client.get('/podcasts/discover/search/?q=okonomie').json() self.assertEqual([e['id'] for e in r['episodes']], [self.a.id]) r = self.client.get('/podcasts/discover/search/?q=plattformkapitalismus&stream=1').json() self.assertNotIn(self.d.id, [e['id'] for e in r['episodes']], 'Auf Abruf is not in the Strom') def test_similar_from_other_podcasts(self): r = self.client.get(f'/podcasts/discover/similar/{self.b.id}/').json() ids = [e['id'] for e in r['episodes']] self.assertIn(self.a.id, ids) self.assertNotIn(self.c.id, ids, 'same podcast, and about gardens') self.assertEqual(self.client.get('/podcasts/discover/similar/999999/').status_code, 404) def test_person_episodes(self): PodcastEpisode.objects.create(feed=self.f1, guid='p', title='Arbeit mit Peter Decker', audio_url='https://example.org/p.mp3') r = self.client.get('/podcasts/discover/person/?name=Peter Decker').json() self.assertEqual([e['title'] for e in r['episodes']], ['Arbeit mit Peter Decker']) def test_forgotten(self): old = PodcastEpisode.objects.create(feed=self.f2, guid='old', title='Alt', audio_url='https://example.org/old.mp3', pub_date=datetime.now(timezone.utc) - timedelta(days=200)) for e in (self.b, self.c, self.a): EpisodeProgress.objects.create(user=self.user, episode=e, played=True) PodcastEpisode.objects.create(feed=self.f2, guid='h1', title='H1', audio_url='https://example.org/h1.mp3') heard = PodcastEpisode.objects.create(feed=self.f2, guid='h2', title='H2', audio_url='https://example.org/h2.mp3') EpisodeProgress.objects.create(user=self.user, episode=heard, played=True) r = self.client.get('/podcasts/discover/forgotten/').json() self.assertEqual([e['id'] for e in r['loved']], [old.id]) class OutsideTests(TestCase): def setUp(self): cache.clear() self.user = User.objects.create_user(username='out', password='pw12345678') self.client.force_login(self.user) PodcastFeed.objects.create(user=self.user, rss_url='https://mine.example/feed', title='Mein Podcast') def test_elsewhere_keeps_only_the_person_and_unknown_podcasts(self): apple = {'results': [ {'collectionName': 'Fremd', 'feedUrl': 'https://other.example/feed', 'trackName': 'Gespräch mit Peter Decker', 'episodeUrl': 'https://other.example/1.mp3', 'episodeGuid': 'g1', 'releaseDate': '2026-01-02T00:00:00Z', 'trackTimeMillis': 60000}, {'collectionName': 'Mein Podcast', 'feedUrl': 'https://mine.example/feed', 'trackName': 'Mit Peter Decker', 'episodeUrl': 'https://mine.example/1.mp3'}, {'collectionName': 'Fremd', 'feedUrl': 'https://other.example/feed', 'trackName': 'Peter Pan', 'episodeUrl': 'https://other.example/2.mp3'}, ]} def fake(url, **params): return apple if 'itunes' in url else {'data': []} with patch.object(discover, '_get_json', fake): r = self.client.get('/podcasts/discover/elsewhere/?name=Peter Decker').json() self.assertEqual([e['title'] for e in r['episodes']], ['Gespräch mit Peter Decker']) self.assertEqual(r['episodes'][0]['duration_seconds'], 60) def test_take_subscribes_auf_abruf_and_merklists(self): item = {'podcast': 'Fremd', 'feed_url': 'https://other.example/feed', 'title': 'Gespräch mit Peter Decker', 'audio_url': 'https://other.example/1.mp3', 'guid': 'g1', 'pub_date': '2026-01-02T00:00:00Z', 'duration_seconds': 60} with patch('podcasts.views._refresh_feed', side_effect=OSError('offline')): r = self.client.post('/podcasts/discover/take/', json.dumps(item), content_type='application/json').json() self.assertTrue(r['subscribed']) feed = PodcastFeed.objects.get(pk=r['feed_id']) self.assertEqual(feed.new_episodes, PodcastFeed.NEW_IGNORE) self.assertTrue(PodcastLater.objects.filter(user=self.user, episode_id=r['episode_id']).exists()) with patch('podcasts.views._refresh_feed', side_effect=OSError('offline')): r2 = self.client.post('/podcasts/discover/take/', json.dumps(item), content_type='application/json').json() self.assertEqual((r2['episode_id'], r2['subscribed']), (r['episode_id'], False)) def test_recommend_counts_and_leaves_out_mine(self): EpisodeProgress.objects.create( user=self.user, played=True, episode=PodcastEpisode.objects.create(feed=PodcastFeed.objects.get(title='Mein Podcast'), guid='m', title='M', audio_url='https://mine.example/m.mp3')) def fake(url, **params): if url.endswith('search/podcast'): return {'data': [{'id': 7, 'title': 'Mein Podcast', 'xmlURL': 'https://mine.example/feed'}]} if url.endswith('recommend'): return {'data': [{'id': 8, 'title': 'Neu', 'xmlURL': 'https://new.example/feed'}, {'id': 7, 'title': 'Mein Podcast', 'xmlURL': 'https://mine.example/feed'}]} return None with patch.object(discover, '_get_json', fake): r = self.client.get('/podcasts/discover/recommend/').json() self.assertEqual([(p['title'], p['because']) for p in r['podcasts']], [('Neu', ['Mein Podcast'])])