Last active
April 9, 2017 03:18
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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...") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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:
@e02c04f:
@391e813: Figured out how to get authentication to work with urllib2, replaced dependency on cmd curl with urllib2.