toggle menu

멜로플레이어 개발일지

2020. 5. 14. 08:14 기타

필자는 음악 듣는걸 참 좋아한다. 코딩하면서 음악 듣는 거야 말로 진정한 행복이 아닐 수 없다.

 

특히 멜론 차트 상위권에 있는 곡들을 참 좋아하는데, 필자는 멜론 계정이 없어서 항상 멜론 차트에 있는 노래를 유튜브에 검색해서 들어야 한다. 상당히 귀찮다.

 

그리고 어느 날 머릿속에 문득 생각이 났다. 이러한 작업을 자동화하기로.

 

멜론 차트 상위권에 있는 노래 제목을 크롤링해서 그 제목을 바탕으로 유튜브에서 노래를 다운로드하기로 했다. 내가 생각해도 꽤 기발한 아이디어였다.

 

이번 포스팅에서는 필자가 처음 도전하는 토이 프로젝트에 대한 설명과 어떻게 진행했는지 남겨두려 한다. 나중에 이 글을 읽고 내가 얼마큼 발전했는지 알 수 있도록 말이다. (언어는 파이썬을 선택하기로 했다)


작업 순서

프로젝트를 시작하기 전에 먼저 어떤 순서로 개발을 해야 할지 순서도를 짜두자.

1. 멜론 차트 제목을 크롤링 한 뒤, 엑셀 파일에 저장

2. 엑셀 파일을 토대로 유튜브에서 동영상을 mp4로 다운

3. mp4 동영상을 mp3로 변환

4. mp4 파일을 모두 제거

5. 플레이어에서 다운로드한 mp3을 실행 가능하게 제작

5번은 꼭 필요한지 아닌지 모르겠다. 이미 윈도우 내장 플레이어가 있는데 해야 할까?

 

일단 PyQt를 연습해볼 겸 5번은 진행하기로 했다.


멜론 차트 크롤링

먼저 requests와 beautifulsoup를 사용해 멜론 차트를 크롤링한다. 1~50위 노래의 제목을 차례대로 뽑아올 것이다.

 

import requests

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'}
req = requests.get('https://www.melon.com/chart/', headers=header)
html = req.text

print(html)

 

헤더를 안 쓰니 406 에러가 나더라... 그래서 붙여줬다.

 

html이 정상적으로 출력된다

위 사진과 같이 에러 없이 html을 파싱 해오는 걸 볼 수 있다.

 

이제, 전체 html이 아닌 제목 부분을 콕콕 집어올 차례다. 멜론 차트 페이지에 html을 유심히 살펴보자.

 

켁...뭐가 이렇게 많냐

td>.wrap>.wrap_song_info>.ellipsis>span>a로 접근할 수 있을 것 같다.

 

이제 beautifulsoup를 통해 제목만 깔끔하게 뽑아보자.

 

import requests
from bs4 import BeautifulSoup

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'}
req = requests.get('https://www.melon.com/chart/', headers=header)
html = req.text
parse = BeautifulSoup(html, 'html.parser')

titles = parse.find_all('div', {'class': 'ellipsis rank01'})
singers = parse.find_all('div', {'class': 'ellipsis rank02'})

title = []
singer = []

for t in titles:
    title.append(t.find('a').text)

for s in singers:
    singer.append(s.find('span', {'class': 'checkEllipsis'}).text)

for i in range(50):
    print('%s - %s' % (title[i], singer[i]))

 

이렇게 된 코드를 실행시키면 결과는 아래와 같다.

 

곡 제목과 아티스트 명을 출력한다

 

1위부터 50위까지의 곡 이름과 아티스트 명을 출력하는 걸 볼 수 있다.

 

이제 크롤링한 제목과 아티스트명을 엑셀 파일에 저장하는 작업이다. 이 작업을 위해서 openpyxl이라는 오픈소스 라이브러리를 사용한다.

 

import requests
from bs4 import BeautifulSoup
from openpyxl import Workbook

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'}
req = requests.get('https://www.melon.com/chart/', headers=header)
html = req.text
parse = BeautifulSoup(html, 'html.parser')

titles = parse.find_all('div', {'class': 'ellipsis rank01'})
singers = parse.find_all('div', {'class': 'ellipsis rank02'})

title = []
singer = []

write_wb = Workbook()
write_ws = write_wb.active

for t in titles:
    title.append(t.find('a').text)

for s in singers:
    singer.append(s.find('span', {'class': 'checkEllipsis'}).text)

for i in range(50):
    write_ws.append([title[i], singer[i]])

write_wb.save("song.xlsx")

 

append 함수를 이용해서 첫 번째 행에는 노래 제목을, 그리고 두 번째 행에는 가수명을 넣어줬다.

 

엑셀을 열어서 데이터가 제대로 저장됐는지 확인해본다.

 

엑셀에 저장된 멜론차트

정상적으로 song.xlsx 파일에 저장된 것을 확인할 수 있다. 생각보다 코드가 길지 않아서 뭔가 시시한 느낌이다.


유튜브 동영상 다운

이제 멜론 차트를 엑셀 데이터 시트로 저장했으니, 그 파일을 토대로 유튜브에서 곡을 다운로드할 차례다.

 

새로운 파이썬 파일을 하나 만들고 그 안에 작성한다.

 

from openpyxl import load_workbook

load_wb = load_workbook('song.xlsx', data_only=True)
load_ws = load_wb['Sheet']

song_title = []
song_artist = []

for row in load_ws['A1':'B50']:
    row_value = []

    for cell in row:
        row_value.append(cell.value)

    song_title.append(row_value[0])
    song_artist.append(row_value[1])

for i in range(50):
    print('%s - %s' % (song_title[i], song_artist[i]))

 

Sheet에 있는 내용들 중 A1부터 B50까지의 데이터를 뽑아오는 작업이다.

 

제목 - 아티스트 형식으로 출력된다

근데 코드를 짜다가 갑자기 생각났는데... 굳이 엑셀 파일로 저장해야 할까?라는 생각이 머리를 스쳤다. 그냥 아까 크롤링된 데이터로 바로 다운로드할 수 있지 않나?

 

일단 중복된 노래가 있는지 나중에 검사하기 위해 남겨두기로 했다. 뭔가 잘못된 기분이 드는걸...

 

별거 아니겠지?

이제 엑셀 파일을 읽어오는 데 성공했으니 이걸 토대로 유튜브에서 영상을 다운로드할 차례다.

 

유튜브에서 제목 - 아티스트로 검색한 후 검색 결과 중 가장 첫 번째 있는 동영상을 다운로드하기로 했다.

 

search_query를 이용해서 검색할수 있을것 같다

search_query 뒤에 검색한 값이 뜨는 걸 확인할 수 있다. 제목과 아티스트명을 통해 검색이 잘 되는지 확인해보자.

 

for i in range(50):
    params = {'search_query': '%s - %s' % (song_title[i], song_artist[i])}
    response = requests.get(URL, params=params)
    html = response.text
    soup = BeautifulSoup(html, 'html.parser')
    watch_url = soup.find_all(class_='yt-uix-sessionlink spf-link')[0]['href']
    print(watch_url)

 

이런 식으로 url을 얻어온다. 이제 잘 되는지 확인해보자.

 

url이 잘 찍힌다

근데 정말.. 더럽게 느리다. 필자는 이걸 데스크톱 애플리케이션으로 만들려고 했는데 속도가 이 정도면 아무도 안 쓸 것 같다. 심지어 필자도.

 

병렬 처리를 해서 크롤링 부분을 나중에 수정해야겠다. (멀티 프로세싱)

 

일단 지금은 최우선 과제가 유튜브 동영상을 다운로드하는 것이므로... 작업을 마저 끝내보자.

 

pytube라는 라이브러리를 사용해서 유튜브 동영상을 다운로드한다.

 

for i in range(50):
    params = {'search_query': '%s - %s' % (song_title[i], song_artist[i])}
    response = requests.get(URL, params=params)
    html = response.text
    soup = BeautifulSoup(html, 'html.parser')
    watch_url = soup.find_all(class_='yt-uix-sessionlink spf-link')[0]['href']

    youtube = YouTube('https://www.youtube.com' + watch_url)
    videos = youtube.streams.first()
    videos.download(os.getcwd() + "/songs")

 

현재 디렉터리 안에 songs라는 폴더를 만들고, 그 안에 mp4를 저장한다.

 

겁나 느리다

저장되긴 하는데 너무 느린 게 흠이다. 50곡 언제 다운로드하려는지...


.mp3로 변환하기 및 .mp4 제거

영상을 다운로드하는 걸 성공했으니, 이제 이 영상들을 mp3로 변환시킬 차례다.

 

moviepy라는 라이브러리를 이용한다.

 

import os

from moviepy.editor import VideoFileClip

file_list_with_extension = os.listdir("songs")

for file in file_list_with_extension:
    file_list = os.path.splitext(file)[0]
    file_mp3 = file_list + ".mp3"
    mp4 = VideoFileClip(os.path.join("songs", file))
    mp4.audio.write_audiofile(os.path.join("songs", file_mp3))
    mp4.close()

for file in file_list_with_extension:
    file_extension = os.path.splitext(file)[-1]

    if os.path.isfile("songs" + "/" + file) and file_extension == ".mp4":
        os.remove("songs" + "/" + file)

 

songs 폴더에 있는 mp4 파일을 변환시키고, 남아있던 mp4 파일을 제거한다. (역시나 시간이 오래 걸린다..)


음악 플레이어 만들기

이제 대부분의 작업은 얼추 끝났다. 남은 건 mp3를 플레이할 수 있는 음악 플레이어를 만들면 된다. 사실 이 부분이 제일 골치 아플 것 같긴 하다.

 

tkinter를 사용해서 데스크톱 애플리케이션을 만들어보기로 했다.

 

새로운 파이썬 파일을 하나 만들고 아래와 같이 적어준다.

 

from tkinter import *

top = Tk()
top.title("멜로플레이어")
top.geometry("384x360")

wall = PhotoImage(file="img/md_w.gif")
w_lb = Label(image=wall)
w_lb.place(x=0, y=0)

img_p = PhotoImage(file="img/md_p.gif")
b_p = Button(top, height=30, width=30, image=img_p)
b_p.place(x=155, y=166)

img_s = PhotoImage(file="img/md_s.gif")
b_s = Button(top, height=30, width=30, image=img_s)
b_s.place(x=195, y=166)

img_b = PhotoImage(file="img/md_b.gif")
b_b = Button(top, height=30, width=40, image=img_b)
b_b.place(x=50, y=166)

img_n = PhotoImage(file="img/md_n.gif")
b_n = Button(top, height=30, width=40, image=img_n)
b_n.place(x=290, y=166)

img_u = PhotoImage(file="img/md_u.gif")
b_u = Button(top, height=30, width=30, image=img_u)
b_u.place(x=176, y=48)

img_d = PhotoImage(file="img/md_d.gif")
b_d = Button(top, height=30, width=30, image=img_d)
b_d.place(x=176, y=288)

img_l = PhotoImage(file="img/md_l.gif")
b_l = Button(top, height=47, width=30, image=img_l)
b_l.place(x=326, y=24)

top.mainloop()

 

멜로 플레이어라는 제목이 달린 작은 창을 하나 띄우는 걸 볼 수 있다.

 

어렸을때 들고 다니던 mp3 플레이어 같다

 

이제 mp3 파일을 재생하는 법을 찾아봤는데, 생각보다 쉬운 방법을 하나 찾았다. 또 다른 라이브러리를 쓰는 건데, pygame을 사용하는 것이다.

 

원래 pygame은 게임 만들 때 사용하는 거긴 한데, 음악 재생 부분을 상당히 쉽게 적용할 수 있어서 의외였다.

 

