add livestream tracking

This commit is contained in:
byte[] 2019-11-30 01:30:45 -05:00
parent 4796d3d628
commit 1e1b0054d0
11 changed files with 253 additions and 63 deletions

View file

@ -18,6 +18,7 @@ config :philomena,
advert_url_root: "/spns", advert_url_root: "/spns",
badge_url_root: "/media", badge_url_root: "/media",
tag_url_root: "/media", tag_url_root: "/media",
channel_url_root: "/media",
image_file_root: "priv/static/system/images", image_file_root: "priv/static/system/images",
cdn_host: "", cdn_host: "",
proxy_host: nil, proxy_host: nil,

View file

@ -16,6 +16,7 @@ config :bcrypt_elixir,
config :philomena, config :philomena,
anonymous_name_salt: System.get_env("ANONYMOUS_NAME_SALT"), anonymous_name_salt: System.get_env("ANONYMOUS_NAME_SALT"),
channel_url_root: System.get_env("CHANNEL_URL_ROOT"),
password_pepper: System.get_env("PASSWORD_PEPPER"), password_pepper: System.get_env("PASSWORD_PEPPER"),
avatar_url_root: System.get_env("AVATAR_URL_ROOT"), avatar_url_root: System.get_env("AVATAR_URL_ROOT"),
advert_url_root: System.get_env("ADVERT_URL_ROOT"), advert_url_root: System.get_env("ADVERT_URL_ROOT"),

View file

