mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
add livestream tracking
This commit is contained in:
parent
4796d3d628
commit
1e1b0054d0
11 changed files with 253 additions and 63 deletions
|
@ -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,
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
30
lib/philomena_web/controllers/channel_controller.ex
Normal file
30
lib/philomena_web/controllers/channel_controller.ex
Normal 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
|
18
lib/philomena_web/plugs/channel_plug.ex
Normal file
18
lib/philomena_web/plugs/channel_plug.ex
Normal 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
|
|
@ -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
|
||||||
|
|
34
lib/philomena_web/templates/channel/_channel_box.html.slime
Normal file
34
lib/philomena_web/templates/channel/_channel_box.html.slime
Normal 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
|
30
lib/philomena_web/templates/channel/index.html.slime
Normal file
30
lib/philomena_web/templates/channel/index.html.slime
Normal 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
|
||||||
|
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.
|
|
@ -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'
|
||||||
|
|
24
lib/philomena_web/views/channel_view.ex
Normal file
24
lib/philomena_web/views/channel_view.ex
Normal 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
|
88
test/philomena_web/controllers/channel_controller_test.exs
Normal file
88
test/philomena_web/controllers/channel_controller_test.exs
Normal 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
|
Loading…
Reference in a new issue