from tkinter.filedialog import *

from pygame import *

top = Tk()
top.title("멜로플레이어")
top.geometry("384x360")

ls_music = []
index = 0
lb_string = StringVar()


def music_list():
    global index
    dir = os.getcwd() + "/songs"
    os.chdir(dir)

    for files in os.listdir(dir):
        ls_music.append(files)

    mixer.init()
    mixer.music.load(ls_music[index])
    mixer.music.play()

    def list_select(event):
        lb_string.set("")
        index = int(lb.curselection()[0])
        mixer.music.load(ls_music[index])
        mixer.music.play()
        lb_string.set(ls_music[index])

    def list_insert():
        i = 0

        for song in ls_music:
            lb.insert(i, song)
            i += 1

    win = Toplevel(top)
    win.title("목록")
    sb = Scrollbar(win)
    sb.pack(side=RIGHT, fill=Y)
    lb = Listbox(win, width=50, yscrollcommand=sb.set)
    lb.pack(side=LEFT)
    sb.config(command=lb.yview)
    song_lb = Label(textvariable=lb_string)
    song_lb.pack()
    list_insert()

    lb.bind("<<ListboxSelect>>", list_select)


wall = PhotoImage(file="img/md_w.gif")
w_lb = Label(image=wall)
w_lb.place(x=0, y=0)

img_p = PhotoImage(file="img/md_p.gif")
b_p = Button(top, height=30, width=30, image=img_p)
b_p.place(x=155, y=166)

img_s = PhotoImage(file="img/md_s.gif")
b_s = Button(top, height=30, width=30, image=img_s)
b_s.place(x=195, y=166)

img_b = PhotoImage(file="img/md_b.gif")
b_b = Button(top, height=30, width=40, image=img_b)
b_b.place(x=50, y=166)

img_n = PhotoImage(file="img/md_n.gif")
b_n = Button(top, height=30, width=40, image=img_n)
b_n.place(x=290, y=166)

img_u = PhotoImage(file="img/md_u.gif")
b_u = Button(top, height=30, width=30, image=img_u)
b_u.place(x=176, y=48)

img_d = PhotoImage(file="img/md_d.gif")
b_d = Button(top, height=30, width=30, image=img_d)
b_d.place(x=176, y=288)

img_l = PhotoImage(file="img/md_l.gif")
b_l = Button(top, height=47, width=30, image=img_l, command=music_list)
b_l.place(x=326, y=24)

top.mainloop()

 

mp3 버튼을 클릭하면 자동으로 songs 폴더에 있는 노래들을 불러오면서 재생하는 걸 확인할 수 있다. 꽤 멋있다.

 

이제 버튼 이벤트를 핸들링할 차례다. 재생 버튼, 정지 버튼, 스킵 버튼 등등.

 

from tkinter.filedialog import *

from pygame import *

top = Tk()
top.title('멜로플레이어')
top.geometry('384x360')

ls_music = []
index = 0
lb_string = StringVar()
v_size = 0.30


def first_start():
    global index
    dir = os.getcwd() + '\\songs'

    if re.search(r'songs\b', os.getcwd()):
        dir = os.getcwd()
    else:
        os.chdir(dir)

        for files in os.listdir(dir):
            ls_music.append(files)

    mixer.init()
    mixer.music.load(ls_music[index])
    mixer.music.play()
    lb_string.set(ls_music[index])
    song_lb = Label(textvariable=lb_string)
    song_lb.pack()


def music_list():
    global index

    def list_select(event):
        lb_string.set('')
        index = int(lb.curselection()[0])
        mixer.music.load(ls_music[index])
        mixer.music.play()
        lb_string.set(ls_music[index])

    def list_insert():
        i = 0

        for song in ls_music:
            lb.insert(i, song)
            i += 1

    win = Toplevel(top)
    win.title('목록')
    sb = Scrollbar(win)
    sb.pack(side=RIGHT, fill=Y)
    lb = Listbox(win, width=50, yscrollcommand=sb.set)
    lb.pack(side=LEFT)
    sb.config(command=lb.yview)
    list_insert()

    lb.bind('<<ListboxSelect>>', list_select)


