-
Notifications
You must be signed in to change notification settings - Fork 0
/
AlbumFetchBot.py
404 lines (372 loc) · 16.3 KB
/
AlbumFetchBot.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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# AlbumFetchBot.py
#
# Copyright 2017 Keaton Brown <linux.keaton@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
########################################################################
# #
# Definitions #
# #
########################################################################
#### Startup
def loadCreds(myPath):
"""
loads the config file, if anything is empty, cause panic
"""
config = configparser.RawConfigParser()
config.optionxform = lambda option: option
config.read(myPath+"credentials.ini")
if not config.sections():
raise Exception
for item in config.sections():
if not [thing[1] for thing in config[item].items()]:
raise Exception
return config
def makeCreds(myPath):
"""
Walks user through creation of credentials.ini
"""
print("Either this is the first time this script is being run, or there "
"was an error reading the config file. You will now be walked "
"through obtaining all the credentials this bot needs in order "
"to function.")
config = configparser.ConfigParser()
config.optionxform = lambda option: option
print("We will first get the bot's Reddit information.")
input("Press enter to continue... ")
############################################################# Reddit
print(" 1) Go to https://www.reddit.com/prefs/apps and sign in with your "
"bot account.\n"
" 2) Press the 'create app' button, then enter the following :\n\n"
" Name: AlbumFetchBot (or another name if you so wish)\n"
" App type: script\n"
" description: (leave this blank or enter whatever you wish)\n"
" about url: https://github.com/WolfgangAxel/AlbumFetchBot\n"
" redirect url: http://127.0.0.1:65010/authorize_callback\n\n"
" 3) Finally, press the 'create app' button.")
input("Press enter to continue... ")
print("Underneath the name of the app, there should be a string of letters and numbers.\n"
"That is the bot's client-id.\n"
"The bot's secret is displayed in the table.")
redCreds = {}
for short,thing in [["u","username"],["p","password"],["c","client-id"],["s","secret"]]:
while True:
value = input("Please enter the bot's "+thing+":\n==> ")
confirm = input("Is '"+value+"' correct? (y/n)\n==> ")
if confirm.lower() == "y":
redCreds[short] = value
break
print("Confirmation failed. Restarting entry")
############################################################ Discogs
print("Next, we will get the bot's Discog information")
input("Press enter to continue... ")
print(" 1) Go to https://www.discogs.com/settings/developers and sign in "
"with the bot's account.\n"
" 2) Press the 'Generate Token' button")
disCreds = {}
while True:
token = input("Paste the user token here:\n==> ")
confirm = input("Is '"+token+"' correct? (y/n)\n==> ")
if confirm.lower() == "y":
disCreds["token"] = token
break
print("Confirmation failed. Restarting entry")
############################################################ Spotify
print("Next, we will get the bot's Spotify information")
input("Press enter to continue... ")
print(" 1) Go to https://developer.spotify.com/my-applications/#!/ "
"and sign in with the bot's account.\n"
" 2) Press 'Create an App', then fill out the form as you wish.\n"
" 3) Submit the form. The resulting page will have the bot's \n"
"client-id and secret listed about halfway through the page.")
sptCreds = {}
for short, thing in [["c","client-id"],["s","secret"]]:
while True:
value = input("Please enter the bot's "+thing+":\n==> ")
confirm = input("Is '"+value+"' correct? (y/n)\n==> ")
if confirm.lower() == "y":
sptCreds[short] = value
break
print("Confirmation failed. Restarting entry")
############################################################### Misc
print("Almost done! Just a few more items to define.")
input("Press enter to continue... ")
mscCreds = {}
for variable,question in [ ["mySub","Enter the name of your subreddit."],
["botMaster","Enter your personal Reddit username. (This is used for Reddit's user-agent, nothing more)"],
["sleepTime","How many seconds to wait between refreshing? (Use whole numbers like 300 or expressions like 5 * 60)"]
]:
while True:
value = input(question+"\n==>")
confirm = input("Is '"+value+"' correct? (y/n)\n==> ")
if confirm.lower() == "y":
mscCreds[variable] = value
break
print("Confirmation failed. Restarting entry.")
config["R"] = redCreds
config["D"] = disCreds
config["S"] = sptCreds
config["M"] = mscCreds
with open(myPath+"credentials.ini","w") as cfg:
config.write(cfg)
print("Config file written successfully!")
return config
#### Utility
def searchDiscogs(artist,song):
"""
Return the url for what we assume is the album.
Gets the CD from the title search, then attempts
to see if the song is in the CD's tracklist.
Should be used in a try statement, as an error
will indicate that the song wasn't found on any
album
"""
print("Searching Discogs")
search = D.search(artist+" "+song)
i=0
while True:
bestGuess = search[i]
if song.lower() in [ s.title.lower() for s in bestGuess.tracklist ]:
break
else:
i += 1
return bestGuess.data["uri"]
def makeUseable(v):
"""
itunespy returns an unusable list, this fixes it.
"""
d = {}
broken = str(v[0]).splitlines()
for result in broken:
if result:
index,value = result.split(":",1)
d[index] = value
return d
def searchITunes(artist,song):
"""
Return the url for what we assume is the song.
Should be used in a try statement, as an error
will indicate that the song wasn't found on
iTunes
"""
print("Searching iTunes")
search = itunespy.search(artist+" "+song)
parsed = makeUseable(search)
return parsed["collection_view_url"]
def searchSpotify(artist,song):
"""
Return the url for what we assume is the album.
Should be used in a try statement, as an error
will indicate that the song wasn't found on
Spotify
"""
print("Searching Spotify")
search = S.search(artist+" "+song)
return search['tracks']['items'][0]['album']['external_urls']['spotify']
def createText(submission,artist,song):
"""
Make the body of the message
"""
comment = "**"+artist.title()+" - "+song.title()+"**\n\n"
fails = 0
try:
comment += "* [View on Discogs]("+searchDiscogs(artist,song)+")\n"
except:
fails += 1
try:
comment += "* [Purchase on iTunes]("+searchITunes(artist,song)+")\n"
except:
fails += 1
try:
comment += "* [Stream on Spotify]("+searchSpotify(artist,song)+")\n"
except:
fails += 1
if fails == 3: # Number of services to search - update this if more are added!
raise Exception
comment += botFlair
return comment
def checkMessages():
"""
Check inbox for Edit and Deletion requests.
Automatically skips over user mentions and comment replies
"""
for reply in R.inbox.comment_replies():
reply.mark_read()
for mention in R.inbox.mentions():
mention.mark_read()
for message in R.inbox.unread():
try:
postID = re.match("[a-z0-9]{7}$",message.body[:7]).group()
post = R.comment(id=postID)
except:
print("Message from /u/"+message.author.name+" failed:\n\n"+message.body)
message.mark_read()
continue
print("Got a message from /u/"+message.author.name)
try:
if post.author.name.lower() != creds["R"]["u"].lower():
print("Not a comment I made. Skipping.")
message.reply("You provided an invalid comment ID. Please do "
"not edit the ID when submitting a request.")
message.mark_read()
continue
except:
print("Not a comment I made. Skipping.")
message.reply("You provided an invalid comment ID. Please do "
"not edit the ID when submitting a request.")
message.mark_read()
continue
if message.author.name != R.submission(id=post.submission.id).author.name:
print("Not OP. Skipping.")
message.reply("You are not the OP of that post, so you cannot "
"request that action.")
message.mark_read()
continue
if message.subject == "Edit autofetch":
print("Edit requested.")
parts = re.search("Artist: (.*?)\n\nSong: (.*)",message.body)
try:
if not (parts.group(1) or parts.group(2)):
message.reply("Please enter both an artist and a song name.")
message.mark_read()
print("Incorrect formatting. Skipping.")
continue
newComment = createText(R.submission(id=post.submission.id),parts.group(1),parts.group(2)).replace("__POSTID__",postID)
post.edit(newComment)
print("Edit success")
message.reply("Autofetch was successfully edited")
except:
print("Edit failed")
message.reply("The post failed to be edited. Ensure the song "
"information was entered correctly, then check "
"if the song/album exists on Discogs, iTunes, "
"and/or Spotify. If problems persist, contact "
"the moderators.")
elif message.subject == "Delete autofetch":
print("Delete request.")
post.delete()
message.reply("Autofetch was successfully deleted.")
print("Message reply sent.")
message.mark_read()
def titleParse(subTitle):
"""
Check submission title for correct form. Raise exception if not in
suggested format, return title without [OPTIONAL TEXT]
"""
title = re.search("(.*?) - (.*)",subTitle)
artist = title.group(1)
song = title.group(2)
optionalText = re.search(r"[[].*[\]]",song)
# Remove all instances of "[.*]" from song
while True:
try:
song = song.replace(optionalText.group(),"")
optionalText = re.search(r"[[].*[\]]",song)
except:
break
# remove trailing whitespace from song
while True:
if song[-1] == " ":
song = song[:-1]
else:
break
print("Read '"+subTitle+"' with the following information:\n"
" artist: "+artist+"\n"
" song: "+song)
return artist,song
def checkSubreddit(sub):
"""
Check the new submissions to the subreddit, comment accordingly
"""
for submission in R.subreddit(sub).new():
# Ignore submissions older than one day
# Since the bot hides posts as it completes them, it might
# pull some really old posts as "new" after a while.
if time.time() - submission.created_utc > 24*60*60:
continue
try:
artist,song = titleParse(submission.title)
except:
print("Submission '"+submission.title+"' failed to match song title conventions. Skipping")
submission.hide()
continue
try:
comment = createText(submission,artist,song)
except:
comment =("Failed to find song information for:\n\n* Artist: "+artist+"\n* Song: "+song+"\n\n"+
"Use the links below to edit this post with the correct information or to delete this comment."+botFlair)
C = submission.reply(comment)
C.edit(C.body.replace("__POSTID__",C.id))
submission.hide() # Prevents the bot from replying to posts it already replied to
print("Replied with:\n"+comment+"\n\n********\n")
########################################################################
# #
# Script Startup #
# #
########################################################################
myPath = __file__.replace("AlbumFetchBot.py","")
try:
mods = ["praw","configparser","time","re","discogs_client","itunespy","spotipy"]
for mod in mods:
print("Importing "+mod)
exec("import "+mod)
except:
exit(mod+" was not found. Install "+mod+" with pip to continue.")
try:
creds = loadCreds(myPath)
print("Credentials loaded from file.")
except:
creds = makeCreds(myPath)
# Normalize the reddit credentials
for variable in creds["M"]:
creds["M"][variable] = creds["M"][variable].replace("/r/","").replace("r/","").replace("/u/","").replace("u/","")
creds["R"]["u"].replace("/u/","").replace("u/","")
## Reddit authentication
R = praw.Reddit(client_id = creds["R"]["c"],
client_secret = creds["R"]["s"],
password = creds["R"]["p"],
user_agent = "AlbumFetchBot, a bot for fetching music information for /r/"+creds["M"]["mySub"]+"; hosted by /u/"+creds["M"]["botMaster"],
username = creds["R"]["u"])
# Discogs authentication
D = discogs_client.Client("AlbumFetchBot, finding music for /r/"+creds["M"]["mySub"]+"; hosted by /u/"+creds["M"]["botMaster"],
user_token = creds["D"]["token"])
# Initialize Spotify instance
from spotipy.oauth2 import SpotifyClientCredentials
client_credentials_manager = SpotifyClientCredentials(client_id=creds["S"]["c"],client_secret=creds["S"]["s"])
S = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
# Define bot flair. This will need to be edited before/immediately after submitting to get the correct postID in the links.
botFlair =("\n****\n\n[^(Edit this)](https://www.reddit.com/message/compose/?to="+creds["R"]["u"]+"&subject=Edit%20autofetch&message=__POSTID__%0A%0AArtist%3A%20%0A%0ASong%3A%20) ^| "
"[^(Delete this)](https://www.reddit.com/message/compose/?to="+creds["R"]["u"]+"&subject=Delete%20autofetch&message=__POSTID__) ^| "
"[^(I am a bot)](https://github.com/WolfgangAxel/AlbumFetchBot) ^| ^(Bugs or features?) [^Github](https://github.com/WolfgangAxel/AlbumFetchBot/issues)")
print("Bot successfully loaded. Entering main loop.")
########################################################################
# #
# Script Actions #
# #
########################################################################
while True:
try:
checkMessages()
checkSubreddit(creds["M"]["mySub"])
time.sleep(eval(creds["M"]["sleepTime"]))
except Exception as e:
print("Error!\n"+"*"*20+"\n"+str(e.args)+"\nTrying again in 30 seconds.")
time.sleep(30)