2 # Author: Eric von Bayer
4 # Date: August 18, 2009
6 # Routines for reading data out of a DVD Folder. This in no way is an
7 # exhaustive implementation, but merely to give a good platform for
8 # some automated DVD processing.
10 # Copyright (c) 2009, Eric von Bayer
11 # All rights reserved.
13 # Redistribution and use in source and binary forms, with or without
14 # modification, are permitted provided that the following conditions are met:
16 # * Redistributions of source code must retain the above copyright notice,
17 # this list of conditions and the following disclaimer.
18 # * Redistributions in binary form must reproduce the above copyright notice,
19 # this list of conditions and the following disclaimer in the documentation
20 # and/or other materials provided with the distribution.
21 # * The names of the contributors may not be used to endorse or promote
22 # products derived from this software without specific prior written
25 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 from dvdtitlestream
import DVDTitleStream
39 from ilvuhack
import ComputeRealSectors
43 except AttributeError:
44 os
.SEEK_SET
, os
.SEEK_CUR
, os
.SEEK_END
= range(3)
46 # Constants for various parts of the structure
51 # Patterns to match against for the various parts of the DVD
52 PATTERN_VIDEO_TS
= re
.compile( r
"(?i)VIDEO_TS$" )
53 PATTERN_VIDEO_TS_IFO
= re
.compile( r
"(?i)VIDEO_TS.IFO$" )
54 PATTERN_VIDEO_TS_VOB
= re
.compile( r
"(?i)VIDEO_TS.VOB$" )
55 PATTERN_VTS_IFO
= re
.compile( r
"(?i)VTS_([0-9]{2})_0.IFO$" )
56 PATTERN_VTS_VOB
= re
.compile( r
"(?i)VTS_([0-9]{2})_([0-9]).VOB$" )
58 def FindDOSFilename( path
, dosname
):
60 if os
.path
.isdir( path
):
61 for f
in os
.listdir( path
):
62 if f
.upper() == dosname
:
63 return os
.path
.join( path
, f
)
67 def MatchAudioAttr( audio_attr
, lang
, chan
):
68 if lang
!= '*' and lang
.lower() != audio_attr
.LanguageCode().lower():
69 #print "Failed based on language"
71 elif chan
!= '*' and int( chan
) != audio_attr
.Channels():
72 #print "Failed based on channels"
74 elif audio_attr
.Coding() != "AC3":
75 #print "Failed based on coding"
77 elif audio_attr
.CodeExtensionValue() > 1:
78 #print "Failed based on extension", audio_attr.CodeExtensionValue()
84 return int( str( "%X" % bcd
) )
86 class DVDNotDVD(Exception):
87 def __init__( self
, txt
):
93 class DVDFormatError(Exception):
94 def __init__( self
, txt
):
100 ########################## Misc Utility Functions ##############################
102 class DVDFileHandle(object):
103 """Utility functions for reading DVD data structures"""
104 DVD_BLOCK_LEN
= 2048L
107 def __init__( self
, handle
, offset
= -1 ):
108 if not isinstance( handle
, file ):
109 raise "handle was not a file"
111 self
.__handle
= handle
112 self
.__sector
_offset
= 0
114 if self
.IsOpen() and offset
>= 0:
118 return not self
.__handle
.closed
121 if self
.__handle
!= None and not self
.__handle
.closed
:
122 self
.__handle
.close()
127 def SetSectorOffset( self
, offset
):
128 self
.__sector
_offset
= offset
130 def SectorOffset( self
):
131 return self
.__sector
_offset
133 def SectorSeek( self
, sector
, offset
= None ):
134 off
= ( sector
- self
.__sector
_offset
) * DVD_BLOCK_LEN
139 def Seek( self
, offset
):
140 self
.__handle
.seek( offset
)
143 return self
.__handle
.tell()
145 def SectorTell( self
):
146 off
= self
.__handle
.tell()
147 sect
= int( off
/ DVD_BLOCK_LEN
)
148 off
= off
% DVD_BLOCK_LEN
151 def Skip( self
, bytes
):
152 self
.__handle
.seek( bytes
, 1 )
154 def Read( self
, bytes
):
155 return self
.__handle
.read( bytes
)
158 return ord(self
.__handle
.read(1))
161 return ( ord(self
.__handle
.read(1)) << 8 ) | \
162 ord(self
.__handle
.read(1))
165 return ( ord(self
.__handle
.read(1)) << 24 ) | \
166 ( ord(self
.__handle
.read(1)) << 16 ) | \
167 ( ord(self
.__handle
.read(1)) << 8 ) | \
168 ord(self
.__handle
.read(1))
170 ############################# IFOPlaybackTime ##################################
172 class IFOPlaybackTime(object):
173 """Playback Time Data in an IFO file"""
177 def __init__( self
, sec_handle
= 0 ):
178 if isinstance( sec_handle
, file ):
179 self
.Read( sec_handle
)
181 def __iadd__( a
, b
):
186 return a
.__sec
< b
.__sec
189 return a
.__sec
>= b
.__sec
or a
== b
192 return a
.__sec
<= b
.__sec
or a
== b
195 return abs( a
.__sec
- b
.__sec
) < 0.04
198 return not ( a
== b
)
201 return "%d:%02d:%06.3f" % ( \
202 int( self
.__sec
/ 3600 ),
203 int( ( self
.__sec
% 3600 ) / 60 ),
206 def SetFrameRate( self
, fr
):
207 self
.__frame
_rate
= fr
209 def Read( self
, handle
):
211 hrs
= BCD2Dec( ord(handle
.read(1)) )
212 mins
= BCD2Dec( ord(handle
.read(1)) )
213 secs
= BCD2Dec( ord(handle
.read(1)) )
214 fms
= ord(handle
.read(1))
215 self
.__frame
_rate
= [ 1000000, 25.0, 1000000, 29.97 ] \
216 [ ( fms
& 0xC0 ) >> 6 ]
217 fms
= BCD2Dec( fms
& 0x3F )
219 self
.__frame
_rate
= 29.97
221 raise DVDFormatError( "Improper time format" )
223 if self
.__frame
_rate
== 1000000:
224 print "Warning: Invalid Frame Rate flag, got " + \
225 str( ( fms
& 0xC0 ) >> 6 ) + " instead of 1 or 3."
227 self
.__sec
= ( fms
/ self
.__frame
_rate
) + secs
+ (mins
* 60) + (hrs
* 3600)
230 def FrameRate( self
):
231 return self
.__frame
_rate
237 return int( self
.__sec
* 1000 )
239 ############################## IFOVideoAttrs ###################################
241 class IFOVideoAttrs(DVDFileHandle
):
242 """Video Attributes in an IFO file"""
243 def __init__( self
, handle
, offset
):
244 DVDFileHandle
.__init
__( self
, handle
, offset
)
245 self
.__data
= self
.ReadU16()
246 self
.__frame
_rate
= 29.97
248 def backdoor_SetFrameRate( self
, rate
):
249 self
.__frame
_rate
= rate
251 def AspectRatio( self
):
252 return [ "4:3", "16:9", "<unknown>", "16:9" ] \
253 [ ( self
.__data
& 0x0C00 ) >> 10 ]
255 def Resolution( self
):
256 if ( self
.__data
& 0x3000 ) == 0:
257 return [ "720x480", "704x480", "352x480", "352x240" ] \
258 [ ( self
.__data
& 0x38 ) >> 3 ]
260 return [ "720x576", "704x576", "352x576", "352x288" ] \
261 [ ( self
.__data
& 0x38 ) >> 3 ]
264 return [ 720, 704, 352, 352 ] \
265 [ ( self
.__data
& 0x38 ) >> 3 ]
268 if ( self
.__data
& 0x3000 ) == 0:
269 return [ 480, 480, 480, 240 ] \
270 [ ( self
.__data
& 0x38 ) >> 3 ]
272 return [ 576, 576, 576, 288 ] \
273 [ ( self
.__data
& 0x38 ) >> 3 ]
275 def Standard( self
):
276 return [ "NTSC", "PAL" ][ ( self
.__data
& 0x3000 ) >> 12 ]
279 return [ "MPEG-1", "MPEG-2" ] [ ( self
.__data
& 0xC000 ) >> 14 ]
281 def FrameRate( self
):
282 return self
.__frame
_rate
284 ############################## IFOAudioAttrs ###################################
286 class IFOAudioAttrs(DVDFileHandle
):
287 """Audio Attributes in an IFO file"""
288 def __init__( self
, handle
, num
, offset
):
289 DVDFileHandle
.__init
__( self
, handle
, offset
)
290 self
.__data
= self
.ReadU16()
291 self
.__lang
= self
.Read(2)
292 self
.__lang
_ext
= self
.Read(1)
293 self
.__code
_ext
= self
.ReadU8()
294 self
.__unk
= self
.Read(1)
295 self
.__mode
= self
.ReadU8()
296 self
.__stream
_id
= [ 0x80, 0, 0xC0, 0xC0, 0xA0, 0, 0x88, 0 ] \
297 [ ( self
.__data
& 0xE000 ) >> 13 ] + num
300 return [ "AC3", "<unknown>", "MPEG-1", "MPEG-2", "LPCM", "<unknown>", "DTS", "<unknown>" ] \
301 [ ( self
.__data
& 0xE000 ) >> 13 ]
303 def LanguageCode( self
):
306 def CodeExtension( self
):
307 return [ "Unspecified", "Normal", "For the Blind", "Director's Comments", \
308 "Alternate Director's Comments" ] [ self
.__code
_ext
]
310 def CodeExtensionValue( self
):
311 return self
.__code
_ext
313 def StreamID( self
):
314 return self
.__stream
_id
316 def Channels( self
):
317 return ( self
.__data
& 0x7 ) + 1
320 cmode
= ( self
.__data
& 0xE000 ) >> 13
321 return ( cmode
== 2 or cmode
== 3 ) and ( self
.__data
& 0xC0 ) == 0xC0
323 def Quantization( self
):
324 if ( ( self
.__data
& 0xE000 ) >> 13 ) == 4:
325 return [ 16, 20, 24, 0 ] [ ( self
.__data
& 0xC0 ) >> 6 ]
329 ################################ IFOAVAttrs ####################################
331 class IFOAVAttrs(DVDFileHandle
):
332 """Audio/Video Attributes in an IFO file"""
333 def __init__( self
, handle
, offset
):
334 DVDFileHandle
.__init
__( self
, handle
, offset
)
335 self
.__video
= IFOVideoAttrs( self
.Handle(), offset
)
336 self
.__audio
_streams
= self
.ReadU16()
337 self
.__audio
= list()
338 for stm
in range(self
.__audio
_streams
):
339 self
.__audio
.append( IFOAudioAttrs( self
.Handle(), stm
, offset
+ 4 + 8*stm
) )
344 def AudioList( self
):
347 ################################# IFOFile ######################################
349 class IFOVMGFile(DVDFileHandle
):
350 """IFO VMG File From a DVD"""
351 def __init__( self
, filename
):
352 self
.__filename
= filename
353 self
.__time
= IFOPlaybackTime()
354 self
.__frame
_rate
= 29.97
356 # Open the file through our parent
357 handle
= open( filename
, "rb" )
358 DVDFileHandle
.__init
__( self
, handle
, 0x0 )
359 if not self
.IsOpen():
360 raise DVDFormatError( "Couldn't open "+filename
)
362 # Make sure we're a VMG IFO file
364 if id != "DVDVIDEO-VMG":
365 raise DVDFormatError( filename
+ "IFO file is not a VMG file" )
367 # Get the VOB, IFO, and BUP sectors
369 self
.__last
_sector
_bup
= self
.ReadU32()
371 self
.__last
_sector
_ifo
= self
.ReadU32()
373 self
.__first
_sector
_menu
= self
.ReadU32()
375 # Read in the version as big endian
377 self
.__version
= self
.ReadU32()
379 # Get the A/V Attributes for the menu
380 self
.__menu
= IFOAVAttrs( self
.Handle(), 0x100 )
382 # Read the simple header information
384 self
.__vmg
_category
= self
.ReadU32()
385 self
.__num
_vols
= self
.ReadU16()
386 self
.__vol
_num
= self
.ReadU16()
387 self
.__side
_id
= self
.ReadU8()
389 self
.__num
_vts
= self
.ReadU16()
390 self
.__provider
_id
= self
.Read(32)
392 self
.__vmg
_tt
_srpt
= self
.ReadU32()
394 # Read the critical bits of the Table of Titles structure
395 self
.SectorSeek( self
.__vmg
_tt
_srpt
)
396 tt_srpt_offset
= self
.Tell()
397 self
.__num
_titles
= self
.ReadU16()
399 self
.__title
_info
= list()
401 # Read in the title information
402 for tn
in range(self
.__num
_titles
):
404 title
['number'] = tn
+1
405 title
['type'] = self
.ReadU8()
406 title
['angles'] = self
.ReadU8()
407 title
['chapters'] = self
.ReadU16()
408 title
['parental'] = self
.ReadU16()
409 title
['vts_num'] = self
.ReadU8()
410 title
['vts_pgc_num'] = self
.ReadU8()
411 title
['vts_ifo_sector'] = self
.ReadU32()
413 # Make sure the information was "sane"
414 assert title
['vts_num'] <= 99, DVDFormatError( "Title "+str(tn
)+" has a vts_num > 99" )
415 assert title
['vts_pgc_num'] <= 99, DVDFormatError( "Title "+str(tn
)+" has a vts_pgc_num > 99" )
417 # Add the title information
418 self
.__title
_info
.append( title
)
434 def NumVolumes( self
):
435 return self
.__num
_vols
437 def VolumeNum( self
):
438 return self
.__vol
_num
441 return self
.__side
_id
443 def NumVTSes( self
):
444 return self
.__num
_vts
446 def NumTitles( self
):
447 return self
.__num
_titles
449 def TitleInfo( self
, tnum
):
450 return self
.__title
_info
[tnum
-1]
452 class IFOVTSFile(DVDFileHandle
):
453 """IFO VTS File From a DVD"""
454 def __init__( self
, filename
):
455 self
.__filename
= filename
456 self
.__time
= IFOPlaybackTime()
457 self
.__frame
_rate
= 29.97
460 match
= PATTERN_VTS_IFO
.match( os
.path
.basename( filename
) )
462 raise DVDFormatError( "Not a valid VTS file" )
463 self
.__num
= int( match
.group(1) )
465 # Read in a list of all the VTS VOB files
466 self
.__vob
_files
= list()
467 path
= os
.path
.dirname( self
.__filename
)
468 for fn
in os
.listdir( path
):
469 match
= PATTERN_VTS_VOB
.match( fn
)
470 if match
!= None and int(match
.group(1)) == self
.__num
and int(match
.group(2)) > 0:
471 self
.__vob
_files
.append( os
.path
.join( path
, fn
) )
472 self
.__vob
_files
.sort()
474 # Open the file through our parent
475 handle
= open( filename
, "rb" )
476 DVDFileHandle
.__init
__( self
, handle
, 0x0 )
477 if not self
.IsOpen():
478 raise DVDFormatError( "Can't open VTS info file" )
480 # Make sure we're a VMG IFO file
482 if id != "DVDVIDEO-VTS":
483 raise DVDFormatError( "Expected a VTS info file" )
485 # Get the VOB, IFO, and BUP sectors
487 self
.__last
_sector
_bup
= self
.ReadU32()
489 self
.__last
_sector
_ifo
= self
.ReadU32()
491 self
.__first
_sector
_menu
= self
.ReadU32()
492 self
.__first
_sector
_title
= self
.ReadU32()
494 # Read in the version as big endian
496 self
.__version
= self
.ReadU32()
498 # Get the A/V Attributes for the menu
499 self
.__menu
= IFOAVAttrs( self
.Handle(), 0x100 )
501 # Get the A/V Attributes for the title (if present)
502 self
.__title
= IFOAVAttrs( self
.Handle(), 0x200 )
504 # Read the program chain structure for playtimes and stream ids
506 self
.__vts
_pgci
_sector
= self
.ReadU32()
508 # Read the critical bits of the Program Chain structure
509 self
.SectorSeek( self
.__vts
_pgci
_sector
)
510 pgci_offset
= self
.Tell()
512 # Get the Program chain information
513 self
.__pgc
_info
= list()
514 self
.__num
_pgc
= self
.ReadU16()
516 self
.__pgc
_end
_off
= self
.ReadU32()
517 for pgc
in range(self
.__num
_pgc
):
519 # Read in the information in the program chain index
521 info
['vts_number'] = self
.__num
522 info
['number'] = pgc
+1
524 info
['title_number'] = t1
& 0x3F
525 info
['entry'] = ( (t1
& 0x80) == 0x80 )
527 info
['parental_ctl_mask'] = self
.ReadU16()
529 # Find the offset of the program chain
530 pgc_off
= self
.ReadU32()
532 # Ignore non entry program chains as they will be picked
534 if info
['entry'] == False:
537 # Save the current location and seek to the Program Chain
538 cur_off
= self
.Tell()
539 self
.Seek( pgci_offset
+ pgc_off
+ 2 )
541 # Read in the number of programs/chapters and cells
542 info
['programs'] = self
.ReadU8()
543 info
['cells'] = self
.ReadU8()
545 # Read in the playback time
546 info
['playtime'] = IFOPlaybackTime( handle
)
548 # Skip the prohibited ops
551 # Read the list of valid audio streams
554 strnum
= self
.ReadU8()
557 astrs
.append( strnum
& 0x7 )
558 info
[ 'audio_stream_nums' ] = astrs
560 # Default these to False and mark true if we find a cell
563 info
['angles'] = False
565 # Get the playback information table and seek to there
566 self
.Seek( pgci_offset
+ pgc_off
+ 0xE8 )
567 pgc_playback_off
= self
.ReadU16()
568 self
.Seek( pgci_offset
+ pgc_off
+ pgc_playback_off
)
570 # Walk the cells showing the info
571 ts
= DVDTitleStream( *self
.__vob
_files
)
572 for cn
in range( info
['cells'] ):
575 if ( t1
& 0xF0 ) != 0x00:
576 info
['angles'] = True
586 #if i == 0 or ( t1 & 0xF0 ) != 0x00:
588 ts
.AddSectors( s
, e
)
591 for [sr
,er
] in ComputeRealSectors( s
, e
, *ts
.files() ):
592 ts
.AddSectors( sr
, er
)
594 raise DVDFormatError( "Error processing ILVU block within title set "+self
.__num
+", program chain "+info
["number"] )
598 # Add all the information to the PGC list
599 self
.__pgc
_info
.append( info
)
601 # Return to the PGC table
604 self
.__title
.Video().backdoor_SetFrameRate( \
605 self
.__pgc
_info
[0]['playtime'].FrameRate() )
616 return self
.__filename
619 return os
.path
.getsize( self
.__filename
)
622 return self
.__last
_sector
_ifo
624 def VOBSectors( self
):
625 return self
.__last
_sector
_bup
- ( self
.__last
_sector
_ifo
* 2 )
627 def VOBFiles( self
):
628 return self
.__vob
_files
630 def BUPSectorOffset( self
):
631 return self
.__last
_sector
_bup
- self
.__last
_sector
_ifo
634 return self
.VOBSectors() * self
.DVD_BLOCK_LEN
637 return self
.__num
_pgc
639 def PGCInfo( self
, num
):
640 if num
< 1 or num
> len(self
.__pgc
_info
):
642 return self
.__pgc
_info
[ num
-1 ]
645 return self
.__version
656 ################################# DVDTitle #####################################
658 class DVDTitle(object):
659 """Information about a set of VOBs based on VMG/VTS info"""
660 def __init__( self
, tnum
, vmg_ifo
, vts_list
):
663 self
.__tinfo
= vmg_ifo
.TitleInfo( tnum
)
664 self
.__vts
= vts_list
[ self
.__tinfo
['vts_num']-1 ]
665 self
.__pgcinfo
= self
.__vts
.PGCInfo( self
.__tinfo
['vts_pgc_num'] )
667 if self
.__pgcinfo
== None:
668 raise DVDFormatError( "Title number: %d - PGC number %d in VTS %d is out of range (%d)" %
669 ( tnum
, self
.__tinfo
['vts_pgc_num'], self
.__tinfo
['vts_num'], self
.__vts
.NumPGCs() ) )
672 self
.__audio
_streams
= list()
673 vts_audio_streams
= self
.__vts
.Title().AudioList()
674 for asnum
in self
.__pgcinfo
['audio_stream_nums']:
675 self
.__audio
_streams
.append( vts_audio_streams
[asnum
] )
683 def TitleNumber( self
):
684 return self
.__tinfo
['number']
689 def VTSNumber( self
):
690 return self
.__tinfo
['vts_num']
692 def PGCNumber( self
):
693 return self
.__tinfo
['vts_pgc_num']
695 def HasAngles( self
):
696 return self
.__pgcinfo
['angles']
698 def HasInterleaved( self
):
699 return self
.__pgcinfo
['ilvu']
701 def AudioStreams( self
):
702 return self
.__audio
_streams
704 def FindBestAudioStreamID( self
, spec
):
705 #print "FindBestAudioStreamID( "+spec+" )"
706 parts
= spec
.split( ',' )
709 elems
= part
.split( ':', 1 )
712 for stream
in self
.__audio
_streams
:
713 #print elems[0], elems[1], "==?", stream.LanguageCode(),stream.Channels(), "(0x%02x)" % stream.StreamID()
714 if MatchAudioAttr( stream
, elems
[0], elems
[1] ):
715 return stream
.StreamID()
717 #print "Defaulted to", self.__audio_streams[0].LanguageCode(),self.__audio_streams[0].Channels(), "(0x%02x)" % self.__audio_streams[0].StreamID()
718 return self
.__audio
_streams
[0].StreamID()
721 return self
.__pgcinfo
['stream']
724 return self
.__pgcinfo
['stream'].size()
727 return self
.__pgcinfo
['playtime']
730 ###########################s###### DVDFolder ####################################
732 class DVDFolder(object):
733 """DVD Folder along with routines to read contents"""
734 def __init__( self
, path
, defer
= False ):
736 self
.__disc
_ifo
= None
737 self
.__disc
_vob
= None
738 self
.__titles
= list()
739 self
.__main
_title
= None
740 self
.__deferred
= False
744 # Make sure we have a directory
745 if not os
.path
.isdir( path
):
746 raise DVDNotDVD( "VIDEO_TS not located in "+path
)
749 # Find the sub VIDEO_TS folder
750 self
.__videots
_path
= FindDOSFilename( path
, "VIDEO_TS" )
751 if self
.__videots
_path
== None:
752 raise DVDNotDVD( "VIDEO_TS not located in "+path
)
754 # Find the top level IFO file
755 self
.__vmg
_ifo
_fn
= FindDOSFilename( self
.__videots
_path
, "VIDEO_TS.IFO" )
756 if self
.__vmg
_ifo
_fn
== None:
757 raise DVDFormatError( "Couldn't locate VIDEO_TS.IFO in "+self
.__videots
_path
)
759 # Defer most of the load if asked to, this lets the pages load faster,
760 # otherwise load it immediately
761 self
.__deferred
= True
765 # We're valid, mark ourself as such
768 except DVDFormatError
, err
:
769 self
.__error
= str(err
)
778 self
.__error
= "Unknown internal error."
782 def __load_full( self
):
783 if self
.__valid
== False or self
.__deferred
== False:
786 self
.__deferred
= False
789 # Read in the top level IFO
790 self
.__vmg
_ifo
= IFOVMGFile( self
.__vmg
_ifo
_fn
)
792 # Read in the list of VTS IFOs
793 self
.__vts
_list
= list()
794 for vts
in range( 1, self
.__vmg
_ifo
.NumVTSes()+1 ):
796 # Figure out the file name we're after
797 ufile
= "VTS_%02d_0.IFO" % vts
798 dfile
= FindDOSFilename( self
.__videots
_path
, ufile
)
800 # Make sure we got a file
802 raise DVDFormatError( "Couldn't find file "+ufile
+" in "+self
.__videots
_path
)
804 # Get the file and if it was valid, add it to our list
805 vts_ifo
= IFOVTSFile( dfile
)
806 if vts_ifo
.Valid() == False:
807 raise DVDFormatError( dfile
+" contained invalid format" )
808 self
.__vts
_list
.append( vts_ifo
)
810 # Walk all the titles assembling the data
811 self
.__titles
= list()
812 lt_time
= IFOPlaybackTime(0)
813 self
.__main
_title
= None
814 for tn
in range( 1, self
.__vmg
_ifo
.NumTitles()+1 ):
816 title
= DVDTitle( tn
, self
.__vmg
_ifo
, self
.__vts
_list
)
818 if title
.Time() > lt_time
:
819 lt_time
= title
.Time()
820 self
.__main
_title
= title
822 self
.__titles
.append( title
)
824 except DVDFormatError
:
827 if len(self
.__titles
) < 1:
828 raise DVDFormatError( "No valid titles present" )
830 except DVDFormatError
, err
:
831 self
.__error
= str(err
)
840 self
.__error
= "Unknown internal error."
845 return self
.__deferred
852 return self
.__vmg
_ifo
854 # Count up the usable titles
855 def NumUsefulTitles( self
, title_threshold
= 30.0 ):
859 for title
in self
.TitleList():
860 if title
.Time() > title_threshold
:
866 def TitleList( self
):
870 def MainTitle( self
):
872 return self
.__main
_title
878 def QuickValid( self
):
881 def HasErrors( self
):
882 return self
.__error
!= None