Added code to make errors show up as a virtual file.
[pyTivo/TheBayer.git] / plugins / dvdvideo / dvdfolder.py
blobc42c0c7abc37b0786acd586b6bd6e008fcd5be92
1 # Module: dvdfolder.py
2 # Author: Eric von Bayer
3 # Contact:
4 # Date: August 18, 2009
5 # Description:
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
23 # permission.
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.
36 import os
37 import re
38 from dvdtitlestream import DVDTitleStream
39 from ilvuhack import ComputeRealSectors
41 try:
42 os.SEEK_SET
43 except AttributeError:
44 os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3)
46 # Constants for various parts of the structure
47 IFO_TYPE_VMG = 0
48 IFO_TYPE_VTS = 1
49 DVD_BLOCK_LEN = 2048
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 ):
59 try:
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 )
64 except:
65 return None
67 def MatchAudioAttr( audio_attr, lang, chan ):
68 if lang != '*' and lang.lower() != audio_attr.LanguageCode().lower():
69 #print "Failed based on language"
70 return False
71 elif chan != '*' and int( chan ) != audio_attr.Channels():
72 #print "Failed based on channels"
73 return False
74 elif audio_attr.Coding() != "AC3":
75 #print "Failed based on coding"
76 return False
77 elif audio_attr.CodeExtensionValue() > 1:
78 #print "Failed based on extension", audio_attr.CodeExtensionValue()
79 return False
80 else:
81 return True
83 def BCD2Dec( bcd ):
84 return int( str( "%X" % bcd ) )
86 class DVDNotDVD(Exception):
87 def __init__( self, txt ):
88 self.__txt = txt
90 def __str__( self ):
91 return self.__txt
93 class DVDFormatError(Exception):
94 def __init__( self, txt ):
95 self.__txt = txt
97 def __str__( self ):
98 return self.__txt
100 ########################## Misc Utility Functions ##############################
102 class DVDFileHandle(object):
103 """Utility functions for reading DVD data structures"""
104 DVD_BLOCK_LEN = 2048L
105 __handle = None
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:
115 self.Seek( offset )
117 def IsOpen( self ):
118 return not self.__handle.closed
120 def Close( self ):
121 if self.__handle != None and not self.__handle.closed:
122 self.__handle.close()
124 def Handle( self ):
125 return self.__handle
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
135 if offset != None:
136 off += offset
137 self.Seek( off )
139 def Seek( self, offset ):
140 self.__handle.seek( offset )
142 def Tell( self ):
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
149 return sect, off
151 def Skip( self, bytes ):
152 self.__handle.seek( bytes, 1 )
154 def Read( self, bytes ):
155 return self.__handle.read( bytes )
157 def ReadU8( self ):
158 return ord(self.__handle.read(1))
160 def ReadU16( self ):
161 return ( ord(self.__handle.read(1)) << 8 ) | \
162 ord(self.__handle.read(1))
164 def ReadU32( self ):
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"""
174 __sec = 0
175 __frame_rate = 29.97
177 def __init__( self, sec_handle = 0 ):
178 if isinstance( sec_handle, file ):
179 self.Read( sec_handle )
181 def __iadd__( a, b ):
182 a.__sec += b.__sec
183 return a
185 def __lt__( a, b ):
186 return a.__sec < b.__sec
188 def __ge__( a, b ):
189 return a.__sec >= b.__sec or a == b
191 def __le__( a, b ):
192 return a.__sec <= b.__sec or a == b
194 def __eq__( a, b ):
195 return abs( a.__sec - b.__sec ) < 0.04
197 def __ne__( a, b ):
198 return not ( a == b )
200 def __str__( self ):
201 return "%d:%02d:%06.3f" % ( \
202 int( self.__sec / 3600 ),
203 int( ( self.__sec % 3600 ) / 60 ),
204 self.__sec % 60 )
206 def SetFrameRate( self, fr ):
207 self.__frame_rate = fr
209 def Read( self, handle ):
210 try:
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 )
218 except:
219 self.__frame_rate = 29.97
220 self.__sec = 0
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
233 def Secs( self ):
234 return self.__sec
236 def MSecs( self ):
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 ]
259 else:
260 return [ "720x576", "704x576", "352x576", "352x288" ] \
261 [ ( self.__data & 0x38 ) >> 3 ]
263 def Width( self ):
264 return [ 720, 704, 352, 352 ] \
265 [ ( self.__data & 0x38 ) >> 3 ]
267 def Height( self ):
268 if ( self.__data & 0x3000 ) == 0:
269 return [ 480, 480, 480, 240 ] \
270 [ ( self.__data & 0x38 ) >> 3 ]
271 else:
272 return [ 576, 576, 576, 288 ] \
273 [ ( self.__data & 0x38 ) >> 3 ]
275 def Standard( self ):
276 return [ "NTSC", "PAL" ][ ( self.__data & 0x3000 ) >> 12 ]
278 def Coding( self ):
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
299 def Coding( self ):
300 return [ "AC3", "<unknown>", "MPEG-1", "MPEG-2", "LPCM", "<unknown>", "DTS", "<unknown>" ] \
301 [ ( self.__data & 0xE000 ) >> 13 ]
303 def LanguageCode( self ):
304 return self.__lang
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
319 def DRC( self ):
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 ]
326 else:
327 return 16
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 ) )
341 def Video( self ):
342 return self.__video
344 def AudioList( self ):
345 return self.__audio
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
355 try:
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
363 id = self.Read( 12 )
364 if id != "DVDVIDEO-VMG":
365 raise DVDFormatError( filename + "IFO file is not a VMG file" )
367 # Get the VOB, IFO, and BUP sectors
368 self.Seek( 0x0C )
369 self.__last_sector_bup = self.ReadU32()
370 self.Seek( 0x1C )
371 self.__last_sector_ifo = self.ReadU32()
372 self.Seek( 0xC0 )
373 self.__first_sector_menu = self.ReadU32()
375 # Read in the version as big endian
376 handle.seek( 0x20 )
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
383 self.Seek( 0x22 )
384 self.__vmg_category = self.ReadU32()
385 self.__num_vols = self.ReadU16()
386 self.__vol_num = self.ReadU16()
387 self.__side_id = self.ReadU8()
388 self.Seek( 0x3E )
389 self.__num_vts = self.ReadU16()
390 self.__provider_id = self.Read(32)
391 self.Seek( 0xC4 )
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()
398 self.Skip(6)
399 self.__title_info = list()
401 # Read in the title information
402 for tn in range(self.__num_titles):
403 title = dict()
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 )
420 self.Close()
421 self.__valid = True
423 except:
424 self.Close()
425 self.__valid = False
426 raise
428 def Menu( self ):
429 return self.__menu
431 def Valid( self ):
432 return self.__valid
434 def NumVolumes( self ):
435 return self.__num_vols
437 def VolumeNum( self ):
438 return self.__vol_num
440 def SideID( self ):
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
458 try:
459 # Get our VTS number
460 match = PATTERN_VTS_IFO.match( os.path.basename( filename ) )
461 if match == None:
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
481 id = self.Read( 12 )
482 if id != "DVDVIDEO-VTS":
483 raise DVDFormatError( "Expected a VTS info file" )
485 # Get the VOB, IFO, and BUP sectors
486 self.Seek( 0x0C )
487 self.__last_sector_bup = self.ReadU32()
488 self.Seek( 0x1C )
489 self.__last_sector_ifo = self.ReadU32()
490 self.Seek( 0xC0 )
491 self.__first_sector_menu = self.ReadU32()
492 self.__first_sector_title = self.ReadU32()
494 # Read in the version as big endian
495 handle.seek( 0x20 )
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
505 handle.seek( 0xCC )
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()
515 self.Skip(2)
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
520 info = dict()
521 info['vts_number'] = self.__num
522 info['number'] = pgc+1
523 t1 = self.ReadU8()
524 info['title_number'] = t1 & 0x3F
525 info['entry'] = ( (t1 & 0x80) == 0x80 )
526 self.Skip(1)
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
533 # up in other ways.
534 if info['entry'] == False:
535 continue
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
549 self.Skip(4)
551 # Read the list of valid audio streams
552 astrs = list()
553 for num in range(8):
554 strnum = self.ReadU8()
555 self.Skip(1)
556 if strnum & 0x80:
557 astrs.append( strnum & 0x7 )
558 info[ 'audio_stream_nums' ] = astrs
560 # Default these to False and mark true if we find a cell
561 # that matches.
562 info['ilvu'] = False
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'] ):
573 t1 = self.ReadU8()
574 t2 = self.ReadU8()
575 if ( t1 & 0xF0 ) != 0x00:
576 info['angles'] = True
578 self.Skip(6)
579 s = self.ReadU32()
580 i = self.ReadU32()
581 self.Skip(4)
582 e = self.ReadU32()
583 if i != 0:
584 info['ilvu'] = True
586 #if i == 0 or ( t1 & 0xF0 ) != 0x00:
587 if i == 0:
588 ts.AddSectors( s, e )
589 else:
590 try:
591 for [sr,er] in ComputeRealSectors( s, e, *ts.files() ):
592 ts.AddSectors( sr, er )
593 except:
594 raise DVDFormatError( "Error processing ILVU block within title set "+self.__num+", program chain "+info["number"] )
596 info['stream'] = ts
598 # Add all the information to the PGC list
599 self.__pgc_info.append( info )
601 # Return to the PGC table
602 self.Seek( cur_off )
604 self.__title.Video().backdoor_SetFrameRate( \
605 self.__pgc_info[0]['playtime'].FrameRate() )
607 self.Close()
608 self.__valid = True
610 except:
611 self.Close()
612 self.__valid = False
613 raise
615 def Name( self ):
616 return self.__filename
618 def Size( self ):
619 return os.path.getsize( self.__filename )
621 def Sectors( self ):
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
633 def VOBSize( self ):
634 return self.VOBSectors() * self.DVD_BLOCK_LEN
636 def NumPGCs( self ):
637 return self.__num_pgc
639 def PGCInfo( self, num ):
640 if num < 1 or num > len(self.__pgc_info):
641 return None
642 return self.__pgc_info[ num-1 ]
644 def Version( self ):
645 return self.__version
647 def Menu( self ):
648 return self.__menu
650 def Title( self ):
651 return self.__title
653 def Valid( self ):
654 return self.__valid
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 ):
661 self.__valid = False
662 try:
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() ) )
670 self.__valid = True
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] )
677 except:
678 raise
680 def Valid( self ):
681 return self.__valid
683 def TitleNumber( self ):
684 return self.__tinfo['number']
686 def VTS( self ):
687 return self.__vts
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( ',' )
707 for part in parts:
708 #print "Part:", part
709 elems = part.split( ':', 1 )
711 if len(elems) >= 2:
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()
720 def Stream( self ):
721 return self.__pgcinfo['stream']
723 def Size( self ):
724 return self.__pgcinfo['stream'].size()
726 def Time( self ):
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 ):
735 self.__valid = False
736 self.__disc_ifo = None
737 self.__disc_vob = None
738 self.__titles = list()
739 self.__main_title = None
740 self.__deferred = False
741 self.__error = None
743 try:
744 # Make sure we have a directory
745 if not os.path.isdir( path ):
746 raise DVDNotDVD( "VIDEO_TS not located in "+path )
747 self.__path = 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
762 if not defer:
763 self.__load_full()
765 # We're valid, mark ourself as such
766 self.__valid = True
768 except DVDFormatError, err:
769 self.__error = str(err)
770 self.__valid = False
771 raise
773 except DVDNotDVD:
774 self.__valid = False
775 raise
777 except:
778 self.__error = "Unknown internal error."
779 self.__valid = False
780 raise
782 def __load_full( self ):
783 if self.__valid == False or self.__deferred == False:
784 return
786 self.__deferred = False
788 try:
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
801 if dfile == None:
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 ):
815 try:
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:
825 pass
827 if len(self.__titles) < 1:
828 raise DVDFormatError( "No valid titles present" )
830 except DVDFormatError, err:
831 self.__error = str(err)
832 self.__valid = False
833 raise
835 except DVDNotDVD:
836 self.__valid = False
837 raise
839 except:
840 self.__error = "Unknown internal error."
841 self.__valid = False
842 raise
844 def Defered( self ):
845 return self.__deferred
847 def Folder( self ):
848 return self.__path
850 def MenuIFO( self ):
851 self.__load_full()
852 return self.__vmg_ifo
854 # Count up the usable titles
855 def NumUsefulTitles( self, title_threshold = 30.0 ):
856 self.__load_full()
857 if self.__valid:
858 utitles = 0
859 for title in self.TitleList():
860 if title.Time() > title_threshold:
861 utitles += 1
863 return utitles
864 return 0
866 def TitleList( self ):
867 self.__load_full()
868 return self.__titles
870 def MainTitle( self ):
871 self.__load_full()
872 return self.__main_title
874 def Valid( self ):
875 self.__load_full()
876 return self.__valid
878 def QuickValid( self ):
879 return self.__valid
881 def HasErrors( self ):
882 return self.__error != None
884 def Error( self ):
885 return self.__error