Merge lp:~renamer-developers/renamer/842236-unicode-filenames into lp:renamer

Proposed by Jonathan Jacobs
Status: Merged
Approved by: Jonathan Jacobs
Approved revision: 92
Merged at revision: 89
Proposed branch: lp:~renamer-developers/renamer/842236-unicode-filenames
Merge into: lp:renamer
Diff against target: 321 lines (+119/-52)
6 files modified
renamer/application.py (+7/-0)
renamer/logging.py (+5/-0)
renamer/plugins/audio.py (+2/-2)
renamer/plugins/tv.py (+28/-11)
renamer/test/test_tvrage.py (+22/-38)
renamer/util.py (+55/-1)
To merge this branch: bzr merge lp:~renamer-developers/renamer/842236-unicode-filenames
Reviewer Review Type Date Requested Status
Tristan Seligmann Needs Fixing
Review via email: mp+75075@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tristan Seligmann (mithrandi) wrote :

26 + # If we put non-ASCII unicode in here, then log.msg just neglects to emit
27 + # it for whatever bizarre reason.

I don't think it so much "neglects to emit it" as "blows up with a UnicodeDecodeError" due to unicode->str coercion. Either way, calling log.msg with a unicode object is not support, so we definitely shouldn't be doing it.

30 + getattr(sys.stdin, 'encoding', None) or sys.getdefaultencoding())

Shouldn't this be sys.stdout? This is kinda awful anyway, but I suppose there's no better way to do it.

107 - Look up TV episode metadata on TV Rage.
108 + Construct the TV Rage URL to the quickinfo page for the seriesName,
109 + season and episode.

The site seems to refer to itself as "TVRage" not "TV Rage", this needs to be fixed in a few places.

237 +class BodyReceiver(Protocol):

_buffer needs to be documented.

261 + def connectionLost(self, reason):
262 + data = self._buffer.getvalue().decode(self.encoding)
263 + self.finished.callback(data)

This needs to do error handling. Something like:

def connectionLost(self, reason):
    if reason.check(PotentialDataLoss, RequestDone) is None:
        self.finished.errback(reason)
    else:
        data = self._buffer.getvalue().decode(self.encoding)
        self.finished.callback(data)

review: Needs Fixing
91. By Jonathan Jacobs

Address review commentary.

Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

Addressed review commentary.

Revision history for this message
Tristan Seligmann (mithrandi) wrote :

227 + lines = self.dataPath.child('tvrage').open().read()

Instead of this, do:

    lines = self.dataPath.child('tvrage').getContent()

Aside from reading more nicely, that will also ensure that the file handle is closed.

Other than that, this looks good to merge.

review: Needs Fixing
92. By Jonathan Jacobs

