mtsend.py – A Command Line Tool for Movable Type
I’ve spent a bit of time coding and testing during the Christmas break, and here it is – mtsend.py, a command line tool for Movable Type (SY: Works fine in WordPress as well) written in Python. It uses XML-RPC exported by Movable Type to retrieve posts, edit posts, make new post entry, fix up categories and list recent posts, etc. It is a command line tool, i.e. no graphical user interface, but it is relatively easy to use. It is written in a scripting language called Python, and it currently has only been tested under Python 2.1 on Windows 2000, and Python 2.2/2.3/2.4 on Linux (Mandrake 8.2/9Gentoo), Windows 2000/XP and Mac OS X 10.2/10.3.
You can download the source code here:
- Download mtsend.py 1.1 (Released on 19 Nov 2005)
What is it?
mtsend.py is a command line tool that utilise Movable Type’s XML-RPC interface. It allows its users to edit/post/view/list post entries on a Movable Type site. It takes input from the standard input, and sends output to the stdout, just like all the other well-behaved command line applications. It uses a file format similar to Movable Type’s import export format.
Updated: It appears that mtsend.py also works with WordPress, as WordPress has implemented a compatible XML-RPC interface. In fact I am using this same script everyday since I switched from MT to WP.
Updated 2: There seems to be some timezone issue regarding how WP handles dateCreated attribute, tested in WP 1.5.
Why command line?
Real man works on text-consoles.
No. It is not true. But I found myself spending most of my day in the front of text console/command prompt. My typical Windows desktop consists 2 of VIM windows and 2 SSH session to other Linux boxes, and half the website I browse everyday is in the excellent text-mode browser w3m. I don’t like to fire up Mozilla and open up the Movable Type site just to post an blog entry. Moreover, I have very bad spelling problem, and posting through the web interface does not allow me to check my spellings. Of course I can edit my post in a separate editor, run that external file through a spell checker, and then paste the content into the Movable Type’s edit entry page. However, since Movable Type has already exported its interface via XML-RPC, why don’t I just write a simple script to post that text file after I’ve run it through the spell checker? The result becomes mtsend.py.
With a command line tool that can work with pipes and standard IO, it makes posting much easier for me. I can now write up a blog entry in Vi, save the text file, run it through aspell check, and then direct it to mtsend.py to have it posted to my blog site. All under your favourite Unix shell!
Documentation
These are taken directly from the source code – so just download the latest source and you will have the lot.
Configuration File
Configuration file for mtsend.py is in the style of Windows INI files, which
consist of sections and key/value pairs. There are 3 main sections - global,
site and blog.
Global Section:
There is only one key/value in this section, and it is used to note the
default blog alias to use if it is not provided on the command line.
For example:
[global]
default=example
It shows the default blog alias will be 'example'
Site Section:
You can have multiple site sections for each Movable Type installation
you have access to. The section name will be [site-"site name"]. For
example:
[site-test]
url=http://testdomain.com/mtinstall/mt-xmlrpc.cgi
username=foo
password=bar
encoding=UTF-8
It defines site "test" with the URL to the MovableType's XML-RPC CGI
script, and the username/password used to access that site. "encoding" is
optional, and defaults to UTF-8.
Blog Section:
You can have multiple blog sections for each Movable Type blogs you have
on the sites you have access to. Blogs are distinguished by their 'alias',
which you can select in the command line using -a. The section name for
this blog will be [blog-"blog alias"]. For example,
[blog-example]
site=test
blogid=3
Each blog section must have "site", which indicates the site this blog
belongs to, so that mtsend would be able to locate site-related
information from the configuration file. It also needs the blog ID on that
site. To find out all the blog IDs, you can use -B "site name" to print
out the list.
Post Format
When editing or posting via mtsend, the post needs to be in a specific
format. The format is very close to Movable Type's import/export format,
which is documented here:
http://www.movabletype.org/docs/mtmanual_importing.html
It consists of a header and body. For example:
[header1]: [value1]
[header2]: [value2]
[header3]: [value3]
-----
BODY:
....
-----
EXTENDED BODY:
....
-----
EXCERPT:
....
Extended body and excerpt are optional in a post. Most header elements are
optional when you are creating a new post. If they do not provide a value,
then the default value configured by Movable Type will be used.
These are the header keys/values:
TITLE:
The title of this post.
ALLOW COMMENTS: 0/1
Whether this post allows comments.
ALLOW PINGS: 0/1
Whether this post allows trackback pings.
CATEGORY:
The category associated with this post entry. You can have multiple
CATEGORY in the header. The first CATEGORY automatically becomes the
primary category, if PRIMARY CATEGORY is not specified.
CONVERT BREAKS: 0/1/customised text filter name.
Whether the line break will be automatically converted into <br/> and
<p/> when posted. It can also be the name of an installed text filer. To
get the list of installed text filter, use mtsend.py -T
DATE: dd/mm/yyyy HH:MM:SS [AM|PM]
The post date. It might not work if you are creating a new post.
KEYWORDS:
The keywords of your post.
PING:
The URL to be pinged during posting. You can have multiple PING in the
header.
Command Line Argument
Usage:
mtsend.py [action] [options]
Actions:
-B site List all the blogs you can access in [site]. Site has to be in
the configuration file.
-C Print out a list of existing categories.
-E postid Edit an old post. It will read the post entry from the
standard input, in Movable Type's import/export format, and
then save it back to the server. If the value is '-', then it
will try to detect the postid from the input message itself.
-G postid Retrieve/get post from the blog. If the value is '-', it will
then try to get the most recent blog entry. Retrieved entry
will be printed to the standard output.
-L num List the most recent [num] posts.
-N Posting a new blog. The entry, in the Movable Type
import/export format, is read from the standard input.
-P postid List out trackback pings to this post.
-R postid Rebuild all the static files related to this entry.
-T List out the text filters installed on the server.
-U filename Upload a file, reading from standard input, to the blog site,
with destination filename provided.
-V Show version information.
Options:
-a alias Use "alias" as the blog alias. This script will locate
relavent site URL/username/password information using this
alias.
-c config Load "config" as configuration file, instead of $HOME/.mtsendrc
-h Display this help message.
-q Decrease verbose level.
-v Increase verbose level. Message goes to standard error.
History
- 1.1 – 19 Nov 2005
- Add SSL support over proxy.
- 1.0 – 26 May 2005
timemodule related fix for Python 2.4.- Ensure all cells passed to printTable() function are in string-type.
- 0.6.1 – 6 Apr 2004
- Properly handles mt_allow_comments for MT2.6 servers.
- 0.6 – 1 Apr 2004
- Add build-in support for HTTP proxy server, which is detected via environment variable HTTP_PROXY.
- Alternative encoding for XML-RPC packets.
- 0.5 – 14 Oct 2003
- Remove the support of MT2.5. Use the older version of mtsend.py if you need these supports.
- Support KEYWORDS and PING into the header.
- Add new functionalities provided by MT2.6’s backend.
- List out trackback pings of a post.
- List out text filters installed.
- Documentation in the source code.
- 0.4 – 10 Mar 2003
- Support the new metaWeblog.newMediaObject() function via mtsend.py -U filename, i.e. you can now upload text/binary files to your MovableType site via mtsend.py!
- Use mt.getRecentPostTitles() function in MT2.6 to save bandwidth.
- Some bug fixes due to some inconsistency between MT2.6 and MT2.5.
- 0.3 – 3 Jan 2003
- Make it to work on Python 2.1. “xmlrpclib” needs to be downloaded separately. It is tested on Python 2.1.2 for Windows.
- 0.2 – 30 Dec 2002
- Fixed a bug in saving the post entry back, where new line characters are stripped.
- 0.1 – 30 Dec 2002
- Initial public version
External Resources
- XML-RPC for Python – you need to install “xmlrpclib” if you are using Python 2.1
- Movable Type XML-RPC API – references from Movable Type’s site.
- PyBlogger – Blogger API wrapped in Python (thanks to Karl)
Links to This Article
- Pudding Time!
- InsultConsult
- From The Orient
- quicklinks
- esprit libre
- The Maelstr
- Raw
- Joe Grossberg
- blog-o-fobik
- zosome :: Another try at stir-fry… :: June :: 2005
- Thought Bottles :: Testing mtsend.py :: August :: 2005
- henman.ca » Blog Archive » Testing out blog clients
- Lewis Collard
- La domo de karotoj » Mi aktualigita mian blogon
- hairy ‘ » Blog Archive » Importing of The Old Important Stuff Into WordPress.com from Old Weblog: mtsend.py (part I)
- sleepy jenkins :: blog by vim :: February :: 2006
- le blog d’Alexandre FROUART » Blog Archive » mtsend.py
- le blog d’Alexandre FROUART » Blog Archive » Poster des images sur son blog en les uploadant automatiquement
- le blog d’Alexandre FROUART » Blog Archive » envoi des formules mathématiques sur son blog
- Emil Sit » Migration
- Emil Sit » Colophon, Part 2
- madame, a move is in order « diskographik
- Mysterya. Наша жизнь. » Архив блога » mtsend.py+vim
- Misc Random
- CLICK
- La domo de karotoj » XML-RPC and Ubuntu
- Finally: Blogging from the CLI with mtsend.py » oscillate it
- Millenniumdark » Blog Archive » mtsend命令行下的blog客户端
- House blogject/tweetject experiments « Notes from a small field
- DJ’s Weblog » Blog Archive » Blosxom entries into MT
Comments
Funny… I’m having some problems with it from OS X/Panther:
ornithopter:~ mph$ cat .blog.tmp|mtsend.py -N -vv
Parsing post entry from standard input…
Traceback (most recent call last):
File “/Users/mph/bin/mtsend.py”, line 771, in ?
main(sys.argv[1:])
File “/Users/mph/bin/mtsend.py”, line 763, in main
mt.execute()
File “/Users/mph/bin/mtsend.py”, line 219, in execute
handler()
File “/Users/mph/bin/mtsend.py”, line 313, in execute_n
post, cts, publish = parsePost()
File “/Users/mph/bin/mtsend.py”, line 579, in parsePost
raise Exception, ‘Invalid field key: %s’ % key
Exception: Invalid field key: BODY
mph, this happened to me under Panther (btw. other unixes too) when i failed to insert the “” line before BODY: (and before EXTENDED BODY: too ).
I now just take care that the input looks very much the same like the output of a “./mtsend.py -G – ” and had no problems since then.
This was exactly the only “problem” I had.
( forgive me, I surely rtfm, but I found the line not really stated as a must :)
This great one-file-software saved my day. Thank you very much.
My search done before leaded me deep into the heart of XML-specs and
RPC-definitions, MT inner working stuff, it made sort of … you know :)
and this one just worked….
Howdy… thanks for writing this script, it’s exactly what I was looking for. I guess great minds think alike.
Just one nitpick: I do believe the “ALLOW COMMENTS” header is not a boolean, but a string (integer, really, but whatever). A value of 2 means comments should be published if they exist, but no further comments can be posted. This can come in handy if, say, one wants to use your script in an occasional cron job to disable comments to old posts, as I in fact was attempting and failing miserably to do until examining the code. Looks like just a couple of lines in obvious places need adjustment.
Thanks again!
Brad,
You are right. Some code is still stuck at the stage of Movable Type 2.5x. I’ll try to fix that. Thanks.
Thanks for this wonderful script. Please tell me one thing. I’m using ExtraFields plugin, and have 2 extra fields. Is it possible for me to post those entries as well using mtsend.py.
Any ideas will be appreciated, thanks.
Thanx for your response. I have a new problem. In order to solve the issue I asked u in my last comment, i wrote a php script, which
1. created a new MT entry,
2. filled up the required table for Extra fields
3. then i rebuild the entry taking the entry id from 1 above (using -R option of mtsend)
My problem is that this rebuild is not working. When i was testing it on my test server, it worked fine, but in the production server, it just does not rebuild. Can u pls help?? here is part of my code:
$blog = "bnaarticles";
$conf_file = "mtsend.conf";
$command_str = " cat $txtfilename | /var/www/mt/admin/mtsend.py -c $conf_file -N -a $blog";
$entry_id_added = `$command_str`;
# now make the entry to the extra plugin table
$query = "insert into mt_bna_author values ($entry_id_added, '$user_name', '$user_email')";
$result = mysql_query($query) or die("Query failed : " . mysql_error());
unlink($txtfilename);
# now rebuild the entry. This is needed becoz the template does not
# know abt the bna_author etc. before the execution of the
# SQL above, and hence the earlier built was not useful
$command_str = " /var/www/mt/admin/mtsend.py -c $conf_file -R $entry_id_added -a $blog ";
$command_executed = exec($command_str);
When i echo out the commands, they seem to be ok. Can u please help me??
Hi again. I have a new problem now. Last week i installed XML::Parser and XML::Simple on my machine as i needed to get some plugin installed. After that, my mtsend.py is giving me the following error:
Error:
What could be the problem here?? Any ideas?
Sorry, the error didnt turn up in the previous comment
Error: <Fault Client: ‘Application failed during request deserialization: \nnot well-formed (invalid token) at line 50, column 62, byte 6788 at /usr/lib/perl5/XML/Parser.pm line 187\n’$gt;
MT’s docs on the importexport format have moved to http://www.sixapart.com/movabletype/docs/mtimport
Is there any chance to use mtsend.py with emacs to have a functionality equaly to TextMate on OS X:
writting in Emacs and publishing via mtsend.py to your wordpress-blog?
I can’t imagine, that I can’t use Emacs as a blogeditor for wordpress.
:-o
Phil,
As you can see that mtsend.py has not been actively developed for a while (anyone would like to take over?) However I don’t see why you can’t invoke it from Emacs.
Too bad that I am not an Emacs user, because Vi rules :)
I’m not having much luck, I keep getting Error: ‘url’ although I’ve verified that the url is correct:
cfg reads:
[global]
default=domainname
[site-domainname]
url=http://www.domainname.com/xmlrpc.php
username=xxx
password=xxx
Dear Scott,
v1.1 is working like a charm on OS X Tiger. I am able to post and then retrieve.
However, when I try to retrieve an old post entered through the web interface, I get:
Error: 'ascii' codec can't encode character you'\u2014' in
position 72: ordinal not in range(128)
BTW, the AUTHOR field is not allowed. We have to remove it.
Thanks for the wonderful tool.
I am having the same problem as Jasper. Everything is giving me an error ‘url’. I am trying to access a wordpress blog.
In case anyone else thought to use this to export content from TypePad, it breaks in enough ways that I wouldn’t feel comfortable using it without thoroughly testing it.
The two issues I encountered before moving on to try other solutions are:
1st. xmlrpclib.DateTime(val) is not in ISO8601, it is like “2007-09-11T01:24:40Z”
2nd. cat in cts are a pair not a tuple, no ‘isPrimary’
hi — great script! could do with an update, though ;)
Recently, I’ve started getting this error with my WP 2.3.3 blog:
blogclient -v -L 2
Traceback (most recent call last):
File “/home/jm/bin/blogclient”, line 906, in
main(sys.argv[1:])
File “/home/jm/bin/blogclient”, line 897, in main
mtsend.execute()
File “/home/jm/bin/blogclient”, line 231, in execute
handler()
File “/home/jm/bin/blogclient”, line 314, in execute_l
self.get_password(), num)
File “/usr/local/putplace/Python-2.5.1/lib/python2.5/xmlrpclib.py”, line 1147, in __call__
return self.__send(self.__name, args)
File “/usr/local/putplace/Python-2.5.1/lib/python2.5/xmlrpclib.py”, line 1437, in __request
verbose=self.__verbose
File “/usr/local/putplace/Python-2.5.1/lib/python2.5/xmlrpclib.py”, line 1201, in request
return self._parse_response(h.getfile(), sock)
File “/usr/local/putplace/Python-2.5.1/lib/python2.5/xmlrpclib.py”, line 1335, in _parse_response
p.feed(response)
File “/usr/local/putplace/Python-2.5.1/lib/python2.5/xmlrpclib.py”, line 547, in feed
self._parser.Parse(data, 0)
xml.parsers.expat.ExpatError: XML or text declaration not at start of entity: line 2, column 0
it appears the server is sending back an additional newline at the start of the XML-RPC response, I’m not sure why, or if this is valid.
I hacked around it by adding the re.sub() line below to xmlrpclib.py:
def _parse_response(self, file, sock):
# read response from input file/socket, and parse it
p, u = self.getparser()
while 1:
if sock:
response = sock.recv(1024)
else:
response = file.read(1024)
if not response:
break
if self.verbose:
print “body:”, repr(response)
response = re.sub(r’(?s)^\n’, ”, response) # jm fix
p.feed(response)
file.close()
p.close()
return u.close()
Hi,
I am interested in using this tool to post to LiveJournal as well. I am having very little success! Does anyone have any information on this?
I have tried both directly with LJ’s XMLRCP interace (no results), and indirectly using Delicious Glue, a script for forwarding del.icio.us’s daily blogposts to LJ via XMLRCP request. When I use delicious glue, I can get the body through, but not the subject and tags.
Any clue what I’m doing wrong?
Dear Scott,
Just a note to tell you that I still use mtsend.py both from Vim and the command line on Mac OS X Leopard (10.5.4), so it’s still chugging along great.
It inspired me to write a ruby blogger for Blogger (blogspot). I use mtsend.py for wordpress blogs.
I just wrote a little script so it decides on the alias based on the folder I am in (from vim, it’s a problem changing aliases)
if [ $# -eq 0 ]
then
echo "I got no filename"
exit 1
fi
alias=`basename $PWD`
echo "Using alias: $alias"
mtsend.py -N -a $alias < $*
–
rahul
Hi,
I’m trying mtsend.py on a drupal site, and I, too, can’t get past the “Error:url’ message. What is the reason? Did anybody find it out?
Another question: can mtsend.py today set or read fields in posts which have custom fields?
TIA,
Marco
I wanted to use this to create future-dated wordpress posts.. but.. it’s hard.
I have to manually offset for GMT. Fine. It’s not relaly 6:26PM, it’s 11:26PM. I know I’m GMT-5. That part is easy.
Now it successfully posts to wordpress — scheduled and everything! Except when the time comes, it doesn’t post. ARGH.
I literally will need to find another complete solution for blogging if that doesn’t work. It sucks, because I have been using mtsend for over a year! ):
I don’t know what you mean exactly, but I do use a shell script I wrote to pick up posts from a certain folder and post them using mtsend.py. The file names contain the date to be posted.
A cron job runs 2 times a day and checks.
mtsend.py follows the usual unix philosophy of one program doing one job.
okay… the timezone issue is due to not using date_created_gmt … but anyway… i’m just trying to do a normal post like the last 200+ posts i’ve used mtsend.py for, and all of a sudden i’m getting an error:
Parsing post entry from standard input…
Saving new post entry…
Error:
Of course, how can i know what isn’t well formed when it doesn’t tell me what it is? Frustrated.
Sorry, the error message didn’t come through. It was:
Fault -32700: ‘parse error. not well formed’
@clintJCL
I’ve had parse errors recently too. Seems like a server issue. I tried posting the next day and all was fine.
–rb
Add a comment
Gravatar is used. Email address is required but will not be displayed. Please keep your comment on topic. No spamming and/or bad language. First time poster will be moderated. Scott reserves the right to delete/edit your comments.

Well done. mtsend.py looks like exactly what I wanted for a command line client, and seems to be working well under Fedora Core.