2 ### -*- mode: python; coding: utf-8 -*-
4 from contextlib
import contextmanager
9 class ExpectedError (Exception): pass
18 def filter(value
, func
= None, dflt
= None):
19 if value
is None: return dflt
20 elif func
is None: return value
21 else: return func(value
)
24 if not cond
: raise ExpectedError(msg
)
27 if not s
.isdigit(): raise ExpectedError("bad integer `%s'" % s
)
33 me
._i
, me
._n
= 0, len(s
)
35 s
, i
, n
= me
._s
, me
._i
, me
._n
37 if not s
[i
].isspace(): return i
42 begin
= i
= me
._wordstart()
43 if begin
< 0: return None
44 while i
< n
and not s
[i
].isspace(): i
+= 1
49 begin
= me
._wordstart()
50 if begin
< 0: return None
51 else: return s
[begin
:].rstrip()
53 URL_SAFE_P
= 256*[False]
55 b
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
56 b
"abcdefghijklmnopqrstuvwxyz" \
57 b
"0123456789" b
"!$%-.,/":
60 return "".join((URL_SAFE_P
[ch
] and chr(ch
) or "%%%02x" % ch
61 for ch
in s
.encode("UTF-8")))
63 PROG
= OS
.path
.basename(SYS
.argv
[0])
65 class BaseLocation (object):
67 SYS
.stderr
.write("%s: %s%s\n" %
(PROG
, me
._loc(), exc
))
69 class DummyLocation (BaseLocation
):
70 def _loc(me
): return ""
72 class FileLocation (BaseLocation
):
73 def __init__(me
, fn
, lno
= 1): me
._fn
, me
._lno
= fn
, lno
74 def _loc(me
): return "%s:%d: " %
(me
._fn
, me
._lno
)
75 def stepline(me
): me
._lno
+= 1
79 class Source (object):
81 TITLEP
= CHAPTERP
= False
84 def url(me
, title
= None, chapter
= None):
86 if me
.TITLEP
: raise ExpectedError("missing title number")
87 if chapter
is not None:
88 raise ExpectedError("can't specify chapter without title")
91 raise ExpectedError("can't specify title with `%s'" % me
.fn
)
93 suffix
= "#%d" % title
95 raise ExpectedError("can't specify chapter with `%s'" % me
.fn
)
97 suffix
= "#%d:%d-%d:%d" %
(title
, chapter
, title
, chapter
)
98 return me
.PREFIX
+ ROOT
+ urlencode(me
.fn
) + suffix
100 class VideoDisc (Source
):
102 TITLEP
= CHAPTERP
= True
104 class VideoSeason (object):
105 def __init__(me
, i
, title
):
109 def set_episode_disc(me
, i
, disc
):
111 raise ExpectedError("season %d episode %d already taken" %
(me
.i
, i
))
112 me
.episodes
[i
] = disc
114 def some_group(m
, *gg
):
117 if s
is not None: return s
120 class VideoDir (object):
122 _R_ISO_PRE
= RX
.compile(r
"""
124 (?: S (?P<si> \d+) (?: \. \ (?P<st> .*)—)? (?: D (?P<sdi> \d+))? |
131 _R_ISO_EP
= RX
.compile(r
"""
132 ^ E (?P<ei> \d+) (?: – (?P<ej> \d+))? $
135 def __init__(me
, dir):
136 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
138 season
, last_j
= None, 0
141 path
= OS
.path
.join(dir, fn
)
142 if not fn
.endswith(".iso"): continue
143 m
= me
._R_ISO_PRE
.match(fn
)
146 i
= filter(m
.group("si"), int, 1)
147 stitle
= m
.group("st")
148 if season
is None or i
!= season
.i
:
149 check(season
is None or i
== season
.i
+ 1,
150 "season %d /= %d" %
(i
, season
is None and -1 or season
.i
+ 1))
151 check(i
not in seasons
, "season %d already seen" % i
)
152 seasons
[i
] = season
= VideoSeason(i
, stitle
)
155 check(stitle
== season
.title
,
156 "season title `%s' /= `%s'" %
(stitle
, season
.title
))
157 j
= filter(some_group(m
, "sdi", "di"), int)
159 check(j
== last_j
+ 1,
160 "season %d disc %d /= %d" %
(season
.i
, j
, last_j
+ 1))
162 disc
= VideoDisc(path
)
163 any
, bad
= False, False
164 for eprange
in m
.group("eps").split(", "):
165 mm
= me
._R_ISO_EP
.match(eprange
)
166 if mm
is None: bad
= True; continue
167 start
= filter(mm
.group("ei"), int)
168 end
= filter(mm
.group("ej"), int, start
)
169 for k
in range(start
, end
+ 1):
170 season
.set_episode_disc(k
, disc
)
173 raise ExpectedError("bad ep list in `%s'", fn
)
177 class AudioDisc (Source
):
179 TITLEP
= CHAPTERP
= False
181 class AudioEpisode (Source
):
183 TITLEP
= CHAPTERP
= False
184 def __init__(me
, fn
, i
, *args
, **kw
):
185 super().__init__(fn
, *args
, **kw
)
188 class AudioDir (object):
190 _R_FLAC
= RX
.compile(r
"""
197 def __init__(me
, dir):
198 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
203 path
= OS
.path
.join(dir, fn
)
204 if not fn
.endswith(".flac"): continue
205 m
= me
._R_FLAC
.match(fn
)
207 i
= filter(m
.group(1), int)
209 check(i
== last_i
+ 1, "episode %d /= %d" %
(i
, last_i
+ 1))
210 episodes
[i
] = AudioEpisode(path
, i
)
212 me
.episodes
= episodes
214 class Chapter (object):
215 def __init__(me
, episode
, title
, i
):
216 me
.title
, me
.i
= title
, i
217 me
.url
= episode
.source
.url(episode
.tno
, i
)
219 class Episode (object):
220 def __init__(me
, season
, i
, neps
, title
, src
, tno
= None):
222 me
.i
, me
.neps
, me
.title
= i
, neps
, title
224 me
.source
, me
.tno
= src
, tno
225 me
.url
= src
.url(tno
)
226 def add_chapter(me
, title
, j
):
227 ch
= Chapter(me
, title
, j
)
228 me
.chapters
.append(ch
)
231 return me
.season
._eplabel(me
.i
, me
.neps
, me
.title
)
233 class Season (object):
234 def __init__(me
, playlist
, title
, i
, implicitp
= False):
235 me
.playlist
= playlist
236 me
.title
, me
.i
= title
, i
237 me
.implicitp
= implicitp
239 def add_episode(me
, j
, neps
, title
, src
, tno
):
240 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
241 me
.episodes
.append(ep
)
243 def _eplabel(me
, i
, neps
, title
):
244 if neps
== 1: epname
= me
.playlist
.epname
; epn
= "%d" % i
245 elif neps
== 2: epname
= me
.playlist
.epnames
; epn
= "%d, %d" %
(i
, i
+ 1)
246 else: epname
= me
.playlist
.epnames
; epn
= "%d–%d" %
(i
, i
+ neps
- 1)
248 if me
.implicitp
: return "%s %s" %
(epname
, epn
)
249 elif me
.title
is None: return "%s %d.%s" %
(epname
, me
.i
, epn
)
250 else: return "%s—%s %s" %
(me
.title
, epname
, epn
)
252 if me
.implicitp
: return "%s. %s" %
(epn
, title
)
253 elif me
.title
is None: return "%d.%s. %s" %
(me
.i
, epn
, title
)
254 else: return "%s—%s. %s" %
(me
.title
, epn
, title
)
256 class MovieSeason (object):
257 def __init__(me
, playlist
):
258 me
.playlist
= playlist
262 def add_episode(me
, j
, neps
, title
, src
, tno
):
263 if title
is None: raise ExpectedError("movie must have a title")
264 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
265 me
.episodes
.append(ep
)
267 def _eplabel(me
, i
, epn
, title
):
270 class Playlist (object):
274 me
.epname
, me
.epnames
= "Episode", "Episodes"
276 def add_season(me
, title
, i
, implicitp
= False):
277 season
= Season(me
, title
, i
, implicitp
)
278 me
.seasons
.append(season
)
282 season
= MovieSeason(me
)
283 me
.seasons
.append(season
)
288 for season
in me
.seasons
:
290 for i
, ep
in enumerate(season
.episodes
, 1):
292 f
.write("#EXTINF:0,,%s\n%s\n" %
(ep
.label(), ep
.url
))
294 for ch
in ep
.chapters
:
295 f
.write("#EXTINF:0,,%s: %s\n%s\n" %
296 (ep
.label(), ch
.title
, ch
.url
))
301 playlist
= Playlist()
302 season
, episode
, chapter
, ep_i
= None, None, None, 1
305 with
location(FileLocation(fn
, 0)) as floc
:
306 with
open(fn
, "r") as f
:
309 sline
= line
.lstrip()
310 if sline
== "" or sline
.startswith(";"): continue
312 if line
.startswith("!"):
315 check(cmd
is not None, "missing command")
319 check(v
is not None, "missing season number")
321 check(v
.rest() is None, "trailing junk")
322 season
= playlist
.add_movies()
326 season
= playlist
.add_season(title
, i
, implicitp
= False)
327 episode
= chapter
= None
331 check(ww
.rest() is None, "trailing junk")
332 season
= playlist
.add_movies()
333 episode
= chapter
= None
336 elif cmd
== "epname":
338 check(name
is not None, "missing episode name")
339 try: sep
= name
.index(":")
340 except ValueError: names
= name
+ "s"
341 else: name
, names
= name
[:sep
], name
[sep
+ 1:]
342 playlist
.epname
, playlist
.epnames
= name
, names
346 check(i
is not None, "missing episode number")
350 fn
= ww
.rest(); check(fn
is not None, "missing filename")
351 if fn
== "-": iso
= None
353 check(OS
.path
.exists(OS
.path
.join(ROOT
, fn
)),
354 "iso file `%s' not found" % fn
)
358 name
= ww
.nextword(); check(name
is not None, "missing name")
359 fn
= ww
.rest(); check(fn
is not None, "missing directory")
362 except KeyError: pass
364 vds
[name
] = VideoDir(fn
)
367 fn
= ww
.rest(); check(fn
is not None, "missing directory")
368 if fn
== "-": ads
= None
369 else: ads
= AudioDir(fn
)
375 raise ExpectedError("unknown command `%s'" % cmd
)
379 if not line
[0].isspace():
383 check(conf
is not None, "missing config")
384 i
, vdname
, neps
, fake_epi
= UNSET
, "-", 1, ep_i
385 for c
in conf
.split(","):
386 if c
.isdigit(): i
= int(c
)
387 elif c
== "-": i
= None
389 eq
= c
.find("="); check(eq
>= 0, "bad assignment `%s'" % c
)
390 k
, v
= c
[:eq
], c
[eq
+ 1:]
391 if k
== "vd": vdname
= v
392 elif k
== "n": neps
= getint(v
)
393 elif k
== "ep": fake_epi
= getint(v
)
394 else: raise ExpectedError("unknown setting `%s'" % k
)
397 check(i
is not UNSET
, "no title number")
399 season
= playlist
.add_season(None, 1, implicitp
= True)
402 check(ads
, "no title, but no audio directory")
403 check(season
.implicitp
, "audio source, but explicit season")
404 src
= ads
.episodes
[ep_i
]
410 check(vdname
in vds
, "title, but no iso or video directory")
411 src
= vds
[vdname
].seasons
[season
.i
].episodes
[ep_i
]
413 episode
= season
.add_episode(fake_epi
, neps
, title
, src
, i
)
420 check(episode
is not None, "no current episode")
421 check(episode
.source
.CHAPTERP
,
422 "episode source doesn't allow chapters")
423 if chapter
is None: j
= 1
425 chapter
= episode
.add_chapter(title
, j
)
429 ROOT
= "/mnt/dvd/archive/"
432 for f
in SYS
.argv
[1:]:
433 parse_list(f
).write(SYS
.stdout
)
434 except (ExpectedError
, IOError, OSError) as e
: