필자는 음악 듣는걸 참 좋아한다. 코딩하면서 음악 듣는 거야 말로 진정한 행복이 아닐 수 없다.
특히 멜론 차트 상위권에 있는 곡들을 참 좋아하는데, 필자는 멜론 계정이 없어서 항상 멜론 차트에 있는 노래를 유튜브에 검색해서 들어야 한다. 상당히 귀찮다.
그리고 어느 날 머릿속에 문득 생각이 났다. 이러한 작업을 자동화하기로.
멜론 차트 상위권에 있는 노래 제목을 크롤링해서 그 제목을 바탕으로 유튜브에서 노래를 다운로드하기로 했다. 내가 생각해도 꽤 기발한 아이디어였다.
이번 포스팅에서는 필자가 처음 도전하는 토이 프로젝트에 대한 설명과 어떻게 진행했는지 남겨두려 한다. 나중에 이 글을 읽고 내가 얼마큼 발전했는지 알 수 있도록 말이다. (언어는 파이썬을 선택하기로 했다)
작업 순서
프로젝트를 시작하기 전에 먼저 어떤 순서로 개발을 해야 할지 순서도를 짜두자.
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을 유심히 살펴보자.
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 뒤에 검색한 값이 뜨는 걸 확인할 수 있다. 제목과 아티스트명을 통해 검색이 잘 되는지 확인해보자.
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을 얻어온다. 이제 잘 되는지 확인해보자.
근데 정말.. 더럽게 느리다. 필자는 이걸 데스크톱 애플리케이션으로 만들려고 했는데 속도가 이 정도면 아무도 안 쓸 것 같다. 심지어 필자도.
병렬 처리를 해서 크롤링 부분을 나중에 수정해야겠다. (멀티 프로세싱)
일단 지금은 최우선 과제가 유튜브 동영상을 다운로드하는 것이므로... 작업을 마저 끝내보자.
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 파일을 재생하는 법을 찾아봤는데, 생각보다 쉬운 방법을 하나 찾았다. 또 다른 라이브러리를 쓰는 건데, 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
댓글