Review commentary.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'renamer/application.py'
2--- renamer/application.py 2010-11-01 08:48:08 +0000
3+++ renamer/application.py 2011-09-16 07:58:24 +0000
4@@ -65,6 +65,13 @@
5 os.path.basename(sys.argv[0]),)
6
7
8+ def postOptions(self):
9+ if self['name'] is not None:
10+ self['name'] = self.decodeCommandLine(self['name'])
11+ if self['prefix'] is not None:
12+ self['prefix'] = self.decodeCommandLine(self['prefix'])
13+
14+
15 def opt_verbose(self):
16 """
17 Increase output, use more times for greater effect.
18
19=== modified file 'renamer/logging.py'
20--- renamer/logging.py 2010-10-04 14:18:07 +0000
21+++ renamer/logging.py 2011-09-16 07:58:24 +0000
22@@ -42,6 +42,11 @@
23
24
25 def msg(message, **kw):
26+ # Passing unicode to log.msg is not supported, don't do it.
27+ if isinstance(message, unicode):
28+ codec = (
29+ getattr(sys.stdout, 'encoding', None) or sys.getdefaultencoding())
30+ message = message.encode(codec, 'replace')
31 log.msg(message, source='renamer', **kw)
32
33
34
35=== modified file 'renamer/plugins/audio.py'
36--- renamer/plugins/audio.py 2010-10-15 08:51:27 +0000
37+++ renamer/plugins/audio.py 2011-09-16 07:58:24 +0000
38@@ -30,11 +30,11 @@
39
40
41 defaultPrefixTemplate = string.Template(
42- '${artist}/${album} (${date})')
43+ u'${artist}/${album} (${date})')
44
45
46 defaultNameTemplate = string.Template(
47- '${tracknumber}. ${title}')
48+ u'${tracknumber}. ${title}')
49
50
51 def postOptions(self):
52
53=== modified file 'renamer/plugins/tv.py'
54--- renamer/plugins/tv.py 2010-10-27 00:48:13 +0000
55+++ renamer/plugins/tv.py 2011-09-16 07:58:24 +0000
56@@ -9,11 +9,13 @@
57 except ImportError:
58 pymeta = None
59
60-from twisted.web.client import getPage
61+from twisted.internet import reactor
62+from twisted.web.client import Agent
63
64 from renamer import logging
65 from renamer.plugin import RenamingCommand
66 from renamer.errors import PluginError
67+from renamer.util import deliverBody, BodyReceiver
68 try:
69 from renamer._compiled_grammar.tv import Parser as FilenameGrammar
70 FilenameGrammar # Ssssh, Pyflakes.
71@@ -69,7 +71,7 @@
72
73
74
75-if FilenameGrammar is None:
76+if pymeta is not None and FilenameGrammar is None:
77 class FilenameGrammar(OMeta.makeGrammar(filenameGrammar, globals())):
78 pass
79
80@@ -79,12 +81,12 @@
81 name = 'tvrage'
82
83
84- description = 'Rename TV episodes with TV Rage metadata.'
85+ description = 'Rename TV episodes with TVRage metadata.'
86
87
88 longdesc = """
89 Extract TV episode information from filenames and rename them based on the
90- correct information from TV Rage <http://tvrage.com/>.
91+ correct information from TVRage <http://tvrage.com/>.
92
93 Available placeholders for templates are:
94
95@@ -93,7 +95,7 @@
96
97
98 defaultNameTemplate = string.Template(
99- '$series [${season}x${padded_episode}] - $title')
100+ u'$series [${season}x${padded_episode}] - $title')
101
102
103 optParameters = [
104@@ -106,6 +108,9 @@
105 if pymeta is None:
106 raise PluginError(
107 'The "pymeta" package is required for this command')
108+ if self['series'] is not None:
109+ self['series'] = self.decodeCommandLine(self['series'])
110+ self.agent = Agent(reactor)
111
112
113 def buildMapping(self, (seriesName, season, episode, episodeName)):
114@@ -165,7 +170,7 @@
115
116 def extractMetadata(self, pageData):
117 """
118- Extract TV episode metadata from a TV Rage response.
119+ Extract TV episode metadata from a TVRage response.
120 """
121 data = {}
122 for line in pageData.splitlines():
123@@ -178,16 +183,28 @@
124 return series, season, episode, title
125
126
127- def lookupMetadata(self, seriesName, season, episode, fetcher=getPage):
128+ def buildURL(self, seriesName, season, episode):
129 """
130- Look up TV episode metadata on TV Rage.
131+ Construct the TVRage URL to the quickinfo page for the seriesName,
132+ season and episode.
133 """
134 ep = '%dx%02d' % (int(season), int(episode))
135 qs = urllib.urlencode({'show': seriesName, 'ep': ep})
136- url = 'http://services.tvrage.com/tools/quickinfo.php?%s' % (qs,)
137- logging.msg('Looking up TV Rage metadata at %s' % (url,),
138+ return 'http://services.tvrage.com/tools/quickinfo.php?%s' % (qs,)
139+
140+
141+ def lookupMetadata(self, seriesName, season, episode):
142+ """
143+ Look up TV episode metadata on TVRage.
144+ """
145+ url = self.buildURL(seriesName, season, episode)
146+ logging.msg('Looking up TVRage metadata at %s' % (url,),
147 verbosity=4)
148- return fetcher(url).addCallback(self.extractMetadata)
149+
150+ d = self.agent.request('GET', url)
151+ d.addCallback(deliverBody, BodyReceiver)
152+ d.addCallback(self.extractMetadata)
153+ return d
154
155
156 # IRenamerCommand
157
158=== modified file 'renamer/test/test_tvrage.py'
159--- renamer/test/test_tvrage.py 2010-10-26 22:39:58 +0000
160+++ renamer/test/test_tvrage.py 2011-09-16 07:58:24 +0000
161@@ -1,7 +1,6 @@
162 import cgi
163 import urllib
164
165-from twisted.internet.defer import succeed
166 from twisted.python.filepath import FilePath
167 from twisted.trial.unittest import TestCase
168
169@@ -50,14 +49,6 @@
170 self.plugin.postOptions()
171
172
173- def fetcher(self, url):
174- """
175- "Fetch" TV rage data.
176- """
177- data = self.dataPath.child('tvrage').open().read()
178- return succeed(data)
179-
180-
181 def test_extractParts(self):
182 """
183 Extracting TV show information from filenames works correctly.
184@@ -120,7 +111,7 @@
185
186 def test_missingPyMeta(self):
187 """
188- Attempting to use the TV Rage plugin without PyMeta installed raises a
189+ Attempting to use the TVRage plugin without PyMeta installed raises a
190 L{renamer.errors.PluginError}.
191 """
192 self.patch(tv, 'pymeta', None)
193@@ -134,31 +125,24 @@
194 def test_extractMetadata(self):
195 """
196 L{renamer.plugins.tv.TVRage.extractMetadata} extracts structured TV
197- episode information from a TV Rage response.
198- """
199- d = self.plugin.lookupMetadata('Dexter', 1, 2, fetcher=self.fetcher)
200-
201- @d.addCallback
202- def checkMetadata((series, season, episode, title)):
203- self.assertEquals(series, u'Dexter')
204- self.assertEquals(season, 1)
205- self.assertEquals(episode, 2)
206- self.assertEquals(title, u'Crocodile')
207-
208- return d
209-
210-
211- def test_lookupMetadata(self):
212- """
213- L{renamer.plugins.tv.TVRage.lookupMetadata} requests structured TV
214- episode information from TV Rage.
215- """
216- def fetcher(url):
217- path, query = urllib.splitquery(url)
218- query = cgi.parse_qs(query)
219- self.assertEquals(
220- query,
221- dict(show=['Dexter'], ep=['1x02']))
222- return self.fetcher(url)
223-
224- return self.plugin.lookupMetadata('Dexter', 1, 2, fetcher=fetcher)
225+ episode information from a TVRage response.
226+ """
227+ lines = self.dataPath.child('tvrage').getContent()
228+ series, season, episode, title = self.plugin.extractMetadata(lines)
229+ self.assertEquals(series, u'Dexter')
230+ self.assertEquals(season, 1)
231+ self.assertEquals(episode, 2)
232+ self.assertEquals(title, u'Crocodile')
233+
234+
235+ def test_buildURL(self):
236+ """
237+ L{renamer.plugins.tv.TVRage.buildURL} constructs a URL for the TVRage
238+ quickinfo API to a specific series, season and episode.
239+ """
240+ url = self.plugin.buildURL('Dexter', 1, 2)
241+ path, query = urllib.splitquery(url)
242+ query = cgi.parse_qs(query)
243+ self.assertEquals(
244+ query,
245+ dict(show=['Dexter'], ep=['1x02']))
246
247=== modified file 'renamer/util.py'
248--- renamer/util.py 2010-10-31 19:34:03 +0000
249+++ renamer/util.py 2011-09-16 07:58:24 +0000
250@@ -1,12 +1,17 @@
251+import cgi
252 import errno
253 import glob
254 import itertools
255 import os
256 import sys
257+from StringIO import StringIO
258 from zope.interface import alsoProvides
259
260-from twisted.internet.defer import DeferredList
261+from twisted.internet.defer import DeferredList, Deferred
262 from twisted.internet.task import Cooperator
263+from twisted.internet.protocol import Protocol
264+from twisted.web.client import ResponseDone
265+from twisted.web.http import PotentialDataLoss
266
267 from renamer import errors
268
269@@ -117,3 +122,52 @@
270 """
271 return itertools.islice(
272 itertools.chain(iterable, itertools.repeat(padding)), count)
273+
274+
275+
276+class BodyReceiver(Protocol):
277+ """
278+ Body receiver, suitable for use with L{IResponse.deliverBody}.
279+
280+ @type finished: C{Deferred<unicode>}
281+ @ivar finished: User-specified deferred that is fired with the decoded body
282+ text, stored in L{_buffer}, when the body has been entirely delivered.
283+
284+ @type encoding: C{str}
285+ @ivar encoding: Body text encoding, as specified in the I{'Content-Type'}
286+ header, defaults to C{'UTF-8'}.
287+
288+ @type _buffer: C{StringIO}
289+ @ivar _buffer: Delivered body content buffer.
290+ """
291+ def __init__(self, response, finished):
292+ header, args = cgi.parse_header(
293+ response.headers.getRawHeaders('Content-Type', default=[''])[0])
294+ self.encoding = args.get('charset', 'utf-8')
295+ self.finished = finished
296+ self._buffer = StringIO()
297+
298+
299+ def dataReceived(self, data):
300+ self._buffer.write(data)
301+
302+
303+ def connectionLost(self, reason):
304+ if reason.check(PotentialDataLoss, ResponseDone) is None:
305+ self.finished.errback(reason)
306+ else:
307+ data = self._buffer.getvalue().decode(self.encoding)
308+ self.finished.callback(data)
309+
310+
311+
312+def deliverBody(response, cls):
313+ """
314+ Invoke C{response.deliverBody} with C{cls(response, deferred)}.
315+
316+ @rtype: C{Deferred}
317+ @return: A deferred that fires when the instance of C{cls} callbacks it.
318+ """
319+ finished = Deferred()
320+ response.deliverBody(cls(response, finished))
321+ return finished

Subscribers

People subscribed via source and target branches