Skip to content

Instantly share code, notes, and snippets.

@maxlapshin
Created February 28, 2018 15:42
Show Gist options
  • Save maxlapshin/f9880426be9eb9063d07c64b43869093 to your computer and use it in GitHub Desktop.
Save maxlapshin/f9880426be9eb9063d07c64b43869093 to your computer and use it in GitHub Desktop.
-module(config2_lexer).
-export([tokens/1, ast/1, ast2/1, maybe_int/1]).
-export([parse/1, parse/2]).
-export([cidr/1]).
parse(Bin) ->
parse(Bin, #{}).
parse(Bin, #{} = Options) when is_binary(Bin) ->
try ast3(Bin, Options)
catch
throw:E -> E;
error:E -> {error, {E, erlang:get_stacktrace()}}
end.
cidr(Bin) ->
ast2_parse_ipcidr(Bin, 0).
tokens(Path) when is_list(Path) ->
case file:read_file(Path) of
{ok, Bin} ->
tokens(Bin);
{error, E} ->
{error, E}
end;
tokens(Bin) ->
config(Bin, 1).
ast(Bin) ->
Tokens = tokens(Bin),
if
is_list(Tokens) ->
Tokens1 = [T || T <- Tokens, element(1,T) =/= comment],
ast_top(Tokens1);
true ->
Tokens
end.
% ast2 уже знает про конкретные опции и знает, когда и кто каким может быть,
% но идет строго по одной строчке
ast2(Bin) ->
AST1 = ast(Bin),
ast2_top(AST1).
% ast3 склеивает соседние строчки в более сложные структуры
ast3(Bin, Options) ->
AST2 = ast2(Bin),
run_ast3_transform(AST2, Options).
config(<<>>, _) -> [];
config(<<" ", Bin/binary>>, L) -> config(Bin, L);
config(<<"\t", Bin/binary>>, L) -> config(Bin, L);
config(<<"\n", Bin/binary>>, L) -> config(Bin, L+1);
config(<<"\r", Bin/binary>>, L) -> config(Bin, L);
config(<<";", Bin/binary>>, L) -> config(Bin, L);
config(<<"#", Bin/binary>>, L) -> comment(Bin, L, <<>>);
config(<<"%", Bin/binary>>, L) -> comment(Bin, L, <<>>);
config(<<"//", Bin/binary>>, L) -> comment(Bin, L, <<>>);
config(<<"}", Bin/binary>>, L) -> [{'}', L}|config(Bin, L)];
config(<<C, Bin/binary>>, L) when C >= $a andalso C =< $z ->
command(Bin, L, <<C>>);
config(<<C, _/binary>>, L) -> throw({error, {invalid_symbol,L,C}}).
comment(<<"\n", Bin/binary>>, L, Acc) -> [{comment, L, Acc}|config(Bin, L+1)];
comment(<<C, Bin/binary>>, L, Acc) -> comment(Bin, L, <<Acc/binary, C>>);
comment(<<>>, L, Acc) -> [{comment, L, Acc}].
command(<<"\n", Bin/binary>>, L, Acc) -> [{command, L, binary_to_atom(Acc,latin1)}|params(Bin, L+1)];
command(<<"{", _/binary>> = Bin, L, Acc) -> [{command, L, binary_to_atom(Acc,latin1)}|params(Bin, L)];
command(<<";", Bin/binary>>, L, Acc) -> [{command, L, binary_to_atom(Acc,latin1)},{';', L}|config(Bin, L)];
command(<<"}",_/binary>> = Bin, L, Acc) -> [{command, L, binary_to_atom(Acc,latin1)},{';', L}|config(Bin, L)];
command(<<C, Bin/binary>>, L, Acc) when C == $\t orelse C == $\r orelse C == $\s -> [{command, L, binary_to_atom(Acc,latin1)}|params(Bin, L)];
command(<<C, Bin/binary>>, L, Acc) -> command(Bin, L, <<Acc/binary, C>>);
command(<<>>, L, Acc) -> throw({error, {invalid_command,L,first20(Acc)}}).
first20(<<Acc:20/binary,Rest/binary>>) -> <<Acc/binary,"(",(integer_to_binary(size(Rest)))/binary," more bytes)">>;
first20(Acc) -> Acc.
params(<<"#", Bin/binary>>, L) -> comment(Bin, L, <<>>);
params(<<"{", Bin/binary>>, L) -> [{'{', L}|config(Bin, L)];
params(<<"}", Bin/binary>>, L) -> [{'}', L}|config(Bin, L)];
params(<<";", Bin/binary>>, L) -> [{';', L}|config(Bin, L)];
params(<<"\n", Bin/binary>>, L) -> params(Bin, L+1);
params(<<C, Bin/binary>>, L) when C == $\t orelse C == $\r orelse C == $\s -> params(Bin, L);
params(<<$", Bin/binary>>, L) -> quoted_string(Bin, L, L, <<>>);
params(<<>>, _L) -> [];
params(<<C, Bin/binary>>, L) -> param(Bin, L, <<C>>).
param(<<"\n", Bin/binary>>, L, Acc) -> [{string, L, Acc}|params(Bin, L+1)];
param(<<"{", _/binary>> = Bin, L, Acc) -> [{string, L, Acc}|params(Bin, L)];
param(<<";", Bin/binary>>, L, Acc) -> [{string, L, Acc},{';',L}|config(Bin, L)];
param(<<"}", _/binary>> = Bin, L, Acc) -> [{string, L, Acc},{';',L}|config(Bin, L)];
param(<<C, Bin/binary>>, L, Acc) when C == $\t orelse C == $\r orelse C == $\s -> [{string, L, Acc}|params(Bin, L)];
param(<<$=,$", Bin/binary>>, L, Acc) -> quoted_param(Bin, L, L, <<Acc/binary, "=">>);
param(<<C, Bin/binary>>, L, Acc) -> param(Bin, L, <<Acc/binary, C>>);
param(<<>>, L, Acc) -> [{string,L,Acc}].
quoted_param(<<$", Bin/binary>>, L0, L, Acc) -> [{string, L0, Acc}|params(Bin, L)];
quoted_param(<<"\n", Bin/binary>>, L0, L, Acc) -> quoted_param(Bin, L0, L+1, <<Acc/binary,"\n">>);
quoted_param(<<$\\,$", Bin/binary>>, L0, L, Acc) -> quoted_param(Bin, L0, L, <<Acc/binary,$">>);
quoted_param(<<C, Bin/binary>>, L0, L, Acc) -> quoted_param(Bin, L0, L, <<Acc/binary,C>>).
quoted_string(<<$", Bin/binary>>, L0, L, Acc) -> [{string, L0, Acc}|params(Bin, L)];
quoted_string(<<"\n", Bin/binary>>, L0, L, Acc) -> quoted_string(Bin, L0, L+1, <<Acc/binary,"\n">>);
quoted_string(<<$\\,$", Bin/binary>>, L0, L, Acc) -> quoted_string(Bin, L0, L, <<Acc/binary,$">>);
quoted_string(<<$\\,$\\, Bin/binary>>, L0, L, Acc) -> quoted_string(Bin, L0, L, <<Acc/binary,$\\>>);
quoted_string(<<C, Bin/binary>>, L0, L, Acc) -> quoted_string(Bin, L0, L, <<Acc/binary,C>>).
maybe_int(<<"16#", Val/binary>>) -> binary_to_integer(Val, 16);
maybe_int(<<"0x", Val/binary>>) -> binary_to_integer(Val, 16);
maybe_int(<<I, _/binary>> = Bin) when I >= $0 andalso I =< $9 ->
case maybe_int(Bin, 0) of
false ->
Bin;
Result ->
Result
end;
maybe_int(<<"true">>) -> true;
maybe_int(<<"false">>) -> false;
maybe_int(<<"off">>) -> false;
maybe_int(V) -> V.
maybe_int(<<I, Bin/binary>>, Acc) when I >= $0 andalso I =< $9 ->
maybe_int(Bin, Acc*10 + I - $0);
maybe_int(<<"G">>, Acc) -> {size,Acc*1024*1024*1024};
maybe_int(<<"M">>, Acc) -> {size,Acc*1024*1024};
maybe_int(<<"m">>, Acc) -> {size,Acc*1000*1000};
maybe_int(<<"K">>, Acc) -> {size,Acc*1024};
maybe_int(<<"k">>, Acc) -> {size,Acc*1000};
maybe_int(<<"d">>, Acc) -> {time,Acc*24*3600};
maybe_int(<<"h">>, Acc) -> {time,Acc*3600};
maybe_int(<<"s">>, Acc) -> {time,Acc};
maybe_int(<<"%">>, Acc) -> {percent,Acc};
maybe_int(<<_, _/binary>>, _Acc) -> false;
maybe_int(<<>>, Acc) -> Acc.
-record(entry, {
command,
line,
name,
options
}).
-record(section, {
command,
line,
name,
options,
section = []
}).
ast_top([{command, L, Command}, {string, _, S}, {';', _}|Tokens]) ->
[#entry{command = Command, line = L, name = S}|ast_top(Tokens)];
ast_top([{command, L, Command}, {';', _}|Tokens]) ->
[#entry{command = Command, line = L}|ast_top(Tokens)];
ast_top([{command, L, Command}, {'{', _}|Tokens]) ->
{Section, Tokens1} = ast_section(Tokens),
[#section{command = Command,line = L, section = Section}|ast_top(Tokens1)];
ast_top([{command, L, Command}, {string, _, S}, {'{', _}|Tokens]) ->
{Section, Tokens1} = ast_section(Tokens),
[#section{command = Command,line = L,name = S,section = Section}|ast_top(Tokens1)];
ast_top([{command, L, Command}, {string, _, S} |Tokens]) ->
case ast_options(Tokens) of
{Options, Tokens1} ->
[#entry{command = Command,line = L, name = S, options = Options}|ast_top(Tokens1)];
{section, Options, Tokens1} ->
{Section, Tokens2} = ast_section(Tokens1),
[#section{command = Command, line = L, name = S, options = Options, section = Section}|ast_top(Tokens2)]
end;
ast_top([]) ->
[].
ast_section([{'}',_}| Tokens]) ->
{[], Tokens};
ast_section([{command, L, Command}, {string, _, S}, {';', _}|Tokens]) ->
{Section,Tokens1} = ast_section(Tokens),
{[#entry{command = Command, line = L, name = S}|Section], Tokens1};
ast_section([{command, L, Command}, {';', _}|Tokens]) ->
{Section,Tokens1} = ast_section(Tokens),
{[#entry{command = Command, line = L}|Section], Tokens1};
ast_section([{command, L, Command}, {'}', _}|Tokens]) ->
{[#entry{command = Command, line = L}], Tokens};
ast_section([{command, L, Command}, {string, _, S} |Tokens]) ->
{Options, Tokens1} = ast_options(Tokens),
{Section,Tokens2} = ast_section(Tokens1),
{ [#entry{command = Command,line=L, name=S,options=Options}|Section], Tokens2}.
ast_options([{'{',_}|Tokens]) ->
{section, [], Tokens};
ast_options([{'}',_}|_] = Tokens) ->
{[], Tokens};
ast_options([{command,_,_}|_] = Tokens) ->
{[], Tokens};
ast_options([]) ->
{[], []};
ast_options([{';',_}|Tokens]) ->
{[], Tokens};
ast_options([{string,_,S}|Tokens]) ->
case ast_options(Tokens) of
{Opts,Tokens1} ->
{[S|Opts], Tokens1};
{section, Opts, Tokens1} ->
{section, [S|Opts], Tokens1}
end.
ast2_top([#entry{} = Entry|AST]) ->
[ast2_top_entry(Entry)|ast2_top(AST)];
ast2_top([#section{} = S|AST]) ->
[ast2_top_section(S)|ast2_top(AST)];
ast2_top([]) ->
[].
ast2_top_entry(#entry{command=C,line=L,name=V}) when
C == http; C == https; C == admin_http; C == admin_https; C == rtsp; C == rtsps;
C == rtmp; C == rtmps; C == mysql; C == snmp ->
case parse_listener(V) of
{ok, I} ->
{C,I};
undefined ->
throw({error, {invalid_listener,L,C,V}})
end;
ast2_top_entry(#entry{command=C,line=L,name=V}) when
C == max_sessions; C == channel_limit; C == cpu_limit ->
try binary_to_integer(V) of
I ->
{C,I}
catch
_:_ -> throw({error, {invalid_spec,L,C,V}})
end;
ast2_top_entry(#entry{command = log_level} = E) ->
ast2_top_entry(E#entry{command = loglevel});
ast2_top_entry(#entry{command = loglevel, line = L, name = V}) ->
Level = case V of
<<"debug">> -> debug;
<<"info">> -> info;
<<"notice">> -> notice;
<<"warning">> -> warning;
<<"error">> -> error;
<<"alert">> -> alert;
<<"critical">> -> critical;
_ -> throw({error, {invalid_loglevel,L,V}})
end,
{loglevel, Level};
ast2_top_entry(#entry{command =C,line=L,name=V}) when C == logrequests; C == log_requests ->
{log_requests, ast2_bool(V,L)};
ast2_top_entry(#entry{command = no_auto_token}) ->
{autogenerate_token, false};
ast2_top_entry(#entry{command = auto_token, line = L, name = V}) ->
Flag = case V of
<<"false">> -> false;
<<"blank">> -> blank;
_ -> throw({error, {invalid_auto_token,L,V}})
end,
{autogenerate_token, Flag};
ast2_top_entry(#entry{command=C,line=L,name=V1,options=Values}) when C == edit_auth; C == view_auth ->
case Values of
[V2] -> {C,#{login => V1, password => V2}};
_ -> throw({error, {invalid_auth,L,C,V1,Values}})
end;
ast2_top_entry(#entry{command=web_script=C,line=L,name=V1,options=Values}) ->
case Values of
[V2|Opts] -> {web_script, #{prefix => V1, path => V2, extra => maps:from_list(ast2_keyvalues(Opts,L))}};
_ -> throw({error, {invalid_web_script,L,C,V1,Values}})
end;
ast2_top_entry(#entry{command = total_bandwidth=C, line = L,name = V1}) ->
case maybe_int(V1) of
{size,S} -> {total_bandwidth,S};
_ -> throw({error, {invalid_bandwidth,L,C,V1}})
end;
ast2_top_entry(#entry{command = plugin, line = L, name = V, options = undefined}) ->
ast2_top_section(#section{command = plugin, name = binary_to_atom(V,latin1), line = L, section = []});
ast2_top_entry(#entry{command = dvr, line = L, name = Name, options = Values}) ->
case Values of
[_|_] ->
{dvr, Name, parse_dvr(Values, L)};
_ ->
throw({error, {invalid_dvr,L,Name}})
end;
ast2_top_entry(#entry{command = http_proxy, name = V, line = L, options = Values}) ->
case Values of
[URL|KVlist] ->
{http_proxy, maps:merge(#{prefix => V, url => URL}, maps:from_list(ast2_keyvalues(KVlist, L)))};
_ ->
throw({error, {invalid_http_proxy,L,Values}})
end;
ast2_top_entry(#entry{command = server} = Entry) ->
ast2_top_entry(Entry#entry{command = peer});
ast2_top_entry(#entry{command = peer, name = V, options = undefined}) ->
case re:run(V, "^(.+)\\:(\\d+)$", [{capture,all_but_first,binary}]) of
nomatch -> {peer, V, #{}};
{match, [V1,P1]} -> {peer, V1, #{port => binary_to_integer(P1)}}
end;
ast2_top_entry(#entry{command = live, name = V}) ->
apply_stream_options(live, V, [{static,false}]);
ast2_top_entry(#entry{command = cdnproxy}) ->
{cdnproxy, true};
ast2_top_entry(#entry{command = vsaas}) ->
{plugin, vsaas, #{}};
ast2_top_entry(#entry{command = ondemand, name = V, options = [U|Opts], line = L}) ->
apply_stream_options(stream, V, [{urls, [maps:from_list([{url,U}] ++ ast2_url_options(ast2_keyvalues(Opts,L)))]},{static,false}]);
ast2_top_entry(#entry{command = stream, name = V, options = [U|Opts], line = L}) ->
apply_stream_options(stream, V, [{urls, [maps:from_list([{url,U}] ++ ast2_url_options(ast2_keyvalues(Opts,L)))]},{static,true}]);
ast2_top_entry(#entry{command = file, name = V, options = [U|Opts], line = L}) ->
apply_stream_options(file, V, [{urls, [maps:from_list([{url,remove_last_slash(U)}] ++ ast2_url_options(ast2_keyvalues(Opts,L)))]}]);
ast2_top_entry(#entry{command = rewrite, name = N, options = Opts} = E) ->
case re:run(N, <<"(.+)/\\*$">>, [{capture,all_but_first,binary}]) of
nomatch ->
ast2_top_entry(E#entry{command = ondemand});
{match, [N1]} when Opts =/= undefined ->
apply_stream_options(dynamic, N1, [{urls, [#{url => V} || V <- Opts]},{static,false}] )
end;
ast2_top_entry(#entry{command = cache, name = Name, options = Opts, line = L}) ->
{cache, Name, parse_cache(Opts, L)};
ast2_top_entry(#entry{command = api_allowed_from, line = L} = Entry) ->
{api_allowed_from, [ast2_parse_ipcidr(V, L) || V <- ast2_values(Entry)]};
ast2_top_entry(#entry{command = notify, name = URL, options = Opts, line = L}) when Opts == undefined orelse Opts == [] ->
ast2_top_section(#section{command = notify, name = URL, line = L, section = [
#entry{command = sink, name = URL, line = L},
#entry{command = buffer, name = <<"off">>, line = L}
]});
ast2_top_entry(#entry{command = source, name = N, options = Options, line = L}) ->
ast2_top_section(#section{command = source, name=N, options=Options, line=L});
ast2_top_entry(#entry{command = C} = Entry) when C == retroview; C == auth; C == wwwroot; C == url_prefix; C == init_script;
C == aliaser; C == auth_token; C == pulsedb; C == session_log; C == include; C == cluster_key;
C == allowed_countries; C == disallowed_countries; C == domain; C == domains; C == meta
->
ast2_entry(Entry);
ast2_top_entry(#entry{} = Entry) ->
error({invalid_entry,Entry#entry.line, Entry#entry.command, Entry}).
% ast2_entry(Entry).
ast2_top_section(#section{command = vsaas, name = V, line = L, section = Entries}) ->
V == undefined orelse throw({error,{invalid_vsaas_name,L,V}}),
{plugin, vsaas, maps:from_list([ast2_entry(E) || E <- Entries])};
ast2_top_section(#section{command = C, name = N, section = Entries}) when C == stream; C == ondemand; C == file; C == live ->
Opts = case C of
ondemand -> [{static,false}];
stream -> [{static,true}];
live -> [{static,false}];
file -> []
end ++ [ast2_entry(E) || E <- Entries],
Cmd1 = case C of
ondemand -> stream;
live -> live;
_ -> C
end,
apply_stream_options(Cmd1, N, Opts);
ast2_top_section(#section{command = rewrite, name = N, section = Entries} = Section) ->
case re:run(N, <<"(.+)/\\*$">>, [{capture,all_but_first,binary}]) of
nomatch ->
ast2_top_section(Section#section{command = ondemand});
{match, [N1]} ->
apply_stream_options(dynamic, N1, [{static,false}] ++ [ast2_entry(E) || E <- Entries])
end;
ast2_top_section(#section{command = notify, name = N, options = undefined, section = Entries}) ->
{notify, N, make_notify([ast2_entry(E) || E <- Entries])};
ast2_top_section(#section{command = source, name = N, options = Options, section = Entries}) ->
URLs = [N] ++ case Options of
undefined -> [];
_ -> Options
end,
Prefixes = [case re:run(URL, "[^:]+://[^/]+/(.*)$", [{capture,all_but_first,binary}]) of
{match, [Prefix]} -> <<Prefix/binary, "/">>;
nomatch -> <<>>
end || URL <- URLs],
Prefix = hd(Prefixes),
{source, URLs, make_options(config2_helpers:replace_meta([ast2_entry(E) || E <- Entries]++[{prefix,Prefix}]))};
ast2_top_section(#section{command = plugin, name = N, section = Entries}) ->
Name = if is_binary(N) -> binary_to_atom(N,latin1);
is_atom(N) -> N
end,
{plugin,Name,maps:from_list([case ast2_entry(E) of
{auth, #{url := U}} -> {auth, U};
Else -> Else
end || E <- Entries])};
ast2_top_section(#section{command = server} = Section) ->
ast2_top_section(Section#section{command = peer});
ast2_top_section(#section{command = peer, name = N, section = Entries}) ->
Opts = maps:from_list([ast2_entry(E) || E <- Entries]),
case re:run(N, "^(.+)\\:(\\d+)$", [{capture,all_but_first,binary}]) of
nomatch -> {peer, N, Opts};
{match, [V1,P1]} -> {peer, V1, Opts#{port => binary_to_integer(P1)}}
end;
ast2_top_section(#section{command = C, name = N, section = Entries}) when C == none
->
{C,N,[ast2_entry(E) || E <- Entries]}.
ast2_keyvalues(undefined,_L) ->
[];
ast2_keyvalues(Opts,L) ->
[ast2_keyvalue(Opt, L, Opts) || Opt <- Opts].
ast2_keyvalue(Opt, L, Opts) ->
case binary:split(Opt,<<"=">>) of
[<<"vb">>,<<"copy">>] ->
{video_bitrate,copy};
[<<"ab">>,<<"copy">>] ->
{audio_bitrate,copy};
[<<"vb">>,V] ->
case maybe_int(V) of
{size,S} -> {video_bitrate,S};
_ -> {vb, V}
end;
[<<"ab">>,V] ->
case maybe_int(V) of
{size,S} -> {audio_bitrate,S};
_ -> {ab, V}
end;
[K,V] -> {binary_to_atom(K,latin1),V};
[_] -> throw({error, {invalid_key_value,L,Opt,Opts}})
end.
ast2_entry(#entry{command = disabled}) -> {disabled, true};
ast2_entry(#entry{command = dvr_offline} = E) ->
{dvr, DVR} = ast2_entry(E#entry{command = dvr}),
{dvr, DVR#{dvr_offline => true}};
ast2_entry(#entry{command = dvr, name = <<"@", Name/binary>>}) ->
{external_dvr, Name};
ast2_entry(#entry{command = dvr, line = L, name = Name, options = Values}) ->
Control = case Values of
undefined -> [Name];
_ -> [Name|Values]
end,
{dvr, parse_dvr(Control, L)};
ast2_entry(#entry{command = record_input, line = L, name = Name, options = Values}) ->
Control = case Values of
undefined -> [Name];
_ -> [Name|Values]
end,
{record_input, parse_dvr(Control, L)};
ast2_entry(#entry{command = C} = Entry) when C == only; C == except ->
{C, ast2_values(Entry)};
ast2_entry(#entry{command = path, name = V, options = Options, line = L}) ->
Opts = ast2_url_options(ast2_keyvalues(Options, L)),
{path, maps:from_list([{url, remove_last_slash(V)}] ++ Opts)};
ast2_entry(#entry{command = url, name = V, options = Options, line = L}) ->
Opts = ast2_url_options(ast2_keyvalues(Options, L)),
{url, maps:from_list([{url,V}] ++ Opts)};
ast2_entry(#entry{command = drm, line = L, name = V, options = Opts}) ->
{drm, maps:merge(#{vendor => binary_to_atom(V,latin1)}, maps:from_list(ast2_keyvalues(Opts, L)))};
ast2_entry(#entry{command = C, name = V, line = L}) when C == hds; C == hls; C == mpegts; C == rtmp;
C == rtsp; C == dash; C == m4s; C == m4f; C == mseld; C == webrtc; C == pulse ->
C1 = atom_to_binary(C,latin1),
{binary_to_atom(<<C1/binary, "_off">>, latin1), not ast2_bool(V,L)};
ast2_entry(#entry{command = mpegts_pids, name = Pid1, options = Options, line = L}) ->
{mpegts_pids, parse_mpegts_pids(Pid1, Options, L)};
ast2_entry(#entry{command = segments_count} = E) ->
ast2_entry(E#entry{command = segment_count});
ast2_entry(#entry{command = client_timeout} = E) ->
ast2_entry(E#entry{command = clients_timeout});
ast2_entry(#entry{command = segments} = E) ->
ast2_entry(E#entry{command = segment_count});
ast2_entry(#entry{command = gop_duration, name = N}) ->
{gop_duration, binary_to_integer(N)*1000};
ast2_entry(#entry{command = C, name = <<"false">>}) when C == source_timeout; C == clients_timeout; C == prepush ->
{C, false};
ast2_entry(#entry{command = C, name = <<"off">>}) when C == source_timeout; C == clients_timeout; C == prepush ->
{C, false};
ast2_entry(#entry{command = rtp, name = <<"udp">>}) ->
{rtp,udp};
ast2_entry(#entry{command = C, line = L, name = I}) when C == retry_limit; C == max_bitrate; C == prepush;
C == clients_timeout; C == source_timeout; C == hls_buffer; C == segment_count; C == read_queue;
C == fetch_timeout; C == stale_timeout;
C == max_readers; C == channel_limit; C == cpu_limit; C == max_sessions ->
try binary_to_integer(I) of
V -> {C,V}
catch
_:_ -> throw({error, {invalid_value,L,C,I}})
end;
ast2_entry(#entry{command = ad_inject, line = L} = Entry) ->
{ad_inject, maps:from_list(ast2_keyvalues(ast2_values(Entry),L))};
ast2_entry(#entry{command = C, line = L} = Entry) when C == cluster_ingest; C == motion_detector ->
Opts = ast2_keyvalues(ast2_values(Entry),L),
Opts0 = case C of
motion_detector -> [{enabled, true}];
_ -> []
end,
Opts1 = Opts0 ++ case proplists:get_all_values(tag, Opts) of
[] -> Opts;
Tags -> [{tags,Tags}] ++ [{K,V} || {K,V} <- Opts, K =/= tag]
end,
Opts2 = case proplists:get_all_values(notify, Opts1) of
[] -> Opts1;
Notify -> [{notify,Notify}] ++ [{K,V} || {K,V} <- Opts1, K =/= notify]
end,
{C, maps:from_list(Opts2)};
ast2_entry(#entry{command = C, line = L} = Entry) when C == transcoder ->
{C, ast2_keyvalues(ast2_values(Entry),L)};
ast2_entry(#entry{command = no_prepush}) -> {prepush, false};
ast2_entry(#entry{command = download}) -> {download, true};
ast2_entry(#entry{command = publish_enabled}) -> {publish_enabled, true};
ast2_entry(#entry{command = add_audio_only}) -> {add_audio_only, true};
ast2_entry(#entry{command = thumbnails, line = L} = E) ->
Opts = maps:from_list(ast2_keyvalues(ast2_values(E), L)),
{thumbnails, Opts#{enabled => true}};
ast2_entry(#entry{command = logo, line = L} = E) ->
Opts = maps:from_list(ast2_keyvalues(ast2_values(E), L)),
{logo, Opts};
ast2_entry(#entry{command = url_prefix, name = <<"false">>}) ->
{url_prefix, false};
ast2_entry(#entry{command = domains} = Entry) ->
{domains, ast2_values(Entry)};
ast2_entry(#entry{command = C} = Entry) when C == allowed_countries; C == disallowed_countries ->
{C, [to_upcase(X) || X <- ast2_values(Entry)]};
ast2_entry(#entry{command = C} = Entry) when C == paths; C == urls ->
{C, [#{url => E} || E <- ast2_values(Entry)]};
ast2_entry(#entry{command = udp, name = UDP}) ->
{push, <<"udp://",UDP/binary>>};
ast2_entry(#entry{command = tracks} = Entry) ->
{tracks, [binary_to_integer(I) || I <- ast2_values(Entry)]};
ast2_entry(#entry{command = auth, name = URL, line = L, options = Opts}) ->
M1 = case Opts of
undefined -> #{};
[] -> #{};
_ -> #{extra => maps:from_list(ast2_keyvalues(Opts, L))}
end,
U = case URL of
<<"true">> -> true;
<<"false">> -> false;
<<"mocked">> -> mocked;
_ -> URL
end,
{auth, M1#{url => U}};
ast2_entry(#entry{command = ad_schedule, name = N1, options = [N2]}) ->
{ad_schedule, #{schedule => N1, alias => N2}};
ast2_entry(#entry{command = meta, name = N1, options = [N2]}) ->
{meta, binary_to_atom(N1,latin1), N2};
ast2_entry(#entry{command = cache, name = <<"@",Cache/binary>>}) ->
{external_cache, Cache};
ast2_entry(#entry{command = cache, line = L} = Entry) ->
{cache, parse_cache(ast2_values(Entry), L)};
ast2_entry(#entry{command = segment_cache, line = L} = Entry) ->
{segment_cache, parse_cache(ast2_values(Entry), L)};
ast2_entry(#entry{command = C, name = V, options = undefined}) ->
{C, V};
ast2_entry(#entry{command = C, name = V, options = []}) ->
{C, V};
ast2_entry(#entry{command = C, name = V, options = Options, line = L}) ->
{C,V, ast2_keyvalues(Options, L)}.
ast2_values(#entry{name = N, options = Opts}) ->
case N of
undefined -> [];
_ -> [N]
end ++ case Opts of
undefined -> [];
_ -> Opts
end.
ast2_url_options([{rtp,P}|List]) -> [{rtp,binary_to_atom(P,latin1)}|ast2_url_options(List)];
ast2_url_options([{priority,P}|List]) -> [{priority,binary_to_integer(P)}|ast2_url_options(List)];
ast2_url_options([{source_timeout,P}|List]) -> [{source_timeout,binary_to_integer(P)}|ast2_url_options(List)];
ast2_url_options([{no_clients_reconnect_delay,P}|List]) -> [{no_clients_reconnect_delay,binary_to_integer(P)}|ast2_url_options(List)];
ast2_url_options([KV|List]) -> [KV|ast2_url_options(List)];
ast2_url_options([]) -> [].
ast2_bool(V,L) ->
case V of
<<"true">> -> true;
<<"on">> -> true;
<<"off">> -> false;
<<"false">> -> false;
_ -> throw({error, {invalid_boolean,L,V}})
end.
parse_listener(Bin) ->
try binary_to_integer(Bin) of
Int -> {ok, Int}
catch
_:_ ->
case binary:split(Bin,<<":">>) of
[_H,P] ->
try binary_to_integer(P) of
_ -> {ok, Bin}
catch _:_ ->
undefined
end;
_ ->
undefined
end
end.
ast2_parse_ipcidr(Bin, _L) ->
{Addr, Len} = case binary:split(Bin, <<"/">>) of
[A,Ln] -> {A,binary_to_integer(Ln)};
[A] -> {A,32}
end,
case binary:match(Addr, <<":">>) of
nomatch ->
Address_ = [binary_to_integer(I) || I <- binary:split(Addr,<<".">>,[global])],
Address = list_to_tuple(lists:sublist(Address_ ++ [0,0,0,0], 4)),
{Address, Len};
_ ->
{ok, Address} = inet_parse:address(binary_to_list(Addr)),
{Address, Len}
end.
parse_cache([Path|Opts], L) ->
parse_cache0(Opts, #{path => Path}, L).
parse_cache0([], Cache, _) -> Cache;
parse_cache0([<<"misses=",M/binary>>|Opts], Cache, L) ->
parse_cache0(Opts, Cache#{misses => binary_to_integer(M)}, L);
parse_cache0([I|Opts], Cache, L) ->
case maybe_int(I) of
{size,S} ->
parse_cache0(Opts, Cache#{disk_limit => S}, L);
{time,S} ->
parse_cache0(Opts, Cache#{time_limit => S}, L);
_ ->
throw({error,{invalid_cache_option,L,I}})
end.
parse_mpegts_pids(E1, undefined, L) -> parse_mpegts_pids(E1, [], L);
parse_mpegts_pids(E1, MorePids, L) -> parse_mpegts_pids([E1|MorePids], L).
parse_mpegts_pids(PidEntries, L) ->
FlatMap = [parse_mpegts_pid(E, L) || E <- PidEntries],
lists:foldl(fun populate_mpegts_map/2, #{}, FlatMap).
parse_mpegts_pid(Entry, L) ->
case binary:split(Entry, <<"=">>) of
[Key, Value] ->
Pid = maybe_int(Value),
is_integer(Pid) orelse throw({error, {invalid_mpegts_pid, L, Value}}),
{mpegts_id(Key, L), Pid};
[_] ->
throw({error, {invalid_mpegts_pid_spec, L, Entry}})
end.
mpegts_id(<<T:8, BinIndex/binary>>, L) when T == $a; T == $v; T == $t ->
Type = case T of
$a -> audio;
$v -> video;
$t -> text
end,
Index = maybe_int(BinIndex),
is_integer(Index) orelse throw({error, {invalid_track_id, L, <<T:8, BinIndex/binary>>}}),
{Type, Index};
mpegts_id(BinID, _) ->
binary_to_atom(BinID, latin1).
populate_mpegts_map({{Type, Index}, Pid}, Map) ->
TMap = maps:get(Type, Map, #{}),
Map#{Type => TMap#{Index => Pid}};
populate_mpegts_map({Key, Pid}, Map) ->
Map#{Key => Pid}.
parse_dvr([Path|Opts], L) ->
parse_dvr0(Opts, #{root => Path}, L).
parse_dvr0([], DVR, _L) -> DVR;
parse_dvr0([<<I,"%">>|Opts], DVR, L) -> parse_dvr0(Opts, DVR#{disk_limit => I-$0}, L);
parse_dvr0([<<I:2/binary,"%">>|Opts], DVR, L) -> parse_dvr0(Opts, DVR#{disk_limit => binary_to_integer(I)}, L);
parse_dvr0([<<"replication_port=",Port/binary>>|Opts], DVR, L) -> parse_dvr0(Opts, DVR#{replication_port => binary_to_integer(Port)}, L);
parse_dvr0([<<"replicate=",Speed/binary>>|Opts], DVR, L) ->
{size,S} = maybe_int(Speed),
parse_dvr0(Opts, DVR#{replication_speed => S, dvr_replicate => true}, L);
parse_dvr0([<<"replicate">>|Opts], DVR, L) ->
parse_dvr0(Opts, DVR#{dvr_replicate => true}, L);
parse_dvr0([<<"schedule=", DvrSchedule/binary>>|Opts], DVR, L) ->
Schedule = parse_dvr_schedule(DvrSchedule, L),
parse_dvr0(Opts, DVR#{schedule => Schedule}, L);
parse_dvr0([<<"copy=", Copy/binary>>|Opts], DVR, L) ->
parse_dvr0(Opts, DVR#{copy => Copy}, L);
parse_dvr0([Num|Opts], DVR, L) ->
case maybe_int(Num) of
T when is_integer(T) andalso T > 0 -> parse_dvr0(Opts, DVR#{dvr_limit => T}, L);
{percent, N} -> parse_dvr0(Opts, DVR#{disk_limit => N}, L);
{size, Size} -> parse_dvr0(Opts, DVR#{disk_space => Size}, L);
{time, T} -> parse_dvr0(Opts, DVR#{dvr_limit => T}, L)
end.
parse_dvr_schedule(S, L) ->
Lexems = dvr_schedule_lexems(S),
Lexems2 = dvr_schedule_parse2(Lexems,L),
dvr_schedule_parse3(Lexems2, L).
dvr_schedule_lexems(<<>>) -> [];
dvr_schedule_lexems(<<",",S/binary>>) -> [','|dvr_schedule_lexems(S)];
dvr_schedule_lexems(<<":",S/binary>>) -> [':'|dvr_schedule_lexems(S)];
dvr_schedule_lexems(<<"-",S/binary>>) -> ['-'|dvr_schedule_lexems(S)];
dvr_schedule_lexems(<<I1,S/binary>>) when I1 >= $0, I1 =< $9 -> dvr_schedule_int(S, I1 - $0).
dvr_schedule_int(<<I,S/binary>>, Acc) when I >= $0, I =< $9 -> dvr_schedule_int(S, Acc*10 + I - $0);
dvr_schedule_int(S, Acc) -> [Acc|dvr_schedule_lexems(S)].
dvr_schedule_parse2([I1,':',I2|List], L) when is_integer(I1), is_integer(I2) ->
[{int,I1*100+I2}|dvr_schedule_parse2(List, L)];
dvr_schedule_parse2([I1|List], L) when is_integer(I1) ->
I2 = if
I1 > 100 -> I1;
true -> I1*100
end,
[{int,I2}|dvr_schedule_parse2(List, L)];
dvr_schedule_parse2([','|List], L) ->
[','|dvr_schedule_parse2(List, L)];
dvr_schedule_parse2(['-'|List], L) ->
['-'|dvr_schedule_parse2(List, L)];
dvr_schedule_parse2([], _) ->
[].
dvr_schedule_parse3([{int,I1},'-',{int,I2}|List], L) ->
[[I1,I2]|dvr_schedule_parse3(List,L)];
dvr_schedule_parse3([','|List], L) ->
dvr_schedule_parse3(List, L);
dvr_schedule_parse3([], _) -> [].
-record(notify, {
sink,
only = [],
except = [],
owner,
verbose,
buffer,
throttle_delay,
args = #{}
}).
make_notify(Entries) ->
Notify = make_notify0(Entries, #notify{}),
maps:from_list(case Notify#notify.only of
[] -> [];
Only -> [{only,Only}]
end ++
case Notify#notify.except of
[] -> [];
Except -> [{except,Except}]
end ++
case Notify#notify.owner of
undefined -> [];
Owner -> [{owner,Owner}]
end ++
case Notify#notify.verbose of
undefined -> [];
Verbose -> [{verbose,Verbose}]
end ++
case Notify#notify.buffer of
undefined -> [];
Buffer -> [{buffer,Buffer}]
end ++
case Notify#notify.throttle_delay of
undefined -> [];
Delay -> [{throttle_delay,Delay}]
end ++
[{sink, Notify#notify.sink},
{args, Notify#notify.args}]).
make_notify0([{only, Conditions}|Entries], #notify{only = Only} = Notify) ->
make_notify0(Entries, Notify#notify{only = Only ++ [notify_conditions(Conditions)] });
make_notify0([{except, Conditions}|Entries], #notify{except = Except} = Notify) ->
make_notify0(Entries, Notify#notify{except = Except ++ [notify_conditions(Conditions)] });
make_notify0([{buffer, <<"off">>}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{buffer = false});
make_notify0([{buffer, Buffer}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{buffer = binary_to_integer(Buffer)});
make_notify0([{throttle_delay, Delay}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{throttle_delay = binary_to_integer(Delay)});
make_notify0([{owner, Owner}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{owner = Owner});
make_notify0([{verbose, Verbose}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{verbose = binary_to_atom(Verbose,latin1)});
make_notify0([{sink, Sink}|Entries], #notify{} = Notify) ->
make_notify0(Entries, Notify#notify{sink = Sink});
make_notify0([{Key, Value} | Entries], #notify{args = Args} = Notify) ->
make_notify0(Entries, Notify#notify{args = maps:put(Key, Value, Args)});
make_notify0([], #notify{} = Notify) ->
Notify.
notify_conditions(Conditions) ->
notify_conditions0(Conditions, #{}).
notify_conditions0([], And) -> And;
notify_conditions0([Condition|Conditions], And) ->
case binary:split(Condition, <<"=">>) of
[_] ->
events:error("Invalid notify condition: ~p", [Condition]),
error({invalid_notify_condition, Condition});
[Key,Value] ->
Values = case binary:split(Value, <<",">>, [global]) of
[Value] -> Value;
List -> List
end,
And1 = maps:put(binary_to_atom(Key,latin1), Values, And),
notify_conditions0(Conditions, And1)
end.
-record(opts, {
prefix,
options = #{},
auth = #{},
groups = [],
urls = [],
push = [],
meta = []
}).
apply_stream_options(Command, Name, Options) ->
#opts{} = Opts = apply_options(Options),
case Command of
live ->
{Command, Name, maps:merge(make_options(Opts#opts{prefix = Name}), #{publish_enabled => true})};
file ->
{Command, Name, make_options(Opts#opts{prefix = Name})};
dynamic ->
{Command, Name, make_options(Opts#opts{prefix = Name})};
stream ->
{Command, Name, make_options(Opts)}
end.
% l(_, undefined) -> [];
% l(Key, Value) -> [{Key,Value}].
make_options([{_,_}|_] = Options) ->
make_options(apply_options(Options));
make_options(#opts{push = Push, urls = URLs, auth = Auth, meta = Meta, prefix = Prefix, groups = Groups, options = #{} = Options}) ->
UnsortedOptions = case Meta of
[] -> [];
_ -> [{meta,maps:from_list(Meta)}]
end ++
case Groups of
[] -> [];
_ -> [{groups,Groups}]
end ++
case Push of
[] -> [];
_ -> [{push,Push}]
end ++
case URLs of
[] -> [];
_ -> [{urls,URLs}]
end ++
case Prefix of
undefined -> [];
_ -> [{prefix,Prefix}]
end ++
case maps:to_list(Auth) of
[] -> [];
_ -> [{auth,Auth}]
end,
maps:merge(Options, maps:from_list(UnsortedOptions)).
apply_options(Options) -> apply_options(#opts{}, Options).
apply_options(#opts{urls = URLs} = O, [{url,URL}|Acc]) ->
apply_options(O#opts{urls = URLs ++ [URL]}, Acc);
apply_options(#opts{} = O, [{urls,URLs2}|Acc]) ->
apply_options(O#opts{urls = URLs2}, Acc);
apply_options(#opts{urls = URLs} = O, [{path,URL}|Acc]) ->
apply_options(O#opts{urls = URLs ++ [URL]}, Acc);
apply_options(#opts{} = O, [{paths,URLs2}|Acc]) ->
apply_options(O#opts{urls = URLs2}, Acc);
apply_options(#opts{auth = Auth} = O, [{domain,Domain}|Acc]) ->
Auth1 = case Auth of
#{domains := [_|_] = D} -> Auth#{domains := D ++ [clean_domain(Domain)]};
#{} -> Auth#{domains => [clean_domain(Domain)]}
end,
apply_options(O#opts{auth = Auth1}, Acc);
apply_options(#opts{auth = Auth} = O, [{domains,Domains}|Acc]) ->
apply_options(O#opts{auth = Auth#{domains => [clean_domain(D) || D <- Domains]}}, Acc);
apply_options(#opts{meta = []} = O, [{meta,KVList}|Acc]) when is_list(KVList) ->
apply_options(O#opts{meta = KVList}, Acc);
apply_options(#opts{meta = Meta} = O, [{meta,K,V}|Acc]) ->
apply_options(O#opts{meta = Meta ++ [{K,V}]}, Acc);
apply_options(#opts{push = Push} = O, [{push,P}|Acc]) ->
apply_options(O#opts{push = Push ++ [P]}, Acc);
apply_options(#opts{groups = Groups} = O, [{group,G}|Acc]) ->
apply_options(O#opts{groups = Groups ++ [G]}, Acc);
apply_options(#opts{auth = Auth} = O, [{auth,#{} = Auth2}|Acc]) ->
apply_options(O#opts{auth = maps:merge(Auth,Auth2)}, Acc);
apply_options(#opts{auth = Auth} = O, [{allowed_countries,Countries}|Acc]) ->
apply_options(O#opts{auth = Auth#{allowed_countries => Countries}}, Acc);
apply_options(#opts{auth = Auth} = O, [{disallowed_countries,Countries}|Acc]) ->
apply_options(O#opts{auth = Auth#{disallowed_countries => Countries}}, Acc);
apply_options(#opts{options = Opts} = O, [{K,V}|Acc]) ->
case lists:member(K, config2_render:all_options()) of
true ->
apply_options(O#opts{options = maps:put(K,V,Opts)}, Acc);
false ->
error({invalid_option, K,V})
end;
apply_options(#opts{} = O, [_|Acc]) -> apply_options(O, Acc);
apply_options(#opts{} = O, []) -> O.
clean_domain(<<"*.", Domain/binary>>) -> <<".",Domain/binary>>;
clean_domain(Domain) -> Domain.
remove_last_slash(Path) ->
Length = byte_size(Path)-1,
case Path of
<<Path1:Length/binary, "/">> -> Path1;
_ -> Path
end.
to_upcase(Binary) -> to_upcase(Binary, <<>>).
to_upcase(<<>>, Acc) -> Acc;
to_upcase(<<C, Binary/binary>>, Acc) when C >= 97 andalso C =< 122 -> to_upcase(Binary, <<Acc/binary, (C-32)>>);
to_upcase(<<C, Binary/binary>>, Acc) -> to_upcase(Binary, <<Acc/binary, C>>).
% Весь трешак ниже скопирован из config2_format.peg
run_ast3_transform(AST, Options) ->
GlobalAuth = read_global_auth(AST),
AST2 = replace_global_auth(AST, GlobalAuth),
AST8 = case maps:get(add_global, Options, true) of
true ->
% 1. Надо вычислить глобальные настройки auth и вмержить их в каждый стрим
% учитываются опции auth, domains, (dis)allowed_countries, autogenerate_token
AST3 = apply_global_auth(AST2, add_suffix_to_keys(GlobalAuth, <<"_global">>)),
% 2. Теперь надо наложить глобальные переменные
AST5 = apply_global_variables(AST3, [cluster_key, url_prefix]),
% 3. Раньше здесь было добавление ad scheduled стримов, теперь выкидываем, только руками
% 5. Проставляем global dvr
AST7 = add_global_dvrs(AST5),
add_global_caches(AST7);
false ->
AST
end,
AST9 = add_positions(AST8),
case validate_config(AST9) of
ok ->
AST9;
{error, E} ->
{error, E}
end.
read_global_auth(AST) -> read_global_auth2(AST, #{}).
read_global_auth2([], Auth) -> Auth;
read_global_auth2([{auth,A}|Lines], Auth) -> read_global_auth2(Lines, maps:merge(Auth, A));
read_global_auth2([{domain,Domain}|Lines], Auth) ->
Auth1 = case Auth of
#{domains := [_|_] = D} -> Auth#{domains := D ++ [clean_domain(Domain)]};
#{} -> Auth#{domains => [clean_domain(Domain)]}
end,
read_global_auth2(Lines, Auth1);
read_global_auth2([{domains,Domains}|Lines], Auth) ->
read_global_auth2(Lines, Auth#{domains => [clean_domain(D) || D <- Domains]});
read_global_auth2([{Tag,Countries}|Lines], Auth) when
Tag == allowed_countries; Tag == disallowed_countries; Tag == autogenerate_token->
read_global_auth2(Lines, maps:put(Tag, Countries, Auth));
read_global_auth2([_|Lines], Auth) -> read_global_auth2(Lines, Auth).
apply_global_auth([{Tag,Name,Config}|AST], GlobalAuth) when
Tag == stream; Tag == file; Tag == source; Tag == live; Tag == dynamic ->
Auth = add_suffix_to_keys(maps:get(auth, Config, #{}), <<"_orig">>),
Config1 = maps:put(auth, maps:merge(GlobalAuth, Auth), Config),
[{Tag,Name,Config1}|apply_global_auth(AST, GlobalAuth)];
apply_global_auth([KV|AST], GlobalAuth) ->
[KV|apply_global_auth(AST, GlobalAuth)];
apply_global_auth([], _) -> [].
add_suffix_to_keys(Map, Suffix) ->
maps:merge(
maps:from_list([{list_to_atom(atom_to_list(K) ++ binary_to_list(Suffix)),V} || {K,V} <- maps:to_list(Map)]),
Map
).
replace_global_auth(Lines, Auth) ->
case maps:to_list(Auth) == [{url,true}] of
true -> Lines;
false -> replace_global_auth0(Lines, Auth)
end.
replace_global_auth0([{K,_}|Lines], Auth) when K == auth; K == domain; K == domains; K == allowed_countries; K == disallowed_countries; K == autogenerate_token ->
[{auth,Auth}|clean_global_auth(Lines)];
replace_global_auth0([Line|Lines], Auth) ->
[Line|replace_global_auth0(Lines, Auth)];
replace_global_auth0([], _) -> [].
clean_global_auth(Lines) ->
[L || L <- Lines, not lists:member(element(1,L), [auth, domain, domains, allowed_countries, disallowed_countries, autogenerate_token])].
apply_global_variables(AST, []) -> AST;
apply_global_variables(AST, [K|Lines]) ->
AST1 = case lists:keyfind(K, 1, AST) of
{K,V} -> apply_global_variable(AST, K, V);
false -> AST
end,
apply_global_variables(AST1, Lines).
apply_global_variable([{Tag,Name,Config}|AST], K, V) when
Tag == stream; Tag == file; Tag == source; Tag == live; Tag == dynamic ->
Config1 = maps:merge(
maps:from_list([ {K,V}, {list_to_atom(atom_to_list(K) ++ "_global"),V} ]),
Config),
[{Tag,Name,Config1}|apply_global_variable(AST, K, V)];
apply_global_variable([Line|AST], K, V) -> [Line|apply_global_variable(AST, K, V)];
apply_global_variable([], _, _) -> [].
validate_config(AST) ->
Validations = [fun validate_unique_names/1, fun validate_source_prefixes/1, fun validate_push_targets/1],
run_validations(Validations, AST).
run_validations([], _AST) ->
ok;
run_validations([Fun1|MoreFuns], AST) ->
case Fun1(AST) of
ok -> run_validations(MoreFuns, AST);
{error, _} = Error -> Error
end.
validate_unique_names(AST) ->
Names = [N || {Tag,N,_} <- AST, Tag == stream orelse Tag == file orelse Tag == live],
case Names -- lists:usort(Names) of
[_|_] = NonUnique -> {error, {non_unique_names, NonUnique}};
[] -> ok
end.
validate_source_prefixes([{source,URLs,_}|Config]) ->
Prefixes = [case re:run(URL, "[^:]+://[^/]+/(.*)$", [{capture,all_but_first,binary}]) of
{match, [Prefix]} -> Prefix;
nomatch -> <<>>
end || URL <- URLs],
case lists:usort(Prefixes) of
[_] -> validate_source_prefixes(Config);
[_|_] -> {error, {different_source_prefixes,URLs}}
end;
validate_source_prefixes([]) -> ok;
validate_source_prefixes([_|Config]) -> validate_source_prefixes(Config).
validate_push_targets(AST) ->
BadTargets = [{S, Target} || {stream, S, #{push := STargets}} <- AST, Target <- STargets, not config2_helpers:is_valid_url(Target)],
case BadTargets of
[] -> ok;
_ -> {error, {bad_push_targets, BadTargets}}
end.
add_global_dvrs(AST) ->
add_global_dvrs2(AST, AST).
add_global_dvrs2([{Tag,Name,#{external_dvr := D} = Options}|AST], Config) ->
Opts1 = case [DVR || {dvr,N,#{} = DVR} <- Config, N == D] of
[#{} = DVR] -> Options#{dvr => DVR, dvr_global => DVR};
[] -> Options
end,
[{Tag,Name,Opts1}|add_global_dvrs2(AST, Config)];
add_global_dvrs2([], _) -> [];
add_global_dvrs2([Line|AST], Config) -> [Line|add_global_dvrs2(AST, Config)].
add_global_caches(AST) ->
add_global_caches2(AST, AST).
add_global_caches2([{Tag,Name,#{external_cache := C} = Options}|AST], Config) ->
Opts1 = case [Cache || {cache,N,#{} = Cache} <- Config, N == C] of
[#{} = Cache] -> Options#{cache => Cache, cache_global => Cache};
[] -> Options
end,
[{Tag,Name,Opts1}|add_global_caches2(AST, Config)];
add_global_caches2([], _) -> [];
add_global_caches2([Line|AST], Config) -> [Line|add_global_caches2(AST, Config)].
add_positions(AST0) ->
AST1 = set_entry_positions(AST0, stream, 1),
AST2 = set_entry_positions(AST1, notify, 1),
AST2.
set_entry_positions([{E1,N,Opts}|Config], E, P) when E1 == E ->
[{E1,N,Opts#{position => P}}|set_entry_positions(Config, E, P+1)];
set_entry_positions([Line|Config], E, P) ->
[Line|set_entry_positions(Config, E, P)];
set_entry_positions([], _, _) ->
[].
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment