Skip to content

Commit 7aacc45

Browse files
authored
Remove custom YT search in favour of youtube-dl's search (#122)
* remove custom YT search in favour of youtubedl's search fixes #115 * remove test not in use anymore * address some lint errors
1 parent dc58ac9 commit 7aacc45

7 files changed

+50
-168
lines changed

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
spotipy==2.16
22
google-api-python-client==1.6.2
3-
youtube-dl>=2015.12.23
3+
youtube-dl>=2020.11.17
44
sentry-sdk==0.14.3
55
colorama==0.4.3
66
click==7.0

spotify_dl/constants.py

-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
__all__ = ['VERSION']
22

3-
YOUTUBE_API_SERVICE_NAME = "youtube"
4-
YOUTUBE_API_VERSION = "v3"
5-
VIDEO = 'youtube#video'
6-
YOUTUBE_VIDEO_URL = 'https://www.youtube.com/watch?v='
73
VERSION = '7.0.0'
84
SAVE_PATH = '~/.spotifydl'

spotify_dl/scaffold.py

+5-15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313

1414
def check_for_tokens():
15+
"""
16+
Checks if the required API keys for Spotify has been set.
17+
:param name: Name to be cleaned up
18+
:return string containing the cleaned name
19+
"""
1520
log.debug('Checking for tokens')
1621
CLIENT_ID = getenv('SPOTIPY_CLIENT_ID')
1722
CLIENT_SECRET = getenv('SPOTIPY_CLIENT_SECRET')
@@ -28,19 +33,4 @@ def check_for_tokens():
2833
https://developer.spotify.com/my-applications
2934
''')
3035
return False
31-
32-
YOUTUBE_DEV_KEY = getenv('YOUTUBE_DEV_KEY')
33-
log.debug("YouTube dev key: {}".format(YOUTUBE_DEV_KEY))
34-
if YOUTUBE_DEV_KEY is None:
35-
print('''
36-
Youtube Data API token has not been setup. You can do this by
37-
setting environment variables like so:
38-
39-
export YOUTUBE_DEV_KEY='your-youtube-dev-key'
40-
41-
Generate the key from
42-
https://console.developers.google.com/apis/api/youtube/overview
43-
44-
Using HTML Scraper as a fallback.
45-
''')
4636
return True

spotify_dl/spotify.py

+2-44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import youtube_dl
21
from spotify_dl.scaffold import *
32
from spotify_dl.utils import sanitize
43

@@ -19,7 +18,8 @@ def fetch_tracks(sp, item_type, url):
1918
while True:
2019
for item in items['items']:
2120
track_name = item['track']['name']
22-
track_artist = " ".join([artist['name'] for artist in item['track']['artists']])
21+
log.debug("Artist: {}".format(item['track']['artists']))
22+
track_artist = ", ".join([artist['name'] for artist in item['track']['artists']])
2323
songs_dict.update({track_name: track_artist})
2424
offset += 1
2525

@@ -48,48 +48,6 @@ def fetch_tracks(sp, item_type, url):
4848
return songs_dict
4949

5050

51-
def download_songs(songs_dict, download_directory, format_string, skip_mp3):
52-
"""
53-
Downloads songs from the YouTube URL passed to either current directory or download_directory, is it is passed.
54-
:param songs_dict: Dictionary of songs and associated artist
55-
:param download_directory: Location where to save
56-
:param format_string: format string for the file conversion
57-
:param skip_mp3: Whether to skip conversion to MP3
58-
"""
59-
download_directory = f"{download_directory}\\"
60-
log.debug(f"Downloading to {download_directory}")
61-
for number, item in enumerate(songs_dict):
62-
log.debug('Songs to download: %s', item)
63-
64-
url_, track_, artist_ = item
65-
download_archive = download_directory + 'downloaded_songs.txt'
66-
outtmpl = download_directory + '%(title)s.%(ext)s'
67-
ydl_opts = {
68-
'format': format_string,
69-
'download_archive': download_archive,
70-
'outtmpl': outtmpl,
71-
'noplaylist': True,
72-
'postprocessor_args': ['-metadata', 'title=' + str(track_),
73-
'-metadata', 'artist=' + str(artist_),
74-
'-metadata', 'track=' + str(number + 1)]
75-
}
76-
if not skip_mp3:
77-
mp3_postprocess_opts = {
78-
'key': 'FFmpegExtractAudio',
79-
'preferredcodec': 'mp3',
80-
'preferredquality': '192',
81-
}
82-
ydl_opts['postprocessors'] = [mp3_postprocess_opts.copy()]
83-
84-
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
85-
try:
86-
log.debug(ydl.download([url_]))
87-
except Exception as e:
88-
log.debug(e)
89-
print('Failed to download: {}'.format(url_))
90-
continue
91-
92-
9351
def parse_spotify_url(url):
9452
"""
9553
Parse the provided Spotify playlist URL and determine if it is a playlist, track or album.

spotify_dl/spotify_dl.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import sys
88

99
from spotify_dl.scaffold import log, check_for_tokens
10-
from spotify_dl.spotify import fetch_tracks, download_songs, parse_spotify_url, validate_spotify_url, get_item_name
11-
from spotify_dl.youtube import fetch_youtube_url, get_youtube_dev_key
10+
from spotify_dl.spotify import fetch_tracks, parse_spotify_url, validate_spotify_url, get_item_name
11+
from spotify_dl.youtube import download_songs
1212
from spotify_dl.constants import VERSION
1313
from spotify_dl.models import db, Song
1414
from spotipy.oauth2 import SpotifyClientCredentials
@@ -74,19 +74,13 @@ def spotify_dl():
7474
if args.output:
7575
item_type, item_id = parse_spotify_url(args.url)
7676
directory_name = get_item_name(sp, item_type, item_id)
77-
path = Path(PurePath.joinpath(Path(args.output), Path(directory_name)))
78-
path.mkdir(parents=True, exist_ok=True)
77+
save_path = Path(PurePath.joinpath(Path(args.output), Path(directory_name)))
78+
save_path.mkdir(parents=True, exist_ok=True)
7979
log.info("Saving songs to: {}".format(directory_name))
8080

8181
songs = fetch_tracks(sp, item_type, args.url)
82-
url = []
83-
for song, artist in songs.items():
84-
link = fetch_youtube_url(song + ' - ' + artist, get_youtube_dev_key())
85-
if link:
86-
url.append((link, song, artist))
87-
8882
if args.download is True:
89-
download_songs(url, str(path), args.format_str, args.skip_mp3)
83+
download_songs(songs, str(save_path), args.format_str, args.skip_mp3)
9084

9185

9286
if __name__ == '__main__':

spotify_dl/youtube.py

+37-82
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,42 @@
1-
from os import getenv
2-
3-
from googleapiclient.discovery import build
4-
from googleapiclient.http import HttpError
5-
from sentry_sdk import capture_exception
6-
7-
8-
from spotify_dl.constants import YOUTUBE_API_SERVICE_NAME
9-
from spotify_dl.constants import YOUTUBE_API_VERSION
10-
from spotify_dl.constants import VIDEO
11-
from spotify_dl.constants import YOUTUBE_VIDEO_URL
121
from spotify_dl.scaffold import log
13-
from spotify_dl.cache import check_if_in_cache, save_to_cache
14-
from json import loads
15-
import requests
16-
from lxml import html # skipcq: BAN-B410
17-
import re
18-
19-
20-
from click import secho
21-
22-
# skipcq: PYL-R1710
23-
def fetch_youtube_url(search_term, dev_key=None):
24-
"""
25-
For each song name/artist name combo, fetch the YouTube URL and return the list of URLs.
26-
:param search_term: Search term to be looked up on YouTube
27-
:param dev_key: Youtube API key
28-
"""
29-
in_cache, video_id = check_if_in_cache(search_term)
30-
if in_cache:
31-
return YOUTUBE_VIDEO_URL + video_id
32-
if not dev_key:
33-
YOUTUBE_SEARCH_BASE = "https://www.youtube.com/results?search_query="
34-
try:
35-
response = requests.get(YOUTUBE_SEARCH_BASE + search_term).content
36-
html_response = html.fromstring(response)
37-
video = html_response.xpath("//a[contains(@class, 'yt-uix-tile-link')]/@href")
38-
video_id = re.search("((\?v=)[a-zA-Z0-9_-]{4,15})", video[0]).group(0)[3:]
39-
log.debug(f"Found video id {video_id} for search term {search_term}")
40-
_ = save_to_cache(search_term=search_term, video_id=video_id)
41-
return YOUTUBE_VIDEO_URL + video_id
42-
except AttributeError as e:
43-
log.warning(f"Could not find scrape details for {search_term}")
44-
capture_exception(e)
45-
return None
46-
except IndexError as e:
47-
log.warning(f"Could not perform scrape search for {search_term}, got a different HTML")
48-
capture_exception(e)
49-
return None
50-
else:
51-
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
52-
developerKey=dev_key,
53-
cache_discovery=False)
54-
try:
55-
in_cache, video_id = check_if_in_cache(search_term)
2+
import youtube_dl
563

57-
if not in_cache:
58-
search_response = youtube.search().list(q=search_term,
59-
part='id, snippet').execute()
60-
for v in search_response['items']:
61-
if v['id']['kind'] == VIDEO:
62-
video_id = v['id']['videoId']
63-
log.debug(f"Adding Video id {video_id}")
64-
_ = save_to_cache(search_term=search_term, video_id=video_id)
65-
return YOUTUBE_VIDEO_URL + video_id
66-
except HttpError as err:
67-
err_details = loads(err.content.decode('utf-8')).get('error').get('errors')
68-
secho("Couldn't complete search due to following errors: ", fg='red')
69-
for e in err_details:
70-
error_reason = e.get('reason')
71-
error_domain = e.get('domain')
72-
error_message = e.get('message')
734

74-
if error_reason == 'quotaExceeded' or error_reason == 'dailyLimitExceeded':
75-
secho(f"\tYou're over daily allowed quota. Unfortunately, YouTube restricts API keys to a max of 10,000 requests per day which translates to a maximum of 100 searches.", fg='red')
76-
secho(f"\tThe quota will be reset at midnight Pacific Time (PT)." ,fg='red')
77-
secho(f"\tYou can request for Quota increase from https://console.developers.google.com/apis/api/youtube.googleapis.com/quotas.", fg='red')
78-
else:
79-
secho(f"\t Search failed due to {error_domain}:{error_reason}, message: {error_message}")
80-
return None
81-
82-
def get_youtube_dev_key():
5+
def download_songs(songs, download_directory, format_string, skip_mp3):
836
"""
84-
Fetches the Youtube Developer API key from the environment variable.
85-
:return string containing the developer API key
7+
Downloads songs from the YouTube URL passed to either current directory or download_directory, is it is passed.
8+
:param songs: Dictionary of songs and associated artist
9+
:param download_directory: Location where to save
10+
:param format_string: format string for the file conversion
11+
:param skip_mp3: Whether to skip conversion to MP3
8612
"""
87-
return getenv('YOUTUBE_DEV_KEY')
13+
download_directory = f"{download_directory}\\"
14+
log.debug(f"Downloading to {download_directory}")
15+
for song, artist in songs.items():
16+
query = f"{artist} - {song}".replace(":", "").replace("\"", "")
17+
download_archive = download_directory + 'downloaded_songs.txt'
18+
outtmpl = download_directory + '%(title)s.%(ext)s'
19+
ydl_opts = {
20+
'format': format_string,
21+
'download_archive': download_archive,
22+
'outtmpl': outtmpl,
23+
'default_search': 'ytsearch',
24+
'noplaylist': True,
25+
'postprocessor_args': ['-metadata', 'title=' + song,
26+
'-metadata', 'artist=' + artist]
27+
}
28+
if not skip_mp3:
29+
mp3_postprocess_opts = {
30+
'key': 'FFmpegExtractAudio',
31+
'preferredcodec': 'mp3',
32+
'preferredquality': '192',
33+
}
34+
ydl_opts['postprocessors'] = [mp3_postprocess_opts.copy()]
35+
36+
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
37+
try:
38+
log.debug(ydl.download([query]))
39+
except Exception as e:
40+
log.debug(e)
41+
print('Failed to download: {}, please ensure YouTubeDL is up-to-date. '.format(query))
42+
continue

tests/test_youtube_url.py

-11
This file was deleted.

0 commit comments

Comments
 (0)