*출처 : http://www.microsoft.com/korea/msdn/coding4fun/webcoder/musiclib/default.aspx

ASP.NET 2.0으로 웹 사이트를 구축하여 음악 라이브러리 탐색

Jeff Key
InRule Technology

소개

컴퓨터 소유자라면 대부분 미디어 라이브러리를 가지고 있습니다. 컴퓨터 가격이 내려가고 네트워크의 연결 및 사용이 편리해짐에 따라 홈 네트워크를 설치하는 수도 더욱 증가할 전망입니다. 서버는 일반 가정에 설치하기 쉽지 않기 때문에 네트워크상의 컴퓨터에 정보가 배포되는데 이로 인해 정보의 관리 및 검색에 어려움이 있습니다. 이러한 어려움이 가장 잘 나타나는 부분이 음악 라이브러리일 것입니다.

대부분의 미디어 플레이어는 인식하는 모든 노래를 포함하는 개인 데이터베이스를 유지 관리합니다. 단일 컴퓨터에서 이러한 데이터베이스를 최신 상태로 유지하기는 매우 번거로우며 컴퓨터가 여러 대라면 거의 불가능하게 여겨지기 십상입니다.

일반적인 해결책은 모든 음악 파일을 한 대의 컴퓨터에 모아 두고 다른 컴퓨터의 미디어 라이브러리를 가리키도록 하여 이 컴퓨터의 파일로 최신 상태를 유지하는 것입니다. 그러나 이 방법을 실제로 시도해 보면 그럭저럭 괜찮기는 하지만 완벽하지는 않다는 사실을 알게 됩니다. 개인적으로 저는 음악을 듣기만 할 뿐 관리하는 성향은 아닙니다.

우리 집 홈 네트워크는 TV를 작동하고 모든 음악이 저장되어 있는 Windows Media Center 컴퓨터와 워크스테이션으로 구성되어 있으며 가끔 랩톱이 추가되기도 합니다. 워크스테이션을 Media Center의 음악으로 업데이트하려면 항상 많은 인내와 노력이 필요하며 랩톱에서 이러한 작업을 수행하기란 더욱 어렵습니다. 저는 라이브러리의 정확성과 관계없이 집안 어디서나 어느 컴퓨터를 통해서든 간단하고 명료한 방식으로 음악을 감상하고 싶었습니다. 그래서 선택한 방법은 원하는 음악을 가능한 편히 들을 수 있도록 해주는 작은 웹 사이트를 구축하는 것이었습니다. 이를 위해 Microsoft Visual Web Developer 2005 Express Edition 베타 2를 사용했습니다.

오랜 친구인 COM

예전에는 사용하는 언어에 관계없이(COM을 지원하는 경우) 코드에서 모든 언어로 작성된 라이브러리를 사용할 수 있게 해주는 COM (영문)이라는 기술이 있었습니다. 아마 지금의 .NET과 비교할 수 있을 것입니다. COM은 몇 가지 문제를 해결하는 데 두각을 나타냈지만 시간이 지날수록 더욱 명백해지는 다른 문제들을 야기시켰습니다. .NET은 이러한 많은 문제를 해결하는 데 가장 효과적인 솔루션입니다.

몇 년 전, Microsoft는 COM을 사용하여 운영 체제와 응용 프로그램 서비스를 출시하기 시작했습니다. 이로 인해 일반 개발자도 Microsoft Word, Windows Scripting Host, Windows Media Player를 비롯한 수많은 Microsoft 제품을 대상으로 코드를 작성할 수 있게 되었습니다. COM은 지원하는 언어, 프레임워크 등이 워낙 많기 때문에 여전히 Microsoft 영역에서 중요한 역할을 수행하고 있을 뿐 아니라, .NET이 지원되기 전까지는 Microsoft 응용 프로그램에서 COM이 지원되는 경우가 일반적이었습니다. 다행히도 .NET은 대개의 경우 COM과의 호환성이 매우 뛰어납니다.

이와 같이 COM에 대해서 간략히 언급한 것은 제가 구축한 사이트가 Windows Media Player COM 구성 요소를 사용하여 미디어 라이브러리에 액세스하기 때문입니다. COM 구성 요소에 대한 참조를 추가하는 것은 .NET 어셈블리에 대한 참조를 추가하는 것만큼이나 간단합니다. 웹 사이트 메뉴를 클릭하고 참조 추가를 선택한 후 참조 추가 대화 상자에서 COM 탭을 클릭하면 됩니다.

그림 1: Windows Media Player COM 구성 요소에 대한 참조 추가 데이터베이스

데이터베이스

WMP 라이브러리 정보의 하위 집합에 빠르게 액세스하려면 Artist, Album, Database 및 Track 클래스로 구성된 메모리 내부 데이터베이스가 있어야 합니다. Database 클래스에는 다음과 같이 Refresh 메서드를 호출하여 데이터베이스를 채우는 정적(Visual Basic에서는 공유) 생성자가 있습니다.

    Private Shared Sub Refresh()
        Dim wmp As WindowsMediaPlayer = New WindowsMediaPlayer
        Dim playlist As IWMPPlaylist = wmp.mediaCollection.getAll()
        Dim artistDictionary As Dictionary(Of String, Artist) = _
New Dictionary(Of String, Artist) For i As Integer = 0 To playlist.count - 1 Dim media As IWMPMedia = playlist.Item(i) Dim albumArtistName As String = media.getItemInfo("AlbumArtist") Dim albumName As String = media.getItemInfo("Album") Dim trackName As String = media.getItemInfo("Title") Dim trackLocation As String = media.getItemInfo("SourceUrl") Dim trackNumberString As String = media.getItemInfo("OriginalIndex") Dim theArtist As Artist Dim artistSortName As String = Artist.GetSortName(albumArtistName) If Not artistDictionary.TryGetValue(artistSortName, theArtist) Then theArtist = New Artist(albumArtistName) artistDictionary.Add(artistSortName, theArtist) End If Dim theAlbum As Album If Not theArtist.Albums.TryGetValue(albumName, theAlbum) Then theAlbum = New Album(albumName, theArtist) theArtist.Albums.Add(albumName, theAlbum) End If Dim theTrack As Track If Not theAlbum.Tracks.TryGetValue(trackName, theTrack) Then Dim trackNumber As Integer If Integer.TryParse(trackNumberString, trackNumber) Then theTrack = New Track(trackNumber, trackName, trackLocation) Else theTrack = New Track(trackName, trackLocation) End If theTrack.Album = theAlbum theAlbum.Tracks.Add(trackName, theTrack) End If Next ArtistList.AddRange(artistDictionary.Values) ArtistList.Sort() End Sub

WMP 라이브러리는 미디어 항목으로 구성된 단층 구조이므로 데이터베이스를 채우면서 Artist -> Albums -> Tracks 계층 구조를 수동으로 만들어야 합니다. WMP 라이브러리에 액세스하는 것은 간단합니다. 즉, WindowsMediaPlayer 개체의 mediaCollection 속성에서 getAll 메서드를 호출하면 라이브러리에 있는 모든 항목(IWMPPlaylist 인터페이스로 나타남)의 재생 목록이 반환됩니다. IWMPMedia 인터페이스로 나타나는 각 미디어 항목에서 필요한 정보를 수집하는 작업도 IWMPMedia의 getItemInfo 메서드를 호출하여 간단히 수행할 수 있습니다. 미디어 항목에 대한 관련 정보를 모두 수집했으면 artist, album 및 track 개체를 검색하거나 만들어야 합니다.

여기서는 generic 개체인 Dictionary를 사용하여 재생 목록을 반복하면서 artist를 저장했습니다. 일부 미디어 컬렉션은 수만 개의 항목으로 이루어져 있으므로 ArtistList를 통해 모든 미디어 항목을 무작위로 검색하면 가뜩이나 느린 동작이 더 느려지게 됩니다. (Dictionary 개체가 더 빠른 이유를 설명하는 것은 이 기사의 범위를 벗어납니다. 자세한 내용은 Scott Mitchell의 데이터 구조 관련 기사 (영문) 시리즈에서 Dictionary와 같은 기능을 가지며 generic이 아닌 Hashtable 클래스에 대한 검토 내용을 참조하십시오.)

