Spotalike๋ฅผ ํ™œ์šฉํ•œ ์Œ์•… ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ํ”„๋กœ์ ํŠธ


๊ฐœ์š”

๋‚˜๋Š” ํ‰์†Œ์— YouTube๋กœ ์Œ์•…์„ ์ฆ๊ฒจ ๋“ฃ๋Š”๋‹ค. ํ•œ ๊ณก์— ๊ฝ‚ํžˆ๋ฉด ๋ฐ˜๋ณต ์žฌ์ƒ์„ ํ•˜๋Š” ํŽธ์ธ๋ฐ, ์ด๋ฒˆ์—๋Š” Christopher, CHUNG HA - When I Get Old์— ๋น ์กŒ๋‹ค. ์ด ๋…ธ๋ž˜๋ฅผ ๋“ฃ๋‹ค ๋ณด๋‹ˆ, ๋น„์Šทํ•œ ๋ถ„์œ„๊ธฐ์˜ ๊ณก๋“ค์„ ๋ชจ์•„ ์ž๋™์œผ๋กœ ์ถ”์ฒœ๋ฐ›๊ณ  ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๊ทธ๋ ‡๊ฒŒ ๋ฐœ๊ฒฌํ•œ ๊ฒƒ์ด Spotalike๋ผ๋Š” ์„œ๋น„์Šค๋‹ค. ์ฒ˜์Œ์—๋Š” ์ด ํ”Œ๋žซํผ์„ ํ™œ์šฉํ•ด ์ถ”์ฒœ๋ฐ›์€ ๊ณก๋“ค์„ YouTube ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๊ธฐํšํ–ˆ๋‹ค.


ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด์˜ ํ™•์žฅ

์ฒ˜์Œ์—๋Š” ๊ฐ„๋‹จํ•œ ๋ฐฉ์‹์œผ๋กœ ์‹œ์ž‘ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค. Spotalike์—์„œ ์ถ”์ฒœ๋ฐ›์€ ๊ณก๋“ค์„ YouTube ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ ๋ฐ”๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ์ง€๋งŒ, ์ด๋Š” YouTube Music์—์„œ ์ด๋ฏธ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ํฌ๊ฒŒ ๋‹ค๋ฅผ ๋ฐ” ์—†์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ฐจ๋ณ„ํ™”๋ฅผ ์œ„ํ•ด Obsidian์˜ Graph View์ฒ˜๋Ÿผ ๋น„์Šทํ•œ ๊ณก๋“ค์„ ์‹œ๊ฐ์ ์œผ๋กœ ์—ฐ๊ฒฐํ•ด ๋ณด์—ฌ์ฃผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋ฐฉ์‹์œผ๋กœ ์žฌ์ƒ๋ชฉ๋ก์„ ๊ตฌ์„ฑํ•˜๊ณ  ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฐœ๋ฐœ ์—ญ๋Ÿ‰์ด ๋ถ€์กฑํ•œ ์ƒํƒœ์ง€๋งŒ, ์ฒœ์ฒœํžˆ ์‹œ๋„ํ•˜๋ฉฐ ๊ฒฐ๊ณผ๋ฌผ์„ ๋งŒ๋“ค์–ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.


๊ฐœ๋ฐœ ๊ณผ์ •

1. Spotalike ํฌ๋กค๋ง ๊ธฐ๋Šฅ ๊ตฌํ˜„

Spotalike์—์„œ ํŠน์ • ๋…ธ๋ž˜๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ฉด ์ถ”์ฒœ ๊ณก ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๋จผ์ € ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด Selenium์„ ์‚ฌ์šฉํ•ด ๋‹ค์Œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ๋‹ค.

  • ๊ฒ€์ƒ‰์ฐฝ์— ๋…ธ๋ž˜ ์ œ๋ชฉ ์ž…๋ ฅ
  • ๋“œ๋กญ๋‹ค์šด ๋ฆฌ์ŠคํŠธ์—์„œ ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ ํด๋ฆญ
  • ์ถ”์ฒœ ๊ณก ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”์ถœ

์ด ๊ณผ์ •์—์„œ ์–ด๋ ค์›€๋„ ์žˆ์—ˆ๋‹ค. ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด๊ฐ€ blur ์ฒ˜๋ฆฌ๋˜์–ด ์š”์†Œ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ค์› ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด focusout ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์‚ญ์ œํ•˜์—ฌ ๋“œ๋กญ๋‹ค์šด ๋ฆฌ์ŠคํŠธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


2. FastAPI๋กœ API ๊ฐœ๋ฐœ

๋‹ค์Œ์œผ๋กœ, ํฌ๋กค๋ง ๋ฐ์ดํ„ฐ๋ฅผ FastAPI ๊ธฐ๋ฐ˜ API๋กœ ์ œ๊ณตํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. ์•„๋ž˜๋Š” API์˜ ์ฃผ์š” ๊ธฐ๋Šฅ์ด๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋…ธ๋ž˜ ์ œ๋ชฉ๊ณผ ๊ฐ€์ˆ˜ ์ด๋ฆ„์„ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค.
  • Spotalike์—์„œ ์ถ”์ฒœ ๊ณก ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  • ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
project/
โ”œโ”€โ”€ main.py                    # FastAPI ์„œ๋ฒ„์˜ ์ง„์ž…์ 
โ”œโ”€โ”€ spotalike/                 # Spotalike ๊ด€๋ จ ๋กœ์ง
โ”‚   โ”œโ”€โ”€ scraper.py             # Selenium ์Šคํฌ๋ž˜ํผ ๊ตฌํ˜„
โ”œโ”€โ”€ utils/                     # ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ชจ๋“ˆ
โ”‚   โ”œโ”€โ”€ logger.py              # ๋กœ๊น… ๊ธฐ๋Šฅ ๊ตฌํ˜„
โ”œโ”€โ”€ requirements.txt           # Python ํŒจํ‚ค์ง€ ์˜์กด์„ฑ ๋ชฉ๋ก
โ”œโ”€โ”€ .env                       # ํ™˜๊ฒฝ ๋ณ€์ˆ˜
โ””โ”€โ”€ README.md                  # ํ”„๋กœ์ ํŠธ ์„ค๋ช…
from fastapi import FastAPI, HTTPException
from spotalike.scraper import get_spotalike_tracks
 
app = FastAPI()
 
@app.get("/api/spotalike")
def fetch_spotalike_tracks(song_title: str, artist_name: str):
    """
    Spotalike์—์„œ ์ถ”์ฒœ ๊ณก์„ ๊ฐ€์ ธ์˜ค๋Š” API.
    """
    try:
        tracks = get_spotalike_tracks(song_title, artist_name)
        if not tracks:
            raise HTTPException(status_code=404, detail="No tracks found")
        return {"tracks": tracks}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

3. Selenium์„ ํ™œ์šฉํ•œ ํฌ๋กค๋ง ์ฝ”๋“œ

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager # type: ignore
import logging
import json
from dotenv import load_dotenv
import os
 
load_dotenv()
BASE_URL = os.getenv("SPOTALIKE_BASE_URL", "https://spotalike.com/en")
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
def get_spotalike_tracks(song_title: str, artist_name: str):
    options = Options()
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--headless")  # Uncomment for performance
 
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
    tracks = []
 
    try:
        logging.info("Opening Spotalike...")
        driver.get(BASE_URL)
        wait = WebDriverWait(driver, 10)
 
        logging.info(f"Searching for: {song_title}")
        search_field = wait.until(EC.presence_of_element_located((By.ID, "search")))
        search_field.send_keys(song_title)
 
        dropdown_locator = (By.CSS_SELECTOR, "ul[class^='Dropdown_dropdown']")
        wait.until(EC.visibility_of_element_located(dropdown_locator))
        first_item = driver.find_element(By.CSS_SELECTOR, "ul[class^='Dropdown_dropdown'] li button")
        first_item.click()
 
        table_body = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "tbody")))
        tracks = extract_tracks(table_body)
 
    except Exception as e:
        logging.error(f"An error occurred: {e}", exc_info=True)
    finally:
        logging.info("Closing the driver.")
        driver.quit()
 
    return tracks
 
def extract_tracks(table_body):
    tracks = []
    track_rows = table_body.find_elements(By.TAG_NAME, "tr")
    for row in track_rows:
        cells = row.find_elements(By.TAG_NAME, "td")
        if len(cells) >= 3:
            title_cell, artist_cell, duration_cell = cells[:3]
            tracks.append({
                "title": title_cell.text.strip(),
                "artist": artist_cell.text.strip(),
                "duration": duration_cell.text.strip()
            })
    return tracks

๊ฒฐ๋ก  ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

ํ˜„์žฌ Spotalike ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ๊ณผ FastAPI ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๊นŒ์ง€ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ดํ›„ ๋ชฉํ‘œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. Graph View ๊ตฌํ˜„

    • ๊ณก๋“ค ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œํ˜„.
    • ์‚ฌ์šฉ์ž๊ฐ€ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์žฌ์ƒ๋ชฉ๋ก์„ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก UI ๊ฐœ๋ฐœ.
  2. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ

    • Graph View์™€ API๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ.
    • React.js ๋˜๋Š” Vue.js๋ฅผ ํ™œ์šฉํ•œ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์„ฑ.
  3. ์œ ์ € ์นœํ™”์ ์ธ ์žฌ์ƒ๋ชฉ๋ก ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

    • ์ €์žฅ๋œ ์žฌ์ƒ๋ชฉ๋ก์„ ํŒŒ์ผ๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ.
    • YouTube์™€ ์—ฐ๋™ํ•˜์—ฌ ์žฌ์ƒ๋ชฉ๋ก ์ž๋™ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์ œ๊ณต.

์ถ”๊ฐ€์ ์œผ๋กœ ๊ณ ๋ฏผํ•ด๋ณด๋ฉด ์ข‹์„ ์งˆ๋ฌธ๋“ค

Q1: Selenium์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  Spotalike ๋ฐ์ดํ„ฐ๋ฅผ ๋” ํšจ์œจ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ?

Q2: Graph View๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ์ ํ•ฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ํ”„๋ ˆ์ž„์›Œํฌ์—๋Š” ์–ด๋–ค ๊ฒƒ๋“ค์ด ์žˆ์„๊นŒ?

Q3: ์ถ”์ฒœ๋ฐ›์€ ๊ณก๋“ค ๊ฐ„์˜ ์—ฐ๊ด€์„ฑ์„ AI ๋ชจ๋ธ๋กœ ๋ถ„์„ํ•˜์—ฌ ์‹œ๊ฐํ™”ํ•œ๋‹ค๋ฉด ์–ด๋–ค ๋ฐฉ์‹์ด ํšจ๊ณผ์ ์ผ๊นŒ?