def play_song(event):
    mixer.music.play(-1)
    lb_update()


def stop_song(event):
    mixer.music.stop()


def lb_update():
    global index
    lb_string.set(ls_music[index])


def previous_song(event):
    global index
    if index == 0:
        return
    index -= 1
    mixer.music.load(ls_music[index])
    mixer.music.play(-1)
    lb_update()


def next_song(event):
    global index
    if index == len(ls_music[index]) - 1:
        return
    index += 1
    mixer.music.load(ls_music[index])
    mixer.music.play(-1)
    lb_update()


def volume_down(event):
    global v_size
    mixer.music.set_volume(v_size)
    v_size -= 0.10
    print(v_size)


def volume_up(event):
    global v_size
    mixer.music.set_volume(v_size)
    v_size += 0.10
    print(v_size)


wall = PhotoImage(file='img/md_w.gif')
w_lb = Label(image=wall)
w_lb.place(x=0, y=0)

img_p = PhotoImage(file='img/md_p.gif')
b_p = Button(top, height=30, width=30, image=img_p)
b_p.place(x=155, y=166)

img_s = PhotoImage(file='img/md_s.gif')
b_s = Button(top, height=30, width=30, image=img_s)
b_s.place(x=195, y=166)

img_b = PhotoImage(file='img/md_b.gif')
b_b = Button(top, height=30, width=40, image=img_b)
b_b.place(x=50, y=166)

img_n = PhotoImage(file='img/md_n.gif')
b_n = Button(top, height=30, width=40, image=img_n)
b_n.place(x=290, y=166)

img_u = PhotoImage(file='img/md_u.gif')
b_u = Button(top, height=30, width=30, image=img_u)
b_u.place(x=176, y=48)

img_d = PhotoImage(file='img/md_d.gif')
b_d = Button(top, height=30, width=30, image=img_d)
b_d.place(x=176, y=288)

img_l = PhotoImage(file='img/md_l.gif')
b_l = Button(top, height=47, width=30, image=img_l, command=music_list)
b_l.place(x=326, y=24)

b_p.bind('<Button-1>', play_song)
b_s.bind('<Button-1>', stop_song)
b_b.bind('<Button-1>', previous_song)
b_n.bind('<Button-1>', next_song)
b_u.bind('<Button-1>', volume_up)
b_d.bind('<Button-1>', volume_down)

first_start()
music_list()
top.mainloop()

 

버튼들에 차례대로 이벤트를 바인딩해줬다.

 

노래가 잘 나오고 있다

이제 작업이 얼마 남지 않았다. 마지막으로 여태까지 했던 작업들을 모두 차례대로 엮어서 이어주면 된다.

 

크롤링>다운로드>플레이어 오픈 순으로 작업하면 될 것 같다.

 

main.py를 만들고 다음과 같이 적어준다.

 

from convert import convert
from crawl import crawl
from download import download
from player import player

crawl()
download()
convert()
player()

 

뭔가 더 좋은 방법이 있을 것 같은데, 내 머리로는 더 생각이 안 난다. 짱구를 더 굴려봐야겠다.


느낀 점

파이썬으로 한 첫 토이 프로젝트였는데, 생각보다 꽤 잘된 것 같다. 그런데 짜깁기만 하는 게 토이 프로젝트라고 볼 수 있을지 의문이 들고, 역시 고쳐야 할 부분이 너무 많았다. 그런데 처음부터 잘하는 사람이 어딨겠는가. 다 이런 시행착오를 거쳐서 한 계단씩 성장하는 거지.

 

완성된 프로젝트 링크


참고

https://kwonsye.github.io/study%20note/2019/03/24/download-youtube.html

blog.naver.com/scyan2011/221731112255blog.naver.com/scyan2011/221731112255

댓글