-module(media).
-export([init/0,insert/3, ask/2, all/0, play/2, vote/2, devote/2, lock_process/2]).
-define(TESTPATTERN, "../ac/*.mp3").
-define(TIMEOUT, 100000000).
% Since we are not willing to calculate and deliver all the id3 tags everytime they are requested,
% we try to get something persistent with mnesia.
% Concerning the parsing of id3tags we use the library id3v2 by Brendon Hogger. For detailed information take a
% look at the header of the library.
% What is an entry in our database made of? By the way the filepath includes the filename.
-record(track, {title, artist, votes, locked, filepath }).
% Before this module becomes usable, we must initialize it with the following steps:
% 1. Initialize the mnesiadatabase and create the table within it.
% 2. Parse the mp3s in the working directory and add them to the database
% 3. Get into a loop so the database can be actually queried and files can be played.
init() ->
mnesia:create_schema([node()]),
mnesia:start(),
mnesia:create_table(track, [{attributes, record_info(fields, track)}]),
read_files(filelib:wildcard(?TESTPATTERN),0,0),
io:format("Initialisation of mnesia successful.~n"),
io:format("Starting to play music~n"),
start_playing().
% uses the algorithm of Brendon Hogger to split the id3-tags and
% inserts the songs into the Database
read_files([FN|Rest],Total,Fail) ->
case id3v2:read_file(FN) of
{ok, Props} -> % insert entry into mnesia DB
Artist = proplists:get_value(tpe1, Props),
Title = proplists:get_value(tit2, Props),
insert(bitstring_to_list(Artist), bitstring_to_list(Title), FN),
read_files(Rest, Total+1, Fail);
not_found -> read_files(Rest, Total+1, Fail+1)
end;
read_files([],Total,Fail) -> io:format("Total: ~w, Failed: ~w~n", [Total, Fail]).
% Our Runloop to play music all the time, play waits on exit_status
start_playing() ->
{Artist, Title} = search_best(media:all(), 0,0),
play(Artist, Title).
% Basic insertion of entrys into the database. Some entries are left out because they are 0 or false.
insert(Artist, Title, Filepath) ->
F = fun() ->
mnesia:write(#track{artist = Artist, title = Title, votes = 0, locked = false, filepath = Filepath})
end,
mnesia:transaction(F).
% search the track with the highest votes and return {Artist, Title}
search_best([Head|Rest], Max_Votes, Track) ->
if
((Max_Votes =< Head#track.votes) and (Head#track.locked == false)) ->
search_best(Rest, Head#track.votes, Head);
true -> search_best(Rest, Max_Votes, Track)
end;
search_best([], 0, 0) -> reset_all(all());
search_best([], _, Track) -> {Track#track.artist, Track#track.title}.
% if nothing is playable anymore just reset them and start playing again...
reset_all([Head|Rest]) ->
unlock(Head#track.artist, Head#track.title),
reset_all(Rest);
reset_all([]) -> ok.
% We want to query in order to simplify the next calls.
ask(Artist, Title) ->
F = fun() ->
mnesia:match_object({track, Title, Artist, '_', '_', '_'})
end,
{atomic, Results} = mnesia:transaction(F),
Results.
% Just in case the client is interested in everything we got.
all() ->
F = fun() ->
mnesia:match_object({track, '_','_','_','_','_'})
end,
{atomic, Results} = mnesia:transaction(F),
Results.
% We want to play mp3s from our database. After we play them they will become locked.
% In practice we are going to set their locked variable to true and spawn a process which will unlock them after a certain time.
% Well this could be considered abuse.
play(Artist, Title) ->
[Head|_] = ask(Artist, Title),
{_, Title, Artist, _, _, Fp} = Head,
Port = erlang:open_port({spawn_executable, "/usr/bin/mplayer"}, [{args, [Fp]}, exit_status]),
reset_votes(Artist, Title),
spawn(media, lock_process, [Artist, Title]),
io:format("playing: ~s, Artist: ~s~n", [Title, Artist]),
receive
{Port, {exit_status, 0}} -> start_playing();
{Port, {exit_status, S}} -> throw({commandfailed, S})
end.
% Of course we need a query to find out whats actually the most wished for track.
% We will do it by requesting all the records from the database and then iteramte over just taking a look at the vote
% variable, so it is like list of integers. In case no tracks were voted for we just take the first track we find and play it. Of course it is locked afterwards so another will be choosen.
vote(Artist, Title) ->
F = fun() ->
[Head|_] = ask(Artist, Title),
Votes = Head#track.votes + 1,
New = Head#track{votes = Votes},
mnesia:write(New)
end,
mnesia:transaction(F).
devote(Artist, Title) ->
F = fun() ->
[Head|_] = ask(Artist, Title),
Votes = Head#track.votes - 1,
New = Head#track{votes = Votes},
mnesia:write(New)
end,
mnesia:transaction(F).
reset_votes(Artist, Title) ->
F = fun() ->
[Head|_] = ask(Artist, Title),
New = Head#track{votes = 0},
mnesia:write(New)
end,
mnesia:transaction(F).
lock(Artist, Title) ->
F = fun() ->
[Head|_] = ask(Artist, Title),
New = Head#track{locked = true},
mnesia:write(New)
end,
mnesia:transaction(F).
unlock(Artist, Title) ->
F = fun() ->
[Head|_] = ask(Artist, Title),
New = Head#track{locked = false},
mnesia:write(New)
end,
mnesia:transaction(F).
lock_process(Artist, Title) ->
lock(Artist, Title),
receive
after ?TIMEOUT ->
unlock(Artist, Title) end.