project:mp3tagretrieval
차이
문서의 선택한 두 판 사이의 차이를 보여줍니다.
project:mp3tagretrieval [2013/01/05 23:09] – [OpenAPI 관련 모듈] 127.0.0.1 | project:mp3tagretrieval [2014/10/09 21:24] (현재) – 바깥 편집 127.0.0.1 | ||
---|---|---|---|
줄 1: | 줄 1: | ||
+ | ====== Maniadb.com OpenAPI를 이용한 MP3 태그 검색기 ====== | ||
+ | |||
+ | ===== 서론 ===== | ||
+ | 이번에는 Maniadb.com의 OpenAPI를 이용한 태그 검색기를 만들어 보겠습니다. 폴더/ | ||
+ | |||
+ | 요즘은 기술이 많이 발달해서 음악 파일의 내용을 직접 분석해 파일이 어떤 노래인지 찾아내는 서비스도 있습니다. 하지만 가끔씩 완전히 엉뚱한 결과를 내놓기도 하고, 특히 옛날 한국 가요의 정보는 잘 찾아지지 않는 때도 있습니다. 이럴 때는 결국 손으로 해결해야 합니다. 혹은 파일의 기존 태그나 몇몇 정보를 이용해 자동으로 태그를 검색하는 서비스도 있습니다. 결국 이런 서비스는 얼마나 다양한 자료를 얼마나 정확하게 보유하고 있는지가 중요합니다. | ||
+ | |||
+ | 얼마 전 MP3 태그를 정리를 하려고 하였습니다. 수십 곡에 대한 정보를 찾으려니 정말 귀찮아지더군요. 90~2000년대 초중반의 한국 가요들이라 태그 정보를 찾기도 쉽지 않았습니다. 일단 자동 MP3 태그 정리 프로그램을 찾아 보니 어떤 분이 고맙게도 [[http:// | ||
+ | |||
+ | 그런데 안타깝게도 약간 아쉬운 점이 있었습니다. | ||
+ | - 곰오디오 서버의 태그 정보도 충분히 쓸만하지만, | ||
+ | - TagBear가 검색하지 못한 파일에 대해 목록을 저장하는 기능이 없습니다. 결과를 유심히 체크하지 않으면 태그를 수정한 파일과 수정하지 않은 파일을 구분할 수 없습니다. | ||
+ | |||
+ | 태그 DB, 특히 한국 가요에 대한 태그 정보 저장소를 찾던 중 [[http:// | ||
+ | |||
+ | 추가) 윈앰프에서는 GraceNote를 사용하고, | ||
+ | |||
+ | ===== 프로그램의 목적과 방향 ===== | ||
+ | MP3 태그를 전문적으로 수정할 수 있는 프로그램은 많이 있습니다. 대표적으로 ' | ||
+ | |||
+ | 제가 하고 싶은 것은 MP3 태그에 대해 한 번쯤 프로그램적으로 접근해보고 싶은 것과 다른 전문적인 태그 편집 프로그램들을 위하여 태그의 초기 정보를 쉽게 검색할 수 있는 수단을 제공하는 것입니다. 우리의 파이썬 장난감 프로그램은 감히 그러한 프로그램들과는 감히 견줄 수 없습니다. 어려운 작업들은 배제하고 원격 서버로부터 있는 태그 정보만 검색해서 저장하는 기능만 구현할 것입니다. | ||
+ | |||
+ | 또한 MP3의 너무 복잡한 내용의 태그는 건드리지 않고, 가급적 간단한 부분의 것만 수정하도록 할 생각입니다. 우리가 사용하게 될 ID3V2 태그는 상당히 구체적으로 태그를 정의했습니다. 별의별 내용들을 다 적을 수 있습니다. 하지만 우리는 Maniadb가 제공하는 정보 이상의 것을 기록하지는 않을 것입니다. | ||
+ | |||
+ | 또한 프로그램은 CLI (Command Line Interface) 형태로 제작될 것입니다. GUI가 사용하기 편리하긴 하지만 제작하기 상당히 까다롭습니다. 그리고 프로토타입으로 빠르게 만들어 본 CLI 프로그램을 써서 약 60곡 정도의 태그를 검색해본 결과 익숙해지면 나름 편리(!)했습니다. | ||
+ | |||
+ | 우리가 만든 프로그램은 우리만의 장난감으로 쓰는게 맞겠지만, | ||
+ | - 파이썬 스크립트를 바이너리 형태로 만들기? | ||
+ | - 파이썬이 설치되지 않은 PC에 우리 장난감이 돌아갈 수 있게 한다면? | ||
+ | |||
+ | 여담이지만, | ||
+ | |||
+ | ===== 기반지식 ===== | ||
+ | [[project: | ||
+ | |||
+ | MP3 태그 수정은 파이썬 기반의 잘 만들어진 라이브러리를 사용할 것입니다. 그렇지만 그것은 우리가 짧은 시간에 그만한 성과를 낼 수 없기 때문에 사용하는 것이지 MP3 태그 내용에 대해 신경쓰고 싶지 않다는 뜻은 아닙니다. 통달은 할 수 없지만 MP3 파일 내부에 대체 어떻게 그런 태그 정보들이 저장되는지에서는 한 번 확실하게 감을 잡을 필요가 있습니다. 그러므로 MP3 태그 포맷의 하나인 ' | ||
+ | |||
+ | |||
+ | ==== OpenAPI ==== | ||
+ | ' | ||
+ | |||
+ | 사실 인터페이스란 말에 대해 딱 정의를 내리고 ' | ||
+ | |||
+ | 다른 예로 DVD 플레이어를 들어볼까요? | ||
+ | |||
+ | 이제 인터페이스에 대해 대략적으로 감을 잡으셨으리라 생각합니다. 그럼 프로그래밍에서의 인터페이스, | ||
+ | |||
+ | 사실 우리가 탐색기에서 파일을 하나 생성하고, | ||
+ | |||
+ | 그렇게 놓고 보면 ' | ||
+ | |||
+ | 이것은 DVD가 고장나서 서비스 센터에 가져갔을 때로 비유할 수 있겠습니다. 기사님이 고장난 DVD 플레이어를 어찌어찌 만지니 우리가 모르던 어떤 화면들이 나와서 기기를 진단합니다. 우리는 처음 보는 화면이고 무슨 말인지 모르겠으나 그 분은 그것으로 문제를 판단해내지요. 허나 우리는 그런 진단 화면이 나오든 말든지, 그 고장이 어떤 이유이고 어떻게 해야 고쳐지는지 자세히 알 필요는 없습니다. 우리는 DVD 플레이어가 고장이 났으니 서비스 센터에 왔고, 신속하고 정확하게 잘 고쳐지기만 하면 됩니다. 괜히 귀찮게 이것저것 기웃기웃댈 필요가 없죠. 무리해서 알아내려 하다 잘못하면 ' | ||
+ | |||
+ | ' | ||
+ | |||
+ | API를 사용하는 쪽은 제공되는 서비스를 활용하여 자신들의 창의성을 발휘하면 됩니다. 서비스 유지보수나 개선에 대해 큰 신경쓰지 않아도 됩니다. 아주 큰 수고를 덜 수 있습니다. API를 공개하는 쪽은 쉽게 서비스 홍보(혹은 저변확대)가 됩니다. 가끔은 API를 만드는 이들이 미처 생각하지 못한 매우 참신하고 창의적인 방법으로 서비스가 이용됩니다. 그러면 쓰는 이와 쓰도록 하는 이 모두에게 큰 이득이 되는 셈입니다. | ||
+ | |||
+ | 만일 Maniadb가 검색 기능을 OpenAPI로 공개하지 않았다면, | ||
+ | ==== XML/DOM ==== | ||
+ | ===XML=== | ||
+ | XML과 DOM에 대해서도 이야기를 시작하면 끝도 없이 나올 수 있습니다. XML 또한 깊게 들어가면 책 몇권으로도 모자랍니다. 우리가 OpenAPI를 이용했을 때 전달되는 XML 메시지만 간략히 이해할 수 있을 정도로만 설명을 하고 마치겠습니다. | ||
+ | |||
+ | XML은 정보를 표현하기 위한 한 방법입니다. 예를 들어 비틀즈의 'Hey Jude' | ||
+ | == 노래 소개 == | ||
+ | 제목: Hey Jude | ||
+ | 가수: the Beatles | ||
+ | 장르: pop | ||
+ | 발표: 1968년 | ||
+ | 가사: Hey Jude, Hey jude, don't make it bad. Take a sad song and make it better... | ||
+ | ============== | ||
+ | 같은 내용을 XML 문서로 표현해 보겠습니다. | ||
+ | <code xml> | ||
+ | <?xml version=" | ||
+ | < | ||
+ | < | ||
+ | <artist link=' | ||
+ | < | ||
+ | <release year=' | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | 아주 간단히 표현해 보았습니다. HTML 코드를 몇 번 보신 분들은 그리 낯설지 않을 것입니다. 사실 둘은 사촌지간 격입니다. XML은 일반적인 정보를 표현하는 방법인 반면, HTML은 웹페이지 문서를 표현하는 방법입니다. XML이 보다 일반적인 방법이므로 HTML은 XML의 부분집합일 겁니다. XML방식으로 HTML을 기술한 것을 ' | ||
+ | |||
+ | XML 문서는 반드시 다음과 같이 시작합니다. | ||
+ | <?xml version=" | ||
+ | 그리고 여는 태그의 시작인 '<' | ||
+ | |||
+ | '< | ||
+ | |||
+ | release에 있는 year=' | ||
+ | |||
+ | XML 문서는 구조적입니다. 위 예에서 보이듯 최상위 엘리먼트로 ' | ||
+ | |||
+ | ' | ||
+ | |||
+ | XML은 보다시피 사람이 보아도 이해가 되는 문서입니다. 문서의 구조는 엄격한 규칙을 갖고 있으므로 기계적으로도 해석하기 용이합니다. 기계가 XML 문서를 이해하기 위해서는 XML 문서 ' | ||
+ | |||
+ | === DOM === | ||
+ | DOM (Document Object Model)은 XML 문서 접근 및 조작을 위한 방법입니다. 앞서 XML은 구조적인 형태로 되어 있다고 하였습니다. 보통 컴퓨터 프로그래밍에서 ' | ||
+ | |||
+ | 트리로 표현한다는 말은 조직도를 그린다는 것과 상당히 유사합니다. XML을 소개하면서 예를 들었던 문서를 가지고 한 번 ' | ||
+ | |||
+ | taginfo: | ||
+ | | | ||
+ | |------title | ||
+ | | | ||
+ | | | ||
+ | |------artist | ||
+ | | \*link | ||
+ | | \textnode | ||
+ | |------genre | ||
+ | | | ||
+ | | | ||
+ | |------release | ||
+ | | | ||
+ | | | ||
+ | |------lyrics | ||
+ | | ||
+ | | ||
+ | XML은 XML 파서에 의해 기계가 이해할 수 있다고 하였습니다. 다시말해 DOM은 XML이 파싱된 결과라고 볼 수 있습니다. DOM을 이용하면 XML 내부의 각 구조에 쉽게 접근할 수도 있고, 손쉽게 수정할 수도 있습니다. 당연히 다시 XML 문서로 재발행할 수도 있습니다. | ||
+ | |||
+ | 트리에서 taginfo: | ||
+ | |||
+ | 엘리먼트 안의 시작 태그와 종료 태그 사이에 있는 텍스트는 textnode에 있습니다. 예를 들어, ' | ||
+ | |||
+ | 이렇게 DOM을 이용하면 잘 조직된 체계(트리)에서 쉽게 원하는 정보를 조회, 수정 및 삽입할 수 있습니다. 우리도 Maniadb가 보내온 XML을 DOM Tree로 만들어 정보를 추출할 것입니다. | ||
+ | ==== ID3 Tag ==== | ||
+ | MP3 태그 포맷으로 ID3가 있습니다. ID3는 두가지 버전이 있습니다. 하나는 ID3v1, 다른 하나는 ID3v2입니다. v1과 v2는 사실상 이름만 비슷할 뿐 전혀 관련이 없으며 ID3v1은 태그 기록에 제약이 많아 현재는 ID3v2를 많이 사용하고 있습니다. ID3v2는 현재 ID3v2.4까지 나와 있으나 아직은 ID3v2.3을 많이 사용하고 있습니다. | ||
+ | |||
+ | === ID3v2.3의 태그 구조 === | ||
+ | == 헤더 == | ||
+ | ID3v1 태그가 파일의 뒷부분에 위치한 것과는 달리, ID3v2.3 태그(이하 태그)는 MP3 파일의 처음에 위치합니다. 태그는 ' | ||
+ | |||
+ | 다음 1바이트는 플래그를 위한 바이트입니다. 상위 3비트만 플래그로 사용합니다. 상위부터 하위로 ' | ||
+ | |||
+ | * unsynchronization: | ||
+ | * entended header: 확장 헤더를 사용할지에 대한 플래그. 1이면 사용합니다. | ||
+ | * experimental header: 실험적인, | ||
+ | |||
+ | 다음 4바이트는 태그의 전체 사이즈를 가리깁니다. 태그 사이즈는 헤더 자체는 제외한 사이즈를 가리킵니다. 단, 확장 헤더는 포함합니다. 그런데 태그 사이즈를 가리키는 4바이트는 4바이트를 전부 사용하는 것이 아니라, 각 바이트의 상위 1비트는 제외하고 계산해야 합니다. 다음의 예에서 자세히 설명하도록 하겠습니다. | ||
+ | |||
+ | == 헤더의 예 == | ||
+ | 파일의 처음부터 다음과 같은 16진수가 나열되어 있다고 합니다. | ||
+ | 49 44 33 03 00 00 00 02 2A 52 | ||
+ | |||
+ | |||
+ | ^ 16진수 값 ^의미 ^ | ||
+ | |49 44 33|' | ||
+ | |03 00 | ||
+ | |00 |어떤 플래그도 없습니다.| | ||
+ | |00 02 2A 52| 각 바이트로부터 상위 1비트를 제외한 7비트만을 가져와 총 28비트의 2진수로 만듭니다. 이 2진수가 헤더의 사이즈입니다. \\ 00 02 2A 52는 2진수로 '0000 0010 0010 1010 0101 0010' | ||
+ | |||
+ | == 프레임 == | ||
+ | 헤더 다음부터는 프레임이라는 정보가 나옵니다. 프레임은 태그의 필드(곡명, | ||
+ | |||
+ | 마지막의 2바이트는 플래그입니다. 각 바이트의 상위 3바이트를 사용하는데 첫번째 바이트는 상태 메시지와 관련이 있고 두번째 바이트는 태그 자체가 인코딩된 방법과 관계가 있습니다. 자세한 사항은 공식 문서를 읽어 보세요. 이 다음부터는 프레임의 내용이 시작되는데, | ||
+ | |||
+ | == 프레임의 예 == | ||
+ | 41 50 49 43 00 00 89 26 00 00 00 69 6D 61 67 65 2F 6A 70 65 67 00 03 00 FF D8 ... | ||
+ | | ||
+ | |||
+ | ^ 16진수 값 ^의미 | ||
+ | |41 50 49 43 | 문자열 ' | ||
+ | |00 00 89 26 | 0x8926, 즉 프레임의 내용은 모두 35110 바이트를 차지합니다. 헤더 크기는 제외되어 있습니다.| | ||
+ | |00 00 | 플래그는 모두 초기화되어 있습니다.| | ||
+ | |00 | 문자 인코딩은 ISO-8859-1을 따른다. | | ||
+ | |69 6D 61 67 65 2F 6A 70 65 67 00| 여기부터는 프레임의 내용이 시작됩니다. 각 프레임의 종류마다 각각 다른 포맷으로 구성되어 있습니다. APIC에서 이 위치에 오는 데이터는 널로 끝나는 문자열로, | ||
+ | |03 | 이것은 이 그림이 전면 커버 그림이란 것을 의미합니다. 그림은 종류에 따라 0x00~0x0E, 총 15가지로 분류되어 있습니다.| | ||
+ | |00 | 이 위치에는 그림에 대한 설명을 넣을 수 있으나, 현재 비어 있습니다. 64글자까지 가능합니다.| | ||
+ | |FF D8 | 이미지 정보의 시작. 실제로 JPEG 포맷 파일은 'FF D8' | ||
+ | |||
+ | 이 다음의 프레임 값은 다음과 같이 되어 있습니다. | ||
+ | 54 41 4C 42 00 00 00 2B 00 00 01 FF FE 53 00 65 | ||
+ | 00 6F 00 74 00 61 00 69 00 6A 00 69 00 20 00 61 | ||
+ | 00 6E 00 64 00 20 00 42 00 6F 00 79 00 73 00 20 | ||
+ | 00 49 00 49 00 | ||
+ | |||
+ | |||
+ | ^ 16진수 값 ^의미 | ||
+ | | 54 41 4C 42 | TALB. ' | ||
+ | | 00 00 00 2B | 프레임 내용의 크기는 43바이트입니다. | | ||
+ | | 00 00 | 플래그가 없습니다. | | ||
+ | | 01 | Unicode를 사용하였습니다.| | ||
+ | | FF FE ... | 여기서부터 문자열이 시작됩니다. 'FF FE'는 BOM(Byte Order Mark). 리틀-엔디안 방식의 ' | ||
+ | |||
+ | |||
+ | |||
+ | 마지막으로, | ||
+ | 54 49 54 32 00 00 00 13 00 00 01 FF FE 58 D5 EC | ||
+ | C5 00 AC 28 00 55 4F 82 59 4C 6B 29 00 | ||
+ | |||
+ | |||
+ | |||
+ | ^ 16진수 값 ^의미 | ||
+ | | 54 49 54 32 | TIT2. The ' | ||
+ | | 00 00 00 13 | 19바이트가 기록되어 있습니다.| | ||
+ | | 01 | Unicode를 사용함| | ||
+ | | FF FE .. | 문자열의 시작 유니코드로 ' | ||
+ | |||
+ | 태그의 구성에 대해서는 어느 정도 감을 잡으셨으리라 생각합니다. 프레임이 워낙 많고 자세하게 나누어져 있어 모두 설명드리기 어렵습니다. 이 정도 설명을 이해했다면 [[http:// | ||
+ | |||
+ | ==== Encoding ==== | ||
+ | ' | ||
+ | * 웹페이지 혹은 이메일이 이상한 문자들로 표현된다. | ||
+ | * MP3 태그의 글씨가 이상한 문자들로 보인다. | ||
+ | * 동영상 파일의 자막이 이상한 문자들로 보인다. | ||
+ | 이 모든 일들의 원인은 바로 ' | ||
+ | |||
+ | === 비트와 바이트 === | ||
+ | 일단 맨 처음으로 돌아가서 컴퓨터가 정보를 저장하는 방식에 대해 논해 보도록 하겠습니다. 컴퓨터는 디지털, 즉 2진수만 취급한다는 사실은 상식적으로 다 알고 계시리라 생각합니다. 2진수 체계에서 표현할 수 있는 상태는 0과 1 단 두 가지 상태 뿐입니다. 0이냐 1이냐, 다른 말로 ' | ||
+ | |||
+ | 컴퓨터는 사실 0 혹은 1의 전기 시그널만 캐치할 뿐이며 다른 어떤 일도 하지 못하는 깡통입니다. 대신 그 단순한 일은 기막히게 빠른 처리가 가능하지요. 그리고 단순한 두 가지 상태 표현은 자릿수를 늘려버리는 방식으로 극복합니다. 0과 1을 무지막지하게 늘어 놓는 겁니다. 0 혹은 1을 하나 표현할 수 있는 한 자릿수를 일컬어 ' | ||
+ | |||
+ | 1바이트만 해도 2^8=256. 256개의 상태를 가지고 있습니다. 이것을 단순히 숫자로 해석할 수도 있었지만, | ||
+ | |||
+ | === 문자 인코딩과 디코딩 === | ||
+ | 이 문자/ | ||
+ | |||
+ | 글자를 숫자로 대응시키는 것을 ' | ||
+ | |||
+ | 그러나 ' | ||
+ | |||
+ | === 코드페이지(codepage) === | ||
+ | 컴퓨터 상에서 문자를 표현하는 규칙을 적은 표는 여러가지가 있습니다. 각국의 언어 환경에 따라서도 달라질 것이고 언어를 어떻게 표기하느냐에 따라서도 달라질 것입니다. ' | ||
+ | chcp | ||
+ | 코드 페이지를 cp949에서 UTF-8로 변경하려면 다음과 같이 하면 됩니다. | ||
+ | chcp 65001 | ||
+ | 다시 cp949로 변경하려면 다음과 같이 하면 됩니다. | ||
+ | chcp 949 | ||
+ | 이제 문자가 깨지는 현상에 대해서 어느 정도 자세한 설명이 가능할 것 같습니다. | ||
+ | |||
+ | === 웹페이지, | ||
+ | 대개 웹페이지나 이메일에서 문자가 올바르게 표현되지 않는 현상은 그 문서의 내용, 대개 HTML 코드(이메일에서도 HTML 코드를 쓰는 경우도 있습니다)에서 어떤 인코딩을 사용하는지에 대해 지시하지 않아서 그렇습니다. 보통 웹브라우저는 기본적으로 UTF-8을 이용하는데, | ||
+ | <code html> | ||
+ | < | ||
+ | <meta http-equiv=" | ||
+ | ... | ||
+ | </ | ||
+ | </ | ||
+ | 이렇게 해야 웹브라우저는 문서가 euc-kr로 인코딩된 문서임을 알아채고 올바르게 문자 디코딩을 euc-kr 체계에 맞춰 합니다. | ||
+ | |||
+ | === ID3 태그의 한글 깨짐 === | ||
+ | ID3V2.3의 문서를 보면 인코딩은 ISO-8859-1/ | ||
+ | |||
+ | === 자막의 한글 깨짐 === | ||
+ | 마지막으로 동영상의 한글 자막은 이런 이유로 제대로 표시되지 않습니다. 한글 자막은 거의 EUC-KR(CP949) 인코딩으로 저장됩니다. 그러나 리눅스의 동영상 프로그램들이나, | ||
+ | |||
+ | === BOM (Byte Order Mark) === | ||
+ | 유니코드에서 인코딩을 지정하다 보면 BOM(Byte Order Mark)라는 말을 만날 때가 있습니다. 이것은 유니코드에서 자신이 어떤 인코딩을, | ||
+ | |||
+ | ===== 프로그램 구상(설계) ===== | ||
+ | 이 장에서는 앞서 논의한 기반 지식들을 바탕으로 어떤 장난감(프로그램)으로 만들어 낼 것인지를 살짝 생각해 보도록 하겠습니다. 실제로 코딩을 하기 전 어떻게 코딩을 해야 할지를 결정하는 중요한 단계입니다. 설계가 잘 되어야 결과가 잘 나오는 법입니다. | ||
+ | ==== 프로그램의 동작 순서 구상 ==== | ||
+ | 우리의 장난감이 동작하는 순서부터 대략적으로 생각해 보도록 하지요. | ||
+ | - 프로그램은 MP3 파일의 목록을 입력받습니다. | ||
+ | - 프로그램은 쉘을 하나 실행합니다. 몇 개의 간단한 명령만 이해하는 단순한 녀석입니다. 이걸로 프로그램은 사람으로부터 명령을 받아들입니다. | ||
+ | - 사람으로부터 입력한 명령을 해석합니다. | ||
+ | - 어떤 명령인지를 분석하여 실행합니다. 태그 검색 명령의 경우, 다음 순서를 따라 움직여야 합니다. | ||
+ | - 사람으로부터 검색어를 입력받습니다. 검색어 초기 값을 제안할 수 있습니다. | ||
+ | - 입력받은 검색어를 확인합니다. 확인받으면 OpenAPI를 통해 질의를 보냅니다. | ||
+ | - 질의에 대한 응답을 수신합니다. | ||
+ | - 응답 문서를 해석해 사람에게 간단히 출력합니다. | ||
+ | - 사람은 응답 중 어떤 항목이 노래에 맞는 태그인지 선택합니다. | ||
+ | - 선택에 대해 확인받고, | ||
+ | - 추가적인 사항을 OpenAPI를 통해 검색할 수 있으며, 응답 사항의 일부를 MP3 태그에 추가적으로 기록할 수도 있습니다. | ||
+ | - 명령이 올바르지 않은 경우 에러를 내고 다시 명령을 대기하여야 합니다. | ||
+ | - 프로그램에서 에러가 나는 경우에도 쉘이 함부로 종료하는 경우는 없어야 합니다. 예를 들어 인터넷 연결 상태가 좋지 못해 일시적으로 질의에 대한 수신을 받을 수 없는 경우라도 에러 메시지를 출력하되, | ||
+ | ==== 프로그램의 인터페이스 구상 ==== | ||
+ | 일단 저는 프로그램을 최대한 간단히 만들고 싶습니다. 물론 사용하는 이를 충분히 고려해야 한다면 두말할 필요 없이 GUI 제작을 해야 하겠죠. 하지만 이 장난감은 거의 ' | ||
+ | |||
+ | === 쉘 명령어 구상 === | ||
+ | == 파일 목록의 번호 == | ||
+ | 입력 받은 파일의 목록은 프로그램 내부에서 리스트로 관리됩니다. 파일 목록의 시작은 1부터입니다. | ||
+ | |||
+ | == load 명령 == | ||
+ | MP3 파일의 목록을 불러옵니다. 인자로 두 가지 형태의 파일 이름을 입력할 수 있습니다. 첫번째 형태는 MP3 파일 이름입니다. 다른 하나는 텍스트 파일입니다. 텍스트 파일 안에는 한 줄에 하나씩 MP3 파일의 경로가 입력되어 있습니다. 텍스트 파일 앞에는 ' | ||
+ | load C: | ||
+ | | ||
+ | == list.txt 예== | ||
+ | C: | ||
+ | C: | ||
+ | C: | ||
+ | | ||
+ | load a.mp3 b.mp3 @list.txt (a.mp3, b.mp3와 배열 파일 ' | ||
+ | 음악 파일 형식은 MP3만 가능합니다. 또한 프로그램은 중복 입력된 파일에 대해서 검사하지 않습니다. 중복 검사는 사전에 하셔야 합니다. | ||
+ | |||
+ | == unload 명령 == | ||
+ | load 명령의 역입니다. 인자 목록을 입력받을 수 있습니다. 인자로 받은 항목은 파일 목록에서 삭제됩니다(목록에서 지워질 뿐 파일이 실제로 삭제되지는 않습니다). | ||
+ | unload 1 (1번 항목을 삭제합니다) | ||
+ | unload 6-9 (6~9번 항목을 삭제합니다. 하이픈 앞뒤 숫자와 공백이 없습니다. 나머지 항목도 동일합니다.) | ||
+ | |||
+ | == clear 명령 == | ||
+ | 목록을 전부 삭제합니다. 입력된 인자는 무시됩니다. | ||
+ | |||
+ | == list 명령 == | ||
+ | 입력받은 파일의 목록을 출력합니다. 인자 목록을 입력받을 수 있습니다. | ||
+ | list 1-5 (1번에서 5번까지 출력합니다) | ||
+ | list 10- (10번부터 마지막까지 출력합니다) | ||
+ | list -7 (처음부터 7번까지 출력합니다) | ||
+ | |||
+ | == det 명령 === | ||
+ | ' | ||
+ | det 10 (파일 목록 10번에 대한 자세한 정보를 출력합니다) | ||
+ | | ||
+ | == retr 명령 == | ||
+ | ' | ||
+ | retr 1-10 (1번부터 10번까지 태그 검색/ | ||
+ | retr 1 7-9 19- (1, 7, 8, 9, 그리고 19번부터 목록의 마지막까지 태그 검색 및 수정을 합니다) | ||
+ | retr -3 5 (1, 2, 3, 5번 목록에 대해 태그 검색 및 수정을 합니다) | ||
+ | | ||
+ | == retrall 명령 == | ||
+ | ' | ||
+ | |||
+ | == nresp 명령 == | ||
+ | ' | ||
+ | |||
+ | == exp 명령 === | ||
+ | ' | ||
+ | exp all_list.txt (all_list.txt 파일에 현재 MP3 목록을 출력) | ||
+ | |||
+ | == expunt 명령 == | ||
+ | ' | ||
+ | expunt untagged.txt (untagged.txt 파일에 현재 수정되지 않은 목록만을 출력) | ||
+ | |||
+ | == exptag 명령 == | ||
+ | ' | ||
+ | exptag tagged.txt (tagged.txt 파일에 현재 수정된 목록을 출력) | ||
+ | |||
+ | == exit 명령 == | ||
+ | 프로그램을 종료하고 명령 프롬프트로 돌아갑니다. | ||
+ | |||
+ | == 인자 에러 == | ||
+ | 쉘은 인자를 입력받을 때 잘못된 형식의 인자를 받을 수 있습니다. 이는 인자 에러에 해당합니다. 그러면 쉘은 해당하는 에러에 대한 메시지를 출력하고 그 명령 자체를 무시합니다. 다음은 인자 에러에 해당하는 경우입니다. | ||
+ | * 목록의 범위를 벗어난 숫자 | ||
+ | * 숫자를 입력받아야 하는 인자가 숫자로 변경 불가능한 경우 | ||
+ | * 잘못된 경로가 입력된 경우 | ||
+ | * 잘못된 파일이 입력된 경우 | ||
+ | |||
+ | == 명령 에러 == | ||
+ | 위 명령에 해당하지 않는 명령을 입력한 경우 에러 메시지를 출력하고 다시 입력을 대기합니다. | ||
+ | |||
+ | |||
+ | |||
+ | ==== Maniadb.com OpenAPI 살펴보기 ==== | ||
+ | === 키(Key) 발급 === | ||
+ | Maniadb의 OpenAPI를 이용하려면 ' | ||
+ | |||
+ | *주: 보통 OpenAPI를 이용하기 위해서는 키가 틀려서는 안 됩니다. 그러나 현재 Maniadb는 key 값에 대해 체크를 하지 않는 것 같습니다. 아무 값을 넣어도 질의 응답이 이뤄집니다. | ||
+ | |||
+ | === OpenAPI 둘러보기 === | ||
+ | OpenAPI에 관한 간략한 문서는 http:// | ||
+ | |||
+ | == 질의 보내기 == | ||
+ | 질의(쿼리, | ||
+ | 다음은 쿼리의 한 예입니다. v0.3의 API로 곡명 검색을 합니다. 노래 제목은 ' | ||
+ | http:// | ||
+ | | ||
+ | 다음은 앨범 정보에 대한 검색입니다. 121480이라는 값은 Maniadb 내부에서 사용하는 앨범의 ID입니다. | ||
+ | http:// | ||
+ | | ||
+ | query, query2 변수의 값은 각각 song, artist로 지정하거나 아예 query2를 넣지 않는 것이 가장 이상적이었습니다. 그러므로 구현할 때는 그렇게 사용하겠습니다. | ||
+ | |||
+ | == 응답 받기 == | ||
+ | 질의의 대한 응답은 XML 문서로 전달됩니다. 아래는 ' | ||
+ | <code xml> | ||
+ | <?xml version=" | ||
+ | <rss xmlns: | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | : song - [하여가] | ||
+ | : artist - [서태지] | ||
+ | : elapsed_time - [0.1132813] | ||
+ | ]]> | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | <item id=" | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | 마음이 | ||
+ | 다시 내게 돌아오는 걸 | ||
+ | 느꼈지 | ||
+ | 너는 언제까지나 | ||
+ | 나만의 나의 연인이라 | ||
+ | 믿어왔던 내 생각은 | ||
+ | 틀리고 말았어 | ||
+ | 변해버린 건 ]]> | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | ... (중략) ... | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | 이 XML 문서에서 ID3 태그에 필요한 가장 기본적인 데이터는 아래 항목입니다. | ||
+ | ^데이터 | ||
+ | |노래 제목|title| | ||
+ | |앨범 제목|maniadb: | ||
+ | |발매 일자|maniadb: | ||
+ | |앨범 커버|maniadb: | ||
+ | |아티스트 명|maniadb: | ||
+ | |||
+ | 다음은 ' | ||
+ | <code xml> | ||
+ | <?xml version=" | ||
+ | <rss xmlns: | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | <item id=" | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | <id> | ||
+ | < | ||
+ | </id> | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | <disc no=" | ||
+ | < | ||
+ | </ | ||
+ | <song track=" | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | <shop name=" | ||
+ | </ | ||
+ | </ | ||
+ | <song track=" | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | ... (중략) ... | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | 이미 대부분의 정보는 곡명 검색의 응답에서 취했습니다. 여기서 얻을 수 있는 정보는 아래와 같습니다. | ||
+ | ^데이터 | ||
+ | |디스크 정보|maniadb: | ||
+ | |트랙 정보|maniadb: | ||
+ | |발매회사|maniadb: | ||
+ | |||
+ | |||
+ | ==== ID3 tag 편집 라이브러리 ==== | ||
+ | ID3 tag를 편집할 수 있는 파이썬 기반의 라이브러리는 몇 가지가 있습니다. 그 중에서 저는 [[http:// | ||
+ | |||
+ | |||
+ | ===== 프로그램 제작 ===== | ||
+ | |||
+ | 이제부터 본격적으로 프로그램을 제작하도록 하겠습니다. 생각보다 소스의 분량이 많습니다. 상세한 설명을 하지 못함을 양해바랍니다. | ||
+ | ==== 인코딩 관련 모듈 ==== | ||
+ | 모든 인코딩이 하나로 통일되었다면 편리한데, | ||
+ | |||
+ | 프로그램 소스의 문자 인코딩은 CP949입니다. 그리고 소스 및 내부 문자열은 모두 유니코드로 처리됨을 유념하십시오. | ||
+ | |||
+ | <code python codeshift.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import locale | ||
+ | |||
+ | ''' | ||
+ | codeshift 모듈 | ||
+ | |||
+ | 로컬 인코딩, utf-8, 유니코드(utf-16) 인코딩을 변경하는 함수들로 | ||
+ | 명령 포롬프트의 코드 페이지가 변경되었다면 | ||
+ | MANIADB_LOCAL_ENCODING 값을 적절히 변경 바람. | ||
+ | ''' | ||
+ | |||
+ | MANIADB_LOCAL_ENCODING = locale.getdefaultlocale()[1] | ||
+ | |||
+ | # Local encoding (mainly, ' | ||
+ | def loc2utf8(input): | ||
+ | return input.decode(MANIADB_LOCAL_ENCODING).encode(' | ||
+ | |||
+ | # ' | ||
+ | def utf82loc(input): | ||
+ | return input.decode(' | ||
+ | |||
+ | # Local encoding to unicode | ||
+ | def loc2uni(input): | ||
+ | return input.decode(MANIADB_LOCAL_ENCODING, | ||
+ | |||
+ | # UTF-8 to unicode | ||
+ | def utf82uni(input): | ||
+ | return input.decode(' | ||
+ | |||
+ | # Unicode to UTF-8 | ||
+ | def uni2utf8(input): | ||
+ | return input.encode(' | ||
+ | |||
+ | # Unicode to local encoding | ||
+ | def uni2loc(input): | ||
+ | return input.encode(MANIADB_LOCAL_ENCODING, | ||
+ | </ | ||
+ | |||
+ | ==== OpenAPI 관련 모듈 ==== | ||
+ | OpenAPI 관련 모듈은 크게 2가지 파트로 나누어서 만들 생각입니다. 하나는 전단부로써 Maniadb 서버에 질의를 보내는 파트이고, | ||
+ | |||
+ | 전단부, 후단부는 각각 아티스트/ | ||
+ | |||
+ | ^파트 | ||
+ | |전단부(서버-> | ||
+ | |::: |앨범 상세 정보 요청| | ||
+ | |후단부(XML문서 -> 파이썬 자료형)|아티스트/ | ||
+ | |::: | ||
+ | |||
+ | XML 문서 구조를 딕셔너리와 리스트로 만드는 것은 XML 문서를 JSON(Javascript Style Object Notation) 스타일로 표현하는 것과 많이 유사합니다. 각 엘리먼트의 태그 이름이나 어트리뷰트는 딕셔너리의 키가 됩니다. 엘리먼트 내부의 텍스트나 어트리뷰트의 값은 딕셔너리 키의 값으로 이용합니다. 엘리먼트 내부의 엘리먼트들은 리스트를 이용하여 열거할 수 있습니다. 간단히 아래 예로 설명하도록 하겠습니다. | ||
+ | <code xml> | ||
+ | <a> | ||
+ | < | ||
+ | < | ||
+ | <d e=' | ||
+ | <f> | ||
+ | < | ||
+ | < | ||
+ | </f> | ||
+ | </a> | ||
+ | </ | ||
+ | 위 XML 문서는 아래와 같이 키와 리스트로 표현할 수 있습니다. 물론 이 방법만이 유일한 방법은 아닙니다. | ||
+ | < | ||
+ | { | ||
+ | ' | ||
+ | { | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | { | ||
+ | ' | ||
+ | }, | ||
+ | ' | ||
+ | ], | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | }, | ||
+ | }, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === 전단부(서버-> | ||
+ | 전단부는 Maniadb에 질의를 보내 원하는 정보를 검색한다는 공통적은 목적을 가지고 있습니다. 그러므로 이러한 몇 가지 공통적인 부분을 모아 부모 클래스로 만듭니다. 이렇게 해 두면 차후 수정 이나 확장을 해야 할 때 간편해집니다. | ||
+ | |||
+ | <code python api_base_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | |||
+ | class api_base_v3(object): | ||
+ | ''' | ||
+ | |||
+ | @property | ||
+ | def api_key(self): | ||
+ | return self.__API_KEY | ||
+ | |||
+ | @property | ||
+ | def api_version(self): | ||
+ | return self.__API_VERSION | ||
+ | |||
+ | @property | ||
+ | def max_buffer(self): | ||
+ | return self.__MAX_BUFFER | ||
+ | |||
+ | @max_buffer.setter | ||
+ | def max_buffer(self, | ||
+ | self.__MAX_BUFFER = value | ||
+ | |||
+ | __API_KEY | ||
+ | __API_VERSION = u' | ||
+ | __MAX_BUFFER | ||
+ | </ | ||
+ | |||
+ | 키 값은 직접 입력하시기 바랍니다. 그러면 이제 아티스트/ | ||
+ | <code python api_search_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import urllib2 | ||
+ | import codeshift | ||
+ | import util | ||
+ | from | ||
+ | |||
+ | class api_search_v3(api_base_v3): | ||
+ | ''' | ||
+ | |||
+ | # Search for data. If an error happens, returns None | ||
+ | def search(self, | ||
+ | ''' | ||
+ | 키워드 기반 검색 (앨범/ | ||
+ | 함수에서 직접 URL을 UTF-8로 변경하므로 인수는 유니코드로 전달하여야 한다. | ||
+ | |||
+ | **NOTE** | ||
+ | itemtype과 option을 동일하게 적지 않으면 올바로 검색이 되지 않는다. | ||
+ | query2의 값은 적지 않아도 무방하나, | ||
+ | |||
+ | Args: | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | Returns: | ||
+ | 서버로부터 전달된 응답 문서. XML 형식으로 되어 있다. | ||
+ | 만일 네트워크 장애로 XML 문서를 전달받지 못한 경우 None을 리턴한다. | ||
+ | |||
+ | Raises: | ||
+ | ''' | ||
+ | request_url = self.__make_request_url(itemtype, | ||
+ | |||
+ | try: | ||
+ | req = urllib2.urlopen(codeshift.uni2utf8(request_url)) | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | return None | ||
+ | |||
+ | return req.read(self.max_buffer) | ||
+ | |||
+ | def __make_request_url(self, | ||
+ | _url = self.__REQUEST_BASE_URL | ||
+ | _url += u'? | ||
+ | _url += u'&' | ||
+ | _url += u'& | ||
+ | _url += u'& | ||
+ | _url += u'& | ||
+ | _url += u'& | ||
+ | |||
+ | if option2 != '' | ||
+ | _url += u'& | ||
+ | _url += u'& | ||
+ | |||
+ | _url += u'& | ||
+ | |||
+ | return _url | ||
+ | |||
+ | __REQUEST_BASE_URL = u' | ||
+ | |||
+ | if __name__ == ' | ||
+ | s = api_search_v3() | ||
+ | r = s.search(u' | ||
+ | |||
+ | if r: | ||
+ | print codeshift.utf82loc(r) | ||
+ | </ | ||
+ | |||
+ | 이번에는 앨범의 상세 정보를 요청하는 모듈 차례입니다. 앨범의 상세 정보는 질의를 던지는 쪽이 앨범에 대한 ID를 알고 있어야 합니다. 이 ID는 Maniadb 내부에서 앨범을 식별/ | ||
+ | |||
+ | <code python api_album_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import urllib2 | ||
+ | import codeshift | ||
+ | import util | ||
+ | from | ||
+ | |||
+ | class api_album_v3(api_base_v3): | ||
+ | ''' | ||
+ | |||
+ | def search(self, | ||
+ | ''' | ||
+ | 앨범 상세 정보 검색 함수 | ||
+ | |||
+ | Args: | ||
+ | album_id: 앨범의 아이디. Maniadb에서 자체적으로 앨범마다 지정한 수. | ||
+ | 문자열로 된 숫자나 실제 정수형이나 관계없이 입력 가능. | ||
+ | 텍스트의 경우 가급적 유니코드로 입력할 것. | ||
+ | |||
+ | Returns: | ||
+ | 서버로부터 전달된 응답 문서. XML 형식으로 되어 있다. | ||
+ | 만일 네트워크 장애로 XML 문서를 전달받지 못한 경우 None을 리턴한다. | ||
+ | |||
+ | Raises: | ||
+ | ''' | ||
+ | request_url = codeshift.uni2utf8(self.__make_request_url(album_id)) | ||
+ | |||
+ | try: | ||
+ | req = urllib2.urlopen(request_url) | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | return None | ||
+ | |||
+ | return req.read(self.max_buffer) | ||
+ | |||
+ | def __make_request_url(self, | ||
+ | ''' | ||
+ | _url = self.__REQUEST_BASE_URL | ||
+ | _url += u'? | ||
+ | _url += u'&' | ||
+ | _url += u'& | ||
+ | return _url | ||
+ | |||
+ | __REQUEST_BASE_URL = u' | ||
+ | |||
+ | if __name__ == ' | ||
+ | |||
+ | album = api_album_v3() | ||
+ | r = album.search(121480) | ||
+ | |||
+ | if r: | ||
+ | print codeshift.utf82loc(r) | ||
+ | </ | ||
+ | |||
+ | === 후단부(XML문서 -> 파이썬 자료형) === | ||
+ | XML 문서가 성공적으로 전달되어 왔어도 이는 그저 텍스트일 뿐입니다. 프로그램 상에서 의미 있는 정보로 만들기 위해 파싱을 해야 합니다. 후단부는 XML 문서를 파싱하는 역할을 맡습니다. 후단부에도 아티스트/ | ||
+ | |||
+ | 일단 이 모듈은 차후에 범용적으로 쓰일 수 있을 거라 가정하고, | ||
+ | |||
+ | 단, 파이썬 자료형이라고 해서 숫자나 날짜 등의 데이터에 대해 일일이 파이썬 내장 자료형으로 변환하지는 않을 것입니다. ID3 태그가 그렇듯 각 값들은 모두 단순 문자열로써만 처리될 것입니다. 만약 그런 데이터 타입이 필요하다면 직접 변환을 하셔야 합니다. | ||
+ | |||
+ | 우선 파싱하는 모듈의 부모 클래스를 제작합니다. 부모에서 공통적인 역할을 하는 속성/ | ||
+ | <code python parse_base_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import re, xml.dom.minidom, | ||
+ | import codeshift | ||
+ | |||
+ | class parse_base_v3(object): | ||
+ | def __init__(self, | ||
+ | _parseText | ||
+ | self.document = xml.dom.minidom.parseString(_parseText) | ||
+ | |||
+ | # get text data. | ||
+ | # item: Parent node of element_name. This node will be the top node to be searched. | ||
+ | # Only the first element_name element within item node will be chosen. | ||
+ | # If item node is None, this fuction returns an empty string. | ||
+ | # | ||
+ | # element_name: | ||
+ | # It must have the only child, and the only child should be a text node. | ||
+ | # If not, this function returns an empty string. | ||
+ | def _get_text_data(self, | ||
+ | |||
+ | # html entity 제거용 모듈 | ||
+ | par = HTMLParser.HTMLParser() | ||
+ | unescape = par.unescape | ||
+ | |||
+ | e = item.getElementsByTagName(element_name) | ||
+ | if e is None: | ||
+ | return u'' | ||
+ | |||
+ | element = e[0] | ||
+ | if element is not None and \ | ||
+ | | ||
+ | | ||
+ | element.firstChild.nodeType == xml.dom.Node.TEXT_NODE): | ||
+ | return unescape(element.firstChild.data.strip()) | ||
+ | else: | ||
+ | return u'' | ||
+ | |||
+ | # remove all whitespaces between two adjacent tags. | ||
+ | def _make_neat_xml(self, | ||
+ | return re.sub(r'> | ||
+ | |||
+ | @property | ||
+ | def document(self): | ||
+ | return self.__document | ||
+ | |||
+ | @document.setter | ||
+ | def document(self, | ||
+ | self.__document = doc | ||
+ | |||
+ | @property | ||
+ | def result(self): | ||
+ | return self.__result | ||
+ | |||
+ | __document = None | ||
+ | __result | ||
+ | </ | ||
+ | |||
+ | %%_make_neat_xml%% 함수는 XML 문서의 인접한 태그 사이에 있는 불필요한 공백을 삭제합니다. 사람이 보기 좋게 들여쓰기를 잘 맞춘 XML 문서는 시각적으로는 보기 좋지만, DOM Tree를 만들 때 불필요한 텍스트 노드가 많이 생깁니다. 이를 삭제하여 공백에 의해 발생하는 불필요한 노드를 줄입니다. DOM Tree를 이용하므로 사실상 이 작업이 꼭 필요한 것은 아닙니다. | ||
+ | |||
+ | 이제 아티스트/ | ||
+ | <code python parse_search_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | from parse_base_v3 import parse_base_v3 | ||
+ | |||
+ | class parse_search_v3(parse_base_v3): | ||
+ | def __init__(self, | ||
+ | super(parse_search_v3, | ||
+ | self.__parse_common_response() | ||
+ | |||
+ | if | ||
+ | elif itemtype == u' | ||
+ | elif itemtype == u' | ||
+ | else: raise Exception(u' | ||
+ | |||
+ | # common response | ||
+ | def __parse_common_response(self): | ||
+ | root = self.document.documentElement | ||
+ | channel = root.getElementsByTagName(u' | ||
+ | |||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | |||
+ | # 노래 검색에 대한 응답 | ||
+ | def __parse_song(self): | ||
+ | root = self.document.documentElement | ||
+ | items = root.getElementsByTagName(u' | ||
+ | |||
+ | self.result[u' | ||
+ | |||
+ | for item in items: | ||
+ | itembuf = {} | ||
+ | |||
+ | ### id, title, runningtime, | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | ### album info: title, release, link, image, description | ||
+ | album_info = item.getElementsByTagName(u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | ### artist info: link, name | ||
+ | artist_info = item.getElementsByTagName(u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | ### 결과 append | ||
+ | self.result[u' | ||
+ | |||
+ | # 앨범에 대한 파싱 결과 | ||
+ | def __parse_album(self): | ||
+ | root = self.document.documentElement | ||
+ | items = root.getElementsByTagName(u' | ||
+ | |||
+ | self.result[u' | ||
+ | |||
+ | for item in items: | ||
+ | itembuf = {} | ||
+ | |||
+ | ### title, release, link, thumbnail, image, pubDate, author, description, | ||
+ | ### NOTICE: ' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | ### artist info: link, name | ||
+ | artist_info = item.getElementsByTagName(u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | ### 결과 append | ||
+ | self.result[u' | ||
+ | |||
+ | # 아티스트에 대한 검색 | ||
+ | def __parse_artist(self): | ||
+ | root = self.document.documentElement | ||
+ | items = root.getElementsByTagName(u' | ||
+ | |||
+ | self.result[u' | ||
+ | |||
+ | for item in items: | ||
+ | itembuf = {} | ||
+ | |||
+ | ### title, reference, demographic, | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | itembuf[u' | ||
+ | |||
+ | # 결과 append | ||
+ | self.result[u' | ||
+ | |||
+ | if __name__ == ' | ||
+ | with open(' | ||
+ | xmldoc = f.read() | ||
+ | ps = parse_search_v3(u' | ||
+ | print ps.result | ||
+ | |||
+ | with open(' | ||
+ | xmldoc = f.read() | ||
+ | ps = parse_search_v3(u' | ||
+ | print ps.result | ||
+ | |||
+ | with open(' | ||
+ | xmldoc = f.read() | ||
+ | ps = parse_search_v3(u' | ||
+ | print ps.result | ||
+ | |||
+ | </ | ||
+ | |||
+ | 마찬가지로 앨범 세부 정보 XML 문서를 파싱하는 코드를 작성합니다. | ||
+ | <code python parse_album_v3.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | from parse_base_v3 import parse_base_v3 | ||
+ | |||
+ | class parse_album_v3(parse_base_v3): | ||
+ | ''' | ||
+ | 앨범 상세 정보 파싱 클래스 | ||
+ | |||
+ | 입력: XML 문서 | ||
+ | 출력: 파싱된 콘텐츠. 파이썬 기본 데이터형인 딕셔너리와 리스트로 XML 내용을 표현. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | def __init__(self, | ||
+ | super(parse_album_v3, | ||
+ | self.__parse_response(self.document) | ||
+ | |||
+ | def __parse_response(self, | ||
+ | root = document.documentElement | ||
+ | item = document.getElementsByTagName(u' | ||
+ | |||
+ | ### id, seq, title, shorttitle, longtitle, link, releasedate, | ||
+ | ### NOTICE: ' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | |||
+ | ### artist info: id, name | ||
+ | artist = document.getElementsByTagName(u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | self.result[u' | ||
+ | |||
+ | ### disc info | ||
+ | self.result[u' | ||
+ | discs = document.getElementsByTagName(u' | ||
+ | for disc in discs: | ||
+ | disc_buf = {} | ||
+ | disc_buf[u' | ||
+ | disc_buf[u' | ||
+ | disc_buf[u' | ||
+ | |||
+ | songs = disc.getElementsByTagName(u' | ||
+ | trackno = 1 | ||
+ | |||
+ | for song in songs: | ||
+ | song_buf = {} | ||
+ | tag_no = int(song.getAttribute(u' | ||
+ | if trackno > tag_no: continue # 중복된 트랙 자료가 넘어옴 | ||
+ | |||
+ | song_buf[u' | ||
+ | song_buf[u' | ||
+ | song_buf[u' | ||
+ | song_buf[u' | ||
+ | song_buf[u' | ||
+ | |||
+ | trackno += 1 | ||
+ | disc_buf[u' | ||
+ | |||
+ | self.result[u' | ||
+ | |||
+ | ### product info | ||
+ | product_infos = item.getElementsByTagName(u' | ||
+ | products | ||
+ | self.result[u' | ||
+ | |||
+ | for product in products: | ||
+ | product_buf = {} | ||
+ | |||
+ | ### seqno, releasedate, | ||
+ | product_buf[u' | ||
+ | product_buf[u' | ||
+ | product_buf[u' | ||
+ | |||
+ | self.result[u' | ||
+ | |||
+ | if __name__ == ' | ||
+ | with open(' | ||
+ | xmldoc = f.read() | ||
+ | pa = parse_album_v3(xmldoc) | ||
+ | print pa.result | ||
+ | </ | ||
+ | |||
+ | ==== ID3Tag 편집 모듈 ==== | ||
+ | 이 모듈은 프로그램에서 필요한 ID3 태그 편집 및 MP3 파일 정보와 관련된 모든 기능을 담당합니다. | ||
+ | |||
+ | <code python id3tag_manip.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import re, urllib2 | ||
+ | import eyed3 | ||
+ | import codeshift | ||
+ | import util | ||
+ | |||
+ | class maniadb_tag_info: | ||
+ | ''' | ||
+ | Maniadb 태그 정보 기록을 위한 클래스 | ||
+ | |||
+ | update_mp3_tag 일반함수의 내부에서 사용된다. 유저 코드에서 직접적으로 선언되어 | ||
+ | 사용되는 일은 없다. | ||
+ | ''' | ||
+ | |||
+ | def get_info(self, | ||
+ | ''' | ||
+ | < | ||
+ | 파싱된 콘텐츠란 ' | ||
+ | OpenAPI를 통해 전달 받은 XML문서의 구조를 파이썬의 딕셔너리와 리스트로 | ||
+ | 변환시킨 것이다. | ||
+ | |||
+ | 이 클래스가 멤버 변수로 기록하는 내용은 다음과 같다. | ||
+ | |||
+ | 제목 | ||
+ | ============================================================= | ||
+ | 앨범 제목 | ||
+ | 아티스트 이름 | ||
+ | 커버 이미지 데이터 | ||
+ | 커버 이미지 MIME type cover_type | ||
+ | 커버 이미지 URL cover_url | ||
+ | 디스크 번호 | ||
+ | 레이블 | ||
+ | 발매일 | ||
+ | 곡명 | ||
+ | 트랙 번호 | ||
+ | 전체 디스크 장수 | ||
+ | 디스크의 전체 트랙 | ||
+ | |||
+ | Args: | ||
+ | song_info: | ||
+ | idx: 곡 정보 검색 결과 중 한 아이템의 인덱스. | ||
+ | album_info: 앨범 세부 정보 (파싱된 콘텐츠) | ||
+ | |||
+ | Returns: | ||
+ | 모듈의 내부 변수에 지정된 값이 저장된다. | ||
+ | |||
+ | Raises: | ||
+ | 일반 예외: 디스크/ | ||
+ | | ||
+ | | ||
+ | ''' | ||
+ | |||
+ | # choose song item | ||
+ | song_item | ||
+ | |||
+ | # song tags | ||
+ | if album_info != None: | ||
+ | self.album_title | ||
+ | else: | ||
+ | self.album_title | ||
+ | |||
+ | self.artist_name | ||
+ | self.song_title | ||
+ | self.release_date = song_item[u' | ||
+ | |||
+ | # fix: some song title may have a html anchor tag. Get rid of it. | ||
+ | detect_link = re.match(r' | ||
+ | if detect_link: | ||
+ | self.song_title = '' | ||
+ | |||
+ | # get url | ||
+ | self.cover_url | ||
+ | |||
+ | # get binary image and image type from url | ||
+ | resp = urllib2.urlopen(self.cover_url) | ||
+ | self.cover_type | ||
+ | self.cover_bin | ||
+ | |||
+ | # optional data | ||
+ | if album_info != None: | ||
+ | # search for disc number, track number | ||
+ | disc_num, track_num = self.__search_song_id(song_item[u' | ||
+ | |||
+ | if disc_num == None or track_num == None: | ||
+ | msg = u'Song ID \'' | ||
+ | msg += u' | ||
+ | raise Exception(msg) | ||
+ | |||
+ | self.song_track | ||
+ | self.disc_num | ||
+ | self.total_disc | ||
+ | self.total_track = unicode(len(album_info[u' | ||
+ | self.publisher | ||
+ | |||
+ | # Just for dumping | ||
+ | def dump(self): | ||
+ | ''' | ||
+ | |||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | # save image to file | ||
+ | def save_cover_as(self, | ||
+ | ''' | ||
+ | |||
+ | if self.cover_bin == '': | ||
+ | raise Exception(u' | ||
+ | |||
+ | if self.cover_type == '': | ||
+ | raise Exception(u' | ||
+ | |||
+ | ext = self.cover_type.split(u'/' | ||
+ | file_name = file_name_no_ext + u' | ||
+ | with open(file_name, | ||
+ | f.write(self.cover_bin) | ||
+ | |||
+ | # find song_id in disc list | ||
+ | def __search_song_id(self, | ||
+ | #print ' | ||
+ | disc_num | ||
+ | track_num | ||
+ | |||
+ | # disc[u' | ||
+ | # 이는 태그 기록 시점에서 문제가 될 수 있으므로 가급적 피한다. | ||
+ | # 디스크 번호와 마찬가지로 트랙 번호도 직접 integer 변수로 처리. | ||
+ | for disc in discs: | ||
+ | songs = disc[u' | ||
+ | # | ||
+ | disc_num | ||
+ | track_num = 0 | ||
+ | |||
+ | for song in songs: | ||
+ | track_num += 1 | ||
+ | #print u' | ||
+ | if song[u' | ||
+ | return disc_num, track_num | ||
+ | |||
+ | return None, None | ||
+ | |||
+ | album_title | ||
+ | artist_name | ||
+ | cover_bin | ||
+ | cover_type | ||
+ | cover_url | ||
+ | disc_num | ||
+ | publisher | ||
+ | release_date = u'' | ||
+ | song_title | ||
+ | song_track | ||
+ | total_disc | ||
+ | total_track | ||
+ | |||
+ | ### END OF CLASS maniadb_tag_info ### | ||
+ | |||
+ | # Update MP3 Tag from information | ||
+ | def update_mp3_tag(file_name, | ||
+ | ''' | ||
+ | mp3 태그를 새롭게 갱신한다. 기존의 태그 정보는 완전 삭제된다. | ||
+ | mp3 태그 수정을 위해 eyed3 라이브러리를 사용하였다. | ||
+ | 태그 수정은 일괄적으로 ID3V2.3을 이용하도록 되어 있다. | ||
+ | |||
+ | Args: | ||
+ | file_name: | ||
+ | song_info: | ||
+ | idx: 곡 정보 검색 결과 중 한 아이템의 인덱스. | ||
+ | album_info: 앨범 세부 정보 (파싱된 콘텐츠) | ||
+ | |||
+ | Returns: | ||
+ | 없음. | ||
+ | |||
+ | Raises: | ||
+ | 정의하지 않음. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | # Maniadb tag information class | ||
+ | tag_info = maniadb_tag_info() | ||
+ | |||
+ | # Get information from information object | ||
+ | tag_info.get_info(song_info, | ||
+ | |||
+ | # DUMP for debugging | ||
+ | tag_info.dump() | ||
+ | |||
+ | # Remove current tag | ||
+ | eyed3.id3.tag.Tag.remove(file_name) | ||
+ | |||
+ | # Load file | ||
+ | audio_file = eyed3.load(file_name) | ||
+ | |||
+ | # Input new tag | ||
+ | audio_file.tag | ||
+ | audio_file.tag.header.version = eyed3.id3.ID3_V2_3 | ||
+ | audio_file.tag.album | ||
+ | audio_file.tag.artist | ||
+ | audio_file.tag.disc_num | ||
+ | audio_file.tag.publisher | ||
+ | audio_file.tag.title | ||
+ | audio_file.tag.track_num | ||
+ | |||
+ | # Image | ||
+ | imagetype = eyed3.id3.frames.ImageFrame.stringToPicType(' | ||
+ | audio_file.tag.images.set(imagetype, | ||
+ | |||
+ | # optional, TYER, release date | ||
+ | reldate = tag_info.release_date | ||
+ | |||
+ | if reldate != '': | ||
+ | try: | ||
+ | year = int(reldate[0: | ||
+ | month = int(reldate[4: | ||
+ | day = int(reldate[6: | ||
+ | |||
+ | if month == 0: month = None | ||
+ | if day == 0: day = None | ||
+ | |||
+ | audio_file.tag.release_date = eyed3.core.Date(year, | ||
+ | audio_file.tag.setTextFrame(u' | ||
+ | |||
+ | except ValueError: | ||
+ | print u' | ||
+ | print u'Skip writing release date tag' | ||
+ | |||
+ | except IndexError: | ||
+ | print u' | ||
+ | print u'Skip writing release date tag' | ||
+ | |||
+ | # Save | ||
+ | audio_file.tag.save(file_name) | ||
+ | |||
+ | # MP3 File information. | ||
+ | def mp3info(file_name): | ||
+ | ''' | ||
+ | MP3 파일과 태그의 정보를 검색한다. | ||
+ | |||
+ | Args: | ||
+ | file_name: 검색할 파일명. | ||
+ | |||
+ | Returns: | ||
+ | 정보를 담은 파이썬 딕셔너리 객체. | ||
+ | 딕셔너리 객체는 다음과 같은 정보를 가지고 있다. | ||
+ | |||
+ | 키 값 | ||
+ | ======================================== | ||
+ | tag_ver | ||
+ | path 파일의 절대 경로. | ||
+ | song 노래 제목. | ||
+ | artist | ||
+ | album | ||
+ | release_date | ||
+ | time 재생시간 hh:mm:ss 혹은 mm:ss 로 표시된다. | ||
+ | vbr | ||
+ | bit_rate | ||
+ | size_bytes | ||
+ | disc_num | ||
+ | track_num | ||
+ | publisher | ||
+ | |||
+ | Raises: | ||
+ | 정의하지 않음. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | audio_file = eyed3.load(file_name) | ||
+ | tag = audio_file.tag | ||
+ | info = audio_file.info | ||
+ | |||
+ | tag_info = { | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | u' | ||
+ | } | ||
+ | |||
+ | ### Some information available even though tag doesn' | ||
+ | # path | ||
+ | tag_info[u' | ||
+ | |||
+ | # time | ||
+ | tag_info[u' | ||
+ | |||
+ | # vbr | ||
+ | tag_info[u' | ||
+ | |||
+ | # bitrate | ||
+ | tag_info[u' | ||
+ | |||
+ | # size_bytes | ||
+ | tag_info[u' | ||
+ | |||
+ | if tag is None: | ||
+ | return tag_info | ||
+ | |||
+ | ### Tag information | ||
+ | # tag version | ||
+ | if tag.header.version is not None: | ||
+ | tag_info[u' | ||
+ | |||
+ | # song title | ||
+ | if tag.title is not None : | ||
+ | tag_info[u' | ||
+ | |||
+ | # artist | ||
+ | if tag.artist is not None : | ||
+ | tag_info[u' | ||
+ | |||
+ | # album title | ||
+ | if tag.album is not None : | ||
+ | tag_info[u' | ||
+ | |||
+ | # release date | ||
+ | if tag.release_date is not None: | ||
+ | tag_info[u' | ||
+ | |||
+ | # disc number | ||
+ | if tag.disc_num is not None: | ||
+ | tag_info[u' | ||
+ | |||
+ | # track number | ||
+ | if tag.track_num is not None: | ||
+ | tag_info[u' | ||
+ | |||
+ | # publisher | ||
+ | if tag.publisher is not None: | ||
+ | tag_info[u' | ||
+ | |||
+ | return tag_info | ||
+ | |||
+ | def sec2hms(time_sec): | ||
+ | ''' | ||
+ | sec = time_sec | ||
+ | hour = sec/3600 | ||
+ | sec = sec%3600 | ||
+ | min = sec/60 | ||
+ | sec = sec%60 | ||
+ | |||
+ | if hour > 0: | ||
+ | return u' | ||
+ | else: | ||
+ | return u' | ||
+ | </ | ||
+ | |||
+ | ==== 콘솔 메뉴 모듈 ==== | ||
+ | 콘솔 인터페이스과 관련된 모듈들입니다. 프롬프트이므로 키보드 입력만을 받을 것인데, 사실 간단한 키보드 입력도 짜임새 있게 구현하려면 귀찮아지기 마련입니다. | ||
+ | |||
+ | 콘솔 메뉴 또한 기본 클래스를 정의하고, | ||
+ | |||
+ | <code python interface_base.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import codeshift | ||
+ | |||
+ | |||
+ | class interface_base: | ||
+ | ''' | ||
+ | 기본 인터페이스 클래스 | ||
+ | |||
+ | CLI 기반의 쉘을 제외한 모든 사용자 인터페이스는 본 클래스를 상속받아 구현됩니다. | ||
+ | CLI로 키 입력, 명령 확인, 입력값 검사 등의 절차를 간결히 구현하는 것이 목적입니다. | ||
+ | 하나의 입력 절차를 하나의 클래스로 정의합니다. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | def before_loop(self): | ||
+ | ''' | ||
+ | pass | ||
+ | |||
+ | def after_loop(self): | ||
+ | ''' | ||
+ | pass | ||
+ | |||
+ | def menuhandler(self, | ||
+ | ''' | ||
+ | 메뉴 핸들러 함수 | ||
+ | |||
+ | 표준 입력으로부터 한 줄을 입력 받아, 입력받은 문자열을 공백 기준으로 분해해 | ||
+ | 리스트로 만든다. 첫번째 리스트 원소가 ' | ||
+ | 인자 목록은 None이 될 수 있다. | ||
+ | |||
+ | Args | ||
+ | menustr: 표준 출력으로 출력될 문자열. | ||
+ | | ||
+ | |||
+ | commands: 핸들러가 처리할 명령어 목록들. 반드시 리스트 형으로 입력. | ||
+ | 명령어는 한번에 2개 이상을 입력할 수 없다. | ||
+ | 매직 워드인 ' | ||
+ | 명령과도 매치될 수 있다. 그러므로 매직 워드는 보통 리스트의 | ||
+ | 가장 마지막 원소로 두는 것이 좋다. | ||
+ | 리스트의 길이는 callback 인자의 리스트 수와 일치해야 한다. | ||
+ | 길이는 0이 될 수 없다. | ||
+ | |||
+ | callbacks: 핸들러 함수의 목록. 반드시 리스트 형으로 입력하여야 한다. | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | Returns | ||
+ | 콜백 함수의 리턴값(True/ | ||
+ | 다음 단계로 넘어갈지, | ||
+ | |||
+ | Raises | ||
+ | 일반 예외. 인자인 commands와 callbacks가 리스트가 아닌경우, | ||
+ | 리스트라도 길이가 0인 경우나 서로의 길이가 일치하지 않을 경우 일어난다. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | if type(commands) != list or type(callbacks) != list: | ||
+ | raise Exception(u' | ||
+ | |||
+ | if len(commands) != len(callbacks): | ||
+ | raise Exception(u' | ||
+ | |||
+ | if len(commands) == 0: | ||
+ | raise Exception(u' | ||
+ | |||
+ | self.before_loop() | ||
+ | |||
+ | # the loop | ||
+ | while True: | ||
+ | print menustr, | ||
+ | str = codeshift.loc2uni(raw_input()) | ||
+ | inp = str.split() | ||
+ | |||
+ | if len(inp) == 0: | ||
+ | for i in xrange(len(commands)): | ||
+ | if commands[i] == u' | ||
+ | return callbacks[i](str) | ||
+ | else: | ||
+ | cmd = inp[0] | ||
+ | args = None | ||
+ | |||
+ | if len(inp) > 1: | ||
+ | args = inp[1:] | ||
+ | |||
+ | for i in xrange(len(commands)): | ||
+ | if commands[i] == u' | ||
+ | return callbacks[i](str) | ||
+ | |||
+ | elif cmd == commands[i]: | ||
+ | return callbacks[i](args) | ||
+ | |||
+ | print self.nocmd_msg % str | ||
+ | |||
+ | self.after_loop() | ||
+ | |||
+ | ''' | ||
+ | __response = 0 | ||
+ | |||
+ | ''' | ||
+ | nocmd_msg = u' | ||
+ | </ | ||
+ | |||
+ | <code python interfaece.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import codeshift | ||
+ | import id3tag_manip | ||
+ | from | ||
+ | |||
+ | ''' | ||
+ | Interface 모듈 | ||
+ | |||
+ | CLI 환경에서 사용자로부터 명령을 입력받거나 메시지를 출력하는 등, | ||
+ | 인터페이스와 관련된 내용이 이곳에 정의되어 있습니다. | ||
+ | ''' | ||
+ | # 사용자로부터 키워드를 입력받는 메뉴입니다. | ||
+ | # base class인 interface_base에는 인터페이스의 종료 결과를 | ||
+ | # 표시하는response 변수가 정의되어 있습니다. | ||
+ | # 0: success, 1:fail | ||
+ | class keyword_interface(interface_base): | ||
+ | ''' | ||
+ | |||
+ | def show_menu(self, | ||
+ | ''' | ||
+ | 인터페이스 시작점 함수 | ||
+ | |||
+ | Args: | ||
+ | file_name: 검색할 파일 이름 | ||
+ | |||
+ | Returns: | ||
+ | 조작이 끝나고 올바른 상태이면 0, 아니면 1을 리턴한다. | ||
+ | |||
+ | Raises: | ||
+ | 정의되지 않음 | ||
+ | |||
+ | ''' | ||
+ | |||
+ | self.file_info | ||
+ | self.keyword_song | ||
+ | self.keyword_artist = self.file_info[u' | ||
+ | |||
+ | menustring | ||
+ | menustring += u' 키워드 입력 ' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | |||
+ | # 영-한 전환이 올바르지 않아도 되도록 처리 | ||
+ | commands | ||
+ | callbacks = [self.show_file_info, | ||
+ | |||
+ | while self.menuhandler(menustring, | ||
+ | print u'' | ||
+ | |||
+ | return self.__response | ||
+ | |||
+ | def show_file_info(self, | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print '' | ||
+ | |||
+ | return False | ||
+ | |||
+ | def quit(self, args): | ||
+ | ''' | ||
+ | |||
+ | self.__response = 1 | ||
+ | return True | ||
+ | |||
+ | def keyword_input(self, | ||
+ | ''' | ||
+ | |||
+ | if str != '': | ||
+ | keywords = str.split(',' | ||
+ | |||
+ | if len(keywords) == 2: | ||
+ | self.keyword_song | ||
+ | self.keyword_artist = keywords[1].strip() | ||
+ | else: | ||
+ | self.keyword_song | ||
+ | self.keyword_artist = '' | ||
+ | |||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | r = codeshift.loc2uni(raw_input()).strip() | ||
+ | if r == u'': | ||
+ | self.__response = 0 | ||
+ | return | ||
+ | else: | ||
+ | return False | ||
+ | |||
+ | ''' | ||
+ | file_info | ||
+ | |||
+ | ''' | ||
+ | keyword_song | ||
+ | |||
+ | ''' | ||
+ | keyword_artist = u'' | ||
+ | |||
+ | |||
+ | class itemselection_interface(interface_base): | ||
+ | ''' | ||
+ | |||
+ | def show_menu(self, | ||
+ | ''' | ||
+ | 메뉴 시작점 함수 | ||
+ | |||
+ | Args | ||
+ | item_list: 곡 정보 검색 결과 (파싱된 콘텐츠) 중 ' | ||
+ | 이 값은 리스트로 되어 있다. | ||
+ | |||
+ | Returns | ||
+ | 0: 정상 종료. 다음 단계로 진행해도 좋음. | ||
+ | 1: 사용자로부터 키워드 검색으로 다시 돌아가라는 명령을 받음. | ||
+ | 2: 사용자로부터 쉘로 다시 돌아가라는 명령을 받음. | ||
+ | |||
+ | Raises | ||
+ | 정의되지 않음 | ||
+ | ''' | ||
+ | |||
+ | self.__item_list = item_list | ||
+ | |||
+ | menustring = u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | menustring += u' | ||
+ | |||
+ | commands | ||
+ | callbacks = [self.show_list, | ||
+ | |||
+ | self.show_list(None) | ||
+ | |||
+ | while self.menuhandler(menustring, | ||
+ | print u'' | ||
+ | |||
+ | return self.__response | ||
+ | |||
+ | def show_list(self, | ||
+ | ''' | ||
+ | |||
+ | for n in xrange(1, len(self.__item_list)+1): | ||
+ | self.__print_item(n) | ||
+ | print '' | ||
+ | return False | ||
+ | |||
+ | def select_item(self, | ||
+ | ''' | ||
+ | |||
+ | if str.isnumeric() == False: | ||
+ | print (str+u' | ||
+ | return False | ||
+ | |||
+ | idx = int(str) | ||
+ | self.__print_item(idx) | ||
+ | |||
+ | confirm = confirm_interface() | ||
+ | resp = confirm.show_menu() | ||
+ | |||
+ | if resp == 0: | ||
+ | return False | ||
+ | |||
+ | self.__response | ||
+ | self.selnum = idx | ||
+ | return True | ||
+ | |||
+ | def cancel(self, | ||
+ | ''' | ||
+ | |||
+ | self.__response = 1 # cancel. search by keyword again | ||
+ | return True | ||
+ | |||
+ | def quit(self, str): | ||
+ | ''' | ||
+ | |||
+ | self.__response = 2 # quit to shell | ||
+ | return True | ||
+ | |||
+ | def __print_item(self, | ||
+ | if 1 <= num and num <= len(self.__item_list): | ||
+ | song = self.__item_list[num-1] | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'' | ||
+ | else: | ||
+ | raise Exception(u' | ||
+ | |||
+ | ''' | ||
+ | selnum = 0 | ||
+ | |||
+ | ''' | ||
+ | __item_list | ||
+ | |||
+ | |||
+ | class confirm_interface(interface_base): | ||
+ | ''' | ||
+ | |||
+ | def show_menu(self): | ||
+ | ''' | ||
+ | 메뉴 시작점 함수 | ||
+ | |||
+ | Args | ||
+ | |||
+ | Returns | ||
+ | 1: yes | ||
+ | 0: no | ||
+ | |||
+ | Raises | ||
+ | 정의되지 않음 | ||
+ | ''' | ||
+ | |||
+ | menustring = u' | ||
+ | commands | ||
+ | callbacks | ||
+ | |||
+ | # 기본 에러메시지 오버라이드. | ||
+ | self.nocmd_msg = u'y나 n 중 하나만을 선택하세요.' | ||
+ | |||
+ | while self.menuhandler(menustring, | ||
+ | print u'' | ||
+ | |||
+ | return self.__response | ||
+ | |||
+ | def yes(self, str): | ||
+ | self.__response = 1 | ||
+ | return True | ||
+ | |||
+ | def no(self, str): | ||
+ | self.__response = 0 | ||
+ | return True | ||
+ | |||
+ | </ | ||
+ | |||
+ | ==== 쉘 관련 모듈 ==== | ||
+ | 쉘은 파이썬 표준 라이브러리의 Cmd를 상속받아 구현합니다. 이것을 이용하면 보다 편리하게 쉘을 구현할 수 있습니다. | ||
+ | 쉘 클래스 또한 분량이 엄청납니다만, | ||
+ | |||
+ | 우리의 프로그램은 쉘부터 시작하므로, | ||
+ | python maniadb_shell.py [인수...] | ||
+ | | ||
+ | <code python manidadb_shell.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import cmd, os, sys | ||
+ | import codeshift | ||
+ | import util | ||
+ | import id3tag_manip | ||
+ | from | ||
+ | from | ||
+ | |||
+ | class maniadb_shell(cmd.Cmd): | ||
+ | def emptyline(self): | ||
+ | pass | ||
+ | |||
+ | def do_load(self, | ||
+ | try: | ||
+ | loaded_num = self.core.load(str.split()) | ||
+ | print u'%d file(s) loaded.' | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_unload(self, | ||
+ | try: | ||
+ | nums = self.__parse_param(str) | ||
+ | unloaded_num = self.core.unload(nums) | ||
+ | print u'%d file(s) unloaded.' | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_clear(self, | ||
+ | try: | ||
+ | self.core.clear_list() | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_list(self, | ||
+ | try: | ||
+ | if str == u'': | ||
+ | nums = [x for x in xrange(1, self.core.list_size()+1)] | ||
+ | else: | ||
+ | nums = self.__parse_param(str) | ||
+ | |||
+ | totnum = self.core.list_size() | ||
+ | for num in nums: | ||
+ | str = u' | ||
+ | if self.core.is_tagged(num) == True: | ||
+ | str += u' | ||
+ | str += util.fit_string(self.core.get_file_name(num), | ||
+ | print str | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_det(self, | ||
+ | try: | ||
+ | num = int(str) | ||
+ | file_name = self.core.get_file_name(num) | ||
+ | file_info = id3tag_manip.mp3info(file_name) | ||
+ | |||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'' | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_retr(self, | ||
+ | try: | ||
+ | nums = self.__parse_param(str) | ||
+ | tot_num = self.core.list_size() | ||
+ | for num in nums: | ||
+ | print u'[%d / %d]' % (num, tot_num) | ||
+ | if self.__retrieve(num) == False: | ||
+ | print u' | ||
+ | return False | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_retrall(self, | ||
+ | try: | ||
+ | tot_num = self.core.list_size() | ||
+ | for num in xrange(1, self.core.list_size()+1): | ||
+ | if self.core.is_tagged(num) == True: | ||
+ | print u' | ||
+ | continue | ||
+ | |||
+ | print u'[%d / %d]' % (num, tot_num) | ||
+ | if self.__retrieve(num) == False: | ||
+ | print u' | ||
+ | return False | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_nresp(self, | ||
+ | try: | ||
+ | if str == u'': | ||
+ | print self.nresp | ||
+ | else: | ||
+ | self.nresp = int(str) | ||
+ | print u'Set number of responses to', self.nresp | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_exp(self, | ||
+ | try: | ||
+ | if str == '': | ||
+ | print >> sys.stderr, u' | ||
+ | return False | ||
+ | |||
+ | all_files = self.core.get_all_files() | ||
+ | with open(str, u' | ||
+ | for one in all_files: | ||
+ | f.write(codeshift.uni2loc(one+u' | ||
+ | print u'%d item(s) exported to %s' % (len(all_files), | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_expunt(self, | ||
+ | try: | ||
+ | if str == '': | ||
+ | print >> sys.stderr, u' | ||
+ | return False | ||
+ | |||
+ | untagged_files = self.core.get_untagged_files() | ||
+ | with open(str, u' | ||
+ | for untagged in untagged_files: | ||
+ | f.write(codeshift.uni2loc(untagged+u' | ||
+ | |||
+ | print u'%d untagged item(s) exported to %s' % (len(untagged_files), | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_exptag(self, | ||
+ | try: | ||
+ | if str == '': | ||
+ | print >> sys.stderr, u' | ||
+ | return False | ||
+ | |||
+ | tagged_files = self.core.get_tagged_files() | ||
+ | with open(str, u' | ||
+ | for tagged in tagged_files: | ||
+ | f.write(codeshift.uni2loc(tagged+u' | ||
+ | |||
+ | print u'%d tagged item(s) exported to %s' % (len(tagged_files), | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_exit(self, | ||
+ | print u' | ||
+ | sys.exit(0) | ||
+ | |||
+ | def do_tag(self, | ||
+ | try: | ||
+ | nums = self.__parse_param(str) | ||
+ | for num in nums: | ||
+ | self.core.set_tagged(num, | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | def do_unt(self, | ||
+ | try: | ||
+ | nums = self.__parse_param(str) | ||
+ | for num in nums: | ||
+ | self.core.set_tagged(num, | ||
+ | |||
+ | except Exception as e: | ||
+ | util.trace_error(u' | ||
+ | |||
+ | |||
+ | def do_EOF(self, | ||
+ | return True | ||
+ | |||
+ | # return True: Ok to go to next item | ||
+ | # return False: Stop right now | ||
+ | def __retrieve(self, | ||
+ | song_info | ||
+ | song_selnum = -1 # chosen item number | ||
+ | album_info | ||
+ | |||
+ | # get the item of num | ||
+ | file_name = self.core.get_file_name(num) | ||
+ | |||
+ | while True: | ||
+ | # cli interface: keyword input from user | ||
+ | kwdiface = keyword_interface() | ||
+ | if kwdiface.show_menu(file_name) != 0: | ||
+ | print u' | ||
+ | return False | ||
+ | |||
+ | # send a query and parse the result | ||
+ | song_info = self.core.song_query(kwdiface.keyword_song, | ||
+ | |||
+ | # cli interface: choose right one from responses | ||
+ | itmiface = itemselection_interface() | ||
+ | |||
+ | # itemselection interface response. | ||
+ | # 0: success, 1: back to keyword search, 2: exit to shell | ||
+ | response = itmiface.show_menu(song_info[u' | ||
+ | |||
+ | if response == 0: | ||
+ | song_selnum = itmiface.selnum | ||
+ | break | ||
+ | |||
+ | elif response == 1: | ||
+ | continue | ||
+ | |||
+ | elif response == 2: | ||
+ | return False | ||
+ | |||
+ | else: | ||
+ | raise Exception(u' | ||
+ | |||
+ | # querying album information | ||
+ | chosen_item = song_info[u' | ||
+ | album_link | ||
+ | album_id | ||
+ | |||
+ | # album info | ||
+ | print u' | ||
+ | album_info | ||
+ | |||
+ | # update mp3 tag | ||
+ | self.core.update(num, | ||
+ | print u' | ||
+ | return True | ||
+ | |||
+ | def __parse_param(self, | ||
+ | fin_list = [] | ||
+ | |||
+ | for t in str.split(): | ||
+ | # convert t to unicode form | ||
+ | ut = codeshift.loc2uni(t) | ||
+ | |||
+ | # hyphen position | ||
+ | hypos = ut.find(u' | ||
+ | |||
+ | # we couldn' | ||
+ | if hypos == -1: | ||
+ | if ut.isnumeric(): | ||
+ | intn = int(ut) | ||
+ | if 0 < intn and intn <= self.core.list_size(): | ||
+ | fin_list.append(intn) | ||
+ | else: | ||
+ | raise Exception(ut+u': | ||
+ | else: | ||
+ | raise Exception(ut+u' | ||
+ | |||
+ | # we've found hyphen, | ||
+ | else: | ||
+ | # hyphen is the first character - | ||
+ | if hypos == 0: | ||
+ | endstr = ut[1:] | ||
+ | # thoroughly check the number form | ||
+ | if endstr.isnumeric(): | ||
+ | endint = int(endstr) | ||
+ | if 0 < endint and endint <= self.core.list_size(): | ||
+ | # valid number form is converted to a series of numbers from 1 to endint | ||
+ | for x in xrange(1, endint+1): | ||
+ | fin_list.append(x) | ||
+ | else: | ||
+ | raise Exception(ut+u': | ||
+ | else: | ||
+ | raise Exception(ut+u' | ||
+ | |||
+ | # hyphen is the last character - | ||
+ | elif hypos == len(ut)-1: | ||
+ | stastr = ut[:hypos] | ||
+ | # thoroughly check the number form | ||
+ | if stastr.isnumeric(): | ||
+ | staint = int(stastr) | ||
+ | if 0 < staint and staint <= self.core.list_size(): | ||
+ | # valid number form is converted to a series of numbers from staint to the last of the list | ||
+ | for x in xrange(staint, | ||
+ | fin_list.append(x) | ||
+ | else: | ||
+ | raise Exception(ut+u': | ||
+ | else: | ||
+ | raise Exception(ut+u' | ||
+ | |||
+ | # hyphen is the middle character | ||
+ | else: | ||
+ | stastr, endstr = ut.split(u' | ||
+ | if stastr.isnumeric() and endstr.isnumeric(): | ||
+ | staint, endint = int(stastr), | ||
+ | # check integer numbers are all valid | ||
+ | if staint > 0 and endint > 0 and staint <= endint and endint <= self.core.list_size(): | ||
+ | for x in xrange(staint, | ||
+ | fin_list.append(x) | ||
+ | else: | ||
+ | raise Exception(ut+u': | ||
+ | else: | ||
+ | raise Exception(ut+u' | ||
+ | |||
+ | # sort numbers and remove duplication, | ||
+ | fin_list = list(set(fin_list)) | ||
+ | fin_list.sort() | ||
+ | return fin_list | ||
+ | |||
+ | def help_EOF(self): | ||
+ | pass | ||
+ | |||
+ | def help_det(self): | ||
+ | print u'=== det (detailed info) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: det 3' | ||
+ | |||
+ | def help_exp(self): | ||
+ | print u'=== exp (export all) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: exp list.txt' | ||
+ | |||
+ | def help_expunt(self): | ||
+ | print u'=== expunt (exprt untagged) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: expunt untagged.txt' | ||
+ | |||
+ | def help_load(self): | ||
+ | print u'=== load 명령어 도움말 ===\n' | ||
+ | print u'한 개 이상의 파일 이름을 인자로 받을 수 있습니다.' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: load a.mp3 b.mp3 @othrs.txt' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'' | ||
+ | print u' | ||
+ | |||
+ | def help_retr(self): | ||
+ | print u'=== retr (retrieve) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'단, 시작 번호와 끝 번호, 그리고 \' | ||
+ | print u' | ||
+ | print u'예: retr 1 2 5-8 (목록 번호 1, 2, 5, 6, 7, 8번의 태그 정보를 검색합니다.)' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | def help_tag(self): | ||
+ | print u'=== tag (set tagged) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: tag 1 7 22 (1, 7, 22번 MP3 파일에 대해 태그를 수정한 것으로 처리.)' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | def help_unt(self): | ||
+ | print u'=== unt (set untagged) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: unt 1 7 22 (1, 7, 22번 MP3 파일에 대해 태그를 수정하지 않은 것으로 처리.)' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | def help_clear(self): | ||
+ | print u'=== clear 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u'예: clear' | ||
+ | |||
+ | def help_exit(self): | ||
+ | print u'=== help_exit 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u'예: exit' | ||
+ | |||
+ | def help_exptag(self): | ||
+ | print u'=== exptag (export tagged) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: exptag tagged.txt' | ||
+ | |||
+ | def help_nresp(self): | ||
+ | print u'=== nresp (number of responses) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: nresp (현재 설정값 출력)' | ||
+ | print u' | ||
+ | |||
+ | def help_retrall(self): | ||
+ | print u'=== retrall (retrieve all) 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u'예: retrall' | ||
+ | |||
+ | def help_unload(self): | ||
+ | print u'=== unload 명령어 도움말 ===\n' | ||
+ | print u'load 명령과 반대로 목록에서 내용을 삭제합니다.' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: unload 3 (3번을 목록에서 삭제)' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | def help_list(self): | ||
+ | print u'=== list 명령어 도움말 ===\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u'예: list -10 (1~10번을 출력)' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | core = maniadb_core() | ||
+ | nresp = 10 | ||
+ | |||
+ | def help(): | ||
+ | print u'A Simple Tag Retreival Agent, powered by maniadb.com\n' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | print u' | ||
+ | |||
+ | def main(argv): | ||
+ | if len(argv) == 2 and argv[1] == u' | ||
+ | help() | ||
+ | return 0 | ||
+ | else: | ||
+ | try: | ||
+ | shell = maniadb_shell() | ||
+ | |||
+ | if len(argv) > 1: shell.core.load(argv[1: | ||
+ | shell.doc_header = codeshift.uni2loc(u' | ||
+ | shell.nohelp | ||
+ | shell.prompt | ||
+ | shell.cmdloop(codeshift.uni2loc(u' | ||
+ | |||
+ | except KeyboardInterrupt: | ||
+ | print u' | ||
+ | |||
+ | return 0 | ||
+ | |||
+ | if __name__ == ' | ||
+ | sys.exit(main(sys.argv)) | ||
+ | </ | ||
+ | |||
+ | ==== 코어 모듈 ==== | ||
+ | 쉘이 명령하고자 하는 바를 쉘 안에 모두 구현해버리면 프로그램도 복잡해지고, | ||
+ | |||
+ | <code python maniadb_core.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import os, time | ||
+ | import codeshift | ||
+ | import id3tag_manip | ||
+ | from | ||
+ | from | ||
+ | from | ||
+ | from | ||
+ | |||
+ | class data_elem: | ||
+ | ''' | ||
+ | |||
+ | @property | ||
+ | def modified(self): | ||
+ | return self.__modified | ||
+ | |||
+ | @modified.setter | ||
+ | def modified(self, | ||
+ | self.__modified = flag | ||
+ | |||
+ | @property | ||
+ | def file_name(self): | ||
+ | return self.__file_name | ||
+ | |||
+ | @file_name.setter | ||
+ | def file_name(self, | ||
+ | return self.__file_name | ||
+ | |||
+ | __file_name = u'' | ||
+ | __modified | ||
+ | |||
+ | |||
+ | class maniadb_core: | ||
+ | ''' | ||
+ | maniadb 태그 검색 에이전트의 코어 기능을 정의한 클래스. | ||
+ | 모든 작업은 코어 클래스를 통해서만 가능하다. | ||
+ | ''' | ||
+ | |||
+ | def list_size(self): | ||
+ | ''' | ||
+ | |||
+ | return len(self.__data_list) | ||
+ | |||
+ | def load(self, args): | ||
+ | ''' | ||
+ | 파일 목록을 불러온다. | ||
+ | |||
+ | Args | ||
+ | args: 파일 목록이 담긴 리스트. 리스트의 원소는 두 가지 타입이 있다. | ||
+ | 하나는 단순 파일 이름, 다름 하나는 파일의 목록을 담은 텍스트 파일이다. | ||
+ | 전자의 경우 단순히 MP3 파일 이름 문자열이다. | ||
+ | 두 번째 또한 파일 이름의 문자열이지만, | ||
+ | 텍스트 파일이다. 이 텍스트 파일 안에 MP3 파일의 경로가 한 줄에 하나씩 | ||
+ | 나열되어 있다. | ||
+ | 만일 인자로 전달할 파일 이름이 후자의 형태인 경우, 파일 이름 앞에 반드시 | ||
+ | ' | ||
+ | |||
+ | Returns | ||
+ | 읽어들인 목록의 개수 | ||
+ | |||
+ | Raises | ||
+ | 일반 예외: 목록으로 전달된 MP3 파일의 경로가 올바르지 않는 경우 발생된다. | ||
+ | | ||
+ | |||
+ | ''' | ||
+ | |||
+ | file_list = [] | ||
+ | |||
+ | # filename checking | ||
+ | for arg in args: | ||
+ | if arg[0] == u' | ||
+ | with open(arg[1: | ||
+ | for line in f.xreadlines(): | ||
+ | path = os.path.normpath(os.path.abspath(line.strip())) | ||
+ | if os.path.exists(path): | ||
+ | file_list.append(path) | ||
+ | else: | ||
+ | raise Exception(path+u' | ||
+ | else: | ||
+ | path = os.path.normpath(os.path.abspath(arg)) | ||
+ | if os.path.exists(path): | ||
+ | file_list.append(path) | ||
+ | else: | ||
+ | raise Exception(path+u' | ||
+ | |||
+ | for f in file_list: | ||
+ | buf = data_elem() | ||
+ | buf.file_name = codeshift.loc2uni(f) | ||
+ | buf.modified | ||
+ | self.__data_list.append(buf) | ||
+ | |||
+ | return len(file_list) | ||
+ | |||
+ | def unload(self, | ||
+ | ''' | ||
+ | 입력한 번호에 해당하는 엔트리를 파일 목록에서 지운다. | ||
+ | |||
+ | Args | ||
+ | nums: 지우려는 목록의 번호. 번호는 항상 1부터 시작한다. | ||
+ | |||
+ | Returns | ||
+ | 지워진 목록의 개수 | ||
+ | |||
+ | Raises | ||
+ | 일반 예외. 번호가 목록 범위를 벗어날 경우 발생한다. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | for num in nums: | ||
+ | if num < 1 or num > self.list_size(): | ||
+ | raise Exception(u' | ||
+ | |||
+ | for off, num in enumerate(nums): | ||
+ | idx = num-off-1 | ||
+ | del self.__data_list[idx] | ||
+ | |||
+ | return len(nums) | ||
+ | |||
+ | def clear_list(self): | ||
+ | ''' | ||
+ | |||
+ | self.__data_list[: | ||
+ | |||
+ | def song_query(self, | ||
+ | ''' | ||
+ | 노래 검색 쿼리를 보내어 결과를 수신한다. | ||
+ | 연결에 실패한 경우 3회까지 재시도한다. | ||
+ | |||
+ | Args | ||
+ | keyword_song: | ||
+ | keyword_artist: | ||
+ | nresp: | ||
+ | |||
+ | Returns | ||
+ | Maniadb 응답. 파싱된 콘텐츠 형태로 리턴된다. | ||
+ | 파싱된 콘텐츠란 서버로부터 받은 XML의 구조를 파이썬 자료형인 | ||
+ | 딕셔너리와 리스트로 표현한 것을 말한다. | ||
+ | |||
+ | Raises | ||
+ | 일반 예외. 3회 연결 실패의 경우 발생한다. | ||
+ | |||
+ | ''' | ||
+ | # throwing query | ||
+ | api = api_search_v3() | ||
+ | |||
+ | # if some problem happens, xmldoc should be None | ||
+ | retry = 3 | ||
+ | xmldoc = None | ||
+ | while True: | ||
+ | if keyword_artist != u'': | ||
+ | xmldoc = api.search(u' | ||
+ | else: | ||
+ | xmldoc = api.search(u' | ||
+ | |||
+ | if xmldoc == None: | ||
+ | if retry > 0: | ||
+ | print u'Song info search failed. Retrying...', | ||
+ | retry -= 1 | ||
+ | time.sleep(3) | ||
+ | continue | ||
+ | else: | ||
+ | raise Exception(' | ||
+ | else: | ||
+ | break | ||
+ | |||
+ | # write xml file to debug. | ||
+ | with open(u' | ||
+ | f.write(xmldoc) | ||
+ | |||
+ | return parse_search_v3(u' | ||
+ | |||
+ | def album_query(self, | ||
+ | ''' | ||
+ | 앨범 상세 정보를 수신받는다. | ||
+ | 연결에 실패한 경우 3회까지 재시도한다. | ||
+ | |||
+ | Args | ||
+ | album_id: Maniadb 내부 고유 앨범 ID | ||
+ | |||
+ | Returns | ||
+ | Maniadb 응답. 파싱된 콘텐츠 형태로 리턴된다. | ||
+ | 파싱된 콘텐츠란 서버로부터 받은 XML의 구조를 파이썬 자료형인 | ||
+ | 딕셔너리와 리스트로 표현한 것을 말한다. | ||
+ | |||
+ | Raises | ||
+ | 일반 예외. 3회 연결 실패의 경우 발생한다. | ||
+ | |||
+ | ''' | ||
+ | |||
+ | api = api_album_v3() | ||
+ | retry = 3 | ||
+ | xmldoc = None | ||
+ | while True: | ||
+ | xmldoc = api.search(album_id) | ||
+ | |||
+ | if xmldoc == None: | ||
+ | if retry > 0: | ||
+ | print u' | ||
+ | retry -= 1 | ||
+ | time.sleep(3) | ||
+ | continue | ||
+ | else: | ||
+ | raise Exception(' | ||
+ | else: | ||
+ | break | ||
+ | |||
+ | # debug | ||
+ | with open(u' | ||
+ | f.write(xmldoc) | ||
+ | |||
+ | return parse_album_v3(xmldoc).result | ||
+ | |||
+ | def get_all_files(self): | ||
+ | ''' | ||
+ | |||
+ | all_files = [] | ||
+ | for item in self.__data_list: | ||
+ | all_files.append(item.file_name) | ||
+ | return all_files | ||
+ | |||
+ | def get_tagged_files(self): | ||
+ | ''' | ||
+ | |||
+ | tagged = [] | ||
+ | for item in self.__data_list: | ||
+ | if item.modified == True: | ||
+ | tagged.append(item.file_name) | ||
+ | return tagged | ||
+ | |||
+ | def get_untagged_files(self): | ||
+ | ''' | ||
+ | |||
+ | untagged = [] | ||
+ | for item in self.__data_list: | ||
+ | if item.modified == False: | ||
+ | untagged.append(item.file_name) | ||
+ | return untagged | ||
+ | |||
+ | def get_file_name(self, | ||
+ | ''' | ||
+ | |||
+ | if 1 <= num and num <= self.list_size(): | ||
+ | return self.__data_list[num-1].file_name | ||
+ | else: | ||
+ | raise Exception(u' | ||
+ | |||
+ | def is_tagged(self, | ||
+ | ''' | ||
+ | |||
+ | if 1 <= num and num <= self.list_size(): | ||
+ | return self.__data_list[num-1].modified | ||
+ | else: | ||
+ | raise Exception(u' | ||
+ | |||
+ | def set_tagged(self, | ||
+ | ''' | ||
+ | |||
+ | if 1 <= num and num <= self.list_size(): | ||
+ | self.__data_list[num-1].modified = modified | ||
+ | else: | ||
+ | raise Exception(u' | ||
+ | |||
+ | def update(self, | ||
+ | ''' | ||
+ | MP3 파일 정보를 업데이트한다. | ||
+ | |||
+ | Args | ||
+ | listnum: | ||
+ | song_info: | ||
+ | song_selnum: | ||
+ | album_info: | ||
+ | |||
+ | Returns | ||
+ | None | ||
+ | |||
+ | Raises | ||
+ | None | ||
+ | |||
+ | ''' | ||
+ | |||
+ | target_file_name = self.get_file_name(listnum) | ||
+ | id3tag_manip.update_mp3_tag(target_file_name, | ||
+ | self.set_tagged(listnum, | ||
+ | |||
+ | ''' | ||
+ | __data_list = [] | ||
+ | </ | ||
+ | |||
+ | ==== 기타 모듈 ==== | ||
+ | 명령 프롬프트에 긴 경로의 파일을 줄여 주기, 에러 메시지 처리 등 잡다한 역할을 하는 모듈을 따로 만들었습니다. | ||
+ | |||
+ | <code python util.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | import traceback, os.path, sys | ||
+ | |||
+ | # fit a long text string to length | ||
+ | def fit_string(input, | ||
+ | input_len = len(input) | ||
+ | |||
+ | if input_len <= length: | ||
+ | return input | ||
+ | |||
+ | omit_len = len(omit_str) | ||
+ | lpos = (length-omit_len)/ | ||
+ | |||
+ | if (length-omit_len)%2 == 1: | ||
+ | lpos += 1 | ||
+ | |||
+ | rpos = input_len-lpos+1 | ||
+ | |||
+ | return input[: | ||
+ | |||
+ | # 빡세게 에러 출력 | ||
+ | MANIADB_DETAILED_TRACE = False | ||
+ | |||
+ | def trace_error(errmsg, | ||
+ | if MANIADB_DETAILED_TRACE == True: | ||
+ | exc_type, exc_obj, exc_tb = sys.exc_info() | ||
+ | file_name = os.path.basename(exc_tb.tb_frame.f_code.co_filename) | ||
+ | |||
+ | print >> sys.stderr, u'%s %s' % (errmsg, err) | ||
+ | print >> sys.stderr, u'' | ||
+ | print >> sys.stderr, u' | ||
+ | print >> sys.stderr, u'' | ||
+ | print >> sys.stderr, u'Call Stack:' | ||
+ | for item in traceback.extract_stack(): | ||
+ | print >> sys.stderr, u'> File: %s, line: %d, function: %s, code: %s' % (item[0], item[1], item[2], item[3]) | ||
+ | |||
+ | else: | ||
+ | print >> sys.stderr, u'%s %s' % (errmsg, err) | ||
+ | |||
+ | |||
+ | if __name__ == ' | ||
+ | print fit_string(u' | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==== 바이너리 (exe) 파일로 만들기 ==== | ||
+ | 처음에는 장난감처럼 만들어 보려고 했는데, 만들다 보니 장난감이 꽤 묵직한 도구 처럼 커져 버렸습니다. 초라한 콘솔 툴이지만 OpenAPI 검색이나 태그 수정등이 나름 상당히 편리합니다. 작은 콘솔 프로그램으로 쓸만하게 된 것 같습니다. | ||
+ | |||
+ | 그래서 파이썬 코드를 실행 가능한 바이너리로 한 번 만들어 보려고 합니다. 이렇게 하려면 우선은 py2exe라는 패키지가 필요합니다. [[http:// | ||
+ | |||
+ | 파이썬 코드를 실행 가능한 파일로 만들기 위해 다음 소스를 추가적으로 작성합니다. | ||
+ | |||
+ | <code python to_binary.py> | ||
+ | # -*- coding: cp949 -*- | ||
+ | from distutils.core import setup | ||
+ | import py2exe | ||
+ | |||
+ | setup(console=[' | ||
+ | </ | ||
+ | |||
+ | 명령 프롬프트에서 다음과 같이 실행하면 바이너리 파일이 생성됩니다. | ||
+ | python to_binary.py py2exe | ||
+ | |||
+ | 별 문제가 없다면 ' | ||
+ | |||
+ | |||
+ | ===== 마치며 ===== | ||
+ | Maniadb OpenAPI를 이용하여 간단한 MP3 파일 태그 검색, 수정 프로그램을 만들어 보았습니다. OpenAPI 가 전달하는 XML 문서를 파싱하여 원하는 정보를 추출하는 것이 프로그램의 거의 전부입니다만, | ||
+ | |||
+ | 아주 간단한 CLI 프로그램이지만 어떤가요? | ||
+ | |||
+ | 사실 인터페이스 부분은 조금 과한 면도 있었습니다. 본 문서에서는 쿼리 전달, XML 파싱, ID3태그 수정만 언급하고 더 깊게 들어가지 않아도 되었다고 생각합니다. 그러나 과정이 있었기에 결과적으로 (여전히 장난감이긴 하지만) 그나마 이 정도로 이용해 볼만한 태그 검색 프로그램이 완성된 듯한 느낌이 듭니다. | ||
+ | |||
+ | 그리고 py2exe를 이용해 바이너리 파일도 만들어 보았습니다. 이렇게 하면 파이썬을 설치하지 않은 pc에도 우리가 작성한 프로그램을 실행할 수 있습니다. | ||
+ | |||
+ | 저는 이 프로그램을 이용하여 약 250개의 가요 MP3파일에 대해 태그 검색을 적접 실험해 보았습니다. 아주 애매한 경우를 제외하고는 검색도 상당히 양호하였고 태그 또한 괜찮게 수정된 것을 보았습니다. | ||
+ | |||
+ | 하지만 Maniadb 서버에서도 가끔 잘못된 정보가 전달되는 경우를 몇 번 경험하였습니다. 그리고 프로그램 자체가 아주 완성도가 높은 것은 아닙니다. 이 나머지는 이 문서를 읽는 분들께 맡깁니다. | ||