from django.contrib.auth.models import User from django.db import models class PodcastFeed(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='podcast_feeds') rss_url = models.URLField(max_length=1000) title = models.CharField(max_length=300) description = models.TextField(blank=True) artwork_url = models.URLField(max_length=1000, blank=True) author = models.CharField(max_length=300, blank=True) link = models.URLField(max_length=1000, blank=True) last_refreshed_at = models.DateTimeField(null=True, blank=True) added_at = models.DateTimeField(auto_now_add=True) auto_queue = models.BooleanField(default=False) class Meta: unique_together = ('user', 'rss_url') ordering = ['title'] def __str__(self): return self.title class PodcastEpisode(models.Model): feed = models.ForeignKey(PodcastFeed, on_delete=models.CASCADE, related_name='episodes') guid = models.CharField(max_length=1000) title = models.CharField(max_length=500) description = models.TextField(blank=True) audio_url = models.URLField(max_length=1000) duration_seconds = models.IntegerField(default=0) pub_date = models.DateTimeField(null=True, blank=True) episode_number = models.IntegerField(null=True, blank=True) season_number = models.IntegerField(null=True, blank=True) artwork_url = models.URLField(max_length=1000, blank=True) discovered_at = models.DateTimeField(auto_now_add=True) # Podcasting 2.0 : a JSON file, loaded on demand. chapters_url = models.URLField(max_length=1000, blank=True) # [{start: seconds, title, url?, img?}], from Podlove Simple Chapters in # the feed, or once fetched: the JSON at chapters_url, else (or if that # has none) the CHAP frames of the audio file's ID3v2 tag. chapters = models.JSONField(default=list, blank=True) # When chapters were fetched into `chapters` (from chapters_url or the # audio file); null = not yet. chapters_fetched_at = models.DateTimeField(null=True, blank=True) # [{url, type, language}] from . transcripts = models.JSONField(default=list, blank=True) class Meta: unique_together = ('feed', 'guid') ordering = ['-pub_date'] def __str__(self): return self.title class EpisodeTranscript(models.Model): """An episode's transcript, fetched and parsed once (transcripts.py). A table of its own rather than fields on PodcastEpisode: a parsed transcript runs to hundreds of KB, and episode lists load whole rows. """ episode = models.OneToOneField(PodcastEpisode, on_delete=models.CASCADE, related_name='transcript_cache') # URLs of the transcripts it was made from, best first. When the feed # offers different ones, the cache is stale. sources = models.JSONField(default=list) # {"segments": [...], "source": {...}}, or null with an error. data = models.JSONField(null=True, blank=True) # '' when data is there; else why there's none for good: 'unsupported' # (no format with timing), 'invalid' (missing, broken, too large). error = models.CharField(max_length=20, blank=True) fetched_at = models.DateTimeField(auto_now=True) def __str__(self): return f'Transcript of {self.episode}' # An episode counts as played this close to its end (AntennaPod's default), # the same rule as the apps' "als gehört zählen". PLAYED_BEFORE_END_SECONDS = 30 def counts_as_played(position_seconds, duration_seconds): return bool(duration_seconds) and duration_seconds > 0 and \ position_seconds >= max(duration_seconds - PLAYED_BEFORE_END_SECONDS, 1) class EpisodeProgress(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='episode_progress') episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='progress') position_seconds = models.IntegerField(default=0) played = models.BooleanField(default=False) dismissed = models.BooleanField(default=False) favorite = models.BooleanField(default=False) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ('user', 'episode') ordering = ['-updated_at'] def __str__(self): return f'{self.user} - {self.episode}' class PodcastQueue(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='podcast_queue') episode = models.ForeignKey(PodcastEpisode, on_delete=models.CASCADE, related_name='queued_by') position = models.PositiveIntegerField(default=0) added_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'episode') ordering = ['position'] def __str__(self): return f'{self.user} queue pos {self.position}: {self.episode}' class PodcastSettings(models.Model): """A user's podcast preferences. Where episodes go in the queue: when added by hand (also when played from outside the queue), and when a feed with auto_queue brings new ones.""" BACK = 'back' FRONT = 'front' AFTER_CURRENT = 'after_current' POSITIONS = [ (BACK, 'Ans Ende'), (FRONT, 'An den Anfang'), (AFTER_CURRENT, 'Nach der laufenden Folge'), ] user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='podcast_settings') enqueue_position = models.CharField(max_length=20, choices=POSITIONS, default=BACK) # front = how auto-queue always worked before this setting existed auto_queue_position = models.CharField(max_length=20, choices=POSITIONS, default=FRONT) @classmethod def for_user(cls, user): obj, _ = cls.objects.get_or_create(user=user) return obj def __str__(self): return f'Podcast settings of {self.user}'