0b5b800ee21186945fe5c8c279d68d70fa746272
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
""" ^
142 (?: S (?P<si> \d+) (?: \. \ (?P<st> .*)—)? (?: D (?P<sdi> \d+))? |
149 _R_ISO_EP
= RX
.compile(r
""" ^
150 (?: S (?P<si> \d+) \ )?
151 E (?P<ei> \d+) (?: – (?P<ej> \d+))? $
154 def __init__(me
, dir):
156 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
161 path
= OS
.path
.join(dir, fn
)
162 if not fn
.endswith(".iso"): continue
163 m
= me
._R_ISO_PRE
.match(fn
)
166 i
= filter(m
.group("si"), int)
167 stitle
= m
.group("st")
168 check(i
is not None or stitle
is None,
169 "explicit season title without number in `%s'" % fn
)
171 if season
is None or i
!= season
.i
:
172 check(season
is None or i
== season
.i
+ 1,
174 (i
, season
is None and -1 or season
.i
+ 1))
175 check(i
not in seasons
, "season %d already seen" % i
)
176 seasons
[i
] = season
= VideoSeason(i
, stitle
)
178 check(stitle
== season
.title
,
179 "season title `%s' /= `%s'" %
(stitle
, season
.title
))
181 disc
= VideoDisc(path
)
183 any
, bad
= False, False
184 for eprange
in m
.group("eps").split(", "):
185 mm
= me
._R_ISO_EP
.match(eprange
)
186 if mm
is None: bad
= True; continue
187 i
= filter(mm
.group("si"), int)
190 except KeyError: ts
= seasons
[i
] = VideoSeason(i
, None)
192 ts
= season
= seasons
[1] = VideoSeason(1, None)
193 start
= filter(mm
.group("ei"), int)
194 end
= filter(mm
.group("ej"), int, start
)
195 for k
in range(start
, end
+ 1):
196 ts
.set_episode_disc(k
, disc
)
199 raise ExpectedError("bad ep list in `%s'", fn
)
202 class AudioDisc (Source
):
204 TITLEP
= CHAPTERP
= False
206 class AudioEpisode (Source
):
208 TITLEP
= CHAPTERP
= False
209 def __init__(me
, fn
, i
, *args
, **kw
):
210 super().__init__(fn
, *args
, **kw
)
213 class AudioDir (object):
215 _R_FLAC
= RX
.compile(r
""" ^
221 def __init__(me
, dir):
223 fns
= OS
.listdir(OS
.path
.join(ROOT
, dir))
228 path
= OS
.path
.join(dir, fn
)
229 if not fn
.endswith(".flac"): continue
230 m
= me
._R_FLAC
.match(fn
)
232 i
= filter(m
.group(1), int)
234 check(i
== last_i
+ 1, "episode %d /= %d" %
(i
, last_i
+ 1))
235 episodes
[i
] = AudioEpisode(path
, i
)
237 me
.episodes
= episodes
239 class Chapter (object):
240 def __init__(me
, episode
, title
, i
):
241 me
.title
, me
.i
= title
, i
242 me
.url
= episode
.source
.url(episode
.tno
, i
)
244 class Episode (object):
245 def __init__(me
, season
, i
, neps
, title
, src
, tno
= None):
247 me
.i
, me
.neps
, me
.title
= i
, neps
, title
249 me
.source
, me
.tno
= src
, tno
250 me
.url
= src
.url(tno
)
251 def add_chapter(me
, title
, j
):
252 ch
= Chapter(me
, title
, j
)
253 me
.chapters
.append(ch
)
256 return me
.season
._eplabel(me
.i
, me
.neps
, me
.title
)
258 class Season (object):
259 def __init__(me
, playlist
, title
, i
, implicitp
= False):
260 me
.playlist
= playlist
261 me
.title
, me
.i
= title
, i
262 me
.implicitp
= implicitp
264 def add_episode(me
, j
, neps
, title
, src
, tno
):
265 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
266 me
.episodes
.append(ep
)
268 def _eplabel(me
, i
, neps
, title
):
269 if neps
== 1: epname
= me
.playlist
.epname
; epn
= "%d" % i
270 elif neps
== 2: epname
= me
.playlist
.epnames
; epn
= "%d, %d" %
(i
, i
+ 1)
271 else: epname
= me
.playlist
.epnames
; epn
= "%d–%d" %
(i
, i
+ neps
- 1)
273 if me
.implicitp
: return "%s %s" %
(epname
, epn
)
274 elif me
.title
is None: return "%s %d.%s" %
(epname
, me
.i
, epn
)
275 else: return "%s—%s %s" %
(me
.title
, epname
, epn
)
277 if me
.implicitp
: return "%s. %s" %
(epn
, title
)
278 elif me
.title
is None: return "%d.%s. %s" %
(me
.i
, epn
, title
)
279 else: return "%s—%s. %s" %
(me
.title
, epn
, title
)
281 class MovieSeason (object):
282 def __init__(me
, playlist
):
283 me
.playlist
= playlist
287 def add_episode(me
, j
, neps
, title
, src
, tno
):
288 if title
is None: raise ExpectedError("movie must have a title")
289 ep
= Episode(me
, j
, neps
, title
, src
, tno
)
290 me
.episodes
.append(ep
)
292 def _eplabel(me
, i
, epn
, title
):
295 class Playlist (object):
299 me
.epname
, me
.epnames
= "Episode", "Episodes"
301 def add_season(me
, title
, i
, implicitp
= False):
302 season
= Season(me
, title
, i
, implicitp
)
303 me
.seasons
.append(season
)
307 season
= MovieSeason(me
)
308 me
.seasons
.append(season
)
313 for season
in me
.seasons
:
315 for i
, ep
in enumerate(season
.episodes
, 1):
317 f
.write("#EXTINF:0,,%s\n%s\n" %
(ep
.label(), ep
.url
))
319 for ch
in ep
.chapters
:
320 f
.write("#EXTINF:0,,%s: %s\n%s\n" %
321 (ep
.label(), ch
.title
, ch
.url
))
326 playlist
= Playlist()
327 season
, episode
, chapter
, ep_i
= None, None, None, 1
330 with
location(FileLocation(fn
, 0)) as floc
:
331 with
open(fn
, "r") as f
:
334 sline
= line
.lstrip()
335 if sline
== "" or sline
.startswith(";"): continue
337 if line
.startswith("!"):
340 check(cmd
is not None, "missing command")
344 check(v
is not None, "missing season number")
346 check(v
.rest() is None, "trailing junk")
347 season
= playlist
.add_movies()
351 season
= playlist
.add_season(title
, i
, implicitp
= False)
352 episode
= chapter
= None
356 check(ww
.rest() is None, "trailing junk")
357 season
= playlist
.add_movies()
358 episode
= chapter
= None
361 elif cmd
== "epname":
363 check(name
is not None, "missing episode name")
364 try: sep
= name
.index(":")
365 except ValueError: names
= name
+ "s"
366 else: name
, names
= name
[:sep
], name
[sep
+ 1:]
367 playlist
.epname
, playlist
.epnames
= name
, names
371 check(i
is not None, "missing episode number")
375 fn
= ww
.rest(); check(fn
is not None, "missing filename")
376 if fn
== "-": iso
= None
378 check(OS
.path
.exists(OS
.path
.join(ROOT
, fn
)),
379 "iso file `%s' not found" % fn
)
383 name
= ww
.nextword(); check(name
is not None, "missing name")
384 fn
= ww
.rest(); check(fn
is not None, "missing directory")
387 except KeyError: pass
389 vds
[name
] = VideoDir(fn
)
392 fn
= ww
.rest(); check(fn
is not None, "missing directory")
393 if fn
== "-": ads
= None
394 else: ads
= AudioDir(fn
)
400 raise ExpectedError("unknown command `%s'" % cmd
)
404 if not line
[0].isspace():
408 check(conf
is not None, "missing config")
409 i
, vdname
, neps
, fake_epi
= UNSET
, "-", 1, ep_i
410 for c
in conf
.split(","):
411 if c
.isdigit(): i
= int(c
)
412 elif c
== "-": i
= None
414 eq
= c
.find("="); check(eq
>= 0, "bad assignment `%s'" % c
)
415 k
, v
= c
[:eq
], c
[eq
+ 1:]
416 if k
== "vd": vdname
= v
417 elif k
== "n": neps
= getint(v
)
418 elif k
== "ep": fake_epi
= getint(v
)
419 else: raise ExpectedError("unknown setting `%s'" % k
)
422 check(i
is not UNSET
, "no title number")
424 season
= playlist
.add_season(None, 1, implicitp
= True)
427 check(ads
, "no title, but no audio directory")
428 check(season
.implicitp
, "audio source, but explicit season")
429 try: src
= ads
.episodes
[ep_i
]
431 raise ExpectedError("episode %d not found in audio dir `%s'" %
438 check(vdname
in vds
, "title, but no iso or video directory")
439 try: vdir
= vds
[vdname
]
441 raise ExpectedError("video dir label `%s' not set" % vdname
)
442 try: s
= vdir
.seasons
[season
.i
]
444 raise ExpectedError("season %d not found in video dir `%s'" %
445 (season
.i
, vdir
.dir))
446 try: src
= s
.episodes
[ep_i
]
448 raise ExpectedError("episode %d.%d not found in video dir `%s'" %
449 (season
.i
, ep_i
, vdir
.dir))
451 episode
= season
.add_episode(fake_epi
, neps
, title
, src
, i
)
453 ep_i
+= neps
; src
.nuses
+= neps
458 check(episode
is not None, "no current episode")
459 check(episode
.source
.CHAPTERP
,
460 "episode source doesn't allow chapters")
461 if chapter
is None: j
= 1
463 chapter
= episode
.add_chapter(title
, j
)
466 for vdir
in vds
.values():
467 for s
in vdir
.seasons
.values():
468 for d
in s
.episodes
.values():
470 for d
in sorted(discs
, key
= lambda d
: d
.fn
):
471 if d
.neps
!= d
.nuses
:
472 raise ExpectedError("disc `%s' has %d episodes, used %d times" %
473 (d
.fn
, d
.neps
, d
.nuses
))
477 ROOT
= "/mnt/dvd/archive/"
480 for f
in SYS
.argv
[1:]:
481 parse_list(f
).write(SYS
.stdout
)
482 except (ExpectedError
, IOError, OSError) as e
: