Skip to content

Instantly share code, notes, and snippets.

@cardin
Last active April 9, 2017 03:18
Show Gist options
  • Save cardin/5135257 to your computer and use it in GitHub Desktop.
Save cardin/5135257 to your computer and use it in GitHub Desktop.
A command line utility to update your anime and manga lists at MyAnimeList.net, since MAL.net is not accessible for users who have dynamic IP addresses due to ISPs.
"""
Requirements:
Python 2.7x onwards (2.6 might work)
Not compatible with Python 3
Purpose:
Help to update MAL list
Functions:
- Adds/updates/deletes new series from list
- Searches for details about a series by name
What it does not do:
- View list
. You can check MAL website yourself for that
Author:
March 2013 - Cardin Lee JH (@cardin)
http://github.com/cardin
License:
No restrictions.
But it would be nice to have attribution, or share back any code updates.
Add. Details:
- Does not store any configuration files on your system
"""
import sys, os
from urllib import urlencode
import urllib2, base64
import getpass
import re, string
import webbrowser, HTMLParser
# ------ Global Vars Declaration -----
__debug = False
user = ""
pswd = ""
mediaType = None
mediaTypeName = ("Manga", "Anime")
seriesID = None
# --------------- FUNCTIONS ---------------
def parseInt(val):
""" Converts a string of mixed characters to integer
Parameters:
val - a string
Returns:
An integer
"""
try:
val = int(val)
except ValueError:
# Go thru each char until a non-digit is found
index = 0
for i in val:
if i.isdigit():
index = index + 1
else:
break
# Extracting the numeral portion
if index == 0:
val = None # No numerals found
else:
val = int(val[0:index])
return val
def system_check(args):
""" Checks for Python version and debugging flag
Parameters:
args - Commandline arguments
Returns:
0 if no issue, 1 if have issue
"""
# Debugging stuff
global __debug
try:
__debug = True if args[1] == "-d" else False
if __debug: # clear screen
os.system('cls' if os.name=='nt' else 'clear')
except Exception as e:
pass # 2nd cmd arg not supplied, no debug
# Check Python version
req_version = (2,7)
cur_version = (sys.version_info.major, sys.version_info.minor)
if cur_version < req_version:
print ("Your Python version is %s. This program needs Python %s at least" % (str(cur_version), str(req_version)))
return 1
elif __debug:
print ("Current Python version is %s" % str(cur_version))
return 0
def send_MAL_request(url, xmlData=None):
""" Sends a URL request with credentials
Parameters:
url - URL to make request to
xmlData - XML data you want to send, in string form
Returns:
(string) Results of URL request
"""
global __debug
# Encode xml data if any
xml = None
if xmlData is not None:
xml = urlencode({'data': data})
# Fill up the connection request
request = urllib2.Request(url, xml)
auth_string = base64.encodestring('%s:%s' % (user, pswd)).replace('\n', '')
request.add_header("Authorization", "Basic %s" % auth_string)
# Open URL
try:
result = urllib2.urlopen(request)
except Exception as e:
print ("\t"+str(e))
return None
result = result.read()
return result
def get_MAL_URL(cmd, idNum=None):
""" Crafts a URL for the REST query
Parameters:
cmd - (add, update, delete, verify)
idNum - an integer specifying the id of series to modify
Returns:
N.A.
Throws:
Exception if program is not yet ready to craft a URL for the specified cmd
"""
global mediaType
uri = ""
# Input Checking
cmd = cmd.lower()
idNum = int(idNum) if (idNum != None) else None
# Making the URL
if cmd == "verify":
uri = "http://myanimelist.net/api/account/verify_credentials.xml"
elif cmd in ("add", "update", "delete"):
if (mediaType is not None) and (idNum is not None):
uri = "http://myanimelist.net/api/%slist/%s/%d.xml" % (mediaTypeName[mediaType].lower(), cmd, idNum)
else:
raise Exception("mediaType or idNum is not defined!")
else:
raise Exception("Bad command " + cmd + " given!")
return uri
def Display_MAL_XML(xmlData):
""" Formats and prints out the series data given
Parameters:
xmlData - XML listing of the series
"""
if mediaType == 0: #Manga
elementsToFind = ("id", "title", "english",
"synonyms", "chapters",
"volumes", "score",
"type", "status",
"start_date", "end_date",
"synopsis", "image")
for element in elementsToFind:
data = re.findall("<{0}>(.*)</{0}>".format(element), xmlData, re.DOTALL)[0]
elementName = string.capwords(element, "_")
if elementName == "Synopsis":
parser = HTMLParser.HTMLParser();
data = parser.unescape(data)
print ("\t\t%s:\t%s"%(elementName, data))
else: #Anime
pass
print ("Appleseed -------------------")
choice = raw_input("\t\tPress any key to continue...")
def request_login():
""" Queries user for login details
Returns:
N.A.
"""
global user
global pswd
url = get_MAL_URL("verify")
# keeps running until login successful
while 1:
user = raw_input("Username: ")
pswd = getpass.getpass("Password: (silent input) ", sys.stderr)
# attempts to login
result = send_MAL_request(url)
if result:
print ("\tLogin Successful!")
break
else:
print ("\tLogin unsuccessful.")
result = raw_input ("\tTry again? (Y/N) ")
if result.lower() != "y":
raise SystemExit()
class Series:
@staticmethod
def get():
""" Finds the series type and series ID the user wants to operate on
Returns:
N.A.
"""
global mediaType
global seriesID
mediaType = raw_input("""\nWhat type of series are you looking for?
(0) Manga
(1) Anime
""")
mediaType = parseInt(mediaType) % 2
# If ID is known, give it back straight. If ID is unknown, we shall search for it
knowledge = raw_input("Do you know the ID of the series you want to modify? (Y/N) ")
if knowledge.lower() == "y": # ID is known
seriesID = raw_input("ID: ")
seriesID = parseInt(seriesID)
else: # ID is unknown
seriesID = Series.obtain_unknown()
@staticmethod
def obtain_unknown():
""" Obtains ID num of an unknown series
Returns:
None if no series were selected. An integer if a series were selected.
"""
global mediaType, mediaTypeName
name = raw_input("\nName of %s Series: "%mediaTypeName[mediaType])
result = Series.find(name)
# If no results
if not result:
choice = raw_input ("\tNo results found.\n\tDo you want to search again? (Y/N) ")
if choice.lower() == 'y':
print ("Back to search menu...")
return # Back out to main function, which will enter here again
else:
raise SystemExit() # quit now
# If reach here, then we have results
while 1:
Series.display(result)
choice = raw_input("""\nPick an option:
pick <num> - Choose to operate on series "num"
detail <num> - Check series "num" in detail
browser - Open search results in browser for better viewing
search - Choose to search again
quit - Quit the program
""")
choice = choice.lower()
if "pick" in choice:
return parseInt(choice[5:])
elif "detail" in choice:
Series.display(result, parseInt(choice[7:]))
elif choice == "browser":
uri = "http://myanimelist.net/%s.php?q=%s"%(mediaTypeName[mediaType].lower(), name)
webbrowser.open_new_tab(uri)
elif choice == "search":
print ("Back to search menu...")
return # Back out to main function, which will enter here again
elif choice == "quit":
raise SystemExit() # quit now
else:
print("\t\tInvalid option, choose again")
continue
@staticmethod
def find(name, openURI = False):
""" Searches for a series and lists all matching series
Parameters:
name - Name of the series. Capitalisation may be ignored.
Searches only for direct matches
openURI - boolean. If true, opens results in default browser
Returns:
A dictionary {"xmlData", "len", "id", "title"}
"""
global mediaType, mediaTypeName
url = "http://myanimelist.net/api/" + mediaTypeName[mediaType].lower() + "/search.xml?q=" + name
# if true, opens default browser
if openURI:
webbrowser.open_new_tab(url)
return
rcv = send_MAL_request(url)
# Organise the series data
titles = re.findall("<title>(.*)</title>", rcv)
ids = re.findall("<id>(.*)</id>", rcv)
resultsLen = len(titles)
# Remove whitespace in each line
tempRcv = []
for i in rcv.split('\n'):
temp = i.strip()
if temp:
tempRcv.append(temp)
rcv = "\n".join(tempRcv)
rcv = rcv.strip() # to remove \n at start and end
# Separate by entries
rcv = "".join( rcv.split("<entry>\n")[1:] )
entries = rcv.split("\n</entry>\n")[:-1]
# Print out results
if not titles:
return None # No results
else:
return {"xmlData":entries, "len":resultsLen, "id":ids, "title":titles}
@staticmethod
def display(dictData, idNum = -1):
"""
Parameters:
dictData - Dictionary data returned by series_find()
idNum - If -1, displays all series in summary form. If given a value, displays that series in detail
Returns:
None
"""
if idNum == -1:
# Displays all summary
print("\n\t\t ID\t\tSeries")
print("\t\t-----------------------------")
for i in range(dictData["len"]):
print ("\t\t{0}\t{1}".format(dictData["id"][i], dictData["title"][i]))
else:
# Display detailed info
if str(idNum) in dictData["id"]:
index = dictData["id"].index(str(idNum))
print(" ")
Display_MAL_XML(dictData["xmlData"][index])
else: # ID not in results
print ("\t\tThe id '%d' wasn't in the results shown."%idNum)
print ("\t\tPlease try again.")
#############################################
### PROGRAM START ###
#############################################
def main(args):
if system_check(args): # has issue
return 0
print ("\tWelcome to the MAL cmd Updater!")
print ("\tKey in your credentials to begin this session!\n")
request_login()
while 1:
seriesID = Series.get()
if seriesID == None:
continue # pick again
print("You picked series %d" % seriesID)
try:
if __name__ == "__main__":
exit(main(sys.argv))
except SystemExit as e:
print ("\nClosing the program...")
except KeyboardInterrupt as e:
print ("\nTerminating the program...")
@cardin
Copy link
Author

cardin commented Mar 16, 2013

@c1f78f8: Refactored the whole thing for clean up and clearer logic flow.

@2be3d92: Did a quick test to make sure that the updating works.

@baedd5e:

  • Added helper functions to parse input, mixedToInt() and processUpdateListInput()
  • Added helper function to parse received result, Display_MAL_XML()

@e02c04f:

  • Added retry prompt for login
  • Hide password input
  • Standardised internal logic for anime/manga selection, now uses integer
  • Separated the main menu into 2 distinct user operations: modify list and search new series
  • Caught SystemExit error on program exit

@391e813: Figured out how to get authentication to work with urllib2, replaced dependency on cmd curl with urllib2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment