1. Готовим базу данных
В качестве базы данных будем использовать MySQL. База данных содержит две таблицы: mp3 — данные об аудиофайлах и mp3_tmp — таблица используется при обновлении базы данных. По структуре обе таблицы идентичны.
Таблицы содержат следующие поля:
path — путь к файлу на диске, PRIMARY KEY;
artist — исполнитель;
album — название альбома;
title — название трека;
year — год записи;
number — номер трека в альбоме;
length — длина трека в формате mm:ss.
Итак, SQL для создания таблицы:
DROP TABLE IF EXISTS `mp3_tmp`;
CREATE TABLE `mp3_tmp` (
`path` varchar(250) NOT NULL,
`artist` varchar(250) DEFAULT NULL,
`album` varchar(250) DEFAULT NULL,
`title` varchar(250) DEFAULT NULL,
`year` varchar(40) DEFAULT NULL,
`length` varchar(40) DEFAULT NULL,
`number` varchar(40) DEFAULT NULL,
PRIMARY KEY (`path`))
ENGINE=InnoDB DEFAULT CHARSET=utf8;
Для формирования таблицы используем программу на Java.
Для начала сформируем список всех mp3-файлов в папке с музыкой:
FileFinder ff = new FileFinder(); //класс для поиска файлов, описание алгоритма опускаю
List<File> files = ff.findFiles(initPath, ".*\.mp3"); // ищем все mp3-файлы в директории initPath
Далее вносим данные о файлах во временную таблицу
(используется JDBC — для соединения с базой данных и
библиотека jaudiotagger — для сканирования тегов mp3):
for (File file : files) {
// Подготавливаем запрос:
PreparedStatement preparedStatement =
connect.prepareStatement("insert into `mp3_tmp` "
+ "(path, artist, album, title, year, number, length) "
+ "values (?, ?, ?, ?, ?, ?, ?)");
String fullName = file.getCanonicalPath();
String fileName = fullName.replace(initPath,""); //будем хранить в базе данных относительный путь
String length = "";
try {
AudioFile af = AudioFileIO.read(file);
int len = af.getAudioHeader().getTrackLength(); //получаем длину трека
int sec = len%60;
int min = (len-sec)/60;
length = String.format("%02d:%02d", min, sec); //форматируем длину
MP3File mp3f = new MP3File(file);
Tag tag = mp3f.getTag(); //получаем теги
String artist = tag.getFirst(FieldKey.ARTIST);
String album = tag.getFirst(FieldKey.ALBUM);
String title = tag.getFirst(FieldKey.TITLE);
String year = tag.getFirst(FieldKey.YEAR);
String number = tag.getFirst(FieldKey.TRACK);
//приводим номер трека к трехзначному виду (для удобства сортировки):
if (!number.equals("")){
Integer num = Integer.parseInt(number);
if (num < 10) {
number = "00"+num.toString();
} else if (num < 100) {
number = "0"+num.toString();
}
}
//подставляем данные в запрос:
preparedStatement.setString(1, fileName);
preparedStatement.setString(2, artist);
preparedStatement.setString(3, album);
preparedStatement.setString(4, title);
preparedStatement.setString(5, year);
preparedStatement.setString(6, number);
} catch (Exception e) {
//в случае ошибки получения тега, заполняем пустыми строками:
preparedStatement.setString(1, fileName);
preparedStatement.setString(2, "");
preparedStatement.setString(3, "");
preparedStatement.setString(4, "");
preparedStatement.setString(5, "");
preparedStatement.setString(6, "");
} finally {
preparedStatement.setString(7, length);
preparedStatement.executeUpdate(); //добавляем трек в базу данных
}
}
//И, наконец, обновляем основную таблицу:
statement.execute("DROP TABLE IF EXISTS `mp3`");
statement.execute("CREATE TABLE `mp3` LIKE `mp3_tmp`");
statement.execute("INSERT INTO `mp3` SELECT * FROM `mp3_tmp`");
2. Backend плеера
Бэкенд плеера состоит из двух сервлетов:
@WebServlet("/getlist") — возвращает список треков по поисковому запросу;
@WebServlet("/hint") — возвращает подсказки по первым буквам.
Подробнее про сервлет GetList.
//формируем SQL-запрос
String query = "select path, artist, title, album, year, length from `mp3` where ";
String[] qArr0 = request.getParameter("query").split("\|");
for (int k=0; k<qArr0.length; k++) {
if (k>0) query += " or ";
String[] qArr = qArr0[k].split(" ");
query += "concat(title,' ',album,' ',artist) like "+"'%"+qArr[0]+"%' ";
for (int j=1; j<qArr.length; j++) {
query += "and concat(title,' ',album,' ',artist) like "+"'%"+qArr[j]+"%' ";
}
}
query += "group by concat(year,album,number,title,artist,length) ";
query += "order by concat(year,' ',album,' ',number,' ',title,' ',artist)";
Statement statement = connect.createStatement();
resultSet = statement.executeQuery(query); //получаем данные
//Используем библиотеку GSON для формирования json
Gson gson = new GsonBuilder().create();
int i=0;
while (resultSet.next()) {
if (i++>0) playlist+="n";
String artist = resultSet.getString("artist");
String title = resultSet.getString("title");
String album = resultSet.getString("album");
String year = resultSet.getString("year");
String path = resultSet.getString("path");
path = musicPath+path;
String length = resultSet.getString("length");
Track track = new Track(title, artist, album, year, path, length);
playlist += gson.toJson(track);
}
Сервлет Hint выполняет следующий SQL-запрос:
// String text = request.getParameter("query");
Statement statement = connect.createStatement();
resultSet = statement
.executeQuery("select title as 'str' from "
+ "`mp3` where title like '%"
+text+"%' union "
+ "select artist as 'str' from "
+ "`mp3` where artist like '%"
+text+"%' union "
+ "select album as 'str' from "
+ "`mp3` where album like '%"
+text+"%' group by str order by str limit 10");
и возвращает данные в json-формате.
3. HTML5 frontend
В качестве фронтенда плеера используем HTML5 и jQuery. Здесь интересны следующие моменты.
Формирование плейлиста:
$('#search').click(function(){
//Получаем трек-лист от сервлета
var uri = '/mp3player/getlist?query='+$('#query').val();
$.get(encodeURI(uri),function(data){
window.playlist = [];
window.number = 0;
var array = data.split('n');
for (i=0; i<array.length; i++) {
window.playlist[i] = $.parseJSON(array[i]);
}
// Формируем HTML списка:
var li_list = '<ol>';
var album = '';
var year = '';
var alb_num = 0;
for (i=0; i<window.playlist.length; i++) {
if (album != window.playlist[i].album || i == 0) {
album = window.playlist[i].album;
year = window.playlist[i].year;
var str = 'Неизвестный альбом';
if (album) str = album;
if (year) str+= ' | ' + year;
li_list += '<h4 class="album" alb_num='+alb_num+'>'+str+'</h4>';
alb_num++;
}
li_list += '<li class="track" id='+i+' alb_num='+alb_num+'>'
+window.playlist[i].title+' ('+window.playlist[i].artist+') | '+window.playlist[i].length+'</li>';
}
li_list += '</ol>';
// Обновляем страницу:
$('#list').html(li_list);
if (window.playlist.length>1) {
window.alb_num = $('li#'+window.number).attr('alb_num');
$('#list').scrollTop($('li#'+window.number).offset().top-$('li#0').offset().top);
}
// При клике по треку начинаем проигрывание:
$('li.track').click(function(){
$('#toogle').prop('disabled',false);
$('#toogle').prop('checked',true);
$('li#'+window.number).attr('style','font-weight:normal; font-style:normal');
window.number = $(this).attr('id');
if (window.playlist.length>1) {
window.alb_num = $('li#'+window.number).attr('alb_num');
$('#list').animate({scrollTop:($('li#'+window.number).offset().top-$('li#0').offset().top)},200);
}
$('li#'+window.number).attr('style','font-weight:bold; font-style:italic');
$('#my_audio').trigger('pause'); // #my_audio - html5 элемент audio. Ставим на паузу.
var track = window.playlist[window.number];
$('#title').html('<h3>'+(Number(window.number)+1)+': '+track.title+' ('+track.artist+')</h3>');
$('#my_audio').attr('src',track.mp3); // Устанавливаем новый источник
$('#my_audio').trigger('play'); // Включаем проигрывание
});
});
});
Подсказки:
// используем плагин jquery.autocomplete
$('#query').autocomplete({serviceUrl:'/mp3player/hint'});
Изменение громкости:
$('#volume_slider').slider({orientation:'vertical',range:'min',min:0,max:100,value:100,stop:function(event,ui){
var volume = (ui.value*1.0)/100.0;
$('#my_audio').prop('volume',volume);
} });
Проматывание трека:
$('#time_slider').slider({disabled:true,range:'min',min:0,max:1000,stop:function(event, ui) {
var dur = $('#my_audio').prop('duration');
var cur = (dur*ui.value)/1000;
$('#my_audio').prop('currentTime',cur);
} });
Интерактивное отображение текущей позиции:
$('#my_audio').bind('timeupdate',function(){
var cur = $('#my_audio').prop('currentTime');
var dur = $('#my_audio').prop('duration');
var left = dur - cur;
if (dur) {
var slider_val = cur*1000/dur;
cur = Math.floor(cur+0.5);
dur = Math.floor(dur+0.5);
left = Math.floor(left+0.5);
cur_s = cur % 60;
cur_m = (cur - cur_s)/60;
dur_s = dur % 60;
dur_m = (dur - dur_s)/60;
left_s = left % 60;
left_m = (left - left_s)/60;
cur_s = $.formatNumber(cur_s,{format:'00',locale:'ru'});
cur_m = $.formatNumber(cur_m,{format:'00',locale:'ru'});
dur_s = $.formatNumber(dur_s,{format:'00',locale:'ru'});
dur_m = $.formatNumber(dur_m,{format:'00',locale:'ru'});
left_s = $.formatNumber(left_s,{format:'00',locale:'ru'});
left_m = $.formatNumber(left_m,{format:'00',locale:'ru'});
$('#time_cur').text(cur_m+':'+cur_s+' ')
$('#time_dur').text(' '+left_m+':'+left_s);
$('#time_slider').slider('option',{disabled:false});
$('#time_slider').slider('value',slider_val);
}
});
Переключение на следующий трек по окончании:
$('#my_audio').on('ended',function(){
var n = (Number(window.number) + 1) % window.playlist.length;
$('li#'+n).trigger('click');
});
Готовый плеер можно увидеть здесь:
http://home.tabatsky.ru/mp3player/homeaudio/desktop.jsp
4. Аудиосервер
Так получилось, что у меня без дела пылились хорошие колонки и одно устройство на андроиде. В итоге появилась идея написать для андроида аудиосервер и слушать музыку через колонки.
Аудиосервер состоит из одной Activity и двух сервисов — HttpService и PlayerService.
И так, подробнее.
HttpService принимает HTTP-запросы и отправляет команды PlayerService.
public int onStartCommand(Intent intent, int flags, int startId) {
t = new Thread() {
public void run() {
try {
ss = new ServerSocket(port, backlog, InetAddress.getByName(addr));
while (true) {
// Принимаем и обрабатываем запросы
Socket s = ss.accept();
Thread tt = new Thread(new SocketProcessor(s));
tt.start();
tt.join(50);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
};
t.start();
// BroadcastReceiver для связи c PlayerService
filter = new IntentFilter("Http");
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent)
{
String res = intent.getStringExtra("result");
if (res!=null) result = "'"+res+"'";
String stat = intent.getStringExtra("status");
if (stat!=null) status = "'"+stat+"'";
String cur = intent.getStringExtra("currentTime");
if (cur!=null) currentTime = cur;
String dur = intent.getStringExtra("duration");
if (dur!=null) duration = dur;
}
};
registerReceiver(receiver, filter);
return START_STICKY;
}
// Класс отвечает за обработку HTTP-запросов
private class SocketProcessor implements Runnable {
private Socket s;
private InputStream is;
private OutputStream os;
private SocketProcessor(Socket s) throws Throwable {
this.s = s;
this.is = s.getInputStream();
this.os = s.getOutputStream();
}
public void run() {
try {
// Считываем заголовки HTTP-запроса:
readInputHeaders();
// Формируем и возвращаем результат запроса в формате jsonp:
String response = "";
response += "window.result="+result+"; ";
response += "window.status="+status+"; ";
response += "window.currentTime="+currentTime+"; ";
response += "window.duration="+duration+"; ";
writeResponse(response);
} catch (Throwable t) {
/*do nothing*/
} finally {
try {
s.close();
} catch (Throwable t) {
/*do nothing*/
}
}
System.err.println("Client processing finished");
}
private void writeResponse(String s) throws Throwable {
String response = "HTTP/1.1 200 OKrn" +
"Server: YarServer/2009-09-09rn" +
"Content-Type: text/javascriptrn" +
"Content-Length: " + s.length() + "rn" +
"Connection: closernrn";
String result = response + s;
os.write(result.getBytes());
os.flush();
}
private void readInputHeaders() throws Throwable {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String data = "";
String action = "";
String src = "";
String volume = "";
while(true) {
String s = br.readLine();
//System.err.println(s);
//data += s+"n";
if (s.startsWith("GET /favicon.ico")) return;
if (s.startsWith("GET /?")) {
s = s.replace("GET /?", "").replace(" HTTP/1.1", "");
String[] arr = s.split("&");
for (String str: arr) {
str = URLDecoder.decode(str, "UTF-8");
if (str.startsWith("action="))
action = str.replace("action=", "");
if (str.startsWith("src="))
src = str.replace("src=", "");
if (str.startsWith("volume="))
volume = str.replace("volume=", "");
}
}
if(s == null || s.trim().length() == 0) {
break;
}
}
// Отправляем broadcast плееру
Intent in = new Intent("Player");
in.putExtra("action", action);
in.putExtra("src",src);
in.putExtra("volume", volume);
sendBroadcast(in);
}
}
}
PlayerService отвечает за проигрывание музыки:
public int onStartCommand(Intent intent, int flags, int startId) {
player = new MediaPlayer();
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
result = "ok";
status = "playing";
}
});
player.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
//player.stop();
result = "ok";
if (mp.getCurrentPosition()>mp.getDuration()-2500)
status = "finished";
}
});
//принимаем команды от HttpService
filter = new IntentFilter("Player");
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent)
{
String action = intent.getStringExtra("action");
if (action.equals("play")) {
player.start();
status = "playing";
}
if (action.equals("pause")) {
player.pause();
status = "paused";
}
if (action.equals("changesrc")) {
src = intent.getStringExtra("src");
try {
status = "preparing";
player.reset();
//host - адрес нашего сервера с музыкой
player.setDataSource(host+src);
player.prepare();
//player.start();
result = "ok";
} catch (Exception e) {
result = "error";
}
}
if (action.equals("setvolume")) {
float volume = Float.parseFloat(intent.getStringExtra("volume"));
player.setVolume(volume, volume);
}
}
};
registerReceiver(receiver, filter);
//каждые 100 мс отправляем данные в HttpService
t = new Thread() {
public void run() {
while (true) {
in = new Intent("Http");
in.putExtra("result", result);
//in.putExtra("status", (player.isPlaying()?"playing":"paused"));
in.putExtra("status", status);
in.putExtra("currentTime",
Integer.valueOf(player.getCurrentPosition()/1000).toString());
in.putExtra("duration",
Integer.valueOf(player.getDuration()/1000).toString());
sendBroadcast(in);
try {
sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
//e.printStackTrace();
}
}
}
};
t.start();
return START_STICKY;
}
Осталось только немного изменить соответствующий код фронтенда.
//Изменяем трек
$('li.track').click(function(){
........
// window.audioServer - адрес сервера с музыкой
var url = window.audioServer+'/?action=changesrc&src='+track.mp3+'&t='+(new Date().getTime());
$.ajax({
url: encodeURI(url),
type: 'GET',
crossDomain: true,
dataType: 'jsonp'
});
.........
}
//Изменяем громкость
$('#volume_slider').slider({orientation:'vertical',range:'min',min:0,max:100,value:100,stop:function(event,ui){
var volume = (ui.value*1.0)/100.0;
//$('#my_audio').prop('volume',volume);
window.setVolume(volume);
} });
window.setVolume = function(volume) {
var url = window.audioServer+'/?action=setvolume&volume='+volume
+'&t='+(new Date().getTime());
$.ajax({
url: encodeURI(url),
type: 'GET',
crossDomain: true,
dataType: 'jsonp'
});
};
//Слушаем статус плеера
window.startUpdate = function() {
window.update_interval = setInterval(function(){
var url = window.audioServer+'/?action=update'+'&t='+(new Date().getTime());;
$.ajax({
url: encodeURI(url),
type: 'GET',
crossDomain: true,
dataType: 'jsonp'
});
if (window.result=='error') {
//alert('Ошибка');
} else if (window.result=='ok') {
if (window.status=='playing') {
if (!window.fin) {
window.fin = 0;
} else {
window.fin--;
}
window.updateTime(window.currentTime, window.duration);
} else if ((window.fin==0)&&(window.status=='finished')) {
window.fin = 2;
$('#fwd').trigger('click');
}
}
},500);
};
Итог: проигрыванием музыки на колонках можно управлять с любого устройства, подключенного к домашней сети.
Автор: jatx