@ -7,6 +7,7 @@ defmodule Philomena.Channels do
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Channels.Channel alias Philomena.Channels.Channel
alias Philomena.Notifications
@doc """ @doc """
Returns the list of channels. Returns the list of channels.
@ -104,35 +105,6 @@ defmodule Philomena.Channels do
alias Philomena.Channels.Subscription alias Philomena.Channels.Subscription
@doc """
Returns the list of channel_subscriptions.
## Examples
iex> list_channel_subscriptions()
[%Subscription{}, ...]
"""
def list_channel_subscriptions do
Repo.all(Subscription)
end
@doc """
Gets a single subscription.
Raises `Ecto.NoResultsError` if the Subscription does not exist.
## Examples
iex> get_subscription!(123)
%Subscription{}
iex> get_subscription!(456)
** (Ecto.NoResultsError)
"""
def get_subscription!(id), do: Repo.get!(Subscription, id)
@doc """ @doc """
Creates a subscription. Creates a subscription.
@ -145,28 +117,11 @@ defmodule Philomena.Channels do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_subscription(attrs \\ %{}) do def create_subscription(_channel, nil), do: {:ok, nil}
%Subscription{} def create_subscription(channel, user) do
|> Subscription.changeset(attrs) %Subscription{channel_id: channel.id, user_id: user.id}
|> Repo.insert() |> Subscription.changeset(%{})
end |> Repo.insert(on_conflict: :nothing)
@doc """
Updates a subscription.
## Examples
iex> update_subscription(subscription, %{field: new_value})
{:ok, %Subscription{}}
iex> update_subscription(subscription, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_subscription(%Subscription{} = subscription, attrs) do
subscription
|> Subscription.changeset(attrs)
|> Repo.update()
end end
@doc """ @doc """
@ -181,20 +136,22 @@ defmodule Philomena.Channels do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def delete_subscription(%Subscription{} = subscription) do def delete_subscription(channel, user) do
Repo.delete(subscription) clear_notification(channel, user)
%Subscription{channel_id: channel.id, user_id: user.id}
|> Repo.delete()
end end
@doc """ def subscribed?(_channel, nil), do: false
Returns an `%Ecto.Changeset{}` for tracking subscription changes. def subscribed?(channel, user) do
Subscription
|> where(channel_id: ^channel.id, user_id: ^user.id)
|> Repo.exists?()
end
## Examples def clear_notification(channel, user) do
Notifications.delete_unread_notification("Channel", channel.id, user)
iex> change_subscription(subscription) Notifications.delete_unread_notification("LivestreamChannel", channel.id, user)
%Ecto.Changeset{source: %Subscription{}}
"""
def change_subscription(%Subscription{} = subscription) do
Subscription.changeset(subscription, %{})
end end
end end

View file

@ -0,0 +1,30 @@
defmodule PhilomenaWeb.ChannelController do
use PhilomenaWeb, :controller
alias Philomena.Channels
alias Philomena.Channels.Channel
alias Philomena.Repo
import Ecto.Query
plug :load_resource, model: Channel
def index(conn, _params) do
channels =
Channel
|> where([c], c.nsfw == false and not is_nil(c.last_fetched_at))
|> order_by(desc: :is_live, asc: :title)
|> preload(:associated_artist_tag)
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", layout_class: "layout--wide", channels: channels)
end
def show(conn, _params) do
channel = conn.assigns.channel
user = conn.assigns.current_user
if user, do: Channels.clear_notification(channel, user)
redirect(conn, external: channel.url)
end
end

View file

@ -0,0 +1,18 @@
defmodule PhilomenaWeb.ChannelPlug do
alias Plug.Conn
alias Philomena.Channels.Channel
alias Philomena.Repo
import Ecto.Query
def init([]), do: []
def call(conn, _opts) do
live_channels =
Channel
|> where(is_live: true)
|> Repo.aggregate(:count, :id)
conn
|> Conn.assign(:live_channels, live_channels)
end
end

View file

@ -18,6 +18,7 @@ defmodule PhilomenaWeb.Router do
plug PhilomenaWeb.SiteNoticePlug plug PhilomenaWeb.SiteNoticePlug
plug PhilomenaWeb.ForumListPlug plug PhilomenaWeb.ForumListPlug
plug PhilomenaWeb.FilterSelectPlug plug PhilomenaWeb.FilterSelectPlug
plug PhilomenaWeb.ChannelPlug
end end
pipeline :api do pipeline :api do
@ -134,6 +135,7 @@ defmodule PhilomenaWeb.Router do
resources "/dnp", DnpEntryController, only: [:index, :show] resources "/dnp", DnpEntryController, only: [:index, :show]
resources "/staff", StaffController, only: [:index] resources "/staff", StaffController, only: [:index]
resources "/stats", StatController, only: [:index] resources "/stats", StatController, only: [:index]
resources "/channels", ChannelController, only: [:index, :show]
get "/:id", ImageController, :show get "/:id", ImageController, :show
# get "/:forum_id", ForumController, :show # impossible to do without constraints # get "/:forum_id", ForumController, :show # impossible to do without constraints

View file

@ -0,0 +1,34 @@
- link_class = "media-box__header media-box__header--channel media-box__header--link"
.media-box
a.media-box__header.media-box__header--channel.media-box__header--link href=Routes.channel_path(@conn, :show, @channel) title=@channel.title
= @channel.title || @channel.short_name
.media-box__header.media-box__header--channel
= if @channel.is_live do
.spacing-right.label.label--success.label--block.label--small: strong LIVE NOW
=> @channel.viewers
=> pluralize "viewer", "viewers", @channel.viewers
- else
.label.label--danger.label--block.label--small: strong OFF AIR
= if @channel.nsfw do
.media-box__overlay
| NSFW
.media-box__content.media-box__content--channel
a href=Routes.channel_path(@conn, :show, @channel)
.image-constrained.media-box__content--channel
img src=channel_image(@channel) alt="#{@channel.title}"
= if @channel.associated_artist_tag do
a href=Routes.tag_path(@conn, :show, @channel.associated_artist_tag) class=link_class
i.fa.fa-fw.fa-tags>
= @channel.associated_artist_tag.name
- else
.media-box__header.media-box__header--channel No artist tag
/- if current_user
/ = subscription_link channel, current_user, class: link_class
/- else
/ .media-box__header.media-box__header--channel Login to subscribe

View file

@ -0,0 +1,30 @@
h1 Livestreams
- route = fn p -> Routes.channel_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @channels, route: route, conn: @conn
/= form_tag channels_path, method: :get, class: 'hform', enforce_utf8: false do
.field
= text_field_tag :cq, params[:cq], class: 'input hform__text', placeholder: 'Search channels'
= submit_tag 'Search', class: 'hform__button button'
.block
.block__header
= pagination
.block__content
= for channel <- @channels do
= render PhilomenaWeb.ChannelView, "_channel_box.html", channel: channel, conn: @conn
.block__header
= pagination
br
.clearfix &nbsp;
h2 FAQ
p
strong> Q: Do you host streams?
| A: No, we cheat and just link to streams on Livestream/Picarto since that's where (almost) everyone is already. This is simply a nice way to track streaming artists.
p
strong> Q: How do I get my stream/a friend's stream/<artist>'s stream here?
' A: Send a private message to a site administrator
' with a link to the stream and the artist tag if applicable.

View file

@ -30,6 +30,11 @@
| Post Search | Post Search
a.header__link href="/tags" a.header__link href="/tags"
| Tags | Tags
a.header__link href="/channels"
' Live
span.header__counter
= @conn.assigns.live_channels
a.header__link href='/galleries' a.header__link href='/galleries'
| Galleries | Galleries
a.header__link href='/commissions' a.header__link href='/commissions'

View file

@ -0,0 +1,24 @@
defmodule PhilomenaWeb.ChannelView do
use PhilomenaWeb, :view
def channel_image(%{channel_image: image, is_live: false}) when image not in [nil, ""],
do: channel_url_root() <> "/" <> image
def channel_image(%{type: "LivestreamChannel", short_name: short_name}) do
now = DateTime.utc_now() |> DateTime.to_unix(:microsecond)
Camo.Image.image_url("https://thumbnail.api.livestream.com/thumbnail?name=#{short_name}&rand=#{now}")
end
def channel_image(%{type: "PicartoChannel", thumbnail_url: thumbnail_url}),
do: Camo.Image.image_url(thumbnail_url || "https://picarto.tv/images/missingthumb.jpg")
def channel_image(%{type: "PiczelChannel", remote_stream_id: remote_stream_id}),
do: Camo.Image.image_url("https://piczel.tv/api/thumbnail/stream_#{remote_stream_id}.jpg")
def channel_image(%{type: "TwitchChannel", short_name: short_name}),
do: Camo.Image.image_url("https://static-cdn.jtvnw.net/previews-ttv/live_user_#{String.downcase(short_name)}-320x180.jpg")
defp channel_url_root do
Application.get_env(:philomena, :channel_url_root)
end
end

View file

@ -0,0 +1,88 @@
defmodule PhilomenaWeb.ChannelControllerTest do
use PhilomenaWeb.ConnCase
alias Philomena.Channels
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
def fixture(:channel) do
{:ok, channel} = Channels.create_channel(@create_attrs)
channel
end
describe "index" do
test "lists all channels", %{conn: conn} do
conn = get(conn, Routes.channel_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Channels"
end
end
describe "new channel" do
test "renders form", %{conn: conn} do
conn = get(conn, Routes.channel_path(conn, :new))
assert html_response(conn, 200) =~ "New Channel"
end
end
describe "create channel" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, Routes.channel_path(conn, :create), channel: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.channel_path(conn, :show, id)
conn = get(conn, Routes.channel_path(conn, :show, id))
assert html_response(conn, 200) =~ "Show Channel"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.channel_path(conn, :create), channel: @invalid_attrs)
assert html_response(conn, 200) =~ "New Channel"
end
end
describe "edit channel" do
setup [:create_channel]
test "renders form for editing chosen channel", %{conn: conn, channel: channel} do
conn = get(conn, Routes.channel_path(conn, :edit, channel))
assert html_response(conn, 200) =~ "Edit Channel"
end
end
describe "update channel" do
setup [:create_channel]
test "redirects when data is valid", %{conn: conn, channel: channel} do
conn = put(conn, Routes.channel_path(conn, :update, channel), channel: @update_attrs)
assert redirected_to(conn) == Routes.channel_path(conn, :show, channel)
conn = get(conn, Routes.channel_path(conn, :show, channel))
assert html_response(conn, 200)
end
test "renders errors when data is invalid", %{conn: conn, channel: channel} do
conn = put(conn, Routes.channel_path(conn, :update, channel), channel: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Channel"
end
end
describe "delete channel" do
setup [:create_channel]
test "deletes chosen channel", %{conn: conn, channel: channel} do
conn = delete(conn, Routes.channel_path(conn, :delete, channel))
assert redirected_to(conn) == Routes.channel_path(conn, :index)
assert_error_sent 404, fn ->
get(conn, Routes.channel_path(conn, :show, channel))
end
end
end
defp create_channel(_) do
channel = fixture(:channel)
{:ok, channel: channel}
end
end