Skip to content

Commit dc253a0

Browse files
authored
feat: Upload captions (#483)
* feat: Upload captions * iterate - Trying to fetch directly from the API is too dangerous at this point, since the API is blocked - I've chosen to iterate on a GraphQL subtitles upload API - Trying the GraphQL mutation with Absinthe, I got a "No query document supplied" error - ```gql mutation SetVideoCaptions($captions: Upload!) { setVideoCaptions(videoId: 1, captions: $captions) { captions { text } } } ``` - Decided to upgrade Absinthe to see how that goes - [absinthe_ecto](https://github.com/absinthe-graphql/absinthe_ecto) is deprecated, so I'm looking to move to [dataloader](https://github.com/absinthe-graphql/dataloader) - Updated the dependencies, now looking to migrate the actual `assoc` calls * misc fixes
1 parent 9312bf7 commit dc253a0

33 files changed

+225
-121
lines changed

apps/cf/lib/actions/action_creator.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule CF.Actions.ActionCreator do
1010
alias DB.Schema.Speaker
1111
alias DB.Schema.Statement
1212
alias DB.Schema.Comment
13+
alias DB.Schema.VideoCaption
1314
alias CF.Actions.ReputationChange
1415

1516
# Create
@@ -175,6 +176,19 @@ defmodule CF.Actions.ActionCreator do
175176
)
176177
end
177178

179+
def action_upload_video_captions(user_id, video_id, caption = %VideoCaption{}) do
180+
action(
181+
user_id,
182+
:video_caption,
183+
:upload,
184+
video_id: video_id,
185+
changes: %{
186+
"format" => caption.format,
187+
"parsed" => caption.parsed
188+
}
189+
)
190+
end
191+
178192
def action_revert_vote(user_id, video_id, vote_type, comment = %Comment{})
179193
when vote_type in [:revert_vote_up, :revert_vote_down, :revert_self_vote] do
180194
action(

apps/cf/lib/videos/captions_fetcher_youtube.ex

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,7 @@ defmodule CF.Videos.CaptionsFetcherYoutube do
6969
end
7070

7171
defp process_transcript(transcript) do
72-
transcript
73-
|> SweetXml.xpath(
74-
~x"//transcript/text"l,
75-
text: ~x"./text()"s |> transform_by(&clean_text/1),
76-
start: ~x"./@start"s |> transform_by(&parse_float/1),
77-
duration: ~x"./@dur"os |> transform_by(&parse_float/1)
78-
)
79-
|> Enum.filter(fn %{text: text, start: start} ->
80-
start != nil and text != nil and text != ""
81-
end)
82-
end
83-
84-
defp clean_text(text) do
85-
text
86-
|> String.replace("&", "&")
87-
|> HtmlEntities.decode()
88-
|> String.trim()
89-
end
90-
91-
defp parse_float(val) do
92-
case Float.parse(val) do
93-
{num, _} -> num
94-
_ -> nil
95-
end
72+
CF.Videos.CaptionsSrv1Parser.parse_file(transcript)
9673
end
9774

9875
# Below is an implementation using the official YouTube API, but it requires OAuth2 authentication.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule CF.Videos.CaptionsSrv1Parser do
2+
@moduledoc """
3+
A captions parser for the srv1 format.
4+
"""
5+
6+
require Logger
7+
import SweetXml
8+
9+
def parse_file(content) do
10+
content
11+
|> SweetXml.xpath(
12+
~x"//transcript/text"l,
13+
text: ~x"./text()"s |> transform_by(&clean_text/1),
14+
start: ~x"./@start"s |> transform_by(&parse_float/1),
15+
duration: ~x"./@dur"os |> transform_by(&parse_float/1)
16+
)
17+
|> Enum.filter(fn %{text: text, start: start} ->
18+
# Filter out text in brackets, like "[Music]"
19+
start != nil and text != nil and text != "" and
20+
String.match?(text, ~r/^\[.*\]$/) == false
21+
end)
22+
end
23+
24+
defp clean_text(text) do
25+
text
26+
|> String.replace("&", "&")
27+
|> HtmlEntities.decode()
28+
|> String.trim()
29+
end
30+
31+
defp parse_float(val) do
32+
case Float.parse(val) do
33+
{num, _} -> num
34+
_ -> nil
35+
end
36+
end
37+
end

apps/cf/test/comments/comments_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ defmodule CF.Comments.CommentsTest do
141141
Comments.delete_comment(random_user, comment.statement.video_id, comment)
142142
end
143143

144-
refute_deleted(comment)
144+
assert_not_deleted(comment)
145145

146146
Comments.delete_comment(comment.user, comment.statement.video_id, comment)
147147
assert_deleted(comment)
@@ -168,7 +168,7 @@ defmodule CF.Comments.CommentsTest do
168168
Comments.delete_comment(comment.user, comment.statement.video_id, comment)
169169
end
170170

171-
refute_deleted(comment)
171+
assert_not_deleted(comment)
172172
end
173173

174174
test "but an admin can" do

apps/cf/test/support/test_utils.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ defmodule CF.TestUtils do
3636
end
3737
end
3838

39-
def refute_deleted(%Comment{id: id}) do
39+
def assert_not_deleted(%Comment{id: id}) do
4040
{comment, _} = get_comment_and_actions(id)
4141
assert comment != nil
4242

apps/cf_graphql/lib/endpoint.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ defmodule CF.GraphQLWeb.Endpoint do
1313

1414
plug(
1515
Plug.Parsers,
16-
parsers: [:urlencoded, :multipart, :json],
16+
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
1717
pass: ["*/*"],
18-
json_decoder: Poison
18+
json_decoder: Jason
1919
)
2020

2121
plug(Plug.MethodOverride)

apps/cf_graphql/lib/resolvers/videos.ex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule CF.Graphql.Resolvers.Videos do
88
import Ecto.Query
99
import Absinthe.Resolution.Helpers, only: [batch: 3]
1010

11+
alias Ecto.Multi
1112
alias DB.Repo
1213
alias DB.Schema.Video
1314
alias DB.Schema.VideoCaption
@@ -137,4 +138,48 @@ defmodule CF.Graphql.Resolvers.Videos do
137138
{:error, "Failed to update video"}
138139
end
139140
end
141+
142+
def set_captions(
143+
_root,
144+
%{video_id: video_id, captions: %{path: path, content_type: content_type}},
145+
%{
146+
context: %{user: user}
147+
}
148+
) do
149+
cond do
150+
content_type != "text/xml" ->
151+
{:error, "Unsupported content type: #{content_type}"}
152+
153+
File.stat!(path).size > 10_000_000 ->
154+
{:error, "File must be < 10MB"}
155+
156+
true ->
157+
video = DB.Repo.get!(DB.Schema.Video, video_id)
158+
captions_file_content = File.read!(path)
159+
parsed = CF.Videos.CaptionsSrv1Parser.parse_file(captions_file_content)
160+
161+
Multi.new()
162+
|> Multi.insert(
163+
:caption,
164+
VideoCaption.changeset(%VideoCaption{
165+
video_id: video.id,
166+
raw: captions_file_content,
167+
parsed: parsed,
168+
format: "xml"
169+
})
170+
)
171+
|> Multi.run(
172+
:action,
173+
fn _repo, %{caption: caption} ->
174+
CF.Actions.ActionCreator.action_upload_video_captions(user.id, video.id, caption)
175+
|> DB.Repo.insert!()
176+
177+
{:ok, caption}
178+
end
179+
)
180+
|> DB.Repo.transaction()
181+
182+
{:ok, video}
183+
end
184+
end
140185
end

apps/cf_graphql/lib/schema/input_objects.ex

Lines changed: 0 additions & 8 deletions
This file was deleted.

apps/cf_graphql/lib/schema/input_objects/statement_filter.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ defmodule CF.Graphql.Schema.InputObjects.StatementFilter do
44
"""
55

66
use Absinthe.Schema.Notation
7-
use Absinthe.Ecto, repo: DB.Repo
87

98
@desc "Props to filter statements on"
109
input_object :statement_filter do

apps/cf_graphql/lib/schema/input_objects/video_filter.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ defmodule CF.Graphql.Schema.InputObjects.VideoFilter do
44
"""
55

66
use Absinthe.Schema.Notation
7-
use Absinthe.Ecto, repo: DB.Repo
87

98
@desc "Props to filter videos on"
109
input_object :video_filter do

0 commit comments

Comments
 (0)