웹 사이트 디자인

간단히 나타내기 위해 Artists, Artist's Albums 및 Album이라는 세 개의 페이지를 만들겠습니다.

그림 2: Artists 페이지

Artists 페이지(그림 2)에는 한 번에 10개의 가수 그룹이 나타납니다. 그룹을 클릭할 때마다 해당 그룹이 나타나며 이는 사용자가 검색하는 가수로 이동할 때까지 계속됩니다.

그림 3: Albums 페이지

Albums 페이지(그림 3)에는 표지를 포함하여 가수의 모든 앨범이 나타납니다. 앨범 이름을 클릭하면 해당 앨범 페이지로 이동하며 재생 단추를 클릭하면 앨범의 트랙이 포함된 재생 목록이 Windows Media Player에서 실행됩니다.

그림 4: Album 페이지

Album 페이지에는 앨범 표지의 큰 그림과 트랙이 표시됩니다. 사용자는 전체 앨범이나 개별 트랙을 재생할 수 있습니다.

각 페이지는 개체 데이터 바인딩을 사용하며 공통 마스터 페이지를 공유합니다. ASP.NET에는 마스터 페이지라는 새로운 기능이 있어 웹 사이트에 대해 일관된 레이아웃을 만들고 이를 페이지마다 복제할 필요 없이 공통적인 기능을 공유할 수 있습니다. 이 웹 사이트의 마스터 페이지는 CSS(Cascading Style Sheet) 정보를 공유하고 "브레드크럼 막대(breadcrumb bar)", 즉 ASP.NET에서 호출하는 SiteMapPath를 호스트하는 데 사용됩니다. 여기서는 SiteMapPath 컨트롤에 대해 다루지 않겠지만 이를 정적 정의가 아닌 프로그래밍 방식으로 구동하려는 개발자라면 구현 내용에 대해 관심을 가질 필요가 있습니다. 마스터 페이지에 대한 자세한 내용은 Fritz Onion의 Master Your Site Design with Visual Inheritance and Page Templates(영문)를 참조하십시오.

또한 Albums 및 Album 페이지에는 Windows Media Player 재생 목록 파일을 실행하는 이미지와 재생 단추가 있습니다. Album 이미지는 웹 액세스가 가능한 디렉터리에 캐시되며 WMP 재생 목록 파일은 HTTP 처리기에서 즉석으로 생성됩니다. 이들 항목은 아래에 자세히 설명되어 있습니다.

표에 개체 바인딩

새로운 ObjectDataSource는 표와 개체 사이의 매개자로 클래스와 연동하여 표에서 요청하는 데이터를 반환하도록 구성해야 합니다. 그럼 지금부터 Albums 페이지의 데이터 원본을 단계별로 만들어 보겠습니다.

그림 5: ObjectDataSource 마법사(첫 번째 페이지)

"Binder" 클래스를 비즈니스 개체로 선택합니다. 가수 등을 나타내는 데 사용되는 이 개체는 단순하며, 주로 데이터를 나타내므로 지속성 메서드가 많지 않습니다. Binder 클래스에서 제공하는 기능은 개체 자체에서 다른 방식으로 처리될 수도 있습니다.

그림 6: ObjectDataSource 마법사(두 번째 페이지)

다음 페이지에서는 데이터를 가져오는 데 사용하는 메서드인 GetAlbums를 선택합니다. 또한 Update, Insert 및 Delete 메서드를 지정할 수 있지만 반드시 그럴 필요는 없습니다.

그림 7: ObjectDataSource 마법사(세 번째 페이지)

마지막 페이지에서는 매개 변수 값을 가져올 위치를 지정합니다. 이 값은 Cookie, Control, Form, Profile, QueryString 및 Session 중 하나에서 가져올 수 있습니다.

데이터 원본을 구성한 후 이를 표의 DataSource로 설정하면 깜짝 놀랄 만한 작업이 완성됩니다.

사용자 지정 표 값 생성

모든 표에서는 템플릿 열과 사용자 지정 코드를 사용하여 하이퍼링크를 생성합니다. 예를 들어 Artists 페이지(그림 2)는 RangeListItems의 generic 목록에 바인딩되는 단일 GridView로 이루어져 있습니다. RangeListItem은 범위에 있는 첫 번째와 마지막 가수의 ID 및 표에 표시되는 텍스트를 나타내는 사용자 지정 클래스입니다. 하이퍼링크는 표의 템플릿 필드를 사용하고 페이지의 서버측 스크립트 섹션에서 메서드를 호출하여 만듭니다.

그림 8: Artists 페이지의 템플릿 필드 열

NavigateUrl 특성은 다음과 같이 CreateNavigateUrl 메서드를 호출하여 채웁니다.

    Private Function CreateNavigateUrl(ByVal o As Object) As String
        Dim listItem As RangeListItem = CType(o, RangeListItem)
        
        If (listItem.StartIndex = listItem.EndIndex) Then
            Return "albums.aspx?artist=" & listItem.StartIndex
        Else
            Return String.Format("default.aspx?start={0}&end={1}", _
listItem.StartIndex, listItem.EndIndex) End If End Function

ASP.NET 2.0의 템플릿에 대한 자세한 내용은 Dino Esposito의 Move Over DataGrid, There's a New Grid in Town!(영문)을 참조하십시오.

앨범 표지 표시

Windows Media Player에서는 가능한 경우 큰 버전과 작은 버전의 앨범 표지를 모두 다운로드합니다. 이 이미지 파일은 앨범 트랙과 동일한 폴더에 저장되며 기본적으로 숨겨져 있습니다. 브라우저에서 이 이미지에 액세스할 수 있도록 웹 사이트의 하위 폴더에 이 이미지를 복사해야 합니다. 이렇게 하려면 먼저 올바른 이미지를 찾아 이 파일을 웹 사이트의 하위 폴더에 복사하고, 다음에 필요할 때 보다 쉽게 검색할 수 있도록 고유한 이름을 지정합니다. 앨범 표지가 없으면 대신 작은 투명 이미지를 사용합니다.

    Public Function GetAlbumArtUrl(ByVal size As AlbumArtSize) As String
        Dim albumArtFileName As String = GetCustomAlbumArtFileName(size)
        Dim albumArtFullFilename As String = _
Path.Combine(_albumArtDirectory, albumArtFileName) Dim fileExists As Boolean = File.Exists(albumArtFullFilename) If Not fileExists Then Dim dir As String = Path.GetDirectoryName(Tracks(0).Location) Dim filename As String If Directory.Exists(dir) Then filename = GetRealAlbumArtFileName(dir, size) If Not filename Is Nothing Then File.Copy(filename, albumArtFullFilename) File.SetAttributes(albumArtFullFilename, FileAttributes.Normal) fileExists = True End If End If End If Dim url As String If fileExists Then url = "/coding4fun/images/coolapps/musiclib/AlbumArt/" & _
albumArtFileName Else url = "/coding4fun/images/coolapps/musiclib/dot.gif" End If Return url End Function

GetCustomAlbumArtFileName 메서드는 이미지의 복사본을 저장하고 이후 요청 시 이를 보다 손쉽게 검색할 수 있도록 일관성 있는 이름을 만듭니다.

    Private Function GetCustomAlbumArtFileName(ByVal size As AlbumArtSize) As String
        Return String.Format("{0}-{1}-{2}.jpg", _
GetAlphanumericString(_artist.Name), _
GetAlphanumericString(Name), size.ToString) End Function Private Function GetAlphanumericString(ByVal s As String) As String Dim sb As StringBuilder = New StringBuilder For Each c As Char In s If Char.IsLetterOrDigit(c) Then sb.Append(c) End If Next Return sb.ToString() End Function
앨범 표지 파일 이름은 다음과 같은 형식으로 저장될 수 있습니다.:

