From 47fef9f6d2028abad767d6f012105a880fbba5b2 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 03:17:19 -0600 Subject: [PATCH 01/21] combined mobileweb of lucas_nz fork and wmcbrine fork --- content/bkgd.png | Bin 0 -> 952 bytes content/iPhoneArrow.png | Bin 0 -> 997 bytes content/iPhoneToolbar.png | Bin 0 -> 2840 bytes content/main.css | 6 + content/main_mob.css | 155 ++++++++++++++++++++ content/mobile.css | 47 ++++++ content/pinstripes.png | Bin 0 -> 117 bytes httpserver.py | 8 +- plugins/settings/templates/settings.tmpl | 1 + plugins/togo/templates/npl.tmpl | 21 +++ plugins/togo/templates/npl_mob.tmpl | 153 +++++++++++++++++++ plugins/togo/togo.py | 15 +- plugins/video/templates/container_html.tmpl | 35 ++++- plugins/video/templates/container_mob.tmpl | 88 +++++++++++ plugins/video/video.py | 5 + templates/info_page.tmpl | 4 +- templates/info_page_mob.tmpl | 23 +++ 17 files changed, 555 insertions(+), 6 deletions(-) create mode 100644 content/bkgd.png create mode 100644 content/iPhoneArrow.png create mode 100644 content/iPhoneToolbar.png create mode 100644 content/main_mob.css create mode 100644 content/mobile.css create mode 100644 content/pinstripes.png create mode 100644 plugins/togo/templates/npl_mob.tmpl create mode 100644 plugins/video/templates/container_mob.tmpl create mode 100644 templates/info_page_mob.tmpl diff --git a/content/bkgd.png b/content/bkgd.png new file mode 100644 index 0000000000000000000000000000000000000000..2f83bfea720f1fef81ea6206806d6656e867d665 GIT binary patch literal 952 zcmeAS@N?(olHy`uVBq!ia0vp^fNn~YUU}gyL32}Y!;KBd@|ADfjU^E0qU-L15RjOeSEA?V8lq5UtY@lczPx4b8K5E=PZ!4!jo{=27j~Y- nN*;;^Qc@>QCUUf}FI~^4diqzyCk?;!NFRp}bs2k@2tT z4}PV>Y)Qr@o-&5^>czSzSDSx6S1{?v7F~y(HkWT_MMWBXZMec#{*?FjmX>clMdzF5 bv2q=Hoycwvw1L6X)z4*}Q$iB}Ad6`K literal 0 HcmV?d00001 diff --git a/content/iPhoneToolbar.png b/content/iPhoneToolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..fe61d20903c689d45f95740f263c1fa144898f14 GIT binary patch literal 2840 zcmV+z3+MESP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0000(Nkl_E)O!2~4dW-8AFQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jio`u#978H@CD~Lx<6>s|lboE)9K^uL#$fKtT%LaBX8=$M NgQu&X%Q~loCIB>$9?k#& literal 0 HcmV?d00001 diff --git a/httpserver.py b/httpserver.py index ad232b22..f5c2c583 100644 --- a/httpserver.py +++ b/httpserver.py @@ -250,10 +250,16 @@ def root_container(self): self.wfile.write(t) def infopage(self): + useragent = self.headers.getheader('User-Agent', '') self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() - t = Template(file=os.path.join(SCRIPTDIR, 'templates', + if useragent.lower().find('mobile') > 0: + t = Template(file=os.path.join(SCRIPTDIR, 'templates', + 'info_page_mob.tmpl'), + filter=EncodeUnicode) + else: + t = Template(file=os.path.join(SCRIPTDIR, 'templates', 'info_page.tmpl'), filter=EncodeUnicode) t.admin = '' diff --git a/plugins/settings/templates/settings.tmpl b/plugins/settings/templates/settings.tmpl index cf06daec..4f7bb0ee 100644 --- a/plugins/settings/templates/settings.tmpl +++ b/plugins/settings/templates/settings.tmpl @@ -79,6 +79,7 @@ pyTivo Web Configuration help

+

Home

diff --git a/plugins/togo/templates/npl.tmpl b/plugins/togo/templates/npl.tmpl index 11165983..d6dad510 100644 --- a/plugins/togo/templates/npl.tmpl +++ b/plugins/togo/templates/npl.tmpl @@ -8,6 +8,19 @@

pyTivo - ToGo - $escape($tname)

+

Home

+
#if $ItemStart > 0 #end if + ## Header Row + + + + + + + #set $i = 0 ## i variable is used to alternate colors of row ## loop through passed data printing row for each show or folder diff --git a/plugins/togo/templates/npl_mob.tmpl b/plugins/togo/templates/npl_mob.tmpl new file mode 100644 index 00000000..e4ca9537 --- /dev/null +++ b/plugins/togo/templates/npl_mob.tmpl @@ -0,0 +1,153 @@ + + + +pyTiVo - $escape($tname) + + + + + + +

pyTiVo

+
@@ -25,6 +38,14 @@ Back to Now Playing List
TitleSizeCapture Date
+ + + + + #if $ItemStart > 0 + + #end if + #set $i = 0 + ## i variable is used to alternate colors of row + ## loop through passed data printing row for each show or folder + #for $row in $data + #set $i += 1 + #set $j = $i%2 + + #if $row['ContentType'] == 'x-tivo-container/folder' + ## This is a folder + + + + #else + ## This is a show + + + + + #end if + + #end for + #if ($TotalItems - $ItemCount) > ($ItemStart + 1) + + #end if +
+ #set $Offset = -($ItemStart + 1) + #if $Offset < -($shows_per_page+1) + #set $Offset = -($shows_per_page+1) + #end if + Previous Page +
$row['Title']
$(row["TotalItems"])
+ #if 'episodeTitle' in $row + $escape($row['title']): $escape($row['episodeTitle']) + #else + $escape($row['title']) + #end if + #if 'description' in $row + $escape($row['description']) + #end if + #if 'displayMajorNumber' in $row and 'callsign' in $row + $row['displayMajorNumber'] $row['callsign'] + #end if + + Recorded on + $row['CaptureDate'] + #if 'Url' in $row and row['Url'] in $status + #set $this_status = $status[$row['Url']] + #if $this_status['running'] and $this_status['rate'] != "" +
+ #set $gb = '%.3f GB' % (float($this_status['size']) / (1024 ** 3)) + Transfering - $this_status['rate']
$gb + Stop Transfer +
+ #elif $this_status['running'] and $this_status['rate'] == "" +
+ Initiating Transfer
+ Please Wait +
+ #elif $this_status['error'] +
+ Error - $this_status['error']
+
+ #elif $this_status['finished'] +
+ Transfer Complete +
+ #elif $this_status['queued'] +
+ Queued: $queue.index($row['Url'])
+ Unqueue +
+ #end if + #end if +
+ #if 'CopyProtected' in $row and $row['CopyProtected'] == 'Yes' + + #elif 'Icon' in $row + + #if $row['Icon'] == 'urn:tivo:image:expires-soon-recording' + + #else if $row['Icon'] == 'urn:tivo:image:expired-recording' + + #else if $row['Icon'] == 'urn:tivo:image:save-until-i-delete-recording' + + #else if $row['Icon'] == 'urn:tivo:image:in-progress-recording' + + #end if + #end if + + #if 'Url' in $row and not ($row['Url'] in $status and ($status[$row['Url']]['running'] or $status[$row['Url']]['queued'])) and not ('CopyProtected' in $row and $row['CopyProtected'] == 'Yes') and not ('Icon' in $row and $row['Icon'] == 'urn:tivo:image:in-progress-recording') + + #end if +
+ #set $Offset = $shows_per_page - 1 + Next Page +
+

+ + + + Metadata
+#if $togo_mpegts == 'default' + Transfer as mpeg-ts
+#elif $togo_mpegts == 'enabled' + Transfer as mpeg-ts
+#end if +

+
+#if $has_tivodecode + Decrypt +#end if + +
+ + + diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index 7fd4143a..2e447037 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -43,8 +43,11 @@ and double check your tivo_mak setting.

""" # Preload the templates -tnname = os.path.join(SCRIPTDIR, 'templates', 'npl.tmpl') -NPL_TEMPLATE = file(tnname, 'rb').read() +def tmpl(name): + return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read() + +CONTAINER_TEMPLATE_MOBILE = tmpl('npl_mob.tmpl') +CONTAINER_TEMPLATE = tmpl('npl.tmpl') status = {} # Global variable to control download threads tivo_cache = {} # Cache of TiVo NPL @@ -82,6 +85,8 @@ def NPL(self, handler, query): folder = '' tivo_mak = config.get_server('tivo_mak') has_tivodecode = bool(config.get_bin('tivodecode')) + togo_mpegts = config.get_server('togo_mpegts', 'False').lower() + useragent = handler.headers.getheader('User-Agent', '') if 'TiVo' in query: tivoIP = query['TiVo'][0] @@ -174,7 +179,11 @@ def NPL(self, handler, query): FirstAnchor = '' cname = query['Container'][0].split('/')[0] - t = Template(NPL_TEMPLATE, filter=EncodeUnicode) + + if useragent.lower().find('mobile') > 0: + t = Template(CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) + else: + t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode) t.escape = escape t.quote = quote t.folder = folder diff --git a/plugins/video/templates/container_html.tmpl b/plugins/video/templates/container_html.tmpl index 1ab6682d..940e771f 100644 --- a/plugins/video/templates/container_html.tmpl +++ b/plugins/video/templates/container_html.tmpl @@ -8,7 +8,40 @@

pyTivo - Push - $escape($name)

+

Home

+ + ## Header Row + + + + + + + + #set $parent = '' + #set $folders = $name.split("/") + #set $current_folder = $folders.pop() + #set $parent = '/'.join($folders) + #if $parent != '' + + + + + #end if #set $i = 0 ## i variable is used to alternate colors of row ## loop through passed data printing row for each show or folder @@ -70,4 +103,4 @@

- + \ No newline at end of file diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl new file mode 100644 index 00000000..1149ff69 --- /dev/null +++ b/plugins/video/templates/container_mob.tmpl @@ -0,0 +1,88 @@ + + + +pyTivo - Push - $escape($name) + + + + + + +

pyTiVo

+
TitleSizeCapture Date
+ Up to Parent Folder +
+ #set $parent = '' + #set $folders = $name.split("/") + #set $current_folder = $folders.pop() + #set $parent = '/'.join($folders) + + + + + #set $i = 0 + ## i variable is used to alternate colors of row + ## loop through passed data printing row for each show or folder + #for $video in $videos + #set $i += 1 + #set $j = $i%2 + + #if $video.is_dir + ## This is a folder + + + + #else + ## This is a show + + + + #end if + + #end for +
$video.title
$video.total_items
+ #if $video.episodeTitle + $video.title: $video.episodeTitle + #else + $video.title + #end if + + #if $video.description + $video.description + #end if + #if $video.displayMajorNumbe and $video.callsign + $video.displayMajorNumber $video.callsign + #end if + Added on $video.textDate + + +
+
+ + + + +
+ + + diff --git a/plugins/video/video.py b/plugins/video/video.py index 55ca73bd..768b9471 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -34,6 +34,7 @@ def tmpl(name): return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read() +HTML_CONTAINER_TEMPLATE_MOBILE = tmpl('container_mob.tmpl') HTML_CONTAINER_TEMPLATE = tmpl('container_html.tmpl') XML_CONTAINER_TEMPLATE = tmpl('container_xml.tmpl') TVBUS_TEMPLATE = tmpl('TvBus.tmpl') @@ -293,6 +294,7 @@ def QueryContainer(self, handler, query): tsn = handler.headers.getheader('tsn', '') subcname = query['Container'][0] cname = subcname.split('/')[0] + useragent = handler.headers.getheader('User-Agent', '') if (not cname in handler.server.containers or not self.get_local_path(handler, query)): @@ -350,8 +352,11 @@ def QueryContainer(self, handler, query): videos.append(video) + logger.debug('mobileagent: %d useragent: %s' % (useragent.lower().find('mobile'), useragent.lower())) if not use_html: t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) + elif useragent.lower().find('mobile') > 0: + t = Template(HTML_CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) else: t = Template(HTML_CONTAINER_TEMPLATE, filter=EncodeUnicode) t.container = cname diff --git a/templates/info_page.tmpl b/templates/info_page.tmpl index 18609626..56c236a6 100644 --- a/templates/info_page.tmpl +++ b/templates/info_page.tmpl @@ -9,8 +9,10 @@

pyTivo

$admin +

$togo +

$shares -

+

diff --git a/templates/info_page_mob.tmpl b/templates/info_page_mob.tmpl new file mode 100644 index 00000000..d1b5bcc4 --- /dev/null +++ b/templates/info_page_mob.tmpl @@ -0,0 +1,23 @@ + + + +pyTivo + + + + +

pyTivo

+
+
+ $admin +
+
+ $togo +
+
+ $shares +
+
+ + From b735ed50c0d5c6cb8718d551365d43fe608363a6 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 03:46:32 -0600 Subject: [PATCH 02/21] missing &Format=text/html in folder pages --- templates/root_container.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/root_container.tmpl b/templates/root_container.tmpl index 0316293d..b68d709d 100644 --- a/templates/root_container.tmpl +++ b/templates/root_container.tmpl @@ -16,7 +16,7 @@ - /TiVoConnect?Command=QueryContainer&Container=$quote($name) + /TiVoConnect?Command=QueryContainer&Container=$quote($name)&Format=text/html $escape($details.content_type) From 2277dbf51841c0226521aa197e234c31ed66d260 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 04:01:57 -0600 Subject: [PATCH 03/21] missing &Format=text/html in various pages --- plugins/video/templates/container_html.tmpl | 2 +- plugins/video/templates/container_mob.tmpl | 2 +- plugins/video/templates/container_xml.tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/video/templates/container_html.tmpl b/plugins/video/templates/container_html.tmpl index 940e771f..cbe7a2e3 100644 --- a/plugins/video/templates/container_html.tmpl +++ b/plugins/video/templates/container_html.tmpl @@ -38,7 +38,7 @@ function toggle(source) { - Up to Parent Folder + Up to Parent Folder #end if diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl index 1149ff69..e0616270 100644 --- a/plugins/video/templates/container_mob.tmpl +++ b/plugins/video/templates/container_mob.tmpl @@ -29,7 +29,7 @@ function toggle(source) { #if $parent != '' - $escape($name) + $escape($name) #else
$escape($name)
#end if diff --git a/plugins/video/templates/container_xml.tmpl b/plugins/video/templates/container_xml.tmpl index d50db804..704af145 100644 --- a/plugins/video/templates/container_xml.tmpl +++ b/plugins/video/templates/container_xml.tmpl @@ -22,7 +22,7 @@ - /TiVoConnect?Command=QueryContainer&Container=$quote($name)/$quote($video.name) + /TiVoConnect?Command=QueryContainer&Container=$quote($name)/$quote($video.name)&Format=text/html x-tivo-container/folder From e9326c00bc9a279b1ff0843f3f2ae286dbd52cef Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 04:57:16 -0600 Subject: [PATCH 04/21] bug chasing mobileweb still wont display NPL page --- plugins/togo/templates/npl_mob.tmpl | 6 +++--- plugins/togo/togo.py | 2 +- plugins/video/templates/container_mob.tmpl | 4 ++-- templates/info_page.tmpl | 2 +- templates/info_page_mob.tmpl | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/togo/templates/npl_mob.tmpl b/plugins/togo/templates/npl_mob.tmpl index e4ca9537..4d2643c9 100644 --- a/plugins/togo/templates/npl_mob.tmpl +++ b/plugins/togo/templates/npl_mob.tmpl @@ -2,12 +2,13 @@ "http://www.w3.org/TR/html4/strict.dtd"> -pyTiVo - $escape($tname) +pyTivo - ToGo - $escape($tname)
+

pyTivo - ToGo -

-

pyTiVo

- + #else ## This is a show diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index 2e447037..c2ba2dc9 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -181,7 +181,7 @@ def NPL(self, handler, query): cname = query['Container'][0].split('/')[0] if useragent.lower().find('mobile') > 0: - t = Template(CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) + t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode) else: t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode) t.escape = escape diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl index e0616270..13b29f5a 100644 --- a/plugins/video/templates/container_mob.tmpl +++ b/plugins/video/templates/container_mob.tmpl @@ -45,9 +45,9 @@ function toggle(source) { #if $video.is_dir ## This is a folder - + - + #else ## This is a show - - + + #else ## This is a show diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl index 13b29f5a..5a6fe418 100644 --- a/plugins/video/templates/container_mob.tmpl +++ b/plugins/video/templates/container_mob.tmpl @@ -21,6 +21,18 @@ function toggle(source) { -->

pyTiVo

+
+

+ + + + +

+
$video.title $video.title
$video.total_items
diff --git a/templates/info_page.tmpl b/templates/info_page.tmpl index 56c236a6..67544a72 100644 --- a/templates/info_page.tmpl +++ b/templates/info_page.tmpl @@ -7,7 +7,7 @@

pyTivo

-
+
$admin

$togo diff --git a/templates/info_page_mob.tmpl b/templates/info_page_mob.tmpl index d1b5bcc4..68e576d2 100644 --- a/templates/info_page_mob.tmpl +++ b/templates/info_page_mob.tmpl @@ -9,7 +9,7 @@

pyTivo


-
+
$admin
From 6729920a0ed5d26e1a4f38092a382180b096a03b Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 16:32:02 -0600 Subject: [PATCH 05/21] functioning mobileUI, needs video plugin ui tweaked for visual and config page is non-mobile --- content/main_mob.css | 8 ++++---- plugins/togo/templates/npl_mob.tmpl | 30 ++++++++++++----------------- plugins/togo/togo.py | 2 +- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/content/main_mob.css b/content/main_mob.css index 30ce150d..1beb566a 100644 --- a/content/main_mob.css +++ b/content/main_mob.css @@ -19,7 +19,7 @@ table, td { } #title { margin: 0; - padding: 10px; + padding: 4px; line-height: 20px; font-size: 18px; font-weight: bold; @@ -46,9 +46,9 @@ table, td { margin-left:15px; } #pushmenu { - position: absolute; - top: 8px; - right:10px; + position: relative; + top: 0px; + right:0px; color:#FFFFFF; text-decoration:none; } diff --git a/plugins/togo/templates/npl_mob.tmpl b/plugins/togo/templates/npl_mob.tmpl index 4d2643c9..3d4c9a6a 100644 --- a/plugins/togo/templates/npl_mob.tmpl +++ b/plugins/togo/templates/npl_mob.tmpl @@ -8,7 +8,7 @@ -

pyTivo - ToGo -

+

pyTivo - ToGo

+
+

+ + + +#if $has_tivodecode + Decrypt with tivodecode
+#end if + Save metadata to .txt
+

+
#end if
-

- - - - Metadata
-#if $togo_mpegts == 'default' - Transfer as mpeg-ts
-#elif $togo_mpegts == 'enabled' - Transfer as mpeg-ts
-#end if -

-
-#if $has_tivodecode - Decrypt -#end if - -
diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index c2ba2dc9..2e447037 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -181,7 +181,7 @@ def NPL(self, handler, query): cname = query['Container'][0].split('/')[0] if useragent.lower().find('mobile') > 0: - t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode) + t = Template(CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) else: t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode) t.escape = escape From 87afb356872047cfee28d5e8a7010f428e080577 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 17:35:02 -0600 Subject: [PATCH 06/21] aesthetic fix to video page --- plugins/togo/templates/npl_mob.tmpl | 4 ++-- plugins/video/templates/container_mob.tmpl | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/togo/templates/npl_mob.tmpl b/plugins/togo/templates/npl_mob.tmpl index 3d4c9a6a..552a63dc 100644 --- a/plugins/togo/templates/npl_mob.tmpl +++ b/plugins/togo/templates/npl_mob.tmpl @@ -62,8 +62,8 @@ function toggle(source) { #if $row['ContentType'] == 'x-tivo-container/folder' ## This is a folder
$row['Title']
$(row["TotalItems"])
$(row["TotalItems"])
#set $parent = '' #set $folders = $name.split("/") @@ -73,16 +85,6 @@ function toggle(source) { #end for
-
- - - - -
From 4b15424fd957ef741e941bc296df80efb410dc8f Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 21 Jan 2012 19:54:45 -0600 Subject: [PATCH 07/21] merged mobileui into master --- plugins/settings/help.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index 14440514..7282dd4f 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -663,3 +663,4 @@ be available! You'll have to remove it from pyTivo.conf with a text editor. Example Settings: True/False Available In: Server + From a59ade8007b87bd64cf6a2c0522216f1839e76c3 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sun, 5 Feb 2012 20:13:08 -0600 Subject: [PATCH 08/21] Added &Format=text/html to title/capture date links --- plugins/video/templates/container_html.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/video/templates/container_html.tmpl b/plugins/video/templates/container_html.tmpl index c8fc55b5..7c5e6bae 100644 --- a/plugins/video/templates/container_html.tmpl +++ b/plugins/video/templates/container_html.tmpl @@ -26,9 +26,9 @@ function toggle(source) { - Title + Title Size - Capture Date + Capture Date #set $parent = '' #set $folders = $name.split("/") From 4561da7d8654c7180d951543af525df6391e1f0a Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 18 Feb 2012 03:22:26 -0600 Subject: [PATCH 09/21] Added an r to the end of $video.displayMajorNumbe --- plugins/video/templates/container_mob.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl index 5a6fe418..312afc2f 100644 --- a/plugins/video/templates/container_mob.tmpl +++ b/plugins/video/templates/container_mob.tmpl @@ -72,7 +72,7 @@ function toggle(source) { #if $video.description $video.description #end if - #if $video.displayMajorNumbe and $video.callsign + #if $video.displayMajorNumber and $video.callsign $video.displayMajorNumber $video.callsign #end if Added on $video.textDate From 8a81e61f8ffe32eba57e021b7558e1c120bff885 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 18 Feb 2012 03:53:41 -0600 Subject: [PATCH 10/21] Added dvdvideo plugin code to httpserver.py --- httpserver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httpserver.py b/httpserver.py index 2a84d085..ae14fe10 100644 --- a/httpserver.py +++ b/httpserver.py @@ -291,7 +291,8 @@ def infopage(self): 'Command=NPL&Container=' + quote(section) + '&TiVo=' + config.tivos[tsn] + '">' + escape(config.tivo_names[tsn]) + '
') - elif plugin_type == 'video' and t.shares: + elif ( plugin_type == 'video' or plugin_type == 'dvdvideo' ) \ + and t.shares: t.shares += ('' + From 61ca4482f6c25ec21647b3e4e2f668766262351c Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Tue, 20 Mar 2012 13:52:41 -0500 Subject: [PATCH 11/21] Merged recent iluvatar branch --- beacon.py | 10 +- config.py | 163 +++++++++----- metadata.py | 248 ++++++++++++++++++++- plugins/music/music.py | 4 +- plugins/photo/photo.py | 2 +- plugins/settings/help.txt | 53 ++++- plugins/settings/settings.py | 10 +- plugins/togo/templates/npl.tmpl | 5 + plugins/togo/templates/npl_mob.tmpl | 5 + plugins/togo/togo.py | 33 ++- plugins/video/templates/container_xml.tmpl | 14 +- plugins/video/transcode.py | 219 ++++++++++++++---- plugins/video/video.py | 35 ++- plugins/webvideo/webvideo.py | 2 +- pyTivo.conf.dist | 3 + pyTivo.py | 1 + 16 files changed, 655 insertions(+), 152 deletions(-) diff --git a/beacon.py b/beacon.py index 13c6613c..dbcbd31e 100644 --- a/beacon.py +++ b/beacon.py @@ -12,6 +12,8 @@ from plugin import GetPlugin SHARE_TEMPLATE = '/TiVoConnect?Command=QueryContainer&Container=%s' +PLATFORM_MAIN = 'pyTivo' +PLATFORM_VIDEO = 'pc' # For the nice icon class ZCListener: def __init__(self, names): @@ -36,10 +38,14 @@ def __init__(self, logger): for section, settings in config.getShares(): ct = GetPlugin(settings['type']).CONTENT_TYPE if ct.startswith('x-container/'): + if 'video' in ct: + platform = PLATFORM_VIDEO + else: + platform = PLATFORM_MAIN logger.info('Registering: %s' % section) self.share_names.append(section) desc = {'path': SHARE_TEMPLATE % quote(section), - 'platform': 'pc', 'protocol': 'http'} + 'platform': platform, 'protocol': 'http'} tt = ct.split('/')[1] info = Zeroconf.ServiceInfo('_%s._tcp.local.' % tt, '%s._%s._tcp.local.' % (section, tt), @@ -108,7 +114,7 @@ def format_beacon(self, conntype, services=True): 'method=%s' % conntype, 'identity=%s' % config.getGUID(), 'machine=%s' % gethostname(), - 'platform=pc'] + 'platform=%s' % PLATFORM_MAIN] if services: beacon.append('services=' + self.format_services()) diff --git a/config.py b/config.py index 6923fc86..fbeae3b2 100644 --- a/config.py +++ b/config.py @@ -10,26 +10,36 @@ import sys from ConfigParser import NoOptionError +config_win_default = '' + +if sys.platform == "win32": + import _winreg + + try: + explorerFolders = _winreg.OpenKey( + _winreg.HKEY_LOCAL_MACHINE, + 'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' + ) + + winCommonAppDataVal, winCommonAppDataType = _winreg.QueryValueEx(explorerFolders, 'Common AppData') + + config_win_default = os.path.join(winCommonAppDataVal, 'pyTivo', 'pyTivo.conf') + + except WindowsError: + print "Can't access Windows Registry to find common Application Data path." + def init(argv): - global config - global guid - global config_files - global configs_found global tivos global tivo_names - global bin_paths - - config = ConfigParser.ConfigParser() + global guid + global config_files + tivos = {} + tivo_names = {} guid = ''.join([random.choice(string.ascii_letters) for i in range(10)]) p = os.path.dirname(__file__) config_files = ['/etc/pyTivo.conf', os.path.join(p, 'pyTivo.conf')] - configs_found = [] - - tivos = {} - tivo_names = {} - bin_paths = {} try: opts, _ = getopt.getopt(argv, 'c:e:', ['config=', 'extraconf=']) @@ -42,11 +52,21 @@ def init(argv): elif opt in ('-e', '--extraconf'): config_files.append(value) + reset() + +def reset(): + global bin_paths + global config + global configs_found + + bin_paths = {} + + config = ConfigParser.ConfigParser() configs_found = config.read(config_files) if not configs_found: - print ('ERROR: pyTivo.conf does not exist.\n' + - 'You must create this file before running pyTivo.') - sys.exit(1) + print ('WARNING: pyTivo.conf does not exist.\n' + + 'Assuming default values.') + configs_found = config_files[-1:] for section in config.sections(): if section.startswith('_tivo_'): @@ -59,19 +79,20 @@ def init(argv): if config.has_option(section, 'address'): tivos[tsn] = config.get(section, 'address') -def reset(): - global config - global bin_paths - bin_paths.clear() - newconfig = ConfigParser.ConfigParser() - newconfig.read(config_files) - config = newconfig + for section in ['Server', '_tivo_SD', '_tivo_HD']: + if not config.has_section(section): + config.add_section(section) def write(): f = open(configs_found[-1], 'w') config.write(f) f.close() +def tivos_by_ip(tivoIP): + for key, value in tivos.items(): + if value == tivoIP: + return key + def get_server(name, default=None): if config.has_option('Server', name): return config.get('Server', name) @@ -192,24 +213,19 @@ def getDebug(): try: return config.getboolean('Server', 'debug') except NoOptionError, ValueError: - return False + return True def getOptres(tsn=None): - if tsn and config.has_section('_tivo_' + tsn): - try: - return config.getboolean('_tivo_' + tsn, 'optres') - except NoOptionError, ValueError: - pass - section_name = get_section(tsn) - if config.has_section(section_name): - try: - return config.getboolean(section_name, 'optres') - except NoOptionError, ValueError: - pass try: - return config.getboolean('Server', 'optres') - except NoOptionError, ValueError: - return False + return config.getboolean('_tivo_' + tsn, 'optres') + except: + try: + return config.getboolean(get_section(tsn), 'optres') + except: + try: + return config.getboolean('Server', 'optres') + except: + return False def getPixelAR(ref): if config.has_option('Server', 'par'): @@ -263,7 +279,28 @@ def getFFmpegTemplate(tsn): return '%(video_codec)s %(video_fps)s %(video_br)s %(max_video_br)s \ %(buff_size)s %(aspect_ratio)s %(audio_br)s \ %(audio_fr)s %(audio_ch)s %(audio_codec)s %(audio_lang)s \ - %(ffmpeg_pram)s %(format)s' + %(ffmpeg_pram)s %(ffmpeg_threads)s %(format)s' + +def getFFmpegThreads(): + if config.has_option('Server', 'ffmpeg_threads'): + logger = logging.getLogger('pyTivo.config') + try: + threads = config.get('Server', 'ffmpeg_threads') + #older FFmpeg builds have history of crashing if threads < 1 + #threads max is 16 + if 1 <= int(threads) <= 16: + return threads + + else: + logger.debug(threads + ' is an invalid ffmpeg_threads setting, must be between 1 and 16, using default') + return None + + except ValueError: + logger.debug(threads + ' is an invalid ffmpeg_threads setting, using defaults') + return None + + else: + return None def getFFmpegPrams(tsn): return get_tsn('ffmpeg_pram', tsn, True) @@ -364,22 +401,42 @@ def get_section(tsn): return ['_tivo_SD', '_tivo_HD'][isHDtivo(tsn)] def get_tsn(name, tsn=None, raw=False): - if tsn and config.has_section('_tivo_' + tsn): - try: - return config.get('_tivo_' + tsn, name, raw) - except NoOptionError: - pass - section_name = get_section(tsn) - if config.has_section(section_name): - try: - return config.get(section_name, name, raw) - except NoOptionError: - pass try: - return config.get('Server', name, raw) - except NoOptionError: - pass - return None + return config.get('_tivo_' + tsn, name, raw) + except: + try: + return config.get(get_section(tsn), name, raw) + except: + try: + return config.get('Server', name, raw) + except: + return None + +def get_random(): + return ''.join([random.choice(string.digits) for i in range(3)]) + +def get_freeSpace(share, inFile): + logger = logging.getLogger('pyTivo.config') + + # checks free space of given output path + if sys.platform=="win32": + import ctypes + freeSize = ctypes.c_ulonglong(0) + ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(share), None, None, ctypes.pointer(freeSize)) + freeSize = freeSize.value + + else: + s = os.statvfs(share) + freeSize = s.f_bavail * s.f_frsize + + temp_fileSize = os.stat(inFile).st_size + + # checks if enough free space exists on drive for temp file (plus padding) + if freeSize < temp_fileSize*1.1: + logger.error('Not enough disk space to remux') + return False + + return True # Parse a bitrate using the SI/IEEE suffix values as if by ffmpeg # For example, 2K==2000, 2Ki==2048, 2MB==16000000, 2MiB==16777216 diff --git a/metadata.py b/metadata.py index 73520f42..63f48416 100755 --- a/metadata.py +++ b/metadata.py @@ -5,6 +5,7 @@ import sys from datetime import datetime from xml.dom import minidom +from xml.parsers import expat try: import plistlib except: @@ -45,6 +46,7 @@ tivo_cache = LRUCache(50) mp4_cache = LRUCache(50) dvrms_cache = LRUCache(50) +nfo_cache = LRUCache(50) mswindows = (sys.platform == "win32") @@ -76,6 +78,15 @@ def _vtag_data(element, tag): elements = element.getElementsByTagName('element') return [x.firstChild.data for x in elements if x.firstChild] +def _vtag_data_alternate(element, tag): + elements = [element] + for name in tag.split('/'): + new_elements = [] + for elmt in elements: + new_elements += elmt.getElementsByTagName(name) + elements = new_elements + return [x.firstChild.data for x in elements if x.firstChild] + def _tag_value(element, tag): item = element.getElementsByTagName(tag) if item: @@ -218,7 +229,7 @@ def from_eyetv(full_path): 'DESCRIPTION': 'description', 'YEAR': 'movieYear', 'EPISODENUM': 'episodeNumber'} metadata = {} - path, name = os.path.split(unicode(full_path, 'utf-8')) + path = os.path.dirname(unicode(full_path, 'utf-8')) eyetvp = [x for x in os.listdir(path) if x.endswith('.eyetvp')][0] eyetvp = os.path.join(path, eyetvp) eyetv = plistlib.readPlist(eyetvp) @@ -237,9 +248,9 @@ def from_eyetv(full_path): for ptag, etag, ratings in [('tvRating', 'TV_RATING', TV_RATINGS), ('mpaaRating', 'MPAA_RATING', MPAA_RATINGS), ('starRating', 'STAR_RATING', STAR_RATINGS)]: - x = info[etag].upper() - if x and x in ratings: - metadata[ptag] = ratings[x] + x = info[etag].upper() + if x and x in ratings: + metadata[ptag] = ratings[x] # movieYear must be set for the mpaa/star ratings to work if (('mpaaRating' in metadata or 'starRating' in metadata) and @@ -253,10 +264,24 @@ def from_text(full_path): path, name = os.path.split(full_path) title, ext = os.path.splitext(name) - for metafile in [os.path.join(path, title) + '.properties', - os.path.join(path, 'default.txt'), full_path + '.txt', + search_paths = [] + ptmp = full_path + while ptmp: + parent = os.path.dirname(ptmp) + if ptmp != parent: + ptmp = parent + else: + break + search_paths.append(os.path.join(ptmp, 'default.txt')) + + search_paths.append(os.path.join(path, title) + '.properties') + search_paths.reverse() + + search_paths += [full_path + '.txt', os.path.join(path, '.meta', 'default.txt'), - os.path.join(path, '.meta', name) + '.txt']: + os.path.join(path, '.meta', name) + '.txt'] + + for metafile in search_paths: if os.path.exists(metafile): sep = ':='[metafile.endswith('.properties')] for line in file(metafile, 'U'): @@ -301,6 +326,7 @@ def basic(full_path): metadata.update(from_dvrms(full_path)) elif 'plistlib' in sys.modules and base_path.endswith('.eyetv'): metadata.update(from_eyetv(full_path)) + metadata.update(from_nfo(full_path)) metadata.update(from_text(full_path)) return metadata @@ -382,6 +408,212 @@ def from_details(xml): return metadata +def _nfo_vitems(source, metadata): + + vItems = {'vGenre': 'genre', + 'vWriter': 'credits', + 'vDirector': 'director', + 'vActor': 'actor/name'} + + for key in vItems: + data = _vtag_data_alternate(source, vItems[key]) + if data: + metadata.setdefault(key, []) + for dat in data: + if not dat in metadata[key]: + metadata[key].append(dat) + + if 'vGenre' in metadata: + metadata['vSeriesGenre'] = metadata['vProgramGenre'] = metadata['vGenre'] + + return metadata + +def _parse_nfo(nfo_path, nfo_data=None): + # nfo files can contain XML or a URL to seed the XBMC metadata scrapers + # It's also possible to have both (a URL after the XML metadata) + # pyTivo only parses the XML metadata, but we'll try to stip the URL + # from mixed XML/URL files. Returns `None` when XML can't be parsed. + if nfo_data is None: + nfo_data = [line.strip() for line in file(nfo_path, 'rU')] + xmldoc = None + try: + xmldoc = minidom.parseString(os.linesep.join(nfo_data)) + except expat.ExpatError, err: + import ipdb; ipdb.set_trace() + if expat.ErrorString(err.code) == expat.errors.XML_ERROR_INVALID_TOKEN: + # might be a URL outside the xml + while len(nfo_data) > err.lineno: + if len(nfo_data[-1]) == 0: + nfo_data.pop() + else: + break + if len(nfo_data) == err.lineno: + # last non-blank line contains the error + nfo_data.pop() + return _parse_nfo(nfo_path, nfo_data) + return xmldoc + +def _from_tvshow_nfo(tvshow_nfo_path): + if tvshow_nfo_path in nfo_cache: + return nfo_cache[tvshow_nfo_path] + + items = {'description': 'plot', + 'title': 'title', + 'seriesTitle': 'showtitle', + 'starRating': 'rating', + 'tvRating': 'mpaa'} + + nfo_cache[tvshow_nfo_path] = metadata = {} + + xmldoc = _parse_nfo(tvshow_nfo_path) + if not xmldoc: + return metadata + + tvshow = xmldoc.getElementsByTagName('tvshow') + if tvshow: + tvshow = tvshow[0] + else: + return metadata + + for item in items: + data = tag_data(tvshow, items[item]) + if data: + metadata[item] = data + + metadata = _nfo_vitems(tvshow, metadata) + + nfo_cache[tvshow_nfo_path] = metadata + return metadata + +def _from_episode_nfo(nfo_path, xmldoc): + metadata = {} + + items = {'description': 'plot', + 'episodeTitle': 'title', + 'seriesTitle': 'showtitle', + 'originalAirDate': 'aired', + 'starRating': 'rating', + 'tvRating': 'mpaa'} + + # find tvshow.nfo + path = nfo_path + while True: + basepath = os.path.dirname(path) + if path == basepath: + break + path = basepath + tv_nfo = os.path.join(path, 'tvshow.nfo') + if os.path.exists(tv_nfo): + metadata.update(_from_tvshow_nfo(tv_nfo)) + break + + if not xmldoc: + xmldoc = _parse_nfo(nfo_path) + if not xmldoc: + return metadata + + episode = xmldoc.getElementsByTagName('episodedetails') + if episode: + episode = episode[0] + else: + return metadata + + metadata['isEpisode'] = 'true' + for item in items: + data = tag_data(episode, items[item]) + if data: + metadata[item] = data + + season = tag_data(episode, 'displayseason') + if not season or season == "-1": + season = tag_data(episode, 'season') + if not season: + season = 1 + + ep_num = tag_data(episode, 'displayepisode') + if not ep_num or ep_num == "-1": + ep_num = tag_data(episode, 'episode') + if ep_num and ep_num != "-1": + metadata['episodeNumber'] = "%d%02d" % (int(season), int(ep_num)) + + if 'originalAirDate' in metadata: + metadata['originalAirDate'] += 'T00:00:00Z' + + metadata = _nfo_vitems(episode, metadata) + + return metadata + +def _from_movie_nfo(xmldoc): + metadata = {} + + if not xmldoc: + xmldoc = _parse_nfo(nfo_path) + if not xmldoc: + return metadata + + movie = xmldoc.getElementsByTagName('movie') + if movie: + movie = movie[0] + else: + return metadata + + items = {'description': 'plot', + 'title': 'title', + 'movieYear': 'year', + 'starRating': 'rating', + 'mpaaRating': 'mpaa'} + + metadata['isEpisode'] = 'false' + + for item in items: + data = tag_data(movie, items[item]) + if data: + metadata[item] = data + + metadata['movieYear'] = "%04d" % int(metadata.get('movieYear', 0)) + + metadata = _nfo_vitems(movie, metadata) + return metadata + +def from_nfo(full_path): + if full_path in nfo_cache: + return nfo_cache[full_path] + + metadata = nfo_cache[full_path] = {} + + nfo_path = "%s.nfo" % os.path.splitext(full_path)[0] + if not os.path.exists(nfo_path): + return metadata + + xmldoc = _parse_nfo(nfo_path) + if not xmldoc: + return metadata + + if xmldoc.getElementsByTagName('episodedetails'): + # it's an episode + metadata.update(_from_episode_nfo(nfo_path, xmldoc)) + elif xmldoc.getElementsByTagName('movie'): + # it's a movie + metadata.update(_from_movie_nfo(xmldoc)) + + # common nfo cleanup + if 'starRating' in metadata: + # .NFO 0-10 -> TiVo 1-7 + rating = int(float(metadata['starRating']) * 6 / 10 + 1.5) + metadata['starRating'] = rating + + for key, mapping in [('mpaaRating', MPAA_RATINGS), + ('tvRating', TV_RATINGS)]: + if key in metadata: + rating = mapping.get(metadata[key], None) + if rating: + metadata[key] = str(rating) + else: + del metadata[key] + + nfo_cache[full_path] = metadata + return metadata + def from_tivo(full_path): if full_path in tivo_cache: return tivo_cache[full_path] @@ -425,7 +657,7 @@ def dump(output, metadata): else: output.write('%s: %s\n' % (key, value.encode('utf-8'))) -if __name__ == '__main__': +if __name__ == '__main__': if len(sys.argv) > 1: metadata = {} fname = force_utf8(sys.argv[1]) diff --git a/plugins/music/music.py b/plugins/music/music.py index ee47e033..b8b8e1ec 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -159,7 +159,7 @@ def media_data(f): item = {} item['path'] = f.name item['part_path'] = f.name.replace(local_base_path, '', 1) - item['name'] = os.path.split(f.name)[1] + item['name'] = os.path.basename(f.name) item['is_dir'] = f.isdir item['is_playlist'] = f.isplay item['params'] = 'No' @@ -220,7 +220,7 @@ def get_tag(tagname, d): item['ArtistName'] = artist item['SongTitle'] = title item['AlbumTitle'] = get_tag('album', audioFile) - item['AlbumYear'] = get_tag('date', audioFile) + item['AlbumYear'] = get_tag('date', audioFile)[:4] item['MusicGenre'] = get_tag('genre', audioFile) except Exception, msg: print msg diff --git a/plugins/photo/photo.py b/plugins/photo/photo.py index 5aadd1a3..b05dac92 100644 --- a/plugins/photo/photo.py +++ b/plugins/photo/photo.py @@ -294,7 +294,7 @@ def media_data(f): item = {} item['path'] = f.name item['part_path'] = f.name.replace(local_base_path, '', 1) - item['name'] = os.path.split(f.name)[1] + item['name'] = os.path.basename(f.name) item['is_dir'] = f.isdir item['rotation'] = 0 item['cdate'] = '%#x' % f.cdate diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index 2ab0c190..14f7b733 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -130,7 +130,7 @@ Available In: Server debug -Default Setting: False +Default Setting: True Valid Entries: True/False Required: No Skill: Advanced @@ -499,6 +499,16 @@ conflict could also occur if the source file has really corrupt sections. Example Settings: True, False Available In: Tivos, HD_tivos, SD_tivos + +ffmpeg_threads + +Default Setting: None +Valid Entries: Any number up to 16 that represents the number of CPU threads FFmpeg has access to. +Required: No +Skill: Very Advanced +Description: Using multiple threads with FFmpeg if your CPU has them can speed up transcoding time. Using a setting of '2' for a dual core CPU may show increased transcoding speeds of 10% or more. If you have a dual core CPU with hyperthreading then the setting might be '4'. There is a diminishing return for this setting so make sure to set this to no more than the amount of threads your CPU is capable of handling. After a certain point the TiVo is the limiting speed factor. +Example Settings: Any number from 1 to 16 +Available In: Server ffmpeg_pram @@ -507,10 +517,8 @@ Valid Entries: A valid ffmpeg command Required: No Skill: Very Advanced Description: This allows you to append additional raw ffmpeg commands to -the ffmpeg template. For example, you would enter '-threads 2' here if -you have multiple processors and want ffmpeg to use both processors to -speed up transcoding. -Example Settings: -threads 2 +the ffmpeg template. +Example Settings: A valid FFmpeg command Available In: Server, Tivos, HD_tivos, SD_tivos ffmpeg_tmpl @@ -518,7 +526,7 @@ ffmpeg_tmpl Default Setting: %(video_codec)s %(video_fps)s %(video_br)s %(max_video_br)s %(buff_size)s %(aspect_ratio)s %(audio_br)s %(audio_fr)s %(audio_ch)s %(audio_codec)s %(audio_lang)s -%(ffmpeg_pram)s %(format)s +%(ffmpeg_pram)s %(ffmpeg_threads)s %(format)s Valid Entries: A valid ffmpeg command Required: No Skill: Very Advanced @@ -579,7 +587,7 @@ Description: Your username (email address) at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. Example Settings: user@example.com -Available In: Server +Available In: Server, Tivos, HD_tivos, SD_tivos tivo_password @@ -590,7 +598,7 @@ Skill: Basic Description: Your password at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. Example Settings: password -Available In: Server +Available In: Server, Tivos, HD_tivos, SD_tivos tivo_mind @@ -618,7 +626,7 @@ tivodecode (pushing .TiVo files, transcoding HD .TiVo files to SD TiVos). If you don't plan to use these features, you don't need to set this. Example Settings: 012345678 -Available In: Server +Available In: Server, Tivos, HD_tivos, SD_tivos togo_path @@ -662,3 +670,30 @@ editor. Example Settings: True/False Available In: Server +temp_share + +Default Setting: None +Valid Entries: Share name +Required: No +Skill: Moderate +Description: Sets a temporarily share to store your media content +that is being remuxed before being sent to your TiVo. If content exists +on network share, setting this to a local share on the computer running +pyTivo will significantly speed up the process. If not set or share is not +valid pyTivo will use the original path of your media content for temporary storage. +Example Settings: Videos, Movies, TV Shows, etc. +Available In: Server + +togo_mpegts + +Default Setting: False +Valid Entries: True/False/Default +Required: No +Skill: Moderate +Description: For Tivos that support mpeg-ts transfer. This will enable at tick box on +the togo screen, when this tick box is ticked pyTivo will transfer files in mpeg-ts format instead +of the default mpeg-ps. This is necessary if your video content contains h264 encoding. +True = display tick box. False = do not display tick box. Default = display tick box and +default to having it ticked. +Example Settings: True/False/Default +Available In: Server diff --git a/plugins/settings/settings.py b/plugins/settings/settings.py index d7c4502f..4069bdd0 100644 --- a/plugins/settings/settings.py +++ b/plugins/settings/settings.py @@ -85,15 +85,9 @@ def Settings(self, handler, query): t.quote = quote t.server_data = dict(config.config.items('Server', raw=True)) t.server_known = buildhelp.getknown('server') - if config.config.has_section('_tivo_HD'): - t.hd_tivos_data = dict(config.config.items('_tivo_HD', raw=True)) - else: - t.hd_tivos_data = {} + t.hd_tivos_data = dict(config.config.items('_tivo_HD', raw=True)) t.hd_tivos_known = buildhelp.getknown('hd_tivos') - if config.config.has_section('_tivo_SD'): - t.sd_tivos_data = dict(config.config.items('_tivo_SD', raw=True)) - else: - t.sd_tivos_data = {} + t.sd_tivos_data = dict(config.config.items('_tivo_SD', raw=True)) t.sd_tivos_known = buildhelp.getknown('sd_tivos') t.shares_data = shares_data t.shares_known = buildhelp.getknown('shares') diff --git a/plugins/togo/templates/npl.tmpl b/plugins/togo/templates/npl.tmpl index d6dad510..44f7ced1 100644 --- a/plugins/togo/templates/npl.tmpl +++ b/plugins/togo/templates/npl.tmpl @@ -147,6 +147,11 @@ function toggle(source) { Decrypt with tivodecode
#end if Save metadata to .txt
+#if $togo_mpegts == 'default' + Transfer as mpeg-ts
+#elif $togo_mpegts == 'enabled' + Transfer as mpeg-ts
+#end if

diff --git a/plugins/togo/templates/npl_mob.tmpl b/plugins/togo/templates/npl_mob.tmpl index 552a63dc..e2ec1c62 100644 --- a/plugins/togo/templates/npl_mob.tmpl +++ b/plugins/togo/templates/npl_mob.tmpl @@ -28,6 +28,11 @@ function toggle(source) { #if $has_tivodecode Decrypt with tivodecode
+#end if +#if $togo_mpegts == 'default' + Transfer as mpeg-ts
+#elif $togo_mpegts == 'enabled' + Transfer as mpeg-ts
#end if Save metadata to .txt

diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index 2e447037..679e372f 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -83,16 +83,15 @@ def NPL(self, handler, query): shows_per_page = 50 # Change this to alter the number of shows returned cname = query['Container'][0].split('/')[0] folder = '' - tivo_mak = config.get_server('tivo_mak') has_tivodecode = bool(config.get_bin('tivodecode')) togo_mpegts = config.get_server('togo_mpegts', 'False').lower() useragent = handler.headers.getheader('User-Agent', '') if 'TiVo' in query: tivoIP = query['TiVo'][0] - tivos_by_ip = dict([(value, key) - for key, value in config.tivos.items()]) - tivo_name = config.tivo_names[tivos_by_ip[tivoIP]] + tsn = config.tivos_by_ip(tivoIP) + tivo_name = config.tivo_names[tsn] + tivo_mak = config.get_tsn('tivo_mak', tsn) theurl = ('https://' + tivoIP + '/TiVoConnect?Command=QueryContainer&ItemCount=' + str(shows_per_page) + '&Container=/NowPlaying') @@ -191,6 +190,7 @@ def NPL(self, handler, query): if tivoIP in queue: t.queue = queue[tivoIP] t.has_tivodecode = has_tivodecode + t.togo_mpegts = togo_mpegts t.tname = tivo_name t.tivoIP = tivoIP t.container = cname @@ -236,15 +236,22 @@ def get_tivo_file(self, tivoIP, url, mak, togo_path): auth_handler.add_password('TiVo DVR', url, 'tivo', mak) try: - handle = self.tivo_open(url) - except IOError, e: + if status[url]['ts_format']: + handle = self.tivo_open('%s&Format=video/x-tivo-mpeg-ts' % url) + else: + handle = self.tivo_open(url) + except urllib2.HTTPError, e: status[url]['running'] = False status[url]['error'] = e.code + logger.error(e.code) + return + except urllib2.URLError, e: + status[url]['running'] = False + status[url]['error'] = e.reason + logger.error(e.reason) return - tivos_by_ip = dict([(value, key) - for key, value in config.tivos.items()]) - tivo_name = config.tivo_names[tivos_by_ip[tivoIP]] + tivo_name = config.tivo_names[config.tivos_by_ip(tivoIP)] logger.info('[%s] Start getting "%s" from %s' % (time.strftime('%d/%b/%Y %H:%M:%S'), outfile, tivo_name)) @@ -312,20 +319,22 @@ def process_queue(self, tivoIP, mak, togo_path): del queue[tivoIP] def ToGo(self, handler, query): - tivo_mak = config.get_server('tivo_mak') togo_path = config.get_server('togo_path') for name, data in config.getShares(): if togo_path == name: togo_path = data.get('path') - if tivo_mak and togo_path: + if togo_path: tivoIP = query['TiVo'][0] + tsn = config.tivos_by_ip(tivoIP) + tivo_mak = config.get_tsn('tivo_mak', tsn) urls = query.get('Url', []) decode = 'decode' in query save = 'save' in query + ts_format = 'ts_format' in query for theurl in urls: status[theurl] = {'running': False, 'error': '', 'rate': '', 'queued': True, 'size': 0, 'finished': False, - 'decode': decode, 'save': save} + 'decode': decode, 'save': save, 'ts_format' : ts_format} if tivoIP in queue: queue[tivoIP].append(theurl) else: diff --git a/plugins/video/templates/container_xml.tmpl b/plugins/video/templates/container_xml.tmpl index 7138c53b..dd0a5793 100644 --- a/plugins/video/templates/container_xml.tmpl +++ b/plugins/video/templates/container_xml.tmpl @@ -60,13 +60,13 @@ $video.mime - /$quote($container)$quote($video.part_path) - - - video/* - No - urn:tivo:image:save-until-i-delete-recording - + /$quote($container)$quote($video.part_path) + + + image/* + No + urn:tivo:image:save-until-i-delete-recording + text/xml No diff --git a/plugins/video/transcode.py b/plugins/video/transcode.py index 61038909..bd415691 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -67,10 +67,11 @@ def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), 'audio_br': select_audiobr(tsn), 'audio_fr': select_audiofr(inFile, tsn), - 'audio_ch': select_audioch(tsn), + 'audio_ch': select_audioch(inFile, tsn), 'audio_codec': select_audiocodec(isQuery, inFile, tsn), 'audio_lang': select_audiolang(inFile, tsn), 'ffmpeg_pram': select_ffmpegprams(tsn), + 'ffmpeg_threads': select_ffmpegthreads(), 'format': select_format(tsn, mime)} if isQuery: @@ -92,12 +93,12 @@ def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): cmd = '' ffmpeg = tivodecode else: - cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() + cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', '-'] + cmd_string.split() ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout, stdout=subprocess.PIPE, bufsize=(512 * 1024)) else: - cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', fname] + cmd_string.split() ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), stdout=subprocess.PIPE) @@ -206,7 +207,7 @@ def cleanup(inFile): reapers[inFile].cancel() del reapers[inFile] -def select_audiocodec(isQuery, inFile, tsn=''): +def select_audiocodec(isQuery, inFile, tsn='', mime=''): if inFile[-5:].lower() == '.tivo': return '-acodec copy' vInfo = video_info(inFile) @@ -215,16 +216,35 @@ def select_audiocodec(isQuery, inFile, tsn=''): if not codec: # Default, compatible with all TiVo's codec = 'ac3' - if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'): + if mime == 'video/mp4': + compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac', + 'ac3', 'liba52') + else: + compatiblecodecs = ('ac3', 'liba52', 'mp2') + + if vInfo['aCodec'] in compatiblecodecs: aKbps = vInfo['aKbps'] + aCh = vInfo['aCh'] if aKbps == None: - if not isQuery: - aKbps = audio_check(inFile, tsn) + if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'): + #along with the channel check below this should pass any AAC audio + #that has undefined 'aKbps' and is <= 2 channels. Should be TiVo compatible. + codec = 'copy' + elif not isQuery: + vInfoQuery = audio_check(inFile, tsn) + if vInfoQuery == None: + aKbps = None + aCh = None + else: + aKbps = vInfoQuery['aKbps'] + aCh = vInfoQuery['aCh'] else: - codec = 'TBD' + codec = 'TBA' if aKbps != None and int(aKbps) <= config.getMaxAudioBR(tsn): # compatible codec and bitrate, do not reencode audio codec = 'copy' + if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2): + codec = 'ac3' copy_flag = config.get_tsn('copy_ts', tsn) copyts = ' -copyts' if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or @@ -243,27 +263,54 @@ def select_audiofr(inFile, tsn): freq = audio_fr return '-ar ' + freq -def select_audioch(tsn): +def select_audioch(inFile, tsn): ch = config.get_tsn('audio_ch', tsn) + vInfo = video_info(inFile) if ch: return '-ac ' + ch + #AC-3 max channels is 5.1 + if vInfo['aCh'] != None and vInfo['aCh'] > 6: + debug('Too many audio channels for AC-3, using 5.1 instead') + return '-ac ' + '6' return '' def select_audiolang(inFile, tsn): vInfo = video_info(inFile) audio_lang = config.get_tsn('audio_lang', tsn) - if audio_lang != None and vInfo['mapVideo'] != None: + debug('audio_lang: %s' % audio_lang) + if vInfo['mapAudio']: + #default to first detected audio stream to begin with stream = vInfo['mapAudio'][0][0] - langmatch = [] + if audio_lang != None and vInfo['mapVideo'] != None: + langmatch_curr = [] + langmatch_prev = vInfo['mapAudio'][:] for lang in audio_lang.replace(' ','').lower().split(','): - for s, l in vInfo['mapAudio']: - if lang in s + l.replace(' ','').lower(): - langmatch.append(s) + for s, l in langmatch_prev: + if lang in s + (l).replace(' ','').lower(): + langmatch_curr.append((s, l)) stream = s - break - if langmatch: break - if stream is not '': - return '-map ' + vInfo['mapVideo'] + ' -map ' + stream + #if only 1 item matched we're done + if len(langmatch_curr) == 1: + del langmatch_prev[:] + break + #if more than 1 item matched copy the curr area to the prev array + #we only need to look at the new shorter list from now on + elif len(langmatch_curr) > 1: + del langmatch_prev[:] + langmatch_prev = langmatch_curr[:] + del langmatch_curr[:] + #if nothing matched we'll keep the prev array and clear the curr array + else: + del langmatch_curr[:] + #if we drop out of the loop with more than 1 item default to the first item + if len(langmatch_prev) > 1: + stream = langmatch_prev[0][0] + #don't let FFmpeg auto select audio stream, pyTivo defaults to first detected + if stream: + debug('selected audio stream: %s' % stream) + return '-map ' + vInfo['mapVideo'] + ' -map ' + stream + #if no audio is found + debug('selected audio stream: None detected') return '' def select_videofps(inFile, tsn): @@ -314,6 +361,14 @@ def select_maxvideobr(tsn): def select_buffsize(tsn): return '-bufsize ' + config.getBuffSize(tsn) +def select_ffmpegthreads(): + threads = config.getFFmpegThreads() + + if not threads: + return '' + + return '-threads ' + threads + def select_ffmpegprams(tsn): params = config.getFFmpegPrams(tsn) if not params: @@ -600,11 +655,25 @@ def tivo_compatible_audio(vInfo, inFile, tsn, mime=''): message = (True, '') while True: codec = vInfo.get('aCodec', '') + + if codec == None: + debug('No audio stream detected') + break + if mime == 'video/mp4': if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac', 'ac3', 'liba52'): message = (False, 'aCodec %s not compatible' % codec) - + break + if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2): + message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh'])) + break + + audio_lang = config.get_tsn('audio_lang', tsn) + if audio_lang: + if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]: + message = (False, '%s preferred audio track exists' % + audio_lang) break if mime == 'video/bif': @@ -654,14 +723,23 @@ def tivo_compatible_container(vInfo, inFile, mime=''): def mp4_remuxable(inFile, tsn=''): vInfo = video_info(inFile) - return (tivo_compatible_video(vInfo, tsn, 'video/mp4')[0] and - tivo_compatible_audio(vInfo, inFile, tsn, 'video/mp4')[0]) - -def mp4_remux(inFile, basename): - outFile = inFile + '.pyTivo-temp' - newname = basename + '.pyTivo-temp' + return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0] + +def mp4_remux(inFile, basename, tsn='', temp_share_path=''): + temp_add = config.get_server('temp_add', '') + unique_id = '' + if temp_add: + unique_id = '_' + config.get_random() + outFile = inFile + unique_id + '.pyTivo-temp' + newname = basename + unique_id + '.pyTivo-temp' + + if temp_share_path: + newname = os.path.splitext(os.path.split(basename)[1])[0] + unique_id + '.mp4.pyTivo-temp' + outFile = os.path.join(temp_share_path, newname) + if os.path.exists(outFile): - return None # ugh! + debug('File already exists. Performing full transcode instead') + return None ffmpeg_path = config.get_bin('ffmpeg') fname = unicode(inFile, 'utf-8') @@ -670,13 +748,35 @@ def mp4_remux(inFile, basename): fname = fname.encode('iso8859-1') oname = oname.encode('iso8859-1') - cmd = [ffmpeg_path, '-i', fname, '-vcodec', 'copy', '-acodec', - 'copy', '-f', 'mp4', oname] + settings = {'video_codec': '-vcodec copy', + 'video_br': select_videobr(inFile, tsn), + 'video_fps': select_videofps(inFile, tsn), + 'max_video_br': select_maxvideobr(tsn), + 'buff_size': select_buffsize(tsn), + 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), + 'audio_br': select_audiobr(tsn), + 'audio_fr': select_audiofr(inFile, tsn), + 'audio_ch': select_audioch(inFile, tsn), + 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'), + 'audio_lang': select_audiolang(inFile, tsn), + 'ffmpeg_pram': select_ffmpegprams(tsn), + 'ffmpeg_threads': select_ffmpegthreads(), + 'format': '-f mp4'} + + cmd_string = config.getFFmpegTemplate(tsn) % settings + + cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', fname] + cmd_string.split() + [oname] + + debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') + debug(' '.join(cmd)) + ffmpeg = subprocess.Popen(cmd) + debug('remuxing ' + inFile + ' to ' + outFile) + if ffmpeg.wait(): - debug('error during remuxing') os.remove(outFile) + debug('FFmpeg error, temp file has been removed: ' + outFile) return None return newname @@ -761,6 +861,21 @@ def video_info(inFile, cache=True): err_tmp.close() debug('ffmpeg output=%s' % output) + #get libavcodec major and minor versions from FFmpeg output + rezre = re.compile(r'libavcodec \s+([0-9]+).?\s*([0-9]+)') + x = rezre.search(output) + if x: + vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = int(x.group(1)), int(x.group(2)) + else: + #should catch very old builds which are formatted differently + rezre = re.compile(r'libavcodec.*:\s+([0-9]+).?\s*([0-9]+)') + x = rezre.search(output) + if x: + vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = int(x.group(1)), int(x.group(2)) + else: + vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = None, None + debug('failed at avcodec check') + attrs = {'container': r'Input #0, ([^,]+),', 'vCodec': r'Video: ([^, ]+)', # video codec 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate @@ -781,6 +896,25 @@ def video_info(inFile, cache=True): vInfo[attr] = None debug('failed at ' + attr) + rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*') + x = rezre.search(output) + if x: + if x.group(3): + if x.group(3) == 'stereo': + vInfo['aCh'] = 2 + elif x.group(3) == 'mono': + vInfo['aCh'] = 1 + elif x.group(2): + vInfo['aCh'] = int(x.group(1)) + int(x.group(2)) + elif x.group(1): + vInfo['aCh'] = int(x.group(1)) + else: + vInfo['aCh'] = None + debug('failed at aCh') + else: + vInfo['aCh'] = None + debug('failed at aCh') + rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') x = rezre.search(output) if x: @@ -869,12 +1003,12 @@ def video_info(inFile, cache=True): vInfo['dar1'] = None # get Audio Stream mapping. - rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:.*') + rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)') x = rezre.search(output) amap = [] if x: for x in rezre.finditer(output): - amap.append(x.groups()) + amap.append((x.group(1), x.group(2)+x.group(3))) else: amap.append(('', '')) debug('failed at mapAudio') @@ -895,15 +1029,18 @@ def video_info(inFile, cache=True): if line.startswith(' Duration:'): flag = False else: - key, value = [x.strip() for x in line.split(':', 1)] try: - value = value.decode('utf-8') + key, value = [x.strip() for x in line.split(':', 1)] + try: + value = value.decode('utf-8') + except: + if sys.platform == 'darwin': + value = value.decode('macroman') + else: + value = value.decode('iso8859-1') + rawmeta[key] = [value] except: - if sys.platform == 'darwin': - value = value.decode('macroman') - else: - value = value.decode('iso8859-1') - rawmeta[key] = [value] + pass vInfo['rawmeta'] = rawmeta @@ -944,12 +1081,12 @@ def audio_check(inFile, tsn): except: kill(ffmpeg) testfile.close() - aKbps = None + vInfo = None else: testfile.close() - aKbps = video_info(testname, False)['aKbps'] + vInfo = video_info(testname, False) os.remove(testname) - return aKbps + return vInfo def supported_format(inFile): if video_info(inFile)['Supported']: diff --git a/plugins/video/video.py b/plugins/video/video.py index aeaea124..6e428ed4 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -97,9 +97,10 @@ def send_file(self, handler, path, query): compatible = (not needs_tivodecode and transcode.tivo_compatible(path, tsn, mime)[0]) - offset = handler.headers.getheader('Range') - if offset: - offset = int(offset[6:-1]) # "bytes=XXX-" + try: # "bytes=XXX-" + offset = int(handler.headers.getheader('Range')[6:-1]) + except: + offset = 0 if needs_tivodecode: valid = bool(config.get_bin('tivodecode') and @@ -183,6 +184,7 @@ def send_file(self, handler, path, query): if fname.endswith('.pyTivo-temp'): os.remove(fname) + logger.debug(fname + ' has been removed') def __duration(self, full_path): return transcode.video_info(full_path)['millisecs'] @@ -261,7 +263,7 @@ def metadata_full(self, full_path, tsn='', mime=''): for k, v in sorted(vInfo.items(), reverse=True)] + ['TRANSCODE OPTIONS: '] + ["%s" % (v) for k, v in transcode_options.items()] + - ['SOURCE FILE: ', os.path.split(full_path)[1]] + ['SOURCE FILE: ', os.path.basename(full_path)] ) now = datetime.utcnow() @@ -333,12 +335,12 @@ def QueryContainer(self, handler, query): ltime = time.localtime(mtime) video['captureDate'] = hex(mtime) video['textDate'] = time.strftime('%b %d, %Y', ltime) - video['name'] = os.path.split(f.name)[1] + video['name'] = os.path.basename(f.name) video['path'] = f.name video['part_path'] = f.name.replace(local_base_path, '', 1) if not video['part_path'].startswith(os.path.sep): video['part_path'] = os.path.sep + video['part_path'] - video['title'] = os.path.split(f.name)[1] + video['title'] = os.path.basename(f.name) video['is_dir'] = f.isdir if video['is_dir']: video['small_path'] = subcname + '/' + video['name'] @@ -365,6 +367,7 @@ def QueryContainer(self, handler, query): videos.append(video) logger.debug('mobileagent: %d useragent: %s' % (useragent.lower().find('mobile'), useragent.lower())) + if not use_html: t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) elif useragent.lower().find('mobile') > 0: @@ -447,6 +450,16 @@ def push_one_file(self, f): file_info = VideoDetails() file_info['valid'] = transcode.supported_format(f['path']) + temp_share = config.get_server('temp_share', '') + temp_share_path = '' + if temp_share: + for name, data in config.getShares(): + if temp_share == name: + temp_share_path = data.get('path') + remux_path = temp_share_path + else: + remux_path = os.path.dirname(f['path']) + mime = 'video/mpeg' if config.isHDtivo(f['tsn']): for m in ['video/mp4', 'video/bif']: @@ -455,11 +468,17 @@ def push_one_file(self, f): break if (mime == 'video/mpeg' and - transcode.mp4_remuxable(f['path'], f['tsn'])): - new_path = transcode.mp4_remux(f['path'], f['name']) + transcode.mp4_remuxable(f['path'], f['tsn']) and config.get_freeSpace(remux_path, f['path'])): + + new_path = transcode.mp4_remux(f['path'], f['name'], f['tsn'], temp_share_path) if new_path: mime = 'video/mp4' f['name'] = new_path + if temp_share_path: + ip = config.get_ip() + port = config.getPort() + container = quote(temp_share) + '/' + f['url'] = 'http://%s:%s/%s' % (ip, port, container) if file_info['valid']: file_info.update(self.metadata_full(f['path'], f['tsn'], mime)) diff --git a/plugins/webvideo/webvideo.py b/plugins/webvideo/webvideo.py index 3f541015..eaf06ea4 100644 --- a/plugins/webvideo/webvideo.py +++ b/plugins/webvideo/webvideo.py @@ -149,7 +149,7 @@ def processDlRequest(self): data['url'] = ('http://%s:%s' % (ip, port) + urllib.quote('/%s/%s' % (share_name, - os.path.split(file_name)[-1]))) + os.path.basename(file_name)))) data['duration'] = file_info['duration'] / 1000 data['size'] = file_info['size'] diff --git a/pyTivo.conf.dist b/pyTivo.conf.dist index c66fa7dc..241ead6f 100644 --- a/pyTivo.conf.dist +++ b/pyTivo.conf.dist @@ -5,9 +5,12 @@ # you get pyTivo up and running. You can access the tool by pointing your # browser to http://localhost:9032/ +#Read the pyTivo support wiki for additional help at http://pytivo.sourceforge.net + [Server] port=9032 +# FFmpeg is a required tool but downloaded separately. See pyTivo wiki for help. # Full path to ffmpeg including filename # For windows: ffmpeg=c:\Program Files\pyTivo\bin\ffmpeg.exe # For linux: ffmpeg=/usr/bin/ffmpeg diff --git a/pyTivo.py b/pyTivo.py index 82cc53e3..b975008a 100755 --- a/pyTivo.py +++ b/pyTivo.py @@ -62,6 +62,7 @@ def build_recursive_list(path): httpd.set_service_status(in_service) logger.info('pyTivo is ready.') + logger.debug('pyTivo/wynneth') return httpd def serve(httpd): From fd6b5b4b233c95919460f9e058f0d5e11064e4b4 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Tue, 20 Mar 2012 13:54:44 -0500 Subject: [PATCH 12/21] Merged iluvatar branch --- content/favicon.ico | Bin 0 -> 1150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 content/favicon.ico diff --git a/content/favicon.ico b/content/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f98e1383f3671bde84bcc25ad602a84d960d944 GIT binary patch literal 1150 zcma)*F(|}g7{=cV3w2IOVY$IzqGVuEib1hhPIe__At@P5WR}TbPzG5{vfaWB)J+n_ zK)DnPib2lr&HL5oz5nIj>$&%Pf6w#X*Z;dakrb~?MtIN4Y`sXEh;$WwCN z0S#L2on9B9^-uHq9goK*hA+$M^~ZI|py%gen6SRG+?4YUjITQG$9>CfI%CE7q2qpB zvfPO?#*E)O?#B_!JvpP@_%~gDmer~iL+?q|=|Uyr!;bebFQXyj_jKJqZgXhI#uo#y zK4AQmoYUR literal 0 HcmV?d00001 From 89c0cb8790968166c7f800fa1e44792adba20d5f Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Tue, 20 Mar 2012 14:10:25 -0500 Subject: [PATCH 13/21] Linkify video count and arrow on mobile --- plugins/video/templates/container_mob.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/video/templates/container_mob.tmpl b/plugins/video/templates/container_mob.tmpl index 312afc2f..3bebe696 100644 --- a/plugins/video/templates/container_mob.tmpl +++ b/plugins/video/templates/container_mob.tmpl @@ -58,8 +58,8 @@ function toggle(source) { #if $video.is_dir ## This is a folder
$video.title -
$video.total_items
- + + #else ## This is a show From d900c46a1b2d490fbcc05145975d4f2eec2eb904 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Tue, 20 Mar 2012 19:57:13 -0500 Subject: [PATCH 14/21] Merged recent wmcbrine changes --- Cheetah/CheetahWrapper.py | 589 --- Cheetah/FileUtils.py | 374 -- Cheetah/ImportHooks.py | 139 - Cheetah/ImportManager.py | 561 --- Cheetah/TemplateCmdLineIface.py | 108 - Cheetah/Templates/SkeletonPage.py | 273 -- Cheetah/Templates/SkeletonPage.tmpl | 44 - Cheetah/Templates/_SkeletonPage.py | 216 -- Cheetah/Templates/__init__.py | 1 - Cheetah/Tests/CheetahWrapper.py | 596 --- Cheetah/Tests/FileRefresh.py | 55 - Cheetah/Tests/NameMapper.py | 539 --- Cheetah/Tests/SyntaxAndOutput.py | 3230 ----------------- Cheetah/Tests/Template.py | 312 -- Cheetah/Tests/Test.py | 70 - Cheetah/Tests/__init__.py | 1 - Cheetah/Tests/unittest_local_copy.py | 977 ----- Cheetah/Tools/CGITemplate.py | 78 - Cheetah/Tools/MondoReport.py | 464 --- Cheetah/Tools/MondoReportDoc.txt | 391 -- Cheetah/Tools/RecursiveNull.py | 23 - Cheetah/Tools/SiteHierarchy.py | 183 - Cheetah/Tools/__init__.py | 8 - Cheetah/Tools/turbocheetah/__init__.py | 5 - Cheetah/Tools/turbocheetah/cheetahsupport.py | 110 - Cheetah/Tools/turbocheetah/tests/__init__.py | 1 - .../Tools/turbocheetah/tests/test_template.py | 66 - Cheetah/Utils/optik/__init__.py | 32 - Cheetah/Utils/optik/errors.py | 52 - Cheetah/Utils/optik/option.py | 354 -- Cheetah/Utils/optik/option_parser.py | 667 ---- Cheetah/_namemapper.c | 490 --- config.py | 46 +- httpserver.py | 128 +- metadata.py | 10 - plugin.py | 14 +- plugins/music/music.py | 47 +- plugins/photo/photo.py | 47 +- plugins/settings/help.txt | 30 +- plugins/settings/settings.py | 16 +- plugins/togo/templates/npl.tmpl | 5 - plugins/togo/templates/npl_mob.tmpl | 5 - plugins/togo/togo.py | 22 +- plugins/video/transcode.py | 104 +- plugins/video/video.py | 236 +- plugins/webvideo/webvideo.py | 4 +- pyTivo.py | 1 - 47 files changed, 299 insertions(+), 11425 deletions(-) delete mode 100755 Cheetah/CheetahWrapper.py delete mode 100644 Cheetah/FileUtils.py delete mode 100755 Cheetah/ImportHooks.py delete mode 100755 Cheetah/ImportManager.py delete mode 100644 Cheetah/TemplateCmdLineIface.py delete mode 100644 Cheetah/Templates/SkeletonPage.py delete mode 100644 Cheetah/Templates/SkeletonPage.tmpl delete mode 100644 Cheetah/Templates/_SkeletonPage.py delete mode 100644 Cheetah/Templates/__init__.py delete mode 100644 Cheetah/Tests/CheetahWrapper.py delete mode 100644 Cheetah/Tests/FileRefresh.py delete mode 100644 Cheetah/Tests/NameMapper.py delete mode 100644 Cheetah/Tests/SyntaxAndOutput.py delete mode 100644 Cheetah/Tests/Template.py delete mode 100755 Cheetah/Tests/Test.py delete mode 100644 Cheetah/Tests/__init__.py delete mode 100755 Cheetah/Tests/unittest_local_copy.py delete mode 100644 Cheetah/Tools/CGITemplate.py delete mode 100644 Cheetah/Tools/MondoReport.py delete mode 100644 Cheetah/Tools/MondoReportDoc.txt delete mode 100644 Cheetah/Tools/RecursiveNull.py delete mode 100644 Cheetah/Tools/SiteHierarchy.py delete mode 100644 Cheetah/Tools/__init__.py delete mode 100644 Cheetah/Tools/turbocheetah/__init__.py delete mode 100644 Cheetah/Tools/turbocheetah/cheetahsupport.py delete mode 100644 Cheetah/Tools/turbocheetah/tests/__init__.py delete mode 100644 Cheetah/Tools/turbocheetah/tests/test_template.py delete mode 100644 Cheetah/Utils/optik/__init__.py delete mode 100644 Cheetah/Utils/optik/errors.py delete mode 100644 Cheetah/Utils/optik/option.py delete mode 100644 Cheetah/Utils/optik/option_parser.py delete mode 100644 Cheetah/_namemapper.c diff --git a/Cheetah/CheetahWrapper.py b/Cheetah/CheetahWrapper.py deleted file mode 100755 index 456c2a9b..00000000 --- a/Cheetah/CheetahWrapper.py +++ /dev/null @@ -1,589 +0,0 @@ -#!/usr/bin/env python -# $Id: CheetahWrapper.py,v 1.26 2007/10/02 01:22:04 tavis_rudd Exp $ -"""Cheetah command-line interface. - -2002-09-03 MSO: Total rewrite. -2002-09-04 MSO: Bugfix, compile command was using wrong output ext. -2002-11-08 MSO: Another rewrite. - -Meta-Data -================================================================================ -Author: Tavis Rudd and Mike Orr > -Version: $Revision: 1.26 $ -Start Date: 2001/03/30 -Last Revision Date: $Date: 2007/10/02 01:22:04 $ -""" -__author__ = "Tavis Rudd and Mike Orr " -__revision__ = "$Revision: 1.26 $"[11:-2] - -import getopt, glob, os, pprint, re, shutil, sys -import cPickle as pickle - -from Cheetah.Version import Version -from Cheetah.Template import Template, DEFAULT_COMPILER_SETTINGS -from Cheetah.Utils.Misc import mkdirsWithPyInitFiles -from Cheetah.Utils.optik import OptionParser - -optionDashesRE = re.compile( R"^-{1,2}" ) -moduleNameRE = re.compile( R"^[a-zA-Z_][a-zA-Z_0-9]*$" ) - -def fprintfMessage(stream, format, *args): - if format[-1:] == '^': - format = format[:-1] - else: - format += '\n' - if args: - message = format % args - else: - message = format - stream.write(message) - -class Error(Exception): - pass - - -class Bundle: - """Wrap the source, destination and backup paths in one neat little class. - Used by CheetahWrapper.getBundles(). - """ - def __init__(self, **kw): - self.__dict__.update(kw) - - def __repr__(self): - return "" % self.__dict__ - - -class MyOptionParser(OptionParser): - standard_option_list = [] # We use commands for Optik's standard options. - - def error(self, msg): - """Print our usage+error page.""" - usage(HELP_PAGE2, msg) - - def print_usage(self, file=None): - """Our usage+error page already has this.""" - pass - - -################################################## -## USAGE FUNCTION & MESSAGES - -def usage(usageMessage, errorMessage="", out=sys.stderr): - """Write help text, an optional error message, and abort the program. - """ - out.write(WRAPPER_TOP) - out.write(usageMessage) - exitStatus = 0 - if errorMessage: - out.write('\n') - out.write("*** USAGE ERROR ***: %s\n" % errorMessage) - exitStatus = 1 - sys.exit(exitStatus) - - -WRAPPER_TOP = """\ - __ ____________ __ - \ \/ \/ / - \/ * * \/ CHEETAH %(Version)s Command-Line Tool - \ | / - \ ==----== / by Tavis Rudd - \__________/ and Mike Orr - -""" % globals() - - -HELP_PAGE1 = """\ -USAGE: ------- - cheetah compile [options] [FILES ...] : Compile template definitions - cheetah fill [options] [FILES ...] : Fill template definitions - cheetah help : Print this help message - cheetah options : Print options help message - cheetah test [options] : Run Cheetah's regression tests - : (same as for unittest) - cheetah version : Print Cheetah version number - -You may abbreviate the command to the first letter; e.g., 'h' == 'help'. -If FILES is a single "-", read standard input and write standard output. -Run "cheetah options" for the list of valid options. -""" - -HELP_PAGE2 = """\ -OPTIONS FOR "compile" AND "fill": ---------------------------------- - --idir DIR, --odir DIR : input/output directories (default: current dir) - --iext EXT, --oext EXT : input/output filename extensions - (default for compile: tmpl/py, fill: tmpl/html) - -R : recurse subdirectories looking for input files - --debug : print lots of diagnostic output to standard error - --env : put the environment in the searchList - --flat : no destination subdirectories - --nobackup : don't make backups - --pickle FILE : unpickle FILE and put that object in the searchList - --stdout, -p : output to standard output (pipe) - --settings : a string representing the compiler settings to use - e.g. --settings='useNameMapper=False,useFilters=False' - This string is eval'd in Python so it should contain - valid Python syntax. - --templateAPIClass : a string representing a subclass of - Cheetah.Template:Template to use for compilation - -Run "cheetah help" for the main help screen. -""" - -################################################## -## CheetahWrapper CLASS - -class CheetahWrapper: - MAKE_BACKUPS = True - BACKUP_SUFFIX = ".bak" - _templateClass = None - _compilerSettings = None - - def __init__(self): - self.progName = None - self.command = None - self.opts = None - self.pathArgs = None - self.sourceFiles = [] - self.searchList = [] - - ################################################## - ## MAIN ROUTINE - - def main(self, argv=None): - """The main program controller.""" - - if argv is None: - argv = sys.argv - - # Step 1: Determine the command and arguments. - try: - self.progName = progName = os.path.basename(argv[0]) - self.command = command = optionDashesRE.sub("", argv[1]) - if command == 'test': - self.testOpts = argv[2:] - else: - self.parseOpts(argv[2:]) - except IndexError: - usage(HELP_PAGE1, "not enough command-line arguments") - - # Step 2: Call the command - meths = (self.compile, self.fill, self.help, self.options, - self.test, self.version) - for meth in meths: - methName = meth.__name__ - # Or meth.im_func.func_name - # Or meth.func_name (Python >= 2.1 only, sometimes works on 2.0) - methInitial = methName[0] - if command in (methName, methInitial): - sys.argv[0] += (" " + methName) - # @@MO: I don't necessarily agree sys.argv[0] should be - # modified. - meth() - return - # If none of the commands matched. - usage(HELP_PAGE1, "unknown command '%s'" % command) - - def parseOpts(self, args): - C, D, W = self.chatter, self.debug, self.warn - self.isCompile = isCompile = self.command[0] == 'c' - defaultOext = isCompile and ".py" or ".html" - parser = MyOptionParser() - pao = parser.add_option - pao("--idir", action="store", dest="idir", default="") - pao("--odir", action="store", dest="odir", default="") - pao("--iext", action="store", dest="iext", default=".tmpl") - pao("--oext", action="store", dest="oext", default=defaultOext) - pao("-R", action="store_true", dest="recurse", default=False) - pao("--stdout", "-p", action="store_true", dest="stdout", default=False) - pao("--debug", action="store_true", dest="debug", default=False) - pao("--env", action="store_true", dest="env", default=False) - pao("--pickle", action="store", dest="pickle", default="") - pao("--flat", action="store_true", dest="flat", default=False) - pao("--nobackup", action="store_true", dest="nobackup", default=False) - pao("--settings", action="store", dest="compilerSettingsString", default=None) - pao("--templateAPIClass", action="store", dest="templateClassName", default=None) - - self.opts, self.pathArgs = opts, files = parser.parse_args(args) - D("""\ -cheetah compile %s -Options are -%s -Files are %s""", args, pprint.pformat(vars(opts)), files) - - - #cleanup trailing path separators - seps = [sep for sep in [os.sep, os.altsep] if sep] - for attr in ['idir', 'odir']: - for sep in seps: - path = getattr(opts, attr, None) - if path and path.endswith(sep): - path = path[:-len(sep)] - setattr(opts, attr, path) - break - - self._fixExts() - if opts.env: - self.searchList.insert(0, os.environ) - if opts.pickle: - f = open(opts.pickle, 'rb') - unpickled = pickle.load(f) - f.close() - self.searchList.insert(0, unpickled) - opts.verbose = not opts.stdout - - ################################################## - ## COMMAND METHODS - - def compile(self): - self._compileOrFill() - - def fill(self): - from Cheetah.ImportHooks import install - install() - self._compileOrFill() - - def help(self): - usage(HELP_PAGE1, "", sys.stdout) - - def options(self): - usage(HELP_PAGE2, "", sys.stdout) - - def test(self): - # @@MO: Ugly kludge. - TEST_WRITE_FILENAME = 'cheetah_test_file_creation_ability.tmp' - try: - f = open(TEST_WRITE_FILENAME, 'w') - except: - sys.exit("""\ -Cannot run the tests because you don't have write permission in the current -directory. The tests need to create temporary files. Change to a directory -you do have write permission to and re-run the tests.""") - else: - f.close() - os.remove(TEST_WRITE_FILENAME) - # @@MO: End ugly kludge. - from Cheetah.Tests import Test - import Cheetah.Tests.unittest_local_copy as unittest - del sys.argv[1:] # Prevent unittest from misinterpreting options. - sys.argv.extend(self.testOpts) - #unittest.main(testSuite=Test.testSuite) - #unittest.main(testSuite=Test.testSuite) - unittest.main(module=Test) - - def version(self): - print Version - - # If you add a command, also add it to the 'meths' variable in main(). - - ################################################## - ## LOGGING METHODS - - def chatter(self, format, *args): - """Print a verbose message to stdout. But don't if .opts.stdout is - true or .opts.verbose is false. - """ - if self.opts.stdout or not self.opts.verbose: - return - fprintfMessage(sys.stdout, format, *args) - - - def debug(self, format, *args): - """Print a debugging message to stderr, but don't if .debug is - false. - """ - if self.opts.debug: - fprintfMessage(sys.stderr, format, *args) - - def warn(self, format, *args): - """Always print a warning message to stderr. - """ - fprintfMessage(sys.stderr, format, *args) - - def error(self, format, *args): - """Always print a warning message to stderr and exit with an error code. - """ - fprintfMessage(sys.stderr, format, *args) - sys.exit(1) - - ################################################## - ## HELPER METHODS - - - def _fixExts(self): - assert self.opts.oext, "oext is empty!" - iext, oext = self.opts.iext, self.opts.oext - if iext and not iext.startswith("."): - self.opts.iext = "." + iext - if oext and not oext.startswith("."): - self.opts.oext = "." + oext - - - - def _compileOrFill(self): - C, D, W = self.chatter, self.debug, self.warn - opts, files = self.opts, self.pathArgs - if files == ["-"]: - self._compileOrFillStdin() - return - elif not files and opts.recurse: - which = opts.idir and "idir" or "current" - C("Drilling down recursively from %s directory.", which) - sourceFiles = [] - dir = os.path.join(self.opts.idir, os.curdir) - os.path.walk(dir, self._expandSourceFilesWalk, sourceFiles) - elif not files: - usage(HELP_PAGE1, "Neither files nor -R specified!") - else: - sourceFiles = self._expandSourceFiles(files, opts.recurse, True) - sourceFiles = [os.path.normpath(x) for x in sourceFiles] - D("All source files found: %s", sourceFiles) - bundles = self._getBundles(sourceFiles) - D("All bundles: %s", pprint.pformat(bundles)) - if self.opts.flat: - self._checkForCollisions(bundles) - for b in bundles: - self._compileOrFillBundle(b) - - def _checkForCollisions(self, bundles): - """Check for multiple source paths writing to the same destination - path. - """ - C, D, W = self.chatter, self.debug, self.warn - isError = False - dstSources = {} - for b in bundles: - if dstSources.has_key(b.dst): - dstSources[b.dst].append(b.src) - else: - dstSources[b.dst] = [b.src] - keys = dstSources.keys() - keys.sort() - for dst in keys: - sources = dstSources[dst] - if len(sources) > 1: - isError = True - sources.sort() - fmt = "Collision: multiple source files %s map to one destination file %s" - W(fmt, sources, dst) - if isError: - what = self.isCompile and "Compilation" or "Filling" - sys.exit("%s aborted due to collisions" % what) - - - def _expandSourceFilesWalk(self, arg, dir, files): - """Recursion extension for .expandSourceFiles(). - This method is a callback for os.path.walk(). - 'arg' is a list to which successful paths will be appended. - """ - iext = self.opts.iext - for f in files: - path = os.path.join(dir, f) - if path.endswith(iext) and os.path.isfile(path): - arg.append(path) - elif os.path.islink(path) and os.path.isdir(path): - os.path.walk(path, self._expandSourceFilesWalk, arg) - # If is directory, do nothing; 'walk' will eventually get it. - - - def _expandSourceFiles(self, files, recurse, addIextIfMissing): - """Calculate source paths from 'files' by applying the - command-line options. - """ - C, D, W = self.chatter, self.debug, self.warn - idir = self.opts.idir - iext = self.opts.iext - files = [] - for f in self.pathArgs: - oldFilesLen = len(files) - D("Expanding %s", f) - path = os.path.join(idir, f) - pathWithExt = path + iext # May or may not be valid. - if os.path.isdir(path): - if recurse: - os.path.walk(path, self._expandSourceFilesWalk, files) - else: - raise Error("source file '%s' is a directory" % path) - elif os.path.isfile(path): - files.append(path) - elif (addIextIfMissing and not path.endswith(iext) and - os.path.isfile(pathWithExt)): - files.append(pathWithExt) - # Do not recurse directories discovered by iext appending. - elif os.path.exists(path): - W("Skipping source file '%s', not a plain file.", path) - else: - W("Skipping source file '%s', not found.", path) - if len(files) > oldFilesLen: - D(" ... found %s", files[oldFilesLen:]) - return files - - - def _getBundles(self, sourceFiles): - flat = self.opts.flat - idir = self.opts.idir - iext = self.opts.iext - nobackup = self.opts.nobackup - odir = self.opts.odir - oext = self.opts.oext - idirSlash = idir + os.sep - bundles = [] - for src in sourceFiles: - # 'base' is the subdirectory plus basename. - base = src - if idir and src.startswith(idirSlash): - base = src[len(idirSlash):] - if iext and base.endswith(iext): - base = base[:-len(iext)] - basename = os.path.basename(base) - if flat: - dst = os.path.join(odir, basename + oext) - else: - dbn = basename - if odir and base.startswith(os.sep): - odd = odir - while odd != '': - idx = base.find(odd) - if idx == 0: - dbn = base[len(odd):] - if dbn[0] == '/': - dbn = dbn[1:] - break - odd = os.path.dirname(odd) - if odd == '/': - break - dst = os.path.join(odir, dbn + oext) - else: - dst = os.path.join(odir, base + oext) - bak = dst + self.BACKUP_SUFFIX - b = Bundle(src=src, dst=dst, bak=bak, base=base, basename=basename) - bundles.append(b) - return bundles - - - def _getTemplateClass(self): - C, D, W = self.chatter, self.debug, self.warn - modname = None - if self._templateClass: - return self._templateClass - - modname = self.opts.templateClassName - - if not modname: - return Template - p = modname.rfind('.') - if ':' not in modname: - self.error('The value of option --templateAPIClass is invalid\n' - 'It must be in the form "module:class", ' - 'e.g. "Cheetah.Template:Template"') - - modname, classname = modname.split(':') - - C('using --templateAPIClass=%s:%s'%(modname, classname)) - - if p >= 0: - mod = getattr(__import__(modname[:p], {}, {}, [modname[p+1:]]), modname[p+1:]) - else: - mod = __import__(modname, {}, {}, []) - - klass = getattr(mod, classname, None) - if klass: - self._templateClass = klass - return klass - else: - self.error('**Template class specified in option --templateAPIClass not found\n' - '**Falling back on Cheetah.Template:Template') - - - def _getCompilerSettings(self): - if self._compilerSettings: - return self._compilerSettings - - def getkws(**kws): - return kws - if self.opts.compilerSettingsString: - try: - exec 'settings = getkws(%s)'%self.opts.compilerSettingsString - except: - self.error("There's an error in your --settings option." - "It must be valid Python syntax.\n" - +" --settings='%s'\n"%self.opts.compilerSettingsString - +" %s: %s"%sys.exc_info()[:2] - ) - - validKeys = DEFAULT_COMPILER_SETTINGS.keys() - if [k for k in settings.keys() if k not in validKeys]: - self.error( - 'The --setting "%s" is not a valid compiler setting name.'%k) - - self._compilerSettings = settings - return settings - else: - return {} - - def _compileOrFillStdin(self): - TemplateClass = self._getTemplateClass() - compilerSettings = self._getCompilerSettings() - if self.isCompile: - pysrc = TemplateClass.compile(file=sys.stdin, - compilerSettings=compilerSettings, - returnAClass=False) - output = pysrc - else: - output = str(TemplateClass(file=sys.stdin, compilerSettings=compilerSettings)) - sys.stdout.write(output) - - def _compileOrFillBundle(self, b): - C, D, W = self.chatter, self.debug, self.warn - TemplateClass = self._getTemplateClass() - compilerSettings = self._getCompilerSettings() - src = b.src - dst = b.dst - base = b.base - basename = b.basename - dstDir = os.path.dirname(dst) - what = self.isCompile and "Compiling" or "Filling" - C("%s %s -> %s^", what, src, dst) # No trailing newline. - if os.path.exists(dst) and not self.opts.nobackup: - bak = b.bak - C(" (backup %s)", bak) # On same line as previous message. - else: - bak = None - C("") - if self.isCompile: - if not moduleNameRE.match(basename): - tup = basename, src - raise Error("""\ -%s: base name %s contains invalid characters. It must -be named according to the same rules as Python modules.""" % tup) - pysrc = TemplateClass.compile(file=src, returnAClass=False, - moduleName=basename, - className=basename, - compilerSettings=compilerSettings) - output = pysrc - else: - #output = str(TemplateClass(file=src, searchList=self.searchList)) - tclass = TemplateClass.compile(file=src, compilerSettings=compilerSettings) - output = str(tclass(searchList=self.searchList)) - - if bak: - shutil.copyfile(dst, bak) - if dstDir and not os.path.exists(dstDir): - if self.isCompile: - mkdirsWithPyInitFiles(dstDir) - else: - os.makedirs(dstDir) - if self.opts.stdout: - sys.stdout.write(output) - else: - f = open(dst, 'w') - f.write(output) - f.close() - - -################################################## -## if run from the command line -if __name__ == '__main__': CheetahWrapper().main() - -# vim: shiftwidth=4 tabstop=4 expandtab diff --git a/Cheetah/FileUtils.py b/Cheetah/FileUtils.py deleted file mode 100644 index c3749f50..00000000 --- a/Cheetah/FileUtils.py +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env python -# $Id: FileUtils.py,v 1.12 2005/11/02 22:26:07 tavis_rudd Exp $ -"""File utitilies for Python: - -Meta-Data -================================================================================ -Author: Tavis Rudd -License: This software is released for unlimited distribution under the - terms of the MIT license. See the LICENSE file. -Version: $Revision: 1.12 $ -Start Date: 2001/09/26 -Last Revision Date: $Date: 2005/11/02 22:26:07 $ -""" -__author__ = "Tavis Rudd " -__revision__ = "$Revision: 1.12 $"[11:-2] - - -from glob import glob -import os -from os import listdir -import os.path -import re -from types import StringType -from tempfile import mktemp - -def _escapeRegexChars(txt, - escapeRE=re.compile(r'([\$\^\*\+\.\?\{\}\[\]\(\)\|\\])')): - return escapeRE.sub(r'\\\1' , txt) - -def findFiles(*args, **kw): - """Recursively find all the files matching a glob pattern. - - This function is a wrapper around the FileFinder class. See its docstring - for details about the accepted arguments, etc.""" - - return FileFinder(*args, **kw).files() - -def replaceStrInFiles(files, theStr, repl): - - """Replace all instances of 'theStr' with 'repl' for each file in the 'files' - list. Returns a dictionary with data about the matches found. - - This is like string.replace() on a multi-file basis. - - This function is a wrapper around the FindAndReplace class. See its - docstring for more details.""" - - pattern = _escapeRegexChars(theStr) - return FindAndReplace(files, pattern, repl).results() - -def replaceRegexInFiles(files, pattern, repl): - - """Replace all instances of regex 'pattern' with 'repl' for each file in the - 'files' list. Returns a dictionary with data about the matches found. - - This is like re.sub on a multi-file basis. - - This function is a wrapper around the FindAndReplace class. See its - docstring for more details.""" - - return FindAndReplace(files, pattern, repl).results() - - -################################################## -## CLASSES - -class FileFinder: - - """Traverses a directory tree and finds all files in it that match one of - the specified glob patterns.""" - - def __init__(self, rootPath, - globPatterns=('*',), - ignoreBasenames=('CVS','.svn'), - ignoreDirs=(), - ): - - self._rootPath = rootPath - self._globPatterns = globPatterns - self._ignoreBasenames = ignoreBasenames - self._ignoreDirs = ignoreDirs - self._files = [] - - self.walkDirTree(rootPath) - - def walkDirTree(self, dir='.', - - listdir=os.listdir, - isdir=os.path.isdir, - join=os.path.join, - ): - - """Recursively walk through a directory tree and find matching files.""" - processDir = self.processDir - filterDir = self.filterDir - - pendingDirs = [dir] - addDir = pendingDirs.append - getDir = pendingDirs.pop - - while pendingDirs: - dir = getDir() - ## process this dir - processDir(dir) - - ## and add sub-dirs - for baseName in listdir(dir): - fullPath = join(dir, baseName) - if isdir(fullPath): - if filterDir(baseName, fullPath): - addDir( fullPath ) - - def filterDir(self, baseName, fullPath): - - """A hook for filtering out certain dirs. """ - - return not (baseName in self._ignoreBasenames or - fullPath in self._ignoreDirs) - - def processDir(self, dir, glob=glob): - extend = self._files.extend - for pattern in self._globPatterns: - extend( glob(os.path.join(dir, pattern)) ) - - def files(self): - return self._files - -class _GenSubberFunc: - - """Converts a 'sub' string in the form that one feeds to re.sub (backrefs, - groups, etc.) into a function that can be used to do the substitutions in - the FindAndReplace class.""" - - backrefRE = re.compile(r'\\([1-9][0-9]*)') - groupRE = re.compile(r'\\g<([a-zA-Z_][a-zA-Z_]*)>') - - def __init__(self, replaceStr): - self._src = replaceStr - self._pos = 0 - self._codeChunks = [] - self.parse() - - def src(self): - return self._src - - def pos(self): - return self._pos - - def setPos(self, pos): - self._pos = pos - - def atEnd(self): - return self._pos >= len(self._src) - - def advance(self, offset=1): - self._pos += offset - - def readTo(self, to, start=None): - if start == None: - start = self._pos - self._pos = to - if self.atEnd(): - return self._src[start:] - else: - return self._src[start:to] - - ## match and get methods - - def matchBackref(self): - return self.backrefRE.match(self.src(), self.pos()) - - def getBackref(self): - m = self.matchBackref() - self.setPos(m.end()) - return m.group(1) - - def matchGroup(self): - return self.groupRE.match(self.src(), self.pos()) - - def getGroup(self): - m = self.matchGroup() - self.setPos(m.end()) - return m.group(1) - - ## main parse loop and the eat methods - - def parse(self): - while not self.atEnd(): - if self.matchBackref(): - self.eatBackref() - elif self.matchGroup(): - self.eatGroup() - else: - self.eatStrConst() - - def eatStrConst(self): - startPos = self.pos() - while not self.atEnd(): - if self.matchBackref() or self.matchGroup(): - break - else: - self.advance() - strConst = self.readTo(self.pos(), start=startPos) - self.addChunk(repr(strConst)) - - def eatBackref(self): - self.addChunk( 'm.group(' + self.getBackref() + ')' ) - - def eatGroup(self): - self.addChunk( 'm.group("' + self.getGroup() + '")' ) - - def addChunk(self, chunk): - self._codeChunks.append(chunk) - - ## code wrapping methods - - def codeBody(self): - return ', '.join(self._codeChunks) - - def code(self): - return "def subber(m):\n\treturn ''.join([%s])\n" % (self.codeBody()) - - def subberFunc(self): - exec self.code() - return subber - - -class FindAndReplace: - - """Find and replace all instances of 'patternOrRE' with 'replacement' for - each file in the 'files' list. This is a multi-file version of re.sub(). - - 'patternOrRE' can be a raw regex pattern or - a regex object as generated by the re module. 'replacement' can be any - string that would work with patternOrRE.sub(replacement, fileContents). - """ - - def __init__(self, files, patternOrRE, replacement, - recordResults=True): - - - if type(patternOrRE) == StringType: - self._regex = re.compile(patternOrRE) - else: - self._regex = patternOrRE - if type(replacement) == StringType: - self._subber = _GenSubberFunc(replacement).subberFunc() - else: - self._subber = replacement - - self._pattern = pattern = self._regex.pattern - self._files = files - self._results = {} - self._recordResults = recordResults - - ## see if we should use pgrep to do the file matching - self._usePgrep = False - if (os.popen3('pgrep')[2].read()).startswith('Usage:'): - ## now check to make sure pgrep understands the pattern - tmpFile = mktemp() - open(tmpFile, 'w').write('#') - if not (os.popen3('pgrep "' + pattern + '" ' + tmpFile)[2].read()): - # it didn't print an error msg so we're ok - self._usePgrep = True - os.remove(tmpFile) - - self._run() - - def results(self): - return self._results - - def _run(self): - regex = self._regex - subber = self._subDispatcher - usePgrep = self._usePgrep - pattern = self._pattern - for file in self._files: - if not os.path.isfile(file): - continue # skip dirs etc. - - self._currFile = file - found = False - if locals().has_key('orig'): - del orig - if self._usePgrep: - if os.popen('pgrep "' + pattern + '" ' + file ).read(): - found = True - else: - orig = open(file).read() - if regex.search(orig): - found = True - if found: - if not locals().has_key('orig'): - orig = open(file).read() - new = regex.sub(subber, orig) - open(file, 'w').write(new) - - def _subDispatcher(self, match): - if self._recordResults: - if not self._results.has_key(self._currFile): - res = self._results[self._currFile] = {} - res['count'] = 0 - res['matches'] = [] - else: - res = self._results[self._currFile] - res['count'] += 1 - res['matches'].append({'contents':match.group(), - 'start':match.start(), - 'end':match.end(), - } - ) - return self._subber(match) - - -class SourceFileStats: - - """ - """ - - _fileStats = None - - def __init__(self, files): - self._fileStats = stats = {} - for file in files: - stats[file] = self.getFileStats(file) - - def rawStats(self): - return self._fileStats - - def summary(self): - codeLines = 0 - blankLines = 0 - commentLines = 0 - totalLines = 0 - for fileStats in self.rawStats().values(): - codeLines += fileStats['codeLines'] - blankLines += fileStats['blankLines'] - commentLines += fileStats['commentLines'] - totalLines += fileStats['totalLines'] - - stats = {'codeLines':codeLines, - 'blankLines':blankLines, - 'commentLines':commentLines, - 'totalLines':totalLines, - } - return stats - - def printStats(self): - pass - - def getFileStats(self, fileName): - codeLines = 0 - blankLines = 0 - commentLines = 0 - commentLineRe = re.compile(r'\s#.*$') - blankLineRe = re.compile('\s$') - lines = open(fileName).read().splitlines() - totalLines = len(lines) - - for line in lines: - if commentLineRe.match(line): - commentLines += 1 - elif blankLineRe.match(line): - blankLines += 1 - else: - codeLines += 1 - - stats = {'codeLines':codeLines, - 'blankLines':blankLines, - 'commentLines':commentLines, - 'totalLines':totalLines, - } - - return stats diff --git a/Cheetah/ImportHooks.py b/Cheetah/ImportHooks.py deleted file mode 100755 index 34aa6ac4..00000000 --- a/Cheetah/ImportHooks.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python -# $Id: ImportHooks.py,v 1.27 2007/11/16 18:28:47 tavis_rudd Exp $ - -"""Provides some import hooks to allow Cheetah's .tmpl files to be imported -directly like Python .py modules. - -To use these: - import Cheetah.ImportHooks - Cheetah.ImportHooks.install() - -Meta-Data -================================================================================ -Author: Tavis Rudd -License: This software is released for unlimited distribution under the - terms of the MIT license. See the LICENSE file. -Version: $Revision: 1.27 $ -Start Date: 2001/03/30 -Last Revision Date: $Date: 2007/11/16 18:28:47 $ -""" -__author__ = "Tavis Rudd " -__revision__ = "$Revision: 1.27 $"[11:-2] - -import sys -import os.path -import types -import __builtin__ -import new -import imp -from threading import RLock -import string -import traceback -from Cheetah import ImportManager -from Cheetah.ImportManager import DirOwner -from Cheetah.Compiler import Compiler -from Cheetah.convertTmplPathToModuleName import convertTmplPathToModuleName - -_installed = False - -################################################## -## HELPER FUNCS - -_cacheDir = [] -def setCacheDir(cacheDir): - global _cacheDir - _cacheDir.append(cacheDir) - -################################################## -## CLASSES - -class CheetahDirOwner(DirOwner): - _lock = RLock() - _acquireLock = _lock.acquire - _releaseLock = _lock.release - - templateFileExtensions = ('.tmpl',) - - def getmod(self, name): - self._acquireLock() - try: - mod = DirOwner.getmod(self, name) - if mod: - return mod - - for ext in self.templateFileExtensions: - tmplPath = os.path.join(self.path, name + ext) - if os.path.exists(tmplPath): - try: - return self._compile(name, tmplPath) - except: - # @@TR: log the error - exc_txt = traceback.format_exc() - exc_txt =' '+(' \n'.join(exc_txt.splitlines())) - raise ImportError( - 'Error while compiling Cheetah module' - ' %(name)s, original traceback follows:\n%(exc_txt)s'%locals()) - ## - return None - - finally: - self._releaseLock() - - def _compile(self, name, tmplPath): - ## @@ consider adding an ImportError raiser here - code = str(Compiler(file=tmplPath, moduleName=name, - mainClassName=name)) - if _cacheDir: - __file__ = os.path.join(_cacheDir[0], - convertTmplPathToModuleName(tmplPath)) + '.py' - try: - open(__file__, 'w').write(code) - except OSError: - ## @@ TR: need to add some error code here - traceback.print_exc(file=sys.stderr) - __file__ = tmplPath - else: - __file__ = tmplPath - co = compile(code+'\n', __file__, 'exec') - - mod = imp.new_module(name) - mod.__file__ = co.co_filename - if _cacheDir: - mod.__orig_file__ = tmplPath # @@TR: this is used in the WebKit - # filemonitoring code - mod.__co__ = co - return mod - - -################################################## -## FUNCTIONS - -def install(templateFileExtensions=('.tmpl',)): - """Install the Cheetah Import Hooks""" - - global _installed - if not _installed: - CheetahDirOwner.templateFileExtensions = templateFileExtensions - import __builtin__ - if type(__builtin__.__import__) == types.BuiltinFunctionType: - global __oldimport__ - __oldimport__ = __builtin__.__import__ - ImportManager._globalOwnerTypes.insert(0, CheetahDirOwner) - #ImportManager._globalOwnerTypes.append(CheetahDirOwner) - global _manager - _manager=ImportManager.ImportManager() - _manager.setThreaded() - _manager.install() - -def uninstall(): - """Uninstall the Cheetah Import Hooks""" - global _installed - if not _installed: - import __builtin__ - if type(__builtin__.__import__) == types.MethodType: - __builtin__.__import__ = __oldimport__ - global _manager - del _manager - -if __name__ == '__main__': - install() diff --git a/Cheetah/ImportManager.py b/Cheetah/ImportManager.py deleted file mode 100755 index eaf398a5..00000000 --- a/Cheetah/ImportManager.py +++ /dev/null @@ -1,561 +0,0 @@ -#!/usr/bin/env python -# $Id: ImportManager.py,v 1.6 2007/04/03 01:56:24 tavis_rudd Exp $ - -"""Provides an emulator/replacement for Python's standard import system. - -@@TR: Be warned that Import Hooks are in the deepest, darkest corner of Python's -jungle. If you need to start hacking with this, be prepared to get lost for a -while. Also note, this module predates the newstyle import hooks in Python 2.3 -http://www.python.org/peps/pep-0302.html. - - -This is a hacked/documented version of Gordon McMillan's iu.py. I have: - - - made it a little less terse - - - added docstrings and explanatations - - - standardized the variable naming scheme - - - reorganized the code layout to enhance readability - -Meta-Data -================================================================================ -Author: Tavis Rudd based on Gordon McMillan's iu.py -License: This software is released for unlimited distribution under the - terms of the MIT license. See the LICENSE file. -Version: $Revision: 1.6 $ -Start Date: 2001/03/30 -Last Revision Date: $Date: 2007/04/03 01:56:24 $ -""" -__author__ = "Tavis Rudd " -__revision__ = "$Revision: 1.6 $"[11:-2] - -################################################## -## DEPENDENCIES - -import sys -import imp -import marshal - -################################################## -## CONSTANTS & GLOBALS - -try: - True,False -except NameError: - True, False = (1==1),(1==0) - -_installed = False - -STRINGTYPE = type('') - -# _globalOwnerTypes is defined at the bottom of this file - -_os_stat = _os_path_join = _os_getcwd = _os_path_dirname = None - -################################################## -## FUNCTIONS - -def _os_bootstrap(): - """Set up 'os' module replacement functions for use during import bootstrap.""" - - names = sys.builtin_module_names - - join = dirname = None - if 'posix' in names: - sep = '/' - from posix import stat, getcwd - elif 'nt' in names: - sep = '\\' - from nt import stat, getcwd - elif 'dos' in names: - sep = '\\' - from dos import stat, getcwd - elif 'os2' in names: - sep = '\\' - from os2 import stat, getcwd - elif 'mac' in names: - from mac import stat, getcwd - def join(a, b): - if a == '': - return b - if ':' not in a: - a = ':' + a - if a[-1:] != ':': - a = a + ':' - return a + b - else: - raise ImportError, 'no os specific module found' - - if join is None: - def join(a, b, sep=sep): - if a == '': - return b - lastchar = a[-1:] - if lastchar == '/' or lastchar == sep: - return a + b - return a + sep + b - - if dirname is None: - def dirname(a, sep=sep): - for i in range(len(a)-1, -1, -1): - c = a[i] - if c == '/' or c == sep: - return a[:i] - return '' - - global _os_stat - _os_stat = stat - - global _os_path_join - _os_path_join = join - - global _os_path_dirname - _os_path_dirname = dirname - - global _os_getcwd - _os_getcwd = getcwd - -_os_bootstrap() - -def packageName(s): - for i in range(len(s)-1, -1, -1): - if s[i] == '.': - break - else: - return '' - return s[:i] - -def nameSplit(s): - rslt = [] - i = j = 0 - for j in range(len(s)): - if s[j] == '.': - rslt.append(s[i:j]) - i = j+1 - if i < len(s): - rslt.append(s[i:]) - return rslt - -def getPathExt(fnm): - for i in range(len(fnm)-1, -1, -1): - if fnm[i] == '.': - return fnm[i:] - return '' - -def pathIsDir(pathname): - "Local replacement for os.path.isdir()." - try: - s = _os_stat(pathname) - except OSError: - return None - return (s[0] & 0170000) == 0040000 - -def getDescr(fnm): - ext = getPathExt(fnm) - for (suffix, mode, typ) in imp.get_suffixes(): - if suffix == ext: - return (suffix, mode, typ) - -################################################## -## CLASSES - -class Owner: - - """An Owner does imports from a particular piece of turf That is, there's - an Owner for each thing on sys.path There are owners for directories and - .pyz files. There could be owners for zip files, or even URLs. A - shadowpath (a dictionary mapping the names in sys.path to their owners) is - used so that sys.path (or a package's __path__) is still a bunch of strings, - """ - - def __init__(self, path): - self.path = path - - def __str__(self): - return self.path - - def getmod(self, nm): - return None - -class DirOwner(Owner): - - def __init__(self, path): - if path == '': - path = _os_getcwd() - if not pathIsDir(path): - raise ValueError, "%s is not a directory" % path - Owner.__init__(self, path) - - def getmod(self, nm, - getsuffixes=imp.get_suffixes, loadco=marshal.loads, newmod=imp.new_module): - - pth = _os_path_join(self.path, nm) - - possibles = [(pth, 0, None)] - if pathIsDir(pth): - possibles.insert(0, (_os_path_join(pth, '__init__'), 1, pth)) - py = pyc = None - for pth, ispkg, pkgpth in possibles: - for ext, mode, typ in getsuffixes(): - attempt = pth+ext - try: - st = _os_stat(attempt) - except: - pass - else: - if typ == imp.C_EXTENSION: - fp = open(attempt, 'rb') - mod = imp.load_module(nm, fp, attempt, (ext, mode, typ)) - mod.__file__ = attempt - return mod - elif typ == imp.PY_SOURCE: - py = (attempt, st) - else: - pyc = (attempt, st) - if py or pyc: - break - if py is None and pyc is None: - return None - while 1: - if pyc is None or py and pyc[1][8] < py[1][8]: - try: - co = compile(open(py[0], 'r').read()+'\n', py[0], 'exec') - break - except SyntaxError, e: - print "Invalid syntax in %s" % py[0] - print e.args - raise - elif pyc: - stuff = open(pyc[0], 'rb').read() - try: - co = loadco(stuff[8:]) - break - except (ValueError, EOFError): - pyc = None - else: - return None - mod = newmod(nm) - mod.__file__ = co.co_filename - if ispkg: - mod.__path__ = [pkgpth] - subimporter = PathImportDirector(mod.__path__) - mod.__importsub__ = subimporter.getmod - mod.__co__ = co - return mod - - -class ImportDirector(Owner): - """ImportDirectors live on the metapath There's one for builtins, one for - frozen modules, and one for sys.path Windows gets one for modules gotten - from the Registry Mac would have them for PY_RESOURCE modules etc. A - generalization of Owner - their concept of 'turf' is broader""" - - pass - -class BuiltinImportDirector(ImportDirector): - """Directs imports of builtin modules""" - def __init__(self): - self.path = 'Builtins' - - def getmod(self, nm, isbuiltin=imp.is_builtin): - if isbuiltin(nm): - mod = imp.load_module(nm, None, nm, ('','',imp.C_BUILTIN)) - return mod - return None - -class FrozenImportDirector(ImportDirector): - """Directs imports of frozen modules""" - - def __init__(self): - self.path = 'FrozenModules' - - def getmod(self, nm, - isFrozen=imp.is_frozen, loadMod=imp.load_module): - if isFrozen(nm): - mod = loadMod(nm, None, nm, ('','',imp.PY_FROZEN)) - if hasattr(mod, '__path__'): - mod.__importsub__ = lambda name, pname=nm, owner=self: owner.getmod(pname+'.'+name) - return mod - return None - - -class RegistryImportDirector(ImportDirector): - """Directs imports of modules stored in the Windows Registry""" - - def __init__(self): - self.path = "WindowsRegistry" - self.map = {} - try: - import win32api - ## import win32con - except ImportError: - pass - else: - HKEY_CURRENT_USER = -2147483647 - HKEY_LOCAL_MACHINE = -2147483646 - KEY_ALL_ACCESS = 983103 - subkey = r"Software\Python\PythonCore\%s\Modules" % sys.winver - for root in (HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE): - try: - hkey = win32api.RegOpenKeyEx(root, subkey, 0, KEY_ALL_ACCESS) - except: - pass - else: - numsubkeys, numvalues, lastmodified = win32api.RegQueryInfoKey(hkey) - for i in range(numsubkeys): - subkeyname = win32api.RegEnumKey(hkey, i) - hskey = win32api.RegOpenKeyEx(hkey, subkeyname, 0, KEY_ALL_ACCESS) - val = win32api.RegQueryValueEx(hskey, '') - desc = getDescr(val[0]) - self.map[subkeyname] = (val[0], desc) - hskey.Close() - hkey.Close() - break - - def getmod(self, nm): - stuff = self.map.get(nm) - if stuff: - fnm, desc = stuff - fp = open(fnm, 'rb') - mod = imp.load_module(nm, fp, fnm, desc) - mod.__file__ = fnm - return mod - return None - -class PathImportDirector(ImportDirector): - """Directs imports of modules stored on the filesystem.""" - - def __init__(self, pathlist=None, importers=None, ownertypes=None): - if pathlist is None: - self.path = sys.path - else: - self.path = pathlist - if ownertypes == None: - self._ownertypes = _globalOwnerTypes - else: - self._ownertypes = ownertypes - if importers: - self._shadowPath = importers - else: - self._shadowPath = {} - self._inMakeOwner = False - self._building = {} - - def getmod(self, nm): - mod = None - for thing in self.path: - if type(thing) is STRINGTYPE: - owner = self._shadowPath.get(thing, -1) - if owner == -1: - owner = self._shadowPath[thing] = self._makeOwner(thing) - if owner: - mod = owner.getmod(nm) - else: - mod = thing.getmod(nm) - if mod: - break - return mod - - def _makeOwner(self, path): - if self._building.get(path): - return None - self._building[path] = 1 - owner = None - for klass in self._ownertypes: - try: - # this may cause an import, which may cause recursion - # hence the protection - owner = klass(path) - except: - pass - else: - break - del self._building[path] - return owner - -#=================ImportManager============================# -# The one-and-only ImportManager -# ie, the builtin import - -UNTRIED = -1 - -class ImportManager: - # really the equivalent of builtin import - def __init__(self): - self.metapath = [ - BuiltinImportDirector(), - FrozenImportDirector(), - RegistryImportDirector(), - PathImportDirector() - ] - self.threaded = 0 - self.rlock = None - self.locker = None - self.setThreaded() - - def setThreaded(self): - thread = sys.modules.get('thread', None) - if thread and not self.threaded: - self.threaded = 1 - self.rlock = thread.allocate_lock() - self._get_ident = thread.get_ident - - def install(self): - import __builtin__ - __builtin__.__import__ = self.importHook - __builtin__.reload = self.reloadHook - - def importHook(self, name, globals=None, locals=None, fromlist=None): - # first see if we could be importing a relative name - #print "importHook(%s, %s, locals, %s)" % (name, globals['__name__'], fromlist) - _sys_modules_get = sys.modules.get - contexts = [None] - if globals: - importernm = globals.get('__name__', '') - if importernm: - if hasattr(_sys_modules_get(importernm), '__path__'): - contexts.insert(0,importernm) - else: - pkgnm = packageName(importernm) - if pkgnm: - contexts.insert(0,pkgnm) - # so contexts is [pkgnm, None] or just [None] - # now break the name being imported up so we get: - # a.b.c -> [a, b, c] - nmparts = nameSplit(name) - _self_doimport = self.doimport - threaded = self.threaded - for context in contexts: - ctx = context - for i in range(len(nmparts)): - nm = nmparts[i] - #print " importHook trying %s in %s" % (nm, ctx) - if ctx: - fqname = ctx + '.' + nm - else: - fqname = nm - if threaded: - self._acquire() - mod = _sys_modules_get(fqname, UNTRIED) - if mod is UNTRIED: - mod = _self_doimport(nm, ctx, fqname) - if threaded: - self._release() - if mod: - ctx = fqname - else: - break - else: - # no break, point i beyond end - i = i + 1 - if i: - break - - if i= len(fromlist): - break - nm = fromlist[i] - i = i + 1 - if not hasattr(bottommod, nm): - if self.threaded: - self._acquire() - mod = self.doimport(nm, ctx, ctx+'.'+nm) - if self.threaded: - self._release() - if not mod: - raise ImportError, "%s not found in %s" % (nm, ctx) - #print "importHook done with %s %s %s (case 3)" % (name, globals['__name__'], fromlist) - return bottommod - - def doimport(self, nm, parentnm, fqname): - # Not that nm is NEVER a dotted name at this point - #print "doimport(%s, %s, %s)" % (nm, parentnm, fqname) - if parentnm: - parent = sys.modules[parentnm] - if hasattr(parent, '__path__'): - importfunc = getattr(parent, '__importsub__', None) - if not importfunc: - subimporter = PathImportDirector(parent.__path__) - importfunc = parent.__importsub__ = subimporter.getmod - mod = importfunc(nm) - if mod: - setattr(parent, nm, mod) - else: - #print "..parent not a package" - return None - else: - # now we're dealing with an absolute import - for director in self.metapath: - mod = director.getmod(nm) - if mod: - break - if mod: - mod.__name__ = fqname - sys.modules[fqname] = mod - if hasattr(mod, '__co__'): - co = mod.__co__ - del mod.__co__ - exec co in mod.__dict__ - if fqname == 'thread' and not self.threaded: -## print "thread detected!" - self.setThreaded() - else: - sys.modules[fqname] = None - #print "..found %s" % mod - return mod - - def reloadHook(self, mod): - fqnm = mod.__name__ - nm = nameSplit(fqnm)[-1] - parentnm = packageName(fqnm) - newmod = self.doimport(nm, parentnm, fqnm) - mod.__dict__.update(newmod.__dict__) -## return newmod - - def _acquire(self): - if self.rlock.locked(): - if self.locker == self._get_ident(): - self.lockcount = self.lockcount + 1 -## print "_acquire incrementing lockcount to", self.lockcount - return - self.rlock.acquire() - self.locker = self._get_ident() - self.lockcount = 0 -## print "_acquire first time!" - - def _release(self): - if self.lockcount: - self.lockcount = self.lockcount - 1 -## print "_release decrementing lockcount to", self.lockcount - else: - self.rlock.release() -## print "_release releasing lock!" - - -################################################## -## MORE CONSTANTS & GLOBALS - -_globalOwnerTypes = [ - DirOwner, - Owner, -] diff --git a/Cheetah/TemplateCmdLineIface.py b/Cheetah/TemplateCmdLineIface.py deleted file mode 100644 index abd8ae25..00000000 --- a/Cheetah/TemplateCmdLineIface.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# $Id: TemplateCmdLineIface.py,v 1.13 2006/01/10 20:34:35 tavis_rudd Exp $ - -"""Provides a command line interface to compiled Cheetah template modules. - -Meta-Data -================================================================================ -Author: Tavis Rudd -Version: $Revision: 1.13 $ -Start Date: 2001/12/06 -Last Revision Date: $Date: 2006/01/10 20:34:35 $ -""" -__author__ = "Tavis Rudd " -__revision__ = "$Revision: 1.13 $"[11:-2] - -import sys -import os -import getopt -import os.path -try: - from cPickle import load -except ImportError: - from pickle import load - -from Cheetah.Version import Version - -class Error(Exception): - pass - -class CmdLineIface: - """A command line interface to compiled Cheetah template modules.""" - - def __init__(self, templateObj, - scriptName=os.path.basename(sys.argv[0]), - cmdLineArgs=sys.argv[1:]): - - self._template = templateObj - self._scriptName = scriptName - self._cmdLineArgs = cmdLineArgs - - def run(self): - """The main program controller.""" - - self._processCmdLineArgs() - print self._template - - def _processCmdLineArgs(self): - try: - self._opts, self._args = getopt.getopt( - self._cmdLineArgs, 'h', ['help', - 'env', - 'pickle=', - ]) - - except getopt.GetoptError, v: - # print help information and exit: - print v - print self.usage() - sys.exit(2) - - for o, a in self._opts: - if o in ('-h','--help'): - print self.usage() - sys.exit() - if o == '--env': - self._template.searchList().insert(0, os.environ) - if o == '--pickle': - if a == '-': - unpickled = load(sys.stdin) - self._template.searchList().insert(0, unpickled) - else: - f = open(a) - unpickled = load(f) - f.close() - self._template.searchList().insert(0, unpickled) - - def usage(self): - return """Cheetah %(Version)s template module command-line interface - -Usage ------ - %(scriptName)s [OPTION] - -Options -------- - -h, --help Print this help information - - --env Use shell ENVIRONMENT variables to fill the - $placeholders in the template. - - --pickle Use a variables from a dictionary stored in Python - pickle file to fill $placeholders in the template. - If is - stdin is used: - '%(scriptName)s --pickle -' - -Description ------------ - -This interface allows you to execute a Cheetah template from the command line -and collect the output. It can prepend the shell ENVIRONMENT or a pickled -Python dictionary to the template's $placeholder searchList, overriding the -defaults for the $placeholders. - -""" % {'scriptName':self._scriptName, - 'Version':Version, - } - -# vim: shiftwidth=4 tabstop=4 expandtab diff --git a/Cheetah/Templates/SkeletonPage.py b/Cheetah/Templates/SkeletonPage.py deleted file mode 100644 index 0049d175..00000000 --- a/Cheetah/Templates/SkeletonPage.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env python - - -"""A Skeleton HTML page template, that provides basic structure and utility methods. -""" - - -################################################## -## DEPENDENCIES -import sys -import os -import os.path -from os.path import getmtime, exists -import time -import types -import __builtin__ -from Cheetah.Version import MinCompatibleVersion as RequiredCheetahVersion -from Cheetah.Version import MinCompatibleVersionTuple as RequiredCheetahVersionTuple -from Cheetah.Template import Template -from Cheetah.DummyTransaction import DummyTransaction -from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList -from Cheetah.CacheRegion import CacheRegion -import Cheetah.Filters as Filters -import Cheetah.ErrorCatchers as ErrorCatchers -from Cheetah.Templates._SkeletonPage import _SkeletonPage - -################################################## -## MODULE CONSTANTS -try: - True, False -except NameError: - True, False = (1==1), (1==0) -VFFSL=valueFromFrameOrSearchList -VFSL=valueFromSearchList -VFN=valueForName -currentTime=time.time -__CHEETAH_version__ = '2.0rc6' -__CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 6) -__CHEETAH_genTime__ = 1139107954.3640411 -__CHEETAH_genTimestamp__ = 'Sat Feb 4 18:52:34 2006' -__CHEETAH_src__ = 'src/Templates/SkeletonPage.tmpl' -__CHEETAH_srcLastModified__ = 'Mon Oct 7 11:37:30 2002' -__CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine' - -if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple: - raise AssertionError( - 'This template was compiled with Cheetah version' - ' %s. Templates compiled before version %s must be recompiled.'%( - __CHEETAH_version__, RequiredCheetahVersion)) - -################################################## -## CLASSES - -class SkeletonPage(_SkeletonPage): - - ################################################## - ## CHEETAH GENERATED METHODS - - - def __init__(self, *args, **KWs): - - _SkeletonPage.__init__(self, *args, **KWs) - if not self._CHEETAH__instanceInitialized: - cheetahKWArgs = {} - allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split() - for k,v in KWs.items(): - if k in allowedKWs: cheetahKWArgs[k] = v - self._initCheetahInstance(**cheetahKWArgs) - - - def writeHeadTag(self, **KWS): - - - - ## CHEETAH: generated from #block writeHeadTag at line 22, col 1. - trans = KWS.get("trans") - if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): - trans = self.transaction # is None unless self.awake() was called - if not trans: - trans = DummyTransaction() - _dummyTrans = True - else: _dummyTrans = False - write = trans.response().write - SL = self._CHEETAH__searchList - _filter = self._CHEETAH__currentFilter - - ######################################## - ## START - generated method body - - write('\n') - _v = VFFSL(SL,"title",True) # '$title' on line 24, col 8 - if _v is not None: write(_filter(_v, rawExpr='$title')) # from line 24, col 8. - write('\n') - _v = VFFSL(SL,"metaTags",True) # '$metaTags' on line 25, col 1 - if _v is not None: write(_filter(_v, rawExpr='$metaTags')) # from line 25, col 1. - write(' \n') - _v = VFFSL(SL,"stylesheetTags",True) # '$stylesheetTags' on line 26, col 1 - if _v is not None: write(_filter(_v, rawExpr='$stylesheetTags')) # from line 26, col 1. - write(' \n') - _v = VFFSL(SL,"javascriptTags",True) # '$javascriptTags' on line 27, col 1 - if _v is not None: write(_filter(_v, rawExpr='$javascriptTags')) # from line 27, col 1. - write('\n\n') - - ######################################## - ## END - generated method body - - return _dummyTrans and trans.response().getvalue() or "" - - - def writeBody(self, **KWS): - - - - ## CHEETAH: generated from #block writeBody at line 36, col 1. - trans = KWS.get("trans") - if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): - trans = self.transaction # is None unless self.awake() was called - if not trans: - trans = DummyTransaction() - _dummyTrans = True - else: _dummyTrans = False - write = trans.response().write - SL = self._CHEETAH__searchList - _filter = self._CHEETAH__currentFilter - - ######################################## - ## START - generated method body - - write('This skeleton page has no flesh. Its body needs to be implemented.\n') - - ######################################## - ## END - generated method body - - return _dummyTrans and trans.response().getvalue() or "" - - - def respond(self, trans=None): - - - - ## CHEETAH: main method generated for this template - if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): - trans = self.transaction # is None unless self.awake() was called - if not trans: - trans = DummyTransaction() - _dummyTrans = True - else: _dummyTrans = False - write = trans.response().write - SL = self._CHEETAH__searchList - _filter = self._CHEETAH__currentFilter - - ######################################## - ## START - generated method body - - - ## START CACHE REGION: ID=header. line 6, col 1 in the source. - _RECACHE_header = False - _cacheRegion_header = self.getCacheRegion(regionID='header', cacheInfo={'type': 2, 'id': 'header'}) - if _cacheRegion_header.isNew(): - _RECACHE_header = True - _cacheItem_header = _cacheRegion_header.getCacheItem('header') - if _cacheItem_header.hasExpired(): - _RECACHE_header = True - if (not _RECACHE_header) and _cacheItem_header.getRefreshTime(): - try: - _output = _cacheItem_header.renderOutput() - except KeyError: - _RECACHE_header = True - else: - write(_output) - del _output - if _RECACHE_header or not _cacheItem_header.getRefreshTime(): - _orig_transheader = trans - trans = _cacheCollector_header = DummyTransaction() - write = _cacheCollector_header.response().write - _v = VFFSL(SL,"docType",True) # '$docType' on line 7, col 1 - if _v is not None: write(_filter(_v, rawExpr='$docType')) # from line 7, col 1. - write('\n') - _v = VFFSL(SL,"htmlTag",True) # '$htmlTag' on line 8, col 1 - if _v is not None: write(_filter(_v, rawExpr='$htmlTag')) # from line 8, col 1. - write(''' - - - -''') - self.writeHeadTag(trans=trans) - write('\n') - trans = _orig_transheader - write = trans.response().write - _cacheData = _cacheCollector_header.response().getvalue() - _cacheItem_header.setData(_cacheData) - write(_cacheData) - del _cacheData - del _cacheCollector_header - del _orig_transheader - ## END CACHE REGION: header - - write('\n') - _v = VFFSL(SL,"bodyTag",True) # '$bodyTag' on line 34, col 1 - if _v is not None: write(_filter(_v, rawExpr='$bodyTag')) # from line 34, col 1. - write('\n\n') - self.writeBody(trans=trans) - write(''' - - - - - -''') - - ######################################## - ## END - generated method body - - return _dummyTrans and trans.response().getvalue() or "" - - ################################################## - ## CHEETAH GENERATED ATTRIBUTES - - - _CHEETAH__instanceInitialized = False - - _CHEETAH_version = __CHEETAH_version__ - - _CHEETAH_versionTuple = __CHEETAH_versionTuple__ - - _CHEETAH_genTime = __CHEETAH_genTime__ - - _CHEETAH_genTimestamp = __CHEETAH_genTimestamp__ - - _CHEETAH_src = __CHEETAH_src__ - - _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__ - - _mainCheetahMethod_for_SkeletonPage= 'respond' - -## END CLASS DEFINITION - -if not hasattr(SkeletonPage, '_initCheetahAttributes'): - templateAPIClass = getattr(SkeletonPage, '_CHEETAH_templateClass', Template) - templateAPIClass._addCheetahPlumbingCodeToClass(SkeletonPage) - - -# CHEETAH was developed by Tavis Rudd and Mike Orr -# with code, advice and input from many other volunteers. -# For more information visit http://www.CheetahTemplate.org/ - -################################################## -## if run from command line: -if __name__ == '__main__': - from Cheetah.TemplateCmdLineIface import CmdLineIface - CmdLineIface(templateObj=SkeletonPage()).run() - - diff --git a/Cheetah/Templates/SkeletonPage.tmpl b/Cheetah/Templates/SkeletonPage.tmpl deleted file mode 100644 index 43c5ecdd..00000000 --- a/Cheetah/Templates/SkeletonPage.tmpl +++ /dev/null @@ -1,44 +0,0 @@ -##doc-module: A Skeleton HTML page template, that provides basic structure and utility methods. -################################################################################ -#extends Cheetah.Templates._SkeletonPage -#implements respond -################################################################################ -#cache id='header' -$docType -$htmlTag - - - -#block writeHeadTag - -$title -$metaTags -$stylesheetTags -$javascriptTags - -#end block writeHeadTag - -#end cache header -################# - -$bodyTag - -#block writeBody -This skeleton page has no flesh. Its body needs to be implemented. -#end block writeBody - - - - - - diff --git a/Cheetah/Templates/_SkeletonPage.py b/Cheetah/Templates/_SkeletonPage.py deleted file mode 100644 index bf10e304..00000000 --- a/Cheetah/Templates/_SkeletonPage.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python -# $Id: _SkeletonPage.py,v 1.13 2002/10/01 17:52:02 tavis_rudd Exp $ -"""A baseclass for the SkeletonPage template - -Meta-Data -========== -Author: Tavis Rudd , -Version: $Revision: 1.13 $ -Start Date: 2001/04/05 -Last Revision Date: $Date: 2002/10/01 17:52:02 $ -""" -__author__ = "Tavis Rudd " -__revision__ = "$Revision: 1.13 $"[11:-2] - -################################################## -## DEPENDENCIES ## - -import time, types, os, sys - -# intra-package imports ... -from Cheetah.Template import Template - - -################################################## -## GLOBALS AND CONSTANTS ## - -True = (1==1) -False = (0==1) - -################################################## -## CLASSES ## - -class _SkeletonPage(Template): - """A baseclass for the SkeletonPage template""" - - docType = '' - - # docType = '' - - title = '' - siteDomainName = 'www.example.com' - siteCredits = 'Designed & Implemented by Tavis Rudd' - siteCopyrightName = "Tavis Rudd" - htmlTag = '' - - def __init__(self, *args, **KWs): - Template.__init__(self, *args, **KWs) - self._metaTags = {'HTTP-EQUIV':{'keywords':'Cheetah', - 'Content-Type':'text/html; charset=iso-8859-1', - }, - 'NAME':{'generator':'Cheetah: The Python-Powered Template Engine'} - } - # metaTags = {'HTTP_EQUIV':{'test':1234}, 'NAME':{'test':1234,'test2':1234} } - self._stylesheets = {} - # stylesheets = {'.cssClassName':'stylesheetCode'} - self._stylesheetsOrder = [] - # stylesheetsOrder = ['.cssClassName',] - self._stylesheetLibs = {} - # stylesheetLibs = {'libName':'libSrcPath'} - self._javascriptLibs = {} - self._javascriptTags = {} - # self._javascriptLibs = {'libName':'libSrcPath'} - self._bodyTagAttribs = {} - - def metaTags(self): - """Return a formatted vesion of the self._metaTags dictionary, using the - formatMetaTags function from Cheetah.Macros.HTML""" - - return self.formatMetaTags(self._metaTags) - - def stylesheetTags(self): - """Return a formatted version of the self._stylesheetLibs and - self._stylesheets dictionaries. The keys in self._stylesheets must - be listed in the order that they should appear in the list - self._stylesheetsOrder, to ensure that the style rules are defined in - the correct order.""" - - stylesheetTagsTxt = '' - for title, src in self._stylesheetLibs.items(): - stylesheetTagsTxt += '\n' - - if not self._stylesheetsOrder: - return stylesheetTagsTxt - - stylesheetTagsTxt += '\n' - - return stylesheetTagsTxt - - def javascriptTags(self): - """Return a formatted version of the javascriptTags and - javascriptLibs dictionaries. Each value in javascriptTags - should be a either a code string to include, or a list containing the - JavaScript version number and the code string. The keys can be anything. - The same applies for javascriptLibs, but the string should be the - SRC filename rather than a code string.""" - - javascriptTagsTxt = [] - for key, details in self._javascriptTags.items(): - if type(details) not in (types.ListType, types.TupleType): - details = ['',details] - - javascriptTagsTxt += ['\n'] - - - for key, details in self._javascriptLibs.items(): - if type(details) not in (types.ListType, types.TupleType): - details = ['',details] - - javascriptTagsTxt += [' + + ## Header Row + + + + + + + + #set $parent = '' + #set $folders = $name.split("/") + #set $current_folder = $folders.pop() + #set $parent = '/'.join($folders) + #if $parent != '' + + + + + #end if + #set $i = 0 + ## i variable is used to alternate colors of row + ## loop through passed data printing row for each show or folder + #for $video in $videos + #set $i += 1 + #set $j = $i%2 + + #if $video.is_dir + ## This is a folder + + + + + + #else + ## This is a show + + + + + + #end if + + #end for +
TitleSizeCapture Date
+ Up to Parent Folder +
$video.title $video.total_items Items$video.textDate + + + #if $video.episodeTitle + $escape($video.title): $escape($video.episodeTitle) + #else + $escape($video.title) + #end if + + #if $video.description + $escape($video.description) + #end if + #if $video.displayMajorNumbe and $video.callsign + $video.displayMajorNumber $video.callsign + #end if + + $video.textSize + $video.textDate
+

+ + + + +

+ + + \ No newline at end of file diff --git a/plugins/dvdvideo/templates/container_mob.tmpl b/plugins/dvdvideo/templates/container_mob.tmpl new file mode 100644 index 00000000..41609bfe --- /dev/null +++ b/plugins/dvdvideo/templates/container_mob.tmpl @@ -0,0 +1,88 @@ + + + +pyTivo - Push - $escape($name) + + + + +
+ +

pyTiVo

+ + #set $parent = '' + #set $folders = $name.split("/") + #set $current_folder = $folders.pop() + #set $parent = '/'.join($folders) + + + + + #set $i = 0 + ## i variable is used to alternate colors of row + ## loop through passed data printing row for each show or folder + #for $video in $videos + #set $i += 1 + #set $j = $i%2 + + #if $video.is_dir + ## This is a folder + + + + #else + ## This is a show + + + + #end if + + #end for +
$video.title + #if $video.episodeTitle + $video.title: $video.episodeTitle + #else + $video.title + #end if + + #if $video.description + $video.description + #end if + #if $video.displayMajorNumbe and $video.callsign + $video.displayMajorNumber $video.callsign + #end if + Added on $video.textDate + + +
+
+ + + + +
+
+ + diff --git a/plugins/dvdvideo/templates/container_xml.tmpl b/plugins/dvdvideo/templates/container_xml.tmpl new file mode 100644 index 00000000..3bb0c52b --- /dev/null +++ b/plugins/dvdvideo/templates/container_xml.tmpl @@ -0,0 +1,70 @@ + + + $start + #echo len($videos) # +
+ $escape($name) + x-container/tivo-videos + x-container/folder + $total + $crc($guid + $name) +
+ #for $video in $videos + #if $video.is_dir + +
+ $escape($video.title) + x-container/folder + x-tivo-container/tivo-dvr + $crc($guid + $video.small_path) + $video.total_items + $video.captureDate +
+ + + /TiVoConnect?Command=QueryContainer&Container=$quote($name)/$quote($video.name) + x-tivo-container/folder + + +
+ #else + +
+ $escape($video.title) + video/x-tivo-mpeg + #if not $video.valid + Yes + #end if + video/x-ms-wmv + $video.size + $video.duration + #if $video.isEpisode != 'false' and $video.episodeTitle + $escape($video.episodeTitle) + #end if + $escape($video.description) + $escape($video.displayMajorNumber) + $escape($video.callsign) + $video.seriesId + $video.captureDate +
+ + + video/x-tivo-mpeg + No + /$quote($container)$quote($video.part_path) + + + video/* + No + urn:tivo:image:save-until-i-delete-recording + + + text/xml + No + /TiVoConnect?Command=TVBusQuery&Container=$quote($container)&File=$quote($video.part_path) + + +
+ #end if + #end for +
\ No newline at end of file diff --git a/plugins/dvdvideo/transcode.py b/plugins/dvdvideo/transcode.py new file mode 100644 index 00000000..f1377ed9 --- /dev/null +++ b/plugins/dvdvideo/transcode.py @@ -0,0 +1,1178 @@ +# Module: dvdfolder.py +# Author: Eric von Bayer +# Updated By: Luke Broadbent +# Contact: +# Date: June 5, 2012 +# Description: +# Routines for transcoding DVD vob files to mpeg file the TiVo can play. +# This is closely aligned with transcode.py from the video plugin (from +# wmcbrine's branch). +# +# Copyright (c) 2009, Eric von Bayer +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The names of the contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import logging +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import time + +import lrucache + +import config +import metadata + +import vobstream + +logger = logging.getLogger('pyTivo.video.transcode') + +info_cache = lrucache.LRUCache(1000) +ffmpeg_procs = {} +reapers = {} + +GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97', + '30.00', '50.00', '59.94', '60.00'] + +BLOCKSIZE = 512 * 1024 +MAXBLOCKS = 2 +TIMEOUT = 600 + +UNSET = 0 +OLD_PAD = 1 +NEW_PAD = 2 + +pad_style = UNSET + +# XXX BIG HACK +# subprocess is broken for me on windows so super hack +def patchSubprocess(): + o = subprocess.Popen._make_inheritable + + def _make_inheritable(self, handle): + if not handle: return subprocess.GetCurrentProcess() + return o(self, handle) + + subprocess.Popen._make_inheritable = _make_inheritable +mswindows = (sys.platform == "win32") +if mswindows: + patchSubprocess() + +def debug(msg): + if type(msg) == str: + try: + msg = msg.decode('utf8') + except: + if sys.platform == 'darwin': + msg = msg.decode('macroman') + else: + msg = msg.decode('iso8859-1') + logger.debug(msg) + +def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): + settings = {'video_codec': select_videocodec(inFile, tsn), + 'video_br': select_videobr(inFile, tsn), + 'video_fps': select_videofps(inFile, tsn), + 'max_video_br': select_maxvideobr(tsn), + 'buff_size': select_buffsize(tsn), + 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), + 'audio_br': select_audiobr(tsn), + 'audio_fr': select_audiofr(inFile, tsn), + 'audio_ch': select_audioch(inFile, tsn), + 'audio_codec': select_audiocodec(isQuery, inFile, tsn), + 'audio_lang': select_audiolang(inFile, tsn), + 'ffmpeg_pram': select_ffmpegprams(tsn), + 'format': select_format(tsn, mime)} + + if isQuery: + return settings + + ffmpeg_path = config.get_bin('ffmpeg') + cmd_string = config.getFFmpegTemplate(tsn) % settings + fname = unicode(inFile, 'utf-8') + if mswindows: + fname = fname.encode('iso8859-1') + + if inFile[-5:].lower() == '.tivo': + tivodecode_path = config.get_bin('tivodecode') + tivo_mak = config.get_server('tivo_mak') + tcmd = [tivodecode_path, '-m', tivo_mak, fname] + tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE, + bufsize=(512 * 1024)) + if tivo_compatible(inFile, tsn)[0]: + cmd = '' + ffmpeg = tivodecode + else: + cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() + ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout, + stdout=subprocess.PIPE, + bufsize=(512 * 1024)) + elif vobstream.is_dvd(inFile): + cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() + ffmpeg = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + bufsize= BLOCKSIZE * MAXBLOCKS ) + proc = vobstream.vobstream(False, inFile, ffmpeg, BLOCKSIZE) + else: + cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), + stdout=subprocess.PIPE) + + if cmd: + debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') + debug(' '.join(cmd)) + + ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0, + 'last_read': time.time(), 'blocks': []} + if thead: + ffmpeg_procs[inFile]['blocks'].append(thead) + if vobstream.is_dvd(inFile): + ffmpeg_procs[inFile]['stream'] = proc['stream'] + ffmpeg_procs[inFile]['thread'] = proc['thread'] + ffmpeg_procs[inFile]['event'] = proc['event'] + + reap_process(inFile) + return resume_transfer(inFile, outFile, 0) + +def is_resumable(inFile, offset): + if inFile in ffmpeg_procs: + proc = ffmpeg_procs[inFile] + if proc['start'] <= offset < proc['end']: + return True + else: + cleanup(inFile) + kill(proc['process']) + return False + +def resume_transfer(inFile, outFile, offset): + proc = ffmpeg_procs[inFile] + offset -= proc['start'] + count = 0 + + try: + for block in proc['blocks']: + length = len(block) + if offset < length: + if offset > 0: + block = block[offset:] + outFile.write('%x\r\n' % len(block)) + outFile.write(block) + outFile.write('\r\n') + count += len(block) + offset -= length + outFile.flush() + except Exception, msg: + logger.info(msg) + return count + + proc['start'] = proc['end'] + proc['blocks'] = [] + + return count + transfer_blocks(inFile, outFile) + +def transfer_blocks(inFile, outFile): + proc = ffmpeg_procs[inFile] + blocks = proc['blocks'] + count = 0 + + while True: + try: + block = proc['process'].stdout.read(BLOCKSIZE) + proc['last_read'] = time.time() + except Exception, msg: + logger.info(msg) + cleanup(inFile) + kill(proc['process']) + break + + if not block: + try: + outFile.flush() + except Exception, msg: + logger.info(msg) + else: + cleanup(inFile) + break + + blocks.append(block) + proc['end'] += len(block) + if len(blocks) > MAXBLOCKS: + proc['start'] += len(blocks[0]) + blocks.pop(0) + + try: + outFile.write('%x\r\n' % len(block)) + outFile.write(block) + outFile.write('\r\n') + count += len(block) + except Exception, msg: + logger.info(msg) + break + + return count + +def reap_process(inFile): + if ffmpeg_procs and inFile in ffmpeg_procs: + proc = ffmpeg_procs[inFile] + if proc['last_read'] + TIMEOUT < time.time(): + del ffmpeg_procs[inFile] + del reapers[inFile] + kill(proc['process']) + else: + reaper = threading.Timer(TIMEOUT, reap_process, (inFile,)) + reapers[inFile] = reaper + reaper.start() + +def cleanup(inFile): + if vobstream.is_dvd(inFile): + proc = ffmpeg_procs[inFile] + kill(proc['process']) + proc['process'].wait() + + # Tell thread to break out of loop + proc['event'].set() + proc['thread'].join() + + del ffmpeg_procs[inFile] + reapers[inFile].cancel() + del reapers[inFile] + +def select_audiocodec(isQuery, inFile, tsn='', mime=''): + if inFile[-5:].lower() == '.tivo': + return '-acodec copy' + vInfo = video_info(inFile) + codectype = vInfo['vCodec'] + codec = config.get_tsn('audio_codec', tsn) + if not codec: + # Default, compatible with all TiVo's + codec = 'ac3' + if mime == 'video/mp4': + compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac', + 'ac3', 'liba52') + else: + compatiblecodecs = ('ac3', 'liba52', 'mp2') + + if vInfo['aCodec'] in compatiblecodecs: + aKbps = vInfo['aKbps'] + aCh = vInfo['aCh'] + if aKbps == None: + if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'): + # along with the channel check below this should + # pass any AAC audio that has undefined 'aKbps' and + # is <= 2 channels. Should be TiVo compatible. + codec = 'copy' + elif not isQuery: + vInfoQuery = audio_check(inFile, tsn) + if vInfoQuery == None: + aKbps = None + aCh = None + else: + aKbps = vInfoQuery['aKbps'] + aCh = vInfoQuery['aCh'] + else: + codec = 'TBA' + if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn): + # compatible codec and bitrate, do not reencode audio + codec = 'copy' + if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2): + codec = 'ac3' + copy_flag = config.get_tsn('copy_ts', tsn) + copyts = ' -copyts' + if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or + (copy_flag and copy_flag.lower() == 'false')): + copyts = '' + return '-acodec ' + codec + copyts + +def select_audiofr(inFile, tsn): + freq = '48000' # default + vInfo = video_info(inFile) + if vInfo['aFreq'] == '44100': + # compatible frequency + freq = vInfo['aFreq'] + audio_fr = config.get_tsn('audio_fr', tsn) + if audio_fr != None: + freq = audio_fr + return '-ar ' + freq + +def select_audioch(inFile, tsn): + ch = config.get_tsn('audio_ch', tsn) + if ch: + return '-ac ' + ch + # AC-3 max channels is 5.1 + if video_info(inFile)['aCh'] > 6: + debug('Too many audio channels for AC-3, using 5.1 instead') + return '-ac 6' + elif video_info(inFile)['aCh']: + return '-ac %i' % video_info(inFile)['aCh'] + return '' + +def select_audiolang(inFile, tsn): + vInfo = video_info(inFile) + audio_lang = config.get_tsn('audio_lang', tsn) + debug('audio_lang: %s' % audio_lang) + if vInfo['mapAudio']: + # default to first detected audio stream to begin with + stream = vInfo['mapAudio'][0][0] + if audio_lang != None and vInfo['mapVideo'] != None: + langmatch_curr = [] + langmatch_prev = vInfo['mapAudio'][:] + for lang in audio_lang.replace(' ', '').lower().split(','): + for s, l in langmatch_prev: + if lang in s + l.replace(' ', '').lower(): + langmatch_curr.append((s, l)) + stream = s + # if only 1 item matched we're done + if len(langmatch_curr) == 1: + break + # if more than 1 item matched copy the curr area to the prev + # array we only need to look at the new shorter list from + # now on + elif len(langmatch_curr) > 1: + langmatch_prev = langmatch_curr[:] + # if we drop out of the loop with more than 1 item default to + # the first item + if len(langmatch_prev) > 1: + stream = langmatch_prev[0][0] + # don't let FFmpeg auto select audio stream, pyTivo defaults to + # first detected + if stream: + debug('selected audio stream: %s' % stream) + return '-map ' + vInfo['mapVideo'] + ' -map ' + stream + # if no audio is found + debug('selected audio stream: None detected') + return '' + +def select_videofps(inFile, tsn): + vInfo = video_info(inFile) + fps = '-r 29.97' # default + if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS: + fps = ' ' + video_fps = config.get_tsn('video_fps', tsn) + if video_fps != None: + fps = '-r ' + video_fps + return fps + +def select_videocodec(inFile, tsn): + vInfo = video_info(inFile) + if tivo_compatible_video(vInfo, tsn)[0]: + codec = 'copy' + else: + codec = 'mpeg2video' # default + return '-vcodec ' + codec + +def select_videobr(inFile, tsn): + return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k' + +def select_videostr(inFile, tsn): + vInfo = video_info(inFile) + if tivo_compatible_video(vInfo, tsn)[0]: + video_str = int(vInfo['kbps']) + if vInfo['aKbps']: + video_str -= int(vInfo['aKbps']) + video_str *= 1000 + else: + video_str = config.strtod(config.getVideoBR(tsn)) + if config.isHDtivo(tsn): + if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0: + video_percent = (int(vInfo['kbps']) * 10 * + config.getVideoPCT(tsn)) + video_str = max(video_str, video_percent) + video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95, + video_str)) + return video_str + +def select_audiobr(tsn): + return '-ab ' + config.getAudioBR(tsn) + +def select_maxvideobr(tsn): + return '-maxrate ' + config.getMaxVideoBR(tsn) + +def select_buffsize(tsn): + return '-bufsize ' + config.getBuffSize(tsn) + +def select_ffmpegprams(tsn): + params = config.getFFmpegPrams(tsn) + if not params: + params = '' + return params + +def select_format(tsn, mime): + if mime == 'video/x-tivo-mpeg-ts': + fmt = 'mpegts' + else: + fmt = 'vob' + return '-f %s -' % fmt + +def pad_check(): + global pad_style + if pad_style == UNSET: + pad_style = OLD_PAD + filters = tempfile.TemporaryFile() + cmd = [config.get_bin('ffmpeg'), '-filters'] + ffmpeg = subprocess.Popen(cmd, stdout=filters, stderr=subprocess.PIPE) + ffmpeg.wait() + filters.seek(0) + for line in filters: + if line.startswith('pad'): + pad_style = NEW_PAD + break + filters.close() + return pad_style == NEW_PAD + +def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): + endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) / + vInfo['vWidth']) * multiplier) + if endHeight % 2: + endHeight -= 1 + if endHeight < TIVO_HEIGHT * 0.99: + topPadding = (TIVO_HEIGHT - endHeight) / 2 + if topPadding % 2: + topPadding -= 1 + newpad = pad_check() + if newpad: + return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), '-vf', + 'pad=%d:%d:0:%d' % (TIVO_WIDTH, TIVO_HEIGHT, topPadding)] + else: + bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding + return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), + '-padtop', str(topPadding), + '-padbottom', str(bottomPadding)] + else: # if only very small amount of padding needed, then + # just stretch it + return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + +def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): + endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) / + (vInfo['vHeight'] * multiplier)) + if endWidth % 2: + endWidth -= 1 + if endWidth < TIVO_WIDTH * 0.99: + leftPadding = (TIVO_WIDTH - endWidth) / 2 + if leftPadding % 2: + leftPadding -= 1 + newpad = pad_check() + if newpad: + return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), '-vf', + 'pad=%d:%d:%d:0' % (TIVO_WIDTH, TIVO_HEIGHT, leftPadding)] + else: + rightPadding = (TIVO_WIDTH - endWidth) - leftPadding + return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), + '-padleft', str(leftPadding), + '-padright', str(rightPadding)] + else: # if only very small amount of padding needed, then + # just stretch it + return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + +def select_aspect(inFile, tsn = ''): + TIVO_WIDTH = config.getTivoWidth(tsn) + TIVO_HEIGHT = config.getTivoHeight(tsn) + + vInfo = video_info(inFile) + + debug('tsn: %s' % tsn) + + aspect169 = config.get169Setting(tsn) + + debug('aspect169: %s' % aspect169) + + optres = config.getOptres(tsn) + + debug('optres: %s' % optres) + + if optres: + optHeight = config.nearestTivoHeight(vInfo['vHeight']) + optWidth = config.nearestTivoWidth(vInfo['vWidth']) + if optHeight < TIVO_HEIGHT: + TIVO_HEIGHT = optHeight + if optWidth < TIVO_WIDTH: + TIVO_WIDTH = optWidth + + if vInfo.get('par2'): + par2 = vInfo['par2'] + elif vInfo.get('par'): + par2 = float(vInfo['par']) + else: + # Assume PAR = 1.0 + par2 = 1.0 + + debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' + + 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'], + vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'], + vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH)) + + if config.isHDtivo(tsn) and not optres: + if config.getPixelAR(0) or vInfo['par']: + if vInfo['par2'] == None: + if vInfo['par']: + npar = par2 + else: + npar = config.getPixelAR(1) + else: + npar = par2 + + # adjust for pixel aspect ratio, if set + + if npar < 1.0: + return ['-s', '%dx%d' % (vInfo['vWidth'], + math.ceil(vInfo['vHeight'] / npar))] + elif npar > 1.0: + # FFMPEG expects width to be a multiple of two + return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2, + vInfo['vHeight'])] + + if vInfo['vHeight'] <= TIVO_HEIGHT: + # pass all resolutions to S3, except heights greater than + # conf height + return [] + # else, resize video. + + d = gcd(vInfo['vHeight'], vInfo['vWidth']) + rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d + debug('rheight=%s rwidth=%s' % (rheight, rwidth)) + + if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9': + debug('File + PAR is within 4:3.') + return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + + elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), + (59, 72), (59, 36), (59, 54)] or + vInfo['dar1'] == '4:3'): + debug('File is within 4:3 list.') + return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + + elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), + (59, 27)] or vInfo['dar1'] == '16:9') + and (aspect169 or config.get169Letterbox(tsn))): + debug('File is within 16:9 list and 16:9 allowed.') + + if config.get169Blacklist(tsn) or (aspect169 and + config.get169Letterbox(tsn)): + aspect = '4:3' + else: + aspect = '16:9' + return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + + else: + settings = ['-aspect'] + + multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2 + multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2 + ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight'] + debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio, + multiplier4by3)) + + # If video is wider than 4:3 add top and bottom padding + + if ratio > 133: # Might be 16:9 file, or just need padding on + # top and bottom + + if aspect169 and ratio > 135: # If file would fall in 4:3 + # assume it is supposed to be 4:3 + + if (config.get169Blacklist(tsn) or + config.get169Letterbox(tsn)): + settings.append('4:3') + else: + settings.append('16:9') + + if ratio > 177: # too short needs padding top and bottom + settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT, + multiplier16by9, vInfo) + debug(('16:9 aspect allowed, file is wider ' + + 'than 16:9 padding top and bottom\n%s') % + ' '.join(settings)) + + else: # too skinny needs padding on left and right. + settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, + multiplier16by9, vInfo) + debug(('16:9 aspect allowed, file is narrower ' + + 'than 16:9 padding left and right\n%s') % + ' '.join(settings)) + + else: # this is a 4:3 file or 16:9 output not allowed + if ratio > 135 and config.get169Letterbox(tsn): + settings.append('16:9') + multiplier = multiplier16by9 + else: + settings.append('4:3') + multiplier = multiplier4by3 + settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT, + multiplier, vInfo) + debug(('File is wider than 4:3 padding ' + + 'top and bottom\n%s') % ' '.join(settings)) + + # If video is taller than 4:3 add left and right padding, this + # is rare. All of these files will always be sent in an aspect + # ratio of 4:3 since they are so narrow. + + else: + settings.append('4:3') + settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo) + debug('File is taller than 4:3 padding left and right\n%s' + % ' '.join(settings)) + + return settings + +def tivo_compatible_video(vInfo, tsn, mime=''): + message = (True, '') + while True: + codec = vInfo.get('vCodec', '') + if mime == 'video/mp4': + if codec != 'h264': + message = (False, 'vCodec %s not compatible' % codec) + + break + + if mime == 'video/bif': + if codec != 'vc1': + message = (False, 'vCodec %s not compatible' % codec) + + break + + if codec not in ('mpeg2video', 'mpeg1video'): + message = (False, 'vCodec %s not compatible' % codec) + break + + if vInfo['kbps'] != None: + abit = max('0', vInfo['aKbps']) + if (int(vInfo['kbps']) - int(abit) > + config.strtod(config.getMaxVideoBR(tsn)) / 1000): + message = (False, '%s kbps exceeds max video bitrate' % + vInfo['kbps']) + break + else: + message = (False, '%s kbps not supported' % vInfo['kbps']) + break + + if config.isHDtivo(tsn): + if vInfo['par2'] != 1.0: + if config.getPixelAR(0): + if vInfo['par2'] != None or config.getPixelAR(1) != 1.0: + message = (False, '%s not correct PAR' % vInfo['par2']) + break + # HD Tivo detected, skipping remaining tests. + break + + if not vInfo['vFps'] in ['29.97', '59.94']: + message = (False, '%s vFps, should be 29.97' % vInfo['vFps']) + break + + if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn)) + or (config.get169Letterbox(tsn) and config.get169Setting(tsn))): + if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'): + message = (False, ('DAR %s not supported ' + + 'by BLACKLIST_169 tivos') % vInfo['dar1']) + break + + mode = (vInfo['vWidth'], vInfo['vHeight']) + if mode not in [(720, 480), (704, 480), (544, 480), + (528, 480), (480, 480), (352, 480), (352, 240)]: + message = (False, '%s x %s not in supported modes' % mode) + break + + return message + +def tivo_compatible_audio(vInfo, inFile, tsn, mime=''): + message = (True, '') + while True: + codec = vInfo.get('aCodec', '') + + if codec == None: + debug('No audio stream detected') + break + + if mime == 'video/mp4': + if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac', + 'ac3', 'liba52'): + message = (False, 'aCodec %s not compatible' % codec) + break + if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2): + message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh'])) + break + + audio_lang = config.get_tsn('audio_lang', tsn) + if audio_lang: + if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]: + message = (False, '%s preferred audio track exists' % + audio_lang) + break + + if mime == 'video/bif': + if codec != 'wmav2': + message = (False, 'aCodec %s not compatible' % codec) + + break + + if inFile[-5:].lower() == '.tivo': + break + + if mime == 'video/x-tivo-mpeg-ts' and codec not in ('ac3', 'liba52'): + message = (False, 'aCodec %s not compatible' % codec) + break + + if codec not in ('ac3', 'liba52', 'mp2'): + message = (False, 'aCodec %s not compatible' % codec) + break + + if (not vInfo['aKbps'] or + int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)): + message = (False, '%s kbps exceeds max audio bitrate' % + vInfo['aKbps']) + break + + audio_lang = config.get_tsn('audio_lang', tsn) + if audio_lang: + if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]: + message = (False, '%s preferred audio track exists' % + audio_lang) + break + + return message + +def tivo_compatible_container(vInfo, inFile, mime=''): + message = (True, '') + container = vInfo.get('container', '') + if ((mime == 'video/mp4' and + (container != 'mov' or inFile.lower().endswith('.mov'))) or + (mime == 'video/bif' and container != 'asf') or + (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or + (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and + (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))): + message = (False, 'container %s not compatible' % container) + + return message + +def mp4_remuxable(inFile, tsn=''): + vInfo = video_info(inFile) + return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0] + +def mp4_remux(inFile, basename, tsn='', temp_share_path=''): + outFile = inFile + '.pyTivo-temp' + newname = basename + '.pyTivo-temp' + + if temp_share_path: + newname = os.path.splitext(os.path.split(basename)[1])[0] + '.mp4.pyTivo-temp' + outFile = os.path.join(temp_share_path, newname) + + if os.path.exists(outFile): + return None # ugh! + + ffmpeg_path = config.get_bin('ffmpeg') + fname = unicode(inFile, 'utf-8') + oname = unicode(outFile, 'utf-8') + if mswindows: + fname = fname.encode('iso8859-1') + oname = oname.encode('iso8859-1') + + settings = {'video_codec': '-vcodec copy', + 'video_br': select_videobr(inFile, tsn), + 'video_fps': select_videofps(inFile, tsn), + 'max_video_br': select_maxvideobr(tsn), + 'buff_size': select_buffsize(tsn), + 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), + 'audio_br': select_audiobr(tsn), + 'audio_fr': select_audiofr(inFile, tsn), + 'audio_ch': select_audioch(inFile, tsn), + 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'), + 'audio_lang': select_audiolang(inFile, tsn), + 'ffmpeg_pram': select_ffmpegprams(tsn), + 'format': '-f mp4'} + + cmd_string = config.getFFmpegTemplate(tsn) % settings + cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname] + + debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') + debug(' '.join(cmd)) + + ffmpeg = subprocess.Popen(cmd) + debug('remuxing ' + inFile + ' to ' + outFile) + if ffmpeg.wait(): + debug('error during remuxing') + os.remove(outFile) + return None + + return newname + +def tivo_compatible(inFile, tsn='', mime=''): + vInfo = video_info(inFile) + + message = (True, 'all compatible') + if not config.get_bin('ffmpeg'): + if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']: + message = (False, 'no ffmpeg') + return message + + while True: + vmessage = tivo_compatible_video(vInfo, tsn, mime) + if not vmessage[0]: + message = vmessage + break + + amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime) + if not amessage[0]: + message = amessage + break + + cmessage = tivo_compatible_container(vInfo, inFile, mime) + if not cmessage[0]: + message = cmessage + break + + dmessage = (False, 'All DVD Video must be re-encapsulated') + if vobstream.is_dvd(inFile): + message = dmessage + + break + + debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]], + message[1], inFile)) + return message + +def video_info(inFile, cache=True): + vInfo = dict() + fname = unicode(inFile, 'utf-8') + #mtime = os.stat(fname).st_mtime + if vobstream.is_dvd(inFile): + is_dvd = True + mtime = os.stat(os.path.dirname(fname)).st_mtime + else: + is_dvd = False + mtime = os.stat(fname).st_mtime + + if cache: + if inFile in info_cache and info_cache[inFile][0] == mtime: + debug('CACHE HIT! %s' % inFile) + return info_cache[inFile][1] + + vInfo['Supported'] = True + + ffmpeg_path = config.get_bin('ffmpeg') + if not ffmpeg_path: + if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg', + '.vob', '.tivo']: + vInfo['Supported'] = False + vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480, + 'rawmeta': {}}) + if cache: + info_cache[inFile] = (mtime, vInfo) + return vInfo + + if mswindows: + fname = fname.encode('iso8859-1') + #cmd = [ffmpeg_path, '-i', fname] + if is_dvd: + cmd = [ffmpeg_path, '-i', '-'] + else: + cmd = [ffmpeg_path, '-i', fname] + debug('cmd: %s' % cmd) + # Windows and other OS buffer 4096 and ffmpeg can output more than that. + err_tmp = tempfile.TemporaryFile() + ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + if is_dvd: + vobstream.vobstream(True, inFile, ffmpeg, BLOCKSIZE) + + # wait configured # of seconds: if ffmpeg is not back give up + wait = config.getFFmpegWait() + debug('starting ffmpeg, will wait %s seconds for it to complete' % wait) + for i in xrange(wait * 20): + time.sleep(.05) + if not ffmpeg.poll() == None: + break + + if ffmpeg.poll() == None: + kill(ffmpeg) + vInfo['Supported'] = False + if cache: + info_cache[inFile] = (mtime, vInfo) + return vInfo + + err_tmp.seek(0) + output = err_tmp.read() + err_tmp.close() + debug('ffmpeg output=%s' % output) + + attrs = {'container': r'Input #0, ([^,]+),', + 'vCodec': r'Video: ([^, ]+)', # video codec + 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate + 'aCodec': r'.*Audio: ([^, ]+)', # audio codec + 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency + 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping + + for attr in attrs: + rezre = re.compile(attrs[attr]) + x = rezre.search(output) + if x: + vInfo[attr] = x.group(1) + else: + if attr in ['container', 'vCodec']: + vInfo[attr] = '' + vInfo['Supported'] = False + else: + vInfo[attr] = None + debug('failed at ' + attr) + + rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*') + x = rezre.search(output) + if x: + if x.group(3): + if x.group(3) == 'stereo': + vInfo['aCh'] = 2 + elif x.group(3) == 'mono': + vInfo['aCh'] = 1 + elif x.group(2): + vInfo['aCh'] = int(x.group(1)) + int(x.group(2)) + elif x.group(1): + vInfo['aCh'] = int(x.group(1)) + else: + vInfo['aCh'] = None + debug('failed at aCh') + else: + vInfo['aCh'] = None + debug('failed at aCh') + + rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') + x = rezre.search(output) + if x: + vInfo['vWidth'] = int(x.group(1)) + vInfo['vHeight'] = int(x.group(2)) + else: + vInfo['vWidth'] = '' + vInfo['vHeight'] = '' + vInfo['Supported'] = False + debug('failed at vWidth/vHeight') + + rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*') + x = rezre.search(output) + if x: + vInfo['vFps'] = x.group(1) + if '.' not in vInfo['vFps']: + vInfo['vFps'] += '.00' + + # Allow override only if it is mpeg2 and frame rate was doubled + # to 59.94 + + if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97': + # First look for the build 7215 version + rezre = re.compile(r'.*film source: 29.97.*') + x = rezre.search(output.lower()) + if x: + debug('film source: 29.97 setting vFps to 29.97') + vInfo['vFps'] = '29.97' + else: + # for build 8047: + rezre = re.compile(r'.*frame rate differs from container ' + + r'frame rate: 29.97.*') + debug('Bug in VideoReDo') + x = rezre.search(output.lower()) + if x: + vInfo['vFps'] = '29.97' + else: + vInfo['vFps'] = '' + vInfo['Supported'] = False + debug('failed at vFps') + + durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),') + d = durre.search(output) + + if d: + vInfo['millisecs'] = ((int(d.group(1)) * 3600 + + int(d.group(2)) * 60 + + int(d.group(3))) * 1000 + + int(d.group(4)) * (10 ** (3 - len(d.group(4))))) + else: + vInfo['millisecs'] = 0 + + if is_dvd: + vInfo['millisecs'] = vobstream.duration(inFile) + + # get bitrate of source for tivo compatibility test. + rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*') + x = rezre.search(output) + if x: + vInfo['kbps'] = x.group(1) + else: + # Fallback method of getting video bitrate + # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p, + # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r) + rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' + + r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*') + x = rezre.search(output) + if x: + vInfo['kbps'] = x.group(1) + else: + vInfo['kbps'] = None + debug('failed at kbps') + + # get par. + rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*') + x = rezre.search(output) + if x and x.group(1) != "0" and x.group(2) != "0": + vInfo['par1'] = x.group(1) + ':' + x.group(2) + vInfo['par2'] = float(x.group(1)) / float(x.group(2)) + else: + vInfo['par1'], vInfo['par2'] = None, None + + # get dar. + rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*') + x = rezre.search(output) + if x and x.group(1) != "0" and x.group(2) != "0": + vInfo['dar1'] = x.group(1) + ':' + x.group(2) + else: + vInfo['dar1'] = None + + # get Audio Stream mapping. + rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)') + x = rezre.search(output) + amap = [] + if x: + for x in rezre.finditer(output): + amap.append((x.group(1), x.group(2) + x.group(3))) + else: + amap.append(('', '')) + debug('failed at mapAudio') + vInfo['mapAudio'] = amap + + vInfo['par'] = None + + # get Metadata dump (newer ffmpeg). + lines = output.split('\n') + rawmeta = {} + flag = False + + for line in lines: + if line.startswith(' Metadata:'): + flag = True + else: + if flag: + if line.startswith(' Duration:'): + flag = False + else: + try: + key, value = [x.strip() for x in line.split(':', 1)] + try: + value = value.decode('utf-8') + except: + if sys.platform == 'darwin': + value = value.decode('macroman') + else: + value = value.decode('iso8859-1') + rawmeta[key] = [value] + except: + pass + + vInfo['rawmeta'] = rawmeta + + data = metadata.from_text(inFile) + for key in data: + if key.startswith('Override_'): + vInfo['Supported'] = True + if key.startswith('Override_mapAudio'): + audiomap = dict(vInfo['mapAudio']) + stream = key.replace('Override_mapAudio', '').strip() + if stream in audiomap: + newaudiomap = (stream, data[key]) + audiomap.update([newaudiomap]) + vInfo['mapAudio'] = sorted(audiomap.items(), + key=lambda (k,v): (k,v)) + elif key.startswith('Override_millisecs'): + vInfo[key.replace('Override_', '')] = int(data[key]) + else: + vInfo[key.replace('Override_', '')] = data[key] + + if cache: + info_cache[inFile] = (mtime, vInfo) + debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()])) + return vInfo + +def audio_check(inFile, tsn): + cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' + + select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -') + fname = unicode(inFile, 'utf-8') + if mswindows: + fname = fname.encode('iso8859-1') + cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split() + ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE) + fd, testname = tempfile.mkstemp() + testfile = os.fdopen(fd, 'wb') + try: + shutil.copyfileobj(ffmpeg.stdout, testfile) + except: + kill(ffmpeg) + testfile.close() + vInfo = None + else: + testfile.close() + vInfo = video_info(testname, False) + os.remove(testname) + return vInfo + +def supported_format(inFile): + if video_info(inFile)['Supported']: + return True + else: + debug('FALSE, file not supported %s' % inFile) + return False + +def kill(popen): + debug('killing pid=%s' % str(popen.pid)) + if mswindows: + win32kill(popen.pid) + else: + import os, signal + for i in xrange(3): + debug('sending SIGTERM to pid: %s' % popen.pid) + os.kill(popen.pid, signal.SIGTERM) + time.sleep(.5) + if popen.poll() is not None: + debug('process %s has exited' % popen.pid) + break + else: + while popen.poll() is None: + debug('sending SIGKILL to pid: %s' % popen.pid) + os.kill(popen.pid, signal.SIGKILL) + time.sleep(.5) + +def win32kill(pid): + import ctypes + handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + +def gcd(a, b): + while b: + a, b = b, a % b + return a + +def dvd_size( full_path ): + return vobstream.size( full_path ) + +def is_dvd( full_path ): + return vobstream.is_dvd( full_path) diff --git a/plugins/dvdvideo/virtualdvd.py b/plugins/dvdvideo/virtualdvd.py new file mode 100644 index 00000000..ded03ab0 --- /dev/null +++ b/plugins/dvdvideo/virtualdvd.py @@ -0,0 +1,216 @@ +# Module: virtualdvd.py +# Author: Eric von Bayer +# Updated By: Luke Broadbent +# Contact: +# Date: June 25, 2011 +# Description: +# Model a DVD as a dynamic directory structure of mpegs. +# +# Copyright (c) 2009, Eric von Bayer +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The names of the contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import re +import time +import dvdfolder + +import metadata + +# Use the LRU Cache if it is available +try: + from lrucache import LRUCache + VDVD_Cache = LRUCache(20) +except: + VDVD_Cache = dict() + +# Patterns to match against virtual files +PATTERN_VDVD_FILES = re.compile( "(?i)__T(-?[0-9]+).mpg" ) +FORMAT_VDVD_FILES = "__T%02d.mpg" + +################################# VirtualDVD ################################### + +class VirtualDVD(object): + class FileData(object): + def __init__(self, vdvd, path, num, title ): + self.name = os.path.join( path, FORMAT_VDVD_FILES % num ).encode('utf-8') + self.isdir = False + st = os.stat( path ) + #self.mdate = int(st.st_mtime) + self.mdate = int(time.time()) + self.title = vdvd.TitleName(num) + if( num >= 0 ): + self.size = title.Size() + else: + self.size = 0 + + def __init__( self, path, title_threshold = 0 ): + + self.valid = False + self.TITLE_LENGTH_THRESHOLD = title_threshold + self.dvd_folder = None + path = unicode(path, 'utf-8') + + if os.path.isdir( path ): + self.path = path + self.file = "" + self.file_id = -1 + else: + self.path = os.path.dirname( path ) + self.file = os.path.basename( path ) + self.file_id = -1 + + try: + if self.path in VDVD_Cache: + self.dvd_folder = VDVD_Cache[self.path] + self.valid = self.dvd_folder.QuickValid() + else: + self.dvd_folder = dvdfolder.DVDFolder( self.path, defer=True ) + self.valid = self.dvd_folder.QuickValid() + if self.valid: + VDVD_Cache[self.path] = self.dvd_folder + + except dvdfolder.DVDNotDVD: + pass + except dvdfolder.DVDFormatError, err: + print "Warning while reading DVD %s: %s" % ( self.path, err ) + + if not os.path.isdir( path ) and self.valid: + m = PATTERN_VDVD_FILES.match(self.file) + if m != None: + self.file_id = int(m.group(1)) + + def Path( self ): + return self.path + + def HasErrors( self ): + return self.dvd_folder and self.dvd_folder.HasErrors() + + def Valid( self ): + try: + if self.valid: + return self.dvd_folder.Valid() + except dvdfolder.DVDNotDVD: + pass + except dvdfolder.DVDFormatError, err: + print "Warning while reading DVD %s: %s" % ( self.path, err ) + + return False + + def QuickValid( self ): + if self.valid: + return self.dvd_folder.QuickValid() + return False + + def TitleNumber( self ): + return self.file_id + + def TitleName( self, num = -1 ): + if num == -1: + num = self.file_id + + if num == 0: + return "Main Feature" + elif num > 0: + if num <= len(self.dvd_folder.TitleList()): + return "Title " + str(num) + " (" + \ + str(self.dvd_folder.TitleList()[num-1].Time()) + ")" + return "" + elif num == -99: + return self.dvd_folder.Error() + else: + return "" + + def IDToTitle( self, id ): + if ( not self.Valid() ) or ( id < 0 ) or \ + ( id > len(self.dvd_folder.TitleList()) ): + return DVDTitle() + elif id == 0: + return self.dvd_folder.MainTitle() + else: + return self.dvd_folder.TitleList()[id-1] + + def FileTitle( self, file = None ): + if file == None: + return self.IDToTitle( self.file_id ) + else: + m = PATTERN_VDVD_FILES.match( file ) + if m != None: + return self.IDToTitle( int(m.group(1)) ) + + return self.IDToTitle( -1 ) + + def DVDTitleName( self ): + return os.path.basename( self.dvd_folder.Folder() ) + + def NumFiles( self ): + if self.Valid(): + return self.dvd_folder.NumUsefulTitles( self.TITLE_LENGTH_THRESHOLD ) + else: + return 0 + + def GetFiles( self ): + files = list() + if self.Valid() and len( self.dvd_folder.TitleList() ) > 0: + #if the title 0 has a name in the metadata files count it if it does not startwith ignore + data = {} + try: + data.update( metadata.from_text( os.path.join( self.path, FORMAT_VDVD_FILES % 0 ).encode('utf-8') ) ) + except: + pass + + if 'episodeTitle' in data: + pass + elif 'Title 0' in data: + data['episodeTitle'] = data['Title 0'] + else: + data['episodeTitle'] = "" + + if not data['episodeTitle'].lower().startswith('ignore'): + files.append( self.FileData( self, self.path, 0, \ + self.dvd_folder.MainTitle() ) ) + + for title in self.dvd_folder.TitleList(): + if title.Time().Secs() > self.TITLE_LENGTH_THRESHOLD: + #if the title has a name in the metadata files count it if it does not startwith ignore + data = {} + try: + data.update( metadata.from_text( os.path.join( self.path, FORMAT_VDVD_FILES % title.TitleNumber() ).encode('utf-8') ) ) + except: + pass + + if 'episodeTitle' in data: + pass + elif 'Title '+ str(title.TitleNumber()) in data: + data['episodeTitle'] = data['Title ' + str(title.TitleNumber())] + else: + data['episodeTitle'] = "" + + if not data['episodeTitle'].lower().startswith('ignore'): + files.append( self.FileData( self, self.path, \ + title.TitleNumber(), title ) ) + elif self.dvd_folder.HasErrors() != None: + files.append( self.FileData( self, self.path, -99, None ) ) + return files \ No newline at end of file diff --git a/plugins/dvdvideo/vobstream.py b/plugins/dvdvideo/vobstream.py new file mode 100644 index 00000000..ee50f849 --- /dev/null +++ b/plugins/dvdvideo/vobstream.py @@ -0,0 +1,303 @@ +# Module: dvdfolder.py +# Author: Eric von Bayer +# Updated By: Luke Broadbent +# Contact: +# Date: June 15, 2011 +# Description: +# Routines for reading DVD vob files and streaming them to the TiVo. +# +# Copyright (c) 2009, Eric von Bayer +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The names of the contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from threading import Thread +from dvdtitlestream import DVDTitleStream + +import logging +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import time + +import lrucache + +import config +import metadata +import virtualdvd + +def WriteStreamToSubprocess( fhin, sub, event, blocksize ): + if event == None: + event = 1 + # Write all the data till either end is closed or done + while not event.isSet(): + + # Read in the block and escape if we got nothing + data = fhin.read( blocksize ) + if len(data) == 0: + break + + if sub.poll() != None and sub.stdin != None: + break + + # Write the data and flush it + try: + sub.stdin.write( data ) + sub.stdin.flush() + except IOError: + break + + # We got less data so we must be at the end + if len(data) < blocksize: + break + + # Close the input if it's not already closed + if not fhin.closed: + fhin.close() + + # Close the output if it's not already closed + if sub.stdin != None and not sub.stdin.closed: + sub.stdin.close() + +def vobstream(isQuery, inFile, ffmpeg, blocksize): + dvd = virtualdvd.VirtualDVD( inFile ) + if not dvd.Valid() or dvd.file_id == -1: + debug('Not a valid dvd file') + return 0 + + title = dvd.FileTitle() + ts = DVDTitleStream( title.Stream() ) + + # Make an event to shutdown the thread + sde = threading.Event() + sde.clear() + + # Stream data to the subprocess + t = Thread( target=WriteStreamToSubprocess, args=(ts, ffmpeg, sde, blocksize) ) + t.start() + + if isQuery: + # Shutdown the helper threads/processes + ffmpeg.wait() + sde.set() + t.join() + + # Close the title stream + ts.close() + else: + proc = {'stream': ts, 'thread': t, 'event':sde} + return proc + +def is_dvd(inFile): + dvd = virtualdvd.VirtualDVD( inFile ) + return dvd.Valid() or dvd.file_id != -1 + +def size(inFile): + try: + dvd = virtualdvd.VirtualDVD( inFile ) + return dvd.FileTitle().Size() + except: + return 0 + +def duration(inFile): + dvd = virtualdvd.VirtualDVD( inFile ) + title = dvd.FileTitle() + return title.Time().MSecs() + +def tivo_compatible(inFile, tsn='', mime=''): + message = (False, 'All DVD Video must be re-encapsulated') + debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]], + message[1], inFile)) + return message + +def video_info(inFile, audio_spec = "", cache=True): + vInfo = dict() + fname = unicode(os.path.dirname(inFile), 'utf-8') + mtime = os.stat(fname).st_mtime + if cache: + if inFile in info_cache and info_cache[inFile][0] == mtime: + debug('CACHE HIT! %s' % inFile) + return info_cache[inFile][1] + + dvd = virtualdvd.VirtualDVD( inFile ) + if not dvd.Valid() or dvd.file_id == -1: + debug('Not a valid dvd file') + return dict() + + ffmpeg_path = config.get_bin('ffmpeg') + + title = dvd.FileTitle() + sid = title.FindBestAudioStreamID( audio_spec ) + ts = DVDTitleStream( title.Stream() ) + ts.seek(0) + + cmd = [ffmpeg_path, '-i', '-'] + # Windows and other OS buffer 4096 and ffmpeg can output more than that. + err_tmp = tempfile.TemporaryFile() + ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + + # Write all the data till either end is closed or done + while 1: + # Read in the block and escape if we got nothing + data = ts.read(BLOCKSIZE) + if len(data) == 0: + break + + if ffmpeg.poll() != None and sub.stdin != None: + break + + try: + ffmpeg.stdin.write(data) + ffmpeg.stdin.flush() + except IOError: + break + + # We got less data so we must be at the end + if len(data) < BLOCKSIZE: + break + + # wait configured # of seconds: if ffmpeg is not back give up + wait = config.getFFmpegWait() + debug('starting ffmpeg, will wait %s seconds for it to complete' % wait) + for i in xrange(wait * 20): + time.sleep(.05) + if not ffmpeg.poll() == None: + break + + if ffmpeg.poll() == None: + kill(ffmpeg) + vInfo['Supported'] = False + if cache: + info_cache[inFile] = (mtime, vInfo) + return vInfo + + err_tmp.seek(0) + output = err_tmp.read() + err_tmp.close() + debug('ffmpeg output=%s' % output) + + # Close the input if it's not already closed + if not ts.closed: + ts.close() + + #print "VOB Info:", output + vInfo['mapAudio'] = '' + + attrs = {'container': r'Input #0, ([^,]+),', + 'vCodec': r'Video: ([^, ]+)', # video codec + 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate + 'aCodec': r'.*Audio: ([^,]+),.*', # audio codec + 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency + 'mapVideo': r'([0-9]+\.[0-9]+).*: Video:.*', # video mapping + 'mapAudio': r'([0-9]+\.[0-9]+)\[0x%02x\]: Audio:.*' % sid } # Audio mapping + + for attr in attrs: + rezre = re.compile(attrs[attr]) + x = rezre.search(output) + if x: + vInfo[attr] = x.group(1) + else: + if attr in ['container', 'vCodec']: + vInfo[attr] = '' + vInfo['Supported'] = False + else: + vInfo[attr] = None + debug('failed at ' + attr) + + # Get the Pixel Aspect Ratio + rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*') + x = rezre.search(output) + if x and x.group(1) != "0" and x.group(2) != "0": + vInfo['par1'] = x.group(1) + ':' + x.group(2) + vInfo['par2'] = float(x.group(1)) / float(x.group(2)) + else: + vInfo['par1'], vInfo['par2'] = None, None + + # Get the Display Aspect Ratio + rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*') + x = rezre.search(output) + if x and x.group(1) != "0" and x.group(2) != "0": + vInfo['dar1'] = x.group(1) + ':' + x.group(2) + else: + vInfo['dar1'] = None + + # Get the video dimensions + rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') + x = rezre.search(output) + if x: + vInfo['vWidth'] = int(x.group(1)) + vInfo['vHeight'] = int(x.group(2)) + else: + vInfo['vWidth'] = '' + vInfo['vHeight'] = '' + vInfo['Supported'] = False + debug('failed at vWidth/vHeight') + + vInfo['millisecs'] = title.Time().MSecs() + vInfo['Supported'] = True + + if cache: + info_cache[inFile] = (mtime, vInfo) + debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()])) + return vInfo + +def supported_format(inFile): + dvd = virtualdvd.VirtualDVD( inFile ) + return dvd.Valid() and dvd.file_id != -1 + if video_info(inFile)['Supported']: + return True + else: + debug('FALSE, file not supported %s' % inFile) + return False + +def kill(popen): + debug('killing pid=%s' % str(popen.pid)) + if mswindows: + win32kill(popen.pid) + else: + import os, signal + for i in xrange(3): + debug('sending SIGTERM to pid: %s' % popen.pid) + os.kill(popen.pid, signal.SIGTERM) + time.sleep(.5) + if popen.poll() is not None: + debug('process %s has exited' % popen.pid) + break + else: + while popen.poll() is None: + debug('sending SIGKILL to pid: %s' % popen.pid) + os.kill(popen.pid, signal.SIGKILL) + time.sleep(.5) + +def win32kill(pid): + import ctypes + handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) diff --git a/plugins/music/music.py b/plugins/music/music.py index 192b6ae2..ac2ae7dc 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -7,6 +7,7 @@ import subprocess import sys import time +import unicodedata import urllib from xml.sax.saxutils import escape @@ -36,9 +37,9 @@ 'genre': ['\xa9gen', u'WM/Genre']} # Search strings for different playlist types -asxfile = re.compile('ref +href *= *"(.+)"', re.IGNORECASE).search -wplfile = re.compile('media +src *= *"(.+)"', re.IGNORECASE).search -b4sfile = re.compile('Playstring="file:(.+)"').search +asxfile = re.compile('ref +href *= *"([^"]*)"', re.IGNORECASE).search +wplfile = re.compile('media +src *= *"([^"]*)"', re.IGNORECASE).search +b4sfile = re.compile('Playstring="file:([^"]*)"').search plsfile = re.compile('[Ff]ile(\d+)=(.+)').match plstitle = re.compile('[Tt]itle(\d+)=(.+)').match plslength = re.compile('[Ll]ength(\d+)=(\d+)').match @@ -411,6 +412,8 @@ def build_recursive_list(path, recurse=True): continue f = os.path.join(path, f) isdir = os.path.isdir(f) + if sys.platform == 'darwin': + f = unicodedata.normalize('NFC', f) f = f.encode('utf-8') if recurse and isdir: files.extend(build_recursive_list(f)) diff --git a/plugins/photo/photo.py b/plugins/photo/photo.py index f4a6edbb..1afab5ef 100644 --- a/plugins/photo/photo.py +++ b/plugins/photo/photo.py @@ -28,8 +28,10 @@ import os import re import random +import sys import threading import time +import unicodedata import urllib from cStringIO import StringIO from xml.sax.saxutils import escape @@ -354,6 +356,8 @@ def build_recursive_list(path, recurse=True): continue f = os.path.join(path, f) isdir = os.path.isdir(f) + if sys.platform == 'darwin': + f = unicodedata.normalize('NFC', f) f = f.encode('utf-8') if recurse and isdir: files.extend(build_recursive_list(f)) diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index b112d0b9..2504a10a 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -499,16 +499,6 @@ conflict could also occur if the source file has really corrupt sections. Example Settings: True, False Available In: Tivos, HD_tivos, SD_tivos - -ffmpeg_threads - -Default Setting: None -Valid Entries: Any number up to 16 that represents the number of CPU threads FFmpeg has access to. -Required: No -Skill: Very Advanced -Description: Using multiple threads with FFmpeg if your CPU has them can speed up transcoding time. Using a setting of '2' for a dual core CPU may show increased transcoding speeds of 10% or more. If you have a dual core CPU with hyperthreading then the setting might be '4'. There is a diminishing return for this setting so make sure to set this to no more than the amount of threads your CPU is capable of handling. After a certain point the TiVo is the limiting speed factor. -Example Settings: Any number from 1 to 16 -Available In: Server ffmpeg_pram @@ -517,8 +507,10 @@ Valid Entries: A valid ffmpeg command Required: No Skill: Very Advanced Description: This allows you to append additional raw ffmpeg commands to -the ffmpeg template. -Example Settings: A valid FFmpeg command +the ffmpeg template. For example, you would enter '-threads 2' here if +you have multiple processors and want ffmpeg to use both processors to +speed up transcoding. +Example Settings: -threads 2 Available In: Server, Tivos, HD_tivos, SD_tivos ffmpeg_tmpl @@ -526,7 +518,7 @@ ffmpeg_tmpl Default Setting: %(video_codec)s %(video_fps)s %(video_br)s %(max_video_br)s %(buff_size)s %(aspect_ratio)s %(audio_br)s %(audio_fr)s %(audio_ch)s %(audio_codec)s %(audio_lang)s -%(ffmpeg_pram)s %(ffmpeg_threads)s %(format)s +%(ffmpeg_pram)s %(format)s Valid Entries: A valid ffmpeg command Required: No Skill: Very Advanced diff --git a/plugins/video/transcode.py b/plugins/video/transcode.py index 304f7706..5d2a52a4 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -71,7 +71,6 @@ def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): 'audio_codec': select_audiocodec(isQuery, inFile, tsn), 'audio_lang': select_audiolang(inFile, tsn), 'ffmpeg_pram': select_ffmpegprams(tsn), - 'ffmpeg_threads': select_ffmpegthreads(), 'format': select_format(tsn, mime)} if isQuery: @@ -93,12 +92,12 @@ def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): cmd = '' ffmpeg = tivodecode else: - cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', '-'] + cmd_string.split() + cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout, stdout=subprocess.PIPE, bufsize=(512 * 1024)) else: - cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', fname] + cmd_string.split() + cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), stdout=subprocess.PIPE) @@ -106,7 +105,7 @@ def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') debug(' '.join(cmd)) - ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0, + ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0, 'last_read': time.time(), 'blocks': []} if thead: ffmpeg_procs[inFile]['blocks'].append(thead) @@ -291,23 +290,17 @@ def select_audiolang(inFile, tsn): stream = s # if only 1 item matched we're done if len(langmatch_curr) == 1: - del langmatch_prev[:] break # if more than 1 item matched copy the curr area to the prev # array we only need to look at the new shorter list from # now on elif len(langmatch_curr) > 1: - del langmatch_prev[:] langmatch_prev = langmatch_curr[:] - del langmatch_curr[:] - # if nothing matched we'll keep the prev array and clear the - # curr array - else: - del langmatch_curr[:] + langmatch_curr = [] # if we drop out of the loop with more than 1 item default to # the first item - if len(langmatch_prev) > 1: - stream = langmatch_prev[0][0] + if len(langmatch_curr) > 1: + stream = langmatch_curr[0][0] # don't let FFmpeg auto select audio stream, pyTivo defaults to # first detected if stream: @@ -365,14 +358,6 @@ def select_maxvideobr(tsn): def select_buffsize(tsn): return '-bufsize ' + config.getBuffSize(tsn) -def select_ffmpegthreads(): - threads = config.getFFmpegThreads() - - if not threads: - return '' - - return '-threads ' + threads - def select_ffmpegprams(tsn): params = config.getFFmpegPrams(tsn) if not params: @@ -407,44 +392,36 @@ def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): vInfo['vWidth']) * multiplier) if endHeight % 2: endHeight -= 1 - if endHeight < TIVO_HEIGHT * 0.99: - topPadding = (TIVO_HEIGHT - endHeight) / 2 - if topPadding % 2: - topPadding -= 1 - newpad = pad_check() - if newpad: - return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), '-vf', - 'pad=%d:%d:0:%d' % (TIVO_WIDTH, TIVO_HEIGHT, topPadding)] - else: - bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding - return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), - '-padtop', str(topPadding), - '-padbottom', str(bottomPadding)] - else: # if only very small amount of padding needed, then - # just stretch it - return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + topPadding = (TIVO_HEIGHT - endHeight) / 2 + if topPadding % 2: + topPadding -= 1 + newpad = pad_check() + if newpad: + return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH, + endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)] + else: + bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding + return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), + '-padtop', str(topPadding), + '-padbottom', str(bottomPadding)] def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) / (vInfo['vHeight'] * multiplier)) if endWidth % 2: endWidth -= 1 - if endWidth < TIVO_WIDTH * 0.99: - leftPadding = (TIVO_WIDTH - endWidth) / 2 - if leftPadding % 2: - leftPadding -= 1 - newpad = pad_check() - if newpad: - return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), '-vf', - 'pad=%d:%d:%d:0' % (TIVO_WIDTH, TIVO_HEIGHT, leftPadding)] - else: - rightPadding = (TIVO_WIDTH - endWidth) - leftPadding - return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), - '-padleft', str(leftPadding), - '-padright', str(rightPadding)] - else: # if only very small amount of padding needed, then - # just stretch it - return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] + leftPadding = (TIVO_WIDTH - endWidth) / 2 + if leftPadding % 2: + leftPadding -= 1 + newpad = pad_check() + if newpad: + return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth, + TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)] + else: + rightPadding = (TIVO_WIDTH - endWidth) - leftPadding + return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), + '-padleft', str(leftPadding), + '-padright', str(rightPadding)] def select_aspect(inFile, tsn = ''): TIVO_WIDTH = config.getTivoWidth(tsn) @@ -754,13 +731,11 @@ def mp4_remux(inFile, basename, tsn=''): 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'), 'audio_lang': select_audiolang(inFile, tsn), 'ffmpeg_pram': select_ffmpegprams(tsn), - 'ffmpeg_threads': select_ffmpegthreads(), 'format': '-f mp4'} cmd_string = config.getFFmpegTemplate(tsn) % settings + cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname] - cmd = [ffmpeg_path] + select_ffmpegthreads().split() + ['-i', fname] + cmd_string.split() + [oname] - debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') debug(' '.join(cmd)) @@ -853,21 +828,6 @@ def video_info(inFile, cache=True): err_tmp.close() debug('ffmpeg output=%s' % output) - #get libavcodec major and minor versions from FFmpeg output - rezre = re.compile(r'libavcodec \s+([0-9]+).?\s*([0-9]+)') - x = rezre.search(output) - if x: - vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = int(x.group(1)), int(x.group(2)) - else: - #should catch very old builds which are formatted differently - rezre = re.compile(r'libavcodec.*:\s+([0-9]+).?\s*([0-9]+)') - x = rezre.search(output) - if x: - vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = int(x.group(1)), int(x.group(2)) - else: - vInfo['avcodecMAJ'], vInfo['avcodecMIN'] = None, None - debug('failed at avcodec check') - attrs = {'container': r'Input #0, ([^,]+),', 'vCodec': r'Video: ([^, ]+)', # video codec 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate diff --git a/plugins/video/video.py b/plugins/video/video.py index 1eb9f7d9..c8b66d9e 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -144,7 +144,9 @@ def Push(self, handler, query): if config.getIsExternal(tsn): exturl = config.get_server('externalurl') if exturl: - baseurl = exturl + if not exturl.endswith('/'): + exturl += '/' + baseurl = exturl + container else: ip = self.readip() baseurl = 'http://%s:%s/%s' % (ip, port, container) @@ -454,7 +456,7 @@ def QueryContainer(self, handler, query): video['valid'] = True video.update(metadata.basic(f.name)) - if config.hasTStivo(tsn): + if self.use_ts(tsn, f.name): video['mime'] = 'video/x-tivo-mpeg-ts' else: video['mime'] = 'video/x-tivo-mpeg' @@ -465,13 +467,15 @@ def QueryContainer(self, handler, query): videos.append(video) logger.debug('mobileagent: %d useragent: %s' % (useragent.lower().find('mobile'), useragent.lower())) - - if not use_html: - t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) - elif useragent.lower().find('mobile') > 0: - t = Template(HTML_CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) + use_mobile = useragent.lower().find('mobile') > 0 + if use_html: + if use_mobile: + t = Template(HTML_CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) + else: + t = Template(HTML_CONTAINER_TEMPLATE, filter=EncodeUnicode) else: - t = Template(HTML_CONTAINER_TEMPLATE, filter=EncodeUnicode) + t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) + t.container = handler.cname t.name = subcname t.total = total @@ -483,14 +487,24 @@ def QueryContainer(self, handler, query): t.guid = config.getGUID() t.tivos = config.tivos t.tivo_names = config.tivo_names - handler.send_response(200) - if not use_html: - handler.send_header('Content-Type', 'text/xml') + if use_html: + handler.send_html(str(t)) else: - handler.send_header('Content-Type', 'text/html; charset=utf-8') - handler.send_header('Expires', '0') - handler.end_headers() - handler.wfile.write(t) + handler.send_xml(str(t)) + + def use_ts(self, tsn, file_path): + if config.is_ts_capable(tsn): + if file_path[-5:].lower() == '.tivo': + try: + flag = file(file_path).read(8) + except: + return False + if ord(flag[7]) & 0x20: + return True + elif config.has_ts_flag(): + return True + + return False def get_details_xml(self, tsn, file_path): if (tsn, file_path) in self.tvbus_cache: diff --git a/pyTivo.conf.dist b/pyTivo.conf.dist index 241ead6f..807f2b6f 100644 --- a/pyTivo.conf.dist +++ b/pyTivo.conf.dist @@ -5,12 +5,14 @@ # you get pyTivo up and running. You can access the tool by pointing your # browser to http://localhost:9032/ -#Read the pyTivo support wiki for additional help at http://pytivo.sourceforge.net +# Read the pyTivo support wiki for additional help at +# http://pytivo.sourceforge.net [Server] port=9032 -# FFmpeg is a required tool but downloaded separately. See pyTivo wiki for help. +# FFmpeg is a required tool but downloaded separately. See pyTivo wiki +# for help. # Full path to ffmpeg including filename # For windows: ffmpeg=c:\Program Files\pyTivo\bin\ffmpeg.exe # For linux: ffmpeg=/usr/bin/ffmpeg diff --git a/templates/root_container.tmpl b/templates/root_container.tmpl index b68d709d..0316293d 100644 --- a/templates/root_container.tmpl +++ b/templates/root_container.tmpl @@ -16,7 +16,7 @@ - /TiVoConnect?Command=QueryContainer&Container=$quote($name)&Format=text/html + /TiVoConnect?Command=QueryContainer&Container=$quote($name) $escape($details.content_type) From 898f2aea8df675b993bd0fe6b53409cdb4a6aa10 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sun, 24 Feb 2013 03:08:13 -0600 Subject: [PATCH 19/21] selective merge with wmcbrine, removed dvdvideo plugin (lack of use) Merged all the guts with wmcbrine's, simply leaving the modification to the ui/mobile browser interface intact here. Removed the dvdvideo plugin entirely, simply due to lack of personal use. --- beacon.py | 12 +- config.py | 16 +- httpserver.py | 7 + metadata.py | 33 +- plugins/dvdvideo/__init__.py | 0 plugins/dvdvideo/compositefile.py | 187 --- plugins/dvdvideo/dvdfolder.py | 917 ------------- plugins/dvdvideo/dvdtitlestream.py | 200 --- plugins/dvdvideo/dvdvideo.py | 738 ----------- plugins/dvdvideo/ilvuhack.py | 198 --- plugins/dvdvideo/qtfaststart.py | 261 ---- plugins/dvdvideo/templates/TvBus.tmpl | 111 -- .../dvdvideo/templates/container_html.tmpl | 106 -- plugins/dvdvideo/templates/container_mob.tmpl | 88 -- plugins/dvdvideo/templates/container_xml.tmpl | 70 - plugins/dvdvideo/transcode.py | 1178 ----------------- plugins/dvdvideo/virtualdvd.py | 216 --- plugins/dvdvideo/vobstream.py | 303 ----- plugins/music/music.py | 2 +- plugins/photo/photo.py | 356 +++-- plugins/settings/help.txt | 140 +- plugins/togo/togo.py | 25 +- plugins/video/qtfaststart.py | 56 +- plugins/video/templates/TvBus.tmpl | 3 + plugins/video/templates/container_xml.tmpl | 3 + plugins/video/transcode.py | 141 +- plugins/video/video.py | 14 +- pyTivo.py | 19 - 28 files changed, 425 insertions(+), 4975 deletions(-) delete mode 100644 plugins/dvdvideo/__init__.py delete mode 100644 plugins/dvdvideo/compositefile.py delete mode 100644 plugins/dvdvideo/dvdfolder.py delete mode 100644 plugins/dvdvideo/dvdtitlestream.py delete mode 100644 plugins/dvdvideo/dvdvideo.py delete mode 100644 plugins/dvdvideo/ilvuhack.py delete mode 100644 plugins/dvdvideo/qtfaststart.py delete mode 100644 plugins/dvdvideo/templates/TvBus.tmpl delete mode 100644 plugins/dvdvideo/templates/container_html.tmpl delete mode 100644 plugins/dvdvideo/templates/container_mob.tmpl delete mode 100644 plugins/dvdvideo/templates/container_xml.tmpl delete mode 100644 plugins/dvdvideo/transcode.py delete mode 100644 plugins/dvdvideo/virtualdvd.py delete mode 100644 plugins/dvdvideo/vobstream.py diff --git a/beacon.py b/beacon.py index dbcbd31e..5a250d82 100644 --- a/beacon.py +++ b/beacon.py @@ -13,7 +13,7 @@ SHARE_TEMPLATE = '/TiVoConnect?Command=QueryContainer&Container=%s' PLATFORM_MAIN = 'pyTivo' -PLATFORM_VIDEO = 'pc' # For the nice icon +PLATFORM_VIDEO = 'pc/pyTivo' # For the nice icon class ZCListener: def __init__(self, names): @@ -87,6 +87,13 @@ def __init__(self): self.UDPSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) self.services = [] + self.platform = PLATFORM_VIDEO + for section, settings in config.getShares(): + ct = GetPlugin(settings['type']).CONTENT_TYPE + if ct in ('x-container/tivo-music', 'x-container/tivo-photos'): + self.platform = PLATFORM_MAIN + break + if config.get_zc(): logger = logging.getLogger('pyTivo.beacon') try: @@ -110,11 +117,10 @@ def format_services(self): def format_beacon(self, conntype, services=True): beacon = ['tivoconnect=1', - 'swversion=1', 'method=%s' % conntype, 'identity=%s' % config.getGUID(), 'machine=%s' % gethostname(), - 'platform=%s' % PLATFORM_MAIN] + 'platform=%s' % self.platform] if services: beacon.append('services=' + self.format_services()) diff --git a/config.py b/config.py index 44cc88c5..fbc94228 100644 --- a/config.py +++ b/config.py @@ -209,14 +209,6 @@ def getOptres(tsn=None): except: return False -def getPixelAR(ref): - if config.has_option('Server', 'par'): - try: - return (True, config.getfloat('Server', 'par'))[ref] - except NoOptionError, ValueError: - pass - return (False, 1.0)[ref] - def get_bin(fname): global bin_paths @@ -252,7 +244,7 @@ def getFFmpegWait(): if config.has_option('Server', 'ffmpeg_wait'): return max(int(float(config.get('Server', 'ffmpeg_wait'))), 1) else: - return 10 + return 0 def getFFmpegTemplate(tsn): tmpl = get_tsn('ffmpeg_tmpl', tsn, True) @@ -341,12 +333,6 @@ def getMaxVideoBR(tsn=None): return _k(rate) return '30000k' -def getVideoPCT(tsn=None): - pct = get_tsn('video_pct', tsn) - if pct: - return float(pct) - return 85 - def getBuffSize(tsn=None): size = get_tsn('bufsize', tsn) if size: diff --git a/httpserver.py b/httpserver.py index f6cb8dd9..9f87d90e 100644 --- a/httpserver.py +++ b/httpserver.py @@ -88,6 +88,13 @@ def address_string(self): host, port = self.client_address[:2] return host + def version_string(self): + """ Override version_string() so it doesn't include the Python + version. + + """ + return self.server_version + def do_GET(self): tsn = self.headers.getheader('TiVo_TCD_ID', self.headers.getheader('tsn', '')) diff --git a/metadata.py b/metadata.py index e9f3348a..3f7cf70e 100755 --- a/metadata.py +++ b/metadata.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import logging import os import subprocess import sys @@ -32,7 +33,8 @@ 'P2': 2, 'P3': 3, 'R4': 4, 'X5': 5, 'N6': 6, 'N8': 8} STAR_RATINGS = {'1': 1, '1.5': 2, '2': 3, '2.5': 4, '3': 5, '3.5': 6, - '4': 7, '*': 1, '**': 3, '***': 5, '****': 7} + '4': 7, '*': 1, '**': 3, '***': 5, '****': 7, 'X1': 1, + 'X2': 2, 'X3': 3, 'X4': 4, 'X5': 5, 'X6': 6, 'X7': 7} HUMAN = {'mpaaRating': {1: 'G', 2: 'PG', 3: 'PG-13', 4: 'R', 5: 'X', 6: 'NC-17', 8: 'NR'}, @@ -61,10 +63,14 @@ def get_stars(rating): def tag_data(element, tag): for name in tag.split('/'): - new_element = element.getElementsByTagName(name) - if not new_element: + found = False + for new_element in element.childNodes: + if new_element.nodeName == name: + found = True + element = new_element + break + if not found: return '' - element = new_element[0] if not element.firstChild: return '' return element.firstChild.data @@ -306,6 +312,12 @@ def from_text(full_path): x = metadata.get(rating, '').upper() if x in ratings: metadata[rating] = ratings[x] + else: + try: + x = int(x) + metadata[rating] = x + except: + pass return metadata @@ -335,10 +347,11 @@ def from_container(xmldoc): metadata = {} keys = {'title': 'Title', 'episodeTitle': 'EpisodeTitle', - 'description': 'Description', 'seriesId': 'SeriesId', - 'episodeNumber': 'EpisodeNumber', 'tvRating': 'TvRating', - 'displayMajorNumber': 'SourceChannel', 'callsign': 'SourceStation', - 'showingBits': 'ShowingBits', 'mpaaRating': 'MpaaRating'} + 'description': 'Description', 'programId': 'ProgramId', + 'seriesId': 'SeriesId', 'episodeNumber': 'EpisodeNumber', + 'tvRating': 'TvRating', 'displayMajorNumber': 'SourceChannel', + 'callsign': 'SourceStation', 'showingBits': 'ShowingBits', + 'mpaaRating': 'MpaaRating'} details = xmldoc.getElementsByTagName('Details')[0] @@ -367,6 +380,7 @@ def from_details(xml): 'title': 'program/title', 'episodeTitle': 'program/episodeTitle', 'episodeNumber': 'program/episodeNumber', + 'programId': 'program/uniqueId', 'seriesId': 'program/series/uniqueId', 'seriesTitle': 'program/series/seriesTitle', 'originalAirDate': 'program/originalAirDate', @@ -649,10 +663,11 @@ def dump(output, metadata): if __name__ == '__main__': if len(sys.argv) > 1: metadata = {} + config.init([]) + logging.basicConfig() fname = force_utf8(sys.argv[1]) ext = os.path.splitext(fname)[1].lower() if ext == '.tivo': - config.init([]) metadata.update(from_tivo(fname)) elif ext in ['.mp4', '.m4v', '.mov']: metadata.update(from_moov(fname)) diff --git a/plugins/dvdvideo/__init__.py b/plugins/dvdvideo/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/dvdvideo/compositefile.py b/plugins/dvdvideo/compositefile.py deleted file mode 100644 index 1b555baf..00000000 --- a/plugins/dvdvideo/compositefile.py +++ /dev/null @@ -1,187 +0,0 @@ -# Module: compositefile.py -# Author: Eric von Bayer -# Contact: -# Date: August 18, 2009 -# Description: -# Class that works like a file but is in reality a series of file system -# files. Intentionally is a similar API to the file object. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -import copy - -try: - os.SEEK_SET -except AttributeError: - os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3) - -# Handle a series of files as if it's one file -class CompositeFile(object): - """Virtual file that is a composite of other files""" - def __init__( self, *files ): - self.__file_map = list() - self.closed = True - - # If we are given a composite file, copy the file map - if len(files) == 1 and isinstance( files[0], CompositeFile ): - self.__file_map = copy.deepcopy( files[0].__file_map ) - - # Build a file offset map - else: - off = 0L - for cfile in files: - size = os.path.getsize( cfile ) - if size > 0 and os.path.isfile( cfile ): - off += size - self.__file_map.append( ( off, cfile ) ) - - # Open the first file - self.open() - - def __str__( self ): - return ",".join( fm[1] for fm in self.__file_map ) - - # Advance to the next file - def __next_file( self ): - # If we have an open handle, close it in preparation for the next one - if self.__handle != None and not self.__handle.closed: - self.__handle.close() - - # Bump the file number that we're on - self.__fileno += 1 - - # If there's another file, open it and get the next offset - if self.__fileno < len( self.__file_map ): - self.__handle = open( self.__file_map[ self.__fileno ][1], "rb" ) - self.__next_file_off = self.__file_map[ self.__fileno ][0] - - # We're done, null the handle and mark as closed - else: - self.close() - - # Read a slice, return the data and how many bytes remain to be read - def __read_slice( self, bytes ): - rem = self.__off + bytes - self.__next_file_off - - # If the remaining bytes aren't negative, then read a slice and advance - # to the next file. - if rem >= 0L: - bytes = bytes - rem - #print "Read %d[%d]: %d: %s+%d" % ( bytes, rem, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() ) - data = self.__handle.read( bytes ) - self.__off += len(data) - assert bytes == len(data), "Failed to read the requested number of bytes" - self.__next_file() - return data, rem - - # Read the bytes all from this file - else: - #print "Read %d: %d: %s+%d" % ( bytes, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() ) - data = self.__handle.read( bytes ) - self.__off += len(data) - assert bytes == len(data), "Failed to read the requested number of bytes" - return data, 0 - - # Get a list of files - def files( self ): - return [ fm[1] for fm in self.__file_map ] - - # Return the linear offset - def tell( self ): - return self.__off - - # Seek into the composite file - def seek( self, off, whence = os.SEEK_SET ): - - # Calculate the new seek offset - if whence == os.SEEK_SET: - new_off = off - elif whence == os.SEEK_CUR: - new_off = self.__off + off - elif whence == os.SEEK_END: - new_off = self.__file_map[-1][0] + off - else: - raise "seek called with an invalid offset type" - - # Determine which file this seek offset is part of - soff = 0 - fileno = 0 - eoff = 0 - for ( eoff, mfile ) in self.__file_map: - if eoff > new_off: - break - fileno += 1 - soff = eoff - - # Make sure this was a valid seek point - if eoff <= new_off: - raise "seek beyond the bounds of the composite file" - - # Make sure the correct file is open - if fileno != self.__fileno: - if not self.__handle.closed: - self.__handle.close() - self.__handle = open( self.__file_map[fileno][1], "rb" ) - self.__next_file_off = eoff - self.__fileno = fileno - - # Seek the file handle - self.__handle.seek( new_off - soff ) - self.__off = new_off - - # Read from the file - def read( self, bytes ): - if self.__handle.closed == False: - data = "" - while bytes > 0 and self.closed == False: - slice_data, bytes = self.__read_slice( bytes ) - data += slice_data - return data - else: - return "" - - def open(self): - if self.closed: - self.__fileno = -1 - self.__off = 0L - self.__next_file_off = 0L - self.__handle = None - self.closed = False - - # Open the first file - self.__next_file() - - - def close( self ): - if self.__handle != None and self.__handle.closed == False: - self.__handle.close() - self.__handle = None - self.closed = True - - def size( self ): - return self.__file_map[-1][0] diff --git a/plugins/dvdvideo/dvdfolder.py b/plugins/dvdvideo/dvdfolder.py deleted file mode 100644 index 4e90a9f5..00000000 --- a/plugins/dvdvideo/dvdfolder.py +++ /dev/null @@ -1,917 +0,0 @@ -# Module: dvdfolder.py -# Author: Eric von Bayer -# Updated By: Luke Broadbent -# Contact: -# Date: June 25, 2011 -# Description: -# Routines for reading data out of a DVD Folder. This in no way is an -# exhaustive implementation, but merely to give a good platform for -# some automated DVD processing. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -import re -import ilvuhack -from dvdtitlestream import DVDTitleStream - -import metadata - -try: - os.SEEK_SET -except AttributeError: - os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3) - -# Constants for various parts of the structure -IFO_TYPE_VMG = 0 -IFO_TYPE_VTS = 1 -DVD_BLOCK_LEN = 2048 - -# Patterns to match against for the various parts of the DVD -PATTERN_VIDEO_TS = re.compile( r"(?i)VIDEO_TS$" ) -PATTERN_VIDEO_TS_IFO = re.compile( r"(?i)VIDEO_TS.IFO$" ) -PATTERN_VIDEO_TS_VOB = re.compile( r"(?i)VIDEO_TS.VOB$" ) -PATTERN_VTS_IFO = re.compile( r"(?i)VTS_([0-9]{2})_0.IFO$" ) -PATTERN_VTS_VOB = re.compile( r"(?i)VTS_([0-9]{2})_([0-9]).VOB$" ) -FORMAT_VDVD_FILES = "__T%02d.mpg" - -def FindDOSFilename( path, dosname ): - try: - if os.path.isdir( path ): - for f in os.listdir( path ): - if f.upper() == dosname: - return os.path.join( path, f ) - except: - return None - -def MatchAudioAttr( audio_attr, lang, chan ): - if lang != '*' and lang.lower() != audio_attr.LanguageCode().lower(): - #print "Failed based on language" - return False - elif chan != '*' and int( chan ) != audio_attr.Channels(): - #print "Failed based on channels" - return False - elif audio_attr.Coding() != "AC3": - #print "Failed based on coding" - return False - elif audio_attr.CodeExtensionValue() > 1: - #print "Failed based on extension", audio_attr.CodeExtensionValue() - return False - else: - return True - -def BCD2Dec( bcd ): - return int( str( "%X" % bcd ) ) - -class DVDNotDVD(Exception): - def __init__( self, txt ): - self.__txt = txt - - def __str__( self ): - return self.__txt - -class DVDFormatError(Exception): - def __init__( self, txt ): - self.__txt = txt - - def __str__( self ): - return self.__txt - -########################## Misc Utility Functions ############################## - -class DVDFileHandle(object): - """Utility functions for reading DVD data structures""" - DVD_BLOCK_LEN = 2048L - __handle = None - - def __init__( self, handle, offset = -1 ): - if not isinstance( handle, file ): - raise "handle was not a file" - - self.__handle = handle - self.__sector_offset = 0 - - if self.IsOpen() and offset >= 0: - self.Seek( offset ) - - def IsOpen( self ): - return not self.__handle.closed - - def Close( self ): - if self.__handle != None and not self.__handle.closed: - self.__handle.close() - - def Handle( self ): - return self.__handle - - def SetSectorOffset( self, offset ): - self.__sector_offset = offset - - def SectorOffset( self ): - return self.__sector_offset - - def SectorSeek( self, sector, offset = None ): - off = ( sector - self.__sector_offset ) * DVD_BLOCK_LEN - if offset != None: - off += offset - self.Seek( off ) - - def Seek( self, offset ): - self.__handle.seek( offset ) - - def Tell( self ): - return self.__handle.tell() - - def SectorTell( self ): - off = self.__handle.tell() - sect = int( off / DVD_BLOCK_LEN ) - off = off % DVD_BLOCK_LEN - return sect, off - - def Skip( self, bytes ): - self.__handle.seek( bytes, 1 ) - - def Read( self, bytes ): - return self.__handle.read( bytes ) - - def ReadU8( self ): - return ord(self.__handle.read(1)) - - def ReadU16( self ): - return ( ord(self.__handle.read(1)) << 8 ) | \ - ord(self.__handle.read(1)) - - def ReadU32( self ): - return ( ord(self.__handle.read(1)) << 24 ) | \ - ( ord(self.__handle.read(1)) << 16 ) | \ - ( ord(self.__handle.read(1)) << 8 ) | \ - ord(self.__handle.read(1)) - -############################# IFOPlaybackTime ################################## - -class IFOPlaybackTime(object): - """Playback Time Data in an IFO file""" - __sec = 0 - __frame_rate = 29.97 - - def __init__( self, sec_handle = 0 ): - if isinstance( sec_handle, file ): - self.Read( sec_handle ) - - def __iadd__( a, b ): - a.__sec += b.__sec - return a - - def __lt__( a, b ): - return a.__sec < b.__sec - - def __ge__( a, b ): - return a.__sec >= b.__sec or a == b - - def __le__( a, b ): - return a.__sec <= b.__sec or a == b - - def __eq__( a, b ): - return abs( a.__sec - b.__sec ) < 0.04 - - def __ne__( a, b ): - return not ( a == b ) - - def __str__( self ): - return "%d:%02d:%06.3f" % ( \ - int( self.__sec / 3600 ), - int( ( self.__sec % 3600 ) / 60 ), - self.__sec % 60 ) - - def SetFrameRate( self, fr ): - self.__frame_rate = fr - - def Read( self, handle ): - try: - hrs = BCD2Dec( ord(handle.read(1)) ) - mins = BCD2Dec( ord(handle.read(1)) ) - secs = BCD2Dec( ord(handle.read(1)) ) - fms = ord(handle.read(1)) - self.__frame_rate = [ 1000000, 25.0, 1000000, 29.97 ] \ - [ ( fms & 0xC0 ) >> 6 ] - fms = BCD2Dec( fms & 0x3F ) - except: - self.__frame_rate = 29.97 - self.__sec = 0 - raise DVDFormatError( "Improper time format" ) - - if self.__frame_rate == 1000000: - print "Warning: Invalid Frame Rate flag, got " + \ - str( ( fms & 0xC0 ) >> 6 ) + " instead of 1 or 3." - - self.__sec = ( fms / self.__frame_rate ) + secs + (mins * 60) + (hrs * 3600) - - - def FrameRate( self ): - return self.__frame_rate - - def Secs( self ): - return self.__sec - - def MSecs( self ): - return int( self.__sec * 1000 ) - -############################## IFOVideoAttrs ################################### - -class IFOVideoAttrs(DVDFileHandle): - """Video Attributes in an IFO file""" - def __init__( self, handle, offset ): - DVDFileHandle.__init__( self, handle, offset ) - self.__data = self.ReadU16() - self.__frame_rate = 29.97 - - def backdoor_SetFrameRate( self, rate ): - self.__frame_rate = rate - - def AspectRatio( self ): - return [ "4:3", "16:9", "", "16:9" ] \ - [ ( self.__data & 0x0C00 ) >> 10 ] - - def Resolution( self ): - if ( self.__data & 0x3000 ) == 0: - return [ "720x480", "704x480", "352x480", "352x240" ] \ - [ ( self.__data & 0x38 ) >> 3 ] - else: - return [ "720x576", "704x576", "352x576", "352x288" ] \ - [ ( self.__data & 0x38 ) >> 3 ] - - def Width( self ): - return [ 720, 704, 352, 352 ] \ - [ ( self.__data & 0x38 ) >> 3 ] - - def Height( self ): - if ( self.__data & 0x3000 ) == 0: - return [ 480, 480, 480, 240 ] \ - [ ( self.__data & 0x38 ) >> 3 ] - else: - return [ 576, 576, 576, 288 ] \ - [ ( self.__data & 0x38 ) >> 3 ] - - def Standard( self ): - return [ "NTSC", "PAL" ][ ( self.__data & 0x3000 ) >> 12 ] - - def Coding( self ): - return [ "MPEG-1", "MPEG-2" ] [ ( self.__data & 0xC000 ) >> 14 ] - - def FrameRate( self ): - return self.__frame_rate - -############################## IFOAudioAttrs ################################### - -class IFOAudioAttrs(DVDFileHandle): - """Audio Attributes in an IFO file""" - def __init__( self, handle, num, offset ): - DVDFileHandle.__init__( self, handle, offset ) - self.__data = self.ReadU16() - self.__lang = self.Read(2) - self.__lang_ext = self.Read(1) - self.__code_ext = self.ReadU8() - self.__unk = self.Read(1) - self.__mode = self.ReadU8() - self.__stream_id = [ 0x80, 0, 0xC0, 0xC0, 0xA0, 0, 0x88, 0 ] \ - [ ( self.__data & 0xE000 ) >> 13 ] + num - - def Coding( self ): - return [ "AC3", "", "MPEG-1", "MPEG-2", "LPCM", "", "DTS", "" ] \ - [ ( self.__data & 0xE000 ) >> 13 ] - - def LanguageCode( self ): - return self.__lang - - def CodeExtension( self ): - return [ "Unspecified", "Normal", "For the Blind", "Director's Comments", \ - "Alternate Director's Comments" ] [ self.__code_ext ] - - def CodeExtensionValue( self ): - return self.__code_ext - - def StreamID( self ): - return self.__stream_id - - def Channels( self ): - return ( self.__data & 0x7 ) + 1 - - def DRC( self ): - cmode = ( self.__data & 0xE000 ) >> 13 - return ( cmode == 2 or cmode == 3 ) and ( self.__data & 0xC0 ) == 0xC0 - - def Quantization( self ): - if ( ( self.__data & 0xE000 ) >> 13 ) == 4: - return [ 16, 20, 24, 0 ] [ ( self.__data & 0xC0 ) >> 6 ] - else: - return 16 - -################################ IFOAVAttrs #################################### - -class IFOAVAttrs(DVDFileHandle): - """Audio/Video Attributes in an IFO file""" - def __init__( self, handle, offset ): - DVDFileHandle.__init__( self, handle, offset ) - self.__video = IFOVideoAttrs( self.Handle(), offset ) - self.__audio_streams = self.ReadU16() - self.__audio = list() - for stm in range(self.__audio_streams): - self.__audio.append( IFOAudioAttrs( self.Handle(), stm, offset + 4 + 8*stm ) ) - - def Video( self ): - return self.__video - - def AudioList( self ): - return self.__audio - -################################# IFOFile ###################################### - -class IFOVMGFile(DVDFileHandle): - """IFO VMG File From a DVD""" - def __init__( self, filename ): - self.__filename = filename - self.__time = IFOPlaybackTime() - self.__frame_rate = 29.97 - try: - # Open the file through our parent - handle = open( filename, "rb" ) - DVDFileHandle.__init__( self, handle, 0x0 ) - if not self.IsOpen(): - raise DVDFormatError( "Couldn't open "+filename ) - - # Make sure we're a VMG IFO file - id = self.Read( 12 ) - if id != "DVDVIDEO-VMG": - raise DVDFormatError( filename + "IFO file is not a VMG file" ) - - # Get the VOB, IFO, and BUP sectors - self.Seek( 0x0C ) - self.__last_sector_bup = self.ReadU32() - self.Seek( 0x1C ) - self.__last_sector_ifo = self.ReadU32() - self.Seek( 0xC0 ) - self.__first_sector_menu = self.ReadU32() - - # Read in the version as big endian - handle.seek( 0x20 ) - self.__version = self.ReadU32() - - # Get the A/V Attributes for the menu - self.__menu = IFOAVAttrs( self.Handle(), 0x100 ) - - # Read the simple header information - self.Seek( 0x22 ) - self.__vmg_category = self.ReadU32() - self.__num_vols = self.ReadU16() - self.__vol_num = self.ReadU16() - self.__side_id = self.ReadU8() - self.Seek( 0x3E ) - self.__num_vts = self.ReadU16() - self.__provider_id = self.Read(32) - self.Seek( 0xC4 ) - self.__vmg_tt_srpt = self.ReadU32() - - # Read the critical bits of the Table of Titles structure - self.SectorSeek( self.__vmg_tt_srpt ) - tt_srpt_offset = self.Tell() - self.__num_titles = self.ReadU16() - self.Skip(6) - self.__title_info = list() - - # Read in the title information - for tn in range(self.__num_titles): - title = dict() - title['number'] = tn+1 - title['type'] = self.ReadU8() - title['angles'] = self.ReadU8() - title['chapters'] = self.ReadU16() - title['parental'] = self.ReadU16() - title['vts_num'] = self.ReadU8() - title['vts_pgc_num'] = self.ReadU8() - title['vts_ifo_sector'] = self.ReadU32() - - # Make sure the information was "sane" - assert title['vts_num'] <= 99, DVDFormatError( "Title "+str(tn)+" has a vts_num > 99" ) - assert title['vts_pgc_num'] <= 99, DVDFormatError( "Title "+str(tn)+" has a vts_pgc_num > 99" ) - - # Add the title information - self.__title_info.append( title ) - - self.Close() - self.__valid = True - - except: - self.Close() - self.__valid = False - raise - - def Menu( self ): - return self.__menu - - def Valid( self ): - return self.__valid - - def NumVolumes( self ): - return self.__num_vols - - def VolumeNum( self ): - return self.__vol_num - - def SideID( self ): - return self.__side_id - - def NumVTSes( self ): - return self.__num_vts - - def NumTitles( self ): - return self.__num_titles - - def TitleInfo( self, tnum ): - return self.__title_info[tnum-1] - -class IFOVTSFile(DVDFileHandle): - """IFO VTS File From a DVD""" - def __init__( self, filename ): - self.__filename = filename - self.__time = IFOPlaybackTime() - self.__frame_rate = 29.97 - try: - # Get our VTS number - match = PATTERN_VTS_IFO.match( os.path.basename( filename ) ) - if match == None: - raise DVDFormatError( "Not a valid VTS file" ) - self.__num = int( match.group(1) ) - - # Read in a list of all the VTS VOB files - self.__vob_files = list() - path = os.path.dirname( self.__filename ) - for fn in os.listdir( path ): - match = PATTERN_VTS_VOB.match( fn ) - if match != None and int(match.group(1)) == self.__num and int(match.group(2)) > 0: - self.__vob_files.append( os.path.join( path, fn ) ) - self.__vob_files.sort() - - # Open the file through our parent - handle = open( filename, "rb" ) - DVDFileHandle.__init__( self, handle, 0x0 ) - if not self.IsOpen(): - raise DVDFormatError( "Can't open VTS info file" ) - - # Make sure we're a VMG IFO file - id = self.Read( 12 ) - if id != "DVDVIDEO-VTS": - raise DVDFormatError( "Expected a VTS info file" ) - - # Get the VOB, IFO, and BUP sectors - self.Seek( 0x0C ) - self.__last_sector_bup = self.ReadU32() - self.Seek( 0x1C ) - self.__last_sector_ifo = self.ReadU32() - self.Seek( 0xC0 ) - self.__first_sector_menu = self.ReadU32() - self.__first_sector_title = self.ReadU32() - - # Read in the version as big endian - handle.seek( 0x20 ) - self.__version = self.ReadU32() - - # Get the A/V Attributes for the menu - self.__menu = IFOAVAttrs( self.Handle(), 0x100 ) - - # Get the A/V Attributes for the title (if present) - self.__title = IFOAVAttrs( self.Handle(), 0x200 ) - - # Read the program chain structure for playtimes and stream ids - handle.seek( 0xCC ) - self.__vts_pgci_sector = self.ReadU32() - - # Read the critical bits of the Program Chain structure - self.SectorSeek( self.__vts_pgci_sector ) - pgci_offset = self.Tell() - - # Get the Program chain information - self.__pgc_info = list() - self.__num_pgc = self.ReadU16() - self.Skip(2) - self.__pgc_end_off = self.ReadU32() - for pgc in range(self.__num_pgc): - - # Read in the information in the program chain index - info = dict() - info['vts_number'] = self.__num - info['number'] = pgc+1 - t1 = self.ReadU8() - info['title_number'] = t1 & 0x3F - info['entry'] = ( (t1 & 0x80) == 0x80 ) - self.Skip(1) - info['parental_ctl_mask'] = self.ReadU16() - - # Find the offset of the program chain - pgc_off = self.ReadU32() - - # Ignore non entry program chains as they will be picked - # up in other ways. - if info['entry'] == False: - continue - - # Save the current location and seek to the Program Chain - cur_off = self.Tell() - self.Seek( pgci_offset + pgc_off + 2 ) - - # Read in the number of programs/chapters and cells - info['programs'] = self.ReadU8() - info['cells'] = self.ReadU8() - - # Read in the playback time - info['playtime'] = IFOPlaybackTime( handle ) - - # Skip the prohibited ops - self.Skip(4) - - # Read the list of valid audio streams - astrs = list() - for num in range(8): - strnum = self.ReadU8() - self.Skip(1) - if strnum & 0x80: - astrs.append( strnum & 0x7 ) - info[ 'audio_stream_nums' ] = astrs - - # Default these to False and mark true if we find a cell - # that matches. - info['ilvu'] = False - info['angles'] = False - - # Get the playback information table and seek to there - self.Seek( pgci_offset + pgc_off + 0xE8 ) - pgc_playback_off = self.ReadU16() - self.Seek( pgci_offset + pgc_off + pgc_playback_off ) - - # Walk the cells showing the info - ts = DVDTitleStream( *self.__vob_files ) - for cn in range( info['cells'] ): - t1 = self.ReadU8() - t2 = self.ReadU8() - if ( t1 & 0xF0 ) != 0x00: - info['angles'] = True - - self.Skip(6) - s = self.ReadU32() - i = self.ReadU32() - self.Skip(4) - e = self.ReadU32() - if i != 0: - info['ilvu'] = True - - # If there are no ILVUs then just add the block - if i == 0: - ts.AddSectors( s, e ) - - # Otherwise, we need to let the ILVU hack compute the real - # sectors by partially decoding the VOB. - else: - try: - for [sr,er] in ilvuhack.ComputeRealSectors( s, e, \ - *ts.files() ): - ts.AddSectors( sr, er ) - except AssertionError, err: - raise DVDFormatError( \ - "Error processing ILVU block within title set "+\ - str(self.__num)+", program chain "+\ - str(info["number"])+": "+str(err) ) - except: - raise DVDFormatError( \ - "Error processing ILVU block within title set "+\ - str(self.__num)+", program chain "+\ - str(info["number"]) ) - - info['stream'] = ts - - # Add all the information to the PGC list - self.__pgc_info.append( info ) - - # Return to the PGC table - self.Seek( cur_off ) - - self.__title.Video().backdoor_SetFrameRate( \ - self.__pgc_info[0]['playtime'].FrameRate() ) - - self.Close() - self.__valid = True - - except: - self.Close() - self.__valid = False - raise - - def Name( self ): - return self.__filename - - def Size( self ): - return os.path.getsize( self.__filename ) - - def Sectors( self ): - return self.__last_sector_ifo - - def VOBSectors( self ): - return self.__last_sector_bup - ( self.__last_sector_ifo * 2 ) - - def VOBFiles( self ): - return self.__vob_files - - def BUPSectorOffset( self ): - return self.__last_sector_bup - self.__last_sector_ifo - - def VOBSize( self ): - return self.VOBSectors() * self.DVD_BLOCK_LEN - - def NumPGCs( self ): - return self.__num_pgc - - def PGCInfo( self, num ): - if num < 1 or num > len(self.__pgc_info): - return None - return self.__pgc_info[ num-1 ] - - def Version( self ): - return self.__version - - def Menu( self ): - return self.__menu - - def Title( self ): - return self.__title - - def Valid( self ): - return self.__valid - -################################# DVDTitle ##################################### - -class DVDTitle(object): - """Information about a set of VOBs based on VMG/VTS info""" - def __init__( self, tnum, vmg_ifo, vts_list ): - self.__valid = False - try: - self.__tinfo = vmg_ifo.TitleInfo( tnum ) - self.__vts = vts_list[ self.__tinfo['vts_num']-1 ] - self.__pgcinfo = self.__vts.PGCInfo( self.__tinfo['vts_pgc_num'] ) - - if self.__pgcinfo == None: - raise DVDFormatError( "Title number: %d - PGC number %d in VTS %d is out of range (%d)" % - ( tnum, self.__tinfo['vts_pgc_num'], self.__tinfo['vts_num'], self.__vts.NumPGCs() ) ) - self.__valid = True - - self.__audio_streams = list() - vts_audio_streams = self.__vts.Title().AudioList() - for asnum in self.__pgcinfo['audio_stream_nums']: - self.__audio_streams.append( vts_audio_streams[asnum] ) - - except: - raise - - def Valid( self ): - return self.__valid - - def TitleNumber( self ): - return self.__tinfo['number'] - - def VTS( self ): - return self.__vts - - def VTSNumber( self ): - return self.__tinfo['vts_num'] - - def PGCNumber( self ): - return self.__tinfo['vts_pgc_num'] - - def HasAngles( self ): - return self.__pgcinfo['angles'] - - def HasInterleaved( self ): - return self.__pgcinfo['ilvu'] - - def AudioStreams( self ): - return self.__audio_streams - - def FindBestAudioStreamID( self, spec ): - #print "FindBestAudioStreamID( "+spec+" )" - parts = spec.split( ',' ) - for part in parts: - #print "Part:", part - elems = part.split( ':', 1 ) - - if len(elems) >= 2: - for stream in self.__audio_streams: - #print elems[0], elems[1], "==?", stream.LanguageCode(),stream.Channels(), "(0x%02x)" % stream.StreamID() - if MatchAudioAttr( stream, elems[0], elems[1] ): - return stream.StreamID() - - #print "Defaulted to", self.__audio_streams[0].LanguageCode(),self.__audio_streams[0].Channels(), "(0x%02x)" % self.__audio_streams[0].StreamID() - return self.__audio_streams[0].StreamID() - - def Stream( self ): - return self.__pgcinfo['stream'] - - def Size( self ): - return self.__pgcinfo['stream'].size() - - def Time( self ): - return self.__pgcinfo['playtime'] - - -###########################s###### DVDFolder #################################### - -class DVDFolder(object): - """DVD Folder along with routines to read contents""" - def __init__( self, path, defer = False ): - self.__valid = False - self.__disc_ifo = None - self.__disc_vob = None - self.__titles = list() - self.__main_title = None - self.__deferred = False - self.__error = None - - try: - # Make sure we have a directory - if not os.path.isdir( path ): - raise DVDNotDVD( "VIDEO_TS not located in "+path ) - self.__path = path - - # Find the sub VIDEO_TS folder - self.__videots_path = FindDOSFilename( path, "VIDEO_TS" ) - if self.__videots_path == None: - raise DVDNotDVD( "VIDEO_TS not located in "+path ) - - # Find the top level IFO file - self.__vmg_ifo_fn = FindDOSFilename( self.__videots_path, "VIDEO_TS.IFO" ) - if self.__vmg_ifo_fn == None: - raise DVDFormatError( "Couldn't locate VIDEO_TS.IFO in "+self.__videots_path ) - - # Defer most of the load if asked to, this lets the pages load faster, - # otherwise load it immediately - self.__deferred = True - if not defer: - self.__load_full() - - # We're valid, mark ourself as such - self.__valid = True - - except DVDFormatError, err: - self.__error = str(err) - self.__valid = False - raise - - except DVDNotDVD: - self.__valid = False - raise - - except: - self.__error = "Unknown internal error." - self.__valid = False - raise - - def __load_full( self ): - if self.__valid == False or self.__deferred == False: - return - - self.__deferred = False - - try: - # Read in the top level IFO - self.__vmg_ifo = IFOVMGFile( self.__vmg_ifo_fn ) - - # Read in the list of VTS IFOs - self.__vts_list = list() - for vts in range( 1, self.__vmg_ifo.NumVTSes()+1 ): - - # Figure out the file name we're after - ufile = "VTS_%02d_0.IFO" % vts - dfile = FindDOSFilename( self.__videots_path, ufile ) - - # Make sure we got a file - if dfile == None: - raise DVDFormatError( "Couldn't find file "+ufile+" in "+self.__videots_path ) - - # Get the file and if it was valid, add it to our list - vts_ifo = IFOVTSFile( dfile ) - if vts_ifo.Valid() == False: - raise DVDFormatError( dfile+" contained invalid format" ) - self.__vts_list.append( vts_ifo ) - - # Walk all the titles assembling the data - self.__titles = list() - lt_time = IFOPlaybackTime(0) - self.__main_title = None - for tn in range( 1, self.__vmg_ifo.NumTitles()+1 ): - try: - title = DVDTitle( tn, self.__vmg_ifo, self.__vts_list ) - - if title.Time() > lt_time: - lt_time = title.Time() - self.__main_title = title - - self.__titles.append( title ) - - except DVDFormatError: - pass - - if len(self.__titles) < 1: - raise DVDFormatError( "No valid titles present" ) - - except DVDFormatError, err: - self.__error = str(err) - self.__valid = False - raise - - except DVDNotDVD: - self.__valid = False - raise - - except: - self.__error = "Unknown internal error." - self.__valid = False - raise - - def Defered( self ): - return self.__deferred - - def Folder( self ): - return self.__path - - def MenuIFO( self ): - self.__load_full() - return self.__vmg_ifo - - # Count up the usable titles - def NumUsefulTitles( self, title_threshold = 30.0 ): - self.__load_full() - if self.__valid: - utitles = 0 - for title in self.TitleList(): - if title.Time() > title_threshold: - #if the title has a name in the metadata files count it if it does not startwith ignore - data = {} - try: - data.update( metadata.from_text( os.path.join( self.__path, FORMAT_VDVD_FILES % title.TitleNumber() ).encode('utf-8') ) ) - except: - pass - - if 'episodeTitle' in data: - pass - elif 'Title '+ str(title.TitleNumber()) in data: - data['episodeTitle'] = data['Title ' + str(title.TitleNumber())] - else: - data['episodeTitle'] = "" - - if not data['episodeTitle'].lower().startswith('ignore'): - utitles += 1 - - return utitles - return 0 - - def TitleList( self ): - self.__load_full() - return self.__titles - - def MainTitle( self ): - self.__load_full() - return self.__main_title - - def Valid( self ): - self.__load_full() - return self.__valid - - def QuickValid( self ): - return self.__valid - - def HasErrors( self ): - return self.__error != None - - def Error( self ): - return self.__error - \ No newline at end of file diff --git a/plugins/dvdvideo/dvdtitlestream.py b/plugins/dvdvideo/dvdtitlestream.py deleted file mode 100644 index fe96c7fa..00000000 --- a/plugins/dvdvideo/dvdtitlestream.py +++ /dev/null @@ -1,200 +0,0 @@ -# Module: dvdtitlestream.py -# Author: Eric von Bayer -# Contact: -# Date: August 18, 2009 -# Description: -# Class that works like a file but is in reality a series of file system -# files along with a sector map. This in conjunction with the underlying -# composite file will allow linear file access to the convoluted underlying -# video stream. Intentionally is a similar API to the file object. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -from compositefile import CompositeFile - -DVD_BLOCK_LEN = 2048L - -try: - os.SEEK_SET -except AttributeError: - os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3) - -class DVDTitleStream(object): - """Virtual file that is a composite of vob files mapped through a sector list""" - def __init__( self, *files ): - if len(files) == 1 and isinstance( files[0], DVDTitleStream ): - self.__cfile = CompositeFile( files[0].__cfile ) - self.__slist = files[0].__slist - else: - self.__cfile = CompositeFile( *files ) - self.__slist = list() - - self.__cfile.close() - self.__sector_map = None - self.__srange = -1 - self.__sects = 0L - self.__off = 0L - self.__next_sec_off = 0L - self.closed = False - - def __str__( self ): - ser = list() - for [s,e] in self.__slist: - ser.append( "[%d-%d]" % (s,e) ) - return "[" + str( self.__cfile ) + "] % " + ",".join( ser ) - - # Build a map of ( virtual offset, real offset ) - def __map_sectors( self ): - if self.__sector_map == None: - self.__cfile.open() - self.__sector_map = list() - off = 0L - for (s,e) in self.__slist: - off += ( e - s + 1 ) * DVD_BLOCK_LEN - self.__sector_map.append( ( off, s * DVD_BLOCK_LEN ) ) - self.__srange = -1 - self.__offset = 0L - self.__next_sec_off = self.__sector_map[0][0] - self.__next_range() - - # Advance to the next file - def __next_range( self ): - - if not self.closed: - # Bump the file number that we're on - self.__srange += 1 - - # If there's another sector range, seek to it and get the next offset - if self.__srange < len( self.__sector_map ): - self.__cfile.seek( self.__sector_map[self.__srange][1] ) - self.__next_sec_off = self.__sector_map[self.__srange][0] - - # We're done, close the file - else: - self.__cfile.close() - self.closed = True - - # Read a slice, return the data and how many bytes remain to be read - def __read_slice( self, bytes ): - - if not self.closed: - rem = self.__off + bytes - self.__next_sec_off - - # If the remaining bytes aren't negative, then read a slice and advance - # to the next file. - if rem >= 0L: - bytes = bytes - rem - data = self.__cfile.read( bytes ) - self.__off += len(data) - self.__next_range() - return data, rem - - # Read the bytes all from this file - else: - data = self.__cfile.read( bytes ) - self.__off += len(data) - return data, 0 - else: - return "", bytes - - def AddSectors( self, start, end ): - self.__sects += (end - start) + 1 - if len(self.__slist) > 0: - if self.__slist[-1][1]+1 == start: - self.__slist[-1][1] = end - return - self.__slist.append( [ start, end ] ) - self.__sector_map = None - - def SectorList( self ): - return self.__slist - - def files( self ): - return self.__cfile.files() - - def tell_real( self ): - return self.__cfile.tell() - - def tell( self ): - return self.__off - - # Seek into the composite file - def seek( self, off, whence = os.SEEK_SET ): - self.__map_sectors() - - # Calculate the new seek offset - if whence == os.SEEK_SET: - new_off = off - elif whence == os.SEEK_CUR: - new_off = self.__off + off - elif whence == os.SEEK_END: - new_off = self.__sector_map[-1][0] + off - else: - raise "seek called with an invalid offset type" - - # Determine which file this seek offset is part of - soff = 0 - srange = 0 - for ( eoff, roff ) in self.__sector_map: - if eoff > new_off: - break - srange += 1 - soff = eoff - - # Make sure this was a valid seek point - if eoff <= new_off: - raise "seek beyond the bounds of the composite file" - - # Make sure the correct file is open - if srange != self.__srange: - self.__next_sec_off = eoff - self.__srange = srange - - # Seek the file handle - self.__cfile.seek( roff + new_off - soff ) - self.__off = new_off - - # Read from the file - def read( self, bytes ): - self.__map_sectors() - if self.closed == False: - data = "" - while bytes > 0 and self.closed == False: - slice_data, bytes = self.__read_slice( bytes ) - data += slice_data - return data - else: - return "" - - def close( self ): - if self.__cfile.closed == False: - self.__cfile.close() - self.closed = True - - def size( self ): - return self.__sects * DVD_BLOCK_LEN diff --git a/plugins/dvdvideo/dvdvideo.py b/plugins/dvdvideo/dvdvideo.py deleted file mode 100644 index d60d4ed4..00000000 --- a/plugins/dvdvideo/dvdvideo.py +++ /dev/null @@ -1,738 +0,0 @@ -# Module: dvdvideo.py -# Author: Eric von Bayer -# Updated By: Luke Broadbent -# Contact: -# Date: June 5, 2012 -# Description: -# DVD Video plugin to allow playing DVD VIDEO_TS format folders on a TiVo -# This is closely aligned with video.py from the video plugin (from -# wmcbrine's branch). -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import sys - -if sys.version_info >= (3, 0): - raise "This plugin requires a 2.x version of python" - -import calendar -import cgi -import logging -import os -import re -import struct -import thread -import time -import traceback -import urllib -import zlib -from UserDict import DictMixin -from datetime import datetime, timedelta -from xml.sax.saxutils import escape - -from Cheetah.Template import Template -from lrucache import LRUCache - -import config -import metadata -import mind -import qtfaststart -import transcode -import dvdfolder -import virtualdvd - -from plugin import EncodeUnicode, Plugin, quote - -logger = logging.getLogger('pyTivo.video.dvdvideo') - -SCRIPTDIR = os.path.dirname(__file__) - -CLASS_NAME = 'DVDVideo' - -PUSHED = '

Queued for Push to %s

%s

' - -# Preload the templates -def tmpl(name): - return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read() - -HTML_CONTAINER_TEMPLATE_MOBILE = tmpl('container_mob.tmpl') -HTML_CONTAINER_TEMPLATE = tmpl('container_html.tmpl') -XML_CONTAINER_TEMPLATE = tmpl('container_xml.tmpl') -TVBUS_TEMPLATE = tmpl('TvBus.tmpl') - -EXTENSIONS = """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv -.ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf -.dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf -.m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21 -.mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb -.rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm -.wm .wmd .wtv .yuv""".split() - -use_extensions = True -try: - assert(config.get_bin('ffmpeg')) -except: - raise "This plugin requires ffmpeg" - -queue = [] # Recordings to push - -def uniso(iso): - return time.strptime(iso[:19], '%Y-%m-%dT%H:%M:%S') - -def isodt(iso): - return datetime(*uniso(iso)[:6]) - -def isogm(iso): - return int(calendar.timegm(uniso(iso))) - -class Pushable(object): - - def push_one_file(self, f): - file_info = VideoDetails() - file_info['valid'] = transcode.supported_format(f['path']) - - temp_share = config.get_server('temp_share', '') - temp_share_path = '' - if temp_share: - for name, data in config.getShares(): - if temp_share == name: - temp_share_path = data.get('path') - - mime = 'video/mpeg' - if config.isHDtivo(f['tsn']): - for m in ['video/mp4', 'video/bif']: - if transcode.tivo_compatible(f['path'], f['tsn'], m)[0]: - mime = m - break - - if (mime == 'video/mpeg' and - transcode.mp4_remuxable(f['path'], f['tsn'])): - new_path = transcode.mp4_remux(f['path'], f['name'], f['tsn'], temp_share_path) - if new_path: - mime = 'video/mp4' - f['name'] = new_path - if temp_share_path: - ip = config.get_ip() - port = config.getPort() - container = quote(temp_share) + '/' - f['url'] = 'http://%s:%s/%s' % (ip, port, container) - - if file_info['valid']: - file_info.update(self.metadata_full(f['path'], f['tsn'], mime)) - - url = f['url'] + quote(f['name']) - - title = file_info['seriesTitle'] - if not title: - title = file_info['title'] - - source = file_info['seriesId'] - if not source: - source = title - - subtitle = file_info['episodeTitle'] - try: - m = mind.getMind(f['tsn']) - m.pushVideo( - tsn = f['tsn'], - url = url, - description = file_info['description'], - duration = file_info['duration'] / 1000, - size = file_info['size'], - title = title, - subtitle = subtitle, - source = source, - mime = mime, - tvrating = file_info['tvRating']) - except Exception, msg: - logger.error(msg) - - def process_queue(self): - while queue: - time.sleep(5) - item = queue.pop(0) - self.push_one_file(item) - - def readip(self): - """ returns your external IP address by querying dyndns.org """ - f = urllib.urlopen('http://checkip.dyndns.org/') - s = f.read() - m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s) - return m.group(0) - - def Push(self, handler, query): - tsn = query['tsn'][0] - for key in config.tivo_names: - if config.tivo_names[key] == tsn: - tsn = key - break - tivo_name = config.tivo_names.get(tsn, tsn) - - container = quote(query['Container'][0].split('/')[0]) - ip = config.get_ip(tsn) - port = config.getPort() - - baseurl = 'http://%s:%s/%s' % (ip, port, container) - if config.getIsExternal(tsn): - exturl = config.get_server('externalurl') - if exturl: - baseurl = exturl - else: - ip = self.readip() - baseurl = 'http://%s:%s/%s' % (ip, port, container) - - path = self.get_local_base_path(handler, query) - - files = query.get('File', []) - for f in files: - file_path = path + os.path.normpath(f) - queue.append({'path': file_path, 'name': f, 'tsn': tsn, - 'url': baseurl}) - if len(queue) == 1: - thread.start_new_thread(DVDVideo.process_queue, (self,)) - - logger.info('[%s] Queued "%s" for Push to %s' % - (time.strftime('%d/%b/%Y %H:%M:%S'), - unicode(file_path, 'utf-8'), tivo_name)) - - files = [unicode(f, 'utf-8') for f in files] - handler.redir(PUSHED % (tivo_name, '
'.join(files)), 5) - -class BaseVideo(Plugin): - - CONTENT_TYPE = 'x-container/tivo-videos' - - tvbus_cache = LRUCache(1) - - def pre_cache(self, full_path): - if DVDVideo.video_file_filter(self, full_path): - transcode.supported_format(full_path) - - def video_file_filter(self, full_path, type=None): - if os.path.isdir(unicode(full_path, 'utf-8')): - return True - if use_extensions: - return os.path.splitext(full_path)[1].lower() in EXTENSIONS - else: - return transcode.supported_format(full_path) - - def send_file(self, handler, path, query): - mime = 'video/x-tivo-mpeg' - tsn = handler.headers.getheader('tsn', '') - tivo_name = config.tivo_names.get(tsn, tsn) - - is_tivo_file = (path[-5:].lower() == '.tivo') - - if 'Format' in query: - mime = query['Format'][0] - - needs_tivodecode = (is_tivo_file and mime == 'video/mpeg') - compatible = (not needs_tivodecode and - transcode.tivo_compatible(path, tsn, mime)[0]) - - try: # "bytes=XXX-" - offset = int(handler.headers.getheader('Range')[6:-1]) - except: - offset = 0 - - if needs_tivodecode: - valid = bool(config.get_bin('tivodecode') and - config.get_server('tivo_mak')) - else: - valid = True - - ##DVDVideo - fname = unicode(path, 'utf-8') - if transcode.is_dvd( path ): - size = transcode.dvd_size( path ) - else: - size = os.stat(fname).st_size - - if valid and offset: - valid = ((compatible and offset < size) or - (not compatible and transcode.is_resumable(path, offset))) - - #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and - faking = (mime == 'video/x-tivo-mpeg' and - not (is_tivo_file and compatible)) - #fname = unicode(path, 'utf-8') - thead = '' - if faking: - thead = self.tivo_header(tsn, path, mime) - if compatible: - size = size + len(thead) - handler.send_response(200) - handler.send_header('Content-Length', size - offset) - handler.send_header('Content-Range', 'bytes %d-%d/%d' % - (offset, size - offset - 1, size)) - else: - handler.send_response(206) - handler.send_header('Transfer-Encoding', 'chunked') - handler.send_header('Content-Type', mime) - handler.send_header('Connection', 'close') - handler.end_headers() - - logger.info('[%s] Start sending "%s" to %s' % - (time.strftime('%d/%b/%Y %H:%M:%S'), fname, tivo_name)) - start = time.time() - count = 0 - - if valid: - if compatible: - if faking and not offset: - handler.wfile.write(thead) - logger.debug('"%s" is tivo compatible' % fname) - f = open(fname, 'rb') - try: - if mime == 'video/mp4': - count = qtfaststart.process(f, handler.wfile, offset) - else: - if offset: - offset -= len(thead) - f.seek(offset) - while True: - block = f.read(512 * 1024) - if not block: - break - handler.wfile.write(block) - count += len(block) - except Exception, msg: - logger.info(msg) - f.close() - else: - logger.debug('"%s" is not tivo compatible' % fname) - if offset: - count = transcode.resume_transfer(path, handler.wfile, - offset) - else: - count = transcode.transcode(False, path, handler.wfile, - tsn, mime, thead) - try: - if not compatible: - handler.wfile.write('0\r\n\r\n') - handler.wfile.flush() - except Exception, msg: - logger.info(msg) - - mega_elapsed = (time.time() - start) * 1024 * 1024 - if mega_elapsed < 1: - mega_elapsed = 1 - rate = count * 8.0 / mega_elapsed - logger.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' % - (time.strftime('%d/%b/%Y %H:%M:%S'), fname, - tivo_name, count, rate)) - - if fname.endswith('.pyTivo-temp'): - os.remove(fname) - - def __duration(self, full_path): - return transcode.video_info(full_path)['millisecs'] - - def __total_items(self, full_path): - count = 0 - try: - full_path = unicode(full_path, 'utf-8') - for f in os.listdir(full_path): - if f.startswith('.'): - continue - f = os.path.join(full_path, f) - f2 = f.encode('utf-8') - if os.path.isdir(f): - count += 1 - elif use_extensions: - if os.path.splitext(f2)[1].lower() in EXTENSIONS: - count += 1 - elif f2 in transcode.info_cache: - if transcode.supported_format(f2): - count += 1 - except: - pass - return count - - def __est_size(self, full_path, tsn='', mime=''): - ##DVDVideo - if transcode.is_dvd( full_path ): - return transcode.dvd_size( full_path ) - - # Size is estimated by taking audio and video bit rate adding 2% - - if transcode.tivo_compatible(full_path, tsn, mime)[0]: - return int(os.stat(unicode(full_path, 'utf-8')).st_size) - else: - # Must be re-encoded - if config.get_tsn('audio_codec', tsn) == None: - audioBPS = config.getMaxAudioBR(tsn) * 1000 - else: - audioBPS = config.strtod(config.getAudioBR(tsn)) - videoBPS = transcode.select_videostr(full_path, tsn) - bitrate = audioBPS + videoBPS - return int((self.__duration(full_path) / 1000) * - (bitrate * 1.02 / 8)) - - def metadata_full(self, full_path, tsn='', mime=''): - data = {} - vInfo = transcode.video_info(full_path) - - if ((int(vInfo['vHeight']) >= 720 and - config.getTivoHeight >= 720) or - (int(vInfo['vWidth']) >= 1280 and - config.getTivoWidth >= 1280)): - data['showingBits'] = '4096' - - data.update(self.metadata_basic(full_path)) - if full_path[-5:].lower() == '.tivo': - data.update(metadata.from_tivo(full_path)) - if full_path[-4:].lower() == '.wtv': - data.update(metadata.from_mscore(vInfo['rawmeta'])) - - if 'episodeNumber' in data: - try: - ep = int(data['episodeNumber']) - except: - ep = 0 - data['episodeNumber'] = str(ep) - - if config.getDebug() and 'vHost' not in data: - compatible, reason = transcode.tivo_compatible(full_path, tsn, mime) - if compatible: - transcode_options = {} - else: - transcode_options = transcode.transcode(True, full_path, - '', tsn, mime) - data['vHost'] = ( - ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible], reason)] + - ['SOURCE INFO: '] + - ["%s=%s" % (k, v) - for k, v in sorted(vInfo.items(), reverse=True)] + - ['TRANSCODE OPTIONS: '] + - ["%s" % (v) for k, v in transcode_options.items()] + - ['SOURCE FILE: ', os.path.basename(full_path)] - ) - - now = datetime.utcnow() - if 'time' in data: - if data['time'].lower() == 'file': - mtime = os.stat(unicode(full_path, 'utf-8')).st_mtime - if (mtime < 0): - mtime = 0 - try: - now = datetime.utcfromtimestamp(mtime) - except: - logger.warning('Bad file time on ' + full_path) - elif data['time'].lower() == 'oad': - now = isodt(data['originalAirDate']) - else: - try: - now = isodt(data['time']) - except: - logger.warning('Bad time format: ' + data['time'] + - ' , using current time') - - duration = self.__duration(full_path) - duration_delta = timedelta(milliseconds = duration) - min = duration_delta.seconds / 60 - sec = duration_delta.seconds % 60 - hours = min / 60 - min = min % 60 - - data.update({'time': now.isoformat(), - 'startTime': now.isoformat(), - 'stopTime': (now + duration_delta).isoformat(), - 'size': self.__est_size(full_path, tsn, mime), - 'duration': duration, - 'iso_duration': ('P%sDT%sH%sM%sS' % - (duration_delta.days, hours, min, sec))}) - - return data - - def metadata_basic(self, full_path): - vdvd = virtualdvd.VirtualDVD(full_path) - if not vdvd.Valid(): - return metadata.basic(full_path) - - base_path, name = os.path.split(full_path) - title, ext = os.path.splitext(name) - mtime = os.stat(unicode(base_path, 'utf-8')).st_mtime - if (mtime < 0): - mtime = 0 - originalAirDate = datetime.utcfromtimestamp(mtime) - - if vdvd.DVDTitleName(): - title = vdvd.DVDTitleName() - - data = {'title': title, - 'originalAirDate': originalAirDate.isoformat()} - ext = ext.lower() - if ext in ['.mp4', '.m4v', '.mov']: - data.update(from_moov(full_path)) - elif ext in ['.dvr-ms', '.asf', '.wmv']: - data.update(from_dvrms(full_path)) - elif 'plistlib' in sys.modules and base_path.endswith('.eyetv'): - data.update(from_eyetv(full_path)) - data.update(metadata.from_nfo(full_path)) - data.update(metadata.from_text(full_path)) - - if 'episodeTitle' in data: - pass - elif 'Title '+ str(vdvd.TitleNumber()) in data: - data['episodeTitle'] = data['Title ' + str(vdvd.TitleNumber())] - else: - data['episodeTitle'] = vdvd.TitleName() - - if vdvd.FileTitle().HasAngles(): - if 'description' in data: - data['description'] = "[Angle] " + data['description'] - else: - data['description'] = "[Angle]" - if vdvd.FileTitle().HasInterleaved(): - if 'description' in data: - data['description'] = "[ILVU] " + data['description'] - else: - data['description'] = "[ILVU]" - - return data - - def QueryContainer(self, handler, query): - tsn = handler.headers.getheader('tsn', '') - subcname = query['Container'][0] - useragent = handler.headers.getheader('User-Agent', '') - dvd = None - - if self.get_local_path(handler, query): - dir_path = self.get_local_path(handler, query) - if os.path.isdir(unicode(dir_path, 'utf-8')): - dvd = virtualdvd.VirtualDVD(dir_path, \ - float( handler.container.get( 'title_min', '10.0' ) ) ) - if not (dvd.Valid() or dvd.HasErrors()): - dvd = None - - if not self.get_local_path(handler, query) and \ - not dvd: - handler.send_error(404) - return - - container = handler.container - precache = container.get('precache', 'False').lower() == 'true' - force_alpha = container.get('force_alpha', 'False').lower() == 'true' - use_html = query.get('Format', [''])[0].lower() == 'text/html' - - # If the DVD folder is valid, then get the files and process them - if dvd: - files = dvd.GetFiles() - files, total, start = self.item_count(handler, query, handler.cname, files, 0) - else: - files, total, start = self.get_files(handler, query, - self.video_file_filter, - force_alpha) - - videos = [] - local_base_path = self.get_local_base_path(handler, query) - for f in files: - video = VideoDetails() - mtime = f.mdate - try: - ltime = time.localtime(mtime) - except: - logger.warning('Bad file time on ' + unicode(f.name, 'utf-8')) - mtime = int(time.time()) - ltime = time.localtime(mtime) - video['captureDate'] = hex(mtime) - video['textDate'] = time.strftime('%b %d, %Y', ltime) - video['name'] = os.path.basename(f.name) - video['path'] = f.name - video['part_path'] = f.name.replace(local_base_path, '', 1) - if not video['part_path'].startswith(os.path.sep): - video['part_path'] = os.path.sep + video['part_path'] - video['title'] = os.path.basename(f.name) - video['is_dir'] = f.isdir - if video['is_dir']: - video['small_path'] = subcname + '/' + video['name'] - video['total_items'] = self.__total_items(f.name) - sub_dvd = virtualdvd.VirtualDVD( f.name ) - if sub_dvd.QuickValid(): - # Greatly speed up listing - if container.get('fast_listing', 'False').lower() == 'false': - video['total_items'] = sub_dvd.NumFiles() - else: - video['total_items'] = 1 - - elif sub_dvd.HasErrors(): - video['total_items'] = 0 - else: - if dvd != None and dvd.HasErrors(): - video['title'] = "Error in DVD Format" - video['callsign'] = "DVD" - video['displayMajorNumber'] = "1" - video['displayMinorNumber'] = "0" - video['description'] = f.title - - if precache or len(files) == 1 or f.name in transcode.info_cache: - video['valid'] = transcode.supported_format(f.name) - if video['valid']: - video.update(self.metadata_full(f.name, tsn)) - if len(files) == 1: - video['captureDate'] = hex(isogm(video['time'])) - elif dvd != None and (dvd.Valid() or dvd.HasErrors()): - video['valid'] = True - video.update(self.metadata_basic(f.name)) - else: - video['valid'] = True - video.update(metadata.basic(f.name)) - - if config.has_ts_flag(): - video['mime'] = 'video/x-tivo-mpeg-ts' - else: - video['mime'] = 'video/x-tivo-mpeg' - - video['textSize'] = ( '%.3f GB' % - (float(f.size) / (1024 ** 3)) ) - - if video['episodeTitle'].lower().startswith('ignore'): - continue - videos.append(video) - - logger.debug('mobileagent: %d useragent: %s' % (useragent.lower().find('mobile'), useragent.lower())) - use_mobile = useragent.lower().find('mobile') > 0 - if use_html: - if use_mobile: - t = Template(HTML_CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) - else: - t = Template(HTML_CONTAINER_TEMPLATE, filter=EncodeUnicode) - else: - t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) - - t.container = handler.cname - t.name = subcname - t.total = total - t.start = start - t.videos = videos - t.quote = quote - t.escape = escape - t.crc = zlib.crc32 - t.guid = config.getGUID() - t.tivos = config.tivos - t.tivo_names = config.tivo_names - if use_html: - handler.send_html(str(t)) - else: - handler.send_xml(str(t)) - - def get_details_xml(self, tsn, file_path): - if (tsn, file_path) in self.tvbus_cache: - details = self.tvbus_cache[(tsn, file_path)] - else: - file_info = VideoDetails() - file_info['valid'] = transcode.supported_format(file_path) - if file_info['valid']: - file_info.update(self.metadata_full(file_path, tsn)) - - t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode) - t.video = file_info - t.escape = escape - t.get_tv = metadata.get_tv - t.get_mpaa = metadata.get_mpaa - t.get_stars = metadata.get_stars - details = str(t) - self.tvbus_cache[(tsn, file_path)] = details - return details - - def tivo_header(self, tsn, path, mime): - if mime == 'video/x-tivo-mpeg-ts': - flag = 45 - else: - flag = 13 - details = self.get_details_xml(tsn, path) - ld = len(details) - chunklen = ld * 2 + 44 - padding = 2048 - chunklen % 1024 - - return ''.join(['TiVo', struct.pack('>HHHLH', 4, flag, 0, - padding + chunklen, 2), - struct.pack('>LLHH', ld + 16, ld, 1, 0), - details, '\0' * 4, - struct.pack('>LLHH', ld + 19, ld, 2, 0), - details, '\0' * padding]) - - def TVBusQuery(self, handler, query): - tsn = handler.headers.getheader('tsn', '') - f = query['File'][0] - path = self.get_local_path(handler, query) - file_path = path + os.path.normpath(f) - - details = self.get_details_xml(tsn, file_path) - - handler.send_xml(details) - -class DVDVideo(BaseVideo, Pushable): - pass - -class VideoDetails(DictMixin): - - def __init__(self, d=None): - if d: - self.d = d - else: - self.d = {} - - def __getitem__(self, key): - if key not in self.d: - self.d[key] = self.default(key) - return self.d[key] - - def __contains__(self, key): - return True - - def __setitem__(self, key, value): - self.d[key] = value - - def __delitem__(self): - del self.d[key] - - def keys(self): - return self.d.keys() - - def __iter__(self): - return self.d.__iter__() - - def iteritems(self): - return self.d.iteritems() - - def default(self, key): - defaults = { - 'showingBits' : '0', - 'displayMajorNumber' : '0', - 'displayMinorNumber' : '0', - 'isEpisode' : 'true', - 'colorCode' : ('COLOR', '4'), - 'showType' : ('SERIES', '5') - } - if key in defaults: - return defaults[key] - elif key.startswith('v'): - return [] - else: - return '' diff --git a/plugins/dvdvideo/ilvuhack.py b/plugins/dvdvideo/ilvuhack.py deleted file mode 100644 index 2d6b6179..00000000 --- a/plugins/dvdvideo/ilvuhack.py +++ /dev/null @@ -1,198 +0,0 @@ -# Module: ilvuhack.py -# Author: Eric von Bayer -# Contact: -# Date: September 24, 2009 -# Description: -# Code for taking a sector block and figuring out what sectors actually -# belong to the ILVU angle we start with for the cell. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -from compositefile import CompositeFile - -DVD_BLOCK_LEN = 2048L -DEBUG = False - -try: - os.SEEK_SET -except AttributeError: - os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3) - -def be_uint8( istr ): - if len(istr) < 1: - return 0 - else: - return ord(istr[0]) - -def be_uint16( istr ): - if len(istr) < 2: - return 0 - else: - return ( ord(istr[0]) << 8 ) + ord(istr[1]) - -def be_uint32( istr ): - if len(istr) < 4: - return 0 - else: - return ( ord(istr[0]) << 24 ) + ( ord(istr[1]) << 16 ) + \ - ( ord(istr[2]) << 8 ) + ord(istr[3]) - -def GetNextDSIPacket( fh ): - while 1: - # Read the packet header - packet_id = fh.read(3) - - if len(packet_id) == 0: - return ( 0, "" ) - - # Make sure we had a valid packet header - if packet_id != '\000\000\001': - raise "Packet Header ID (0x%06x) doesn't match 0x000001" % \ - ( ( ord(packet_id[0]) << 16 ) + ( ord(packet_id[1]) << 8 ) + \ - ord(packet_id[2]) ) - - # Read in the stream ID - stream_id = ord(fh.read(1)) - - # Skip the specialty pack header packet that does not conform to the - # usual standard - if stream_id == 0xBA: - packet_len = 10 - data = fh.read(packet_len) - pad = ord(data[9]) & 0x7 - fh.read( pad ) - - # Skip past all the normal packets till we find the DSI packet - else: - - # Get the sector offset - off = fh.tell() - 4 - - # Read in the packet length - packet_len = ( ord(fh.read(1)) << 8 ) + ord(fh.read(1)) - - # If the stream is Private Stream 2 (0xBF) then check the substream - if stream_id == 0xBF: - - # Get the sub stream id - sub_stream_id = ord( fh.read(1) ) - - # If we have the DSI packet, then return out - if packet_len == 1018 and sub_stream_id == 1: - return ( off, fh.read(packet_len-1) ) - - # Otherwise skip the rest - else: - fh.seek( packet_len-1, os.SEEK_CUR ) - - # Seek past the packet - else: - fh.seek( packet_len, os.SEEK_CUR ) - -import sys -def cond( test, t, f ): - if test: - return t - else: - return f - - -# Walk the VOB encapsulated MPEG stream to determine which sectors are really -# part of our ILVU stream. -def ComputeRealSectors( start, end, *files ): - - if DEBUG: - print "ComputeRealSectors(",start,",",end,"):" - - # Assemble a composite file - cfile = CompositeFile( *files ) - - # Set the current start to the real start - cur_start = start - - # Real sector list - rsl = list() - first = True - - while cur_start >= start and cur_start <= end: - - # if the seek fails, we're done. - try: - # Seek to our starting sector - cfile.seek( DVD_BLOCK_LEN * cur_start ) - - except: - break - - # Read in the next DSI packet - off, dsi = GetNextDSIPacket( cfile ) - - if DEBUG: - print "[S%d/0x%08x+%04X]: [%c%c%c%c]" % ( - off/DVD_BLOCK_LEN, off/DVD_BLOCK_LEN, off%DVD_BLOCK_LEN, - cond( be_uint8(dsi[32]) & 0x80, "P", "." ), - cond( be_uint8(dsi[32]) & 0x40, "I", "." ), - cond( be_uint8(dsi[32]) & 0x20, "S", "." ), - cond( be_uint8(dsi[32]) & 0x10, "E", "." ) - ) - #for i in range(48): - # if i % 16 == 0: - # sys.stdout.write( "+%03X: " % i ) - # - # sys.stdout.write( "%02X " % ord(dsi[i]) ) - # if i % 16 == 15 and i != 0: - # print "" - - print "Audio Packet Off:",list( be_uint16( dsi[apo:apo+2] ) for apo in range(402,418,2) ) - - print "ILVU End: %08x" % be_uint32( dsi[34:38] ) - print "ILVU Next: %08x" % be_uint32( dsi[38:42] ) - print "ILVU Size: %04x" % be_uint16( dsi[42:44] ) - - # If we're not an ILVU, then return our original start,end - if be_uint8(dsi[32]) & 0x40 == 0 and first == True: - return [ [ start, end ] ] - - # Read in the information for the - end_ilvu_block = be_uint32( dsi[34:38] ) - next_ilvu_block = be_uint32( dsi[38:42] ) - - if be_uint8(dsi[32]) & 0x60 != 0x60: - #print "* Not a start, skipping this block. **********************************" - cur_start += next_ilvu_block - continue - - - # Add the block to the block list - rsl.append( [ cur_start, cur_start + end_ilvu_block ] ) - - # Advance the next cur start - cur_start = cur_start + next_ilvu_block - first = False - - return rsl \ No newline at end of file diff --git a/plugins/dvdvideo/qtfaststart.py b/plugins/dvdvideo/qtfaststart.py deleted file mode 100644 index 7603daa1..00000000 --- a/plugins/dvdvideo/qtfaststart.py +++ /dev/null @@ -1,261 +0,0 @@ -""" - Quicktime/MP4 Fast Start - ------------------------ - Enable streaming and pseudo-streaming of Quicktime and MP4 files by - moving metadata and offset information to the front of the file. - - This program is based on qt-faststart.c from the ffmpeg project, which is - released into the public domain, as well as ISO 14496-12:2005 (the official - spec for MP4), which can be obtained from the ISO or found online. - - The goals of this project are to run anywhere without compilation (in - particular, many Windows and Mac OS X users have trouble getting - qt-faststart.c compiled), to run about as fast as the C version, to be more - user friendly, and to use less actual lines of code doing so. - - Features - -------- - - * Works everywhere Python can be installed - * Handles both 32-bit (stco) and 64-bit (co64) atoms - * Handles any file where the mdat atom is before the moov atom - * Preserves the order of other atoms - * Can replace the original file (if given no output file) - - History - ------- - * 2010-02-21: Add support for final mdat atom with zero size, patch by - Dmitry Simakov , version bump - to 1.4. - * 2009-11-05: Add --sample option. Version bump to 1.3. - * 2009-03-13: Update to be more library-friendly by using logging module, - rename fast_start => process, version bump to 1.2 - * 2008-10-04: Bug fixes, support multiple atoms of the same type, - version bump to 1.1 - * 2008-09-02: Initial release - - License - ------- - Copyright (C) 2008 - 2009 Daniel G. Taylor - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import struct - -from StringIO import StringIO - -VERSION = "1.4wjm3" -CHUNK_SIZE = 8192 -SEEK_CUR = 1 # Not defined in Python 2.4, so we define it here -- WJM3 - -log = logging.getLogger('pyTivo.video.qt-faststart') - -count = 0 - -class FastStartException(Exception): - pass - -def read_atom(datastream): - """ - Read an atom and return a tuple of (size, type) where size is the size - in bytes (including the 8 bytes already read) and type is a "fourcc" - like "ftyp" or "moov". - """ - return struct.unpack(">L4s", datastream.read(8)) - -def get_index(datastream): - """ - Return an index of top level atoms, their absolute byte-position in the - file and their size in a list: - - index = [ - ("ftyp", 0, 24), - ("moov", 25, 2658), - ("free", 2683, 8), - ... - ] - - The tuple elements will be in the order that they appear in the file. - """ - index = [] - - log.debug("Getting index of top level atoms...") - - # Read atoms until we catch an error - while(datastream): - try: - skip = 8 - atom_size, atom_type = read_atom(datastream) - if atom_size == 1: - atom_size = struct.unpack(">Q", datastream.read(8))[0] - skip = 16 - log.debug("%s: %s" % (atom_type, atom_size)) - except: - break - - index.append((atom_type, datastream.tell() - skip, atom_size)) - - if atom_size == 0: - # Some files may end in mdat with no size set, which generally - # means to seek to the end of the file. We can just stop indexing - # as no more entries will be found! - break - - datastream.seek(atom_size - skip, SEEK_CUR) - - # Make sure the atoms we need exist - top_level_atoms = set([item[0] for item in index]) - for key in ["moov", "mdat"]: - if key not in top_level_atoms: - log.error("%s atom not found, is this a valid MOV/MP4 file?" % key) - raise FastStartException() - - return index - -def find_atoms(size, datastream): - """ - This function is a generator that will yield either "stco" or "co64" - when either atom is found. datastream can be assumed to be 8 bytes - into the stco or co64 atom when the value is yielded. - - It is assumed that datastream will be at the end of the atom after - the value has been yielded and processed. - - size is the number of bytes to the end of the atom in the datastream. - """ - stop = datastream.tell() + size - - while datastream.tell() < stop: - try: - atom_size, atom_type = read_atom(datastream) - except: - log.exception("Error reading next atom!") - raise FastStartException() - - if atom_type in ["trak", "mdia", "minf", "stbl"]: - # Known ancestor atom of stco or co64, search within it! - for atype in find_atoms(atom_size - 8, datastream): - yield atype - elif atom_type in ["stco", "co64"]: - yield atom_type - else: - # Ignore this atom, seek to the end of it. - datastream.seek(atom_size - 8, SEEK_CUR) - -def output(outfile, skip, data): - global count - length = len(data) - if count + length > skip: - if skip > count: - data = data[skip - count:] - outfile.write(data) - count += length - -def process(datastream, outfile, skip=0): - """ - Convert a Quicktime/MP4 file for streaming by moving the metadata to - the front of the file. This method writes a new file. - """ - - global count - count = 0 - - # Get the top level atom index - index = get_index(datastream) - - mdat_pos = 999999 - free_size = 0 - - # Make sure moov occurs AFTER mdat, otherwise no need to run! - for atom, pos, size in index: - # The atoms are guaranteed to exist from get_index above! - if atom == "moov": - moov_pos = pos - moov_size = size - elif atom == "mdat": - mdat_pos = pos - elif atom == "free" and pos < mdat_pos: - # This free atom is before the mdat! - free_size += size - log.info("Removing free atom at %d (%d bytes)" % (pos, size)) - - # Offset to shift positions - offset = moov_size - free_size - - if moov_pos < mdat_pos: - # moov appears to be in the proper place, don't shift by moov size - offset -= moov_size - if not free_size: - # No free atoms and moov is correct, we are done! - log.debug('mp4 already streamable -- copying') - datastream.seek(skip) - while True: - block = datastream.read(CHUNK_SIZE) - if not block: - break - output(outfile, 0, block) - return count - - # Read and fix moov - datastream.seek(moov_pos) - moov = StringIO(datastream.read(moov_size)) - - # Ignore moov identifier and size, start reading children - moov.seek(8) - - for atom_type in find_atoms(moov_size - 8, moov): - # Read either 32-bit or 64-bit offsets - ctype, csize = atom_type == "stco" and ("L", 4) or ("Q", 8) - - # Get number of entries - version, entry_count = struct.unpack(">2L", moov.read(8)) - - log.info("Patching %s with %d entries" % (atom_type, entry_count)) - - # Read entries - entries = struct.unpack(">" + ctype * entry_count, - moov.read(csize * entry_count)) - - # Patch and write entries - moov.seek(-csize * entry_count, SEEK_CUR) - moov.write(struct.pack(">" + ctype * entry_count, - *[entry + offset for entry in entries])) - - log.info("Writing output...") - - # Write ftype - for atom, pos, size in index: - if atom == "ftyp": - datastream.seek(pos) - output(outfile, skip, datastream.read(size)) - - # Write moov - moov.seek(0) - output(outfile, skip, moov.read()) - - # Write the rest - atoms = [item for item in index if item[0] not in ["ftyp", "moov", "free"]] - for atom, pos, size in atoms: - datastream.seek(pos) - - # Write in chunks to not use too much memory - for x in range(size / CHUNK_SIZE): - output(outfile, skip, datastream.read(CHUNK_SIZE)) - - if size % CHUNK_SIZE: - output(outfile, skip, datastream.read(size % CHUNK_SIZE)) - - return count - skip diff --git a/plugins/dvdvideo/templates/TvBus.tmpl b/plugins/dvdvideo/templates/TvBus.tmpl deleted file mode 100644 index a83c2778..00000000 --- a/plugins/dvdvideo/templates/TvBus.tmpl +++ /dev/null @@ -1,111 +0,0 @@ - - - $video.iso_duration - - - HIGH - - - - $video.iso_duration - #if $video.partCount and $video.partIndex - $video.partCount - $video.partIndex - #end if - - - #for $element in $video.vActor - $escape($element) - #end for - - - - - #for $element in $video.vChoreographer - $escape($element) - #end for - - $video.colorCode[0] - $escape($video.description) - - #for $element in $video.vDirector - $escape($element) - #end for - - $escape($video.episodeNumber) - #if $video.isEpisode != 'false' and $video.episodeTitle - $escape($video.episodeTitle) - #end if - - #for $element in $video.vExecProducer - $escape($element) - #end for - - - #for $element in $video.vProgramGenre - $escape($element) - #end for - - - #for $element in $video.vGuestStar - $escape($element) - #end for - - - #for $element in $video.vHost - $escape($element) - #end for - - $video.isEpisode - #if $video.movieYear - $video.movieYear - #else - $video.originalAirDate - #end if - #if $video.mpaaRating - $video.mpaaRating[0] - #end if - - #for $element in $video.vProducer - $escape($element) - #end for - - - $video.isEpisode - - #for $element in $video.vSeriesGenre - $escape($element) - #end for - - $escape($video.seriesTitle) - #if $video.seriesId - $video.seriesId - #end if - - $video.showType[0] - #if $video.starRating - $video.starRating[0] - #end if - #if $video.seriesTitle - $escape($video.seriesTitle) - #else - $escape($video.title) - #end if - - #for $element in $video.vWriter - $escape($element) - #end for - - - - $video.displayMajorNumber - $video.displayMinorNumber - $escape($video.callsign) - - #if $video.tvRating - $video.tvRating[0] - #end if - - $video.startTime - $video.stopTime - diff --git a/plugins/dvdvideo/templates/container_html.tmpl b/plugins/dvdvideo/templates/container_html.tmpl deleted file mode 100644 index b56b0401..00000000 --- a/plugins/dvdvideo/templates/container_html.tmpl +++ /dev/null @@ -1,106 +0,0 @@ - - - -pyTivo - Push - $escape($name) - - - -
-

pyTivo - Push - $escape($name)

-

Home

- - - ## Header Row - - - - - - - - #set $parent = '' - #set $folders = $name.split("/") - #set $current_folder = $folders.pop() - #set $parent = '/'.join($folders) - #if $parent != '' - - - - - #end if - #set $i = 0 - ## i variable is used to alternate colors of row - ## loop through passed data printing row for each show or folder - #for $video in $videos - #set $i += 1 - #set $j = $i%2 - - #if $video.is_dir - ## This is a folder - - - - - - #else - ## This is a show - - - - - - #end if - - #end for -
TitleSizeCapture Date
- Up to Parent Folder -
$video.title $video.total_items Items$video.textDate - - - #if $video.episodeTitle - $escape($video.title): $escape($video.episodeTitle) - #else - $escape($video.title) - #end if - - #if $video.description - $escape($video.description) - #end if - #if $video.displayMajorNumbe and $video.callsign - $video.displayMajorNumber $video.callsign - #end if - - $video.textSize - $video.textDate
-

- - - - -

-
- - \ No newline at end of file diff --git a/plugins/dvdvideo/templates/container_mob.tmpl b/plugins/dvdvideo/templates/container_mob.tmpl deleted file mode 100644 index 41609bfe..00000000 --- a/plugins/dvdvideo/templates/container_mob.tmpl +++ /dev/null @@ -1,88 +0,0 @@ - - - -pyTivo - Push - $escape($name) - - - - -
- -

pyTiVo

- - #set $parent = '' - #set $folders = $name.split("/") - #set $current_folder = $folders.pop() - #set $parent = '/'.join($folders) - - - - - #set $i = 0 - ## i variable is used to alternate colors of row - ## loop through passed data printing row for each show or folder - #for $video in $videos - #set $i += 1 - #set $j = $i%2 - - #if $video.is_dir - ## This is a folder - - - - #else - ## This is a show - - - - #end if - - #end for -
$video.title - #if $video.episodeTitle - $video.title: $video.episodeTitle - #else - $video.title - #end if - - #if $video.description - $video.description - #end if - #if $video.displayMajorNumbe and $video.callsign - $video.displayMajorNumber $video.callsign - #end if - Added on $video.textDate - - -
-
- - - - -
-
- - diff --git a/plugins/dvdvideo/templates/container_xml.tmpl b/plugins/dvdvideo/templates/container_xml.tmpl deleted file mode 100644 index 3bb0c52b..00000000 --- a/plugins/dvdvideo/templates/container_xml.tmpl +++ /dev/null @@ -1,70 +0,0 @@ - - - $start - #echo len($videos) # -
- $escape($name) - x-container/tivo-videos - x-container/folder - $total - $crc($guid + $name) -
- #for $video in $videos - #if $video.is_dir - -
- $escape($video.title) - x-container/folder - x-tivo-container/tivo-dvr - $crc($guid + $video.small_path) - $video.total_items - $video.captureDate -
- - - /TiVoConnect?Command=QueryContainer&Container=$quote($name)/$quote($video.name) - x-tivo-container/folder - - -
- #else - -
- $escape($video.title) - video/x-tivo-mpeg - #if not $video.valid - Yes - #end if - video/x-ms-wmv - $video.size - $video.duration - #if $video.isEpisode != 'false' and $video.episodeTitle - $escape($video.episodeTitle) - #end if - $escape($video.description) - $escape($video.displayMajorNumber) - $escape($video.callsign) - $video.seriesId - $video.captureDate -
- - - video/x-tivo-mpeg - No - /$quote($container)$quote($video.part_path) - - - video/* - No - urn:tivo:image:save-until-i-delete-recording - - - text/xml - No - /TiVoConnect?Command=TVBusQuery&Container=$quote($container)&File=$quote($video.part_path) - - -
- #end if - #end for -
\ No newline at end of file diff --git a/plugins/dvdvideo/transcode.py b/plugins/dvdvideo/transcode.py deleted file mode 100644 index f1377ed9..00000000 --- a/plugins/dvdvideo/transcode.py +++ /dev/null @@ -1,1178 +0,0 @@ -# Module: dvdfolder.py -# Author: Eric von Bayer -# Updated By: Luke Broadbent -# Contact: -# Date: June 5, 2012 -# Description: -# Routines for transcoding DVD vob files to mpeg file the TiVo can play. -# This is closely aligned with transcode.py from the video plugin (from -# wmcbrine's branch). -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import logging -import math -import os -import re -import shutil -import subprocess -import sys -import tempfile -import threading -import time - -import lrucache - -import config -import metadata - -import vobstream - -logger = logging.getLogger('pyTivo.video.transcode') - -info_cache = lrucache.LRUCache(1000) -ffmpeg_procs = {} -reapers = {} - -GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97', - '30.00', '50.00', '59.94', '60.00'] - -BLOCKSIZE = 512 * 1024 -MAXBLOCKS = 2 -TIMEOUT = 600 - -UNSET = 0 -OLD_PAD = 1 -NEW_PAD = 2 - -pad_style = UNSET - -# XXX BIG HACK -# subprocess is broken for me on windows so super hack -def patchSubprocess(): - o = subprocess.Popen._make_inheritable - - def _make_inheritable(self, handle): - if not handle: return subprocess.GetCurrentProcess() - return o(self, handle) - - subprocess.Popen._make_inheritable = _make_inheritable -mswindows = (sys.platform == "win32") -if mswindows: - patchSubprocess() - -def debug(msg): - if type(msg) == str: - try: - msg = msg.decode('utf8') - except: - if sys.platform == 'darwin': - msg = msg.decode('macroman') - else: - msg = msg.decode('iso8859-1') - logger.debug(msg) - -def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): - settings = {'video_codec': select_videocodec(inFile, tsn), - 'video_br': select_videobr(inFile, tsn), - 'video_fps': select_videofps(inFile, tsn), - 'max_video_br': select_maxvideobr(tsn), - 'buff_size': select_buffsize(tsn), - 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), - 'audio_br': select_audiobr(tsn), - 'audio_fr': select_audiofr(inFile, tsn), - 'audio_ch': select_audioch(inFile, tsn), - 'audio_codec': select_audiocodec(isQuery, inFile, tsn), - 'audio_lang': select_audiolang(inFile, tsn), - 'ffmpeg_pram': select_ffmpegprams(tsn), - 'format': select_format(tsn, mime)} - - if isQuery: - return settings - - ffmpeg_path = config.get_bin('ffmpeg') - cmd_string = config.getFFmpegTemplate(tsn) % settings - fname = unicode(inFile, 'utf-8') - if mswindows: - fname = fname.encode('iso8859-1') - - if inFile[-5:].lower() == '.tivo': - tivodecode_path = config.get_bin('tivodecode') - tivo_mak = config.get_server('tivo_mak') - tcmd = [tivodecode_path, '-m', tivo_mak, fname] - tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE, - bufsize=(512 * 1024)) - if tivo_compatible(inFile, tsn)[0]: - cmd = '' - ffmpeg = tivodecode - else: - cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() - ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout, - stdout=subprocess.PIPE, - bufsize=(512 * 1024)) - elif vobstream.is_dvd(inFile): - cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split() - ffmpeg = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - bufsize= BLOCKSIZE * MAXBLOCKS ) - proc = vobstream.vobstream(False, inFile, ffmpeg, BLOCKSIZE) - else: - cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() - ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), - stdout=subprocess.PIPE) - - if cmd: - debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') - debug(' '.join(cmd)) - - ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0, - 'last_read': time.time(), 'blocks': []} - if thead: - ffmpeg_procs[inFile]['blocks'].append(thead) - if vobstream.is_dvd(inFile): - ffmpeg_procs[inFile]['stream'] = proc['stream'] - ffmpeg_procs[inFile]['thread'] = proc['thread'] - ffmpeg_procs[inFile]['event'] = proc['event'] - - reap_process(inFile) - return resume_transfer(inFile, outFile, 0) - -def is_resumable(inFile, offset): - if inFile in ffmpeg_procs: - proc = ffmpeg_procs[inFile] - if proc['start'] <= offset < proc['end']: - return True - else: - cleanup(inFile) - kill(proc['process']) - return False - -def resume_transfer(inFile, outFile, offset): - proc = ffmpeg_procs[inFile] - offset -= proc['start'] - count = 0 - - try: - for block in proc['blocks']: - length = len(block) - if offset < length: - if offset > 0: - block = block[offset:] - outFile.write('%x\r\n' % len(block)) - outFile.write(block) - outFile.write('\r\n') - count += len(block) - offset -= length - outFile.flush() - except Exception, msg: - logger.info(msg) - return count - - proc['start'] = proc['end'] - proc['blocks'] = [] - - return count + transfer_blocks(inFile, outFile) - -def transfer_blocks(inFile, outFile): - proc = ffmpeg_procs[inFile] - blocks = proc['blocks'] - count = 0 - - while True: - try: - block = proc['process'].stdout.read(BLOCKSIZE) - proc['last_read'] = time.time() - except Exception, msg: - logger.info(msg) - cleanup(inFile) - kill(proc['process']) - break - - if not block: - try: - outFile.flush() - except Exception, msg: - logger.info(msg) - else: - cleanup(inFile) - break - - blocks.append(block) - proc['end'] += len(block) - if len(blocks) > MAXBLOCKS: - proc['start'] += len(blocks[0]) - blocks.pop(0) - - try: - outFile.write('%x\r\n' % len(block)) - outFile.write(block) - outFile.write('\r\n') - count += len(block) - except Exception, msg: - logger.info(msg) - break - - return count - -def reap_process(inFile): - if ffmpeg_procs and inFile in ffmpeg_procs: - proc = ffmpeg_procs[inFile] - if proc['last_read'] + TIMEOUT < time.time(): - del ffmpeg_procs[inFile] - del reapers[inFile] - kill(proc['process']) - else: - reaper = threading.Timer(TIMEOUT, reap_process, (inFile,)) - reapers[inFile] = reaper - reaper.start() - -def cleanup(inFile): - if vobstream.is_dvd(inFile): - proc = ffmpeg_procs[inFile] - kill(proc['process']) - proc['process'].wait() - - # Tell thread to break out of loop - proc['event'].set() - proc['thread'].join() - - del ffmpeg_procs[inFile] - reapers[inFile].cancel() - del reapers[inFile] - -def select_audiocodec(isQuery, inFile, tsn='', mime=''): - if inFile[-5:].lower() == '.tivo': - return '-acodec copy' - vInfo = video_info(inFile) - codectype = vInfo['vCodec'] - codec = config.get_tsn('audio_codec', tsn) - if not codec: - # Default, compatible with all TiVo's - codec = 'ac3' - if mime == 'video/mp4': - compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac', - 'ac3', 'liba52') - else: - compatiblecodecs = ('ac3', 'liba52', 'mp2') - - if vInfo['aCodec'] in compatiblecodecs: - aKbps = vInfo['aKbps'] - aCh = vInfo['aCh'] - if aKbps == None: - if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'): - # along with the channel check below this should - # pass any AAC audio that has undefined 'aKbps' and - # is <= 2 channels. Should be TiVo compatible. - codec = 'copy' - elif not isQuery: - vInfoQuery = audio_check(inFile, tsn) - if vInfoQuery == None: - aKbps = None - aCh = None - else: - aKbps = vInfoQuery['aKbps'] - aCh = vInfoQuery['aCh'] - else: - codec = 'TBA' - if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn): - # compatible codec and bitrate, do not reencode audio - codec = 'copy' - if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2): - codec = 'ac3' - copy_flag = config.get_tsn('copy_ts', tsn) - copyts = ' -copyts' - if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or - (copy_flag and copy_flag.lower() == 'false')): - copyts = '' - return '-acodec ' + codec + copyts - -def select_audiofr(inFile, tsn): - freq = '48000' # default - vInfo = video_info(inFile) - if vInfo['aFreq'] == '44100': - # compatible frequency - freq = vInfo['aFreq'] - audio_fr = config.get_tsn('audio_fr', tsn) - if audio_fr != None: - freq = audio_fr - return '-ar ' + freq - -def select_audioch(inFile, tsn): - ch = config.get_tsn('audio_ch', tsn) - if ch: - return '-ac ' + ch - # AC-3 max channels is 5.1 - if video_info(inFile)['aCh'] > 6: - debug('Too many audio channels for AC-3, using 5.1 instead') - return '-ac 6' - elif video_info(inFile)['aCh']: - return '-ac %i' % video_info(inFile)['aCh'] - return '' - -def select_audiolang(inFile, tsn): - vInfo = video_info(inFile) - audio_lang = config.get_tsn('audio_lang', tsn) - debug('audio_lang: %s' % audio_lang) - if vInfo['mapAudio']: - # default to first detected audio stream to begin with - stream = vInfo['mapAudio'][0][0] - if audio_lang != None and vInfo['mapVideo'] != None: - langmatch_curr = [] - langmatch_prev = vInfo['mapAudio'][:] - for lang in audio_lang.replace(' ', '').lower().split(','): - for s, l in langmatch_prev: - if lang in s + l.replace(' ', '').lower(): - langmatch_curr.append((s, l)) - stream = s - # if only 1 item matched we're done - if len(langmatch_curr) == 1: - break - # if more than 1 item matched copy the curr area to the prev - # array we only need to look at the new shorter list from - # now on - elif len(langmatch_curr) > 1: - langmatch_prev = langmatch_curr[:] - # if we drop out of the loop with more than 1 item default to - # the first item - if len(langmatch_prev) > 1: - stream = langmatch_prev[0][0] - # don't let FFmpeg auto select audio stream, pyTivo defaults to - # first detected - if stream: - debug('selected audio stream: %s' % stream) - return '-map ' + vInfo['mapVideo'] + ' -map ' + stream - # if no audio is found - debug('selected audio stream: None detected') - return '' - -def select_videofps(inFile, tsn): - vInfo = video_info(inFile) - fps = '-r 29.97' # default - if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS: - fps = ' ' - video_fps = config.get_tsn('video_fps', tsn) - if video_fps != None: - fps = '-r ' + video_fps - return fps - -def select_videocodec(inFile, tsn): - vInfo = video_info(inFile) - if tivo_compatible_video(vInfo, tsn)[0]: - codec = 'copy' - else: - codec = 'mpeg2video' # default - return '-vcodec ' + codec - -def select_videobr(inFile, tsn): - return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k' - -def select_videostr(inFile, tsn): - vInfo = video_info(inFile) - if tivo_compatible_video(vInfo, tsn)[0]: - video_str = int(vInfo['kbps']) - if vInfo['aKbps']: - video_str -= int(vInfo['aKbps']) - video_str *= 1000 - else: - video_str = config.strtod(config.getVideoBR(tsn)) - if config.isHDtivo(tsn): - if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0: - video_percent = (int(vInfo['kbps']) * 10 * - config.getVideoPCT(tsn)) - video_str = max(video_str, video_percent) - video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95, - video_str)) - return video_str - -def select_audiobr(tsn): - return '-ab ' + config.getAudioBR(tsn) - -def select_maxvideobr(tsn): - return '-maxrate ' + config.getMaxVideoBR(tsn) - -def select_buffsize(tsn): - return '-bufsize ' + config.getBuffSize(tsn) - -def select_ffmpegprams(tsn): - params = config.getFFmpegPrams(tsn) - if not params: - params = '' - return params - -def select_format(tsn, mime): - if mime == 'video/x-tivo-mpeg-ts': - fmt = 'mpegts' - else: - fmt = 'vob' - return '-f %s -' % fmt - -def pad_check(): - global pad_style - if pad_style == UNSET: - pad_style = OLD_PAD - filters = tempfile.TemporaryFile() - cmd = [config.get_bin('ffmpeg'), '-filters'] - ffmpeg = subprocess.Popen(cmd, stdout=filters, stderr=subprocess.PIPE) - ffmpeg.wait() - filters.seek(0) - for line in filters: - if line.startswith('pad'): - pad_style = NEW_PAD - break - filters.close() - return pad_style == NEW_PAD - -def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): - endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) / - vInfo['vWidth']) * multiplier) - if endHeight % 2: - endHeight -= 1 - if endHeight < TIVO_HEIGHT * 0.99: - topPadding = (TIVO_HEIGHT - endHeight) / 2 - if topPadding % 2: - topPadding -= 1 - newpad = pad_check() - if newpad: - return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), '-vf', - 'pad=%d:%d:0:%d' % (TIVO_WIDTH, TIVO_HEIGHT, topPadding)] - else: - bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding - return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), - '-padtop', str(topPadding), - '-padbottom', str(bottomPadding)] - else: # if only very small amount of padding needed, then - # just stretch it - return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] - -def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo): - endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) / - (vInfo['vHeight'] * multiplier)) - if endWidth % 2: - endWidth -= 1 - if endWidth < TIVO_WIDTH * 0.99: - leftPadding = (TIVO_WIDTH - endWidth) / 2 - if leftPadding % 2: - leftPadding -= 1 - newpad = pad_check() - if newpad: - return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), '-vf', - 'pad=%d:%d:%d:0' % (TIVO_WIDTH, TIVO_HEIGHT, leftPadding)] - else: - rightPadding = (TIVO_WIDTH - endWidth) - leftPadding - return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), - '-padleft', str(leftPadding), - '-padright', str(rightPadding)] - else: # if only very small amount of padding needed, then - # just stretch it - return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] - -def select_aspect(inFile, tsn = ''): - TIVO_WIDTH = config.getTivoWidth(tsn) - TIVO_HEIGHT = config.getTivoHeight(tsn) - - vInfo = video_info(inFile) - - debug('tsn: %s' % tsn) - - aspect169 = config.get169Setting(tsn) - - debug('aspect169: %s' % aspect169) - - optres = config.getOptres(tsn) - - debug('optres: %s' % optres) - - if optres: - optHeight = config.nearestTivoHeight(vInfo['vHeight']) - optWidth = config.nearestTivoWidth(vInfo['vWidth']) - if optHeight < TIVO_HEIGHT: - TIVO_HEIGHT = optHeight - if optWidth < TIVO_WIDTH: - TIVO_WIDTH = optWidth - - if vInfo.get('par2'): - par2 = vInfo['par2'] - elif vInfo.get('par'): - par2 = float(vInfo['par']) - else: - # Assume PAR = 1.0 - par2 = 1.0 - - debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' + - 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'], - vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'], - vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH)) - - if config.isHDtivo(tsn) and not optres: - if config.getPixelAR(0) or vInfo['par']: - if vInfo['par2'] == None: - if vInfo['par']: - npar = par2 - else: - npar = config.getPixelAR(1) - else: - npar = par2 - - # adjust for pixel aspect ratio, if set - - if npar < 1.0: - return ['-s', '%dx%d' % (vInfo['vWidth'], - math.ceil(vInfo['vHeight'] / npar))] - elif npar > 1.0: - # FFMPEG expects width to be a multiple of two - return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2, - vInfo['vHeight'])] - - if vInfo['vHeight'] <= TIVO_HEIGHT: - # pass all resolutions to S3, except heights greater than - # conf height - return [] - # else, resize video. - - d = gcd(vInfo['vHeight'], vInfo['vWidth']) - rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d - debug('rheight=%s rwidth=%s' % (rheight, rwidth)) - - if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9': - debug('File + PAR is within 4:3.') - return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] - - elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), - (59, 72), (59, 36), (59, 54)] or - vInfo['dar1'] == '4:3'): - debug('File is within 4:3 list.') - return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] - - elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), - (59, 27)] or vInfo['dar1'] == '16:9') - and (aspect169 or config.get169Letterbox(tsn))): - debug('File is within 16:9 list and 16:9 allowed.') - - if config.get169Blacklist(tsn) or (aspect169 and - config.get169Letterbox(tsn)): - aspect = '4:3' - else: - aspect = '16:9' - return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)] - - else: - settings = ['-aspect'] - - multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2 - multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2 - ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight'] - debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio, - multiplier4by3)) - - # If video is wider than 4:3 add top and bottom padding - - if ratio > 133: # Might be 16:9 file, or just need padding on - # top and bottom - - if aspect169 and ratio > 135: # If file would fall in 4:3 - # assume it is supposed to be 4:3 - - if (config.get169Blacklist(tsn) or - config.get169Letterbox(tsn)): - settings.append('4:3') - else: - settings.append('16:9') - - if ratio > 177: # too short needs padding top and bottom - settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT, - multiplier16by9, vInfo) - debug(('16:9 aspect allowed, file is wider ' + - 'than 16:9 padding top and bottom\n%s') % - ' '.join(settings)) - - else: # too skinny needs padding on left and right. - settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, - multiplier16by9, vInfo) - debug(('16:9 aspect allowed, file is narrower ' + - 'than 16:9 padding left and right\n%s') % - ' '.join(settings)) - - else: # this is a 4:3 file or 16:9 output not allowed - if ratio > 135 and config.get169Letterbox(tsn): - settings.append('16:9') - multiplier = multiplier16by9 - else: - settings.append('4:3') - multiplier = multiplier4by3 - settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT, - multiplier, vInfo) - debug(('File is wider than 4:3 padding ' + - 'top and bottom\n%s') % ' '.join(settings)) - - # If video is taller than 4:3 add left and right padding, this - # is rare. All of these files will always be sent in an aspect - # ratio of 4:3 since they are so narrow. - - else: - settings.append('4:3') - settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo) - debug('File is taller than 4:3 padding left and right\n%s' - % ' '.join(settings)) - - return settings - -def tivo_compatible_video(vInfo, tsn, mime=''): - message = (True, '') - while True: - codec = vInfo.get('vCodec', '') - if mime == 'video/mp4': - if codec != 'h264': - message = (False, 'vCodec %s not compatible' % codec) - - break - - if mime == 'video/bif': - if codec != 'vc1': - message = (False, 'vCodec %s not compatible' % codec) - - break - - if codec not in ('mpeg2video', 'mpeg1video'): - message = (False, 'vCodec %s not compatible' % codec) - break - - if vInfo['kbps'] != None: - abit = max('0', vInfo['aKbps']) - if (int(vInfo['kbps']) - int(abit) > - config.strtod(config.getMaxVideoBR(tsn)) / 1000): - message = (False, '%s kbps exceeds max video bitrate' % - vInfo['kbps']) - break - else: - message = (False, '%s kbps not supported' % vInfo['kbps']) - break - - if config.isHDtivo(tsn): - if vInfo['par2'] != 1.0: - if config.getPixelAR(0): - if vInfo['par2'] != None or config.getPixelAR(1) != 1.0: - message = (False, '%s not correct PAR' % vInfo['par2']) - break - # HD Tivo detected, skipping remaining tests. - break - - if not vInfo['vFps'] in ['29.97', '59.94']: - message = (False, '%s vFps, should be 29.97' % vInfo['vFps']) - break - - if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn)) - or (config.get169Letterbox(tsn) and config.get169Setting(tsn))): - if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'): - message = (False, ('DAR %s not supported ' + - 'by BLACKLIST_169 tivos') % vInfo['dar1']) - break - - mode = (vInfo['vWidth'], vInfo['vHeight']) - if mode not in [(720, 480), (704, 480), (544, 480), - (528, 480), (480, 480), (352, 480), (352, 240)]: - message = (False, '%s x %s not in supported modes' % mode) - break - - return message - -def tivo_compatible_audio(vInfo, inFile, tsn, mime=''): - message = (True, '') - while True: - codec = vInfo.get('aCodec', '') - - if codec == None: - debug('No audio stream detected') - break - - if mime == 'video/mp4': - if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac', - 'ac3', 'liba52'): - message = (False, 'aCodec %s not compatible' % codec) - break - if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2): - message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh'])) - break - - audio_lang = config.get_tsn('audio_lang', tsn) - if audio_lang: - if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]: - message = (False, '%s preferred audio track exists' % - audio_lang) - break - - if mime == 'video/bif': - if codec != 'wmav2': - message = (False, 'aCodec %s not compatible' % codec) - - break - - if inFile[-5:].lower() == '.tivo': - break - - if mime == 'video/x-tivo-mpeg-ts' and codec not in ('ac3', 'liba52'): - message = (False, 'aCodec %s not compatible' % codec) - break - - if codec not in ('ac3', 'liba52', 'mp2'): - message = (False, 'aCodec %s not compatible' % codec) - break - - if (not vInfo['aKbps'] or - int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)): - message = (False, '%s kbps exceeds max audio bitrate' % - vInfo['aKbps']) - break - - audio_lang = config.get_tsn('audio_lang', tsn) - if audio_lang: - if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]: - message = (False, '%s preferred audio track exists' % - audio_lang) - break - - return message - -def tivo_compatible_container(vInfo, inFile, mime=''): - message = (True, '') - container = vInfo.get('container', '') - if ((mime == 'video/mp4' and - (container != 'mov' or inFile.lower().endswith('.mov'))) or - (mime == 'video/bif' and container != 'asf') or - (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or - (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and - (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))): - message = (False, 'container %s not compatible' % container) - - return message - -def mp4_remuxable(inFile, tsn=''): - vInfo = video_info(inFile) - return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0] - -def mp4_remux(inFile, basename, tsn='', temp_share_path=''): - outFile = inFile + '.pyTivo-temp' - newname = basename + '.pyTivo-temp' - - if temp_share_path: - newname = os.path.splitext(os.path.split(basename)[1])[0] + '.mp4.pyTivo-temp' - outFile = os.path.join(temp_share_path, newname) - - if os.path.exists(outFile): - return None # ugh! - - ffmpeg_path = config.get_bin('ffmpeg') - fname = unicode(inFile, 'utf-8') - oname = unicode(outFile, 'utf-8') - if mswindows: - fname = fname.encode('iso8859-1') - oname = oname.encode('iso8859-1') - - settings = {'video_codec': '-vcodec copy', - 'video_br': select_videobr(inFile, tsn), - 'video_fps': select_videofps(inFile, tsn), - 'max_video_br': select_maxvideobr(tsn), - 'buff_size': select_buffsize(tsn), - 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)), - 'audio_br': select_audiobr(tsn), - 'audio_fr': select_audiofr(inFile, tsn), - 'audio_ch': select_audioch(inFile, tsn), - 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'), - 'audio_lang': select_audiolang(inFile, tsn), - 'ffmpeg_pram': select_ffmpegprams(tsn), - 'format': '-f mp4'} - - cmd_string = config.getFFmpegTemplate(tsn) % settings - cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname] - - debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:') - debug(' '.join(cmd)) - - ffmpeg = subprocess.Popen(cmd) - debug('remuxing ' + inFile + ' to ' + outFile) - if ffmpeg.wait(): - debug('error during remuxing') - os.remove(outFile) - return None - - return newname - -def tivo_compatible(inFile, tsn='', mime=''): - vInfo = video_info(inFile) - - message = (True, 'all compatible') - if not config.get_bin('ffmpeg'): - if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']: - message = (False, 'no ffmpeg') - return message - - while True: - vmessage = tivo_compatible_video(vInfo, tsn, mime) - if not vmessage[0]: - message = vmessage - break - - amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime) - if not amessage[0]: - message = amessage - break - - cmessage = tivo_compatible_container(vInfo, inFile, mime) - if not cmessage[0]: - message = cmessage - break - - dmessage = (False, 'All DVD Video must be re-encapsulated') - if vobstream.is_dvd(inFile): - message = dmessage - - break - - debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]], - message[1], inFile)) - return message - -def video_info(inFile, cache=True): - vInfo = dict() - fname = unicode(inFile, 'utf-8') - #mtime = os.stat(fname).st_mtime - if vobstream.is_dvd(inFile): - is_dvd = True - mtime = os.stat(os.path.dirname(fname)).st_mtime - else: - is_dvd = False - mtime = os.stat(fname).st_mtime - - if cache: - if inFile in info_cache and info_cache[inFile][0] == mtime: - debug('CACHE HIT! %s' % inFile) - return info_cache[inFile][1] - - vInfo['Supported'] = True - - ffmpeg_path = config.get_bin('ffmpeg') - if not ffmpeg_path: - if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg', - '.vob', '.tivo']: - vInfo['Supported'] = False - vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480, - 'rawmeta': {}}) - if cache: - info_cache[inFile] = (mtime, vInfo) - return vInfo - - if mswindows: - fname = fname.encode('iso8859-1') - #cmd = [ffmpeg_path, '-i', fname] - if is_dvd: - cmd = [ffmpeg_path, '-i', '-'] - else: - cmd = [ffmpeg_path, '-i', fname] - debug('cmd: %s' % cmd) - # Windows and other OS buffer 4096 and ffmpeg can output more than that. - err_tmp = tempfile.TemporaryFile() - ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE, - stdin=subprocess.PIPE) - if is_dvd: - vobstream.vobstream(True, inFile, ffmpeg, BLOCKSIZE) - - # wait configured # of seconds: if ffmpeg is not back give up - wait = config.getFFmpegWait() - debug('starting ffmpeg, will wait %s seconds for it to complete' % wait) - for i in xrange(wait * 20): - time.sleep(.05) - if not ffmpeg.poll() == None: - break - - if ffmpeg.poll() == None: - kill(ffmpeg) - vInfo['Supported'] = False - if cache: - info_cache[inFile] = (mtime, vInfo) - return vInfo - - err_tmp.seek(0) - output = err_tmp.read() - err_tmp.close() - debug('ffmpeg output=%s' % output) - - attrs = {'container': r'Input #0, ([^,]+),', - 'vCodec': r'Video: ([^, ]+)', # video codec - 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate - 'aCodec': r'.*Audio: ([^, ]+)', # audio codec - 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency - 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping - - for attr in attrs: - rezre = re.compile(attrs[attr]) - x = rezre.search(output) - if x: - vInfo[attr] = x.group(1) - else: - if attr in ['container', 'vCodec']: - vInfo[attr] = '' - vInfo['Supported'] = False - else: - vInfo[attr] = None - debug('failed at ' + attr) - - rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*') - x = rezre.search(output) - if x: - if x.group(3): - if x.group(3) == 'stereo': - vInfo['aCh'] = 2 - elif x.group(3) == 'mono': - vInfo['aCh'] = 1 - elif x.group(2): - vInfo['aCh'] = int(x.group(1)) + int(x.group(2)) - elif x.group(1): - vInfo['aCh'] = int(x.group(1)) - else: - vInfo['aCh'] = None - debug('failed at aCh') - else: - vInfo['aCh'] = None - debug('failed at aCh') - - rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') - x = rezre.search(output) - if x: - vInfo['vWidth'] = int(x.group(1)) - vInfo['vHeight'] = int(x.group(2)) - else: - vInfo['vWidth'] = '' - vInfo['vHeight'] = '' - vInfo['Supported'] = False - debug('failed at vWidth/vHeight') - - rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*') - x = rezre.search(output) - if x: - vInfo['vFps'] = x.group(1) - if '.' not in vInfo['vFps']: - vInfo['vFps'] += '.00' - - # Allow override only if it is mpeg2 and frame rate was doubled - # to 59.94 - - if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97': - # First look for the build 7215 version - rezre = re.compile(r'.*film source: 29.97.*') - x = rezre.search(output.lower()) - if x: - debug('film source: 29.97 setting vFps to 29.97') - vInfo['vFps'] = '29.97' - else: - # for build 8047: - rezre = re.compile(r'.*frame rate differs from container ' + - r'frame rate: 29.97.*') - debug('Bug in VideoReDo') - x = rezre.search(output.lower()) - if x: - vInfo['vFps'] = '29.97' - else: - vInfo['vFps'] = '' - vInfo['Supported'] = False - debug('failed at vFps') - - durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),') - d = durre.search(output) - - if d: - vInfo['millisecs'] = ((int(d.group(1)) * 3600 + - int(d.group(2)) * 60 + - int(d.group(3))) * 1000 + - int(d.group(4)) * (10 ** (3 - len(d.group(4))))) - else: - vInfo['millisecs'] = 0 - - if is_dvd: - vInfo['millisecs'] = vobstream.duration(inFile) - - # get bitrate of source for tivo compatibility test. - rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*') - x = rezre.search(output) - if x: - vInfo['kbps'] = x.group(1) - else: - # Fallback method of getting video bitrate - # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p, - # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r) - rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' + - r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*') - x = rezre.search(output) - if x: - vInfo['kbps'] = x.group(1) - else: - vInfo['kbps'] = None - debug('failed at kbps') - - # get par. - rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*') - x = rezre.search(output) - if x and x.group(1) != "0" and x.group(2) != "0": - vInfo['par1'] = x.group(1) + ':' + x.group(2) - vInfo['par2'] = float(x.group(1)) / float(x.group(2)) - else: - vInfo['par1'], vInfo['par2'] = None, None - - # get dar. - rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*') - x = rezre.search(output) - if x and x.group(1) != "0" and x.group(2) != "0": - vInfo['dar1'] = x.group(1) + ':' + x.group(2) - else: - vInfo['dar1'] = None - - # get Audio Stream mapping. - rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)') - x = rezre.search(output) - amap = [] - if x: - for x in rezre.finditer(output): - amap.append((x.group(1), x.group(2) + x.group(3))) - else: - amap.append(('', '')) - debug('failed at mapAudio') - vInfo['mapAudio'] = amap - - vInfo['par'] = None - - # get Metadata dump (newer ffmpeg). - lines = output.split('\n') - rawmeta = {} - flag = False - - for line in lines: - if line.startswith(' Metadata:'): - flag = True - else: - if flag: - if line.startswith(' Duration:'): - flag = False - else: - try: - key, value = [x.strip() for x in line.split(':', 1)] - try: - value = value.decode('utf-8') - except: - if sys.platform == 'darwin': - value = value.decode('macroman') - else: - value = value.decode('iso8859-1') - rawmeta[key] = [value] - except: - pass - - vInfo['rawmeta'] = rawmeta - - data = metadata.from_text(inFile) - for key in data: - if key.startswith('Override_'): - vInfo['Supported'] = True - if key.startswith('Override_mapAudio'): - audiomap = dict(vInfo['mapAudio']) - stream = key.replace('Override_mapAudio', '').strip() - if stream in audiomap: - newaudiomap = (stream, data[key]) - audiomap.update([newaudiomap]) - vInfo['mapAudio'] = sorted(audiomap.items(), - key=lambda (k,v): (k,v)) - elif key.startswith('Override_millisecs'): - vInfo[key.replace('Override_', '')] = int(data[key]) - else: - vInfo[key.replace('Override_', '')] = data[key] - - if cache: - info_cache[inFile] = (mtime, vInfo) - debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()])) - return vInfo - -def audio_check(inFile, tsn): - cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' + - select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -') - fname = unicode(inFile, 'utf-8') - if mswindows: - fname = fname.encode('iso8859-1') - cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split() - ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE) - fd, testname = tempfile.mkstemp() - testfile = os.fdopen(fd, 'wb') - try: - shutil.copyfileobj(ffmpeg.stdout, testfile) - except: - kill(ffmpeg) - testfile.close() - vInfo = None - else: - testfile.close() - vInfo = video_info(testname, False) - os.remove(testname) - return vInfo - -def supported_format(inFile): - if video_info(inFile)['Supported']: - return True - else: - debug('FALSE, file not supported %s' % inFile) - return False - -def kill(popen): - debug('killing pid=%s' % str(popen.pid)) - if mswindows: - win32kill(popen.pid) - else: - import os, signal - for i in xrange(3): - debug('sending SIGTERM to pid: %s' % popen.pid) - os.kill(popen.pid, signal.SIGTERM) - time.sleep(.5) - if popen.poll() is not None: - debug('process %s has exited' % popen.pid) - break - else: - while popen.poll() is None: - debug('sending SIGKILL to pid: %s' % popen.pid) - os.kill(popen.pid, signal.SIGKILL) - time.sleep(.5) - -def win32kill(pid): - import ctypes - handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) - -def gcd(a, b): - while b: - a, b = b, a % b - return a - -def dvd_size( full_path ): - return vobstream.size( full_path ) - -def is_dvd( full_path ): - return vobstream.is_dvd( full_path) diff --git a/plugins/dvdvideo/virtualdvd.py b/plugins/dvdvideo/virtualdvd.py deleted file mode 100644 index ded03ab0..00000000 --- a/plugins/dvdvideo/virtualdvd.py +++ /dev/null @@ -1,216 +0,0 @@ -# Module: virtualdvd.py -# Author: Eric von Bayer -# Updated By: Luke Broadbent -# Contact: -# Date: June 25, 2011 -# Description: -# Model a DVD as a dynamic directory structure of mpegs. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os -import re -import time -import dvdfolder - -import metadata - -# Use the LRU Cache if it is available -try: - from lrucache import LRUCache - VDVD_Cache = LRUCache(20) -except: - VDVD_Cache = dict() - -# Patterns to match against virtual files -PATTERN_VDVD_FILES = re.compile( "(?i)__T(-?[0-9]+).mpg" ) -FORMAT_VDVD_FILES = "__T%02d.mpg" - -################################# VirtualDVD ################################### - -class VirtualDVD(object): - class FileData(object): - def __init__(self, vdvd, path, num, title ): - self.name = os.path.join( path, FORMAT_VDVD_FILES % num ).encode('utf-8') - self.isdir = False - st = os.stat( path ) - #self.mdate = int(st.st_mtime) - self.mdate = int(time.time()) - self.title = vdvd.TitleName(num) - if( num >= 0 ): - self.size = title.Size() - else: - self.size = 0 - - def __init__( self, path, title_threshold = 0 ): - - self.valid = False - self.TITLE_LENGTH_THRESHOLD = title_threshold - self.dvd_folder = None - path = unicode(path, 'utf-8') - - if os.path.isdir( path ): - self.path = path - self.file = "" - self.file_id = -1 - else: - self.path = os.path.dirname( path ) - self.file = os.path.basename( path ) - self.file_id = -1 - - try: - if self.path in VDVD_Cache: - self.dvd_folder = VDVD_Cache[self.path] - self.valid = self.dvd_folder.QuickValid() - else: - self.dvd_folder = dvdfolder.DVDFolder( self.path, defer=True ) - self.valid = self.dvd_folder.QuickValid() - if self.valid: - VDVD_Cache[self.path] = self.dvd_folder - - except dvdfolder.DVDNotDVD: - pass - except dvdfolder.DVDFormatError, err: - print "Warning while reading DVD %s: %s" % ( self.path, err ) - - if not os.path.isdir( path ) and self.valid: - m = PATTERN_VDVD_FILES.match(self.file) - if m != None: - self.file_id = int(m.group(1)) - - def Path( self ): - return self.path - - def HasErrors( self ): - return self.dvd_folder and self.dvd_folder.HasErrors() - - def Valid( self ): - try: - if self.valid: - return self.dvd_folder.Valid() - except dvdfolder.DVDNotDVD: - pass - except dvdfolder.DVDFormatError, err: - print "Warning while reading DVD %s: %s" % ( self.path, err ) - - return False - - def QuickValid( self ): - if self.valid: - return self.dvd_folder.QuickValid() - return False - - def TitleNumber( self ): - return self.file_id - - def TitleName( self, num = -1 ): - if num == -1: - num = self.file_id - - if num == 0: - return "Main Feature" - elif num > 0: - if num <= len(self.dvd_folder.TitleList()): - return "Title " + str(num) + " (" + \ - str(self.dvd_folder.TitleList()[num-1].Time()) + ")" - return "" - elif num == -99: - return self.dvd_folder.Error() - else: - return "" - - def IDToTitle( self, id ): - if ( not self.Valid() ) or ( id < 0 ) or \ - ( id > len(self.dvd_folder.TitleList()) ): - return DVDTitle() - elif id == 0: - return self.dvd_folder.MainTitle() - else: - return self.dvd_folder.TitleList()[id-1] - - def FileTitle( self, file = None ): - if file == None: - return self.IDToTitle( self.file_id ) - else: - m = PATTERN_VDVD_FILES.match( file ) - if m != None: - return self.IDToTitle( int(m.group(1)) ) - - return self.IDToTitle( -1 ) - - def DVDTitleName( self ): - return os.path.basename( self.dvd_folder.Folder() ) - - def NumFiles( self ): - if self.Valid(): - return self.dvd_folder.NumUsefulTitles( self.TITLE_LENGTH_THRESHOLD ) - else: - return 0 - - def GetFiles( self ): - files = list() - if self.Valid() and len( self.dvd_folder.TitleList() ) > 0: - #if the title 0 has a name in the metadata files count it if it does not startwith ignore - data = {} - try: - data.update( metadata.from_text( os.path.join( self.path, FORMAT_VDVD_FILES % 0 ).encode('utf-8') ) ) - except: - pass - - if 'episodeTitle' in data: - pass - elif 'Title 0' in data: - data['episodeTitle'] = data['Title 0'] - else: - data['episodeTitle'] = "" - - if not data['episodeTitle'].lower().startswith('ignore'): - files.append( self.FileData( self, self.path, 0, \ - self.dvd_folder.MainTitle() ) ) - - for title in self.dvd_folder.TitleList(): - if title.Time().Secs() > self.TITLE_LENGTH_THRESHOLD: - #if the title has a name in the metadata files count it if it does not startwith ignore - data = {} - try: - data.update( metadata.from_text( os.path.join( self.path, FORMAT_VDVD_FILES % title.TitleNumber() ).encode('utf-8') ) ) - except: - pass - - if 'episodeTitle' in data: - pass - elif 'Title '+ str(title.TitleNumber()) in data: - data['episodeTitle'] = data['Title ' + str(title.TitleNumber())] - else: - data['episodeTitle'] = "" - - if not data['episodeTitle'].lower().startswith('ignore'): - files.append( self.FileData( self, self.path, \ - title.TitleNumber(), title ) ) - elif self.dvd_folder.HasErrors() != None: - files.append( self.FileData( self, self.path, -99, None ) ) - return files \ No newline at end of file diff --git a/plugins/dvdvideo/vobstream.py b/plugins/dvdvideo/vobstream.py deleted file mode 100644 index ee50f849..00000000 --- a/plugins/dvdvideo/vobstream.py +++ /dev/null @@ -1,303 +0,0 @@ -# Module: dvdfolder.py -# Author: Eric von Bayer -# Updated By: Luke Broadbent -# Contact: -# Date: June 15, 2011 -# Description: -# Routines for reading DVD vob files and streaming them to the TiVo. -# -# Copyright (c) 2009, Eric von Bayer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The names of the contributors may not be used to endorse or promote -# products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from threading import Thread -from dvdtitlestream import DVDTitleStream - -import logging -import math -import os -import re -import shutil -import subprocess -import sys -import tempfile -import threading -import time - -import lrucache - -import config -import metadata -import virtualdvd - -def WriteStreamToSubprocess( fhin, sub, event, blocksize ): - if event == None: - event = 1 - # Write all the data till either end is closed or done - while not event.isSet(): - - # Read in the block and escape if we got nothing - data = fhin.read( blocksize ) - if len(data) == 0: - break - - if sub.poll() != None and sub.stdin != None: - break - - # Write the data and flush it - try: - sub.stdin.write( data ) - sub.stdin.flush() - except IOError: - break - - # We got less data so we must be at the end - if len(data) < blocksize: - break - - # Close the input if it's not already closed - if not fhin.closed: - fhin.close() - - # Close the output if it's not already closed - if sub.stdin != None and not sub.stdin.closed: - sub.stdin.close() - -def vobstream(isQuery, inFile, ffmpeg, blocksize): - dvd = virtualdvd.VirtualDVD( inFile ) - if not dvd.Valid() or dvd.file_id == -1: - debug('Not a valid dvd file') - return 0 - - title = dvd.FileTitle() - ts = DVDTitleStream( title.Stream() ) - - # Make an event to shutdown the thread - sde = threading.Event() - sde.clear() - - # Stream data to the subprocess - t = Thread( target=WriteStreamToSubprocess, args=(ts, ffmpeg, sde, blocksize) ) - t.start() - - if isQuery: - # Shutdown the helper threads/processes - ffmpeg.wait() - sde.set() - t.join() - - # Close the title stream - ts.close() - else: - proc = {'stream': ts, 'thread': t, 'event':sde} - return proc - -def is_dvd(inFile): - dvd = virtualdvd.VirtualDVD( inFile ) - return dvd.Valid() or dvd.file_id != -1 - -def size(inFile): - try: - dvd = virtualdvd.VirtualDVD( inFile ) - return dvd.FileTitle().Size() - except: - return 0 - -def duration(inFile): - dvd = virtualdvd.VirtualDVD( inFile ) - title = dvd.FileTitle() - return title.Time().MSecs() - -def tivo_compatible(inFile, tsn='', mime=''): - message = (False, 'All DVD Video must be re-encapsulated') - debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]], - message[1], inFile)) - return message - -def video_info(inFile, audio_spec = "", cache=True): - vInfo = dict() - fname = unicode(os.path.dirname(inFile), 'utf-8') - mtime = os.stat(fname).st_mtime - if cache: - if inFile in info_cache and info_cache[inFile][0] == mtime: - debug('CACHE HIT! %s' % inFile) - return info_cache[inFile][1] - - dvd = virtualdvd.VirtualDVD( inFile ) - if not dvd.Valid() or dvd.file_id == -1: - debug('Not a valid dvd file') - return dict() - - ffmpeg_path = config.get_bin('ffmpeg') - - title = dvd.FileTitle() - sid = title.FindBestAudioStreamID( audio_spec ) - ts = DVDTitleStream( title.Stream() ) - ts.seek(0) - - cmd = [ffmpeg_path, '-i', '-'] - # Windows and other OS buffer 4096 and ffmpeg can output more than that. - err_tmp = tempfile.TemporaryFile() - ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE, - stdin=subprocess.PIPE) - - # Write all the data till either end is closed or done - while 1: - # Read in the block and escape if we got nothing - data = ts.read(BLOCKSIZE) - if len(data) == 0: - break - - if ffmpeg.poll() != None and sub.stdin != None: - break - - try: - ffmpeg.stdin.write(data) - ffmpeg.stdin.flush() - except IOError: - break - - # We got less data so we must be at the end - if len(data) < BLOCKSIZE: - break - - # wait configured # of seconds: if ffmpeg is not back give up - wait = config.getFFmpegWait() - debug('starting ffmpeg, will wait %s seconds for it to complete' % wait) - for i in xrange(wait * 20): - time.sleep(.05) - if not ffmpeg.poll() == None: - break - - if ffmpeg.poll() == None: - kill(ffmpeg) - vInfo['Supported'] = False - if cache: - info_cache[inFile] = (mtime, vInfo) - return vInfo - - err_tmp.seek(0) - output = err_tmp.read() - err_tmp.close() - debug('ffmpeg output=%s' % output) - - # Close the input if it's not already closed - if not ts.closed: - ts.close() - - #print "VOB Info:", output - vInfo['mapAudio'] = '' - - attrs = {'container': r'Input #0, ([^,]+),', - 'vCodec': r'Video: ([^, ]+)', # video codec - 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate - 'aCodec': r'.*Audio: ([^,]+),.*', # audio codec - 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency - 'mapVideo': r'([0-9]+\.[0-9]+).*: Video:.*', # video mapping - 'mapAudio': r'([0-9]+\.[0-9]+)\[0x%02x\]: Audio:.*' % sid } # Audio mapping - - for attr in attrs: - rezre = re.compile(attrs[attr]) - x = rezre.search(output) - if x: - vInfo[attr] = x.group(1) - else: - if attr in ['container', 'vCodec']: - vInfo[attr] = '' - vInfo['Supported'] = False - else: - vInfo[attr] = None - debug('failed at ' + attr) - - # Get the Pixel Aspect Ratio - rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*') - x = rezre.search(output) - if x and x.group(1) != "0" and x.group(2) != "0": - vInfo['par1'] = x.group(1) + ':' + x.group(2) - vInfo['par2'] = float(x.group(1)) / float(x.group(2)) - else: - vInfo['par1'], vInfo['par2'] = None, None - - # Get the Display Aspect Ratio - rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*') - x = rezre.search(output) - if x and x.group(1) != "0" and x.group(2) != "0": - vInfo['dar1'] = x.group(1) + ':' + x.group(2) - else: - vInfo['dar1'] = None - - # Get the video dimensions - rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') - x = rezre.search(output) - if x: - vInfo['vWidth'] = int(x.group(1)) - vInfo['vHeight'] = int(x.group(2)) - else: - vInfo['vWidth'] = '' - vInfo['vHeight'] = '' - vInfo['Supported'] = False - debug('failed at vWidth/vHeight') - - vInfo['millisecs'] = title.Time().MSecs() - vInfo['Supported'] = True - - if cache: - info_cache[inFile] = (mtime, vInfo) - debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()])) - return vInfo - -def supported_format(inFile): - dvd = virtualdvd.VirtualDVD( inFile ) - return dvd.Valid() and dvd.file_id != -1 - if video_info(inFile)['Supported']: - return True - else: - debug('FALSE, file not supported %s' % inFile) - return False - -def kill(popen): - debug('killing pid=%s' % str(popen.pid)) - if mswindows: - win32kill(popen.pid) - else: - import os, signal - for i in xrange(3): - debug('sending SIGTERM to pid: %s' % popen.pid) - os.kill(popen.pid, signal.SIGTERM) - time.sleep(.5) - if popen.poll() is not None: - debug('process %s has exited' % popen.pid) - break - else: - while popen.poll() is None: - debug('sending SIGKILL to pid: %s' % popen.pid) - os.kill(popen.pid, signal.SIGKILL) - time.sleep(.5) - -def win32kill(pid): - import ctypes - handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) diff --git a/plugins/music/music.py b/plugins/music/music.py index ac2ae7dc..b8aa668f 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -112,7 +112,7 @@ def send_file(self, handler, path, query): if mswindows: fname = fname.encode('iso8859-1') - cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd = [config.get_bin('ffmpeg'), '-i', fname, '-vn'] if ext in ['.mp3', '.mp2']: cmd += ['-acodec', 'copy'] else: diff --git a/plugins/photo/photo.py b/plugins/photo/photo.py index 1afab5ef..d0db7223 100644 --- a/plugins/photo/photo.py +++ b/plugins/photo/photo.py @@ -24,11 +24,12 @@ # Version 0.2, Dec. 8 -- thumbnail caching, faster thumbnails # Version 0.1, Dec. 7, 2007 -import cgi import os import re import random +import subprocess import sys +import tempfile import threading import time import unicodedata @@ -36,17 +37,21 @@ from cStringIO import StringIO from xml.sax.saxutils import escape +use_pil = True try: from PIL import Image except ImportError: try: import Image except ImportError: - print 'Photo Plugin Error: The Python Imaging Library is not installed' + use_pil = False + print 'Python Imaging Library not found; using FFmpeg' +import config from Cheetah.Template import Template from lrucache import LRUCache from plugin import EncodeUnicode, Plugin, quote, unquote +from plugins.video.transcode import kill SCRIPTDIR = os.path.dirname(__file__) @@ -61,12 +66,17 @@ exif_orient_m = \ re.compile('\x01\x12\x00\x03\x00\x00\x00\x01\x00(.)\x00\x00').search +# Find size in FFmpeg output +ffmpeg_size = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*') + # Preload the template tname = os.path.join(SCRIPTDIR, 'templates', 'container.tmpl') iname = os.path.join(SCRIPTDIR, 'templates', 'item.tmpl') PHOTO_TEMPLATE = file(tname, 'rb').read() ITEM_TEMPLATE = file(iname, 'rb').read() +JFIF_TAG = '\xff\xe0\x00\x10JFIF\x00\x01\x02\x00\x00\x01\x00\x01\x00\x00' + class Photo(Plugin): CONTENT_TYPE = 'x-container/tivo-photos' @@ -102,166 +112,280 @@ def __getitem__(self, key): recurse_cache = LockedLRUCache(5) # recursive directory lists dir_cache = LockedLRUCache(10) # non-recursive lists - def send_file(self, handler, path, query): + def new_size(self, oldw, oldh, width, height, pshape): + pixw, pixh = [int(x) for x in pshape.split(':')] - def send_jpeg(data): - handler.send_fixed(data, 'image/jpeg') + if not width: width = oldw + if not height: height = oldh - if 'Format' in query and query['Format'][0] != 'image/jpeg': - handler.send_error(415) - return + oldw *= pixh + oldh *= pixw - try: - attrs = self.media_data_cache[path] - except: - attrs = None + ratio = float(oldw) / oldh - # Set rotation - if attrs: - rot = attrs['rotation'] + if float(width) / height < ratio: + height = int(width / ratio) else: - rot = 0 - - if 'Rotation' in query: - rot = (rot - int(query['Rotation'][0])) % 360 - if attrs: - attrs['rotation'] = rot - if 'thumb' in attrs: - del attrs['thumb'] - - # Requested size - width = int(query.get('Width', ['0'])[0]) - height = int(query.get('Height', ['0'])[0]) - - # Return saved thumbnail? - if attrs and 'thumb' in attrs and 0 < width < 100 and 0 < height < 100: - send_jpeg(attrs['thumb']) - return + width = int(height * ratio) + return width, height + + def parse_exif(self, exif, rot, attrs): + # Capture date + if attrs and not 'odate' in attrs: + date = exif_date(exif) + if date: + year, month, day, hour, minute, second = (int(x) + for x in date.groups()) + if year: + odate = time.mktime((year, month, day, hour, + minute, second, -1, -1, -1)) + attrs['odate'] = '%#x' % int(odate) + + # Orientation + if attrs and 'exifrot' in attrs: + rot = (rot + attrs['exifrot']) % 360 + else: + if exif[6] == 'I': + orient = exif_orient_i(exif) + else: + orient = exif_orient_m(exif) + + if orient: + exifrot = { + 1: 0, + 2: 0, + 3: 180, + 4: 180, + 5: 90, + 6: -90, + 7: -90, + 8: 90}.get(ord(orient.group(1)), 0) + + rot = (rot + exifrot) % 360 + if attrs: + attrs['exifrot'] = exifrot + + return rot + + def get_image_pil(self, path, width, height, pshape, rot, attrs): # Load try: pic = Image.open(unicode(path, 'utf-8')) except Exception, msg: - handler.server.logger.error('Could not open %s -- %s' % - (path, msg)) - handler.send_error(404) - return + return False, 'Could not open %s -- %s' % (path, msg) # Set draft mode try: pic.draft('RGB', (width, height)) except Exception, msg: - handler.server.logger.error('Failed to set draft mode ' + - 'for %s -- %s' % (path, msg)) - handler.send_error(404) - return + return False, 'Failed to set draft mode for %s -- %s' % (path, msg) # Read Exif data if possible if 'exif' in pic.info: - exif = pic.info['exif'] - - # Capture date - if attrs and not 'odate' in attrs: - date = exif_date(exif) - if date: - year, month, day, hour, minute, second = (int(x) - for x in date.groups()) - if year: - odate = time.mktime((year, month, day, hour, - minute, second, -1, -1, -1)) - attrs['odate'] = '%#x' % int(odate) - - # Orientation - if attrs and 'exifrot' in attrs: - rot = (rot + attrs['exifrot']) % 360 - else: - if exif[6] == 'I': - orient = exif_orient_i(exif) - else: - orient = exif_orient_m(exif) - - if orient: - exifrot = { - 1: 0, - 2: 0, - 3: 180, - 4: 180, - 5: 90, - 6: -90, - 7: -90, - 8: 90}.get(ord(orient.group(1)), 0) - - rot = (rot + exifrot) % 360 - if attrs: - attrs['exifrot'] = exifrot + rot = self.parse_exif(pic.info['exif'], rot, attrs) # Rotate try: if rot: pic = pic.rotate(rot) except Exception, msg: - handler.server.logger.error('Rotate failed on %s -- %s' % - (path, msg)) - handler.send_error(404) - return + return False, 'Rotate failed on %s -- %s' % (path, msg) # De-palletize try: - if pic.mode == 'P': - pic = pic.convert() + if pic.mode not in ('RGB', 'L'): + pic = pic.convert('RGB') except Exception, msg: - handler.server.logger.error('Palette conversion failed ' + - 'on %s -- %s' % (path, msg)) - handler.send_error(404) - return + return False, 'Palette conversion failed on %s -- %s' % (path, msg) # Old size oldw, oldh = pic.size - if not width: width = oldw - if not height: height = oldh - - # Correct aspect ratio - if 'PixelShape' in query: - pixw, pixh = query['PixelShape'][0].split(':') - oldw *= int(pixh) - oldh *= int(pixw) - - # Resize - ratio = float(oldw) / oldh - - if float(width) / height < ratio: - height = int(width / ratio) - else: - width = int(height * ratio) + width, height = self.new_size(oldw, oldh, width, height, pshape) try: pic = pic.resize((width, height), Image.ANTIALIAS) except Exception, msg: - handler.server.logger.error('Resize failed on %s -- %s' % - (path, msg)) - handler.send_error(404) - return + return False, 'Resize failed on %s -- %s' % (path, msg) # Re-encode try: out = StringIO() - pic.save(out, 'JPEG') + pic.save(out, 'JPEG', quality=85) encoded = out.getvalue() out.close() except Exception, msg: - handler.server.logger.error('Encode failed on %s -- %s' % - (path, msg)) - handler.send_error(404) + return False, 'Encode failed on %s -- %s' % (path, msg) + + return True, encoded + + def get_size_ffmpeg(self, ffmpeg_path, fname): + cmd = [ffmpeg_path, '-i', fname] + # Windows and other OS buffer 4096 and ffmpeg can output more + # than that. + err_tmp = tempfile.TemporaryFile() + ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + + # wait configured # of seconds: if ffmpeg is not back give up + limit = config.getFFmpegWait() + if limit: + for i in xrange(limit * 20): + time.sleep(.05) + if not ffmpeg.poll() == None: + break + + if ffmpeg.poll() == None: + kill(ffmpeg) + return False, 'FFmpeg timed out' + else: + ffmpeg.wait() + + err_tmp.seek(0) + output = err_tmp.read() + err_tmp.close() + + x = ffmpeg_size.search(output) + if x: + width = int(x.group(1)) + height = int(x.group(2)) + else: + return False, "Couldn't parse size" + + return True, (width, height) + + def get_image_ffmpeg(self, path, width, height, pshape, rot, attrs): + ffmpeg_path = config.get_bin('ffmpeg') + if not ffmpeg_path: + return False, 'FFmpeg not found' + + fname = unicode(path, 'utf-8') + if sys.platform == 'win32': + fname = fname.encode('iso8859-1') + + if attrs and 'size' in attrs: + result = attrs['size'] + else: + status, result = self.get_size_ffmpeg(ffmpeg_path, fname) + if not status: + return False, result + if attrs: + attrs['size'] = result + + if rot in (90, 270): + oldh, oldw = result + else: + oldw, oldh = result + + width, height = self.new_size(oldw, oldh, width, height, pshape) + + if rot == 270: + filters = 'transpose=1,' + elif rot == 180: + filters = 'hflip,vflip,' + elif rot == 90: + filters = 'transpose=2,' + else: + filters = '' + + filters += 'format=yuvj420p,' + + neww, newh = oldw, oldh + while (neww / width >= 50) or (newh / height >= 50): + neww /= 2 + newh /= 2 + filters += 'scale=%d:%d,' % (neww, newh) + + filters += 'scale=%d:%d' % (width, height) + + cmd = [ffmpeg_path, '-i', fname, '-vf', filters, '-f', 'mjpeg', '-'] + jpeg_tmp = tempfile.TemporaryFile() + ffmpeg = subprocess.Popen(cmd, stdout=jpeg_tmp, + stdin=subprocess.PIPE) + + # wait configured # of seconds: if ffmpeg is not back give up + limit = config.getFFmpegWait() + if limit: + for i in xrange(limit * 20): + time.sleep(.05) + if not ffmpeg.poll() == None: + break + + if ffmpeg.poll() == None: + kill(ffmpeg) + return False, 'FFmpeg timed out' + else: + ffmpeg.wait() + + jpeg_tmp.seek(0) + output = jpeg_tmp.read() + jpeg_tmp.close() + + if 'JFIF' not in output[:10]: + output = output[:2] + JFIF_TAG + output[2:] + + return True, output + + def send_file(self, handler, path, query): + + def send_jpeg(data): + handler.send_fixed(data, 'image/jpeg') + + if 'Format' in query and query['Format'][0] != 'image/jpeg': + handler.send_error(415) return - # Save thumbnails - if attrs and width < 100 and height < 100: - attrs['thumb'] = encoded + try: + attrs = self.media_data_cache[path] + except: + attrs = None + + # Set rotation + if attrs: + rot = attrs['rotation'] + else: + rot = 0 + + if 'Rotation' in query: + rot = (rot - int(query['Rotation'][0])) % 360 + if attrs: + attrs['rotation'] = rot + if 'thumb' in attrs: + del attrs['thumb'] + + # Requested size + width = int(query.get('Width', ['0'])[0]) + height = int(query.get('Height', ['0'])[0]) + + # Return saved thumbnail? + if attrs and 'thumb' in attrs and 0 < width < 100 and 0 < height < 100: + send_jpeg(attrs['thumb']) + return + + # Requested pixel shape + pshape = query.get('PixelShape', ['1:1'])[0] + + # Build a new image + if use_pil: + status, result = self.get_image_pil(path, width, height, + pshape, rot, attrs) + else: + status, result = self.get_image_ffmpeg(path, width, height, + pshape, rot, attrs) + + if status: + # Save thumbnails + if attrs and width < 100 and height < 100: + attrs['thumb'] = result + + # Send it + send_jpeg(result) + else: + handler.server.logger.error(result) + handler.send_error(404) - # Send it - send_jpeg(encoded) - def QueryContainer(self, handler, query): # Reject a malformed request -- these attributes should only diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index 2504a10a..ee173d4f 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -177,43 +177,6 @@ alpha-sorted, you need this option. Example Settings: True/False Available In: Shares -precache - -Default Setting: False -Valid Entries: True/False -Required: No -Skill: Moderate -Description: In order to verify that the video files present on your -computer were compatible with ffmpeg in older versions pyTivo would -query ffmpeg for each file. While this information was cached it still -caused a delay in the initial loading of a list of files. This precache -setting forced pyTivo to inspect each video prior to starting the pyTivo -server. However, this had two drawbacks. 1. It took time as much as two -minutes before pyTivo was ready to accept requests. 2. It did not update -the cache if new files were added while the pyTivo server was -running.

-In the more recent releases, anything after Feb 16, 2008, pyTivo no -longer needs to query ffmpeg when creating a file list. Instead pyTivo -has a list of accepted video format extensions. If the file extension -falls within this list it is displayed on the TiVo. This achieves the -same speed increase as the precache method without the delay in loading -the pyTivo server.

-There are still two drawbacks to this method. 1. The video file must -have an extension that is in the list. There is a possibility that a new -video file extension could come out before pyTivo is updated. 2. -Incomplete or video files with errors will still appear in the TiVo -listing if they have the correct extension, even though they are not -valid files. Both of these concerns are minimal. 1. Very few new formats -of video files come out very often. And all extensions are stored in the -video.ext file which is easily edited. 2. When viewing the file details -before transferring the pyTivo server queries ffmpeg to make sure it is -valid. If the file is not valid it will show up as a copyrighted file -and transferring it will be prevented by TiVo.

-It is recommended that you leave precaching turned off as it is no -longer needed. -Example Settings: True/False -Available In: Shares - optres Default Setting: False @@ -233,31 +196,6 @@ resolution to an "S2 compatible" resolution. Example Settings: True/False Available In: Tivos, HD_tivos, SD_tivos -par - -Default Setting: 1.0 -Valid Entries: any valid par -Required: No -Skill: Very Advanced -Description: Set pixel aspect ratio. Applies to S3 Tivos only. OPTRES -must be false to use PAR. For some videofiles (e.g., AVC files), ffmpeg ->cannot determine the pixel aspect ratio correctly and assumes 1:1 which -leads to a distorted image on anamorphically encoded video files. This -setting allows you to override this default. If you do not understand -this setting, and you observe no distortions, you can ignore it. Set -par=1.0 to tell pyTivo to automatically adjust the pixel aspect ratio -whenever possible.

->par=1.18518519 (32:27, usually the correct setting for wide-screen -anamorphically encoded DVD content)

->par=0.88888889 (8:9, usually the correct setting for anamorphically -encoded DVDs from TV shows) -Example Settings: 1.0, 1.18518519, 0.88888889 -Alternative option: Specify par in metadata txt files by adding ->"Override_par : x.xxxx" to a metadata file associated with the video -file. If all files in the folder have the same par, add the option to a -file called default.txt instead of creating individual metadata files. -Available In: Server - video_fps Default Setting: 29.97 for S2 Tivo, same as source for S3/HD TiVo @@ -288,25 +226,6 @@ height=1080) Example Settings: 4096K, 8Mi, 12Mi, 16Mi, 20Mi Available In: Tivos, HD_tivos, SD_tivos -video_pct - -Default Setting: 85 (percent of source bitrate) -Valid Entries: Any number (set this to 0 to disable this option) -Required: No -Skill: Advanced -Description: This sets video_br to a percent of the source video -bitrate. pyTivo will multiply the source video bitrate by this -percentage setting, compare it to the video_br setting and use whichever -is greater as the video_br. This allows for setting higher bitrates -automatically when transcoding High Definition sources. For example, if -your source file has a bitrate of 22000k and video_pct is set to 70, -pyTivo will use 15400k for video_br instead of the default 8192k -setting. With this setting enabled, the video_br setting essentially -becomes the minimum video bitrate. This setting only applies to S3 -tivos. -Example Settings: 60, 70, 85, 100, 125 -Available In: Tivos, HD_tivos, SD_tivos - max_video_br Default Setting: 30000k @@ -381,9 +300,7 @@ Required: No Skill: Advanced Description: This allows you to choose the default audio bit rate used for transcoding. The default is likely fine for most users. 384k is the -minimum recommended for ac3 audio. For S2 Tivos, you may want to lower -this setting to 192k and set audio_ch=2 to slightly reduce the file -size. See audio_codec for more info. +minimum recommended for ac3 audio. Example Settings: 192K, 384K, 448K. Available In: Tivos, HD_tivos, SD_tivos @@ -419,45 +336,15 @@ Required: No Skill: Advanced Description: Sets the number of audio channels used by ffmpeg. ffmpeg will retain the same number of channels as the source file by default. -The default setting should work fine for most transfers unless the -default audio_codec is changed. Change this setting to 2 if you do not -want to retain 5.1 audio. A bug in ffmpeg will sometimes move the center -audio channel to the left or right speaker. Setting this option to 2, on -an as needed basis, or permanently, will correct this at the loss of 5.1 -audio. But this should only be necessary on rare occasions where the -source file is an mkv or xvid with ac3 5.1 audio bitrate above 448k. +Change this setting to 2 if you do not want to retain 5.1 audio. A bug +in ffmpeg will sometimes move the center audio channel to the left or +right speaker. Setting this option to 2, on an as needed basis, or +permanently, will correct this at the loss of 5.1 audio. But this should +only be necessary on rare occasions where the source file is an mkv or +xvid with ac3 5.1 audio bitrate above 448k. Example Settings: 2, 6 Available In: Tivos, HD_tivos, SD_tivos -audio_codec - -Default Setting: ac3 or copy -Valid Entries: mp2, ac3 -Required: No -Skill: Advanced -Description: Sets the audio codec used by ffmpeg during transcoding. -pyTivo defaults to ac3 in order to retain 5.1 audio should the source -contain it. pyTivo also checks the audio codec and bitrate of the source -and uses '-acodec copy' if it is compatible. Otherwise the Default -Setting is used. Specifying an audio codec will disable these features -and pyTivo will re-encode all audio using the codec specified.

-Allowing pyTivo to select '-acodec copy' whenever possible will -generally produce the best results. This is meant to eliminate garbled -audio during transcoding when the source contains audio glitches that -cause ffmpeg to lose synchronization. This will also prevent the center -audio channel from being moved to the right front speaker by ffmpeg when -transferring mkv's and xvid's with audio bitrate of 448k or -less.

-You may want to change this setting to mp2 if you only have an S2, or -have specified a per tivo section for an S2, and do not wish to retain -5.1 audio for possible transfer to an S3 Tivo or back to the PC should -the need arise. This will slightly reduce the amount of disk space used -and allow for more recordings on an S2 Tivo. In this case, you will want -to specify audio_codec=mp2, audio_ch=2, and audio_br=192k. These are the -settings normally used by the S2 for recordings. -Example Settings: mp2, ac3 -Available In: Tivos, HD_tivos, SD_tivos - audio_lang Recommended Setting: 5.1, DTS, en (entire string including commas) @@ -559,13 +446,12 @@ Available In: Tivos ffmpeg_wait -Default Setting: 10 (seconds) +Default Setting: 0 (no limit) Valid Entries: any integer Required: No Skill: Advanced -Description: Slow computers or NAS servers may result in pytivo -reporting supported videos as not supported (copy protected). -Increasing this setting will give ffmpeg more time to scan the file. +Description: Limits the amount of time FFmpeg can run (when used to +check file info, not for transcoding), in seconds. Example Settings: 10, 15, 20. Available In: Server @@ -579,7 +465,7 @@ Description: Your username (email address) at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. Example Settings: user@example.com -Available In: Server, Tivos, HD_tivos, SD_tivos +Available In: Server, Tivos tivo_password @@ -590,7 +476,7 @@ Skill: Basic Description: Your password at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. Example Settings: password -Available In: Server, Tivos, HD_tivos, SD_tivos +Available In: Server, Tivos tivo_mind @@ -618,7 +504,7 @@ tivodecode (pushing .TiVo files, transcoding HD .TiVo files to SD TiVos). If you don't plan to use these features, you don't need to set this. Example Settings: 012345678 -Available In: Server, Tivos, HD_tivos, SD_tivos +Available In: Server, Tivos togo_path diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index 6122970d..a1ad752e 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -54,8 +54,13 @@ def tmpl(name): queue = {} # Recordings to download -- list per TiVo basic_meta = {} # Data from NPL, parsed, indexed by progam URL +def null_cookie(name, value): + return cookielib.Cookie(0, name, value, None, False, '', False, + False, '', False, False, None, False, None, None, None) + auth_handler = urllib2.HTTPDigestAuthHandler() -cj = cookielib.LWPCookieJar() +cj = cookielib.CookieJar() +cj.set_cookie(null_cookie('sid', 'ADEADDA7EDEBAC1E')) tivo_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj), auth_handler) @@ -116,25 +121,25 @@ def NPL(self, handler, query): xmldoc = tivo_cache[theurl]['thepage'] items = xmldoc.getElementsByTagName('Item') - TotalItems = tag_data(xmldoc, 'Details/TotalItems') - ItemStart = tag_data(xmldoc, 'ItemStart') - ItemCount = tag_data(xmldoc, 'ItemCount') + TotalItems = tag_data(xmldoc, 'TiVoContainer/Details/TotalItems') + ItemStart = tag_data(xmldoc, 'TiVoContainer/ItemStart') + ItemCount = tag_data(xmldoc, 'TiVoContainer/ItemCount') FirstAnchor = tag_data(items[0], 'Links/Content/Url') data = [] for item in items: entry = {} - entry['ContentType'] = tag_data(item, 'ContentType') + entry['ContentType'] = tag_data(item, 'Details/ContentType') for tag in ('CopyProtected', 'UniqueId'): - value = tag_data(item, tag) + value = tag_data(item, 'Details/' + tag) if value: entry[tag] = value if entry['ContentType'] == 'x-tivo-container/folder': - entry['Title'] = tag_data(item, 'Title') - entry['TotalItems'] = tag_data(item, 'TotalItems') - lc = tag_data(item, 'LastCaptureDate') + entry['Title'] = tag_data(item, 'Details/Title') + entry['TotalItems'] = tag_data(item, 'Details/TotalItems') + lc = tag_data(item, 'Details/LastCaptureDate') if not lc: - lc = tag_data(item, 'LastChangeDate') + lc = tag_data(item, 'Details/LastChangeDate') entry['LastChangeDate'] = time.strftime('%b %d, %Y', time.localtime(int(lc, 16))) else: diff --git a/plugins/video/qtfaststart.py b/plugins/video/qtfaststart.py index 7603daa1..257a997e 100644 --- a/plugins/video/qtfaststart.py +++ b/plugins/video/qtfaststart.py @@ -24,6 +24,8 @@ History ------- + * 2013-01-28: Support strange zero-name, zero-length atoms, re-license + under the MIT license, version bump to 1.7 * 2010-02-21: Add support for final mdat atom with zero size, patch by Dmitry Simakov , version bump to 1.4. @@ -36,20 +38,28 @@ License ------- - Copyright (C) 2008 - 2009 Daniel G. Taylor - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . + Copyright (C) 2008 - 2013 Daniel G. Taylor + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. """ import logging @@ -57,7 +67,7 @@ from StringIO import StringIO -VERSION = "1.4wjm3" +VERSION = "1.7wjm3" CHUNK_SIZE = 8192 SEEK_CUR = 1 # Not defined in Python 2.4, so we define it here -- WJM3 @@ -109,10 +119,14 @@ def get_index(datastream): index.append((atom_type, datastream.tell() - skip, atom_size)) if atom_size == 0: - # Some files may end in mdat with no size set, which generally - # means to seek to the end of the file. We can just stop indexing - # as no more entries will be found! - break + if atom_type == "mdat": + # Some files may end in mdat with no size set, which + # generally means to seek to the end of the file. We can + # just stop indexing as no more entries will be found! + break + else: + # Weird, but just continue to try to find more atoms + atom_size = skip datastream.seek(atom_size - skip, SEEK_CUR) @@ -191,7 +205,11 @@ def process(datastream, outfile, skip=0): # This free atom is before the mdat! free_size += size log.info("Removing free atom at %d (%d bytes)" % (pos, size)) - + elif atom == "\x00\x00\x00\x00" and pos < mdat_pos: + # This is some strange zero atom with incorrect size + free_size += 8 + log.info("Removing strange zero atom at %s (8 bytes)" % pos) + # Offset to shift positions offset = moov_size - free_size diff --git a/plugins/video/templates/TvBus.tmpl b/plugins/video/templates/TvBus.tmpl index a81bacbc..f3038e5d 100644 --- a/plugins/video/templates/TvBus.tmpl +++ b/plugins/video/templates/TvBus.tmpl @@ -98,6 +98,9 @@ $escape($element) #end for + #if $video.programId + $video.programId + #end if $video.displayMajorNumber diff --git a/plugins/video/templates/container_xml.tmpl b/plugins/video/templates/container_xml.tmpl index 84ccd389..5d4cf09f 100644 --- a/plugins/video/templates/container_xml.tmpl +++ b/plugins/video/templates/container_xml.tmpl @@ -44,6 +44,9 @@ $escape($video.description) $escape($video.displayMajorNumber) $escape($video.callsign) + #if $video.programId + $video.programId + #end if $video.seriesId #if $video.episodeNumber $video.episodeNumber diff --git a/plugins/video/transcode.py b/plugins/video/transcode.py index 5d2a52a4..c73054a6 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -59,7 +59,7 @@ def debug(msg): logger.debug(msg) def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''): - settings = {'video_codec': select_videocodec(inFile, tsn), + settings = {'video_codec': select_videocodec(inFile, tsn, mime), 'video_br': select_videobr(inFile, tsn), 'video_fps': select_videofps(inFile, tsn), 'max_video_br': select_maxvideobr(tsn), @@ -211,40 +211,38 @@ def select_audiocodec(isQuery, inFile, tsn='', mime=''): return '-acodec copy' vInfo = video_info(inFile) codectype = vInfo['vCodec'] - codec = config.get_tsn('audio_codec', tsn) - if not codec: - # Default, compatible with all TiVo's - codec = 'ac3' - if mime == 'video/mp4': - compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac', - 'ac3', 'liba52') - else: - compatiblecodecs = ('ac3', 'liba52', 'mp2') - - if vInfo['aCodec'] in compatiblecodecs: - aKbps = vInfo['aKbps'] - aCh = vInfo['aCh'] - if aKbps == None: - if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'): - # along with the channel check below this should - # pass any AAC audio that has undefined 'aKbps' and - # is <= 2 channels. Should be TiVo compatible. - codec = 'copy' - elif not isQuery: - vInfoQuery = audio_check(inFile, tsn) - if vInfoQuery == None: - aKbps = None - aCh = None - else: - aKbps = vInfoQuery['aKbps'] - aCh = vInfoQuery['aCh'] - else: - codec = 'TBA' - if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn): - # compatible codec and bitrate, do not reencode audio + # Default, compatible with all TiVo's + codec = 'ac3' + if mime == 'video/mp4': + compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac', + 'ac3', 'liba52') + else: + compatiblecodecs = ('ac3', 'liba52', 'mp2') + + if vInfo['aCodec'] in compatiblecodecs: + aKbps = vInfo['aKbps'] + aCh = vInfo['aCh'] + if aKbps == None: + if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'): + # along with the channel check below this should + # pass any AAC audio that has undefined 'aKbps' and + # is <= 2 channels. Should be TiVo compatible. codec = 'copy' - if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2): - codec = 'ac3' + elif not isQuery: + vInfoQuery = audio_check(inFile, tsn) + if vInfoQuery == None: + aKbps = None + aCh = None + else: + aKbps = vInfoQuery['aKbps'] + aCh = vInfoQuery['aCh'] + else: + codec = 'TBA' + if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn): + # compatible codec and bitrate, do not reencode audio + codec = 'copy' + if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2): + codec = 'ac3' copy_flag = config.get_tsn('copy_ts', tsn) copyts = ' -copyts' if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or @@ -320,31 +318,31 @@ def select_videofps(inFile, tsn): fps = '-r ' + video_fps return fps -def select_videocodec(inFile, tsn): +def select_videocodec(inFile, tsn, mime=''): vInfo = video_info(inFile) - if tivo_compatible_video(vInfo, tsn)[0]: + if tivo_compatible_video(vInfo, tsn, mime)[0]: codec = 'copy' + if (mime == 'video/x-tivo-mpeg-ts' and + vInfo.get('vCodec', '') == 'h264'): + codec += ' -bsf h264_mp4toannexb' else: codec = 'mpeg2video' # default return '-vcodec ' + codec -def select_videobr(inFile, tsn): - return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k' +def select_videobr(inFile, tsn, mime=''): + return '-b ' + str(select_videostr(inFile, tsn, mime) / 1000) + 'k' -def select_videostr(inFile, tsn): +def select_videostr(inFile, tsn, mime=''): vInfo = video_info(inFile) - if tivo_compatible_video(vInfo, tsn)[0]: + if tivo_compatible_video(vInfo, tsn, mime)[0]: video_str = int(vInfo['kbps']) if vInfo['aKbps']: video_str -= int(vInfo['aKbps']) video_str *= 1000 else: video_str = config.strtod(config.getVideoBR(tsn)) - if config.isHDtivo(tsn): - if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0: - video_percent = (int(vInfo['kbps']) * 10 * - config.getVideoPCT(tsn)) - video_str = max(video_str, video_percent) + if config.isHDtivo(tsn) and vInfo['kbps']: + video_str = max(video_str, int(vInfo['kbps']) * 1000) video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95, video_str)) return video_str @@ -461,14 +459,8 @@ def select_aspect(inFile, tsn = ''): vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH)) if config.isHDtivo(tsn) and not optres: - if config.getPixelAR(0) or vInfo['par']: - if vInfo['par2'] == None: - if vInfo['par']: - npar = par2 - else: - npar = config.getPixelAR(1) - else: - npar = par2 + if vInfo['par']: + npar = par2 # adjust for pixel aspect ratio, if set @@ -589,6 +581,12 @@ def tivo_compatible_video(vInfo, tsn, mime=''): break + if mime == 'video/x-tivo-mpeg-ts': + if codec not in ('h264', 'mpeg2video'): + message = (False, 'vCodec %s not compatible' % codec) + + break + if codec not in ('mpeg2video', 'mpeg1video'): message = (False, 'vCodec %s not compatible' % codec) break @@ -605,11 +603,6 @@ def tivo_compatible_video(vInfo, tsn, mime=''): break if config.isHDtivo(tsn): - if vInfo['par2'] != 1.0: - if config.getPixelAR(0): - if vInfo['par2'] != None or config.getPixelAR(1) != 1.0: - message = (False, '%s not correct PAR' % vInfo['par2']) - break # HD Tivo detected, skipping remaining tests. break @@ -666,8 +659,10 @@ def tivo_compatible_audio(vInfo, inFile, tsn, mime=''): if inFile[-5:].lower() == '.tivo': break - if mime == 'video/x-tivo-mpeg-ts' and codec not in ('ac3', 'liba52'): - message = (False, 'aCodec %s not compatible' % codec) + if mime == 'video/x-tivo-mpeg-ts': + if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'): + message = (False, 'aCodec %s not compatible' % codec) + break if codec not in ('ac3', 'liba52', 'mp2'): @@ -809,19 +804,21 @@ def video_info(inFile, cache=True): stdin=subprocess.PIPE) # wait configured # of seconds: if ffmpeg is not back give up - wait = config.getFFmpegWait() - debug('starting ffmpeg, will wait %s seconds for it to complete' % wait) - for i in xrange(wait * 20): - time.sleep(.05) - if not ffmpeg.poll() == None: - break + limit = config.getFFmpegWait() + if limit: + for i in xrange(limit * 20): + time.sleep(.05) + if not ffmpeg.poll() == None: + break - if ffmpeg.poll() == None: - kill(ffmpeg) - vInfo['Supported'] = False - if cache: - info_cache[inFile] = (mtime, vInfo) - return vInfo + if ffmpeg.poll() == None: + kill(ffmpeg) + vInfo['Supported'] = False + if cache: + info_cache[inFile] = (mtime, vInfo) + return vInfo + else: + ffmpeg.wait() err_tmp.seek(0) output = err_tmp.read() diff --git a/plugins/video/video.py b/plugins/video/video.py index c8b66d9e..01b28a47 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -174,10 +174,6 @@ class BaseVideo(Plugin): tvbus_cache = LRUCache(1) - def pre_cache(self, full_path): - if Video.video_file_filter(self, full_path): - transcode.supported_format(full_path) - def video_file_filter(self, full_path, type=None): if os.path.isdir(unicode(full_path, 'utf-8')): return True @@ -319,10 +315,8 @@ def __est_size(self, full_path, tsn='', mime=''): return int(os.stat(unicode(full_path, 'utf-8')).st_size) else: # Must be re-encoded - if config.get_tsn('audio_codec', tsn) == None: - audioBPS = config.getMaxAudioBR(tsn) * 1000 - else: - audioBPS = config.strtod(config.getAudioBR(tsn)) + audioBPS = config.getMaxAudioBR(tsn) * 1000 + #audioBPS = config.strtod(config.getAudioBR(tsn)) videoBPS = transcode.select_videostr(full_path, tsn) bitrate = audioBPS + videoBPS return int((self.__duration(full_path) / 1000) * @@ -414,7 +408,6 @@ def QueryContainer(self, handler, query): return container = handler.container - precache = container.get('precache', 'False').lower() == 'true' force_alpha = container.get('force_alpha', 'False').lower() == 'true' use_html = query.get('Format', [''])[0].lower() == 'text/html' @@ -446,7 +439,7 @@ def QueryContainer(self, handler, query): video['small_path'] = subcname + '/' + video['name'] video['total_items'] = self.__total_items(f.name) else: - if precache or len(files) == 1 or f.name in transcode.info_cache: + if len(files) == 1 or f.name in transcode.info_cache: video['valid'] = transcode.supported_format(f.name) if video['valid']: video.update(self.metadata_full(f.name, tsn)) @@ -475,7 +468,6 @@ def QueryContainer(self, handler, query): t = Template(HTML_CONTAINER_TEMPLATE, filter=EncodeUnicode) else: t = Template(XML_CONTAINER_TEMPLATE, filter=EncodeUnicode) - t.container = handler.cname t.name = subcname t.total = total diff --git a/pyTivo.py b/pyTivo.py index 30eee670..38a07145 100755 --- a/pyTivo.py +++ b/pyTivo.py @@ -50,25 +50,6 @@ def setup(in_service=False): for section, settings in config.getShares(): httpd.add_container(section, settings) - # Precaching of files: does a recursive list of base path - if settings.get('precache', 'False').lower() == 'true': - plugin = GetPlugin(settings.get('type')) - if hasattr(plugin, 'pre_cache'): - logger.info('Pre-caching the ' + section + ' share.') - pre_cache_filter = getattr(plugin, 'pre_cache') - - def build_recursive_list(path): - try: - for f in os.listdir(path): - f = os.path.join(path, f) - if os.path.isdir(f): - build_recursive_list(f) - else: - pre_cache_filter(f) - except: - pass - - build_recursive_list(settings.get('path')) b = beacon.Beacon() b.add_service('TiVoMediaServer:%s/http' % port) From e3d27516635e2dadf9b823cd028790a2d0f2b1d1 Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 6 Jul 2013 22:41:43 -0500 Subject: [PATCH 20/21] updated to latest wmcbrine changes --- Zeroconf.py | 17 +++-- beacon.py | 74 +++++++++++++-------- httpserver.py | 73 +++++++++++++------- metadata.py | 33 +++++++-- plugin.py | 6 +- plugins/music/music.py | 38 ++++++++--- plugins/settings/help.txt | 66 +++++------------- plugins/settings/templates/settings.tmpl | 4 +- plugins/togo/templates/npl.tmpl | 49 +++++--------- plugins/togo/togo.py | 13 ++-- plugins/video/templates/container_html.tmpl | 67 +++++++------------ plugins/video/transcode.py | 24 ++++--- plugins/video/video.py | 4 +- pyTivo.conf.dist | 8 +-- templates/info_page.tmpl | 6 +- templates/root_container.tmpl | 7 +- 16 files changed, 256 insertions(+), 233 deletions(-) diff --git a/Zeroconf.py b/Zeroconf.py index f04c3392..da7e693f 100644 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -1,4 +1,4 @@ -""" Multicast DNS Service Discovery for Python, v0.12 +""" Multicast DNS Service Discovery for Python, v0.12-wmcbrine Copyright (C) 2003, Paul Scott-Murphy This module provides a framework for the use of DNS Service Discovery @@ -22,6 +22,8 @@ """ +"""0.12-wmcbrine update - see git for changes""" + """0.12 update - allow selection of binding interface typo fix - Thanks A. M. Kuchlingi removed all use of word 'Rendezvous' - this is an API change""" @@ -77,7 +79,7 @@ __author__ = "Paul Scott-Murphy" __email__ = "paul at scott dash murphy dot com" -__version__ = "0.12" +__version__ = "0.12-wmcbrine" import time import struct @@ -474,7 +476,7 @@ def unpack(self, format): def readHeader(self): """Reads header portion of packet""" (self.id, self.flags, self.numQuestions, self.numAnswers, - self.numAuthorities, self.numAdditionals) = self.unpack('!HHHHHH') + self.numAuthorities, self.numAdditionals) = self.unpack('!6H') def readQuestions(self): """Reads questions section of packet""" @@ -1560,10 +1562,13 @@ def handleQuery(self, msg, addr, port): def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): """Sends an outgoing packet.""" - # This is a quick test to see if we can parse the packets we generate - #temp = DNSIncoming(out.packet()) + packet = out.packet() try: - bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port)) + while packet: + bytes_sent = self.socket.sendto(packet, 0, (addr, port)) + if bytes_sent < 0: + break + packet = packet[bytes_sent:] except: # Ignore this, it may be a temporary loss of network connection pass diff --git a/beacon.py b/beacon.py index 5a250d82..7d45b5a3 100644 --- a/beacon.py +++ b/beacon.py @@ -20,11 +20,10 @@ def __init__(self, names): self.names = names def removeService(self, server, type, name): - if name in self.names: - self.names.remove(name) + self.names.remove(name.replace('.' + type, '')) def addService(self, server, type, name): - self.names.append(name) + self.names.append(name.replace('.' + type, '')) class ZCBroadcast: def __init__(self, logger): @@ -33,8 +32,11 @@ def __init__(self, logger): self.share_info = [] self.logger = logger self.rz = Zeroconf.Zeroconf() + self.renamed = {} + old_titles = self.scan() address = inet_aton(config.get_ip()) port = int(config.getPort()) + logger.info('Announcing shares...') for section, settings in config.getShares(): ct = GetPlugin(settings['type']).CONTENT_TYPE if ct.startswith('x-container/'): @@ -47,8 +49,14 @@ def __init__(self, logger): desc = {'path': SHARE_TEMPLATE % quote(section), 'platform': platform, 'protocol': 'http'} tt = ct.split('/')[1] + title = section + count = 1 + while title in old_titles: + count += 1 + title = '%s [%d]' % (section, count) + self.renamed[section] = title info = Zeroconf.ServiceInfo('_%s._tcp.local.' % tt, - '%s._%s._tcp.local.' % (section, tt), + '%s._%s._tcp.local.' % (title, tt), address, port, 0, 0, desc) self.rz.registerService(info) self.share_info.append(info) @@ -58,6 +66,8 @@ def scan(self): VIDS = '_tivo-videos._tcp.local.' names = [] + self.logger.info('Scanning for TiVos...') + # Get the names of servers offering TiVo videos browser = Zeroconf.ServiceBrowser(self.rz, VIDS, ZCListener(names)) @@ -66,15 +76,16 @@ def scan(self): # Now get the addresses -- this is the slow part for name in names: - info = self.rz.getServiceInfo(VIDS, name) + info = self.rz.getServiceInfo(VIDS, name + '.' + VIDS) if info and 'TSN' in info.properties: tsn = info.properties['TSN'] address = inet_ntoa(info.getAddress()) config.tivos[tsn] = address - name = name.replace('.' + VIDS, '') self.logger.info(name) config.tivo_names[tsn] = name + return names + def shutdown(self): self.logger.info('Unregistering: %s' % ' '.join(self.share_names)) for info in self.share_info: @@ -97,14 +108,10 @@ def __init__(self): if config.get_zc(): logger = logging.getLogger('pyTivo.beacon') try: - logger.info('Announcing shares...') self.bd = ZCBroadcast(logger) except: logger.error('Zeroconf failure') self.bd = None - else: - logger.info('Scanning for TiVos...') - self.bd.scan() else: self.bd = None @@ -127,15 +134,20 @@ def format_beacon(self, conntype, services=True): else: beacon.append('services=TiVoMediaServer:0/http') - return '\n'.join(beacon) + return '\n'.join(beacon) + '\n' def send_beacon(self): beacon_ips = config.getBeaconAddresses() + beacon = self.format_beacon('broadcast') for beacon_ip in beacon_ips.split(): if beacon_ip != 'listen': try: - self.UDPSock.sendto(self.format_beacon('broadcast'), - (beacon_ip, 2190)) + packet = beacon + while packet: + result = self.UDPSock.sendto(packet, (beacon_ip, 2190)) + if result < 0: + break + packet = packet[result:] except error, e: print e @@ -149,6 +161,22 @@ def stop(self): if self.bd: self.bd.shutdown() + def recv_bytes(self, sock, length): + block = '' + while len(block) < length: + add = sock.recv(length - len(block)) + if not add: + break + block += add + return block + + def recv_packet(self, sock): + length = struct.unpack('!I', self.recv_bytes(sock, 4))[0] + return self.recv_bytes(sock, length) + + def send_packet(self, sock, packet): + sock.sendall(struct.pack('!I', len(packet)) + packet) + def listen(self): """ For the direct-connect, TCP-style beacon """ import thread @@ -162,14 +190,12 @@ def server(): # Wait for a connection client, address = TCPSock.accept() - # Accept the client's beacon - client_length = struct.unpack('!I', client.recv(4))[0] - client_message = client.recv(client_length) + # Accept (and discard) the client's beacon + self.recv_packet(client) # Send ours - message = self.format_beacon('connected') - client.send(struct.pack('!I', len(message))) - client.send(message) + self.send_packet(client, self.format_beacon('connected')) + client.close() thread.start_new_thread(server, ()) @@ -182,15 +208,9 @@ def get_name(self, address): try: tsock = socket() tsock.connect((address, 2190)) - - tsock.send(struct.pack('!I', len(our_beacon))) - tsock.send(our_beacon) - - length = struct.unpack('!I', tsock.recv(4))[0] - tivo_beacon = tsock.recv(length) - + self.send_packet(tsock, our_beacon) + tivo_beacon = self.recv_packet(tsock) tsock.close() - name = machine_name(tivo_beacon).groups()[0] except: name = address diff --git a/httpserver.py b/httpserver.py index 9f87d90e..797bcfdf 100644 --- a/httpserver.py +++ b/httpserver.py @@ -1,12 +1,15 @@ import BaseHTTPServer import SocketServer import cgi +import gzip import logging import mimetypes import os import shutil import socket import time +from cStringIO import StringIO +from email.utils import formatdate from urllib import unquote_plus, quote from xml.sax.saxutils import escape @@ -38,7 +41,9 @@ BASE_HTML = """ - pyTivo %s """ + pyTivo + + %s """ RELOAD = '

The page will reload in %d seconds.

' UNSUP = '

Unsupported Command

Query:

    %s
' @@ -80,6 +85,7 @@ class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.wbufsize = 0x10000 self.server_version = 'pyTivo/1.0' + self.protocol_version = 'HTTP/1.1' self.sys_version = '' BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, server) @@ -193,13 +199,40 @@ def handle_query(self, query, tsn): elif command in ('FlushServer', 'ResetServer'): # Does nothing -- included for completeness self.send_response(200) + self.send_header('Content-Length', '0') self.end_headers() + self.wfile.flush() return # If we made it here it means we couldn't match the request to # anything. self.unsupported(query) + def send_content_file(self, path): + lmdate = os.path.getmtime(path) + try: + handle = open(path, 'rb') + except: + self.send_error(404) + return + + # Send the header + mime = mimetypes.guess_type(path)[0] + self.send_response(200) + if mime: + self.send_header('Content-Type', mime) + self.send_header('Content-Length', os.path.getsize(path)) + self.send_header('Last-Modified', formatdate(lmdate)) + self.end_headers() + + # Send the body of the file + try: + shutil.copyfileobj(handle, self.wfile) + except: + pass + handle.close() + self.wfile.flush() + def handle_file(self, query, splitpath): if '..' not in splitpath: # Protect against path exploits ## Pass it off to a plugin? @@ -218,26 +251,7 @@ def handle_file(self, query, splitpath): path = os.path.join(base, 'content', splitpath[-1]) if os.path.isfile(path): - try: - handle = open(path, 'rb') - except: - self.send_error(404) - return - - # Send the header - mime = mimetypes.guess_type(path)[0] - self.send_response(200) - if mime: - self.send_header('Content-type', mime) - self.send_header('Content-length', os.path.getsize(path)) - self.end_headers() - - # Send the body of the file - try: - shutil.copyfileobj(handle, self.wfile) - except: - pass - handle.close() + self.send_content_file(path) return ## Give up @@ -261,15 +275,24 @@ def log_message(self, format, *args): self.log_date_time_string(), format%args)) def send_fixed(self, page, mime, code=200, refresh=''): + squeeze = (len(page) > 256 and mime.startswith('text') and + 'gzip' in self.headers.getheader('Accept-Encoding', '')) + if squeeze: + out = StringIO() + gzip.GzipFile(mode='wb', fileobj=out).write(page) + page = out.getvalue() + out.close() self.send_response(code) self.send_header('Content-Type', mime) self.send_header('Content-Length', len(page)) - self.send_header('Connection', 'close') + if squeeze: + self.send_header('Content-Encoding', 'gzip') self.send_header('Expires', '0') if refresh: self.send_header('Refresh', refresh) self.end_headers() self.wfile.write(page) + self.wfile.flush() def send_xml(self, page): self.send_fixed(page, 'text/xml') @@ -293,6 +316,10 @@ def root_container(self): t = Template(file=os.path.join(SCRIPTDIR, 'templates', 'root_container.tmpl'), filter=EncodeUnicode) + if self.server.beacon.bd: + t.renamed = self.server.beacon.bd.renamed + else: + t.renamed = {} t.containers = tsncontainers t.hostname = socket.gethostname() t.escape = escape @@ -327,7 +354,7 @@ def infopage(self): if plugin_type == 'settings': t.admin += ('Web Configuration
') + '">Settings
') elif plugin_type == 'togo' and t.togo: for tsn in config.tivos: if tsn: diff --git a/metadata.py b/metadata.py index 3f7cf70e..a9a8c5d9 100755 --- a/metadata.py +++ b/metadata.py @@ -45,6 +45,10 @@ BOM = '\xef\xbb\xbf' +GB = 1024 ** 3 +MB = 1024 ** 2 +KB = 1024 + tivo_cache = LRUCache(50) mp4_cache = LRUCache(50) dvrms_cache = LRUCache(50) @@ -61,6 +65,18 @@ def get_tv(rating): def get_stars(rating): return HUMAN['starRating'].get(rating, '') +def human_size(raw): + raw = float(raw) + if raw > GB: + tsize = '%.2f GB' % (raw / GB) + elif raw > MB: + tsize = '%.2f MB' % (raw / MB) + elif raw > KB: + tsize = '%.2f KB' % (raw / KB) + else: + tsize = '%d Bytes' % raw + return tsize + def tag_data(element, tag): for name in tag.split('/'): found = False @@ -163,10 +179,14 @@ def from_moov(full_path): 'plistlib' in sys.modules): items = {'cast': 'vActor', 'directors': 'vDirector', 'producers': 'vProducer', 'screenwriters': 'vWriter'} - data = plistlib.readPlistFromString(value) - for item in items: - if item in data: - metadata[items[item]] = [x['name'] for x in data[item]] + try: + data = plistlib.readPlistFromString(value) + except: + pass + else: + for item in items: + if item in data: + metadata[items[item]] = [x['name'] for x in data[item]] mp4_cache[full_path] = metadata return metadata @@ -238,7 +258,10 @@ def from_eyetv(full_path): path = os.path.dirname(unicode(full_path, 'utf-8')) eyetvp = [x for x in os.listdir(path) if x.endswith('.eyetvp')][0] eyetvp = os.path.join(path, eyetvp) - eyetv = plistlib.readPlist(eyetvp) + try: + eyetv = plistlib.readPlist(eyetvp) + except: + return metadata if 'epg info' in eyetv: info = eyetv['epg info'] for key in keys: diff --git a/plugin.py b/plugin.py index 268a41c7..229b2170 100644 --- a/plugin.py +++ b/plugin.py @@ -70,11 +70,7 @@ def init(self): pass def send_file(self, handler, path, query): - handler.send_response(200) - handler.end_headers() - f = open(unicode(path, 'utf-8'), 'rb') - shutil.copyfileobj(f, handler.wfile) - f.close() + handler.send_content_file(unicode(path, 'utf-8')) def get_local_base_path(self, handler, query): return os.path.normpath(handler.container['path']) diff --git a/plugins/music/music.py b/plugins/music/music.py index b8aa668f..6bfef08a 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -36,6 +36,8 @@ 'date': ['\xa9day', u'WM/Year'], 'genre': ['\xa9gen', u'WM/Genre']} +BLOCKSIZE = 64 * 1024 + # Search strings for different playlist types asxfile = re.compile('ref +href *= *"([^"]*)"', re.IGNORECASE).search wplfile = re.compile('media +src *= *"([^"]*)"', re.IGNORECASE).search @@ -100,12 +102,14 @@ def send_file(self, handler, path, query): ext = os.path.splitext(fname)[1].lower() needs_transcode = ext in TRANSCODE or seek or duration or always - handler.send_response(200) - handler.send_header('Content-Type', 'audio/mpeg') if not needs_transcode: fsize = os.path.getsize(fname) + handler.send_response(200) handler.send_header('Content-Length', fsize) - handler.send_header('Connection', 'close') + else: + handler.send_response(206) + handler.send_header('Transfer-Encoding', 'chunked') + handler.send_header('Content-Type', 'audio/mpeg') handler.end_headers() if needs_transcode: @@ -123,12 +127,21 @@ def send_file(self, handler, path, query): if duration: cmd[-1:] = ['-t', '%.3f' % (duration / 1000.0), '-'] - ffmpeg = subprocess.Popen(cmd, bufsize=(64 * 1024), + ffmpeg = subprocess.Popen(cmd, bufsize=BLOCKSIZE, stdout=subprocess.PIPE) - try: - shutil.copyfileobj(ffmpeg.stdout, handler.wfile) - except: - kill(ffmpeg) + while True: + try: + block = ffmpeg.stdout.read(BLOCKSIZE) + handler.wfile.write('%x\r\n' % len(block)) + handler.wfile.write(block) + handler.wfile.write('\r\n') + except Exception, msg: + handler.server.logger.info(msg) + kill(ffmpeg) + break + + if not block: + break else: f = open(fname, 'rb') try: @@ -137,6 +150,11 @@ def send_file(self, handler, path, query): pass f.close() + try: + handler.wfile.flush() + except Exception, msg: + handler.server.logger.info(msg) + def QueryContainer(self, handler, query): def AudioFileFilter(f, filter_type=None): @@ -419,9 +437,7 @@ def build_recursive_list(path, recurse=True): files.extend(build_recursive_list(f)) else: fd = FileData(f, isdir) - if recurse and fd.isplay: - files.extend(self.parse_playlist(f, recurse)) - elif isdir or filterFunction(f, file_type): + if isdir or filterFunction(f, file_type): files.append(fd) except: pass diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index ee173d4f..12c54142 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -61,7 +61,6 @@ port Default Setting: 9032 Valid Entries: 1-65535 Required: No -Skill: Basic Description: The port which pyTivo uses to serve your files. Can be changed if it conflicts with another program. Example Settings: 9032 @@ -72,14 +71,13 @@ ffmpeg Default Setting: None Valid Entries: Operating system path Required: No -Skill: Basic Description: This is the full path to your ffmpeg binary. If not set, pyTivo checks for it in a "bin" subdirectory, and then in the PATH. If no ffmpeg is found, pyTivo will operate in a limited mode, serving only MPEG and TiVo files in video shares, and only MP3 files in music shares, with no seek capability. Example Settings: Linux = /usr/bin/ffmpeg | ->Windows = c:\Program Files\pyTivo\bin\ffmpeg.exe +>Windows = C:\pyTivo\bin\ffmpeg.exe Available In: Server tivodecode @@ -87,13 +85,12 @@ tivodecode Default Setting: None Valid Entries: Operating system path Required: No -Skill: Basic Description: This is the full path to your tivodecode binary. If not set, pyTivo checks for it in a "bin" subdirectory, and then in the PATH. tivodecode is only needed for certain functions (currently pushing .TiVo files or transcoding HD .TiVo files to SD). Example Settings: Linux = /usr/bin/tivodecode | ->Windows = c:\Program Files\pyTivo\bin\tivodecode.exe +>Windows = C:\pyTivo\bin\tivodecode.exe Available In: Server tdcat @@ -101,13 +98,12 @@ tdcat Default Setting: None Valid Entries: Operating system path Required: No -Skill: Basic Description: This is the full path to your tdcat binary. If not set, pyTivo checks for it in a "bin" subdirectory, and then in the PATH. tdcat is only needed to view the data from a .TiVo file in the details screen. It comes with tivodecode. Example Settings: Linux = /usr/bin/tdcat | ->Windows = c:\Program Files\pyTivo\bin\tdcat.exe +>Windows = C:\pyTivo\bin\tdcat.exe Available In: Server beacon @@ -116,7 +112,6 @@ Default Setting: 255.255.255.255 Valid Entries: Beacon IP address(es) or "listen". Can contain multiple IPs separated by spaces. Required: No -Skill: Advanced Description: The addresses on which the beacon should broadcast. Most people can leave this at the default. If set to "listen", will accept incoming TCP beacons. If you're having issues with your shares not @@ -133,7 +128,6 @@ debug Default Setting: False Valid Entries: True/False Required: No -Skill: Advanced Description: Will generate more output for debugging purposes. Example Settings: True/False Available In: Server @@ -143,7 +137,6 @@ type Default Setting: None Valid Entries: video, music, photo, or any other valid plugin name. Required: Yes -Skill: Basic Description: Sets the type of share that this will be. This must be set to something otherwise pyTivo will not start. NOTE plugins names are generally lowercase. @@ -155,7 +148,6 @@ path Default Setting: None Valid Entries: Any operating system path Required: Yes -Skill: Basic Description: Sets the base path to your media content. While pyTivo will start with an invalid path your shares will not work at all. Example Settings: Windows = C:\videos | Linux = /home/user/media @@ -166,7 +158,6 @@ force_alpha Default Setting: False Valid Entries: True/False Required: No -Skill: Basic Description: Only meaningful in shares of type "video". When false, pyTivo will display videos in the order requested by the TiVo, as described at the bottom of the screen. When true, pyTivo will ignore the @@ -182,7 +173,6 @@ optres Default Setting: False Valid Entries: True/False Required: No -Skill: Moderate Description: Allows for the use of the Optimal Resolution in transcoding. By setting optres = true pyTivo will treat the height and width settings in the conf file as a maximum. If the video to be @@ -190,8 +180,8 @@ transcoded has smaller dimensions that are closer to other acceptable TiVo dimensions then pyTivo will use these dimensions. This allows for faster transcoding and small files when the initial video is a lower quality. pyTivo uses the same resolution as the source file on HD Tivos -for optimal transcoding efficiency. It is not necessay to to set this -option with HD TiVos unles you wish to force pyTivo to change the +for optimal transcoding efficiency. It is not necessary to to set this +option with HD TiVos unless you wish to force pyTivo to change the resolution to an "S2 compatible" resolution. Example Settings: True/False Available In: Tivos, HD_tivos, SD_tivos @@ -201,7 +191,6 @@ video_fps Default Setting: 29.97 for S2 Tivo, same as source for S3/HD TiVo Valid Entries: 29.97, 23.98, 25, 59.94 Required: No -Skill: Advanced Description: Sets the frame rate used by ffmpeg. pyTivo uses 29.97 for S2's, and uses the same frame rate as the source on HD TiVos. The default setting should work fine for most transfers. @@ -213,7 +202,6 @@ video_br Default Setting: 4096K for SD TiVo's, 16384K for HD TiVo's Valid Entries: Any valid Bit rate. 1024K = 1Mi Required: No -Skill: Advanced Description: This allows you to choose the default server video bit rate used in transcoding. FFmpeg does not strictly follow this bit rate, there is a certain level of tolerance that is allowed. Also a low @@ -231,7 +219,6 @@ max_video_br Default Setting: 30000k Valid Entries: Any valid Bit rate. 1024K = 1Mi Required: No -Skill: Advanced Description: This allows you to choose the maximum bit rate and is more strict than the video_br setting above. However setting this can cause buffer overflows and can cause issues with ffmpeg. In addition to @@ -251,7 +238,6 @@ bufsize Default Setting: 1024k for S2, 4096k for S3 Valid Entries: Any valid byte size Required: No -Skill: Very Advanced Description: Allows you to set the buffer size used by ffmpeg. Increasing this setting will allow higher bitrates during transcoding (see video_br setting), especially when transcoding to HD resolutions. @@ -264,11 +250,10 @@ Available In: Tivos, HD_tivos, SD_tivos width -Default Setting: 544 for S2, 1920 for S3 +Default Setting: 544 for S2, 1920 for S3+ Valid Entries: Any valid pixel dimension. Setting will be rounded to nearest acceptable TiVo dimension. Required: No -Skill: Moderate Description: Allows you to choose the output dimension of the transcoded videos. SD units are limited to 720 and below. Likely HD users will want to choose a higher value. Higher values may slow down transcoding and @@ -279,11 +264,10 @@ Available In: Tivos, HD_tivos, SD_tivos height -Default Setting: 480 for S2, 1080 for S3 +Default Setting: 480 for S2, 1080 for S3+ Valid Entries: Any valid pixel dimension. Setting will be rounded to nearest acceptable TiVo dimension Required: No -Skill: Moderate Description: Allows you to choose the output dimension of the transcoded videos. SD units are limited to 480 and below. Likely HD users will want to choose a higher value. Higher values may slow down transcoding and @@ -297,7 +281,6 @@ audio_br Default Setting: same bitrate as source or 448k Valid Entries: Any valid bitrate up to 448k Required: No -Skill: Advanced Description: This allows you to choose the default audio bit rate used for transcoding. The default is likely fine for most users. 384k is the minimum recommended for ac3 audio. @@ -309,7 +292,6 @@ max_audio_br Default Setting: 448k Valid Entries: Any valid bitrate Required: No -Skill: Advanced Description: This sets the maximum audio bit rate that can be sent to the TiVo. Files having a higher bit rate will be transcoded to ensure TiVo compatibility. @@ -321,7 +303,6 @@ audio_fr Default Setting: same frequency as source Valid Entries: 44100, 48000 Required: No -Skill: Advanced Description: Sets the audio sampling frequency. Defaults to frequency of the source file for better audio sync if it is 44100 or 48000. Otherwise 48000 is used. @@ -333,7 +314,6 @@ audio_ch Default Setting: same channels as source Valid Entries: any number compatible with ffmpeg and the audio codec selected Required: No -Skill: Advanced Description: Sets the number of audio channels used by ffmpeg. ffmpeg will retain the same number of channels as the source file by default. Change this setting to 2 if you do not want to retain 5.1 audio. A bug @@ -351,7 +331,6 @@ Recommended Setting: 5.1, DTS, en (entire string including commas) pyTivo Defaults To: first audio stream Valid Entries: any language tag or audio stream number reported by ffmpeg Required: No -Skill: Basic Description: Sets the preferred language track used by pyTivo. ffmpeg/pytivo defaults to the first audio stream. Specifying this parameter, tells pyTivo to use the first audio stream that matches this @@ -364,10 +343,11 @@ You can also assign new language tags to your files by adding Override lines to your metadata txt files. This will enable pytivo to detect your audio language setting in files that do not contain language tags. The syntax is
-Override_mapAudio 0.1 colon eng (replace the word colon with a colon)
-Where 0.1 is the audio stream number reported by ffmpeg and eng is the -new audio tag to assign to that stream. Add a separate Override line -for each audio stream that you want to replace with a new tag. +Override_mapAudio: 0.1 eng
+Where 0.1 is the audio stream number reported by ffmpeg and eng is the +new audio tag to assign to that stream. You can specify multiple +streams with one Override line --
+Override_mapAudio: 0:1 eng 0:2 "long tag" 0:3 foo
Example Settings: eng, ger, spa, en, ge, 0.0, 0.1, 0.2, 0x80, 0x81 etc... Available In: Tivos, HD_tivos, SD_tivos @@ -376,7 +356,6 @@ copy_ts Default Setting: True Valid Entries: True/False Required: No -Skill: Basic Description: Adds the copy timestamps setting (-copyts) to the ffmpeg command. This setting helps correct audio synchronization problems that commonly occur during transcoding. You can leave this field blank @@ -392,7 +371,6 @@ ffmpeg_pram Default Setting: None Valid Entries: A valid ffmpeg command Required: No -Skill: Very Advanced Description: This allows you to append additional raw ffmpeg commands to the ffmpeg template. For example, you would enter '-threads 2' here if you have multiple processors and want ffmpeg to use both processors to @@ -408,7 +386,6 @@ Default Setting: %(video_codec)s %(video_fps)s %(video_br)s %(ffmpeg_pram)s %(format)s Valid Entries: A valid ffmpeg command Required: No -Skill: Very Advanced Description: This is a template used by pyTivo to control the parameters passed to ffmpeg. It should not be necessary to modify this template unless there is a particular parameter you do not wish ffmpeg to use and @@ -422,7 +399,6 @@ aspect169 Default Setting: True Valid Entries: True/False Required: No -Skill: Moderate Description: Most TiVos, even S2, can handle 16:9 videos perfectly. Some >S2s are known not to handle 16:9 and will default to false in this setting. If you are experiencing major distortion you can try setting @@ -436,7 +412,6 @@ Default Setting: None (allow all shares on this TiVo). Valid Entries: The names of any shares in your pyTivo.conf file, in a comma-separated list. Required: No -Skill: Easy Description: Only the shares listed in this setting will be visible on this TiVo. Will ignore invalid shares. If no valid shares are listed, no shares will be visible on this TiVo. If the "shares" line is not @@ -449,7 +424,6 @@ ffmpeg_wait Default Setting: 0 (no limit) Valid Entries: any integer Required: No -Skill: Advanced Description: Limits the amount of time FFmpeg can run (when used to check file info, not for transcoding), in seconds. Example Settings: 10, 15, 20. @@ -460,7 +434,6 @@ tivo_username Default Setting: None Valid Entries: tivo.com username Required: No -Skill: Basic Description: Your username (email address) at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. @@ -472,7 +445,6 @@ tivo_password Default Setting: None Valid Entries: tivo.com password Required: No -Skill: Basic Description: Your password at tivo.com. This is required for the "Push" feature. If you don't plan to use Push, you don't need to set this. Example Settings: password @@ -483,7 +455,6 @@ tivo_mind Default Setting: mind.tivo.com:8181 Valid Entries: address:port Required: No -Skill: Moderate Description: The TiVo "mind" server and port to use. This is the server that pyTivo connects to in order to make Push requests. For most users in the U.S., the default is the correct value. Australian users will @@ -496,7 +467,6 @@ tivo_mak Default Setting: None Valid Entries: Your Media Access Key Required: No -Skill: Basic Description: Your Media Access Key -- find it on your TiVo under Messages and Settings, Account and System information, Media Access Key. This is required for the "ToGo" feature, and for anything that uses @@ -511,7 +481,6 @@ togo_path Default Setting: None Valid Entries: System path or share name Required: No -Skill: Basic Description: The path used to save programs downloaded via the ToGo menu. It can be either a direct path, or the name of a share, in which case pyTivo will use the path specified for the share. If you don't plan @@ -524,7 +493,6 @@ zeroconf Default Setting: Auto Valid Entries: True/False/Auto Required: No -Skill: Moderate Description: Controls whether or not new-style, zeroconf-based beacons are used. The default is to use them, unless there's a "_tivo_" section with "shares" defined. The zeroconf beacons bypass the usual mechanism @@ -539,11 +507,9 @@ nosettings Default Setting: False Valid Entries: True/False Required: No -Skill: Basic -Description: Disable the "Web Configuration" (settings) item in the -infopage (i.e. the very thing you're using now). Note that you can't -turn this off the way you turned it on, since the settings page will not -be available! You'll have to remove it from pyTivo.conf with a text -editor. +Description: Disable the "Settings" item in the infopage (i.e. the very +thing you're using now). Note that you can't turn this off the way you +turned it on, since the settings page will not be available! You'll have +to remove it from pyTivo.conf with a text editor. Example Settings: True/False Available In: Server diff --git a/plugins/settings/templates/settings.tmpl b/plugins/settings/templates/settings.tmpl index 1726e417..8a8250d4 100644 --- a/plugins/settings/templates/settings.tmpl +++ b/plugins/settings/templates/settings.tmpl @@ -2,7 +2,7 @@ "http://www.w3.org/TR/html4/strict.dtd"> -pyTivo Web Configuration +pyTivo - Settings @@ -76,7 +76,7 @@ #end def

-pyTivo Web Configuration +pyTivo Settings help

Home

diff --git a/plugins/togo/templates/npl.tmpl b/plugins/togo/templates/npl.tmpl index d6dad510..2f2c9e3c 100644 --- a/plugins/togo/templates/npl.tmpl +++ b/plugins/togo/templates/npl.tmpl @@ -7,21 +7,17 @@
-

pyTivo - ToGo - $escape($tname)

-

Home

- - +

+pyTivo / + #if $folder != '' + + #end if +Pull from $escape($tname) + #if $folder != '' + / $escape($title) + #end if +

+
#if $ItemStart > 0 #end if - #if $folder != '' - ## We are in a subfolder, offer the option to return to NPL - - - - - #end if - ## Header Row - - - - - - - #set $i = 0 ## i variable is used to alternate colors of row ## loop through passed data printing row for each show or folder @@ -58,8 +39,8 @@ function toggle(source) { - - + + #else ## This is a show - - + #end if #end for diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index a1ad752e..ea7c163d 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -39,8 +39,8 @@ UNABLE = """

Unable to Connect to TiVo

pyTivo was unable to connect to the TiVo at %s.

This is most likely caused by an -incorrect Media Access Key. Please return to the Web Configuration page -and double check your tivo_mak setting.

""" +incorrect Media Access Key. Please return to the Settings page and +double check your tivo_mak setting.

""" # Preload the templates def tmpl(name): @@ -125,6 +125,7 @@ def NPL(self, handler, query): ItemStart = tag_data(xmldoc, 'TiVoContainer/ItemStart') ItemCount = tag_data(xmldoc, 'TiVoContainer/ItemCount') FirstAnchor = tag_data(items[0], 'Links/Content/Url') + title = tag_data(xmldoc, 'TiVoContainer/Details/Title') data = [] for item in items: @@ -153,11 +154,11 @@ def NPL(self, handler, query): if value: entry[key] = value - entry['SourceSize'] = ( '%.3f GB' % - (float(entry['SourceSize']) / (1024 ** 3)) ) + rawsize = entry['SourceSize'] + entry['SourceSize'] = metadata.human_size(rawsize) dur = int(entry['Duration']) / 1000 - entry['Duration'] = ( '%02d:%02d:%02d' % + entry['Duration'] = ( '%d:%02d:%02d' % (dur / 3600, (dur % 3600) / 60, dur % 60) ) entry['CaptureDate'] = time.strftime('%b %d, %Y', @@ -179,6 +180,7 @@ def NPL(self, handler, query): ItemStart = 0 ItemCount = 0 FirstAnchor = '' + title = '' if useragent.lower().find('mobile') > 0: t = Template(CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode) @@ -201,6 +203,7 @@ def NPL(self, handler, query): t.ItemCount = int(ItemCount) t.FirstAnchor = quote(FirstAnchor) t.shows_per_page = shows_per_page + t.title = title handler.send_html(str(t), refresh='300') def get_tivo_file(self, tivoIP, url, mak, togo_path): diff --git a/plugins/video/templates/container_html.tmpl b/plugins/video/templates/container_html.tmpl index 7c5e6bae..2d6bb6b2 100644 --- a/plugins/video/templates/container_html.tmpl +++ b/plugins/video/templates/container_html.tmpl @@ -6,42 +6,26 @@ +#set $folders = $name.split('/') -

pyTivo - Push - $escape($name)

-

Home

- -
#set $Offset = -($ItemStart + 1) @@ -31,21 +27,6 @@ function toggle(source) { Previous Page
Back to Now Playing List
TitleSizeCapture Date
$row['Title'] $(row["TotalItems"]) Items$row["LastChangeDate"]$(row["TotalItems"]) Items$row["LastChangeDate"] @@ -125,10 +106,10 @@ function toggle(source) { #end if #end if $row['SourceSize']
+
$row['SourceSize']
$row['Duration']
$row['CaptureDate']$row['CaptureDate']
- ## Header Row - - - - - - - - #set $parent = '' - #set $folders = $name.split("/") - #set $current_folder = $folders.pop() - #set $parent = '/'.join($folders) - #if $parent != '' - - - - - #end if +

+pyTivo / +#if len($folders) > 1 + +#end if +Push from $escape($folders[0]) +#if len($folders) > 1 + + #if len($folders) > 2 + #for $pos, $n in enumerate($folders[1:-1]) + #set $ppath = '/'.join($folders[:$pos + 2]) +/ $n + #end for + #end if +/ $escape($folders[-1]) +#end if +

+
TitleSizeCapture Date
- Up to Parent Folder -
#set $i = 0 ## i variable is used to alternate colors of row ## loop through passed data printing row for each show or folder @@ -52,10 +36,10 @@ function toggle(source) { #if $video.is_dir ## This is a folder - - - - + + + + #else ## This is a show - - + + #end if #end for diff --git a/plugins/video/transcode.py b/plugins/video/transcode.py index c73054a6..f99a2ded 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -2,6 +2,7 @@ import math import os import re +import shlex import shutil import subprocess import sys @@ -278,27 +279,30 @@ def select_audiolang(inFile, tsn): if vInfo['mapAudio']: # default to first detected audio stream to begin with stream = vInfo['mapAudio'][0][0] + debug('set first detected audio stream by default: %s' % stream) if audio_lang != None and vInfo['mapVideo'] != None: langmatch_curr = [] langmatch_prev = vInfo['mapAudio'][:] for lang in audio_lang.replace(' ', '').lower().split(','): + debug('matching lang: %s' % lang) for s, l in langmatch_prev: if lang in s + l.replace(' ', '').lower(): + debug('matched: %s' % s + l.replace(' ', '').lower()) langmatch_curr.append((s, l)) - stream = s # if only 1 item matched we're done if len(langmatch_curr) == 1: + stream = langmatch_curr[0][0] + debug('found exactly one match: %s' % stream) break # if more than 1 item matched copy the curr area to the prev # array we only need to look at the new shorter list from # now on elif len(langmatch_curr) > 1: langmatch_prev = langmatch_curr[:] + # default to the first item matched thus far + stream = langmatch_curr[0][0] + debug('remember first match: %s' % stream) langmatch_curr = [] - # if we drop out of the loop with more than 1 item default to - # the first item - if len(langmatch_curr) > 1: - stream = langmatch_curr[0][0] # don't let FFmpeg auto select audio stream, pyTivo defaults to # first detected if stream: @@ -999,12 +1003,10 @@ def video_info(inFile, cache=True): vInfo['Supported'] = True if key.startswith('Override_mapAudio'): audiomap = dict(vInfo['mapAudio']) - stream = key.replace('Override_mapAudio', '').strip() - if stream in audiomap: - newaudiomap = (stream, data[key]) - audiomap.update([newaudiomap]) - vInfo['mapAudio'] = sorted(audiomap.items(), - key=lambda (k,v): (k,v)) + newmap = shlex.split(data[key]) + audiomap.update(zip(newmap[::2], newmap[1::2])) + vInfo['mapAudio'] = sorted(audiomap.items(), + key=lambda (k,v): (k,v)) elif key.startswith('Override_millisecs'): vInfo[key.replace('Override_', '')] = int(data[key]) else: diff --git a/plugins/video/video.py b/plugins/video/video.py index 01b28a47..472949c6 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -228,7 +228,6 @@ def send_file(self, handler, path, query): handler.send_response(206) handler.send_header('Transfer-Encoding', 'chunked') handler.send_header('Content-Type', mime) - handler.send_header('Connection', 'close') handler.end_headers() logger.info('[%s] Start sending "%s" to %s' % @@ -454,8 +453,7 @@ def QueryContainer(self, handler, query): else: video['mime'] = 'video/x-tivo-mpeg' - video['textSize'] = ( '%.3f GB' % - (float(f.size) / (1024 ** 3)) ) + video['textSize'] = metadata.human_size(f.size) videos.append(video) diff --git a/pyTivo.conf.dist b/pyTivo.conf.dist index 807f2b6f..0abeb234 100644 --- a/pyTivo.conf.dist +++ b/pyTivo.conf.dist @@ -14,9 +14,9 @@ port=9032 # FFmpeg is a required tool but downloaded separately. See pyTivo wiki # for help. # Full path to ffmpeg including filename -# For windows: ffmpeg=c:\Program Files\pyTivo\bin\ffmpeg.exe +# For windows: ffmpeg=C:\pyTivo\bin\ffmpeg.exe # For linux: ffmpeg=/usr/bin/ffmpeg -#ffmpeg=c:\Program Files\pyTivo\bin\ffmpeg.exe +#ffmpeg=C:\pyTivo\bin\ffmpeg.exe ffmpeg=/usr/bin/ffmpeg # Setting this to True will log more ouput for debugging purposes. @@ -73,11 +73,11 @@ ffmpeg=/usr/bin/ffmpeg #[_tivo_HD] # section for default video options applicable to all HD TiVos -# see pyTivo Web Configuration for all available settings +# see pyTivo Settings for all available settings #[_tivo_SD] # section for default video options applicable to all SD TiVos -# see pyTivo Web Configuration for all available settings +# see pyTivo Settings for all available settings [MyMovies] # Type can be 'video', 'music', or 'photo' diff --git a/templates/info_page.tmpl b/templates/info_page.tmpl index 67544a72..18609626 100644 --- a/templates/info_page.tmpl +++ b/templates/info_page.tmpl @@ -7,12 +7,10 @@

pyTivo

-
+
$admin -

$togo -

$shares -

+
diff --git a/templates/root_container.tmpl b/templates/root_container.tmpl index 0316293d..cc40c585 100644 --- a/templates/root_container.tmpl +++ b/templates/root_container.tmpl @@ -8,9 +8,14 @@ #for $name, $details in $containers + #if $name in $renamed + #set $title = $renamed[name] + #else + #set $title = $name + #end if
- $escape($name) + $escape($title) $escape($details.content_type) x-container/folder
From c62a3b80bca9df6abb7e7bb257c871d65d7f7b7b Mon Sep 17 00:00:00 2001 From: Bruce Goheen Date: Sat, 7 Sep 2013 01:44:40 -0500 Subject: [PATCH 21/21] updated to latest wmcbrine commits --- README | 2 +- Zeroconf.py | 147 ++----- beacon.py | 28 +- config.py | 19 +- mind.py | 509 +++++++++++------------ plugins/music/music.py | 4 +- plugins/settings/buildhelp.py | 12 + plugins/settings/help.txt | 28 +- plugins/settings/settings.py | 74 ++-- plugins/settings/templates/settings.tmpl | 59 ++- plugins/togo/togo.py | 21 +- plugins/video/qtfaststart.py | 8 +- plugins/video/video.py | 2 +- plugins/webvideo/webvideo.py | 10 +- pyTivo.py | 7 +- 15 files changed, 458 insertions(+), 472 deletions(-) diff --git a/README b/README index e84af2ea..2dc6450b 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ OS = Anything that will run python and ffmpeg, which I think is anything. Known to work on Linux, Mac OS X and Windows. Python - http://www.python.org/download/ -- You need at least version 2.4 of python +- You need at least version 2.5 of python pywin32 (only to install as a service) - http://sourceforge.net/project/showfiles.php?group_id=78018&package_id=79063 diff --git a/Zeroconf.py b/Zeroconf.py index da7e693f..69341f8d 100644 --- a/Zeroconf.py +++ b/Zeroconf.py @@ -1,10 +1,8 @@ -""" Multicast DNS Service Discovery for Python, v0.12-wmcbrine - Copyright (C) 2003, Paul Scott-Murphy +""" Multicast DNS Service Discovery for Python, v0.13-wmcbrine + Copyright 2003 Paul Scott-Murphy, 2013 William McBrine This module provides a framework for the use of DNS Service Discovery - using IP multicast. It has been tested against the JRendezvous - implementation from StrangeBerry, - and against the mDNSResponder from Mac OS X 10.3.8. + using IP multicast. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -22,64 +20,10 @@ """ -"""0.12-wmcbrine update - see git for changes""" - -"""0.12 update - allow selection of binding interface - typo fix - Thanks A. M. Kuchlingi - removed all use of word 'Rendezvous' - this is an API change""" - -"""0.11 update - correction to comments for addListener method - support for new record types seen from OS X - - IPv6 address - - hostinfo - ignore unknown DNS record types - fixes to name decoding - works alongside other processes using port 5353 - (e.g. on Mac OS X) - tested against Mac OS X 10.3.2's mDNSResponder - corrections to removal of list entries for service browser""" - -"""0.10 update - Jonathon Paisley contributed these corrections: - always multicast replies, even when query is unicast - correct a pointer encoding problem - can now write records in any order - traceback shown on failure - better TXT record parsing - server is now separate from name - can cancel a service browser - - modified some unit tests to accommodate these changes""" - -"""0.09 update - remove all records on service unregistration - fix DOS security problem with readName""" - -"""0.08 update - changed licensing to LGPL""" - -"""0.07 update - faster shutdown on engine - pointer encoding of outgoing names - ServiceBrowser now works - new unit tests""" - -"""0.06 update - small improvements with unit tests - added defined exception types - new style objects - fixed hostname/interface problem - fixed socket timeout problem - fixed addServiceListener() typo bug - using select() for socket reads - tested on Debian unstable with Python 2.2.2""" - -"""0.05 update - ensure case insensitivty on domain names - support for unicast DNS queries""" - -"""0.04 update - added some unit tests - added __ne__ adjuncts where required - ensure names end in '.local.' - timeout on receiving socket for clean shutdown""" - -__author__ = "Paul Scott-Murphy" -__email__ = "paul at scott dash murphy dot com" -__version__ = "0.12-wmcbrine" +__author__ = 'Paul Scott-Murphy' +__maintainer__ = 'William McBrine ' +__version__ = '0.13-wmcbrine' +__license__ = 'LGPL' import time import struct @@ -231,17 +175,11 @@ def __ne__(self, other): def getClazz(self, clazz): """Class accessor""" - try: - return _CLASSES[clazz] - except: - return "?(%s)" % (clazz) + return _CLASSES.get(clazz, "?(%s)" % clazz) - def getType(self, type): + def getType(self, t): """Type accessor""" - try: - return _TYPES[type] - except: - return "?(%s)" % (type) + return _TYPES.get(t, "?(%s)" % t) def toString(self, hdr, other): """String representation with additional information""" @@ -462,7 +400,7 @@ def __init__(self, data): self.numAnswers = 0 self.numAuthorities = 0 self.numAdditionals = 0 - + self.readHeader() self.readQuestions() self.readOthers() @@ -483,7 +421,7 @@ def readQuestions(self): for i in xrange(self.numQuestions): name = self.readName() type, clazz = self.unpack('!HH') - + question = DNSQuestion(name, type, clazz) self.questions.append(question) @@ -539,7 +477,7 @@ def readOthers(self): if rec is not None: self.answers.append(rec) - + def isQuery(self): """Returns true if this is a query""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY @@ -548,10 +486,10 @@ def isResponse(self): """Returns true if this is a response""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - def readUTF(self, offset, len): + def readUTF(self, offset, length): """Reads a UTF-8 string of a given length from the packet""" - return unicode(self.data[offset:offset+len], 'utf-8', 'replace') - + return unicode(self.data[offset:offset+length], 'utf-8', 'replace') + def readName(self): """Reads a domain name from the packet""" result = '' @@ -560,18 +498,18 @@ def readName(self): first = off while True: - len = ord(self.data[off]) + length = ord(self.data[off]) off += 1 - if len == 0: + if length == 0: break - t = len & 0xC0 + t = length & 0xC0 if t == 0x00: - result = ''.join((result, self.readUTF(off, len) + '.')) - off += len + result = ''.join((result, self.readUTF(off, length) + '.')) + off += length elif t == 0xC0: if next < 0: next = off + 1 - off = ((len & 0x3F) << 8) | ord(self.data[off]) + off = ((length & 0x3F) << 8) | ord(self.data[off]) if off >= first: raise "Bad domain name (circular) at " + str(off) first = off @@ -588,7 +526,7 @@ def readName(self): class DNSOutgoing(object): """Object representation of an outgoing packet""" - + def __init__(self, flags, multicast=True): self.finished = False self.id = 0 @@ -597,7 +535,7 @@ def __init__(self, flags, multicast=True): self.names = {} self.data = [] self.size = 12 - + self.questions = [] self.answers = [] self.authorities = [] @@ -638,7 +576,7 @@ def insertShort(self, index, value): """Inserts an unsigned short in a certain position in the packet""" self.data.insert(index, struct.pack('!H', value)) self.size += 2 - + def writeShort(self, value): """Writes an unsigned short to the packet""" self.pack('!H', value) @@ -664,11 +602,16 @@ def writeUTF(self, s): def writeName(self, name): """Writes a domain name to the packet""" - try: + if name in self.names: # Find existing instance of this name in packet # index = self.names[name] - except KeyError: + + # An index was found, so write a pointer to it + # + self.writeByte((index >> 8) | 0xC0) + self.writeByte(index & 0xFF) + else: # No record of this name already, so write it # out as normal, recording the location of the name # for future pointers to it. @@ -680,12 +623,6 @@ def writeName(self, name): for part in parts: self.writeUTF(part) self.writeByte(0) - return - - # An index was found, so write a pointer to it - # - self.writeByte((index >> 8) | 0xC0) - self.writeByte(index) def writeQuestion(self, question): """Writes a question to the packet""" @@ -731,7 +668,7 @@ def packet(self): self.writeRecord(authority, 0) for additional in self.additionals: self.writeRecord(additional, 0) - + self.insertShort(0, len(self.additionals)) self.insertShort(0, len(self.authorities)) self.insertShort(0, len(self.answers)) @@ -746,7 +683,7 @@ def packet(self): class DNSCache(object): """A cache of DNS entries""" - + def __init__(self): self.cache = {} @@ -869,7 +806,7 @@ class Listener(object): It requires registration with an Engine object in order to have the read() method called when a socket is availble for reading.""" - + def __init__(self, zc): self.zc = zc self.zc.engine.addReader(self, self.zc.socket) @@ -1002,11 +939,11 @@ def run(self): if event is not None: event(self.zc) - + class ServiceInfo(object): """Service information""" - + def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): """Create a service description. @@ -1074,7 +1011,7 @@ def setText(self, text): index += 1 strs.append(text[index:index+length]) index += length - + for s in strs: try: key, value = s.split('=', 1) @@ -1095,7 +1032,7 @@ def setText(self, text): except: traceback.print_exc() self.properties = None - + def getType(self): """Type accessor""" return self.type @@ -1193,7 +1130,7 @@ def request(self, zc, timeout): result = True finally: zc.removeListener(self) - + return result def __eq__(self, other): @@ -1219,7 +1156,7 @@ def __repr__(self): result += self.text[:17] + "..." result += "]" return result - + class Zeroconf(object): """Implementation of Zeroconf Multicast DNS Service Discovery @@ -1278,7 +1215,7 @@ def __init__(self, bindaddress=None): self.cache = DNSCache() self.condition = threading.Condition() - + self.engine = Engine(self) self.listener = Listener(self) self.reaper = Reaper(self) diff --git a/beacon.py b/beacon.py index 7d45b5a3..05af377e 100644 --- a/beacon.py +++ b/beacon.py @@ -1,8 +1,8 @@ import logging import re +import socket import struct import time -from socket import * from threading import Timer from urllib import quote @@ -34,11 +34,14 @@ def __init__(self, logger): self.rz = Zeroconf.Zeroconf() self.renamed = {} old_titles = self.scan() - address = inet_aton(config.get_ip()) + address = socket.inet_aton(config.get_ip()) port = int(config.getPort()) logger.info('Announcing shares...') for section, settings in config.getShares(): - ct = GetPlugin(settings['type']).CONTENT_TYPE + try: + ct = GetPlugin(settings['type']).CONTENT_TYPE + except: + continue if ct.startswith('x-container/'): if 'video' in ct: platform = PLATFORM_VIDEO @@ -79,7 +82,7 @@ def scan(self): info = self.rz.getServiceInfo(VIDS, name + '.' + VIDS) if info and 'TSN' in info.properties: tsn = info.properties['TSN'] - address = inet_ntoa(info.getAddress()) + address = socket.inet_ntoa(info.getAddress()) config.tivos[tsn] = address self.logger.info(name) config.tivo_names[tsn] = name @@ -94,13 +97,16 @@ def shutdown(self): class Beacon: def __init__(self): - self.UDPSock = socket(AF_INET, SOCK_DGRAM) - self.UDPSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + self.UDPSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.UDPSock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.services = [] self.platform = PLATFORM_VIDEO for section, settings in config.getShares(): - ct = GetPlugin(settings['type']).CONTENT_TYPE + try: + ct = GetPlugin(settings['type']).CONTENT_TYPE + except: + continue if ct in ('x-container/tivo-music', 'x-container/tivo-photos'): self.platform = PLATFORM_MAIN break @@ -125,8 +131,8 @@ def format_services(self): def format_beacon(self, conntype, services=True): beacon = ['tivoconnect=1', 'method=%s' % conntype, - 'identity=%s' % config.getGUID(), - 'machine=%s' % gethostname(), + 'identity={%s}' % config.getGUID(), + 'machine=%s' % socket.gethostname(), 'platform=%s' % self.platform] if services: @@ -182,7 +188,7 @@ def listen(self): import thread def server(): - TCPSock = socket(AF_INET, SOCK_STREAM) + TCPSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) TCPSock.bind(('', 2190)) TCPSock.listen(5) @@ -206,7 +212,7 @@ def get_name(self, address): machine_name = re.compile('machine=(.*)\n').search try: - tsock = socket() + tsock = socket.socket() tsock.connect((address, 2190)) self.send_packet(tsock, our_beacon) tivo_beacon = self.recv_packet(tsock) diff --git a/config.py b/config.py index fbc94228..3f01469b 100644 --- a/config.py +++ b/config.py @@ -4,12 +4,15 @@ import logging.config import os import re -import random import socket -import string import sys +import uuid from ConfigParser import NoOptionError +class Bdict(dict): + def getboolean(self, x): + return self.get(x, 'False').lower() in ('1', 'yes', 'true', 'on') + def init(argv): global tivos global tivo_names @@ -18,7 +21,7 @@ def init(argv): tivos = {} tivo_names = {} - guid = ''.join([random.choice(string.ascii_letters) for i in range(10)]) + guid = uuid.uuid4() p = os.path.dirname(__file__) config_files = ['/etc/pyTivo.conf', os.path.join(p, 'pyTivo.conf')] @@ -82,7 +85,7 @@ def get_server(name, default=None): return default def getGUID(): - return guid + return str(guid) def get_ip(tsn=None): dest_ip = tivos.get(tsn, '4.2.2.1') @@ -161,12 +164,10 @@ def isTsnInConfig(tsn): return ('_tivo_' + tsn) in config.sections() def getShares(tsn=''): - shares = [(section, dict(config.items(section))) + shares = [(section, Bdict(config.items(section))) for section in config.sections() - if not (section.startswith('_tivo_') - or section.startswith('logger_') - or section.startswith('handler_') - or section.startswith('formatter_') + if not (section.startswith(('_tivo_', 'logger_', 'handler_', + 'formatter_')) or section in ('Server', 'loggers', 'handlers', 'formatters') ) diff --git a/mind.py b/mind.py index 5a0784be..3bef95b2 100644 --- a/mind.py +++ b/mind.py @@ -5,300 +5,283 @@ import urllib2 import urllib import warnings +import xml.etree.ElementTree as ElementTree import config import metadata -try: - import xml.etree.ElementTree as ElementTree -except ImportError: - try: - import elementtree.ElementTree as ElementTree - except ImportError: - warnings.warn('Python 2.5 or higher or elementtree is ' + - 'needed to use the TivoPush') - -if 'ElementTree' not in locals(): - - class Mind: - def __init__(self, *arg, **karg): - raise Exception('Python 2.5 or higher or elementtree is ' + - 'needed to use the TivoPush') - -else: - - class Mind: - def __init__(self, username, password, tsn): - self.__logger = logging.getLogger('pyTivo.mind') - self.__username = username - self.__password = password - self.__mind = config.get_mind(tsn) - - cj = cookielib.CookieJar() - cp = urllib2.HTTPCookieProcessor(cj) - self.__opener = urllib2.build_opener(cp) - - self.__login() - - def pushVideo(self, tsn, url, description, duration, size, - title, subtitle, source='', mime='video/mpeg', - tvrating=None): - # It looks like tivo only supports one pc per house - pc_body_id = self.__pcBodySearch() - - if not source: - source = title - - data = { - 'bodyId': 'tsn:' + tsn, - 'description': description, - 'duration': duration, - 'partnerId': 'tivo:pt.3187', - 'pcBodyId': pc_body_id, - 'publishDate': time.strftime('%Y-%m-%d %H:%M%S', time.gmtime()), - 'size': size, - 'source': source, - 'state': 'complete', - 'title': title - } - - rating = metadata.get_tv(tvrating) - if rating: - data['tvRating'] = rating.lower() - +class Mind: + def __init__(self, username, password, tsn): + self.__logger = logging.getLogger('pyTivo.mind') + self.__username = username + self.__password = password + self.__mind = config.get_mind(tsn) + + cj = cookielib.CookieJar() + cp = urllib2.HTTPCookieProcessor(cj) + self.__opener = urllib2.build_opener(cp) + + self.__login() + + def pushVideo(self, tsn, url, description, duration, size, + title, subtitle, source='', mime='video/mpeg', + tvrating=None): + # It looks like tivo only supports one pc per house + pc_body_id = self.__pcBodySearch() + + if not source: + source = title + + data = { + 'bodyId': 'tsn:' + tsn, + 'description': description, + 'duration': duration, + 'partnerId': 'tivo:pt.3187', + 'pcBodyId': pc_body_id, + 'publishDate': time.strftime('%Y-%m-%d %H:%M%S', time.gmtime()), + 'size': size, + 'source': source, + 'state': 'complete', + 'title': title + } + + rating = metadata.get_tv(tvrating) + if rating: + data['tvRating'] = rating.lower() + + mtypes = {'video/mp4': 'avcL41MP4', 'video/bif': 'vc1ApL3'} + data['encodingType'] = mtypes.get(mime, 'mpeg2ProgramStream') + + data['url'] = url + '?Format=' + mime + + if subtitle: + data['subtitle'] = subtitle + + offer_id, content_id = self.__bodyOfferModify(data) + self.__subscribe(offer_id, content_id, tsn) + + def getDownloadRequests(self): + NEEDED_VALUES = [ + 'bodyId', + 'bodyOfferId', + 'description', + 'partnerId', + 'pcBodyId', + 'publishDate', + 'source', + 'state', + 'subscriptionId', + 'subtitle', + 'title', + 'url' + ] + + # It looks like tivo only supports one pc per house + pc_body_id = self.__pcBodySearch() + + requests = [] + offer_list = self.__bodyOfferSchedule(pc_body_id) + + for offer in offer_list.findall('bodyOffer'): + d = {} + if offer.findtext('state') != 'scheduled': + continue + + for n in NEEDED_VALUES: + d[n] = offer.findtext(n) + requests.append(d) + + return requests + + def completeDownloadRequest(self, request, status, mime='video/mpeg'): + if status: mtypes = {'video/mp4': 'avcL41MP4', 'video/bif': 'vc1ApL3'} - data['encodingType'] = mtypes.get(mime, 'mpeg2ProgramStream') - - data['url'] = url + '?Format=' + mime - - if subtitle: - data['subtitle'] = subtitle - - offer_id, content_id = self.__bodyOfferModify(data) - self.__subscribe(offer_id, content_id, tsn) - - def getDownloadRequests(self): - NEEDED_VALUES = [ - 'bodyId', - 'bodyOfferId', - 'description', - 'partnerId', - 'pcBodyId', - 'publishDate', - 'source', - 'state', - 'subscriptionId', - 'subtitle', - 'title', - 'url' - ] - - # It looks like tivo only supports one pc per house - pc_body_id = self.__pcBodySearch() - - requests = [] - offer_list = self.__bodyOfferSchedule(pc_body_id) - - for offer in offer_list.findall('bodyOffer'): - d = {} - if offer.findtext('state') != 'scheduled': - continue - - for n in NEEDED_VALUES: - d[n] = offer.findtext(n) - requests.append(d) - - return requests - - def completeDownloadRequest(self, request, status, mime='video/mpeg'): - if status: - mtypes = {'video/mp4': 'avcL41MP4', 'video/bif': 'vc1ApL3'} - request['encodingType'] = mtypes.get(mime, 'mpeg2ProgramStream') - request['url'] += '?Format=' + mime - request['state'] = 'complete' - else: - request['state'] = 'cancelled' - request['cancellationReason'] = 'httpFileNotFound' - request['type'] = 'bodyOfferModify' - request['updateDate'] = time.strftime('%Y-%m-%d %H:%M%S', - time.gmtime()) - - offer_id, content_id = self.__bodyOfferModify(request) - if status: - self.__subscribe(offer_id, content_id, request['bodyId'][4:]) - - def getXMPPLoginInfo(self): - # It looks like tivo only supports one pc per house - pc_body_id = self.__pcBodySearch() - - xml = self.__bodyXmppInfoGet(pc_body_id) - - results = { - 'server': xml.findtext('server'), - 'port': int(xml.findtext('port')), - 'username': xml.findtext('xmppId') - } - - for sendPresence in xml.findall('sendPresence'): - results.setdefault('presence_list',[]).append(sendPresence.text) - - return results - - def __login(self): - - data = { - 'cams_security_domain': 'tivocom', - 'cams_login_config': 'http', - 'cams_cb_username': self.__username, - 'cams_cb_password': self.__password, - 'cams_original_url': '/mind/mind7?type=infoGet' - } - - r = urllib2.Request( - 'https://%s/mind/login' % self.__mind, - urllib.urlencode(data) - ) - try: - result = self.__opener.open(r) - except: - pass - - self.__logger.debug('__login\n%s' % (data)) - - def __dict_request(self, data, req): - r = urllib2.Request( - 'https://%s/mind/mind7?type=%s' % (self.__mind, req), - dictcode(data), - {'Content-Type': 'x-tivo/dict-binary'} - ) + request['encodingType'] = mtypes.get(mime, 'mpeg2ProgramStream') + request['url'] += '?Format=' + mime + request['state'] = 'complete' + else: + request['state'] = 'cancelled' + request['cancellationReason'] = 'httpFileNotFound' + request['type'] = 'bodyOfferModify' + request['updateDate'] = time.strftime('%Y-%m-%d %H:%M%S', + time.gmtime()) + + offer_id, content_id = self.__bodyOfferModify(request) + if status: + self.__subscribe(offer_id, content_id, request['bodyId'][4:]) + + def getXMPPLoginInfo(self): + # It looks like tivo only supports one pc per house + pc_body_id = self.__pcBodySearch() + + xml = self.__bodyXmppInfoGet(pc_body_id) + + results = { + 'server': xml.findtext('server'), + 'port': int(xml.findtext('port')), + 'username': xml.findtext('xmppId') + } + + for sendPresence in xml.findall('sendPresence'): + results.setdefault('presence_list',[]).append(sendPresence.text) + + return results + + def __login(self): + + data = { + 'cams_security_domain': 'tivocom', + 'cams_login_config': 'http', + 'cams_cb_username': self.__username, + 'cams_cb_password': self.__password, + 'cams_original_url': '/mind/mind7?type=infoGet' + } + + r = urllib2.Request( + 'https://%s/mind/login' % self.__mind, + urllib.urlencode(data) + ) + try: result = self.__opener.open(r) - - xml = ElementTree.parse(result).find('.') - - self.__logger.debug('%s\n%s\n\n%sg' % (req, data, - ElementTree.tostring(xml))) - return xml - - def __bodyOfferModify(self, data): - """Create an offer""" - - xml = self.__dict_request(data, 'bodyOfferModify&bodyId=' + - data['bodyId']) - - offer_id = xml.findtext('offerId') - if offer_id: - content_id = offer_id.replace('of','ct') - - return offer_id, content_id - else: - raise Exception(ElementTree.tostring(xml)) - - def __subscribe(self, offer_id, content_id, tsn): - """Push the offer to the tivo""" - data = { - 'bodyId': 'tsn:' + tsn, - 'idSetSource': { - 'contentId': content_id, - 'offerId': offer_id, - 'type': 'singleOfferSource' - }, - 'title': 'pcBodySubscription', - 'uiType': 'cds' - } - - return self.__dict_request(data, 'subscribe&bodyId=tsn:' + tsn) - - def __bodyOfferSchedule(self, pc_body_id): - """Get pending stuff for this pc""" - - data = {'pcBodyId': pc_body_id} - return self.__dict_request(data, 'bodyOfferSchedule') - - def __pcBodySearch(self): - """Find PCS""" - - xml = self.__dict_request({}, 'pcBodySearch') + except: + pass + + self.__logger.debug('__login\n%s' % (data)) + + def __dict_request(self, data, req): + r = urllib2.Request( + 'https://%s/mind/mind7?type=%s' % (self.__mind, req), + dictcode(data), + {'Content-Type': 'x-tivo/dict-binary'} + ) + result = self.__opener.open(r) + + xml = ElementTree.parse(result).find('.') + + self.__logger.debug('%s\n%s\n\n%sg' % (req, data, + ElementTree.tostring(xml))) + return xml + + def __bodyOfferModify(self, data): + """Create an offer""" + + xml = self.__dict_request(data, 'bodyOfferModify&bodyId=' + + data['bodyId']) + + offer_id = xml.findtext('offerId') + if offer_id: + content_id = offer_id.replace('of','ct') + + return offer_id, content_id + else: + raise Exception(ElementTree.tostring(xml)) + + def __subscribe(self, offer_id, content_id, tsn): + """Push the offer to the tivo""" + data = { + 'bodyId': 'tsn:' + tsn, + 'idSetSource': { + 'contentId': content_id, + 'offerId': offer_id, + 'type': 'singleOfferSource' + }, + 'title': 'pcBodySubscription', + 'uiType': 'cds' + } + + return self.__dict_request(data, 'subscribe&bodyId=tsn:' + tsn) + + def __bodyOfferSchedule(self, pc_body_id): + """Get pending stuff for this pc""" + + data = {'pcBodyId': pc_body_id} + return self.__dict_request(data, 'bodyOfferSchedule') + + def __pcBodySearch(self): + """Find PCS""" + + xml = self.__dict_request({}, 'pcBodySearch') + id = xml.findtext('.//pcBodyId') + if not id: + xml = self.__pcBodyStore('pyTivo', True) id = xml.findtext('.//pcBodyId') - if not id: - xml = self.__pcBodyStore('pyTivo', True) - id = xml.findtext('.//pcBodyId') - return id + return id - def __collectionIdSearch(self, url): - """Find collection ids""" + def __collectionIdSearch(self, url): + """Find collection ids""" - xml = self.__dict_request({'url': url}, 'collectionIdSearch') - return xml.findtext('collectionId') + xml = self.__dict_request({'url': url}, 'collectionIdSearch') + return xml.findtext('collectionId') - def __pcBodyStore(self, name, replace=False): - """Setup a new PC""" + def __pcBodyStore(self, name, replace=False): + """Setup a new PC""" - data = { - 'name': name, - 'replaceExisting': str(replace).lower() - } + data = { + 'name': name, + 'replaceExisting': str(replace).lower() + } - return self.__dict_request(data, 'pcBodyStore') + return self.__dict_request(data, 'pcBodyStore') - def __bodyXmppInfoGet(self, body_id): + def __bodyXmppInfoGet(self, body_id): - return self.__dict_request({'bodyId': body_id}, - 'bodyXmppInfoGet&bodyId=' + body_id) + return self.__dict_request({'bodyId': body_id}, + 'bodyXmppInfoGet&bodyId=' + body_id) - def dictcode(d): - """Helper to create x-tivo/dict-binary""" - output = [] +def dictcode(d): + """Helper to create x-tivo/dict-binary""" + output = [] - keys = [str(k) for k in d] - keys.sort() + keys = [str(k) for k in d] + keys.sort() - for k in keys: - v = d[k] + for k in keys: + v = d[k] - output.append( varint( len(k) ) ) - output.append( k ) + output.append( varint( len(k) ) ) + output.append( k ) - if isinstance(v, dict): - output.append( chr(2) ) - output.append( dictcode(v) ) + if isinstance(v, dict): + output.append( chr(2) ) + output.append( dictcode(v) ) - else: - if type(v) == str: - try: - v = v.decode('utf8') - except: - if sys.platform == 'darwin': - v = v.decode('macroman') - else: - v = v.decode('iso8859-1') - elif type(v) != unicode: - v = str(v) - v = v.encode('utf-8') - output.append( chr(1) ) - output.append( varint( len(v) ) ) - output.append( v ) + else: + if type(v) == str: + try: + v = v.decode('utf8') + except: + if sys.platform == 'darwin': + v = v.decode('macroman') + else: + v = v.decode('iso8859-1') + elif type(v) != unicode: + v = str(v) + v = v.encode('utf-8') + output.append( chr(1) ) + output.append( varint( len(v) ) ) + output.append( v ) - output.append( chr(0) ) + output.append( chr(0) ) - output.append( chr(0x80) ) + output.append( chr(0x80) ) - return ''.join(output) + return ''.join(output) - def varint(i): - output = [] - while i > 0x7f: - output.append( chr(i & 0x7f) ) - i >>= 7 - output.append( chr(i | 0x80) ) - return ''.join(output) +def varint(i): + output = [] + while i > 0x7f: + output.append( chr(i & 0x7f) ) + i >>= 7 + output.append( chr(i | 0x80) ) + return ''.join(output) def getMind(tsn=None): username = config.get_tsn('tivo_username', tsn) password = config.get_tsn('tivo_password', tsn) if not username or not password: - raise Exception("tivo_username and tivo_password required") + raise Exception("tivo_username and tivo_password required") return Mind(username, password, tsn) diff --git a/plugins/music/music.py b/plugins/music/music.py index 6bfef08a..d1ab376a 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -95,8 +95,8 @@ class Music(Plugin): def send_file(self, handler, path, query): seek = int(query.get('Seek', [0])[0]) duration = int(query.get('Duration', [0])[0]) - always = (handler.container.get('force_ffmpeg', - 'False').lower() == 'true' and config.get_bin('ffmpeg')) + always = (handler.container.getboolean('force_ffmpeg') and + config.get_bin('ffmpeg')) fname = unicode(path, 'utf-8') ext = os.path.splitext(fname)[1].lower() diff --git a/plugins/settings/buildhelp.py b/plugins/settings/buildhelp.py index 56b2d394..95b5fece 100644 --- a/plugins/settings/buildhelp.py +++ b/plugins/settings/buildhelp.py @@ -6,6 +6,9 @@ help_list = {} title = '' settings_known = {} +mode = {} +options = {} +default = {} titlemode = True f = open(os.path.join(SCRIPTDIR, 'help.txt')) try: @@ -33,11 +36,20 @@ if section not in settings_known: settings_known[section] = [] settings_known[section].append(title) + elif value.lower() == 'mode': + mode[title] = data + elif value.lower() == 'options': + options[title] = data.split('/') else: help_list[title].append(line) + if value.lower() == 'default setting': + default[title] = data finally: f.close() ## Done building help list +plugins = [p for p in os.listdir(os.path.dirname(SCRIPTDIR)) + if not p.startswith(('__init__', 'togo', 'settings'))] +options['type'] = plugins def gethelp(): return help_list diff --git a/plugins/settings/help.txt b/plugins/settings/help.txt index 12c54142..ce94b2c6 100644 --- a/plugins/settings/help.txt +++ b/plugins/settings/help.txt @@ -125,6 +125,7 @@ Available In: Server debug +Mode: checkbox Default Setting: False Valid Entries: True/False Required: No @@ -134,6 +135,7 @@ Available In: Server type +Mode: select Default Setting: None Valid Entries: video, music, photo, or any other valid plugin name. Required: Yes @@ -155,6 +157,7 @@ Available In: Shares force_alpha +Mode: checkbox Default Setting: False Valid Entries: True/False Required: No @@ -168,8 +171,24 @@ alpha-sorted, you need this option. Example Settings: True/False Available In: Shares +force_ffmpeg + +Mode: checkbox +Default Setting: False +Valid Entries: True/False +Required: No +Description: Only meaningful in shares of type "music". When false, +pyTivo will pass through TiVo-compatible MP3 files as-is (unless you +seek within them). When true, even these files will be processed by +FFmpeg, in order to strip out album artwork that the TiVo would +otherwise try to play as sound, producing a squeal. This is done with +the "copy" codec, so it's low-overhead. +Example Settings: True/False +Available In: Shares + optres +Mode: checkbox Default Setting: False Valid Entries: True/False Required: No @@ -363,7 +382,7 @@ unless you wish to disable it. pyTivo defaults to True except when pyTivo uses acodec copy, in which case copyts is not needed, and a conflict could also occur if the source file has really corrupt sections. -Example Settings: True, False +Example Settings: True/False Available In: Tivos, HD_tivos, SD_tivos ffmpeg_pram @@ -490,8 +509,10 @@ Available In: Server zeroconf +Mode: select +Options: Auto/On/Off Default Setting: Auto -Valid Entries: True/False/Auto +Valid Entries: On/Off/Auto Required: No Description: Controls whether or not new-style, zeroconf-based beacons are used. The default is to use them, unless there's a "_tivo_" section @@ -499,11 +520,12 @@ with "shares" defined. The zeroconf beacons bypass the usual mechanism whereby only the allowed shares are announced to specific TiVos; the contents of the shares will still not appear on unauthorized TiVos, but the names will. -Example Settings: True/False/Auto +Example Settings: On/Off/Auto Available In: Server nosettings +Mode: checkbox Default Setting: False Valid Entries: True/False Required: No diff --git a/plugins/settings/settings.py b/plugins/settings/settings.py index 0cf7b4e9..a520609e 100644 --- a/plugins/settings/settings.py +++ b/plugins/settings/settings.py @@ -39,6 +39,7 @@ def Quit(self, handler, query): handler.server.stop = True else: handler.server.shutdown() + handler.server.socket.close() else: handler.send_error(501) @@ -50,6 +51,7 @@ def Restart(self, handler, query): handler.server.stop = True else: handler.server.shutdown() + handler.server.socket.close() else: handler.send_error(501) @@ -65,8 +67,7 @@ def Settings(self, handler, query): shares_data = [] for section in config.config.sections(): - if not (section.startswith('_tivo_') - or section.startswith('Server')): + if not section.startswith(('_tivo_', 'Server')): if (not (config.config.has_option(section, 'type')) or config.config.get(section, 'type').lower() not in ['settings', 'togo']): @@ -75,6 +76,8 @@ def Settings(self, handler, query): raw=True)))) t = Template(SETTINGS_TEMPLATE, filter=EncodeUnicode) + t.mode = buildhelp.mode + t.options = buildhelp.options t.container = handler.cname t.quote = quote t.server_data = dict(config.config.items('Server', raw=True)) @@ -88,40 +91,40 @@ def Settings(self, handler, query): t.tivos_data = [(section, dict(config.config.items(section, raw=True))) for section in config.config.sections() if section.startswith('_tivo_') - and not section.startswith('_tivo_SD') - and not section.startswith('_tivo_HD')] + and not section.startswith(('_tivo_SD', '_tivo_HD'))] t.tivos_known = buildhelp.getknown('tivos') t.help_list = buildhelp.gethelp() t.has_shutdown = hasattr(handler.server, 'shutdown') handler.send_html(str(t)) + def each_section(self, query, label, section): + new_setting = new_value = ' ' + if config.config.has_section(section): + config.config.remove_section(section) + config.config.add_section(section) + for key, value in query.items(): + key = key.replace('opts.', '', 1) + if key.startswith(label + '.'): + _, option = key.split('.') + default = buildhelp.default.get(option, ' ') + value = value[0] + if not config.config.has_section(section): + config.config.add_section(section) + if option == 'new__setting': + new_setting = value + elif option == 'new__value': + new_value = value + elif value not in (' ', default): + config.config.set(section, option, value) + if not(new_setting == ' ' and new_value == ' '): + config.config.set(section, new_setting, new_value) + def UpdateSettings(self, handler, query): config.reset() for section in ['Server', '_tivo_SD', '_tivo_HD']: - new_setting = new_value = ' ' - for key in query: - if key.startswith('opts.'): - data = query[key] - del query[key] - key = key[5:] - query[key] = data - if key.startswith(section + '.'): - _, option = key.split('.') - if not config.config.has_section(section): - config.config.add_section(section) - if option == 'new__setting': - new_setting = query[key][0] - elif option == 'new__value': - new_value = query[key][0] - elif query[key][0] == ' ': - config.config.remove_option(section, option) - else: - config.config.set(section, option, query[key][0]) - if not(new_setting == ' ' and new_value == ' '): - config.config.set(section, new_setting, new_value) - - sections = query['Section_Map'][0].split(']') - sections.pop() # last item is junk + self.each_section(query, section, section) + + sections = query['Section_Map'][0].split(']')[:-1] for section in sections: ID, name = section.split('|') if query[ID][0] == 'Delete_Me': @@ -130,19 +133,8 @@ def UpdateSettings(self, handler, query): if query[ID][0] != name: config.config.remove_section(name) config.config.add_section(query[ID][0]) - for key in query: - if key.startswith(ID + '.'): - _, option = key.split('.') - if option == 'new__setting': - new_setting = query[key][0] - elif option == 'new__value': - new_value = query[key][0] - elif query[key][0] == ' ': - config.config.remove_option(query[ID][0], option) - else: - config.config.set(query[ID][0], option, query[key][0]) - if not(new_setting == ' ' and new_value == ' '): - config.config.set(query[ID][0], new_setting, new_value) + self.each_section(query, ID, query[ID][0]) + if query['new_Section'][0] != ' ': config.config.add_section(query['new_Section'][0]) config.write() diff --git a/plugins/settings/templates/settings.tmpl b/plugins/settings/templates/settings.tmpl index 8a8250d4..430c7e6d 100644 --- a/plugins/settings/templates/settings.tmpl +++ b/plugins/settings/templates/settings.tmpl @@ -11,17 +11,36 @@ #def row($i, $key, $section, $source) #set $j = $i%2 -
+ - #end def @@ -30,9 +49,24 @@
$video.title $video.total_items Items$video.textDate$video.title$video.total_items Items$video.textDate @@ -77,9 +61,8 @@ function toggle(source) { #end if $video.textSize - $video.textDate$video.textSize$video.textDate
$key: - #if $key in $source - value="$source[$key]" + #set $value=$source[$key] #else - value="" + #set $value='' + #end if + #if $mode.get($key, '') == 'select' + + #elif $mode.get($key, '') == 'checkbox' + + #else + #end if - onChange="saveNotify();" onfocus="switchDiv('help-$key', 'help-'); - return true;" type="text">
#set $i = 0 - #for $key in $source_known - #set $i += 1 - $row($i, $key, $section, $source_data) + #set $source_select = [] + #set $source_text = [] + #set $source_checkbox = [] + #for $k in $source_known + #set $m = $mode.get($k, 'text') + #if $m == 'select' + #set $source_select += [$k] + #elif $m == 'checkbox' + #set $source_checkbox += [$k] + #else + #set $source_text += [$k] + #end if + #end for + #for $l in [$source_select, $source_text, $source_checkbox] + #for $key in $l + #set $i += 1 + $row($i, $key, $section, $source_data) + #end for #end for
User Defined @@ -76,11 +110,10 @@ #end def

-pyTivo Settings +pyTivo / Settings help

-

Home

- +
diff --git a/plugins/togo/togo.py b/plugins/togo/togo.py index ea7c163d..6c7339e7 100644 --- a/plugins/togo/togo.py +++ b/plugins/togo/togo.py @@ -84,9 +84,18 @@ def tivo_open(self, url): raise def NPL(self, handler, query): + + def getint(thing): + try: + result = int(thing) + except: + result = 0 + return result + global basic_meta shows_per_page = 50 # Change this to alter the number of shows returned folder = '' + FirstAnchor = '' has_tivodecode = bool(config.get_bin('tivodecode')) useragent = handler.headers.getheader('User-Agent', '') @@ -124,8 +133,9 @@ def NPL(self, handler, query): TotalItems = tag_data(xmldoc, 'TiVoContainer/Details/TotalItems') ItemStart = tag_data(xmldoc, 'TiVoContainer/ItemStart') ItemCount = tag_data(xmldoc, 'TiVoContainer/ItemCount') - FirstAnchor = tag_data(items[0], 'Links/Content/Url') title = tag_data(xmldoc, 'TiVoContainer/Details/Title') + if items: + FirstAnchor = tag_data(items[0], 'Links/Content/Url') data = [] for item in items: @@ -157,7 +167,7 @@ def NPL(self, handler, query): rawsize = entry['SourceSize'] entry['SourceSize'] = metadata.human_size(rawsize) - dur = int(entry['Duration']) / 1000 + dur = getint(entry['Duration']) / 1000 entry['Duration'] = ( '%d:%02d:%02d' % (dur / 3600, (dur % 3600) / 60, dur % 60) ) @@ -179,7 +189,6 @@ def NPL(self, handler, query): TotalItems = 0 ItemStart = 0 ItemCount = 0 - FirstAnchor = '' title = '' if useragent.lower().find('mobile') > 0: @@ -198,9 +207,9 @@ def NPL(self, handler, query): t.container = handler.cname t.data = data t.len = len - t.TotalItems = int(TotalItems) - t.ItemStart = int(ItemStart) - t.ItemCount = int(ItemCount) + t.TotalItems = getint(TotalItems) + t.ItemStart = getint(ItemStart) + t.ItemCount = getint(ItemCount) t.FirstAnchor = quote(FirstAnchor) t.shows_per_page = shows_per_page t.title = title diff --git a/plugins/video/qtfaststart.py b/plugins/video/qtfaststart.py index 257a997e..a5ef3e36 100644 --- a/plugins/video/qtfaststart.py +++ b/plugins/video/qtfaststart.py @@ -63,13 +63,13 @@ """ import logging +import os import struct from StringIO import StringIO VERSION = "1.7wjm3" CHUNK_SIZE = 8192 -SEEK_CUR = 1 # Not defined in Python 2.4, so we define it here -- WJM3 log = logging.getLogger('pyTivo.video.qt-faststart') @@ -128,7 +128,7 @@ def get_index(datastream): # Weird, but just continue to try to find more atoms atom_size = skip - datastream.seek(atom_size - skip, SEEK_CUR) + datastream.seek(atom_size - skip, os.SEEK_CUR) # Make sure the atoms we need exist top_level_atoms = set([item[0] for item in index]) @@ -167,7 +167,7 @@ def find_atoms(size, datastream): yield atom_type else: # Ignore this atom, seek to the end of it. - datastream.seek(atom_size - 8, SEEK_CUR) + datastream.seek(atom_size - 8, os.SEEK_CUR) def output(outfile, skip, data): global count @@ -248,7 +248,7 @@ def process(datastream, outfile, skip=0): moov.read(csize * entry_count)) # Patch and write entries - moov.seek(-csize * entry_count, SEEK_CUR) + moov.seek(-csize * entry_count, os.SEEK_CUR) moov.write(struct.pack(">" + ctype * entry_count, *[entry + offset for entry in entries])) diff --git a/plugins/video/video.py b/plugins/video/video.py index 472949c6..fb98e92f 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -407,7 +407,7 @@ def QueryContainer(self, handler, query): return container = handler.container - force_alpha = container.get('force_alpha', 'False').lower() == 'true' + force_alpha = container.getboolean('force_alpha') use_html = query.get('Format', [''])[0].lower() == 'text/html' files, total, start = self.get_files(handler, query, diff --git a/plugins/webvideo/webvideo.py b/plugins/webvideo/webvideo.py index 223bd9ef..46624e0b 100644 --- a/plugins/webvideo/webvideo.py +++ b/plugins/webvideo/webvideo.py @@ -7,15 +7,7 @@ import urllib import urllib2 import warnings - -try: - import xml.etree.ElementTree as ElementTree -except ImportError: - try: - import elementtree.ElementTree as ElementTree - except ImportError: - warnings.warn('Python 2.5 or higher or elementtree is ' + - 'needed to use the TivoPush') +import xml.etree.ElementTree as ElementTree import xmpp import mind diff --git a/pyTivo.py b/pyTivo.py index 38a07145..a931fbc4 100755 --- a/pyTivo.py +++ b/pyTivo.py @@ -6,14 +6,13 @@ import sys import time -if sys.version_info[0] != 2 or sys.version_info[1] < 4: - print ('ERROR: pyTivo requires Python >= 2.4, < 3.0.\n') +if sys.version_info[0] != 2 or sys.version_info[1] < 5: + print ('ERROR: pyTivo requires Python >= 2.5, < 3.0.\n') sys.exit(1) import beacon import config import httpserver -from plugin import GetPlugin def exceptionLogger(*args): sys.excepthook = sys.__excepthook__ @@ -77,4 +76,4 @@ def mainloop(): if __name__ == '__main__': while mainloop(): - time.sleep(5) + time.sleep(5)