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

apps/cf_graphql/lib/schema/schema.ex

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,42 @@ defmodule CF.Graphql.Schema do
22
use Absinthe.Schema
33
alias CF.Graphql.Resolvers
44
alias CF.Graphql.Schema.Middleware
5+
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
6+
7+
import_types(Absinthe.Plug.Types)
8+
9+
import_types(CF.Graphql.Schema.Types.{
10+
AppInfo,
11+
Comment,
12+
Notification,
13+
Paginated,
14+
Source,
15+
Speaker,
16+
Statement,
17+
Statistics,
18+
Subscription,
19+
UserAction,
20+
User,
21+
Video,
22+
VideoCaption
23+
})
24+
25+
import_types(CF.Graphql.Schema.InputObjects.{
26+
VideoFilter,
27+
StatementFilter
28+
})
29+
30+
def context(ctx) do
31+
loader =
32+
Dataloader.new()
33+
|> Dataloader.add_source(DB.Repo, Dataloader.Ecto.new(DB.Repo))
34+
35+
Map.put(ctx, :loader, loader)
36+
end
537

6-
import_types(CF.Graphql.Schema.Types)
7-
import_types(CF.Graphql.Schema.InputObjects)
38+
def plugins do
39+
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
40+
end
841

942
# Query API
1043

@@ -110,5 +143,15 @@ defmodule CF.Graphql.Schema do
110143

111144
resolve(&Resolvers.Videos.edit/3)
112145
end
146+
147+
field :set_video_captions, :video do
148+
middleware(Middleware.RequireAuthentication)
149+
middleware(Middleware.RequireReputation, 450)
150+
151+
arg(:video_id, non_null(:id))
152+
arg(:captions, non_null(:upload))
153+
154+
resolve(&Resolvers.Videos.set_captions/3)
155+
end
113156
end
114157
end

apps/cf_graphql/lib/schema/types.ex

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

apps/cf_graphql/lib/schema/types/app_info.ex

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

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

98
@desc "Information about the application"
109
object :app_info do

apps/cf_graphql/lib/schema/types/comment.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ defmodule CF.Graphql.Schema.Types.Comment do
44
"""
55

66
use Absinthe.Schema.Notation
7-
use Absinthe.Ecto, repo: DB.Repo
7+
8+
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
89
import CF.Graphql.Schema.Utils
910
alias CF.Graphql.Resolvers
1011

@@ -13,7 +14,7 @@ defmodule CF.Graphql.Schema.Types.Comment do
1314
field(:id, non_null(:id))
1415
@desc "User who made the comment"
1516
field :user, :user do
16-
resolve(assoc(:user))
17+
resolve(dataloader(DB.Repo))
1718
complexity(join_complexity())
1819
end
1920

@@ -31,7 +32,7 @@ defmodule CF.Graphql.Schema.Types.Comment do
3132

3233
@desc "Source of the scomment. If null, a text must be set"
3334
field :source, :source do
34-
resolve(assoc(:source))
35+
resolve(dataloader(DB.Repo))
3536
complexity(join_complexity())
3637
end
3738

apps/cf_graphql/lib/schema/types/notification.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ defmodule CF.Graphql.Schema.Types.Notification do
44
"""
55

66
use Absinthe.Schema.Notation
7-
use Absinthe.Ecto, repo: DB.Repo
8-
import CF.Graphql.Schema.Utils
97

10-
import_types(CF.Graphql.Schema.Types.Paginated)
8+
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9+
import CF.Graphql.Schema.Utils
1110

1211
@desc "A user notification"
1312
object :notification do
@@ -20,7 +19,7 @@ defmodule CF.Graphql.Schema.Types.Notification do
2019
field(:seen_at, :string)
2120
@desc "Action the notification is referencing"
2221
field :action, :user_action do
23-
resolve(assoc(:action))
22+
resolve(dataloader(DB.Repo))
2423
complexity(join_complexity())
2524
end
2625
end

apps/cf_graphql/lib/schema/types/paginated.ex

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

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

98
object :paginated do
109
field(:page_number, :integer)

apps/cf_graphql/lib/schema/types/source.ex

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

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

98
@desc "An URL pointing toward a source (article, video, pdf...)"
109
object :source do

apps/cf_graphql/lib/schema/types/speaker.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ defmodule CF.Graphql.Schema.Types.Speaker do
44
"""
55

66
use Absinthe.Schema.Notation
7-
use Absinthe.Ecto, repo: DB.Repo
7+
8+
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
9+
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
810
import CF.Graphql.Schema.Utils
911
alias CF.Graphql.Resolvers
1012

@@ -26,7 +28,7 @@ defmodule CF.Graphql.Schema.Types.Speaker do
2628
field(:picture, :string, do: resolve(&Resolvers.Speakers.picture/3))
2729
@desc "List of speaker's videos"
2830
field :videos, list_of(:video) do
29-
resolve(assoc(:videos))
31+
resolve(dataloader(DB.Repo))
3032
complexity(join_complexity())
3133
end
3234
end

0 commit comments

Comments
 (0)