큰 버전 AlbumArt_{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}_Large.jpg
AlbumArt__Large.jpg
Folder.jpg
작은 버전 AlbumArt_{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}_Small.jpg
AlbumArt__Small.jpg
AlbumArtSmall.jpg

GetRealAlbumArtFileName에서는 위 형식대로 앨범 표지의 파일을 검색하여 해당 파일이 존재할 경우 파일 이름을 가져옵니다.:

    Private Function GetRealAlbumArtFileName(ByVal dir As String, _
ByVal size As AlbumArtSize) As String Dim filename As String = Nothing Dim filenames() As String = Directory.GetFiles(dir, "AlbumArt*" & _
size.ToString() & ".jpg") If (filenames.Length > 0) Then filename = filenames(0) ElseIf (size = AlbumArtSize.Large) Then Dim file As FileInfo = New FileInfo(Path.Combine(dir, "Folder.jpg")) If file.Exists Then filename = file.FullName End If End If Return filename End Function

재생 목록 만들기

재생 목록은 IHttpHandler 인터페이스 (영문)를 구현하는 경량 웹 요청 처리기인 PlaylistCreator 클래스를 사용하여 만듭니다. 이 처리기는 매우 간단합니다. 즉, 반환하는 콘텐츠 형식이 "video/x-ms-asf"임을 브라우저에 알리는 헤더를 작성한 다음 XML 재생 목록을 만들어 반환하는 것입니다. 이 콘텐츠 형식 헤더는 브라우저에서 콘텐츠를 사용하여 어떤 작업을 수행할지 결정하는 데 도움이 될 뿐 아니라, 재생 목록을 브라우저에 표시하는 것이 아니라 재생하는 것이므로 매우 중요합니다.

PlaylistCreator는 다음과 같이 XmlTextWriter를 사용하여 응답 스트림에 바로 작성합니다.:

    Public Sub ProcessRequest(ByVal context As HttpContext) _
Implements IHttpHandler.ProcessRequest _trackIndex = QueryStringHelper.TrackIndex _artistIndex = QueryStringHelper.ArtistIndex _albumIndex = QueryStringHelper.AlbumIndex context.Response.ContentType = "video/x-ms-asf" Dim streamWriter As StreamWriter = _
New StreamWriter(context.Response.OutputStream) _writer = New XmlTextWriter(streamWriter) _writer.WriteProcessingInstruction("wpl", "version=\""1.0\""") _writer.WriteStartElement("smil") CreateHead() _writer.WriteStartElement("body") _writer.WriteStartElement("seq") If _trackIndex.HasValue Then CreateTrackEntry() ElseIf _albumIndex.HasValue Then CreateAlbumEntries() End If _writer.WriteEndElement() _writer.WriteEndElement() _writer.WriteEndElement() _writer.Close() End Sub

PlaylistCreator 등록

ASP.NET에서는 이름이 같은 페이지를 검색하고 이를 실행하여 ASPX 페이지에 대한 요청을 처리합니다. HTTP 처리기는 조금 다른 방식으로 이를 처리합니다. 파일 이름 또는 패턴과 이에 대한 요청을 처리하는 데 사용할 개체 유형을 연결하는 항목을 web.config 파일에 만들어야 합니다. 아래 XML 부분은 web.config 파일의 configuration/system.web 섹션에서 가져온 것입니다. 이 부분에서는 "wpl" 확장명을 사용하는 요청을 "PlaylistCreator" 유형의 개체에 전달하도록 ASP.NET에 지시합니다.

     <httpHandlers>
          <add path="*.wpl" type="PlaylistCreator" verb="*" validate="false" />
     </httpHandlers>

Jeff Key는 .NET 비즈니스 규칙 엔진 기술의 선두 업체인 InRule Technology (영문)의 개발자입니다. Jeff는 팻 클라이언트에서 CGI, MTS/COM+ 및 ASP에 이르는 다양한 Microsoft 플랫폼을 두루 거치면서 전문 경력을 쌓아왔으며 베타 시기 이후부터 .NET을 다양하게 경험하고 있습니다. Jeff는 그의 웹 사이트 (영문)를 통해 연락할 수 있습니다.

+ Recent posts