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
85 me
.used_titles
= dict()
86 me
.used_chapters
= set()
88 def url(me
, title
= None, chapter
= None):
90 if me
.TITLEP
: raise ExpectedError("missing title number")
91 if chapter
is not None:
92 raise ExpectedError("can't specify chapter without title")
95 raise ExpectedError("can't specify title with `%s'" % me
.fn
)
97 suffix
= "#%d" % title
99 raise ExpectedError("can't specify chapter with `%s'" % me
.fn
)
101 suffix
= "#%d:%d-%d:%d" %
(title
, chapter
, title
, chapter
)
102 if chapter
is not None: key
, set = (title
, chapter
), me
.used_chapters
103 else: key
, set = title
, me
.used_titles
106 raise ExpectedError("`%s' already used" % me
.fn
)
107 elif chapter
is None:
108 raise ExpectedError("`%s' title %d already used" %
(me
.fn
, title
))
110 raise ExpectedError("`%s' title %d chapter %d already used" %
111 (me
.fn
, title
, chapter
))
112 if chapter
is not None: me
.used_chapters
.add((title
, chapter
))
113 return me
.PREFIX
+ ROOT
+ urlencode(me
.fn
) + suffix
115 class VideoDisc (Source
):
117 TITLEP
= CHAPTERP
= True
119 def __init__(me
, fn
, *args
, **kw
):
120 super().__init__(fn
, *args
, **kw
)
123 class VideoSeason (object):
124 def __init__(me
, i
, title
):
128 def set_episode_disc(me
, i
, disc
):
130 raise ExpectedError("season %d episode %d already taken" %
(me
.i
, i
))
131 me
.episodes
[i
] = disc
; disc
.neps
+= 1
133 def some_group(m
, *gg
):
136 if s
is not None: return s
139 class VideoDir (object):
141 _R_ISO_PRE
= RX
.compile(r
""" ^
143 (?: \. \ (?P<st> .*) — (?: D \d+ \. \ )? |
149 (?: S \d+ \ )? E \d+ (?: – \d+)?
150 (?: , \ (?: S \d+ \ )? E \d+ (?: – \d+)?)*) |
151 (?P<epname> E \d+) \. \ .*)
155 _R_ISO_EP
= RX
.compile(r
""" ^
156 (?: S (?P<si> \d+) \ )?
157 E (?P<ei> \d+) (?: – (?P<ej> \d+))? $
160 def __init__(me
, dir):
162 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
167 path
= OS
.path
.join(dir, fn
)
168 if not fn
.endswith(".iso"): continue
169 m
= me
._R_ISO_PRE
.match(fn
)
171 #print(";; `%s' ignored" % path, file = SYS.stderr)
174 i
= filter(m
.group("si"), int)
175 stitle
= m
.group("st")
176 check(i
is not None or stitle
is None,
177 "explicit season title without number in `%s'" % fn
)
179 if season
is None or i
!= season
.i
:
180 check(season
is None or i
== season
.i
+ 1,
182 (i
, season
is None and -1 or season
.i
+ 1))
183 check(i
not in seasons
, "season %d already seen" % i
)
184 seasons
[i
] = season
= VideoSeason(i
, stitle
)
186 check(stitle
== season
.title
,
187 "season title `%s' /= `%s'" %
(stitle
, season
.title
))
189 disc
= VideoDisc(path
)
191 any
, bad
= False, False
192 epname
= m
.group("epname")
193 if epname
is not None: eplist
= [epname
]
194 else: eplist
= m
.group("eplist").split(", ")
195 for eprange
in eplist
:
196 mm
= me
._R_ISO_EP
.match(eprange
)
197 if mm
is None: bad
= True; continue
199 #print(";; `%s'" % path, file = SYS.stderr)
201 i
= filter(mm
.group("si"), int)
204 except KeyError: ts
= seasons
[i
] = VideoSeason(i
, None)
206 ts
= season
= seasons
[1] = VideoSeason(1, None)
207 start
= filter(mm
.group("ei"), int)
208 end
= filter(mm
.group("ej"), int, start
)
209 for k
in range(start
, end
+ 1):
210 ts
.set_episode_disc(k
, disc
)
211 #print(";;\tepisode %d.%d" % (ts.i, k), file = SYS.stderr)
212 if not any
: pass #print(";; `%s' ignored" % path, file = SYS.stderr)
213 elif bad
: raise ExpectedError("bad ep list in `%s'", fn
)
216 class AudioDisc (Source
):
218 TITLEP
= CHAPTERP
= False
220 class AudioEpisode (Source
):
222 TITLEP
= CHAPTERP
= False
223 def __init__(me
, fn
, i
, *args
, **kw
):
224 super().__init__(fn
, *args
, **kw
)
227 class AudioDir (object):
229 _R_FLAC
= RX
.compile(r
""" ^
235 def __init__(me
, dir):
237 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
242 path
= OS
.path
.join(dir, fn
)
243 if not fn
.endswith(".flac"): continue
244 m
= me
._R_FLAC
.match(fn
)
246 i
= filter(m
.group(1), int)
248 check(i
== last_i
+ 1, "episode %d /= %d" %
(i
, last_i
+ 1))
249 episodes
[i
] = AudioEpisode(path
, i
)
251 me
.episodes
= episodes
253 class Chapter (object):
254 def __init__(me
, episode
, title
, i
):
255 me
.title
, me
.i
= title
, i
256 me
.url
= episode
.source
.url(episode
.tno
, i
)
258 class Episode (object):
259 def __init__(me
, season
, i
, neps
, title
, src
, tno
= None):
261 me
.i
, me
.neps
, me
.title
= i
, neps
, title
263 me
.source
, me
.tno
= src
, tno
264 me
.url
= src
.url(tno
)
265 def add_chapter(me
, title
, j
):
266 ch
= Chapter(me
, title
, j
)
267 me
.chapters
.append(ch
)
270 return me
.season
._eplabel(me
.i
, me
.neps
, me
.title
)
272 class Season (object):
273 def __init__(me
, playlist
, title
, i
, implicitp
= False):
274 me
.playlist
= playlist
275 me
.title
, me
.i
= title
, i
276 me
.implicitp
= implicitp
278 def add_episode(me
, j
, neps
, title
, src
, tno
):
279 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
280 me
.episodes
.append(ep
)
282 def _eplabel(me
, i
, neps
, title
):
283 if neps
== 1: epname
= me
.playlist
.epname
; epn
= "%d" % i
284 elif neps
== 2: epname
= me
.playlist
.epnames
; epn
= "%d, %d" %
(i
, i
+ 1)
285 else: epname
= me
.playlist
.epnames
; epn
= "%d–%d" %
(i
, i
+ neps
- 1)
287 if me
.implicitp
: return "%s %s" %
(epname
, epn
)
288 elif me
.title
is None: return "%s %d.%s" %
(epname
, me
.i
, epn
)
289 else: return "%s—%s %s" %
(me
.title
, epname
, epn
)
291 if me
.implicitp
: return "%s. %s" %
(epn
, title
)
292 elif me
.title
is None: return "%d.%s. %s" %
(me
.i
, epn
, title
)
293 else: return "%s—%s. %s" %
(me
.title
, epn
, title
)
295 class MovieSeason (object):
296 def __init__(me
, playlist
):
297 me
.playlist
= playlist
301 def add_episode(me
, j
, neps
, title
, src
, tno
):
302 if title
is None: raise ExpectedError("movie must have a title")
303 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
304 me
.episodes
.append(ep
)
306 def _eplabel(me
, i
, epn
, title
):
309 class Playlist (object):
313 me
.epname
, me
.epnames
= "Episode", "Episodes"
315 def add_season(me
, title
, i
, implicitp
= False):
316 season
= Season(me
, title
, i
, implicitp
)
317 me
.seasons
.append(season
)
321 season
= MovieSeason(me
)
322 me
.seasons
.append(season
)
327 for season
in me
.seasons
:
329 for i
, ep
in enumerate(season
.episodes
, 1):
331 f
.write("#EXTINF:0,,%s\n%s\n" %
(ep
.label(), ep
.url
))
333 for ch
in ep
.chapters
:
334 f
.write("#EXTINF:0,,%s: %s\n%s\n" %
335 (ep
.label(), ch
.title
, ch
.url
))
340 playlist
= Playlist()
341 season
, episode
, chapter
, ep_i
= None, None, None, 1
344 with
location(FileLocation(fn
, 0)) as floc
:
345 with
open(fn
, "r") as f
:
348 sline
= line
.lstrip()
349 if sline
== "" or sline
.startswith(";"): continue
351 if line
.startswith("!"):
354 check(cmd
is not None, "missing command")
358 check(v
is not None, "missing season number")
360 check(v
.rest() is None, "trailing junk")
361 season
= playlist
.add_movies()
365 season
= playlist
.add_season(title
, i
, implicitp
= False)
366 episode
= chapter
= None
370 check(ww
.rest() is None, "trailing junk")
371 season
= playlist
.add_movies()
372 episode
= chapter
= None
375 elif cmd
== "epname":
377 check(name
is not None, "missing episode name")
378 try: sep
= name
.index(":")
379 except ValueError: names
= name
+ "s"
380 else: name
, names
= name
[:sep
], name
[sep
+ 1:]
381 playlist
.epname
, playlist
.epnames
= name
, names
385 check(i
is not None, "missing episode number")
389 fn
= ww
.rest(); check(fn
is not None, "missing filename")
390 if fn
== "-": iso
= None
392 check(OS
.path
.exists(OS
.path
.join(ROOT
, fn
)),
393 "iso file `%s' not found" % fn
)
397 name
= ww
.nextword(); check(name
is not None, "missing name")
398 fn
= ww
.rest(); check(fn
is not None, "missing directory")
401 except KeyError: pass
403 vds
[name
] = VideoDir(fn
)
406 fn
= ww
.rest(); check(fn
is not None, "missing directory")
407 if fn
== "-": ads
= None
408 else: ads
= AudioDir(fn
)
414 raise ExpectedError("unknown command `%s'" % cmd
)
418 if not line
[0].isspace():
422 check(conf
is not None, "missing config")
423 i
, vdname
, neps
, fake_epi
= UNSET
, "-", 1, ep_i
424 for c
in conf
.split(","):
425 if c
.isdigit(): i
= int(c
)
426 elif c
== "-": i
= None
428 eq
= c
.find("="); check(eq
>= 0, "bad assignment `%s'" % c
)
429 k
, v
= c
[:eq
], c
[eq
+ 1:]
430 if k
== "vd": vdname
= v
431 elif k
== "n": neps
= getint(v
)
432 elif k
== "ep": fake_epi
= getint(v
)
433 else: raise ExpectedError("unknown setting `%s'" % k
)
436 check(i
is not UNSET
, "no title number")
438 season
= playlist
.add_season(None, 1, implicitp
= True)
441 check(ads
, "no title, but no audio directory")
442 check(season
.implicitp
, "audio source, but explicit season")
443 try: src
= ads
.episodes
[ep_i
]
445 raise ExpectedError("episode %d not found in audio dir `%s'" %
452 check(vdname
in vds
, "title, but no iso or video directory")
453 try: vdir
= vds
[vdname
]
455 raise ExpectedError("video dir label `%s' not set" % vdname
)
456 try: s
= vdir
.seasons
[season
.i
]
458 raise ExpectedError("season %d not found in video dir `%s'" %
459 (season
.i
, vdir
.dir))
460 try: src
= s
.episodes
[ep_i
]
462 raise ExpectedError("episode %d.%d not found in video dir `%s'" %
463 (season
.i
, ep_i
, vdir
.dir))
465 episode
= season
.add_episode(fake_epi
, neps
, title
, src
, i
)
467 ep_i
+= neps
; src
.nuses
+= neps
472 check(episode
is not None, "no current episode")
473 check(episode
.source
.CHAPTERP
,
474 "episode source doesn't allow chapters")
475 if chapter
is None: j
= 1
477 chapter
= episode
.add_chapter(title
, j
)
480 for vdir
in vds
.values():
481 for s
in vdir
.seasons
.values():
482 for d
in s
.episodes
.values():
484 for d
in sorted(discs
, key
= lambda d
: d
.fn
):
485 if d
.neps
!= d
.nuses
:
486 raise ExpectedError("disc `%s' has %d episodes, used %d times" %
487 (d
.fn
, d
.neps
, d
.nuses
))
491 ROOT
= "/mnt/dvd/archive/"
494 for f
in SYS
.argv
[1:]:
495 parse_list(f
).write(SYS
.stdout
)
496 except (ExpectedError
, IOError, OSError) as e
: