-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API episode feeds #1027
API episode feeds #1027
Changes from all commits
31557a4
fa2f119
c36d545
9a27851
a08e805
4b7d693
62d751c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
require "active_support/concern" | ||
|
||
module EpisodeHasFeeds | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
has_many :episodes_feeds, dependent: :delete_all | ||
has_many :feeds, through: :episodes_feeds | ||
|
||
after_initialize :set_default_feeds, if: :new_record? | ||
before_validation :set_default_feeds, if: :new_record? | ||
|
||
# TODO: this doesn't filter by display_episodes_count | ||
scope :in_feed, ->(feed) do | ||
Episode.joins(:episodes_feeds) | ||
.where(episodes_feeds: {feed: feed}) | ||
.published_by(feed.episode_offset_seconds.to_i) | ||
end | ||
|
||
# TODO: this doesn't filter by display_episodes_count | ||
scope :in_default_feed, -> { joins(:feeds).where(feeds: {slug: nil}).published } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not trivial to apply SELECT e.id, e.published_at
FROM (
SELECT id, podcast_id, published_at,
ROW_NUMBER() OVER (
PARTITION BY podcast_id ORDER BY published_at DESC
) AS row
FROM episodes
WHERE published_at <= NOW()
) e
INNER JOIN feeds f ON (f.slug IS NULL AND f.podcast_id = e.podcast_id)
WHERE (display_episodes_count IS NULL OR row <= display_episodes_count) That isn't exactly quick, when returning all podcasts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interesting, yeah, I see your point; I think that's probably fine not to worry about - it makes me wonder if we should get rid of the public /episodes endpoint - does anything/one use it? spooler should be authenticated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think that's the next thing we should decide after this PR. What's the utility of our public endpoints? Just to provide a JSON version of what's in public RSS? Or something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say to provide a json version of the public feed and individual public episodes, and I am fairly convinced it should mirror the public feed in the scope of what data it provides, but as a JSON API, and this gets us much much closer to that |
||
end | ||
|
||
def set_default_feeds | ||
if feeds.blank? | ||
self.feeds = (podcast&.feeds || []).filter_map do |feed| | ||
feed if feed.default? || feed.apple? | ||
end | ||
end | ||
end | ||
|
||
# TODO: this doesn't filter by display_episodes_count | ||
def in_feed?(feed) | ||
published_by?(feed.episode_offset_seconds.to_i) && episodes_feeds.where(feed: feed).any? | ||
end | ||
|
||
# TODO: this doesn't filter by display_episodes_count | ||
def in_default_feed? | ||
published? && feeds.where(slug: nil).any? | ||
end | ||
|
||
def feed_slugs | ||
feeds.pluck(:slug).map { |s| s || "default" } | ||
end | ||
|
||
def feed_slugs=(slugs) | ||
self.feeds = (podcast&.feeds || []).filter_map do |feed| | ||
feed if slugs.try(:include?, feed.slug || "default") | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,11 +41,12 @@ class Feed < ApplicationRecord | |
|
||
validates :slug, uniqueness: {scope: :podcast_id}, if: :podcast_id? | ||
validates_format_of :slug, allow_nil: true, with: /\A[0-9a-zA-Z_-]+\z/ | ||
validates_format_of :slug, without: /\A(images|\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\z/ | ||
validates_format_of :slug, without: /\A(default|images|\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\z/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I'm using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lots of back and forth on this in slack - I have no strong opinion, but I am fine with owning the :default as a namespace |
||
validates :file_name, presence: true, format: {with: /\A[0-9a-zA-Z_.-]+\z/} | ||
validates :include_zones, placement_zones: true | ||
validates :audio_format, audio_format: true | ||
validates :title, presence: true, unless: :default? | ||
validates :episode_offset_seconds, numericality: {equal_to: 0}, allow_nil: true, if: :default? | ||
kookster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
validates :url, http_url: true | ||
validates :new_feed_url, http_url: true | ||
validates :enclosure_prefix, http_url: true | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
require "test_helper" | ||
|
||
class EpisodeHasFeedsTest < ActiveSupport::TestCase | ||
let(:podcast) { create(:podcast) } | ||
let(:f1) { podcast.default_feed } | ||
let(:f2) { create(:feed, podcast: podcast, slug: "feed2") } | ||
let(:f3) { create(:feed, podcast: podcast, slug: "feed3") } | ||
let(:episode) { create(:episode, podcast: podcast) } | ||
|
||
before { assert [f1, f2, f3] } | ||
|
||
describe ".in_feed" do | ||
it "returns episodes in a feed" do | ||
e2 = create(:episode, podcast: podcast, feeds: [f2]) | ||
e3 = create(:episode, podcast: podcast, feeds: [f2]) | ||
assert_equal [e2, e3], Episode.in_feed(f2).order(id: :asc) | ||
|
||
# only includes published | ||
e2.update(published_at: 1.hour.from_now) | ||
assert_equal [e3], Episode.in_feed(f2).order(id: :asc) | ||
|
||
# does apply offsets | ||
f2.episode_offset_seconds = -3601 | ||
assert_equal [e2, e3], Episode.in_feed(f2).order(id: :asc) | ||
|
||
# does NOT apply limits | ||
f2.display_episodes_count = 1 | ||
assert_equal [e2, e3], Episode.in_feed(f2).order(id: :asc) | ||
end | ||
end | ||
|
||
describe ".in_default_feed" do | ||
it "returns episodes in the default feed" do | ||
assert_equal [episode], Episode.in_default_feed | ||
|
||
# unpublished episodes not in default feed | ||
episode.update(published_at: 1.hour.from_now) | ||
assert_empty Episode.in_default_feed | ||
|
||
# or episodes in other feeds | ||
episode.update(published_at: 1.hour.ago, feeds: []) | ||
assert_empty Episode.in_default_feed | ||
end | ||
end | ||
|
||
describe "#in_feed?" do | ||
it "checks if an episode is in a feed" do | ||
assert episode.in_feed?(f1) | ||
refute episode.in_feed?(f2) | ||
refute episode.in_feed?(f3) | ||
|
||
episode.published_at = 1.minute.from_now | ||
refute episode.in_feed?(f1) | ||
|
||
f1.episode_offset_seconds = -61 | ||
assert episode.in_feed?(f1) | ||
end | ||
end | ||
|
||
describe "#in_default_feed?" do | ||
it "checks if an episode is in the default feed" do | ||
assert episode.in_default_feed? | ||
|
||
episode.published_at = nil | ||
refute episode.in_default_feed? | ||
|
||
episode.published_at = 1.minute.ago | ||
episode.feeds = [f2] | ||
refute episode.in_default_feed? | ||
end | ||
end | ||
|
||
describe "#set_default_feeds" do | ||
it "sets default feeds on new episodes" do | ||
# saved episodes get default+apple feeds | ||
f3.update(type: "Feeds::AppleSubscription") | ||
assert_equal [f1.id, f3.id], episode.feeds.map(&:id).sort | ||
|
||
# new episodes initialized with defaults | ||
e2 = podcast.episodes.new | ||
assert_equal [f1.id, f3.id], e2.feeds.map(&:id).sort | ||
|
||
# UNLESS episode already has feeds | ||
e2 = podcast.episodes.new(feeds: [f2]) | ||
assert_equal [f2.id], e2.feeds.map(&:id) | ||
end | ||
end | ||
|
||
describe "#feed_slugs" do | ||
it "gets and sets feeds based on their slugs" do | ||
assert_equal [f1], episode.feeds | ||
assert_equal ["default"], episode.feed_slugs | ||
|
||
episode.feed_slugs = ["default", "whatev", "feed3"] | ||
assert_equal [f1, f3], episode.feeds | ||
|
||
episode.feed_slugs = ["feed3"] | ||
assert_equal [f3], episode.feeds | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very clean, makes sense