/
youtube_to_gmusic.py
executable file
·207 lines (167 loc) · 6.91 KB
/
youtube_to_gmusic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env python2
from __future__ import unicode_literals, print_function
import argparse
import json
import shutil
import tempfile
import requests
import acoustid
import mutagen.id3
import youtube_dl
from apiclient.discovery import build
from gmusicapi import CallFailure
from gmusicapi.clients import Musicmanager, OAUTH_FILEPATH
from mutagen.mp3 import MP3
from mutagen.easyid3 import EasyID3
requests.packages.urllib3.disable_warnings()
VERBOSE = False
try:
with file('settings', 'r') as f:
settings = json.load(f)
# Load to local variables directly so that any missing settings are detected at the start
acoustid_api_key = settings['acoustid_api_key']
try:
google_api_key = settings['google_api_key']
except:
google_api_key = None
except Exception as e:
print('Error loading settings file!')
raise(e)
def vprint(line):
if VERBOSE:
print(line)
def download(link, temp_path):
print('Downloading...')
ydl_opts = {'format': 'bestaudio/best',
'outtmpl': temp_path + '/%(autonumber)s.%(ext)s',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
}],
'quiet': not VERBOSE,
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
ydl.download([link])
# TODO: Check file extension!
return temp_path + '/00001.mp3'
def tag_file(file_path, title, artist, album):
print("Tagging...")
mp3file = MP3(file_path, ID3=EasyID3)
try:
mp3file.add_tags(ID3=EasyID3)
except mutagen.id3.error:
pass
if isinstance(artist, str):
artist = artist.decode('utf-8')
if isinstance(title, str):
title = title.decode('utf-8')
if isinstance(album, str):
album = album.decode('utf-8')
mp3file['artist'] = artist
mp3file['title'] = title
mp3file['album'] = album
mp3file.save()
def get_song_info(file_path, title, artist, album, link):
album = 'Youtube' if album is None else album
if not title or not artist:
match = acoustid.match(acoustid_api_key, file_path)
try:
result = match.next()
artist = result[3] if artist is None else artist
title = result[2] if title is None else title
except:
print("Unable to match via AcoustID! Falling back to video title")
artist = 'Unknown' if artist is None else artist
title = get_youtube_title(link) if title is None else title
vprint("Found song info:\n" +
" Artist: %s\n" % artist +
" Title: %s\n" % title +
" Album: %s" % album)
return [title, artist, album]
def get_youtube_title(link):
ydl = youtube_dl.YoutubeDL({'quiet': not VERBOSE})
result = ydl.extract_info(link, download=False)
return result['title']
def gm_login(credentials):
if not hasattr(gm_login, 'api'):
# Stored api connection for multiple upload support without re-authing every time
gm_login.api = Musicmanager(debug_logging=VERBOSE)
# Credential pass through is working here
if not gm_login.api.is_authenticated():
if not gm_login.api.login(OAUTH_FILEPATH if not credentials else credentials):
try:
gm_login.api.perform_oauth()
except:
print("Unable to login with specified oauth code.")
gm_login.api.login(OAUTH_FILEPATH if not credentials else credentials)
return gm_login.api
def upload(file_path, credentials):
# TODO: Report song being uploaded? 'artist - title'?
print("Uploading...")
email = credentials.id_token['email'] if credentials else '--'
vprint("File is: '%s'. User is: '%s'." % (file_path, email))
api = gm_login(credentials)
try:
# Can set enable_matching on the api.upload call but apparently requires avconv
# matching appears to suck. acoustID is more accurate
uploaded, matched, not_uploaded = api.upload(file_path, enable_matching=False)
except CallFailure as e:
print("Failed to upload: %s" % e)
else:
if uploaded:
print("Uploaded successfully!")
else:
if 'ALREADY_EXISTS' in not_uploaded[file_path]:
raise Exception("Failed to upload file. Already exists.")
def process_link(link, artist=None, title=None, album=None, credentials=None):
album = 'Youtube Uploads'
try:
email = credentials.id_token['email'] if credentials else '--'
vprint("Starting to process link '%s' for user '%s'" % (link, email))
temp_path = tempfile.mkdtemp()
downloaded_file = download(link, temp_path)
title, artist, album = get_song_info(downloaded_file, title, artist, album, link)
tag_file(downloaded_file, title, artist, album)
upload(downloaded_file, credentials)
except Exception as e:
raise e
finally:
shutil.rmtree(temp_path)
def search_for_id(search):
print('Searching for video...')
if not google_api_key:
raise Exception('No google_api_key found in settings')
youtube = build('youtube', 'v3', developerKey=google_api_key)
search_response = youtube.search().list(q=search, part='id,snippet', maxResults=1).execute()
video_id = search_response['items'][0]['id']['videoId']
title = search_response['items'][0]['snippet']['title']
print("Found video '%s'" % title)
vprint("Video ID: %s\n" % video_id)
return video_id
def process_search(search, artist=None, title=None, album=None, credentials=None):
video_id = search_for_id(search)
process_link(video_id, artist, title, album, credentials)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.description = ('Import the provided youtube link/ID to your Google Music account ' +
'Default album will be "Youtube" ' +
'Default artist/title will be discovered via acoustID. ' +
'If no match is found via acoustID the Youtube title will be used ' +
'with "Unknown" as the artist. Either link or search must be specified.')
parser.add_argument('-v', '--verbose',
help='Increase verbosity of output',
action='count')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-l', '--link', help='Link (or just the ID) to youtube video to be used')
group.add_argument('-s', '--search')
parser.add_argument('-a', '--artist')
parser.add_argument('-b', '--album')
parser.add_argument('-t', '--title')
args = parser.parse_args()
if args.verbose > 0:
VERBOSE = True
if args.link:
process_link(args.link, args.artist, args.title, args.album)
elif args.search:
process_search(args.search, args.artist, args.title, args.album)