[488] | 1 | #!/usr/bin/python
|
---|
| 2 | # -*- coding: utf-8 -*-
|
---|
| 3 |
|
---|
| 4 | from appscript import *
|
---|
[489] | 5 | from aem import AEEnum
|
---|
[488] | 6 | from itertools import izip
|
---|
| 7 | import htmlentitydefs
|
---|
| 8 | import re
|
---|
[491] | 9 | import sys
|
---|
[489] | 10 | import time
|
---|
[488] | 11 |
|
---|
| 12 | # based on <http://sebsauvage.net/python/snyppets/>
|
---|
| 13 | RE_ENTITY_CHR = re.compile(u'&(%s);' % u'|'.join(htmlentitydefs.name2codepoint))
|
---|
| 14 | RE_ENTITY_DEC = re.compile(u'&#(\d+);')
|
---|
| 15 | RE_ENTITY_HEX = re.compile(u'&#x(\w+);')
|
---|
| 16 | def decode_entities(s):
|
---|
| 17 | def entity2char(m):
|
---|
| 18 | entity = m.group(1)
|
---|
| 19 | if entity in htmlentitydefs.name2codepoint:
|
---|
| 20 | return unichr(htmlentitydefs.name2codepoint[entity])
|
---|
[489] | 21 | return u' ' # Unknown entity: We replace with a space.
|
---|
[488] | 22 | replaced = 0
|
---|
| 23 | s, n = RE_ENTITY_CHR.subn(entity2char, s)
|
---|
| 24 | replaced += n
|
---|
| 25 | s, n = RE_ENTITY_DEC.subn(lambda x: unichr(int(x.group(1))), s)
|
---|
| 26 | replaced += n
|
---|
| 27 | s, n = RE_ENTITY_HEX.subn(lambda x: unichr(int(x.group(1),16)), s)
|
---|
| 28 | replaced += n
|
---|
| 29 | return s, replaced
|
---|
| 30 |
|
---|
| 31 | OPEN_P = r'<[Pp][^>]*>'
|
---|
| 32 | RE_OPEN_P = re.compile(OPEN_P)
|
---|
| 33 | RE_MATCHED_P = re.compile(OPEN_P + r'(?!<[Pp])(.*)</[Pp]>')
|
---|
| 34 | RE_TOO_MANY_CR = re.compile(r'\s*\n\s*\n\s*\n+', re.U)
|
---|
| 35 | RE_TAG = re.compile(r'<[^>]*>')
|
---|
[492] | 36 | RE_BR = re.compile(r'<[Bb][Rr][^>]*>\s*', re.U)
|
---|
[488] | 37 | RE_WHITESPACE = re.compile(r'\s+', re.U)
|
---|
| 38 | RE_LEADING_WHITESPACE = re.compile(r'^\s+(.*)', re.U)
|
---|
| 39 | RE_TRAILING_WHITESPACE = re.compile(r'(.*)\s+$', re.U)
|
---|
| 40 |
|
---|
| 41 | def html_to_text(s):
|
---|
| 42 | s, replaced = decode_entities(s)
|
---|
| 43 | s = s.replace('\r\n', '\n')
|
---|
| 44 | s = s.replace('\r', '\n')
|
---|
| 45 | if replaced > 0 or RE_TAG.search(s): # HTML
|
---|
| 46 | s = RE_WHITESPACE.sub(' ', s)
|
---|
| 47 | s = RE_MATCHED_P.sub(r'\1\n\n', s)
|
---|
| 48 | s = RE_OPEN_P.sub(r'\n\n', s)
|
---|
[492] | 49 | s = RE_BR.sub(r'\n', s)
|
---|
[488] | 50 | s = RE_TAG.sub('', s)
|
---|
| 51 | s = RE_TOO_MANY_CR.sub(r'\n\n', s)
|
---|
| 52 | s = RE_LEADING_WHITESPACE.sub(r'\1', s)
|
---|
| 53 | s = RE_TRAILING_WHITESPACE.sub(r'\1', s)
|
---|
| 54 | return s
|
---|
| 55 |
|
---|
[492] | 56 | def words(s):
|
---|
| 57 | return set(RE_WHITESPACE.split(s))
|
---|
| 58 |
|
---|
[489] | 59 | def podcasts_to_lyrics(iTunes):
|
---|
[488] | 60 | podcasts = iTunes.tracks[its.podcast == True]
|
---|
| 61 |
|
---|
| 62 | ids = podcasts.id()
|
---|
| 63 | descs = podcasts.description()
|
---|
| 64 | longdescs = podcasts.long_description()
|
---|
| 65 | lyricses = podcasts.lyrics()
|
---|
| 66 |
|
---|
| 67 | for id_, desc, longdesc, lyrics in izip(ids, descs, longdescs, lyricses):
|
---|
| 68 | if lyrics == k.missing_value: # video
|
---|
| 69 | continue
|
---|
| 70 |
|
---|
| 71 | if longdesc == k.missing_value:
|
---|
| 72 | if desc == k.missing_value:
|
---|
| 73 | continue
|
---|
| 74 | longdesc = desc
|
---|
| 75 | else:
|
---|
| 76 | longdesc = html_to_text(longdesc)
|
---|
[492] | 77 | if desc != k.missing_value:
|
---|
| 78 | desc = html_to_text(desc)
|
---|
| 79 | if (desc not in longdesc and
|
---|
| 80 | len(words(desc) - words(longdesc)) > 1):
|
---|
| 81 | longdesc = '%s\n\n%s' % (desc, longdesc)
|
---|
[488] | 82 |
|
---|
| 83 | if lyrics == longdesc:
|
---|
| 84 | continue
|
---|
| 85 |
|
---|
| 86 | iTunes.tracks[its.id == id_].lyrics.set(longdesc)
|
---|
| 87 |
|
---|
[489] | 88 | def iTunes_main_pane():
|
---|
| 89 | return app(u'System Events').application_processes[u'iTunes'].windows[u'iTunes'].splitter_groups[1].scroll_areas[1]
|
---|
[488] | 90 |
|
---|
[489] | 91 | def wait_for_podcast_update(iTunes):
|
---|
| 92 | # show 'Podcasts'
|
---|
| 93 | iTunes.playlists[its.special_kind == AEEnum('kSpP')].reveal()
|
---|
| 94 | podcast_status = iTunes_main_pane().outlines[1].rows.static_texts[1].value
|
---|
[491] | 95 | while any(status in (u'downloading', u'queued for download')
|
---|
| 96 | for status in podcast_status.get()):
|
---|
[489] | 97 | time.sleep(0.5)
|
---|
[488] | 98 |
|
---|
[489] | 99 | def wait_for_iPod_update(iTunes):
|
---|
| 100 | # show iPod
|
---|
| 101 | iTunes.sources[its.kind == AEEnum('kPod')].sources[1].library_playlists[1].reveal()
|
---|
| 102 | sync_enabled = iTunes_main_pane().buttons[u'Sync'].enabled
|
---|
[546] | 103 | while True:
|
---|
| 104 | try:
|
---|
| 105 | if sync_enabled.get() == True:
|
---|
| 106 | return
|
---|
| 107 | except CommandError:
|
---|
| 108 | pass
|
---|
[489] | 109 | time.sleep(0.5)
|
---|
| 110 |
|
---|
[488] | 111 | if __name__ == '__main__':
|
---|
[489] | 112 | iTunes = app('iTunes')
|
---|
[491] | 113 | print >> sys.stderr, 'Synchronizing iPod...'
|
---|
| 114 | iTunes.update()
|
---|
[489] | 115 | wait_for_iPod_update(iTunes)
|
---|
[491] | 116 | print >> sys.stderr, 'Updating podcasts...'
|
---|
[489] | 117 | iTunes.updateAllPodcasts()
|
---|
| 118 | wait_for_podcast_update(iTunes)
|
---|
[491] | 119 | print >> sys.stderr, 'Copying podcast descriptions to lyrics...'
|
---|
[489] | 120 | podcasts_to_lyrics(iTunes)
|
---|
[546] | 121 | print >> sys.stderr, 'Synchronizing iPod...'
|
---|
[489] | 122 | iTunes.update()
|
---|
[546] | 123 | wait_for_iPod_update(iTunes)
|
---|
[574] | 124 | app(id='net.sabi.UpdatePodcasts').quit()
|
---|