우선 Airsonic에 대해서 설명하자면

Airsonic은 Subsonic에서 파생된 프로그램으로 Subsonic은 간단히 말해 미디어 서버 프로그램이다.

Subsonic은 서버내의 음악파일을 외부 클라이언트로 공유하여 들을 수 있도록 다양한 기능을 제공하는 서버 프로그램으로 버전 6 이전까지는 무료였다가 이후에는 유료로 변경되었다고 한다.

Airsonic은 Subsonic의 유료화에 반발하여 분리된 프로젝트로 Subsonic의 기능과 API 등을 가지고도 무료로 사용할 수 있는 서버 프로그램이다.

때문에 Subsonic과 연동되는 클라이언트 프로그램은 Airsonic에도 연결할 수 있다.

https://github.com/airsonic/airsonic

 

airsonic/airsonic

:satellite: :cloud: :notes:Airsonic, a Free and Open Source community driven media server (fork of Subsonic and Libresonic) - airsonic/airsonic

github.com

Subsonic은 클라이언트에서 사용할 수 있는 다양한 API를 제공하는데

http://www.subsonic.org/pages/api.jsp

 

Subsonic

maxBitRate No (Since 1.13.0) The maximum bit rate (in Kbps) for the user. Audio streams of higher bit rates are automatically downsampled to this bit rate. Legal values: 0 (no limit), 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320.

www.subsonic.org

Airsonic 또한 Subsonic의 API를 대부분 사용할 수 있다.

그 중에서도 이번에 수정해볼 API는 이것이다.

가사를 얻어오는 API인데

이전 포스트에서도 설명했지만 저 API를 실행시키면

Airsonic은 파라미터로 보내진 artist, title 정보를 이용해서

String url = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect?artist=" + artist + "&song=" + song; String xml = executeGetRequest(url); lyrics = parseSearchResult(xml);

이런 위의 URL을 통해 가사를 가져온다.

즉 저 사이트에 가사 정보가 없으면 가사를 가져오지 못한다는 뜻이다.

하지만 Airsonic은 오픈소스 프로젝트이므로 내가 코드를 수정해서 사용하는것이 가능하다.

이번에는 저 코드를 수정해서 Airsonic이 getLyric API를 요청받았을 때

저 URL이 아닌 로컬 환경에서 가사를 가져오도록 수정할 예정이다.

우선 Airsonic을 컴파일할 환경을 구성해야 한다.

환경을 구성하는 방법은 아래 사이트에서 설명해주고 있다.

https://airsonic.github.io/docs/install/source/

 

Airsonic

Airsonic, a Free and Open Source community driven media server, providing ubiquitous access to your music.

airsonic.github.io

계획은 단순한데

Airsonic에서 제공하는 getLyrics API를 실행하면

Airsonic이 자신의 musicDirectory에서 title 파라미터값을 이용해 파일을 검색해 가사 파일을 찾아내는 것이다.

만약 가사파일을 찾으면 가사파일을 읽어서 API를 요청한 클라이언트한테 보내주면 된다.

Airsonic 서버가 로컬 가사파일을 읽어서 클라이언트에게 가사를 전송하도록 수정할 것이다.

나는 윈도우 환경에서 MVN, JDK 버전등을 맞추고 컴파일을 시도해 보았더니

윈도우 환경에서는 컴파일 할 수 없다는 메세지가 출력되어서 Virtualbox 가상머신에 MX 리눅스를 설치했다.

이후 설명 페이지에 나와있는대로 환경을 구성하고 소스코드를 수정하지 않은 상태에서 컴파일하여 .war 파일을 생성하여 테스트 해보았다.

tomcat 9버전대에 올려보니 정상동작하는것을 확인할 수 있었다.

참고로 컴파일에는 한 20분정도 시간이 걸렸는데 실제 컴파일하는데는 5분이면 되는것 같은데

Airsonic 컴파일을 할 때 실행되는 기능 테스트 동작하는데에만 한 15분이 넘게 걸리는 것 같다.

어쨋든 컴파일이 된것을 확인하고 나서는 코드를 수정해 주었다.

참고로 코드 규칙이 정말 까다롭게 되어있는데

탭문자쓰면 탭문자 쓰지 말라고 에러나고 띄어쓰기 한개때문에 에러나고 if , for 문 앞뒤로 한칸씩 띄어쓰라고 에러나고 대괄호 내려썼다고 에러나고 == 쓰지말고 equals 쓰라고 에러나고

얼마나 깐깐하게 만들었는지 알 수 있었다.

그렇게 코드를 수정하고 리눅스 환경에서 Airsonic에 getLyrics API를 날려보니

 

SERVER='http://127.0.0.1:8080/airsonic'
CLIENT='CLI'
USERNAME='admin'
PASSWORD='admin'
SALT="$(openssl rand -hex 20)"
TOKEN="$(echo -n "${PASSWORD}${SALT}" | md5sum | awk '{ print $1 }')"
echo ${SALT}
echo ${TOKEN}
curl "${SERVER}/rest/getLyrics.view?u=${USERNAME}&t=${TOKEN}&s=${SALT}&v=1.15.0&c=${CLIENT}&artist=&title=Anemone"

 

가사가 정상적으로 출력되었다.

안드로이드 클라이언트에서도 가사가 정상적으로 출력되는것을 확인했다.

다만 lrc 파일 특유의 시간표시가 남아있는데

저 시간정보를 이용하려면 서버 프로그램이 아닌 클라이언트 프로그램을 수정해야만 한다.

Subsonic API는 가사정보가 LRC 형식이 아닌 그냥 단순 텍스트로 전송되기때문에

저 시간정보를 이용할 수 있는 클라이언트가 없다.

만약 저걸 이용하려면 클라이언트 기능까지 직접 만들어야된다는건데...

일단은 저 시간정보를 빼고 깔끔하게 보이는 텍스트 가사로 출력하는것에 만족해야겠다.

시간정보를 제거한 가사 출력

이번에 추가한 코드는 아래와 같다.

 

 @RequestMapping("/getLyrics")
    public void getLyrics(HttpServletRequest request, HttpServletResponse response) {
        request = wrapRequest(request);
        String artist = request.getParameter("artist");
        String title = request.getParameter("title");

        Lyrics result = new Lyrics();
        result.setArtist(artist);
        result.setTitle(title);
        result.setContent(getContentMain(title));

        Response res = createResponse();
        res.setLyrics(result);
        jaxbWriter.writeResponse(request, response, res);
    }

    public String getContentMain(String title) {
        for (org.airsonic.player.domain.MusicFolder folder: settingsService.getAllMusicFolders()) {
            File[] fileList = folder.getPath().listFiles();
            for (int i = 0; i < fileList.length; i++) {
                File file = fileList[i];
                if (file.isFile() && file.getName().contains(title) && file.getName().toLowerCase().contains(".lrc")) {
                    String str = "";
                    try {
                        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF8"));
                        br.readLine();
                        br.readLine();
                        br.readLine();
                        String line = br.readLine();
                        while (str != null) {
                            str += line.substring(10) + "\n";
                            line = br.readLine();
                        }
                        br.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                    return str;
                } else if (file.isDirectory()) {
                    String r = getContentSub(file, title);
                    if (!r.isEmpty()) {
                        return r;
                    }
                }
            }
        }
        return "";
    }

    public String getContentSub(File subdir, String title) {
        File[] fileList = subdir.listFiles();
        for (int i = 0; i < fileList.length; i++) {
            File file = fileList[i];
            if (file.isFile() && file.getName().contains(title) && file.getName().toLowerCase().contains(".lrc")) {
                String str = "";
                try {
                    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF8"));
                    br.readLine();
                    br.readLine();
                    br.readLine();
                    String line = br.readLine();
                    while (line != null) {
                        str += line.substring(10) + "\n";
                        line = br.readLine();
                    }
                    br.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                return str;
            } else if (file.isDirectory()) {
                String r = getContentSub(file, title);
                if (!r.isEmpty()) {
                    return r;
                }
            }
        }
        return "";
    }

 

getLyrics 함수를 수정하고 getContentMain, getContentSub 함수를 추가해주었다.

새벽 늦게까지 했지만 기능을 완성하니 뿌듯하고 기분이 좋다.

컴파일 시간이 짧았으면 좀 더 빨리 끝났을것 같은데 테스트를 스킵하는 명령이 있는지 찾아볼걸 그랬다 싶다.

아무튼 이로써 무료 음악 스트리밍 서버를 이용하면서 노래의 가사까지 볼 수 있게 되었다.

+ Recent posts