diff --git a/data/css/browser.css b/data/css/browser.css new file mode 100644 index 0000000000000000000000000000000000000000..42b6a387797b97bcc822fb6aae4b557c83deb937 --- /dev/null +++ b/data/css/browser.css @@ -0,0 +1,54 @@ +#fileBrowserDialog { + max-height: 480px; + overflow-y: auto; +} +#fileBrowserDialog ul { + margin: 0; + padding: 0; +} +#fileBrowserDialog ul li { + margin: 2px 0; + cursor: pointer; + list-style-type: none; +} +#fileBrowserDialog ul li a { + display: block; + padding: 4px 0; +} +#fileBrowserDialog ul li a:hover { + color: blue; + background: none; +} +#fileBrowserDialog ul li a span.ui-icon { + margin: 0 4px; + float: left; +} +/* +.browserDialog.busy .ui-dialog-buttonpane { + background: url("/images/loading.gif") 10px 50% no-repeat; +} +*/ + +/* jquery ui autocomplete overrides to make it look more like the old autocomplete */ +.ui-autocomplete { + max-height: 180px; + overflow-y: auto; + /* prevent horizontal scrollbar */ + overflow-x: hidden; + /* add padding to account for vertical scrollbar */ + padding-right: 20px; +} +* html .ui-autocomplete { + height: 180px; +} +.ui-menu .ui-menu-item { + background-color: #eeeeee; +} +.ui-menu .ui-menu-item-alternate{ + background-color: #ffffff; +} +.ui-menu a.ui-state-hover{ + background: none; + background-color: #0A246A; + color: #ffffff; +} diff --git a/data/css/comingEpisodes.css b/data/css/comingEpisodes.css new file mode 100644 index 0000000000000000000000000000000000000000..4c747c8f56d6fdd1a02c9e1844eefa6a71879964 --- /dev/null +++ b/data/css/comingEpisodes.css @@ -0,0 +1,222 @@ +.tvshowDiv { + display: block; + clear: both; + border-left: 1px solid #CCCCCC; + border-right: 1px solid #CCCCCC; + border-bottom: 1px solid #CCCCCC; + margin: auto; + padding: 0px; + text-align: left; + width: 750px; +} + +.tvshowDiv a, .tvshowDiv a:link, .tvshowDiv a:visited, .tvshowDiv a:hover { + text-decoration: none; + background: none; +} + +.tvshowTitle a { + color: #000000; + float: left; + padding-top: 3px; + line-height: 1.2em; + font-size: 1.1em; + text-shadow: -1px -1px 0 #FFF); +} + +.tvshowTitleIcons { + float: right; + padding: 3px 5px; +} + +.tvshowDiv .title { + font-weight: 900; + color: #333; +} +.imgWrapper { + background: url("../images/loading.gif") no-repeat scroll center center #FFFFFF; + border: 3px solid #FFFFFF; + box-shadow: 1px 1px 2px 0 #555555; + float: left; + height: 50px; + overflow: hidden; + text-indent: -3000px; + width: 50px; +} +.imgWrapper .posterThumb { + float: left; + min-height: 100%; + min-width: 100%; + width: 50px; + height: auto; + position: relative; + border: none; + vertical-align: middle; +} +.posterThumb { + -ms-interpolation-mode: bicubic; /* make scaling look nicer for ie */ + vertical-align: top; + height: auto; + width: 160px; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; +} +.bannerThumb { + -ms-interpolation-mode: bicubic; /* make scaling look nicer for ie */ + vertical-align: top; + height: auto; + width: 750px; + /* margin-bottom: 1px; */ +} + +.tvshowDiv th { + color: #000; + letter-spacing: 1px; + text-align: left; + background-color: #333333; +} + +.tvshowDiv th.nobg { + background: #efefef; + border-top: 1px solid #666; + text-align: center; +} + +.tvshowDiv td { + border-top: 1px solid #d2ebe8; + background: #fff; + padding: 5px 10px 5px 10px; + color: #000; +} + +.tvshowDiv td.next_episode { + width: 100%; + height: 90%; + border-top: 1px solid #ccc; + vertical-align: top; + background: #F5FAFA; + color: #000; +} + +h1.day { + font-weight: bold; + margin-top: 10px; + padding: 4px; + letter-spacing: 1px; + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + color: #fff; + text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3); + text-align: center; +} + +h1.network { + font-weight: bold; + padding: 4px; + letter-spacing: 1px; + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + color: #fff; + text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3); + text-align: center; +} + +.ep_listing { + width: auto; + border: 1px solid #CCCCCC; + margin-bottom: 10px; + /* margin: 10px; */ + /* overflow: hidden; */ + padding: 10px; +} + +.h2footer .listing_default, +.h2footer .listing_current, +.h2footer .listing_waiting, +.h2footer .listing_overdue, +.h2footer .listing_toofar { + padding: 2px 10px; + display: inline-block; + font-size: 13px; + font-weight: bold; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.listing_default { + background-color: #F5F1E4; + color: #cec198; +} + +.listing_current { + background-color: #E2FFD8; + color: #AAD450; +} + +.listing_waiting { + background-color: #99ff99; +} + +.listing_overdue { + background-color: #FDEBF3; + color: #F49AC1; +} + +.listing_toofar { + background-color: #E9F7FB; + color: #90D5EC; +} + +.listing_unknown { + background-color: #ffdc89; +} + +tr.listing_default { + color: #000000; +} + +tr.listing_current { + color: #000000; +} + +tr.listing_waiting { + color: #000000; +} + +tr.listing_overdue { + color: #000000; +} + +tr.listing_toofar { + color: #000000; +} + +tr.listing_unknown { + color: #000000; +} + +span.pause { + color: #FF0000; + font-size: 12px; +} + +.ep_summaryTrigger { +/* float: left; + padding-top: 9px;*/ + margin-top: -1px; +} +.ep_summary { + margin-left: 5px; +/* padding-top: 5px; */ + font-style: italic; + line-height: 21px; +} \ No newline at end of file diff --git a/data/css/config.css b/data/css/config.css new file mode 100644 index 0000000000000000000000000000000000000000..6110e179405b7100d722f55f38bed4911d6f9530 --- /dev/null +++ b/data/css/config.css @@ -0,0 +1,163 @@ +#config{text-align:center;padding:0 0 0 0;font-size: 12px;} +#config /**{font-family:Verdana, sans-serif;}*/ +#config *{font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;} +#config a:hover {color: #005580 !important;/*text-decoration: underline;*/} +#config h3 a {text-decoration: none;font-size: 18px;} +#config h3 img {vertical-align: text-bottom; padding-right: 5px;} +#config ul{list-style-type:none;} +#config h3{font-size:1.5em;color:#000;} +#config h4{font-size:1em;color:#333;text-transform:uppercase;margin:0 0 .2em;line-height: 12px;margin-top: 2px;} +#config h5{font-size:1em;color:#000;margin:0 0 .2em;} +#config p{font-size:1.2em;line-height:1.3;} +#config .path{font-size:1em;color:#333;font-family:Verdana;} +#config .jumbo{font-size:15px !important;} +#config-content{display:block;width:100%;text-align:left;clear:both;background:#fff;margin:0 auto;padding:0 0 0;} +#config-components{width:auto;} +#config-components-border{float:left;width:auto;border-top:1px solid #999;padding:5px 0;} +#config .title-group{border-bottom:1px dotted #666;position:relative;padding:25px 15px 25px;} +#config .component-group{border-bottom:1px dotted #666;padding:15px 15px 25px;} +#config .component-group-desc{float:left;width:235px;} +#config .component-group-desc h3{font-size:1.5em;} +#config .component-group-desc p{width:85%;font-size:1.0em;color:#666;margin:.8em 0;} +#config .component-group-desc p.note{width:90%;/*font-size:1.2em*/;color:#333;margin:.8em 0;} +#config .component-group-list{float:left;width:605px;margin-top:16px;} + +#config fieldset{border:0;outline:0;} +#config div.field-pair{margin:.9em 0 1.4em;} +#config div.field-pair input:not(.btn){float:left;margin-right:6px;margin-top:5px;padding-top:4px;padding-bottom:4px;padding-right:4px;padding-left:4px;border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top-left-radius:3px;border-top-right-radius:3px;border-top-width: 1px; + border-left-width: 1px; + border-left-style: solid; + border-bottom-color: #CCCCCC; + border-right-color: #CCCCCC; + border-left-color: #CCCCCC; + border-right-style: solid; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-style: solid; + border-bottom-width: 1px; + border-right-width: 1px; + border-right-style: solid; + border-right-width-value: 1px; + border-top-color: #CCCCCC; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-top-style: solid; + border-top-width: 1px; +} +#config label.nocheck,#config div.providerDiv,#config div #customQuality{padding-left:20px;} +#config label span.component-title{font-size:13px;font-weight:700;float:left;width:175px;margin-right:10px;} +#config label span.component-desc{font-size:11px; margin-left:190px; display:block; font-size:11px;} +#config label.nocheck span.component-desc{margin-left:170px;} +#config div.field-pair select{font-size:12px;} +/* #config div.field-pair select{/*font-size:1.1em*//*;border:1px solid #d4d0c8;padding-top:4px;padding-bottom:4px;padding-right:4px;padding-left:4px;border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top-left-radius:3px;border-top-right-radius:3px;} +#config div.field-pair select option{line-height:1.4;padding:0 10px; border-bottom: 1px dotted #D7D7D7;}*/ +#config-settings{float:right;width:200px;background:#fffae5;border-bottom:1px dotted #666;border-top:1px solid #999;margin-right:20px;padding:20px 0 30px;} +#config-settings .config-settings-group{border-bottom:1px dotted #999;padding-bottom:15px;margin:0 15px 15px;} +#config-settings .config-settings-group h2{margin-bottom:0;} +#config-settings .config-settings-group p{font-size:1.1em;color:#666;margin:.6em 0 1em;} +#config-settings .config-settings-group div.field-pair{margin:1.2em 0 .6em;} +#config-settings .config-settings-group input{float:left;} +.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;} +.clearfix{display:block;} +/* Hides from IE-mac \*/ +* html .clearfix{height:1%;} +/* End hide from IE-mac */ +#provider_order_list, #service_order_list{list-style-type:none;width:270px;margin:0;padding:0;} +#provider_order_list li, #service_order_list li{font-size:15px;font-weight:bold;height:1.3em;font-family:Verdana;margin:0 5px 5px;padding:6px;} +#provider_order_list input, #service_order_list input{margin:0px 2px;} +.providerDiv{display:none;padding-left:20px;}#config div.metadataDiv{display:none;} +#config div.metadataDiv{display:none;} +#config div.metadata_options{float:left;font-size:14px;font-family:Verdana;width:185px;color:#036;background:#F5F1E4;overflow:auto;border-left:1px solid #404040;border-top:1px solid #404040;border-bottom:1px solid #d4d0c8;border-right:1px solid #d4d0c8;padding:7px;} +#config div.metadata_options label:hover{background-color:#9f9;} +#config div.metadata_options label{display:block;color:#036;line-height:1.5em;padding-left:7px;} +#config div.metadata_example{float:right;font-size:14px;font-family:Verdana;width:265px;color:#036;overflow:auto;margin-right:30px;padding:7px;} +#config div.metadata_example label{display:block;color:#000;line-height:1.5em;} +#config div.metadataDiv .disabled{color:#ccc;} +#config div #metadataLegend{font-size:14px;font-family:Verdana;font-weight:900;display:block;width:auto;text-align:center;padding-bottom:3px;} + +.infoTable {border-collapse: collapse;} +.infoTableHeader, .infoTableCell {padding: 5px;} +.infoTableHeader{font-weight:700;} +.infoTableSeperator { border-top: 1px dotted #666666; } + +#config div.testNotification {border: 1px dotted #CCCCCC; padding: 5px; margin-bottom: 10px; line-height:20px;} + +.config_message { + width: 100%; + text-align: center; + font-size: 1.3em; + background: #ffecac; +} + +[class^="icon16-"], [class*=" icon16-"] { + background-image: url("/images/glyphicons-config.png"); + background-position: -40px 0; + background-repeat: no-repeat; + display: inline-block; + height: 16px; + line-height: 16px; + vertical-align: text-top; + width: 16px; +} + +.icon16-github { + background-position: 0 0; +} +.icon16-mirc { + background-position: -20px 0; +} +.icon16-sb { + background-position: -40px 0; +} +.icon16-web { + background-position: -60px 0; +} +.icon16-win { + background-position: -80px 0; +} + +#config span.path { + background-color: #F5F1E4; + color: #6666FF; + padding-bottom: 3px; + padding-left: 6px; + padding-right: 6px; + padding-top: 3px; +} + +/* ======================================================================= +config_postProcessing.tmpl +========================================================================== */ +#config div.example { + padding: 4px 10px 4px 10px; + margin-top: 2px; + background-color: #dfdede; +} +.Key { + width: 100%; + padding: 6px; + font-family: sans-serif; + font-size: 13px; + background-color: #f4f4f4; + border: 1px solid #ccc; + border-collapse: collapse; + border-spacing: 0; + line-height: 18px !important; + margin-left: 15px !important; +} +.Key th, .tableHeader { + padding: 3px 9px; + margin: 0; + color: #fff; + text-align: center; + background: none repeat scroll 0 0 #666; +} +.Key td { + padding: 1px 5px !important; +} +.Key tr { + border-bottom: 1px solid #ccc; +} +.Key tr.even { + background-color: #dfdede; +} \ No newline at end of file diff --git a/data/css/config.less b/data/css/config.less new file mode 100644 index 0000000000000000000000000000000000000000..ed02b06ff3ca19cf7ed827516d57b6325af59dfc --- /dev/null +++ b/data/css/config.less @@ -0,0 +1,78 @@ +/* Variables */ +@base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; +@alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif; +@base-font-size: 12px; +@text-color: #343434; +@swatch-blue: #4183C4; +@swatch-green: #BDE433; +@swatch-grey: #666666; +@link-color: #75ADD8; +@border-color: #CCCCCC; +@msg-bg: #FFF6A9; +@msg-bg-success: #D3FFD7; +@msg-bg-error: #FFD3D3; + +/* Mixins */ +.rounded(@radius: 5px) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} +.roundedTop(@radius: 5px) { + -moz-border-radius-topleft: @radius; + -moz-border-radius-topright: @radius; + -webkit-border-top-right-radius: @radius; + -webkit-border-top-left-radius: @radius; + border-top-left-radius: @radius; + border-top-right-radius: @radius; +} +.roundedLeftTop(@radius: 5px) { + -moz-border-radius-topleft: @radius; + -webkit-border-top-left-radius: @radius; + border-top-left-radius: @radius; +} +.roundedRightTop(@radius: 5px) { + -moz-border-radius-topright: @radius; + -webkit-border-top-right-radius: @radius; + border-top-right-radius: @radius; +} +.roundedBottom(@radius: 5px) { + -moz-border-radius-bottomleft: @radius; + -moz-border-radius-bottomright: @radius; + -webkit-border-bottom-right-radius: @radius; + -webkit-border-bottom-left-radius: @radius; + border-bottom-left-radius: @radius; + border-bottom-right-radius: @radius; +} +.roundedLeftBottom(@radius: 5px) { + -moz-border-radius-bottomleft: @radius; + -webkit-border-bottom-left-radius: @radius; + border-bottom-left-radius: @radius; +} +.roundedRightBottom(@radius: 5px) { + -moz-border-radius-bottomright: @radius; + -webkit-border-bottom-right-radius: @radius; + border-bottom-right-radius: @radius; +} +.shadow(@shadow: 0 17px 11px -1px #ced8d9) { + -moz-box-shadow: @shadow; + -webkit-box-shadow: @shadow; + -o-box-shadow: @shadow; + box-shadow: @shadow; +} +.gradient(@gradientFrom: #FFFFFF, @gradientTo: #EEEEEE){ + background-image: -moz-linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: -webkit-linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: -o-linear-gradient(@gradientFrom, @gradientTo) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=@gradientFrom, endColorstr=@gradientTo) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=@gradientFrom, endColorstr=@gradientTo) !important; +} +.opacity(@opacity_percent:85) { + filter: ~"alpha(opacity=85)"; + -moz-opacity: @opacity_percent / 100 !important; + -khtml-opacity:@opacity_percent / 100 !important; + -o-opacity:@opacity_percent / 100 !important; + opacity:@opacity_percent / 100 !important; +} + diff --git a/data/css/default.css b/data/css/default.css new file mode 100644 index 0000000000000000000000000000000000000000..2a5ba73b72cc2d0b49a17db71611c382ac5fa379 --- /dev/null +++ b/data/css/default.css @@ -0,0 +1,1874 @@ +/* Variables *//* Mixins */ +* { + outline: 0; + margin: 0; +} + +*:focus { + outline: none; +} + +input, textarea { + -moz-transition-delay: 0s, 0s; + -webkit-transition-delay: 0s, 0s; + -o-transition-delay: 0s, 0s; + -moz-transition-duration: 0.2s, 0.2s; + -webkit-transition-duration: 0.2s, 0.2s; + -o-transition-duration: 0.2s, 0.2s; + -moz-transition-property: border, box-shadow; + -webkit-transition-property: border, box-shadow; + -o-transition-property: border, box-shadow; + -moz-transition-timing-function: linear, linear; + -webkit-transition-timing-function: linear, linear; + -o-transition-timing-function: linear, linear; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset; +} +input:focus, textarea:focus { + border-bottom-color: rgba(0, 168, 236, 0.8); + border-left-color-ltr-source: physical; + border-left-color-rtl-source: physical; + border-left-color-value: rgba(0, 168, 236, 0.8); + border-right-color-ltr-source: physical; + border-right-color-rtl-source: physical; + border-right-color-value: rgba(0, 168, 236, 0.8); + border-top-color: rgba(0, 168, 236, 0.8); + border-right-color: rgba(0, 168, 236, 0.8); + border-left-color: rgba(0, 168, 236, 0.8); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 2px rgba(0, 168, 236, 1.0); + outline-color: -moz-use-text-color; + outline-style: none; + outline-width: 0; +} + +/*input:focus, select:focus, textarea:focus { + box-shadow: 0 0 3px 0 #0066FF; + z-index: 1; +}*/ + +input[type="checkbox"]:focus, input[type="checkbox"]:active, input[type="submit"]:focus, +input[type="submit"]:active,input[type="button"]:focus, input[type="button"]:active { + box-shadow: none; +} + +img { + border: 0; +/* vertical-align: middle; + vertical-align: sub;*/ + vertical-align: text-top; +} + +.imgLink img { + padding-bottom: 1px; + padding-left: 2px; + padding-right: 2px; +} + +.imgHomeWrapper { + background: url("../images/loading.gif") no-repeat scroll center center #FFFFFF; + border: 3px solid #FFFFFF; + box-shadow: 1px 1px 2px 0 #555555; + float: left; + height: 52px; + overflow: hidden; +} + +.imgHomeWrapperRounded{ + border-radius: 8px; +} + +.imgHomeWrapper .poster { + float: left; + width: 50px; + height: auto; + position: relative; + vertical-align: middle; +} + +.imgHomeWrapper .banner { + height: 52px; + overflow: hidden; + border-radius: 8px; + vertical-align: top; + height: auto; + width: 300px; + border-radius: 8px; +} +.imgHomeWrapperbig { + background: url("../images/loading.gif") no-repeat scroll center center #FFFFFF; + border: 3px solid #FFFFFF; + box-shadow: 1px 1px 2px 0 #555555; + margin-left: 200px; + height: 100px; + overflow: hidden; +} + +.imgHomeWrapperRoundedbig{ + border-radius: 8px; +} + +.imgHomeWrapper .posterbig { + float: left; + width: 50px; + height: auto; + position: relative; + vertical-align: middle; +} + +.imgHomeWrapper .bannerbig { + height: 100px; + overflow: hidden; + border-radius: 8px; + vertical-align: top; + height: auto; + width: 500px; + border-radius: 8px; +} +html { + margin: 0; + padding: 0; +} +body { + text-rendering: optimizeLegibility; + background: none repeat scroll 0 0 #FFFFFF; + color: #343434; + font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + margin: 0; + overflow-y: scroll; + padding: 0; + font-size: 14px; +} +form { + border: none; + display: inline; + margin: 0; + padding: 0; +} +a{ + color: #75add8; + -moz-text-decoration-line: none; + text-decoration: none; +} + +.update { + color: #339933; + text-decoration: blink; +} + +/* these are for inc_top.tmpl */ +#upgrade-notification { + position: fixed; + line-height: 0.5em; +/* color: #000; */ + font-size: 1em; + font-weight: bold; + height: 0px; + text-align: center; + width: 100%; + z-index: 100; + margin: 0; + padding: 0; +} +#upgrade-notification div { +/* background-image: -moz-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -webkit-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -o-linear-gradient(#fdf0d5, #fff9ee) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + border-bottom: 1px solid #af986b; */ + background-color: #C6B695; + border-bottom-color: #AF986B; + border-bottom-style: solid; + border-bottom-width: 1px; + padding: 7px 0; +} +#header-fix { + *margin-bottom: -31px; + /* IE fix */ + height: 21px; + padding: 0; +} +#header { + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + border-bottom: 1px solid #CACACA; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + height: 58px; + width: 100%; + z-index: 999; +} +#header .wrapper { + margin: 0 auto; + position: relative; + width: 960px; +} +#header a:hover { + background: none; +} + +#header .showInfo .checkboxControls { + margin: 0 auto; + position: relative; + width: 960px; +} + +#logo { + float: left; + position: relative; + top: 0px; + left: -5px; +} +#versiontext { + color: #FFFFFF; + font-family: Arial, Helvetica, sans-serif; + font-size: 11px; + position: relative; +/* text-transform: lowercase;*/ + top: 42px; + left: -5px +} + +.update { + color: #339933; + text-decoration: none; +} + +.navShows { + margin-top: -15px; + margin-bottom: -20px; +} +.tvshowImg { + background: url("../images/loading.gif") no-repeat scroll center center #ffffff; + border: 5px solid #FFFFFF; + -moz-box-shadow: 1px 1px 2px 0 #555555; + -webkit-box-shadow: 1px 1px 2px 0 #555555; + -o-box-shadow: 1px 1px 2px 0 #555555; + box-shadow: 1px 1px 2px 0 #555555; + float: right; + height: auto; + margin-bottom: 0px; + margin-top: 73px; + margin-right: 0px; + overflow: hidden; + text-indent: -3000px; + width: 200px; +} +.tvshowImg img { + float: right; + min-width: 100%; + position: relative; +} +/* --------------------------------------------- */ +table { + margin: 0; +} + +table .ep_name { + color: #000000; +} + + +table td a { + color: #555; +} +table td a:hover { + color: #343434; +} + +h1 { + text-align: left; + font-size: 21px; + line-height: 23px; + font-weight: 400; +} +h1.title { + padding-bottom: 10px; + margin-bottom: 12px; + font-weight: bold; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + font-size: 38px; + line-height: 5px; + text-rendering: optimizelegibility; +} + +h1.header { + padding-bottom: 10px; + margin-top: 12px; + margin-bottom: 12px; + font-size: 30px; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + font-weight: bold; +} +h1 a { + text-decoration: none; +} +h2 { + font-size: 18px; + font-weight: 700; +} +.h2footer { + margin: -35px 5px 6px 0px; +} +.h2footer select { + margin-top: -6px; + margin-bottom: -6px; +} +.separator { + font-size: 90%; + color: #999; +} + +div select { +/* font-size: 12px; !important*/ + border: 1px solid #d4d0c8; +} +div select{font-size:0.9em ;height:28px;border:1px solid #d4d0c8;padding-top:4px;padding-bottom:4px;padding-right:4px;padding-left:4px;border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top-left-radius:3px;border-top-right-radius:3px;} + +div select[multiple], div select[size] { + height: auto; +} + +div select option { +/* line-height: 1.4; + padding: 0 10px; + border-bottom: 1px dotted #D7D7D7;*/ + font-size:13px; + padding-bottom: 2px; + padding-left: 8px; + padding-right: 8px; + padding-top: 2px; +} + +input:not(.btn){margin-right:6px;margin-top:5px;padding-top:4px;padding-bottom:4px;padding-right:4px;padding-left:4px;border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top-left-radius:3px;border-top-right-radius:3px;border-top-width: 1px; + border-left-width: 1px; + border-left-style: solid; + border-bottom-color: #CCCCCC; + border-right-color: #CCCCCC; + border-left-color: #CCCCCC; + border-right-style: solid; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-style: solid; + border-bottom-width: 1px; + border-right-width: 1px; + border-right-style: solid; + border-right-width-value: 1px; + border-top-color: #CCCCCC; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-top-style: solid; + border-top-width: 1px; +} + +/* --------------- alignment ------------------- */ +.float-left { + float: left; +} +.float-right { + float: right; +} +.align-left { + text-align: left; +} +.align-right { + text-align: right; +} +.nowrap { + white-space: nowrap; +} +/* --------------------------------------------- */ +.footer { + clear: both; + width: 960px; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + margin: 20px auto; + border-top: 1px solid #eee; + line-height: 1.4em; + font-size: 11px; +} +.footer a { + color: #75add8; + text-decoration: none; +} +.footer ul li { + list-style: none; + float: left; + margin-left: 10px; +} +.footer ul li img { + margin-right: 1px; + position: relative; + top: -2px; + vertical-align: middle; +} +.sickbeardTable { + width: 100%; + margin-left: auto; + margin-right: auto; + text-align: left; + color: #343434; + background-color: #fff; + border-spacing: 0; +} +.sickbeardTable th, +.sickbeardTable td { + padding: 4px; + border-top: #fff 1px solid; + vertical-align: middle; +} + +#massUpdateTable.sickbeardTable th { + padding: 4px 0 !important; + border-top: #fff 1px solid; + vertical-align: middle; +} + +#massUpdateTable.sickbeardTable th { + padding: 4px 0 !important; + border-top: #fff 1px solid; + vertical-align: middle; +} + +#massUpdateTable.tablesorter td { + padding: 8px 5px; +} + +#massUpdateTable.tablesorter td.tvShow a{ + font-size: 16px; +} + +.sickbeardTable th:first-child, +.sickbeardTable td:first-child { + border-left: none; +} +.sickbeardTable th { + border-collapse: collapse; + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + color: #fff; + text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3); + text-align: center; +} +.sickbeardTable tfoot a { + text-decoration: none; + color: #bde433; + float: left; +} +.sickbeardTable td { + font-size: 14px; +} + +.sickbeardTable td.title { + font-size: 14px; + line-height: normal; +} + +.sickbeardTable td.filename { +width: 30%; + +} + +.sickbeardTable td.subtitles_column { + vertical-align: middle; +/* width: 10%; */ +} + +.sickbeardTable td.subtitles_column img { + padding-right: 2px; + padding-top: 2px; +} + + +.sickbeardTable td.status_column { + font-weight: bold; + line-height: 20px; + text-align: center; + width: 13%; + color: #555555; +} + +.sickbeardTable td.search img { + padding-right: 2px; +} + +.sickbeardTable td small { + font-size: 11px; + font-style: italic; + line-height: normal; +} +.row { + clear: left; +} + +.plotInfo { + cursor: help; + float: right; + font-weight: 700; + position: relative; + padding-left: 2px; +} + +.sickbeardTable td.plotInfo { + align: center; +} + +#actions .selectAll { + margin-right: 10px; + border-right: 1px solid #eee; + padding-right: 10px; +} +#tooltip { + display: none; + z-index: 3343434; + border: 1px solid #111; + background-color: #eee; + padding: 5px; + margin-right: 10px; +} +.progressbarText { + text-shadow: 0 0 0.1em #fff; + position: absolute; + top: 0; + font-size: 12px; + color: #555555; + font-weight: bold; + width: 100%; + height: 100%; + overflow: visible; + text-align: center; + vertical-align: middle; +} +.ui-progressbar .ui-widget-header { + background-image: -moz-linear-gradient(#a3e532, #90cc2a) !important; + background-image: linear-gradient(#a3e532, #90cc2a) !important; + background-image: -webkit-linear-gradient(#a3e532, #90cc2a) !important; + background-image: -o-linear-gradient(#a3e532, #90cc2a) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} +tr.seasonheader { + background-color: #FFFFFF; + padding-bottom: 5px; + padding-top: 10px; + text-align: left; +/* white-space: nowrap;*/ +} +tr.seasonheader td { + padding-top: 20px; + padding-bottom: 10px; +} +tr.seasonheader h2 { + display: inline; + font-size: 22px; + line-height: 20px; + letter-spacing: 1px; + margin: 0; + color: #343434; +} +tr.seasonheader a:not(.btn) { + text-decoration: none; +} + +tr.seasonheader a:hover:not(.btn) { + background-color: #fff; + color: #343434; +} +#checkboxControls label { + white-space: nowrap; +} +tr.unaired, +span.unaired { + background-color: #F5F1E4; + padding: 5px; + font-size: 13px; + color: #cec198; + font-weight: bold; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +tr.unaired b, +span.unaired b { + color: #343434; +} +tr.skipped, +span.skipped { + background-color: #E9F7FB; + color: #90D5EC; + font-weight: bold; + font-size: 13px; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +tr.skipped b, +span.skipped b { + color: #343434; +} +tr.good, +span.good { + background-color: #E2FFD8; + color: #AAD450; + padding: 5px; + font-size: 13px; + font-weight: bold; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +tr.good b, +span.good b { + color: #343434; +} +tr.qual, +span.qual { + background-color: #FDF0D5; + padding: 5px; + font-size: 13px; + font-weight: bold; + color: #F7B42C; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +tr.qual b, +span.qual b { + color: #343434; +} +tr.wanted, +span.wanted { + background-color: #FDEBF3; + padding: 5px; + font-size: 13px; + font-weight: bold; + color: #F49AC1; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +tr.wanted b, +span.wanted b { + color: #343434; +} + +tr.snatched, +span.snatched { + background-color: #efefef; + padding: 5px; + font-size: 13px; + font-weight: bold; + color: #F49AC1; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +tr.snatched b, +span.snatched b { + color: #343434; +} + +tr.wanted, +tr.qual, +tr.good, +tr.skipped, +tr.unaired, +tr.snatched { + font-size: 14px; + font-weight: normal; + color: #343434; + height: 35px; +} +.showInfo { + width: 745px; + float: left; + margin-left: 0px; + padding-top: 2px; +} + +span .headerInfo { + color: #666666; +} + +div .seasonList { + padding-top: 12px; + padding-right: 10px; +} + +div#summary { + background-color: #f9f9f9; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + padding: 5px 10px; + border: 1px solid #ddd; + margin: 3px 0 10px 0; +} +div#summary .infoTable { + width: 90%; +} +div#summary tr { + line-height: 17px; +} +div#summary tr td { + font-size: 14px; + line-height: 25px; +} +#MainMenu { + float: right; + height: 58px; + margin: 0; + padding: 0 0 0 10px; +} +#SubMenu { + float: right; + margin-top: -35px; + position: relative; + z-index: inherit; +} +#SubMenu a { + font-size: 12px; + text-decoration: none; +} +#SubMenu span b { + margin-left: 20px; +} +#donate { + line-height: 1em; + float: right; +/* display: none;*/ +} +#donate a, +#donate a:hover { + border: 0 ; + padding: 4px 15px 4px; +} + +#contentWrapper { + background: none; +} +#content { + line-height: 24px; + margin: 0 auto; + padding: 105px 0 0; + width: 960px; +} +.showLegend { + font-weight: 700; + padding-right: 10px; + padding-bottom: 1px; +} +/* for the add new/existing show */ +.alt { + background-color: #efefef; +} + +#tabs div.field-pair, +.stepDiv div.field-pair { + padding: 0.75em 0; +} + +#tabs div.field-pair input, +.stepDiv div.field-pair input { + float: left; + margin-top: 5px; + margin-left: 3px; +} + +#tabs label.nocheck, +#tabs div.providerDiv, +#tabs div #customQuality, +.stepDiv label.nocheck, +.stepDiv div.providerDiv, +.stepDiv div #customQuality { + padding-left: 23px; +} + +#tabs label span.component-title, +.stepDiv label span.component-title { +/* font-size: 1.1em; */ + font-weight: 700; + float: left; + width: 165px; + padding-left: 6px; + margin-right: 10px; +} + +#tabs label span.component-desc, +.stepDiv label span.component-desc { + font-size: .9em; + float: left; +} + +#tabs div.field-pair select, +.stepDiv div.field-pair select { + font-size: 12px; + border: 1px solid #d4d0c8; +} + +#tabs div.field-pair select option, +.stepDiv div.field-pair select option { + line-height: 1.4; + padding: 0 10px; +/* border-bottom: 1px dotted #D7D7D7;*/ +} +ul#rootDirStaticList { + width: 90%; + text-align: left; + margin-left: auto; + margin-right: auto; + padding-left: 0; +} +ul#rootDirStaticList li { + list-style: none outside none; + margin: 2px; + padding: 4px 5px 4px 5px; + cursor: pointer; +} + +/*#rootDirs, #rootDirsControls { + width: 50%; + min-width: 400px; + height:auto !important; +}*/ + +#episodeDir, #newRootDir { + margin-right: 6px; + } + +#displayText { + background-color: #efefef; + padding: 8px; + border: 1px solid #DFDEDE; + font-size: 1.1em; + overflow: hidden; +} +div#addShowPortal { + margin: 50px auto; + width: 100%; +} +div#addShowPortal button { + float: left; + margin-left: 20px; + padding: 10px; + width: 47%; +} +div#addShowPortal button div.button img { + position: absolute; + display: block; + top: 35%; + padding-left: 0.4em; + text-align: center; +} +div#addShowPortal button .buttontext { + position: relative; + display: block; + padding: 0.1em 0.4em 0.1em 4.4em; + text-align: left; +} + +td.tvShow a { + text-decoration: none; + font-size: 18px; + font-weight: bold; +} + +.navShow { + display: inline; + cursor: pointer; + vertical-align: top; + position: relative; + top: 3px; + filter: alpha(opacity=85); + -moz-opacity: 0.5 !important; + -khtml-opacity: 0.5 !important; + -o-opacity: 0.5 !important; + opacity: 0.5 !important; +} +.navShow:hover { + filter: alpha(opacity=85); + -moz-opacity: 1 !important; + -khtml-opacity: 1 !important; + -o-opacity: 1 !important; + opacity: 1 !important; +} +/* for manage_massEdit */ +.optionWrapper { + width: 450px; + margin-left: auto; + margin-right: auto; + padding: 6px 12px; +} +.optionWrapper span.selectTitle { + float: left; + font-weight: 700; + font-size: 1.2em; + text-align: left; + width: 225px; +} +.optionWrapper div.selectChoices { + float: left; + width: 175px; + margin-left: 25px; +} +.optionWrapper br { + clear: both; +} +a.whitelink { + color: white; +} +/* for displayShow notice */ +#show_message { + border: 1px solid #cccccc; + background-image: -moz-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -webkit-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -o-linear-gradient(#fdf0d5, #fff9ee) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + font-size: 14px; + right: 10px; + -moz-box-shadow: 0px 0px 2px #aaaaaa; + -webkit-box-shadow: 0px 0px 2px #aaaaaa; + -o-box-shadow: 0px 0px 2px #aaaaaa; + box-shadow: 0px 0px 2px #aaaaaa; + padding: 7px 10px; + position: fixed; + text-align: center; + bottom: 10px; + min-height: 22px; + width: 250px; + z-index: 9999; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; + filter: alpha(opacity=85); + -moz-opacity: 0.8 !important; + -khtml-opacity: 0.8 !important; + -o-opacity: 0.8 !important; + opacity: 0.8 !important; +} +#show_message .msg { + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; + padding-left: 20px; +} +#show_message .loader { + position: relative; + top: 2px; +} +#show_message.success { + background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important; + background-image: linear-gradient(#d3ffd7, #c2edc6) !important; + background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important; + background-image: -o-linear-gradient(#d3ffd7, #c2edc6) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + padding: 15px 10px; + text-align: left; +} +#show_message.error { + background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important; + background-image: linear-gradient(#ffd3d3, #edc4c4) !important; + background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important; + background-image: -o-linear-gradient(#ffd3d3, #edc4c4) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fdf0d5, endColorstr=#fff9ee) !important; + padding: 15px 10px; + text-align: left; +} +#show_message .ui-icon { + display: inline-block; + margin-left: -20px; + top: 2px; + position: relative; + margin-right: 3px; +} + +ui-pnotify-text img { + /*padding-top: 3px;*/ + margin-top: -3px; +} + +div.ui-pnotify { + min-width: 340px; + max-width: 550px; + width: auto !important; +} +/* override for qtip2 */ +.ui-tooltip-sb .ui-tooltip-titlebar a { + color: #222222; + text-decoration: none; +} +.ui-tooltip, +.qtip { + max-width: 500px !important; +} + +.changelog { max-width: 650px !important; } + +option.flag { + padding-left: 35px; + background-color: #fff; + background-repeat: no-repeat; + background-position: 10px 50%; +} + +span.quality { + font: bold 1em/1.2em verdana, sans-serif; + background: none repeat scroll 0 0 #999999; + color: #FFFFFF; + display: inline-block; + padding: 2px 4px; + text-align: center; + -webkit-border-radius: 4px; + font-size: 12px; + -moz-border-radius: 4px; + border-radius: 4px; +} +span.Custom { + background: none repeat scroll 0 0 #449; + /* purplish blue */ +} + +span.HD { + background: none repeat scroll 0 0 #008fbb; + /* greenish blue */ +} + +span.HD720p { + background: none repeat scroll 0 0 #494; + /* green */ +} + +span.HD1080p { + background: none repeat scroll 0 0 #499; + /* blue */ +} + +span.RawHD { + background: none repeat scroll 0 0 #999944; + /* dark orange */ +} + +span.SD { + background: none repeat scroll 0 0 #944; + /* red */ +} + +span.Any { + background: none repeat scroll 0 0 #444; + /* black */ +} + +span.Proper { + background: none repeat scroll 0 0 #CD7300; + /* orange_red */ +} + +span.false { + color: #993333; + /* red */ + +} +span.true { + color: #669966; + /* green */ + +} +.ui-progressbar { + height: 18px !important; + line-height: 17px; +} + +.pull-left { + float: left; + padding-right: 2px +} + +.pull-right { + float: right; +} + +#searchResults a { + color: #343434; +} + +.btn { + display: inline-block; + *display: inline; + padding: 3.5px 10px 3.5px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 13px; + line-height: 18px; + *line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); +/* vertical-align: middle; */ + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + margin-top: 4px; +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + /* Buttons in IE7 don't get borders, so darken on hover */ + + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-color: #e6e6e6; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.btn-large [class^="icon-"] { + margin-top: 1px; +} + +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} + +.btn-small [class^="icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding-left: 6px; + padding-right: 6px; + font-size: 11px; + line-height: 14px; + margin-top: -2px; +} + +.btn-mini a { + color: #333333; +} + +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover, +.btn-inverse, +.btn-inverse:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + background-color: #0074cc; + *background-color: #0055cc; + background-image: -ms-linear-gradient(top, #0088cc, #0055cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); + background-image: -o-linear-gradient(top, #0088cc, #0055cc); + background-image: -moz-linear-gradient(top, #0088cc, #0055cc); + background-image: linear-gradient(top, #0088cc, #0055cc); + background-repeat: repeat-x; + border-color: #0055cc #0055cc #003580; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0055cc; + *background-color: #004ab3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #004099 \9; +} + +.btn-warning { + background-color: #faa732; + *background-color: #f89406; + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + background-color: #da4f49; + *background-color: #bd362f; + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + background-color: #5bb75b; + *background-color: #51a351; + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + background-color: #414141; + *background-color: #414141; + background-image: -ms-linear-gradient(top, #555555, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); + background-image: -webkit-linear-gradient(top, #555555, #222222); + background-image: -o-linear-gradient(top, #555555, #222222); + background-image: -moz-linear-gradient(top, #555555, #222222); + background-image: linear-gradient(top, #555555, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + vertical-align: middle; +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +.btn:focus, +.btn:active { + border: 1px solid #4d90fe; + outline: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-primary:focus, +.btn-warning:focus, +.btn-danger:focus, +.btn-success:focus, +.btn-info:focus, +.btn-inverse:focus { + border: 1px solid transparent; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.75) inset; +} + +/*button.btn, +input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +}*/ + +/* button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +}*/ + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-group { + position: relative; + *margin-left: .3em; + *zoom: 1; +} + +.btn-group:before, +.btn-group:after { + display: table; + content: ""; +} + +.btn-group:after { + clear: both; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} + +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group > .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .dropdown-toggle { + *padding-top: 4px; + padding-right: 8px; + *padding-bottom: 4px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini.dropdown-toggle { + padding-right: 5px; + padding-left: 5px; +} + +.btn-group > .btn-small.dropdown-toggle { + *padding-top: 4px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large.dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0055cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 7px; + margin-left: 0; +} + +.btn:hover .caret, +.open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} + +.btn-mini .caret { + margin-top: 5px; +} + +.btn-small .caret { + margin-top: 6px; +} + +.btn-large .caret { + margin-top: 6px; + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-top: 0; + border-bottom: 5px solid #000000; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} + +#customQuality { + clear: both; + display: block ; + overflow-x: hidden; + overflow-y: hidden; + padding-bottom: 10px; + padding-left: 0; + padding-right: 0; + padding-top: 10px; + font-size: 14px; + padding-left:20px; +} + +.stepDiv > #customQualityWrapper { + overflow-x: hidden; + overflow-y: hidden; +} + +#customQualityWrapper div.component-group-desc { + float: left; + width: 165px; +} +#customQualityWrapper div.component-group-desc p { + color: #666666; +/* font-size: 1.2em;*/ + margin-bottom: 0.8em; + margin-left: 0; + margin-right: 0; + margin-top: 0.8em; + width: 85%; +} + +#SceneException { + height: 180px; + padding-bottom: 10px; + padding-left: 20px; + padding-right: 0; + padding-top: 10px; +} + +#SceneException div.component-group-desc { + float: left; + width: 165px; +} + +#SceneException div.component-group-desc p { + color: #666666; +/* font-size: 1.2em;*/ + margin-bottom: 0.8em; + margin-left: 0; + margin-right: 0; + margin-top: 0.8em; + width: 85%; +} + +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { + font-size: 14px; +} + +[class^="icon-"], [class*=" icon-"] { + background-image: url("/images/glyphicons-halflings.png"); +} + +.icon-white { + background-image: url("/images/glyphicons-halflings-white.png"); +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 16px; + height: 16px; +/* margin-left: -21px; + margin-right: 8px; + position: absolute; */ + vertical-align: bottom; + background-repeat: no-repeat; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-calendar { + background-position: -192px -120px; +} + +h3 { + font-size: 18px; + line-height: 27px; +} + +h4.note { + color: #000000; + float: left; + padding-right: 5px; +} +h4 { + font-size: 14px; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #838B8B; + opacity: 0.6; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +pre { + display: block; + padding: 8.5px; + margin: 10px 0 9px; + font-size: 12.025px; + line-height: 18px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} \ No newline at end of file diff --git a/data/css/default.less b/data/css/default.less new file mode 100644 index 0000000000000000000000000000000000000000..6969ec23474c967e1c2de8d3112030b459c4baff --- /dev/null +++ b/data/css/default.less @@ -0,0 +1,542 @@ +// Config +@import "config.less"; + +* { outline: 0;margin:0; } +*:focus { outline: none; } +img { border: 0; vertical-align: middle;} +html { margin:0; padding:0} +body { +text-rendering: optimizeLegibility; +background: none repeat scroll 0 0 #FFFFFF; + color: #343434; + font-family: @base-font-face; + margin: 0; + overflow-y: scroll; + padding: 0; + font-size: 12px; +} + +form { +border:none; +display:inline; +margin:0; +padding:0; +} +a {color: @link-color;} + +/* these are for inc_top.tmpl */ +#upgrade-notification{position: fixed;line-height:0.5em;color:#000;font-size:1em; height:0px;text-align:center;width:100%;z-index:100;margin:0;padding:0;} +#upgrade-notification div{.gradient(#FDF0D5,#fff9ee);border-bottom:1px solid #af986b;padding:7px 0;} +#header-fix{*margin-bottom: -31px; /* IE fix */height:21px;padding:0;} + +#header { + .gradient(#555555, #333333); + border-bottom: 1px solid #CACACA; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + height: 58px; + position: fixed; + width: 100%; + z-index: 999; +} +#header .wrapper { + margin: 0 auto; + position: relative; + width: 960px; +} +#header a:hover { +background:none; +} + +#logo { +float: left; + position: relative; + top: 0px; + left: -5px; +} + +#versiontext { +color: #FFFFFF; + font-family: Arial,Helvetica,sans-serif; + font-size: 11px; + position: relative; + text-transform: lowercase; + top: 7px; +} + +.navShows { margin-top:-25px; margin-bottom: 40px;} + +.tvshowImg { + background: url("../images/loading.gif") no-repeat scroll center center #FFFFFF; + border: 5px solid #FFFFFF; + .shadow(1px 1px 2px 0 #555555); + float: left; + height: auto; + margin-bottom: 30px; + margin-right: 25px; + overflow: hidden; + text-indent: -3000px; + width: 175px; + img { + float: left; + min-width: 100%; + position: relative; + } +} +/* --------------------------------------------- */ + +table { +margin:0; + td a { + color: #555; + &:hover { color: @text-color;} + } +} + +h1 { +text-align:left; +font-size:21px; +line-height:23px; +font-weight:400; +} +h1.title { +padding-bottom:4px; +margin-bottom:12px; +font-weight: bold; +font-family: @alt-font-face; +font-size: 38px; +} +h1.header { +padding-bottom:4px; +margin-bottom:12px; +font-size: 30px; +font-family: @alt-font-face; +font-weight: bold; +} +h1 a { +text-decoration:none; +} + +h2 { +font-size:18px; +font-weight:700; +} + +.h2footer { +margin: -33px 5px 6px 0px; +} +.h2footer select { +margin-top: -6px; +margin-bottom: -6px; +} + +.separator { +font-size:90%; +color:#999; +} + +div select { +font-size:10px; +border:1px solid #d4d0c8; +} + +div select option { +line-height:1.4; +padding:0 10px; +border-bottom: 1px dotted #D7D7D7; +} + +/* --------------- alignment ------------------- */ +.float-left { float:left; } +.float-right { float:right; } +.align-left { text-align:left; } +.align-right { text-align:right; } +.nowrap { white-space: nowrap; } +/* --------------------------------------------- */ + +.footer { +clear:both; +width: 960px; +text-align:center; +padding-top: 5px; +padding-bottom: 5px; +margin: 20px auto; +border-top:1px solid #eee; +line-height: 1.4em; +font-size: 11px; + a { + color: @link-color; + text-decoration: none; + } + ul { + li { + list-style: none; + float: left; + margin-left: 10px; + img { margin-right: 3px; position: relative; top: -2px;} + } + } +} + +.sickbeardTable { + width: 100%; + margin-left:auto; + margin-right:auto; + text-align:left; + color: #343434; + background-color: #fff; + border-spacing: 0; +} +.sickbeardTable th, +.sickbeardTable td { + padding: 4px; + border-top: #fff 1px solid; + vertical-align: middle; +} +.sickbeardTable th:first-child, +.sickbeardTable td:first-child { + border-left: none; +} +.sickbeardTable th{ + border-collapse: collapse; + .gradient(#555555,#333333); + color: #fff; + text-shadow: -1px -1px 0 rgba(0,0,0,0.3); + text-align: left; +} +.sickbeardTable tfoot a { + text-decoration: none; + color: @swatch-green; +} +.sickbeardTable td { + font-size: 12px; + &.title { font-size: 14px; line-height: normal;} + &.status_column{ line-height: normal;} + small { font-size:11px; font-style: italic; line-height: normal;} +} +.row { +clear:left; +} + +.plotInfo { +cursor:help; +font-weight: 700; +position: relative; +} + +#actions { + .selectAll { margin-right: 10px; border-right: 1px solid #eee; padding-right: 10px;} + .clearAll {} +} +#tooltip { +display:none; +z-index:3343434; +border:1px solid #111; +background-color:#eee; +padding:5px; +margin-right:10px; +} + +.progressbarText { +text-shadow: 0 0 0.1em #fff; +position:absolute; +top:0; +font-size: 9px; +width:100%; +height:100%; +overflow:visible; +text-align:center; +vertical-align: middle; +} +.ui-progressbar .ui-widget-header { + .gradient(#A3E532, #90CC2A); + .rounded(3px); +} +tr.seasonheader { +background-color: #FFFFFF; + padding-bottom: 5px; + padding-top: 10px; + text-align: left; + white-space: nowrap; + td { + padding-top: 20px; + padding-bottom: 10px; + } +} +tr.seasonheader h2 { +display:inline; +font-size:22px; +line-height:20px; +letter-spacing:1px; +margin:0; +color:#343434; +} +tr.seasonheader a { +text-decoration:none; +} +tr.seasonheader a:hover { +background-color: #fff; +color:#343434; +} + + +#checkboxControls label { white-space:nowrap; } +tr.unaired,span.unaired { +background-color:#F5F1E4; +padding:5px; +font-size: 13px; +color: #cec198; +font-weight: bold; +b { color:@text-color;} +.rounded(5px); +} + +tr.skipped,span.skipped { +background-color:#E9F7FB; +color: #90D5EC; +font-weight: bold; +font-size: 13px; +padding:5px; +.rounded(5px); +b { color:@text-color;} +} + +tr.good,span.good { +background-color:#E2FFD8; +color: #AAD450; +padding:5px; +font-size: 13px; +font-weight: bold; +.rounded(5px); +b { color:@text-color;} +} + +tr.qual,span.qual { +background-color:#FDF0D5; +padding:5px; +font-size: 13px; +font-weight: bold; +color: #F7B42C; +.rounded(5px); +b { color:@text-color;} +} + +tr.wanted,span.wanted { +background-color:#FDEBF3; +padding:5px; +font-size: 13px; +font-weight: bold; +color: #F49AC1; +.rounded(5px); +b { color:@text-color;} +} + +tr.wanted,tr.qual,tr.good,tr.skipped,tr.unaired { + font-size: 14px; + font-weight: normal; + color: @text-color; +} +.showInfo { + width: 745px; + float: right; + padding-top: 10px; +} +div#summary { +background-color:#f9f9f9; +.rounded(10px); +padding:10px; +border:1px solid #ddd; +margin:10px 0; + .infoTable { + width: 85%; + } +} +div#summary tr { +line-height: 17px; + td { font-size: 14px; line-height: 25px;} +} + +#MainMenu { + float: right; + height: 58px; + margin: 0; + padding: 0 0 0 10px; +} +#SubMenu { + float: right; + margin-top: -30px; + position: relative; + z-index: 99; +} + +#SubMenu a { + font-size: 12px; + text-decoration: none; +} +#SubMenu span b { margin-left: 20px;} +#donate { +line-height:1em; +background: #57442B; +float: right; +display: none; +} +#donate a,#donate a:hover { +background-color:#57442B; +border:0; +padding:4px 15px 0px; +} +#contentWrapper { + background: none; +} +#content { + line-height: 24px; + margin: 0 auto; + padding: 105px 0 0; + width: 960px; +} +.showLegend{ +font-weight:700; +padding-right:10px; +padding-bottom:1px; +} + +/* for the add new/existing show */ +.alt { background-color: #efefef; } +#tabs div.field-pair, .stepDiv div.field-pair{padding:0.75em 0;} +#tabs div.field-pair input, .stepDiv div.field-pair input{float:left;} +#tabs label.nocheck, #tabs div.providerDiv, #tabs div #customQuality, .stepDiv label.nocheck,.stepDiv div.providerDiv,.stepDiv div #customQuality{padding-left:23px;} +#tabs label span.component-title, .stepDiv label span.component-title{font-size:1.1em;font-weight:700;float:left;width:165px; padding-left: 6px; margin-right:10px;} +#tabs label span.component-desc, .stepDiv label span.component-desc{font-size:.9em; float:left;} +#tabs div.field-pair select, .stepDiv div.field-pair select{font-size:1em;border:1px solid #d4d0c8;} +#tabs div.field-pair select option, .stepDiv div.field-pair select option{line-height:1.4;padding:0 10px; border-bottom: 1px dotted #D7D7D7;} + +ul#rootDirStaticList { width: 90%; text-align: left; margin-left: auto; margin-right: auto; padding-left: 0; } +ul#rootDirStaticList li{ list-style: none outside none; margin: 2px; padding: 4px 5px 4px 5px; cursor: pointer; } + +#displayText { +background-color:#efefef; +padding:8px; +border:1px solid #DFDEDE; +font-size:1.1em; +overflow: hidden; +} + +div#addShowPortal { +margin: 50px auto; + width: 100%; +} + +div#addShowPortal button { float: left; + margin-left: 20px; + padding: 10px; + width: 47%;} +div#addShowPortal button div.button img{ position: absolute; display: block; top: 35%; padding-left: 0.4em; text-align: center; } +div#addShowPortal button .buttontext { position: relative; display: block; padding: 0.1em 0.4em 0.1em 4.4em; text-align: left; } + +#rootDirs, #rootDirsControls { width: 50%; min-width: 400px; } + +td.tvShow a {text-decoration: none; font-size:16px; } +.navShow { display: inline; cursor: pointer; vertical-align: top; position:relative;top:0px; .opacity(50); + &:hover { .opacity(100);} +} + +/* for manage_massEdit */ +.optionWrapper { width: 450px; margin-left: auto; margin-right: auto; padding: 6px 12px; } +.optionWrapper span.selectTitle { float: left; font-weight: 700; font-size: 1.2em; text-align: left; width: 225px; } +.optionWrapper div.selectChoices { float: left; width: 175px; margin-left: 25px; } +.optionWrapper br { clear: both; } + +a.whitelink { color: white; } + +/* for displayShow notice */ +#show_message { + border: 1px solid @border-color; + .gradient(#FDF0D5,#fff9ee); + .rounded(7px); + font-size: 14px; + right: 10px; + .shadow(0px 0px 2px #aaa); + padding: 7px 10px; + position: fixed; + text-align: center; + bottom: 10px; + min-height: 22px; + width: 250px; + z-index: 9999; + font-family: @alt-font-face; + line-height: normal; + .opacity(80); + .msg { + font-family: @alt-font-face; + line-height: normal; + padding-left: 20px; + } + .loader { + position: relative; + top: 2px; + } + &.success { + .gradient(@msg-bg-success,#C2EDC6); + padding: 15px 10px; + text-align: left; + } + &.error { + .gradient(@msg-bg-error,#EDC4C4); + padding:15px 10px; + text-align: left; + } + .ui-icon { + display: inline-block; + margin-left: -20px; + top: 2px; + position: relative; + margin-right: 3px; + } +} +div.ui-pnotify { min-width: 340px; max-width: 550px; width: auto !important;} + +/* override for qtip2 */ +.ui-tooltip-sb .ui-tooltip-titlebar a { color: #222222; text-decoration: none; } +.ui-tooltip, .qtip { max-width: 500px !important; } + +option.flag { + padding-left: 35px; + background-color: #fff; + background-repeat: no-repeat; + background-position: 10px 50%; +} + +span.quality { + font: bold 1em/1.2em verdana, sans-serif; + background: none repeat scroll 0 0 #999999; + color: #FFFFFF; + display: inline-block; + padding: 2px 4px; + text-align: center; + -webkit-border-radius: 4px; + font-size:12px; + -moz-border-radius: 4px; + border-radius: 4px; +} +span.Custom { + background: none repeat scroll 0 0 #444499; /* blue */ +} +span.HD,span.WEB-DL,span.BluRay { + background: none repeat scroll 0 0 #449944; /* green */ +} +span.SD { + background: none repeat scroll 0 0 #994444; /* red */ +} +span.Any { + background: none repeat scroll 0 0 #444444; /* black */ +} + +span.false { + color: #993333; /* red */ +} +span.true { + color: #669966; /* green */ +} +.ui-progressbar { + height: 15px !important; + line-height: 17px; +} \ No newline at end of file diff --git a/data/css/formwizard.css b/data/css/formwizard.css new file mode 100644 index 0000000000000000000000000000000000000000..4524d56d6b7db80058b3e92e095c48ef36a2ab3f --- /dev/null +++ b/data/css/formwizard.css @@ -0,0 +1,91 @@ +fieldset.sectionwrap{ +text-align: left; +width: 800px; +border-width:0; +padding:5px; +top: -25px !important; +} + +legend.legendStep{ +font:bold 16px Arial; +color: #343434; +display: none; +} + +div.stepsguide{ /*div that contains all the "steps" text located at top of form */ +text-align: left; +width: 800px; /*width of "steps" container*/ +overflow:hidden; +margin-bottom:15px; +cursor:pointer; +} + +div.stepsguide .step{ +width:250px; /*width of each "steps" text*/ +font: bold 24px Arial; +float:left; +} +div.stepsguide .step p { +border-bottom: 4px solid #57442B; +} + +div.stepsguide .disabledstep{ +color:#C4C4C4; +} +div.stepsguide .disabledstep p { +border-bottom: 4px solid #8A775E; +} + +div.stepsguide .step .smalltext{ +font-size: 13px; +font-weight: normal; +} + +div.formpaginate{ +width: 800px; +overflow:auto; +font-weight:bold; +text-align:center; +margin-top:1em; +} + +div.formpaginate .prev, div.formpaginate .next{ +border-radius:6px; +-webkit-border-radius:6px; +-moz-border-radius:6px; +padding:3px 6px; +background:#57442B; +color:white; +cursor:hand; +cursor:pointer; +} + +.stepDiv {padding: 21px 15px 15px 15px;} + +input:not(.btn){float:left;margin-right:6px;margin-top:5px;padding-top:4px;padding-bottom:4px;padding-right:4px;padding-left:4px;border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top-left-radius:3px;border-top-right-radius:3px;border-top-width: 1px; + border-left-width: 1px; + border-left-style: solid; + border-bottom-color: #CCCCCC; + border-right-color: #CCCCCC; + border-left-color: #CCCCCC; + border-right-style: solid; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-style: solid; + border-bottom-width: 1px; + border-right-width: 1px; + border-right-style: solid; + border-right-width-value: 1px; + border-top-color: #CCCCCC; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-top-style: solid; + border-top-width: 1px; +} + +/* step 3 related */ + +#customQualityWrapper {/* height: 190px;*/ overflow: hidden; } +#customQualityWrapper div.component-group-desc{float:left;width:165px;} +#customQualityWrapper div.component-group-desc p{width:85%;font-size:14px;color:#666;margin:.8em 0;} +#customQualityWrapper div.component-group-desc p.note{width:90%;font-size:14px;color:#333;margin:.8em 0;} diff --git a/data/css/imports/config.less b/data/css/imports/config.less new file mode 100644 index 0000000000000000000000000000000000000000..a9fba3b2c0dbce77481c539d88084e8202994378 --- /dev/null +++ b/data/css/imports/config.less @@ -0,0 +1,78 @@ +/* Variables */ +@base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; +@alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif; +@base-font-size: 12px; +@text-color: #343434; +@swatch-blue: #4183C4; +@swatch-green: #BDE433; +@swatch-grey: #666666; +@link-color: #555555; +@border-color: #CCCCCC; +@msg-bg: #FFF6A9; +@msg-bg-success: #D3FFD7; +@msg-bg-error: #FFD3D3; + +/* Mixins */ +.rounded(@radius: 5px) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} +.roundedTop(@radius: 5px) { + -moz-border-radius-topleft: @radius; + -moz-border-radius-topright: @radius; + -webkit-border-top-right-radius: @radius; + -webkit-border-top-left-radius: @radius; + border-top-left-radius: @radius; + border-top-right-radius: @radius; +} +.roundedLeftTop(@radius: 5px) { + -moz-border-radius-topleft: @radius; + -webkit-border-top-left-radius: @radius; + border-top-left-radius: @radius; +} +.roundedRightTop(@radius: 5px) { + -moz-border-radius-topright: @radius; + -webkit-border-top-right-radius: @radius; + border-top-right-radius: @radius; +} +.roundedBottom(@radius: 5px) { + -moz-border-radius-bottomleft: @radius; + -moz-border-radius-bottomright: @radius; + -webkit-border-bottom-right-radius: @radius; + -webkit-border-bottom-left-radius: @radius; + border-bottom-left-radius: @radius; + border-bottom-right-radius: @radius; +} +.roundedLeftBottom(@radius: 5px) { + -moz-border-radius-bottomleft: @radius; + -webkit-border-bottom-left-radius: @radius; + border-bottom-left-radius: @radius; +} +.roundedRightBottom(@radius: 5px) { + -moz-border-radius-bottomright: @radius; + -webkit-border-bottom-right-radius: @radius; + border-bottom-right-radius: @radius; +} +.shadow(@shadow: 0 17px 11px -1px #ced8d9) { + -moz-box-shadow: @shadow; + -webkit-box-shadow: @shadow; + -o-box-shadow: @shadow; + box-shadow: @shadow; +} +.gradient(@gradientFrom: #FFFFFF, @gradientTo: #EEEEEE){ + background-image: -moz-linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: -webkit-linear-gradient(@gradientFrom, @gradientTo) !important; + background-image: -o-linear-gradient(@gradientFrom, @gradientTo) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=@gradientFrom, endColorstr=@gradientTo) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=@gradientFrom, endColorstr=@gradientTo) !important; +} +.opacity(@opacity_percent:85) { + filter: ~"alpha(opacity=85)"; + -moz-opacity: @opacity_percent / 100 !important; + -khtml-opacity:@opacity_percent / 100 !important; + -o-opacity:@opacity_percent / 100 !important; + opacity:@opacity_percent / 100 !important; +} + diff --git a/data/css/iphone.css b/data/css/iphone.css new file mode 100644 index 0000000000000000000000000000000000000000..617f16619c7a49727f464f8d8f0617d62dc10ff6 --- /dev/null +++ b/data/css/iphone.css @@ -0,0 +1,24 @@ +body { +width: 100%; +padding: 0; +margin: 0; +font-size: 10px; +line-height:10px; +} + +.MainMenu a, .SubMenu a { padding: 2px; font-weight: normal; } +#btnExistingShow, #btnNewShow { min-height: 150px; } + +.sickbeardTable { +margin-left:1%; +margin-right:1%; +width:98%; +} + +table { +margin:0; +font-size: 10px; +} + +#outerWrapper { width: 98%; padding-left: 1%; padding-right: 1%; } + diff --git a/data/css/jquery-ui-1.8.23.custom.css b/data/css/jquery-ui-1.8.23.custom.css new file mode 100644 index 0000000000000000000000000000000000000000..a9c737aa3330fc43db2b55e360796b69239be31e --- /dev/null +++ b/data/css/jquery-ui-1.8.23.custom.css @@ -0,0 +1,461 @@ +/*! + * jQuery UI CSS Framework 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } +.ui-helper-clearfix:after { clear: both; } +.ui-helper-clearfix { zoom: 1; } +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } + + +/*! + * jQuery UI CSS Framework 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ffDefault=Verdana,Arial,sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=ffffff&bgTextureHeader=01_flat.png&bgImgOpacityHeader=0&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=dcdcdc&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=efefef&bgTextureDefault=03_highlight_soft.png&bgImgOpacityDefault=75&borderColorDefault=aaaaaa&fcDefault=222222&iconColorDefault=8c291d&bgColorHover=dddddd&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=222222&iconColorHover=222222&bgColorActive=dfdfdf&bgTextureActive=05_inset_soft.png&bgImgOpacityActive=75&borderColorActive=aaaaaa&fcActive=140f06&iconColorActive=8c291d&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=aaaaaa&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=aaaaaa&fcError=8c291d&iconColorError=cd0a0a&bgColorOverlay=6e4f1c&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=35&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=35&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; } +.ui-widget .ui-widget { font-size: 1em; } +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } +/*.ui-widget-content { border: 1px solid #aaaaaa; background: #dcdcdc url(images/ui-bg_highlight-soft_75_dcdcdc_1x100.png) 50% top repeat-x; color: #222222; }*/ +.ui-widget-content a { color: #222222; } +/*.ui-widget-header { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_0_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }*/ +.ui-widget-header a { color: #222222; } + +/* Interaction states +----------------------------------*/ +/*.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aaaaaa; background: #efefef url(images/ui-bg_highlight-soft_75_efefef_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #222222; }*/ +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #222222; text-decoration: none; } +/*.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dddddd url(images/ui-bg_highlight-soft_75_dddddd_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #222222; }*/ +.ui-state-hover a, .ui-state-hover a:hover { color: #222222; text-decoration: none; } +/*.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #dfdfdf url(images/ui-bg_inset-soft_75_dfdfdf_1x100.png) 50% 50% repeat-x; font-weight: bold; color: #140f06; }*/ +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #140f06; text-decoration: none; } +.ui-widget :active { outline: none; } + +/* Interaction Cues +----------------------------------*/ +/*.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #aaaaaa; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }*/ +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } +/*.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #aaaaaa; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #8c291d; }*/ +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #8c291d; } +/*.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #8c291d; }*/ +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } +/*.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }*/ +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } + +/* Icons +----------------------------------*/ + +/* states and images */ +/* +.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-state-default .ui-icon { background-image: url(images/ui-icons_8c291d_256x240.png); } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-state-active .ui-icon {background-image: url(images/ui-icons_8c291d_256x240.png); } +.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); } +*/ + +/* positioning */ +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-off { background-position: -96px -144px; } +.ui-icon-radio-on { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } + +/* Overlays */ +/* +.ui-widget-overlay { background: #6e4f1c url(images/ui-bg_flat_0_6e4f1c_40x100.png) 50% 50% repeat-x; opacity: .35;filter:Alpha(Opacity=35); } +.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #000000 url(images/ui-bg_flat_0_000000_40x100.png) 50% 50% repeat-x; opacity: .35;filter:Alpha(Opacity=35); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; } +*/ +/*! + * jQuery UI Resizable 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Resizable#theming + */ +.ui-resizable { position: relative;} +.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; } +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } +.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } +.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } +.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } +.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } +.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } +.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } +.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } +.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*! + * jQuery UI Selectable 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Selectable#theming + */ +.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } +/*! + * jQuery UI Autocomplete 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete#theming + */ +.ui-autocomplete { position: absolute; cursor: default; } + +/* workarounds */ +* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ + +/* + * jQuery UI Menu 1.8.23 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu#theming + */ +.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.ui-menu .ui-menu { + margin-top: -3px; +} +.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.ui-menu .ui-menu-item a.ui-state-hover, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +/*! + * jQuery UI Button 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Button#theming + */ +.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ +.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ +button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ +.ui-button-icons-only { width: 3.4em; } +button.ui-button-icons-only { width: 3.7em; } + +/*button text element */ +.ui-button .ui-button-text { display: block; line-height: 1.4; } +.ui-button-text-only .ui-button-text { padding: .4em 1em; } +.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } +.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } +.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } +.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } +/* no icon support for input elements, provide padding by default */ +input.ui-button { padding: .4em 1em; } + +/*button icon element(s) */ +.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } +.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } +.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } +.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } +.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } + +/*button sets*/ +.ui-buttonset { margin-right: 7px; } +.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } + +/* workarounds */ +button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ +/*! + * jQuery UI Dialog 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog#theming + */ +.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } +.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } +.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } +.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } +.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } +.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } +.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } +.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } +.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } +.ui-draggable .ui-dialog-titlebar { cursor: move; } +/*! + * jQuery UI Tabs 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs#theming + */ +.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ +.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } +.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } +.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } +.ui-tabs .ui-tabs-hide { display: none !important; } +/*! + * jQuery UI Progressbar 1.8.23 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar#theming + */ +.ui-progressbar { height:1.3em; text-align: left; overflow: hidden; } +.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } \ No newline at end of file diff --git a/data/css/jquery.pnotify.default.css b/data/css/jquery.pnotify.default.css new file mode 100644 index 0000000000000000000000000000000000000000..5fc8b5b9d4228ad6679085a89cad4635d0deacb7 --- /dev/null +++ b/data/css/jquery.pnotify.default.css @@ -0,0 +1,101 @@ +/* + Document : jquery.pnotify.default.css + Created on : Nov 23, 2009, 3:14:10 PM + Author : Hunter Perrin + Version : 1.0.0 + Description: + Default styling for Pines Notify jQuery plugin. +*/ + +/* Notice +----------------------------------*/ +.ui-pnotify { + position: fixed; + right: 10px; + bottom: 10px; + /* Ensure that the notices are on top of everything else. */ + z-index: 9999; +} +/* This hides position: fixed from IE6, which doesn't understand it. */ +html > body .ui-pnotify { + position: fixed; +} +.ui-pnotify .ui-widget { + background: none; +} +.ui-pnotify-container { + background-position: 0 0; + border: 1px solid #cccccc; + background-image: -moz-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -webkit-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -o-linear-gradient(#fdf0d5, #fff9ee) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + font-size: 14px; + -moz-box-shadow: 0px 0px 2px #aaaaaa; + -webkit-box-shadow: 0px 0px 2px #aaaaaa; + -o-box-shadow: 0px 0px 2px #aaaaaa; + box-shadow: 0px 0px 2px #aaaaaa; + padding: 7px 10px; + text-align: center; + min-height: 22px; + width: 250px; + z-index: 9999; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; + filter: alpha(opacity=85); + -moz-opacity: 0.8 !important; + -khtml-opacity: 0.8 !important; + -o-opacity: 0.8 !important; + opacity: 0.8 !important; +} +.ui-pnotify-closer { + float: right; + margin-left: .2em; +} +.ui-pnotify-title { + display: block; + background: none; + font-size: 14px; + font-weight: bold; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; +} +.ui-pnotify-text { + display: block; + font-size: 14px; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; +} +.ui-pnotify-icon, .ui-pnotify-icon span { + display: block; + float: left; + margin-right: .2em; +} +/* History Pulldown +----------------------------------*/ +.ui-pnotify-history-container { + position: absolute; + top: 0; + right: 18px; + width: 70px; + border-top: none; + /* Ensure that the history container is on top of the notices. */ + z-index: 10000; +} +.ui-pnotify-history-container .ui-pnotify-history-header { + /*padding: 2px;*/ +} +.ui-pnotify-history-container button { + cursor: pointer; + display: block; + width: 100%; +} +.ui-pnotify-history-container .ui-pnotify-history-pulldown { + display: block; + margin: 0 auto; +} diff --git a/data/css/lib/jquery.qtip.css b/data/css/jquery.qtip2.css similarity index 95% rename from data/css/lib/jquery.qtip.css rename to data/css/jquery.qtip2.css index 0b2d42d4fc037c46f9f0751af91fcdfbc01cca7d..173ce4ba2a0589c2bc7681f48efe75efa2375fde 100644 --- a/data/css/lib/jquery.qtip.css +++ b/data/css/jquery.qtip2.css @@ -9,7 +9,7 @@ * http://en.wikipedia.org/wiki/MIT_License * http://en.wikipedia.org/wiki/GNU_General_Public_License * -* Date: Thu Apr 26 12:17:04.0000000000 2012 +* Date: Thu Nov 17 12:01:03.0000000000 2011 */ /* Core qTip styles */ @@ -24,6 +24,8 @@ font-size: 10.5px; line-height: 12px; + + z-index: 15000; } /* Fluid class for determining actual width in IE */ @@ -38,9 +40,10 @@ position: relative; padding: 5px 9px; overflow: hidden; - - border: 1px solid #000001; - + + border-width: 1px; + border-style: solid; + text-align: left; word-wrap: break-word; overflow: hidden; @@ -51,9 +54,9 @@ min-height: 14px; padding: 5px 35px 5px 10px; overflow: hidden; - - border: 1px solid #000001; + border-width: 1px 1px 0; + border-style: solid; font-weight: bold; } @@ -157,29 +160,6 @@ .ui-tooltip .ui-tooltip-tip canvas{ top: 0; left: 0; } -/* Modal plugin */ -#qtip-overlay{ - position: fixed; - left: -10000em; - top: -10000em; -} - - /* Applied to modals with show.modal.blur set to true */ - #qtip-overlay.blurs{ cursor: pointer; } - - /* Change opacity of overlay here */ - #qtip-overlay div{ - position: absolute; - left: 0; top: 0; - width: 100%; height: 100%; - - background-color: black; - - opacity: 0.7; - filter:alpha(opacity=70); - -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; - } - /*! Light tooltip style */ .ui-tooltip-light .ui-tooltip-titlebar, .ui-tooltip-light .ui-tooltip-content{ @@ -553,5 +533,4 @@ .ui-tooltip:not(.ie9haxors) div.ui-tooltip-titlebar{ filter: none; -ms-filter: none; -} - +} \ No newline at end of file diff --git a/data/css/lib/bootstrap.css b/data/css/lib/bootstrap.css deleted file mode 100644 index 09e2833dcdefc4d7eededbcea0dabba19af1b356..0000000000000000000000000000000000000000 --- a/data/css/lib/bootstrap.css +++ /dev/null @@ -1,4960 +0,0 @@ -/*! - * Bootstrap v2.0.3 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -audio:not([controls]) { - display: none; -} - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -a:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -a:hover, -a:active { - outline: 0; -} - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -img { - max-width: 100%; - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} - -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} - -button, -input { - *overflow: visible; - line-height: normal; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} - -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} - -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -textarea { - overflow: auto; - vertical-align: top; -} - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - content: ""; -} - -.clearfix:after { - clear: both; -} - -.hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -.input-block-level { - display: block; - width: 100%; - min-height: 28px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; -} - -body { - margin: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; - color: #333333; - background-color: #ffffff; -} - -a { - color: #0088cc; - text-decoration: none; -} - -a:hover { - color: #005580; - text-decoration: underline; -} - -.row { - margin-left: -20px; - *zoom: 1; -} - -.row:before, -.row:after { - display: table; - content: ""; -} - -.row:after { - clear: both; -} - -[class*="span"] { - float: left; - margin-left: 20px; -} - -.container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.span12 { - width: 940px; -} - -.span11 { - width: 860px; -} - -.span10 { - width: 780px; -} - -.span9 { - width: 700px; -} - -.span8 { - width: 620px; -} - -.span7 { - width: 540px; -} - -.span6 { - width: 460px; -} - -.span5 { - width: 380px; -} - -.span4 { - width: 300px; -} - -.span3 { - width: 220px; -} - -.span2 { - width: 140px; -} - -.span1 { - width: 60px; -} - -.offset12 { - margin-left: 980px; -} - -.offset11 { - margin-left: 900px; -} - -.offset10 { - margin-left: 820px; -} - -.offset9 { - margin-left: 740px; -} - -.offset8 { - margin-left: 660px; -} - -.offset7 { - margin-left: 580px; -} - -.offset6 { - margin-left: 500px; -} - -.offset5 { - margin-left: 420px; -} - -.offset4 { - margin-left: 340px; -} - -.offset3 { - margin-left: 260px; -} - -.offset2 { - margin-left: 180px; -} - -.offset1 { - margin-left: 100px; -} - -.row-fluid { - width: 100%; - *zoom: 1; -} - -.row-fluid:before, -.row-fluid:after { - display: table; - content: ""; -} - -.row-fluid:after { - clear: both; -} - -.row-fluid [class*="span"] { - display: block; - float: left; - width: 100%; - min-height: 28px; - margin-left: 2.127659574%; - *margin-left: 2.0744680846382977%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; -} - -.row-fluid [class*="span"]:first-child { - margin-left: 0; -} - -.row-fluid .span12 { - width: 99.99999998999999%; - *width: 99.94680850063828%; -} - -.row-fluid .span11 { - width: 91.489361693%; - *width: 91.4361702036383%; -} - -.row-fluid .span10 { - width: 82.97872339599999%; - *width: 82.92553190663828%; -} - -.row-fluid .span9 { - width: 74.468085099%; - *width: 74.4148936096383%; -} - -.row-fluid .span8 { - width: 65.95744680199999%; - *width: 65.90425531263828%; -} - -.row-fluid .span7 { - width: 57.446808505%; - *width: 57.3936170156383%; -} - -.row-fluid .span6 { - width: 48.93617020799999%; - *width: 48.88297871863829%; -} - -.row-fluid .span5 { - width: 40.425531911%; - *width: 40.3723404216383%; -} - -.row-fluid .span4 { - width: 31.914893614%; - *width: 31.8617021246383%; -} - -.row-fluid .span3 { - width: 23.404255317%; - *width: 23.3510638276383%; -} - -.row-fluid .span2 { - width: 14.89361702%; - *width: 14.8404255306383%; -} - -.row-fluid .span1 { - width: 6.382978723%; - *width: 6.329787233638298%; -} - -.container { - margin-right: auto; - margin-left: auto; - *zoom: 1; -} - -.container:before, -.container:after { - display: table; - content: ""; -} - -.container:after { - clear: both; -} - -.container-fluid { - padding-right: 20px; - padding-left: 20px; - *zoom: 1; -} - -.container-fluid:before, -.container-fluid:after { - display: table; - content: ""; -} - -.container-fluid:after { - clear: both; -} - -p { - margin: 0 0 9px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; -} - -p small { - font-size: 11px; - color: #999999; -} - -.lead { - margin-bottom: 18px; - font-size: 20px; - font-weight: 200; - line-height: 27px; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 0; - font-family: inherit; - font-weight: bold; - color: inherit; - text-rendering: optimizelegibility; -} - -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small { - font-weight: normal; - color: #999999; -} - -h1 { - font-size: 30px; - line-height: 36px; -} - -h1 small { - font-size: 18px; -} - -h2 { - font-size: 24px; - line-height: 36px; -} - -h2 small { - font-size: 18px; -} - -h3 { - font-size: 18px; - line-height: 27px; -} - -h3 small { - font-size: 14px; -} - -h4, -h5, -h6 { - line-height: 18px; -} - -h4 { - font-size: 14px; -} - -h4 small { - font-size: 12px; -} - -h5 { - font-size: 12px; -} - -h6 { - font-size: 11px; - color: #999999; - text-transform: uppercase; -} - -.page-header { - padding-bottom: 17px; - margin: 18px 0; - border-bottom: 1px solid #eeeeee; -} - -.page-header h1 { - line-height: 1; -} - -ul, -ol { - padding: 0; - margin: 0 0 9px 25px; -} - -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} - -ul { - list-style: disc; -} - -ol { - list-style: decimal; -} - -li { - line-height: 18px; -} - -ul.unstyled, -ol.unstyled { - margin-left: 0; - list-style: none; -} - -dl { - margin-bottom: 18px; -} - -dt, -dd { - line-height: 18px; -} - -dt { - font-weight: bold; - line-height: 17px; -} - -dd { - margin-left: 9px; -} - -.dl-horizontal dt { - float: left; - width: 120px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; -} - -.dl-horizontal dd { - margin-left: 130px; -} - -hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #ffffff; -} - -strong { - font-weight: bold; -} - -em { - font-style: italic; -} - -.muted { - color: #999999; -} - -abbr[title] { - cursor: help; - border-bottom: 1px dotted #ddd; -} - -abbr.initialism { - font-size: 90%; - text-transform: uppercase; -} - -blockquote { - padding: 0 0 0 15px; - margin: 0 0 18px; - border-left: 5px solid #eeeeee; -} - -blockquote p { - margin-bottom: 0; - font-size: 16px; - font-weight: 300; - line-height: 22.5px; -} - -blockquote small { - display: block; - line-height: 18px; - color: #999999; -} - -blockquote small:before { - content: '\2014 \00A0'; -} - -blockquote.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eeeeee; - border-left: 0; -} - -blockquote.pull-right p, -blockquote.pull-right small { - text-align: right; -} - -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} - -address { - display: block; - margin-bottom: 18px; - font-style: normal; - line-height: 18px; -} - -small { - font-size: 100%; -} - -cite { - font-style: normal; -} - -code, -pre { - padding: 0 3px 2px; - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 12px; - color: #333333; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -code { - padding: 2px 4px; - color: #d14; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; -} - -pre { - display: block; - padding: 8.5px; - margin: 0 0 9px; - font-size: 12.025px; - line-height: 18px; - word-break: break-all; - word-wrap: break-word; - white-space: pre; - white-space: pre-wrap; - background-color: #f5f5f5; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.15); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -pre.prettyprint { - margin-bottom: 18px; -} - -pre code { - padding: 0; - color: inherit; - background-color: transparent; - border: 0; -} - -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} - -form { - margin: 0 0 18px; -} - -fieldset { - padding: 0; - margin: 0; - border: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 27px; - font-size: 19.5px; - line-height: 36px; - color: #333333; - border: 0; - border-bottom: 1px solid #eee; -} - -legend small { - font-size: 13.5px; - color: #999999; -} - -label, -input, -button, -select, -textarea { - font-size: 13px; - font-weight: normal; - line-height: 18px; -} - -input, -button, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -label { - display: block; - margin-bottom: 5px; - color: #333333; -} - -input, -textarea, -select, -.uneditable-input { - display: inline-block; - width: 210px; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 13px; - line-height: 18px; - color: #555555; - background-color: #ffffff; - border: 1px solid #cccccc; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.uneditable-textarea { - width: auto; - height: auto; -} - -label input, -label textarea, -label select { - display: block; -} - -input[type="image"], -input[type="checkbox"], -input[type="radio"] { - width: auto; - height: auto; - padding: 0; - margin: 3px 0; - *margin-top: 0; - /* IE7 */ - - line-height: normal; - cursor: pointer; - background-color: transparent; - border: 0 \9; - /* IE9 and down */ - - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -input[type="image"] { - border: 0; -} - -input[type="file"] { - width: auto; - padding: initial; - line-height: initial; - background-color: #ffffff; - background-color: initial; - border: initial; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -input[type="button"], -input[type="reset"], -input[type="submit"] { - width: auto; - height: auto; -} - -select, -input[type="file"] { - height: 28px; - /* In IE7, the height of the select element cannot be changed by height, only font-size */ - - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ - - line-height: 28px; -} - -input[type="file"] { - line-height: 18px \9; -} - -select { - width: 220px; - background-color: #ffffff; -} - -select[multiple], -select[size] { - height: auto; -} - -input[type="image"] { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -textarea { - height: auto; -} - -input[type="hidden"] { - display: none; -} - -.radio, -.checkbox { - min-height: 18px; - padding-left: 18px; -} - -.radio input[type="radio"], -.checkbox input[type="checkbox"] { - float: left; - margin-left: -18px; -} - -.controls > .radio:first-child, -.controls > .checkbox:first-child { - padding-top: 5px; -} - -.radio.inline, -.checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} - -.radio.inline + .radio.inline, -.checkbox.inline + .checkbox.inline { - margin-left: 10px; -} - -input, -textarea { - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -ms-transition: border linear 0.2s, box-shadow linear 0.2s; - -o-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; -} - -input:focus, -textarea:focus { - border-color: rgba(82, 168, 236, 0.8); - outline: 0; - outline: thin dotted \9; - /* IE6-9 */ - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -} - -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus, -select:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.input-mini { - width: 60px; -} - -.input-small { - width: 90px; -} - -.input-medium { - width: 150px; -} - -.input-large { - width: 210px; -} - -.input-xlarge { - width: 270px; -} - -.input-xxlarge { - width: 530px; -} - -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"] { - float: none; - margin-left: 0; -} - -input, -textarea, -.uneditable-input { - margin-left: 0; -} - -input.span12, -textarea.span12, -.uneditable-input.span12 { - width: 930px; -} - -input.span11, -textarea.span11, -.uneditable-input.span11 { - width: 850px; -} - -input.span10, -textarea.span10, -.uneditable-input.span10 { - width: 770px; -} - -input.span9, -textarea.span9, -.uneditable-input.span9 { - width: 690px; -} - -input.span8, -textarea.span8, -.uneditable-input.span8 { - width: 610px; -} - -input.span7, -textarea.span7, -.uneditable-input.span7 { - width: 530px; -} - -input.span6, -textarea.span6, -.uneditable-input.span6 { - width: 450px; -} - -input.span5, -textarea.span5, -.uneditable-input.span5 { - width: 370px; -} - -input.span4, -textarea.span4, -.uneditable-input.span4 { - width: 290px; -} - -input.span3, -textarea.span3, -.uneditable-input.span3 { - width: 210px; -} - -input.span2, -textarea.span2, -.uneditable-input.span2 { - width: 130px; -} - -input.span1, -textarea.span1, -.uneditable-input.span1 { - width: 50px; -} - -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - cursor: not-allowed; - background-color: #eeeeee; - border-color: #ddd; -} - -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][readonly], -input[type="checkbox"][readonly] { - background-color: transparent; -} - -.control-group.warning > label, -.control-group.warning .help-block, -.control-group.warning .help-inline { - color: #c09853; -} - -.control-group.warning input, -.control-group.warning select, -.control-group.warning textarea { - color: #c09853; - border-color: #c09853; -} - -.control-group.warning input:focus, -.control-group.warning select:focus, -.control-group.warning textarea:focus { - border-color: #a47e3c; - -webkit-box-shadow: 0 0 6px #dbc59e; - -moz-box-shadow: 0 0 6px #dbc59e; - box-shadow: 0 0 6px #dbc59e; -} - -.control-group.warning .input-prepend .add-on, -.control-group.warning .input-append .add-on { - color: #c09853; - background-color: #fcf8e3; - border-color: #c09853; -} - -.control-group.error > label, -.control-group.error .help-block, -.control-group.error .help-inline { - color: #b94a48; -} - -.control-group.error input, -.control-group.error select, -.control-group.error textarea { - color: #b94a48; - border-color: #b94a48; -} - -.control-group.error input:focus, -.control-group.error select:focus, -.control-group.error textarea:focus { - border-color: #953b39; - -webkit-box-shadow: 0 0 6px #d59392; - -moz-box-shadow: 0 0 6px #d59392; - box-shadow: 0 0 6px #d59392; -} - -.control-group.error .input-prepend .add-on, -.control-group.error .input-append .add-on { - color: #b94a48; - background-color: #f2dede; - border-color: #b94a48; -} - -.control-group.success > label, -.control-group.success .help-block, -.control-group.success .help-inline { - color: #468847; -} - -.control-group.success input, -.control-group.success select, -.control-group.success textarea { - color: #468847; - border-color: #468847; -} - -.control-group.success input:focus, -.control-group.success select:focus, -.control-group.success textarea:focus { - border-color: #356635; - -webkit-box-shadow: 0 0 6px #7aba7b; - -moz-box-shadow: 0 0 6px #7aba7b; - box-shadow: 0 0 6px #7aba7b; -} - -.control-group.success .input-prepend .add-on, -.control-group.success .input-append .add-on { - color: #468847; - background-color: #dff0d8; - border-color: #468847; -} - -input:focus:required:invalid, -textarea:focus:required:invalid, -select:focus:required:invalid { - color: #b94a48; - border-color: #ee5f5b; -} - -input:focus:required:invalid:focus, -textarea:focus:required:invalid:focus, -select:focus:required:invalid:focus { - border-color: #e9322d; - -webkit-box-shadow: 0 0 6px #f8b9b7; - -moz-box-shadow: 0 0 6px #f8b9b7; - box-shadow: 0 0 6px #f8b9b7; -} - -.form-actions { - padding: 17px 20px 18px; - margin-top: 18px; - margin-bottom: 18px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - *zoom: 1; -} - -.form-actions:before, -.form-actions:after { - display: table; - content: ""; -} - -.form-actions:after { - clear: both; -} - -.uneditable-input { - overflow: hidden; - white-space: nowrap; - cursor: not-allowed; - background-color: #ffffff; - border-color: #eee; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -} - -:-moz-placeholder { - color: #999999; -} - -::-webkit-input-placeholder { - color: #999999; -} - -.help-block, -.help-inline { - color: #555555; -} - -.help-block { - display: block; - margin-bottom: 9px; -} - -.help-inline { - display: inline-block; - *display: inline; - padding-left: 5px; - vertical-align: middle; - *zoom: 1; -} - -.input-prepend, -.input-append { - margin-bottom: 5px; -} - -.input-prepend input, -.input-append input, -.input-prepend select, -.input-append select, -.input-prepend .uneditable-input, -.input-append .uneditable-input { - position: relative; - margin-bottom: 0; - *margin-left: 0; - vertical-align: middle; - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} - -.input-prepend input:focus, -.input-append input:focus, -.input-prepend select:focus, -.input-append select:focus, -.input-prepend .uneditable-input:focus, -.input-append .uneditable-input:focus { - z-index: 2; -} - -.input-prepend .uneditable-input, -.input-append .uneditable-input { - border-left-color: #ccc; -} - -.input-prepend .add-on, -.input-append .add-on { - display: inline-block; - width: auto; - height: 18px; - min-width: 16px; - padding: 4px 5px; - font-weight: normal; - line-height: 18px; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - vertical-align: middle; - background-color: #eeeeee; - border: 1px solid #ccc; -} - -.input-prepend .add-on, -.input-append .add-on, -.input-prepend .btn, -.input-append .btn { - margin-left: -1px; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-prepend .active, -.input-append .active { - background-color: #a9dba9; - border-color: #46a546; -} - -.input-prepend .add-on, -.input-prepend .btn { - margin-right: -1px; -} - -.input-prepend .add-on:first-child, -.input-prepend .btn:first-child { - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} - -.input-append input, -.input-append select, -.input-append .uneditable-input { - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} - -.input-append .uneditable-input { - border-right-color: #ccc; - border-left-color: #eee; -} - -.input-append .add-on:last-child, -.input-append .btn:last-child { - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} - -.input-prepend.input-append input, -.input-prepend.input-append select, -.input-prepend.input-append .uneditable-input { - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.input-prepend.input-append .add-on:first-child, -.input-prepend.input-append .btn:first-child { - margin-right: -1px; - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} - -.input-prepend.input-append .add-on:last-child, -.input-prepend.input-append .btn:last-child { - margin-left: -1px; - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} - -.search-query { - padding-right: 14px; - padding-right: 4px \9; - padding-left: 14px; - padding-left: 4px \9; - /* IE7-8 doesn't have border-radius, so don't indent the padding */ - - margin-bottom: 0; - -webkit-border-radius: 14px; - -moz-border-radius: 14px; - border-radius: 14px; -} - -.form-search input, -.form-inline input, -.form-horizontal input, -.form-search textarea, -.form-inline textarea, -.form-horizontal textarea, -.form-search select, -.form-inline select, -.form-horizontal select, -.form-search .help-inline, -.form-inline .help-inline, -.form-horizontal .help-inline, -.form-search .uneditable-input, -.form-inline .uneditable-input, -.form-horizontal .uneditable-input, -.form-search .input-prepend, -.form-inline .input-prepend, -.form-horizontal .input-prepend, -.form-search .input-append, -.form-inline .input-append, -.form-horizontal .input-append { - display: inline-block; - *display: inline; - margin-bottom: 0; - *zoom: 1; -} - -.form-search .hide, -.form-inline .hide, -.form-horizontal .hide { - display: none; -} - -.form-search label, -.form-inline label { - display: inline-block; -} - -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - margin-bottom: 0; -} - -.form-search .radio, -.form-search .checkbox, -.form-inline .radio, -.form-inline .checkbox { - padding-left: 0; - margin-bottom: 0; - vertical-align: middle; -} - -.form-search .radio input[type="radio"], -.form-search .checkbox input[type="checkbox"], -.form-inline .radio input[type="radio"], -.form-inline .checkbox input[type="checkbox"] { - float: left; - margin-right: 3px; - margin-left: 0; -} - -.control-group { - margin-bottom: 9px; -} - -legend + .control-group { - margin-top: 18px; - -webkit-margin-top-collapse: separate; -} - -.form-horizontal .control-group { - margin-bottom: 18px; - *zoom: 1; -} - -.form-horizontal .control-group:before, -.form-horizontal .control-group:after { - display: table; - content: ""; -} - -.form-horizontal .control-group:after { - clear: both; -} - -.form-horizontal .control-label { - float: left; - width: 140px; - padding-top: 5px; - text-align: right; -} - -.form-horizontal .controls { - *display: inline-block; - *padding-left: 20px; - margin-left: 160px; - *margin-left: 0; -} - -.form-horizontal .controls:first-child { - *padding-left: 160px; -} - -.form-horizontal .help-block { - margin-top: 9px; - margin-bottom: 0; -} - -.form-horizontal .form-actions { - padding-left: 160px; -} - -table { - max-width: 100%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; -} - -.table { - width: 100%; - margin-bottom: 18px; -} - -.table th, -.table td { - padding: 8px; - line-height: 18px; - text-align: left; - vertical-align: top; - border-top: 1px solid #dddddd; -} - -.table th { - font-weight: bold; -} - -.table thead th { - vertical-align: bottom; -} - -.table caption + thead tr:first-child th, -.table caption + thead tr:first-child td, -.table colgroup + thead tr:first-child th, -.table colgroup + thead tr:first-child td, -.table thead:first-child tr:first-child th, -.table thead:first-child tr:first-child td { - border-top: 0; -} - -.table tbody + tbody { - border-top: 2px solid #dddddd; -} - -.table-condensed th, -.table-condensed td { - padding: 4px 5px; -} - -.table-bordered { - border: 1px solid #dddddd; - border-collapse: separate; - *border-collapse: collapsed; - border-left: 0; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.table-bordered th, -.table-bordered td { - border-left: 1px solid #dddddd; -} - -.table-bordered caption + thead tr:first-child th, -.table-bordered caption + tbody tr:first-child th, -.table-bordered caption + tbody tr:first-child td, -.table-bordered colgroup + thead tr:first-child th, -.table-bordered colgroup + tbody tr:first-child th, -.table-bordered colgroup + tbody tr:first-child td, -.table-bordered thead:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child th, -.table-bordered tbody:first-child tr:first-child td { - border-top: 0; -} - -.table-bordered thead:first-child tr:first-child th:first-child, -.table-bordered tbody:first-child tr:first-child td:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; -} - -.table-bordered thead:first-child tr:first-child th:last-child, -.table-bordered tbody:first-child tr:first-child td:last-child { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; -} - -.table-bordered thead:last-child tr:last-child th:first-child, -.table-bordered tbody:last-child tr:last-child td:first-child { - -webkit-border-radius: 0 0 0 4px; - -moz-border-radius: 0 0 0 4px; - border-radius: 0 0 0 4px; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; -} - -.table-bordered thead:last-child tr:last-child th:last-child, -.table-bordered tbody:last-child tr:last-child td:last-child { - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; -} - -.table-striped tbody tr:nth-child(odd) td, -.table-striped tbody tr:nth-child(odd) th { - background-color: #f9f9f9; -} - -.table tbody tr:hover td, -.table tbody tr:hover th { - background-color: #f5f5f5; -} - -table .span1 { - float: none; - width: 44px; - margin-left: 0; -} - -table .span2 { - float: none; - width: 124px; - margin-left: 0; -} - -table .span3 { - float: none; - width: 204px; - margin-left: 0; -} - -table .span4 { - float: none; - width: 284px; - margin-left: 0; -} - -table .span5 { - float: none; - width: 364px; - margin-left: 0; -} - -table .span6 { - float: none; - width: 444px; - margin-left: 0; -} - -table .span7 { - float: none; - width: 524px; - margin-left: 0; -} - -table .span8 { - float: none; - width: 604px; - margin-left: 0; -} - -table .span9 { - float: none; - width: 684px; - margin-left: 0; -} - -table .span10 { - float: none; - width: 764px; - margin-left: 0; -} - -table .span11 { - float: none; - width: 844px; - margin-left: 0; -} - -table .span12 { - float: none; - width: 924px; - margin-left: 0; -} - -table .span13 { - float: none; - width: 1004px; - margin-left: 0; -} - -table .span14 { - float: none; - width: 1084px; - margin-left: 0; -} - -table .span15 { - float: none; - width: 1164px; - margin-left: 0; -} - -table .span16 { - float: none; - width: 1244px; - margin-left: 0; -} - -table .span17 { - float: none; - width: 1324px; - margin-left: 0; -} - -table .span18 { - float: none; - width: 1404px; - margin-left: 0; -} - -table .span19 { - float: none; - width: 1484px; - margin-left: 0; -} - -table .span20 { - float: none; - width: 1564px; - margin-left: 0; -} - -table .span21 { - float: none; - width: 1644px; - margin-left: 0; -} - -table .span22 { - float: none; - width: 1724px; - margin-left: 0; -} - -table .span23 { - float: none; - width: 1804px; - margin-left: 0; -} - -table .span24 { - float: none; - width: 1884px; - margin-left: 0; -} - -[class^="icon-"], -[class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - *margin-right: .3em; - line-height: 14px; - vertical-align: text-top; - background-image: url("../img/glyphicons-halflings.png"); - background-position: 14px 14px; - background-repeat: no-repeat; -} - -[class^="icon-"]:last-child, -[class*=" icon-"]:last-child { - *margin-left: 0; -} - -.icon-white { - background-image: url("../img/glyphicons-halflings-white.png"); -} - -.icon-glass { - background-position: 0 0; -} - -.icon-music { - background-position: -24px 0; -} - -.icon-search { - background-position: -48px 0; -} - -.icon-envelope { - background-position: -72px 0; -} - -.icon-heart { - background-position: -96px 0; -} - -.icon-star { - background-position: -120px 0; -} - -.icon-star-empty { - background-position: -144px 0; -} - -.icon-user { - background-position: -168px 0; -} - -.icon-film { - background-position: -192px 0; -} - -.icon-th-large { - background-position: -216px 0; -} - -.icon-th { - background-position: -240px 0; -} - -.icon-th-list { - background-position: -264px 0; -} - -.icon-ok { - background-position: -288px 0; -} - -.icon-remove { - background-position: -312px 0; -} - -.icon-zoom-in { - background-position: -336px 0; -} - -.icon-zoom-out { - background-position: -360px 0; -} - -.icon-off { - background-position: -384px 0; -} - -.icon-signal { - background-position: -408px 0; -} - -.icon-cog { - background-position: -432px 0; -} - -.icon-trash { - background-position: -456px 0; -} - -.icon-home { - background-position: 0 -24px; -} - -.icon-file { - background-position: -24px -24px; -} - -.icon-time { - background-position: -48px -24px; -} - -.icon-road { - background-position: -72px -24px; -} - -.icon-download-alt { - background-position: -96px -24px; -} - -.icon-download { - background-position: -120px -24px; -} - -.icon-upload { - background-position: -144px -24px; -} - -.icon-inbox { - background-position: -168px -24px; -} - -.icon-play-circle { - background-position: -192px -24px; -} - -.icon-repeat { - background-position: -216px -24px; -} - -.icon-refresh { - background-position: -240px -24px; -} - -.icon-list-alt { - background-position: -264px -24px; -} - -.icon-lock { - background-position: -287px -24px; -} - -.icon-flag { - background-position: -312px -24px; -} - -.icon-headphones { - background-position: -336px -24px; -} - -.icon-volume-off { - background-position: -360px -24px; -} - -.icon-volume-down { - background-position: -384px -24px; -} - -.icon-volume-up { - background-position: -408px -24px; -} - -.icon-qrcode { - background-position: -432px -24px; -} - -.icon-barcode { - background-position: -456px -24px; -} - -.icon-tag { - background-position: 0 -48px; -} - -.icon-tags { - background-position: -25px -48px; -} - -.icon-book { - background-position: -48px -48px; -} - -.icon-bookmark { - background-position: -72px -48px; -} - -.icon-print { - background-position: -96px -48px; -} - -.icon-camera { - background-position: -120px -48px; -} - -.icon-font { - background-position: -144px -48px; -} - -.icon-bold { - background-position: -167px -48px; -} - -.icon-italic { - background-position: -192px -48px; -} - -.icon-text-height { - background-position: -216px -48px; -} - -.icon-text-width { - background-position: -240px -48px; -} - -.icon-align-left { - background-position: -264px -48px; -} - -.icon-align-center { - background-position: -288px -48px; -} - -.icon-align-right { - background-position: -312px -48px; -} - -.icon-align-justify { - background-position: -336px -48px; -} - -.icon-list { - background-position: -360px -48px; -} - -.icon-indent-left { - background-position: -384px -48px; -} - -.icon-indent-right { - background-position: -408px -48px; -} - -.icon-facetime-video { - background-position: -432px -48px; -} - -.icon-picture { - background-position: -456px -48px; -} - -.icon-pencil { - background-position: 0 -72px; -} - -.icon-map-marker { - background-position: -24px -72px; -} - -.icon-adjust { - background-position: -48px -72px; -} - -.icon-tint { - background-position: -72px -72px; -} - -.icon-edit { - background-position: -96px -72px; -} - -.icon-share { - background-position: -120px -72px; -} - -.icon-check { - background-position: -144px -72px; -} - -.icon-move { - background-position: -168px -72px; -} - -.icon-step-backward { - background-position: -192px -72px; -} - -.icon-fast-backward { - background-position: -216px -72px; -} - -.icon-backward { - background-position: -240px -72px; -} - -.icon-play { - background-position: -264px -72px; -} - -.icon-pause { - background-position: -288px -72px; -} - -.icon-stop { - background-position: -312px -72px; -} - -.icon-forward { - background-position: -336px -72px; -} - -.icon-fast-forward { - background-position: -360px -72px; -} - -.icon-step-forward { - background-position: -384px -72px; -} - -.icon-eject { - background-position: -408px -72px; -} - -.icon-chevron-left { - background-position: -432px -72px; -} - -.icon-chevron-right { - background-position: -456px -72px; -} - -.icon-plus-sign { - background-position: 0 -96px; -} - -.icon-minus-sign { - background-position: -24px -96px; -} - -.icon-remove-sign { - background-position: -48px -96px; -} - -.icon-ok-sign { - background-position: -72px -96px; -} - -.icon-question-sign { - background-position: -96px -96px; -} - -.icon-info-sign { - background-position: -120px -96px; -} - -.icon-screenshot { - background-position: -144px -96px; -} - -.icon-remove-circle { - background-position: -168px -96px; -} - -.icon-ok-circle { - background-position: -192px -96px; -} - -.icon-ban-circle { - background-position: -216px -96px; -} - -.icon-arrow-left { - background-position: -240px -96px; -} - -.icon-arrow-right { - background-position: -264px -96px; -} - -.icon-arrow-up { - background-position: -289px -96px; -} - -.icon-arrow-down { - background-position: -312px -96px; -} - -.icon-share-alt { - background-position: -336px -96px; -} - -.icon-resize-full { - background-position: -360px -96px; -} - -.icon-resize-small { - background-position: -384px -96px; -} - -.icon-plus { - background-position: -408px -96px; -} - -.icon-minus { - background-position: -433px -96px; -} - -.icon-asterisk { - background-position: -456px -96px; -} - -.icon-exclamation-sign { - background-position: 0 -120px; -} - -.icon-gift { - background-position: -24px -120px; -} - -.icon-leaf { - background-position: -48px -120px; -} - -.icon-fire { - background-position: -72px -120px; -} - -.icon-eye-open { - background-position: -96px -120px; -} - -.icon-eye-close { - background-position: -120px -120px; -} - -.icon-warning-sign { - background-position: -144px -120px; -} - -.icon-plane { - background-position: -168px -120px; -} - -.icon-calendar { - background-position: -192px -120px; -} - -.icon-random { - background-position: -216px -120px; -} - -.icon-comment { - background-position: -240px -120px; -} - -.icon-magnet { - background-position: -264px -120px; -} - -.icon-chevron-up { - background-position: -288px -120px; -} - -.icon-chevron-down { - background-position: -313px -119px; -} - -.icon-retweet { - background-position: -336px -120px; -} - -.icon-shopping-cart { - background-position: -360px -120px; -} - -.icon-folder-close { - background-position: -384px -120px; -} - -.icon-folder-open { - background-position: -408px -120px; -} - -.icon-resize-vertical { - background-position: -432px -119px; -} - -.icon-resize-horizontal { - background-position: -456px -118px; -} - -.icon-hdd { - background-position: 0 -144px; -} - -.icon-bullhorn { - background-position: -24px -144px; -} - -.icon-bell { - background-position: -48px -144px; -} - -.icon-certificate { - background-position: -72px -144px; -} - -.icon-thumbs-up { - background-position: -96px -144px; -} - -.icon-thumbs-down { - background-position: -120px -144px; -} - -.icon-hand-right { - background-position: -144px -144px; -} - -.icon-hand-left { - background-position: -168px -144px; -} - -.icon-hand-up { - background-position: -192px -144px; -} - -.icon-hand-down { - background-position: -216px -144px; -} - -.icon-circle-arrow-right { - background-position: -240px -144px; -} - -.icon-circle-arrow-left { - background-position: -264px -144px; -} - -.icon-circle-arrow-up { - background-position: -288px -144px; -} - -.icon-circle-arrow-down { - background-position: -312px -144px; -} - -.icon-globe { - background-position: -336px -144px; -} - -.icon-wrench { - background-position: -360px -144px; -} - -.icon-tasks { - background-position: -384px -144px; -} - -.icon-filter { - background-position: -408px -144px; -} - -.icon-briefcase { - background-position: -432px -144px; -} - -.icon-fullscreen { - background-position: -456px -144px; -} - -.dropup, -.dropdown { - position: relative; -} - -.dropdown-toggle { - *margin-bottom: -3px; -} - -.dropdown-toggle:active, -.open .dropdown-toggle { - outline: 0; -} - -.caret { - display: inline-block; - width: 0; - height: 0; - vertical-align: top; - border-top: 4px solid #000000; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; - opacity: 0.3; - filter: alpha(opacity=30); -} - -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; -} - -.dropdown:hover .caret, -.open .caret { - opacity: 1; - filter: alpha(opacity=100); -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 4px 0; - margin: 1px 0 0; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - *border-right-width: 2px; - *border-bottom-width: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.dropdown-menu .divider { - *width: 100%; - height: 1px; - margin: 8px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.dropdown-menu a { - display: block; - padding: 3px 15px; - clear: both; - font-weight: normal; - line-height: 18px; - color: #333333; - white-space: nowrap; -} - -.dropdown-menu li > a:hover, -.dropdown-menu .active > a, -.dropdown-menu .active > a:hover { - color: #ffffff; - text-decoration: none; - background-color: #0088cc; -} - -.open { - *z-index: 1000; -} - -.open .dropdown-menu { - display: block; -} - -.pull-right .dropdown-menu { - right: 0; - left: auto; -} - -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px solid #000000; - content: "\2191"; -} - -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; -} - -.typeahead { - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #eee; - border: 1px solid rgba(0, 0, 0, 0.05); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} - -.well-large { - padding: 24px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.well-small { - padding: 9px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.fade { - opacity: 0; - filter: alpha(opacity=0); - -webkit-transition: opacity 0.15s linear; - -moz-transition: opacity 0.15s linear; - -ms-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} - -.fade.in { - opacity: 1; - filter: alpha(opacity=100); -} - -.collapse { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition: height 0.35s ease; - -moz-transition: height 0.35s ease; - -ms-transition: height 0.35s ease; - -o-transition: height 0.35s ease; - transition: height 0.35s ease; -} - -.collapse.in { - height: auto; -} - -.close { - float: right; - font-size: 20px; - font-weight: bold; - line-height: 18px; - color: #000000; - text-shadow: 0 1px 0 #ffffff; - opacity: 0.2; - filter: alpha(opacity=20); -} - -.close:hover { - color: #000000; - text-decoration: none; - cursor: pointer; - opacity: 0.4; - filter: alpha(opacity=40); -} - -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -.btn { - display: inline-block; - *display: inline; - padding: 4px 10px 4px; - margin-bottom: 0; - *margin-left: .3em; - font-size: 13px; - line-height: 18px; - *line-height: 20px; - color: #333333; - text-align: center; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - vertical-align: middle; - cursor: pointer; - background-color: #f5f5f5; - *background-color: #e6e6e6; - background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(top, #ffffff, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border: 1px solid #cccccc; - *border: 0; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-bottom-color: #b3b3b3; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn:hover, -.btn:active, -.btn.active, -.btn.disabled, -.btn[disabled] { - background-color: #e6e6e6; - *background-color: #d9d9d9; -} - -.btn:active, -.btn.active { - background-color: #cccccc \9; -} - -.btn:first-child { - *margin-left: 0; -} - -.btn:hover { - color: #333333; - text-decoration: none; - background-color: #e6e6e6; - *background-color: #d9d9d9; - /* Buttons in IE7 don't get borders, so darken on hover */ - - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -ms-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.btn:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} - -.btn.active, -.btn:active { - background-color: #e6e6e6; - background-color: #d9d9d9 \9; - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn.disabled, -.btn[disabled] { - cursor: default; - background-color: #e6e6e6; - background-image: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.btn-large { - padding: 9px 14px; - font-size: 15px; - line-height: normal; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.btn-large [class^="icon-"] { - margin-top: 1px; -} - -.btn-small { - padding: 5px 9px; - font-size: 11px; - line-height: 16px; -} - -.btn-small [class^="icon-"] { - margin-top: -1px; -} - -.btn-mini { - padding: 2px 6px; - font-size: 11px; - line-height: 14px; -} - -.btn-primary, -.btn-primary:hover, -.btn-warning, -.btn-warning:hover, -.btn-danger, -.btn-danger:hover, -.btn-success, -.btn-success:hover, -.btn-info, -.btn-info:hover, -.btn-inverse, -.btn-inverse:hover { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-inverse.active { - color: rgba(255, 255, 255, 0.75); -} - -.btn { - border-color: #ccc; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); -} - -.btn-primary { - background-color: #0074cc; - *background-color: #0055cc; - background-image: -ms-linear-gradient(top, #0088cc, #0055cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); - background-image: -o-linear-gradient(top, #0088cc, #0055cc); - background-image: -moz-linear-gradient(top, #0088cc, #0055cc); - background-image: linear-gradient(top, #0088cc, #0055cc); - background-repeat: repeat-x; - border-color: #0055cc #0055cc #003580; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-primary:hover, -.btn-primary:active, -.btn-primary.active, -.btn-primary.disabled, -.btn-primary[disabled] { - background-color: #0055cc; - *background-color: #004ab3; -} - -.btn-primary:active, -.btn-primary.active { - background-color: #004099 \9; -} - -.btn-warning { - background-color: #faa732; - *background-color: #f89406; - background-image: -ms-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(top, #fbb450, #f89406); - background-repeat: repeat-x; - border-color: #f89406 #f89406 #ad6704; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-warning:hover, -.btn-warning:active, -.btn-warning.active, -.btn-warning.disabled, -.btn-warning[disabled] { - background-color: #f89406; - *background-color: #df8505; -} - -.btn-warning:active, -.btn-warning.active { - background-color: #c67605 \9; -} - -.btn-danger { - background-color: #da4f49; - *background-color: #bd362f; - background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); - background-image: linear-gradient(top, #ee5f5b, #bd362f); - background-repeat: repeat-x; - border-color: #bd362f #bd362f #802420; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-danger:hover, -.btn-danger:active, -.btn-danger.active, -.btn-danger.disabled, -.btn-danger[disabled] { - background-color: #bd362f; - *background-color: #a9302a; -} - -.btn-danger:active, -.btn-danger.active { - background-color: #942a25 \9; -} - -.btn-success { - background-color: #5bb75b; - *background-color: #51a351; - background-image: -ms-linear-gradient(top, #62c462, #51a351); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); - background-image: -webkit-linear-gradient(top, #62c462, #51a351); - background-image: -o-linear-gradient(top, #62c462, #51a351); - background-image: -moz-linear-gradient(top, #62c462, #51a351); - background-image: linear-gradient(top, #62c462, #51a351); - background-repeat: repeat-x; - border-color: #51a351 #51a351 #387038; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-success:hover, -.btn-success:active, -.btn-success.active, -.btn-success.disabled, -.btn-success[disabled] { - background-color: #51a351; - *background-color: #499249; -} - -.btn-success:active, -.btn-success.active { - background-color: #408140 \9; -} - -.btn-info { - background-color: #49afcd; - *background-color: #2f96b4; - background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); - background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); - background-image: linear-gradient(top, #5bc0de, #2f96b4); - background-repeat: repeat-x; - border-color: #2f96b4 #2f96b4 #1f6377; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-info:hover, -.btn-info:active, -.btn-info.active, -.btn-info.disabled, -.btn-info[disabled] { - background-color: #2f96b4; - *background-color: #2a85a0; -} - -.btn-info:active, -.btn-info.active { - background-color: #24748c \9; -} - -.btn-inverse { - background-color: #414141; - *background-color: #222222; - background-image: -ms-linear-gradient(top, #555555, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); - background-image: -webkit-linear-gradient(top, #555555, #222222); - background-image: -o-linear-gradient(top, #555555, #222222); - background-image: -moz-linear-gradient(top, #555555, #222222); - background-image: linear-gradient(top, #555555, #222222); - background-repeat: repeat-x; - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); -} - -.btn-inverse:hover, -.btn-inverse:active, -.btn-inverse.active, -.btn-inverse.disabled, -.btn-inverse[disabled] { - background-color: #222222; - *background-color: #151515; -} - -.btn-inverse:active, -.btn-inverse.active { - background-color: #080808 \9; -} - -button.btn, -input[type="submit"].btn { - *padding-top: 2px; - *padding-bottom: 2px; -} - -button.btn::-moz-focus-inner, -input[type="submit"].btn::-moz-focus-inner { - padding: 0; - border: 0; -} - -button.btn.btn-large, -input[type="submit"].btn.btn-large { - *padding-top: 7px; - *padding-bottom: 7px; -} - -button.btn.btn-small, -input[type="submit"].btn.btn-small { - *padding-top: 3px; - *padding-bottom: 3px; -} - -button.btn.btn-mini, -input[type="submit"].btn.btn-mini { - *padding-top: 1px; - *padding-bottom: 1px; -} - -.btn-group { - position: relative; - *margin-left: .3em; - *zoom: 1; -} - -.btn-group:before, -.btn-group:after { - display: table; - content: ""; -} - -.btn-group:after { - clear: both; -} - -.btn-group:first-child { - *margin-left: 0; -} - -.btn-group + .btn-group { - margin-left: 5px; -} - -.btn-toolbar { - margin-top: 9px; - margin-bottom: 9px; -} - -.btn-toolbar .btn-group { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; -} - -.btn-group > .btn { - position: relative; - float: left; - margin-left: -1px; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.btn-group > .btn:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - -moz-border-radius-topleft: 4px; -} - -.btn-group > .btn:last-child, -.btn-group > .dropdown-toggle { - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -moz-border-radius-topright: 4px; - -moz-border-radius-bottomright: 4px; -} - -.btn-group > .btn.large:first-child { - margin-left: 0; - -webkit-border-bottom-left-radius: 6px; - border-bottom-left-radius: 6px; - -webkit-border-top-left-radius: 6px; - border-top-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - -moz-border-radius-topleft: 6px; -} - -.btn-group > .btn.large:last-child, -.btn-group > .large.dropdown-toggle { - -webkit-border-top-right-radius: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - border-bottom-right-radius: 6px; - -moz-border-radius-topright: 6px; - -moz-border-radius-bottomright: 6px; -} - -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active { - z-index: 2; -} - -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - -.btn-group > .dropdown-toggle { - *padding-top: 4px; - padding-right: 8px; - *padding-bottom: 4px; - padding-left: 8px; - -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group > .btn-mini.dropdown-toggle { - padding-right: 5px; - padding-left: 5px; -} - -.btn-group > .btn-small.dropdown-toggle { - *padding-top: 4px; - *padding-bottom: 4px; -} - -.btn-group > .btn-large.dropdown-toggle { - padding-right: 12px; - padding-left: 12px; -} - -.btn-group.open .dropdown-toggle { - background-image: none; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group.open .btn.dropdown-toggle { - background-color: #e6e6e6; -} - -.btn-group.open .btn-primary.dropdown-toggle { - background-color: #0055cc; -} - -.btn-group.open .btn-warning.dropdown-toggle { - background-color: #f89406; -} - -.btn-group.open .btn-danger.dropdown-toggle { - background-color: #bd362f; -} - -.btn-group.open .btn-success.dropdown-toggle { - background-color: #51a351; -} - -.btn-group.open .btn-info.dropdown-toggle { - background-color: #2f96b4; -} - -.btn-group.open .btn-inverse.dropdown-toggle { - background-color: #222222; -} - -.btn .caret { - margin-top: 7px; - margin-left: 0; -} - -.btn:hover .caret, -.open.btn-group .caret { - opacity: 1; - filter: alpha(opacity=100); -} - -.btn-mini .caret { - margin-top: 5px; -} - -.btn-small .caret { - margin-top: 6px; -} - -.btn-large .caret { - margin-top: 6px; - border-top-width: 5px; - border-right-width: 5px; - border-left-width: 5px; -} - -.dropup .btn-large .caret { - border-top: 0; - border-bottom: 5px solid #000000; -} - -.btn-primary .caret, -.btn-warning .caret, -.btn-danger .caret, -.btn-info .caret, -.btn-success .caret, -.btn-inverse .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; - opacity: 0.75; - filter: alpha(opacity=75); -} - -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 18px; - color: #c09853; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.alert-heading { - color: inherit; -} - -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 18px; -} - -.alert-success { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; -} - -.alert-danger, -.alert-error { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; -} - -.alert-info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; -} - -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} - -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} - -.alert-block p + p { - margin-top: 5px; -} - -.nav { - margin-bottom: 18px; - margin-left: 0; - list-style: none; -} - -.nav > li > a { - display: block; -} - -.nav > li > a:hover { - text-decoration: none; - background-color: #eeeeee; -} - -.nav > .pull-right { - float: right; -} - -.nav .nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: 18px; - color: #999999; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-transform: uppercase; -} - -.nav li + .nav-header { - margin-top: 9px; -} - -.nav-list { - padding-right: 15px; - padding-left: 15px; - margin-bottom: 0; -} - -.nav-list > li > a, -.nav-list .nav-header { - margin-right: -15px; - margin-left: -15px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} - -.nav-list > li > a { - padding: 3px 15px; -} - -.nav-list > .active > a, -.nav-list > .active > a:hover { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - background-color: #0088cc; -} - -.nav-list [class^="icon-"] { - margin-right: 2px; -} - -.nav-list .divider { - *width: 100%; - height: 1px; - margin: 8px 1px; - *margin: -5px 0 5px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; -} - -.nav-tabs, -.nav-pills { - *zoom: 1; -} - -.nav-tabs:before, -.nav-pills:before, -.nav-tabs:after, -.nav-pills:after { - display: table; - content: ""; -} - -.nav-tabs:after, -.nav-pills:after { - clear: both; -} - -.nav-tabs > li, -.nav-pills > li { - float: left; -} - -.nav-tabs > li > a, -.nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; -} - -.nav-tabs { - border-bottom: 1px solid #ddd; -} - -.nav-tabs > li { - margin-bottom: -1px; -} - -.nav-tabs > li > a { - padding-top: 8px; - padding-bottom: 8px; - line-height: 18px; - border: 1px solid transparent; - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee #dddddd; -} - -.nav-tabs > .active > a, -.nav-tabs > .active > a:hover { - color: #555555; - cursor: default; - background-color: #ffffff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} - -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.nav-pills > .active > a, -.nav-pills > .active > a:hover { - color: #ffffff; - background-color: #0088cc; -} - -.nav-stacked > li { - float: none; -} - -.nav-stacked > li > a { - margin-right: 0; -} - -.nav-tabs.nav-stacked { - border-bottom: 0; -} - -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.nav-tabs.nav-stacked > li:first-child > a { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} - -.nav-tabs.nav-stacked > li:last-child > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.nav-tabs.nav-stacked > li > a:hover { - z-index: 2; - border-color: #ddd; -} - -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} - -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; -} - -.nav-tabs .dropdown-menu { - -webkit-border-radius: 0 0 5px 5px; - -moz-border-radius: 0 0 5px 5px; - border-radius: 0 0 5px 5px; -} - -.nav-pills .dropdown-menu { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.nav-tabs .dropdown-toggle .caret, -.nav-pills .dropdown-toggle .caret { - margin-top: 6px; - border-top-color: #0088cc; - border-bottom-color: #0088cc; -} - -.nav-tabs .dropdown-toggle:hover .caret, -.nav-pills .dropdown-toggle:hover .caret { - border-top-color: #005580; - border-bottom-color: #005580; -} - -.nav-tabs .active .dropdown-toggle .caret, -.nav-pills .active .dropdown-toggle .caret { - border-top-color: #333333; - border-bottom-color: #333333; -} - -.nav > .dropdown.active > a:hover { - color: #000000; - cursor: pointer; -} - -.nav-tabs .open .dropdown-toggle, -.nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover { - color: #ffffff; - background-color: #999999; - border-color: #999999; -} - -.nav li.dropdown.open .caret, -.nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; - opacity: 1; - filter: alpha(opacity=100); -} - -.tabs-stacked .open > a:hover { - border-color: #999999; -} - -.tabbable { - *zoom: 1; -} - -.tabbable:before, -.tabbable:after { - display: table; - content: ""; -} - -.tabbable:after { - clear: both; -} - -.tab-content { - overflow: auto; -} - -.tabs-below > .nav-tabs, -.tabs-right > .nav-tabs, -.tabs-left > .nav-tabs { - border-bottom: 0; -} - -.tab-content > .tab-pane, -.pill-content > .pill-pane { - display: none; -} - -.tab-content > .active, -.pill-content > .active { - display: block; -} - -.tabs-below > .nav-tabs { - border-top: 1px solid #ddd; -} - -.tabs-below > .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} - -.tabs-below > .nav-tabs > li > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} - -.tabs-below > .nav-tabs > li > a:hover { - border-top-color: #ddd; - border-bottom-color: transparent; -} - -.tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover { - border-color: transparent #ddd #ddd #ddd; -} - -.tabs-left > .nav-tabs > li, -.tabs-right > .nav-tabs > li { - float: none; -} - -.tabs-left > .nav-tabs > li > a, -.tabs-right > .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} - -.tabs-left > .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} - -.tabs-left > .nav-tabs > li > a { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} - -.tabs-left > .nav-tabs > li > a:hover { - border-color: #eeeeee #dddddd #eeeeee #eeeeee; -} - -.tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: #ffffff; -} - -.tabs-right > .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} - -.tabs-right > .nav-tabs > li > a { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} - -.tabs-right > .nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee #eeeeee #dddddd; -} - -.tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: #ffffff; -} - -.navbar { - *position: relative; - *z-index: 2; - margin-bottom: 18px; - overflow: visible; -} - -.navbar-inner { - min-height: 40px; - padding-right: 20px; - padding-left: 20px; - background-color: #2c2c2c; - background-image: -moz-linear-gradient(top, #333333, #222222); - background-image: -ms-linear-gradient(top, #333333, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); - background-image: -webkit-linear-gradient(top, #333333, #222222); - background-image: -o-linear-gradient(top, #333333, #222222); - background-image: linear-gradient(top, #333333, #222222); - background-repeat: repeat-x; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -} - -.navbar .container { - width: auto; -} - -.nav-collapse.collapse { - height: auto; -} - -.navbar { - color: #999999; -} - -.navbar .brand:hover { - text-decoration: none; -} - -.navbar .brand { - display: block; - float: left; - padding: 8px 20px 12px; - margin-left: -20px; - font-size: 20px; - font-weight: 200; - line-height: 1; - color: #999999; -} - -.navbar .navbar-text { - margin-bottom: 0; - line-height: 40px; -} - -.navbar .navbar-link { - color: #999999; -} - -.navbar .navbar-link:hover { - color: #ffffff; -} - -.navbar .btn, -.navbar .btn-group { - margin-top: 5px; -} - -.navbar .btn-group .btn { - margin: 0; -} - -.navbar-form { - margin-bottom: 0; - *zoom: 1; -} - -.navbar-form:before, -.navbar-form:after { - display: table; - content: ""; -} - -.navbar-form:after { - clear: both; -} - -.navbar-form input, -.navbar-form select, -.navbar-form .radio, -.navbar-form .checkbox { - margin-top: 5px; -} - -.navbar-form input, -.navbar-form select { - display: inline-block; - margin-bottom: 0; -} - -.navbar-form input[type="image"], -.navbar-form input[type="checkbox"], -.navbar-form input[type="radio"] { - margin-top: 3px; -} - -.navbar-form .input-append, -.navbar-form .input-prepend { - margin-top: 6px; - white-space: nowrap; -} - -.navbar-form .input-append input, -.navbar-form .input-prepend input { - margin-top: 0; -} - -.navbar-search { - position: relative; - float: left; - margin-top: 6px; - margin-bottom: 0; -} - -.navbar-search .search-query { - padding: 4px 9px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: 1; - color: #ffffff; - background-color: #626262; - border: 1px solid #151515; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); - -webkit-transition: none; - -moz-transition: none; - -ms-transition: none; - -o-transition: none; - transition: none; -} - -.navbar-search .search-query:-moz-placeholder { - color: #cccccc; -} - -.navbar-search .search-query::-webkit-input-placeholder { - color: #cccccc; -} - -.navbar-search .search-query:focus, -.navbar-search .search-query.focused { - padding: 5px 10px; - color: #333333; - text-shadow: 0 1px 0 #ffffff; - background-color: #ffffff; - border: 0; - outline: 0; - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; - margin-bottom: 0; -} - -.navbar-fixed-top .navbar-inner, -.navbar-fixed-bottom .navbar-inner { - padding-right: 0; - padding-left: 0; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - width: 940px; -} - -.navbar-fixed-top { - top: 0; -} - -.navbar-fixed-bottom { - bottom: 0; -} - -.navbar .nav { - position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} - -.navbar .nav.pull-right { - float: right; -} - -.navbar .nav > li { - display: block; - float: left; -} - -.navbar .nav > li > a { - float: none; - padding: 9px 10px 11px; - line-height: 19px; - color: #999999; - text-decoration: none; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.navbar .btn { - display: inline-block; - padding: 4px 10px 4px; - margin: 5px 5px 6px; - line-height: 18px; -} - -.navbar .btn-group { - padding: 5px 5px 6px; - margin: 0; -} - -.navbar .nav > li > a:hover { - color: #ffffff; - text-decoration: none; - background-color: transparent; -} - -.navbar .nav .active > a, -.navbar .nav .active > a:hover { - color: #ffffff; - text-decoration: none; - background-color: #222222; -} - -.navbar .divider-vertical { - width: 1px; - height: 40px; - margin: 0 9px; - overflow: hidden; - background-color: #222222; - border-right: 1px solid #333333; -} - -.navbar .nav.pull-right { - margin-right: 0; - margin-left: 10px; -} - -.navbar .btn-navbar { - display: none; - float: right; - padding: 7px 10px; - margin-right: 5px; - margin-left: 5px; - background-color: #2c2c2c; - *background-color: #222222; - background-image: -ms-linear-gradient(top, #333333, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); - background-image: -webkit-linear-gradient(top, #333333, #222222); - background-image: -o-linear-gradient(top, #333333, #222222); - background-image: linear-gradient(top, #333333, #222222); - background-image: -moz-linear-gradient(top, #333333, #222222); - background-repeat: repeat-x; - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -} - -.navbar .btn-navbar:hover, -.navbar .btn-navbar:active, -.navbar .btn-navbar.active, -.navbar .btn-navbar.disabled, -.navbar .btn-navbar[disabled] { - background-color: #222222; - *background-color: #151515; -} - -.navbar .btn-navbar:active, -.navbar .btn-navbar.active { - background-color: #080808 \9; -} - -.navbar .btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -} - -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} - -.navbar .dropdown-menu:before { - position: absolute; - top: -7px; - left: 9px; - display: inline-block; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-left: 7px solid transparent; - border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; -} - -.navbar .dropdown-menu:after { - position: absolute; - top: -6px; - left: 10px; - display: inline-block; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - border-left: 6px solid transparent; - content: ''; -} - -.navbar-fixed-bottom .dropdown-menu:before { - top: auto; - bottom: -7px; - border-top: 7px solid #ccc; - border-bottom: 0; - border-top-color: rgba(0, 0, 0, 0.2); -} - -.navbar-fixed-bottom .dropdown-menu:after { - top: auto; - bottom: -6px; - border-top: 6px solid #ffffff; - border-bottom: 0; -} - -.navbar .nav li.dropdown .dropdown-toggle .caret, -.navbar .nav li.dropdown.open .caret { - border-top-color: #ffffff; - border-bottom-color: #ffffff; -} - -.navbar .nav li.dropdown.active .caret { - opacity: 1; - filter: alpha(opacity=100); -} - -.navbar .nav li.dropdown.open > .dropdown-toggle, -.navbar .nav li.dropdown.active > .dropdown-toggle, -.navbar .nav li.dropdown.open.active > .dropdown-toggle { - background-color: transparent; -} - -.navbar .nav li.dropdown.active > .dropdown-toggle:hover { - color: #ffffff; -} - -.navbar .pull-right .dropdown-menu, -.navbar .dropdown-menu.pull-right { - right: 0; - left: auto; -} - -.navbar .pull-right .dropdown-menu:before, -.navbar .dropdown-menu.pull-right:before { - right: 12px; - left: auto; -} - -.navbar .pull-right .dropdown-menu:after, -.navbar .dropdown-menu.pull-right:after { - right: 13px; - left: auto; -} - -.breadcrumb { - padding: 7px 14px; - margin: 0 0 18px; - list-style: none; - background-color: #fbfbfb; - background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); - background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); - background-image: linear-gradient(top, #ffffff, #f5f5f5); - background-repeat: repeat-x; - border: 1px solid #ddd; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} - -.breadcrumb li { - display: inline-block; - *display: inline; - text-shadow: 0 1px 0 #ffffff; - *zoom: 1; -} - -.breadcrumb .divider { - padding: 0 5px; - color: #999999; -} - -.breadcrumb .active a { - color: #333333; -} - -.pagination { - height: 36px; - margin: 18px 0; -} - -.pagination ul { - display: inline-block; - *display: inline; - margin-bottom: 0; - margin-left: 0; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - *zoom: 1; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.pagination li { - display: inline; -} - -.pagination a { - float: left; - padding: 0 14px; - line-height: 34px; - text-decoration: none; - border: 1px solid #ddd; - border-left-width: 0; -} - -.pagination a:hover, -.pagination .active a { - background-color: #f5f5f5; -} - -.pagination .active a { - color: #999999; - cursor: default; -} - -.pagination .disabled span, -.pagination .disabled a, -.pagination .disabled a:hover { - color: #999999; - cursor: default; - background-color: transparent; -} - -.pagination li:first-child a { - border-left-width: 1px; - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} - -.pagination li:last-child a { - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} - -.pagination-centered { - text-align: center; -} - -.pagination-right { - text-align: right; -} - -.pager { - margin-bottom: 18px; - margin-left: 0; - text-align: center; - list-style: none; - *zoom: 1; -} - -.pager:before, -.pager:after { - display: table; - content: ""; -} - -.pager:after { - clear: both; -} - -.pager li { - display: inline; -} - -.pager a { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} - -.pager a:hover { - text-decoration: none; - background-color: #f5f5f5; -} - -.pager .next a { - float: right; -} - -.pager .previous a { - float: left; -} - -.pager .disabled a, -.pager .disabled a:hover { - color: #999999; - cursor: default; - background-color: #fff; -} - -.modal-open .dropdown-menu { - z-index: 2050; -} - -.modal-open .dropdown.open { - *z-index: 2050; -} - -.modal-open .popover { - z-index: 2060; -} - -.modal-open .tooltip { - z-index: 2070; -} - -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} - -.modal-backdrop.fade { - opacity: 0; -} - -.modal-backdrop, -.modal-backdrop.fade.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.modal { - position: fixed; - top: 50%; - left: 50%; - z-index: 1050; - width: 560px; - margin: -250px 0 0 -280px; - overflow: auto; - background-color: #ffffff; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, 0.3); - *border: 1px solid #999; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} - -.modal.fade { - top: -25%; - -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; - -moz-transition: opacity 0.3s linear, top 0.3s ease-out; - -ms-transition: opacity 0.3s linear, top 0.3s ease-out; - -o-transition: opacity 0.3s linear, top 0.3s ease-out; - transition: opacity 0.3s linear, top 0.3s ease-out; -} - -.modal.fade.in { - top: 50%; -} - -.modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; -} - -.modal-header .close { - margin-top: 2px; -} - -.modal-body { - max-height: 400px; - padding: 15px; - overflow-y: auto; -} - -.modal-form { - margin-bottom: 0; -} - -.modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; - text-align: right; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - *zoom: 1; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} - -.modal-footer:before, -.modal-footer:after { - display: table; - content: ""; -} - -.modal-footer:after { - clear: both; -} - -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} - -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} - -.tooltip { - position: absolute; - z-index: 1020; - display: block; - padding: 5px; - font-size: 11px; - opacity: 0; - filter: alpha(opacity=0); - visibility: visible; -} - -.tooltip.in { - opacity: 0.8; - filter: alpha(opacity=80); -} - -.tooltip.top { - margin-top: -2px; -} - -.tooltip.right { - margin-left: 2px; -} - -.tooltip.bottom { - margin-top: 2px; -} - -.tooltip.left { - margin-left: -2px; -} - -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top: 5px solid #000000; - border-right: 5px solid transparent; - border-left: 5px solid transparent; -} - -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #000000; -} - -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-right: 5px solid transparent; - border-bottom: 5px solid #000000; - border-left: 5px solid transparent; -} - -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-right: 5px solid #000000; - border-bottom: 5px solid transparent; -} - -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; -} - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - display: none; - padding: 5px; -} - -.popover.top { - margin-top: -5px; -} - -.popover.right { - margin-left: 5px; -} - -.popover.bottom { - margin-top: 5px; -} - -.popover.left { - margin-left: -5px; -} - -.popover.top .arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top: 5px solid #000000; - border-right: 5px solid transparent; - border-left: 5px solid transparent; -} - -.popover.right .arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-right: 5px solid #000000; - border-bottom: 5px solid transparent; -} - -.popover.bottom .arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-right: 5px solid transparent; - border-bottom: 5px solid #000000; - border-left: 5px solid transparent; -} - -.popover.left .arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #000000; -} - -.popover .arrow { - position: absolute; - width: 0; - height: 0; -} - -.popover-inner { - width: 280px; - padding: 3px; - overflow: hidden; - background: #000000; - background: rgba(0, 0, 0, 0.8); - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -} - -.popover-title { - padding: 9px 15px; - line-height: 1; - background-color: #f5f5f5; - border-bottom: 1px solid #eee; - -webkit-border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} - -.popover-content { - padding: 14px; - background-color: #ffffff; - -webkit-border-radius: 0 0 3px 3px; - -moz-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} - -.popover-content p, -.popover-content ul, -.popover-content ol { - margin-bottom: 0; -} - -.thumbnails { - margin-left: -20px; - list-style: none; - *zoom: 1; -} - -.thumbnails:before, -.thumbnails:after { - display: table; - content: ""; -} - -.thumbnails:after { - clear: both; -} - -.row-fluid .thumbnails { - margin-left: 0; -} - -.thumbnails > li { - float: left; - margin-bottom: 18px; - margin-left: 20px; -} - -.thumbnail { - display: block; - padding: 4px; - line-height: 1; - border: 1px solid #ddd; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); -} - -a.thumbnail:hover { - border-color: #0088cc; - -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -} - -.thumbnail > img { - display: block; - max-width: 100%; - margin-right: auto; - margin-left: auto; -} - -.thumbnail .caption { - padding: 9px; -} - -.label, -.badge { - font-size: 10.998px; - font-weight: bold; - line-height: 14px; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - white-space: nowrap; - vertical-align: baseline; - background-color: #999999; -} - -.label { - padding: 1px 4px 2px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -.badge { - padding: 1px 9px 2px; - -webkit-border-radius: 9px; - -moz-border-radius: 9px; - border-radius: 9px; -} - -a.label:hover, -a.badge:hover { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} - -.label-important, -.badge-important { - background-color: #b94a48; -} - -.label-important[href], -.badge-important[href] { - background-color: #953b39; -} - -.label-warning, -.badge-warning { - background-color: #f89406; -} - -.label-warning[href], -.badge-warning[href] { - background-color: #c67605; -} - -.label-success, -.badge-success { - background-color: #468847; -} - -.label-success[href], -.badge-success[href] { - background-color: #356635; -} - -.label-info, -.badge-info { - background-color: #3a87ad; -} - -.label-info[href], -.badge-info[href] { - background-color: #2d6987; -} - -.label-inverse, -.badge-inverse { - background-color: #333333; -} - -.label-inverse[href], -.badge-inverse[href] { - background-color: #1a1a1a; -} - -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-moz-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-ms-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -@-o-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} - -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} - -.progress { - height: 18px; - margin-bottom: 18px; - overflow: hidden; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(top, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.progress .bar { - width: 0; - height: 18px; - font-size: 12px; - color: #ffffff; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(top, #149bdf, #0480be); - background-image: -ms-linear-gradient(top, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - -ms-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} - -.progress-striped .bar { - background-color: #149bdf; - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; -} - -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} - -.progress-danger .bar { - background-color: #dd514c; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(top, #ee5f5b, #c43c35); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); -} - -.progress-danger.progress-striped .bar { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-success .bar { - background-color: #5eb95e; - background-image: -moz-linear-gradient(top, #62c462, #57a957); - background-image: -ms-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); - background-image: -webkit-linear-gradient(top, #62c462, #57a957); - background-image: -o-linear-gradient(top, #62c462, #57a957); - background-image: linear-gradient(top, #62c462, #57a957); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); -} - -.progress-success.progress-striped .bar { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-info .bar { - background-color: #4bb1cf; - background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); - background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); - background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); - background-image: -o-linear-gradient(top, #5bc0de, #339bb9); - background-image: linear-gradient(top, #5bc0de, #339bb9); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); -} - -.progress-info.progress-striped .bar { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.progress-warning .bar { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -ms-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(top, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); -} - -.progress-warning.progress-striped .bar { - background-color: #fbb450; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} - -.accordion { - margin-bottom: 18px; -} - -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.accordion-heading { - border-bottom: 0; -} - -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -.accordion-toggle { - cursor: pointer; -} - -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} - -.carousel { - position: relative; - margin-bottom: 18px; - line-height: 1; -} - -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} - -.carousel .item { - position: relative; - display: none; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -ms-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} - -.carousel .item > img { - display: block; - line-height: 1; -} - -.carousel .active, -.carousel .next, -.carousel .prev { - display: block; -} - -.carousel .active { - left: 0; -} - -.carousel .next, -.carousel .prev { - position: absolute; - top: 0; - width: 100%; -} - -.carousel .next { - left: 100%; -} - -.carousel .prev { - left: -100%; -} - -.carousel .next.left, -.carousel .prev.right { - left: 0; -} - -.carousel .active.left { - left: -100%; -} - -.carousel .active.right { - left: 100%; -} - -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - -webkit-border-radius: 23px; - -moz-border-radius: 23px; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} - -.carousel-control.right { - right: 15px; - left: auto; -} - -.carousel-control:hover { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} - -.carousel-caption { - position: absolute; - right: 0; - bottom: 0; - left: 0; - padding: 10px 15px 5px; - background: #333333; - background: rgba(0, 0, 0, 0.75); -} - -.carousel-caption h4, -.carousel-caption p { - color: #ffffff; -} - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - background-color: #eeeeee; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} - -.hero-unit h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - letter-spacing: -1px; - color: inherit; -} - -.hero-unit p { - font-size: 18px; - font-weight: 200; - line-height: 27px; - color: inherit; -} - -.pull-right { - float: right; -} - -.pull-left { - float: left; -} - -.hide { - display: none; -} - -.show { - display: block; -} - -.invisible { - visibility: hidden; -} diff --git a/data/css/lib/images/tablesorter/asc.gif b/data/css/lib/images/tablesorter/asc.gif new file mode 100644 index 0000000000000000000000000000000000000000..fa3fe40113a4482d2d3ff9b39c9056067477999e Binary files /dev/null and b/data/css/lib/images/tablesorter/asc.gif differ diff --git a/data/css/lib/images/tablesorter/bg.gif b/data/css/lib/images/tablesorter/bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..40c6a65aa2862b0939e2707b4d9c1174225f21c1 Binary files /dev/null and b/data/css/lib/images/tablesorter/bg.gif differ diff --git a/data/css/lib/images/tablesorter/desc.gif b/data/css/lib/images/tablesorter/desc.gif new file mode 100644 index 0000000000000000000000000000000000000000..88c6971d61ea3eecd468cd38c296346ef8b21bd1 Binary files /dev/null and b/data/css/lib/images/tablesorter/desc.gif differ diff --git a/data/css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png b/data/css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png deleted file mode 100644 index 46567420785a91503f30de00eec3a331cd4a969e..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png and /dev/null differ diff --git a/data/css/lib/images/ui-bg_flat_0_000000_40x100.png b/data/css/lib/images/ui-bg_flat_0_000000_40x100.png deleted file mode 100644 index abdc01082bf3534eafecc5819d28c9574d44ea89..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_flat_0_000000_40x100.png and /dev/null differ diff --git a/data/css/lib/images/ui-bg_flat_0_ffffff_40x100.png b/data/css/lib/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from data/css/lib/images/ui-bg_flat_0_ffffff_40x100.png rename to data/css/lib/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/data/css/lib/images/ui-bg_glass_55_fbf9ee_1x400.png b/data/css/lib/images/ui-bg_glass_55_fbf9ee_1x400.png index b39a6fb27ffbb1f3712e6cfa09e32d8ac084469b..ad3d6346e00f246102f72f2e026ed0491988b394 100644 Binary files a/data/css/lib/images/ui-bg_glass_55_fbf9ee_1x400.png and b/data/css/lib/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/data/css/lib/images/ui-bg_glass_65_ffffff_1x400.png b/data/css/lib/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..42ccba269b6e91bef12ad0fa18be651b5ef0ee68 Binary files /dev/null and b/data/css/lib/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/data/css/lib/images/ui-bg_glass_75_dadada_1x400.png b/data/css/lib/images/ui-bg_glass_75_dadada_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..5a46b47cb16631068aee9e0bd61269fc4e95e5cd Binary files /dev/null and b/data/css/lib/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/data/css/lib/images/ui-bg_glass_75_e6e6e6_1x400.png b/data/css/lib/images/ui-bg_glass_75_e6e6e6_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..86c2baa655eac8539db34f8d9adb69ec1226201c Binary files /dev/null and b/data/css/lib/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/data/css/lib/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/data/css/lib/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9fa6c6edcfcdd3e5b77e6f547b719e6fc66e30 Binary files /dev/null and b/data/css/lib/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/data/css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png b/data/css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png deleted file mode 100644 index 66acd27c22878fbd9c68d3e8b88ba292d595c118..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png and /dev/null differ diff --git a/data/css/lib/images/ui-bg_highlight-soft_75_dddddd_1x100.png b/data/css/lib/images/ui-bg_highlight-soft_75_dddddd_1x100.png deleted file mode 100644 index 8392ac785d9d71c0a05b2af8d1c516421bc03de0..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_highlight-soft_75_dddddd_1x100.png and /dev/null differ diff --git a/data/css/lib/images/ui-bg_highlight-soft_75_efefef_1x100.png b/data/css/lib/images/ui-bg_highlight-soft_75_efefef_1x100.png deleted file mode 100644 index 077a5024dbd1dd8e8724f41a10bd8314eb57c9b9..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_highlight-soft_75_efefef_1x100.png and /dev/null differ diff --git a/data/css/lib/images/ui-bg_inset-soft_75_dfdfdf_1x100.png b/data/css/lib/images/ui-bg_inset-soft_75_dfdfdf_1x100.png deleted file mode 100644 index c33667aee3d67c6620bdcf7b8a6a7b1c40d4a9ea..0000000000000000000000000000000000000000 Binary files a/data/css/lib/images/ui-bg_inset-soft_75_dfdfdf_1x100.png and /dev/null differ diff --git a/data/css/lib/images/ui-icons_8c291d_256x240.png b/data/css/lib/images/ui-icons_454545_256x240.png similarity index 92% rename from data/css/lib/images/ui-icons_8c291d_256x240.png rename to data/css/lib/images/ui-icons_454545_256x240.png index a90f0cec755b4a555e4d505ed45a02893c305dd5..59bd45b907c4fd965697774ce8c5fc6b2fd9c105 100644 Binary files a/data/css/lib/images/ui-icons_8c291d_256x240.png and b/data/css/lib/images/ui-icons_454545_256x240.png differ diff --git a/data/css/lib/images/ui-icons_888888_256x240.png b/data/css/lib/images/ui-icons_888888_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..6d02426c114be4b57aabc0a80b8a63d9e56b9eb6 Binary files /dev/null and b/data/css/lib/images/ui-icons_888888_256x240.png differ diff --git a/data/css/lib/jquery-ui-1.8.17.custom.css b/data/css/lib/jquery-ui-1.8.17.custom.css new file mode 100644 index 0000000000000000000000000000000000000000..620035d160e0a16e75f6f25ec6c539f2c3a57999 --- /dev/null +++ b/data/css/lib/jquery-ui-1.8.17.custom.css @@ -0,0 +1,567 @@ +/*! + * jQuery UI CSS Framework 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } +.ui-helper-clearfix:after { clear: both; } +.ui-helper-clearfix { zoom: 1; } +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } + + +/*! + * jQuery UI CSS Framework 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; } +.ui-widget .ui-widget { font-size: 1em; } +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } +.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; } +.ui-widget-content a { color: #222222; } +.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; } +.ui-widget-header a { color: #222222; } + +/* Interaction states +----------------------------------*/ +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; } +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; } +.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; } +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; } +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; } +.ui-widget :active { outline: none; } + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; } +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; } +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; } +.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; } +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } +.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } +.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); } +.ui-pnotify-icon span { background-image: url(images/ui-icons_222222_256x240.png) !important;} + +/* positioning */ +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-off { background-position: -96px -144px; } +.ui-icon-radio-on { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } + +/* Overlays */ +.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); } +.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*! + * jQuery UI Resizable 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Resizable#theming + */ +.ui-resizable { position: relative;} +.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; } +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } +.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } +.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } +.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } +.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } +.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } +.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } +.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } +.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*! + * jQuery UI Selectable 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Selectable#theming + */ +.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } +/*! + * jQuery UI Accordion 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Accordion#theming + */ +/* IE/Win - Fix animation bug - #4615 */ +.ui-accordion { width: 100%; } +.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } +.ui-accordion .ui-accordion-li-fix { display: inline; } +.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } +.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } +.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } +.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } +.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } +.ui-accordion .ui-accordion-content-active { display: block; } +/*! + * jQuery UI Autocomplete 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete#theming + */ +.ui-autocomplete { position: absolute; cursor: default; } + +/* workarounds */ +* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ + +/* + * jQuery UI Menu 1.8.20 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu#theming + */ +.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.ui-menu .ui-menu { + margin-top: -3px; +} +.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.ui-menu .ui-menu-item a.ui-state-hover, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +/*! + * jQuery UI Button 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Button#theming + */ +.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ +.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ +button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ +.ui-button-icons-only { width: 3.4em; } +button.ui-button-icons-only { width: 3.7em; } + +/*button text element */ +.ui-button .ui-button-text { display: block; line-height: 1.4; } +.ui-button-text-only .ui-button-text { padding: .4em 1em; } +.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } +.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } +.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } +.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } +/* no icon support for input elements, provide padding by default */ +input.ui-button { padding: .4em 1em; } + +/*button icon element(s) */ +.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } +.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } +.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } +.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } +.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } + +/*button sets*/ +.ui-buttonset { margin-right: 7px; } +.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } + +/* workarounds */ +button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ +/*! + * jQuery UI Dialog 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog#theming + */ +.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } +.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } +.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } +.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } +.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } +.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } +.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } +.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } +.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } +.ui-draggable .ui-dialog-titlebar { cursor: move; } +/*! + * jQuery UI Slider 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Slider#theming + */ +.ui-slider { position: relative; text-align: left; } +.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } +.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } + +.ui-slider-horizontal { height: .8em; } +.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } +.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } +.ui-slider-horizontal .ui-slider-range-min { left: 0; } +.ui-slider-horizontal .ui-slider-range-max { right: 0; } + +.ui-slider-vertical { width: .8em; height: 100px; } +.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } +.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } +.ui-slider-vertical .ui-slider-range-min { bottom: 0; } +.ui-slider-vertical .ui-slider-range-max { top: 0; }/*! + * jQuery UI Tabs 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs#theming + */ +.ui-tabs { position: relative; padding: 0em; zoom: 1; background: transparent; border: none;} /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ +.ui-tabs .ui-tabs-nav { margin: 0; padding: 0; background: none; border:none;} +.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap;background: #FFF; } +.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; font-weight: normal; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; background-color: #f7f7f7; border-color: #CCC; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a { color: #454545; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: #f7f7f7; border: 1px solid #CCC; } +.ui-tabs .ui-tabs-hide { display: none !important; } +/*! + * jQuery UI Datepicker 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Datepicker#theming + */ +.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } +.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } +.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } +.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } +.ui-datepicker .ui-datepicker-prev { left:2px; } +.ui-datepicker .ui-datepicker-next { right:2px; } +.ui-datepicker .ui-datepicker-prev-hover { left:1px; } +.ui-datepicker .ui-datepicker-next-hover { right:1px; } +.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } +.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } +.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } +.ui-datepicker select.ui-datepicker-month-year {width: 100%;} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { width: 49%;} +.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } +.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } +.ui-datepicker td { border: 0; padding: 1px; } +.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } +.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } +.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { width:auto; } +.ui-datepicker-multi .ui-datepicker-group { float:left; } +.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } +.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } +.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } +.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } +.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } +.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } + +/* RTL support */ +.ui-datepicker-rtl { direction: rtl; } +.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } +.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } +.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } +.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } +.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } +.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } +.ui-datepicker-rtl .ui-datepicker-group { float:right; } +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } + +/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ +.ui-datepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ +}/*! + * jQuery UI Progressbar 1.8.20 + * + * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar#theming + */ +.ui-progressbar { height:2em; text-align: left; overflow: hidden; } +.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } \ No newline at end of file diff --git a/data/css/lib/jquery.pnotify.default.css b/data/css/lib/jquery.pnotify.default.css index 896fcb8afbfbc610e6b4f7372d9b4b9eeff9c877..5fc8b5b9d4228ad6679085a89cad4635d0deacb7 100644 --- a/data/css/lib/jquery.pnotify.default.css +++ b/data/css/lib/jquery.pnotify.default.css @@ -1,83 +1,101 @@ -/* -Document : jquery.pnotify.default.css -Created on : Nov 23, 2009, 3:14:10 PM -Author : Hunter Perrin -Version : 1.2.0 -Link : http://pinesframework.org/pnotify/ -Description: - Default styling for Pines Notify jQuery plugin. +/* + Document : jquery.pnotify.default.css + Created on : Nov 23, 2009, 3:14:10 PM + Author : Hunter Perrin + Version : 1.0.0 + Description: + Default styling for Pines Notify jQuery plugin. */ -/* -- Notice */ + +/* Notice +----------------------------------*/ .ui-pnotify { -top: 10px; -right: 15px; -position: absolute; -height: auto; -/* Ensures notices are above everything */ -z-index: 9999; + position: fixed; + right: 10px; + bottom: 10px; + /* Ensure that the notices are on top of everything else. */ + z-index: 9999; } -/* Hides position: fixed from IE6 */ +/* This hides position: fixed from IE6, which doesn't understand it. */ html > body .ui-pnotify { -position: fixed; + position: fixed; } -.ui-pnotify .ui-pnotify-shadow { --webkit-box-shadow: 0px 2px 10px rgba(50, 50, 50, 0.5); --moz-box-shadow: 0px 2px 10px rgba(50, 50, 50, 0.5); -box-shadow: 0px 2px 10px rgba(50, 50, 50, 0.5); +.ui-pnotify .ui-widget { + background: none; } .ui-pnotify-container { -background-position: 0 0; -padding: .8em; -height: 100%; -margin: 0; + background-position: 0 0; + border: 1px solid #cccccc; + background-image: -moz-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -webkit-linear-gradient(#fdf0d5, #fff9ee) !important; + background-image: -o-linear-gradient(#fdf0d5, #fff9ee) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + font-size: 14px; + -moz-box-shadow: 0px 0px 2px #aaaaaa; + -webkit-box-shadow: 0px 0px 2px #aaaaaa; + -o-box-shadow: 0px 0px 2px #aaaaaa; + box-shadow: 0px 0px 2px #aaaaaa; + padding: 7px 10px; + text-align: center; + min-height: 22px; + width: 250px; + z-index: 9999; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; + filter: alpha(opacity=85); + -moz-opacity: 0.8 !important; + -khtml-opacity: 0.8 !important; + -o-opacity: 0.8 !important; + opacity: 0.8 !important; } -.ui-pnotify-sharp { --webkit-border-radius: 0; --moz-border-radius: 0; -border-radius: 0; -} -.ui-pnotify-closer, .ui-pnotify-sticker { -float: right; -margin-left: .2em; +.ui-pnotify-closer { + float: right; + margin-left: .2em; } .ui-pnotify-title { -display: block; -margin-bottom: .4em; + display: block; + background: none; + font-size: 14px; + font-weight: bold; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; } .ui-pnotify-text { -display: block; + display: block; + font-size: 14px; + font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; + line-height: normal; } .ui-pnotify-icon, .ui-pnotify-icon span { -display: block; -float: left; -margin-right: .2em; + display: block; + float: left; + margin-right: .2em; } -/* -- History Pulldown */ +/* History Pulldown +----------------------------------*/ .ui-pnotify-history-container { -position: absolute; -top: 0; -right: 18px; -width: 70px; -border-top: none; -padding: 0; --webkit-border-top-left-radius: 0; --moz-border-top-left-radius: 0; -border-top-left-radius: 0; --webkit-border-top-right-radius: 0; --moz-border-top-right-radius: 0; -border-top-right-radius: 0; -/* Ensures history container is above notices. */ -z-index: 10000; + position: absolute; + top: 0; + right: 18px; + width: 70px; + border-top: none; + /* Ensure that the history container is on top of the notices. */ + z-index: 10000; } .ui-pnotify-history-container .ui-pnotify-history-header { -padding: 2px; + /*padding: 2px;*/ } .ui-pnotify-history-container button { -cursor: pointer; -display: block; -width: 100%; + cursor: pointer; + display: block; + width: 100%; } .ui-pnotify-history-container .ui-pnotify-history-pulldown { -display: block; -margin: 0 auto; -} \ No newline at end of file + display: block; + margin: 0 auto; +} diff --git a/data/css/lib/jquery.qtip2.css b/data/css/lib/jquery.qtip2.css new file mode 100644 index 0000000000000000000000000000000000000000..173ce4ba2a0589c2bc7681f48efe75efa2375fde --- /dev/null +++ b/data/css/lib/jquery.qtip2.css @@ -0,0 +1,536 @@ +/* +* qTip2 - Pretty powerful tooltips +* http://craigsworks.com/projects/qtip2/ +* +* Version: nightly +* Copyright 2009-2010 Craig Michael Thompson - http://craigsworks.com +* +* Dual licensed under MIT or GPLv2 licenses +* http://en.wikipedia.org/wiki/MIT_License +* http://en.wikipedia.org/wiki/GNU_General_Public_License +* +* Date: Thu Nov 17 12:01:03.0000000000 2011 +*/ + +/* Core qTip styles */ +.ui-tooltip, .qtip{ + position: absolute; + left: -28000px; + top: -28000px; + display: none; + + max-width: 280px; + min-width: 50px; + + font-size: 10.5px; + line-height: 12px; + + z-index: 15000; +} + + /* Fluid class for determining actual width in IE */ + .ui-tooltip-fluid{ + display: block; + visibility: hidden; + position: static !important; + float: left !important; + } + + .ui-tooltip-content{ + position: relative; + padding: 5px 9px; + overflow: hidden; + + border-width: 1px; + border-style: solid; + + text-align: left; + word-wrap: break-word; + overflow: hidden; + } + + .ui-tooltip-titlebar{ + position: relative; + min-height: 14px; + padding: 5px 35px 5px 10px; + overflow: hidden; + + border-width: 1px 1px 0; + border-style: solid; + + font-weight: bold; + } + + .ui-tooltip-titlebar + .ui-tooltip-content{ border-top-width: 0px !important; } + + /*! Default close button class */ + .ui-tooltip-titlebar .ui-state-default{ + position: absolute; + right: 4px; + top: 50%; + margin-top: -9px; + + cursor: pointer; + outline: medium none; + + border-width: 1px; + border-style: solid; + } + + * html .ui-tooltip-titlebar .ui-state-default{ top: 16px; } /* IE fix */ + + .ui-tooltip-titlebar .ui-icon, + .ui-tooltip-icon .ui-icon{ + display: block; + text-indent: -1000em; + } + + .ui-tooltip-icon, .ui-tooltip-icon .ui-icon{ + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + } + + .ui-tooltip-icon .ui-icon{ + width: 18px; + height: 14px; + + text-align: center; + text-indent: 0; + font: normal bold 10px/13px Tahoma,sans-serif; + + color: inherit; + background: transparent none no-repeat -100em -100em; + } + + +/* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ +.ui-tooltip-focus{ + +} + +/* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */ +.ui-tooltip-hover{ + +} + + +/*! Default tooltip style */ +.ui-tooltip-default .ui-tooltip-titlebar, +.ui-tooltip-default .ui-tooltip-content{ + border-color: #F1D031; + background-color: #FFFFA3; + color: #555; +} + + .ui-tooltip-default .ui-tooltip-titlebar{ + background-color: #FFEF93; + } + + .ui-tooltip-default .ui-tooltip-icon{ + border-color: #CCC; + background: #F1F1F1; + color: #777; + } + + .ui-tooltip-default .ui-tooltip-titlebar .ui-state-hover{ + border-color: #AAA; + color: #111; + } + +/* Tips plugin */ +.ui-tooltip .ui-tooltip-tip{ + margin: 0 auto; + overflow: hidden; + z-index: 10; +} + + .ui-tooltip .ui-tooltip-tip, + .ui-tooltip .ui-tooltip-tip *{ + position: absolute; + + line-height: 0.1px !important; + font-size: 0.1px !important; + color: #123456; + + background: transparent; + border: 0px dashed transparent; + } + + .ui-tooltip .ui-tooltip-tip canvas{ top: 0; left: 0; } + + +/*! Light tooltip style */ +.ui-tooltip-light .ui-tooltip-titlebar, +.ui-tooltip-light .ui-tooltip-content{ + border-color: #E2E2E2; + color: #454545; +} + + .ui-tooltip-light .ui-tooltip-content{ + background-color: white; + } + + .ui-tooltip-light .ui-tooltip-titlebar{ + background-color: #f1f1f1; + } + + +/*! Dark tooltip style */ +.ui-tooltip-dark .ui-tooltip-titlebar, +.ui-tooltip-dark .ui-tooltip-content{ + border-color: #303030; + color: #f3f3f3; +} + + .ui-tooltip-dark .ui-tooltip-content{ + background-color: #505050; + } + + .ui-tooltip-dark .ui-tooltip-titlebar{ + background-color: #404040; + } + + .ui-tooltip-dark .ui-tooltip-icon{ + border-color: #444; + } + + .ui-tooltip-dark .ui-tooltip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/*! Cream tooltip style */ +.ui-tooltip-cream .ui-tooltip-titlebar, +.ui-tooltip-cream .ui-tooltip-content{ + border-color: #F9E98E; + color: #A27D35; +} + + .ui-tooltip-cream .ui-tooltip-content{ + background-color: #FBF7AA; + } + + .ui-tooltip-cream .ui-tooltip-titlebar{ + background-color: #F0DE7D; + } + + .ui-tooltip-cream .ui-state-default .ui-tooltip-icon{ + background-position: -82px 0; + } + + +/*! Red tooltip style */ +.ui-tooltip-red .ui-tooltip-titlebar, +.ui-tooltip-red .ui-tooltip-content{ + border-color: #D95252; + color: #912323; +} + + .ui-tooltip-red .ui-tooltip-content{ + background-color: #F78B83; + } + + .ui-tooltip-red .ui-tooltip-titlebar{ + background-color: #F06D65; + } + + .ui-tooltip-red .ui-state-default .ui-tooltip-icon{ + background-position: -102px 0; + } + + .ui-tooltip-red .ui-tooltip-icon{ + border-color: #D95252; + } + + .ui-tooltip-red .ui-tooltip-titlebar .ui-state-hover{ + border-color: #D95252; + } + + +/*! Green tooltip style */ +.ui-tooltip-green .ui-tooltip-titlebar, +.ui-tooltip-green .ui-tooltip-content{ + border-color: #90D93F; + color: #3F6219; +} + + .ui-tooltip-green .ui-tooltip-content{ + background-color: #CAED9E; + } + + .ui-tooltip-green .ui-tooltip-titlebar{ + background-color: #B0DE78; + } + + .ui-tooltip-green .ui-state-default .ui-tooltip-icon{ + background-position: -42px 0; + } + + +/*! Blue tooltip style */ +.ui-tooltip-blue .ui-tooltip-titlebar, +.ui-tooltip-blue .ui-tooltip-content{ + border-color: #ADD9ED; + color: #5E99BD; +} + + .ui-tooltip-blue .ui-tooltip-content{ + background-color: #E5F6FE; + } + + .ui-tooltip-blue .ui-tooltip-titlebar{ + background-color: #D0E9F5; + } + + .ui-tooltip-blue .ui-state-default .ui-tooltip-icon{ + background-position: -2px 0; + } + +/*! Add shadows to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE6+, Safari 2+ */ +.ui-tooltip-shadow{ + -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); +} + + .ui-tooltip-shadow .ui-tooltip-titlebar, + .ui-tooltip-shadow .ui-tooltip-content{ + filter: progid:DXImageTransform.Microsoft.Shadow(Color='gray', Direction=135, Strength=3); + -ms-filter:"progid:DXImageTransform.Microsoft.Shadow(Color='gray', Direction=135, Strength=3)"; + + _margin-bottom: -3px; /* IE6 */ + .margin-bottom: -3px; /* IE7 */ + } + + +/*! Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */ +.ui-tooltip-rounded, +.ui-tooltip-rounded .ui-tooltip-content, +.ui-tooltip-tipsy, +.ui-tooltip-tipsy .ui-tooltip-content, +.ui-tooltip-youtube, +.ui-tooltip-youtube .ui-tooltip-content{ + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +.ui-tooltip-rounded .ui-tooltip-titlebar, +.ui-tooltip-tipsy .ui-tooltip-titlebar, +.ui-tooltip-youtube .ui-tooltip-titlebar{ + -moz-border-radius: 5px 5px 0 0; + -webkit-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.ui-tooltip-rounded .ui-tooltip-titlebar + .ui-tooltip-content, +.ui-tooltip-tipsy .ui-tooltip-titlebar + .ui-tooltip-content, +.ui-tooltip-youtube .ui-tooltip-titlebar + .ui-tooltip-content{ + -moz-border-radius: 0 0 5px 5px; + -webkit-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; +} + + +/*! Youtube tooltip style */ +.ui-tooltip-youtube{ + -webkit-box-shadow: 0 0 3px #333; + -moz-box-shadow: 0 0 3px #333; + box-shadow: 0 0 3px #333; +} + + .ui-tooltip-youtube .ui-tooltip-titlebar, + .ui-tooltip-youtube .ui-tooltip-content{ + _margin-bottom: 0; /* IE6 */ + .margin-bottom: 0; /* IE7 */ + + background: transparent; + background: rgba(0, 0, 0, 0.85); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)"; + + color: white; + border-color: #CCCCCC; + } + + .ui-tooltip-youtube .ui-tooltip-icon{ + border-color: #222; + } + + .ui-tooltip-youtube .ui-tooltip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/* jQuery TOOLS Tooltip style */ +.ui-tooltip-jtools{ + background: #232323; + background: rgba(0, 0, 0, 0.7); + background-image: -moz-linear-gradient(top, #717171, #232323); + background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323)); + + border: 2px solid #ddd; + border: 2px solid rgba(241,241,241,1); + + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + + -webkit-box-shadow: 0 0 12px #333; + -moz-box-shadow: 0 0 12px #333; + box-shadow: 0 0 12px #333; +} + + /* IE Specific */ + .ui-tooltip-jtools .ui-tooltip-titlebar{ + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"; + } + .ui-tooltip-jtools .ui-tooltip-content{ + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"; + } + + .ui-tooltip-jtools .ui-tooltip-titlebar, + .ui-tooltip-jtools .ui-tooltip-content{ + background: transparent; + color: white; + border: 0 dashed transparent; + } + + .ui-tooltip-jtools .ui-tooltip-icon{ + border-color: #555; + } + + .ui-tooltip-jtools .ui-tooltip-titlebar .ui-state-hover{ + border-color: #333; + } + + +/* Cluetip style */ +.ui-tooltip-cluetip{ + -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); + -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); + box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); +} + + .ui-tooltip-cluetip .ui-tooltip-titlebar{ + background-color: #87876A; + color: white; + border: 0 dashed transparent; + } + + .ui-tooltip-cluetip .ui-tooltip-content{ + background-color: #D9D9C2; + color: #111; + border: 0 dashed transparent; + } + + .ui-tooltip-cluetip .ui-tooltip-icon{ + border-color: #808064; + } + + .ui-tooltip-cluetip .ui-tooltip-titlebar .ui-state-hover{ + border-color: #696952; + color: #696952; + } + + +/* Tipsy style */ +.ui-tooltip-tipsy{ + border: 0; +} + + .ui-tooltip-tipsy .ui-tooltip-titlebar, + .ui-tooltip-tipsy .ui-tooltip-content{ + _margin-bottom: 0; /* IE6 */ + .margin-bottom: 0; /* IE7 */ + + background: transparent; + background: rgba(0, 0, 0, .87); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)"; + + color: white; + border: 0px transparent; + + font-size: 11px; + font-family: 'Lucida Grande', sans-serif; + font-weight: bold; + line-height: 16px; + text-shadow: 0 1px black; + } + + .ui-tooltip-tipsy .ui-tooltip-titlebar{ + padding: 6px 35px 0 10; + } + + .ui-tooltip-tipsy .ui-tooltip-content{ + padding: 6px 10; + } + + .ui-tooltip-tipsy .ui-tooltip-icon{ + border-color: #222; + text-shadow: none; + } + + .ui-tooltip-tipsy .ui-tooltip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/* Tipped style */ +.ui-tooltip-tipped{ + +} + + .ui-tooltip-tipped .ui-tooltip-titlebar, + .ui-tooltip-tipped .ui-tooltip-content{ + border: 3px solid #959FA9; + + filter: none; -ms-filter: none; + } + + .ui-tooltip-tipped .ui-tooltip-titlebar{ + background: #3A79B8; + background-image: -moz-linear-gradient(top, #3A79B8, #2E629D); + background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D)); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"; + + color: white; + font-weight: normal; + font-family: serif; + + border-bottom-width: 0; + -moz-border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; + } + + .ui-tooltip-tipped .ui-tooltip-content{ + background-color: #F9F9F9; + color: #454545; + + -moz-border-radius: 0 0 3px 3px; + -webkit-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + } + + .ui-tooltip-tipped .ui-tooltip-icon{ + border: 2px solid #285589; + background: #285589; + } + + .ui-tooltip-tipped .ui-tooltip-icon .ui-icon{ + background-color: #FBFBFB; + color: #555; + } + +/* IE9 fix - removes all filters */ +.ui-tooltip:not(.ie9haxors) div.ui-tooltip-content, +.ui-tooltip:not(.ie9haxors) div.ui-tooltip-titlebar{ + filter: none; + -ms-filter: none; +} \ No newline at end of file diff --git a/data/css/lib/tablesorter.css b/data/css/lib/tablesorter.css new file mode 100644 index 0000000000000000000000000000000000000000..591dc7a84d3360a78f324cd997627c1de5cd2f58 --- /dev/null +++ b/data/css/lib/tablesorter.css @@ -0,0 +1,100 @@ +/* Variables *//* Mixins */ +/* SB Theme */ +table.tablesorter { + width: 100%; + margin-left: auto; + margin-right: auto; + text-align: left; + color: #000; + background-color: #fff; + border-spacing: 0; +} +table.tablesorter td { + font-size: 14px; + padding: 8px 10px; +} +/* remove extra border from left edge */ +table.tablesorter th:first-child, +table.tablesorter td:first-child { + border-left: none; +} +table.tablesorter th { + border-collapse: collapse; + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + color: #fff; +} +table.tablesorter .tablesorter-header { + /* background-image: url(../images/tablesorter/bg.gif); */ + + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; +} +table.tablesorter .tablesorter-header-inner { + padding: 0px 15px 0px 4px; +} +table.tablesorter th.tablesorter-headerSortUp .tablesorter-header-inner { + background: url(../lib/images/tablesorter/asc.gif) no-repeat right center; +} +table.tablesorter th.tablesorter-headerSortDown .tablesorter-header-inner { + background: url(../lib/images/tablesorter/desc.gif) no-repeat right center; +} +table.tablesorter th.tablesorter-headerSortUp { + background-image: -moz-linear-gradient(#777777, #555555) !important; + background-image: linear-gradient(#777777, #555555) !important; + background-image: -webkit-linear-gradient(#777777, #555555) !important; + background-image: -o-linear-gradient(#777777, #555555) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + color: #FFFFFF; + /* background-image: url(../images/tablesorter/asc.gif); */ + +} +table.tablesorter th.tablesorter-headerSortDown { + background-image: -moz-linear-gradient(#777777, #555555) !important; + background-image: linear-gradient(#777777, #555555) !important; + background-image: -webkit-linear-gradient(#777777, #555555) !important; + background-image: -o-linear-gradient(#777777, #555555) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#555555, endColorstr=#333333) !important; + color: #FFFFFF; + /* background-image: url(../images/tablesorter/desc.gif); */ + +} +/* Zebra Widget - row alternating colors */ +table.tablesorter tr.odd td { + background-color: #F5F1E4; +} +table.tablesorter tr.even td { + background-color: #fbf9f3; +} +/* filter widget */ +table.tablesorter input.tablesorter-filter { + width: 98%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +table.tablesorter tr.tablesorter-filter, +table.tablesorter tr.tablesorter-filter td { + text-align: center; + background: #eee; +} +/* optional disabled input styling */table.tablesorter input.tablesorter-filter.disabled { + display: none; +} +/* xtra css for sb */ +.tablesorter-header-inner { + text-align: center; + white-space: nowrap; + padding: 0 2px; +} +tr.tablesorter-stickyHeader { + background-color: #fff; + padding: 2px 0; +} diff --git a/data/css/style.css b/data/css/style.css deleted file mode 100644 index b283536388b610fe22a385bfb8965fac91b2d585..0000000000000000000000000000000000000000 --- a/data/css/style.css +++ /dev/null @@ -1,1280 +0,0 @@ -/* ======================================================================= -browser.css -========================================================================== */ -#fileBrowserDialog { - overflow-y: auto; -} -#fileBrowserDialog ul { - padding: 0; - margin: 0; -} -#fileBrowserDialog ul li { - margin: 2px 0; - list-style-type: none; - cursor: pointer; -} -#fileBrowserDialog ul li a { - display: block; - padding: 4px 0; -} -#fileBrowserDialog ul li a:hover { - color: #00f; - background: none; -} -#fileBrowserDialog ul li a span.ui-icon { - float: left; - margin: 0 4px; -} -#fileBrowserDialog h2 { - font-size: 20px; -} -/* -.browserDialog.busy .ui-dialog-buttonpane { - background: url("/images/loading.gif") 10px 50% no-repeat; -} -*/ -.ui-autocomplete { - max-height: 180px; - overflow-x: hidden; - overflow-y: auto; -} -/* IE6 hack since it doesn't support max-height */ -* html .ui-autocomplete { - height: 180px; - padding-right: 20px; -} -.ui-menu .ui-menu-item { - background-color: #eee; -} -.ui-menu .ui-menu-item-alternate{ - background-color: #fff; -} -.ui-menu a.ui-state-hover{ - color: #fff; - background: none; - background-color: #0a246a; -} - -/* ======================================================================= -formWizard.css -========================================================================== */ -fieldset.sectionwrap { - width: 800px; - padding: 5px; - text-align: left; - border-width: 0; -} -legend.legendStep { - font: bold 16px Arial; - color: #57442b; -} -div.stepsguide { - margin-bottom: 15px; - overflow: hidden; - text-align: left; - cursor: pointer; -} -div.stepsguide .step { - float: left; - width: 250px; - font: bold 24px Arial; -} -div.stepsguide .step p { - margin: 12px 0; - border-bottom: 4px solid #57442b; -} -div.stepsguide .disabledstep { - color: #c4c4c4; -} -div.stepsguide .disabledstep p { - border-bottom: 4px solid #8a775e; -} -div.stepsguide .step .smalltext { - font-size: 13px; - font-weight: normal; -} -div.formpaginate { - width: 800px; - margin-top: 1em; - overflow: auto; - font-weight: bold; - text-align: center; -} -div.formpaginate .prev, div.formpaginate .next { - padding: 3px 6px; - color: #fff; - cursor: hand; - cursor: pointer; - background: #57442b; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} -.stepDiv { - padding: 15px; -} -/* step 3 related */ -#customQuality { - display: block; - padding: 10px 0; - overflow: hidden; - clear: both; -} -#customQualityWrapper div.component-group-desc { - float: left; - width: 165px; -} -#customQualityWrapper div.component-group-desc p { - width: 85%; - margin: .8em 0; - font-size: 1.2em; - color: #666; -} - -/* ======================================================================= -tablesorter.css -- custom -========================================================================== */ -table.tablesorter { - width: 100%; - margin-right: auto; - margin-left: auto; - color: #000; - text-align: left; - background-color: #fff; - border-spacing: 0; -} -table.tablesorter th, -table.tablesorter td { - padding: 4px; - border-top: #fff 1px solid; - border-left: #fff 1px solid; -} -/* remove extra border from left edge */ -table.tablesorter th:first-child, -table.tablesorter td:first-child { - border-left: none; -} -table.tablesorter th { - color: #fff; - text-align: center; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); - background-color: #333; - border-collapse: collapse; -} -table.tablesorter .tablesorter-header { - padding: 4px 18px 4px 4px; - cursor: pointer; - background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAAP///////yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); - background-position: center right; - background-repeat: no-repeat; - /* background-image: url(../images/tablesorter/bg.gif); */ -} -table.tablesorter th.tablesorter-headerSortUp { - background-color: #57442b; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); - /* background-image: url(../images/tablesorter/asc.gif); */ -} -table.tablesorter th.tablesorter-headerSortDown { - background-color: #57442b; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); - /* background-image: url(../images/tablesorter/desc.gif); */ -} -/* Zebra Widget - row alternating colors */ -table.tablesorter tr.odd, .sickbeardTable tr.odd { - background-color: #f5f1e4; -} -table.tablesorter tr.even, .sickbeardTable tr.even { - background-color: #dfdacf; -} -/* filter widget */ -table.tablesorter input.tablesorter-filter { - width: 98%; - height: inherit; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -table.tablesorter tr.tablesorter-filter, -table.tablesorter tr.tablesorter-filter td { - text-align: center; - background: #eee; -} -/* optional disabled input styling */ -table.tablesorter input.tablesorter-filter.disabled { - display: none; -} -.tablesorter-header-inner { - padding: 0 2px; - text-align: center; -} -tr.tablesorter-stickyHeader { - padding: 2px 0; - background-color: #fff; -} -table.tablesorter tfoot tr { - color: #fff; - text-align: center; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); - background-color: #333; - border-collapse: collapse; -} -table.tablesorter tfoot a { - color:#fff; - text-decoration: none; -} - -/* ======================================================================= -inc_top.tmpl -========================================================================== */ -body { - font-family: Verdana, "Helvetica", sans-serif; - font-size: 12px; - color: #000; - background-color: #f5f1e4; -} -#upgrade-notification { - width: 100%; - height: 0; - padding: 0; - margin: 0; - font-size: 12px; - font-weight: bold; - line-height: 6px; - color: #57442b; - text-align: center; -} -#upgrade-notification div { - padding: 7px 0; - background-color: #c6b695; - border-bottom: 1px solid #af986b; -} -#header-fix { - height: 21px; - padding: 0; - *margin-bottom: -31px; - /* IE fix */ -} -#header { - padding: 5px 0; - background-color: #fff; -} -#header a:hover { - background: none; -} -#logo { - padding: 0 5px 0 15px; - font-family: Arial, Helvetica, sans-serif; - font-size: 33px; - font-weight: bold; - text-transform: uppercase; -} -#versiontext { - font-family: Arial, Helvetica, sans-serif; - font-size: 10px; - font-style: italic; - color: #57442b; - text-rendering: optimizelegibility; -} -#versiontext a { - font-weight: bold; - color: #57442b; -} -#navDonate { - padding: 5px 10px 4px; -} -/* submenu related */ -#SubMenu { - padding-left: 20px; - clear: both; - font-size: 12px; - color: #333; - background-color: #f5f1e4; - border-top: 1px solid #333; - border-bottom: 1px solid #b3b3b3; -} -#SubMenu span { - padding: 2px 6px 2px 8px; -} -#SubMenu span a { - padding: 2px 6px 2px; - font-weight: bold; - line-height: 24px; - color: #333; - text-decoration: none; -} -#SubMenu span a:hover { - color: #fff; - background-color: #000; -} -/* for comingEpisode submenu */ -#SubMenu span a.inner { - padding: 2px; - font-weight: normal; -} -#content { - width: 90%; - min-width: 875px; - padding: 15px; - background: #fff; - border-right: 1px solid #b3b3b3; - border-left: 1px solid #b3b3b3; - margin-left: auto; - margin-right: auto; - clear: both; -} - -/* ======================================================================= -inc_bottom.tmpl -========================================================================== */ -.footer { - width: 100%; - padding: 6px 0; - color: #4e4e4e; - text-align: center; - background-color: #f5f1e4; - border-top: 1px solid #b3b3b3; -} - -/* ======================================================================= -home.tmpl -========================================================================== */ -.ui-progressbar { - height: 20px; - line-height: 18px; -} -.progressbarText { - position: absolute; - top: 0; - width: 100%; - height: 100%; - overflow: visible; - text-align: center; - text-shadow: 0 0 0.1em #fff; - vertical-align: middle; -} - -/* ======================================================================= -home_postprocess.tmpl -========================================================================== */ -#episodeDir, #newRootDir { - margin-right: 6px; -} - -/* ======================================================================= -editShow.tmpl -========================================================================== */ -/* only works on FF :( */ -option.flag { - padding-left: 35px; - background-color: #fff; - background-position: 10px 50%; - background-repeat: no-repeat; -} - -/* ======================================================================= -manage_massEdit.tmpl -========================================================================== */ -.optionWrapper { - width: 450px; - padding: 6px 12px; - margin-right: auto; - margin-left: auto; -} -.optionWrapper span.selectTitle { - float: left; - width: 225px; - font-size: 14px; - font-weight: bold; - line-height: 22px; - text-align: left; -} -.optionWrapper div.selectChoices { - float: left; - width: 175px; - margin-left: 25px; -} - -/* ======================================================================= -manage_episodeStatuses.tmpl -========================================================================== */ -a.whitelink { - color: #fff; -} - -/* ======================================================================= -home_addShows.tmpl -========================================================================== */ -#addShowPortal { - width: 480px; - padding: 10px 0; - margin-right: auto; - margin-left: auto; -} -#addShowPortal a { - padding: 10px; -} -div.button img { - display: block; - float: left; - padding: 25px 5px; -} -div.buttontext { - display: block; - margin-left: 55px; - text-align: left; -} - -/* ======================================================================= -home_addExistingShow.tmpl + inc_addShowOptions.tmpl -========================================================================== */ -input.cb { - float: left; - margin: 3px 5px 3px 4px; -} -ul#rootDirStaticList { - width: 90%; - margin-right: auto; - margin-left: auto; - text-align: left; -} -ul#rootDirStaticList li { - padding: 4px 5px 4px 5px; - margin: 2px; - list-style: none outside none; - cursor: pointer; -} -/* for tabs: customize options */ -#tabs div.field-pair, -.stepDiv div.field-pair { - padding: 0.75em 0; -} -#tabs div.field-pair input, -.stepDiv div.field-pair input { - float: left; -} -#tabs label.nocheck, -#tabs div.providerDiv, -#tabs div #customQuality, -.stepDiv label.nocheck, -.stepDiv div.providerDiv, -.stepDiv div #customQuality { - padding-left: 23px; -} -#tabs label span.component-title, -.stepDiv label span.component-title { - float: left; - width: 165px; - padding-left: 6px; - margin-right: 10px; - font-size: 1.1em; - font-weight: bold; -} -#tabs label span.component-desc, -.stepDiv label span.component-desc { - float: left; - font-size: .9em; -} -#tabs div.field-pair select, -.stepDiv div.field-pair select { - font-size: 1em; - border: 1px solid #d4d0c8; -} -#tabs div.field-pair select option, -.stepDiv div.field-pair select option { - padding: 0 10px; - line-height: 1.4; - border-bottom: 1px dotted #d7d7d7; -} - -/* ======================================================================= -home_newShow.tmpl -========================================================================== */ -#displayText { - padding: 8px; - overflow: hidden; - font-size: 13px; - background-color: #efefef; - border: 1px solid #dfdede; -} - -/* ======================================================================= -displayShow.tmpl + manage_backlogOverview.tmpl -========================================================================== */ -.navShow { - display: inline; - cursor: pointer; -} -.showLegend { - padding-right: 10px; - padding-bottom: 1px; - font-weight: bold; -} -.checkbox.inline { - padding: 0 5px; -} -.checkbox.inline > input { - margin-right: 5px; - margin-left: 0; -} -.unaired { - background-color: #f5f1e4; -} -.skipped { - background-color: #bedeed; -} -.good { - background-color: #c3e3c8; -} -.qual { - background-color: #ffda8a; -} -.wanted { - background-color: #ffb0b0; -} -.snatched { - background-color: #ebc1ea; -} -/* ======================================================================= -manage_backlogOverview.tmpl -========================================================================== */ -.forceBacklog { - margin-left: 10px; -} -h2.backlogShow { - display: inline; - margin: 0; - font-size: 18px; - line-height: 18px; - letter-spacing: 1px; - color: #000; -} - -/* ======================================================================= -config*.tmpl -========================================================================== */ -#config-content { - display: block; - width: 875px; - padding: 0 0 40px; - margin: 0 auto; - clear: both; - text-align: left; - background: #fff; -} -.component-group { - padding: 15px 15px 25px; - border-bottom: 1px dotted #666; -} -/* .component-group-desc :width - + .component-group-list :width - + .component-group :padding - ------------------------------- - must be less than config-content -*/ -.component-group-desc{ - float: left; - width: 250px; -} -.component-group-desc p { - width: 90%; - margin: 10px 0; - color: #666; -} -.component-group-list { - float: left; - width: 590px; -} -#config div.field-pair { - padding: 8px 4px; -} -#config div.field-pair input { - float: left; - margin-right: 6px; -} -#config .nocheck, #config div #customQuality, .metadataDiv, .providerDiv { - padding-left: 20px; -} -#config label span.component-title { - float: left; - width: 172px; - margin-right: 10px; - font-size: 13px; - font-weight: bold; -} -#config label span.component-desc { - display: block; - padding-bottom: 2px; - overflow: hidden; - font-size: 12px; -} -.component-group-save { - float: right; - padding-top: 10px; -} -select .selected { - font-weight: 700; -} -.jumbo { - font-size: 15px !important; - line-height: 24px; -} - -/* ======================================================================= -comingEpisodes.tmpl -========================================================================== */ -.tvshowDiv { - display: block; - width: 606px; - padding: 0; - margin: auto; - clear: both; - text-align: left; - border-right: 1px solid #ccc; - border-bottom: 1px solid #ccc; - border-left: 1px solid #ccc; -} -.tvshowDiv a, .tvshowDiv a:link, .tvshowDiv a:visited, .tvshowDiv a:hover { - text-decoration: none; - background: none; -} -.tvshowTitle a { - padding-left: 8px; - font-size: 13px; - line-height: 23px; - color: #fff; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); -} -.tvshowTitleIcons { - float: right; - padding: 3px 5px; -} -.tvshowDiv .title { - font-weight: 900; - color: #333; -} -.posterThumb { - height: 200px; - vertical-align: top; - -ms-interpolation-mode: bicubic; -} -.bannerThumb { - height: 112px; - vertical-align: top; - border-bottom: 1px solid #d2ebe8; - -ms-interpolation-mode: bicubic; -} -.tvshowDiv th { - letter-spacing: 1px; - color: #000; - text-align: left; - background-color: #333; -} -.tvshowDiv th.nobg { - text-align: center; - background: #efefef; - border-top: 1px solid #666; -} -.tvshowDiv td { - padding: 10px; - color: #000; - background: #fff; -} -.tvshowDiv td.nextEp { - width: 100%; - color: #000; - background: #f5fafa; - border-bottom: 1px solid #d2ebe8; -} -h2.day { - margin: 10px 0; - letter-spacing: 1px; - color: #000; - text-align: center; - background-color: #9cb5cf; -} -h2.network { - margin: 10px 0; - letter-spacing: 1px; - color: #000; - text-align: center; - background-color: #8fbfaf; -} -.epListing { - width: auto; - padding: 10px; - margin-bottom: 10px; - border: 1px solid #ccc; -} -.listing-default { - background-color: #f5f1e4; -} -.listing-current { - background-color: #dfd; -} -.listing-waiting { - background-color: #9f9; -} -.listing-overdue { - background-color: #fdd; -} -.listing-toofar { - background-color: #ddf; -} -.listing-unknown { - background-color: #ffdc89; -} -span.pause { - font-size: 12px; - color: #f00; -} -.epSummaryTrigger { - float: left; - padding-top: 5px; -} -.epSummary { - padding-top: 5px; - margin-left: 25px; -} - -/* ======================================================================= -config.tmpl -========================================================================== */ -.infoTable td { - padding: 5px; -} -.infoTableHeader { - font-weight: bold; -} -.infoTableSeperator { - border-top: 1px dotted #666; -} -[class^="icon16-"], [class*=" icon16-"] { - display: inline-block; - width: 16px; - height: 16px; - line-height: 16px; - vertical-align: text-top; - /* background-image: url("../images/glyphicons-config.png"); */ - background-position: -40px 0; - background-repeat: no-repeat; -} -.icon16-github { - background-position: 0 0; -} -.icon16-mirc { - background-position: -20px 0; -} -.icon16-sb { - background-position: -40px 0; -} -.icon16-web { - background-position: -60px 0; -} -.icon16-win { - background-position: -80px 0; -} - -/* ======================================================================= -config_notifications.tmpl -========================================================================== */ -.notifier-icon { - float: left; - margin: 2px 10px 0 0; -} -.testNotification { - padding: 5px; - margin-bottom: 10px; - line-height: 20px; - border: 1px dotted #ccc; -} - -/* ======================================================================= -config_postProcessing.tmpl -========================================================================== */ -#config div.example { - padding: 10px; background-color: #efefef; -} -.Key { - width: 100%; - padding: 6px; - font-family: sans-serif; - font-size: 13px; - background-color: #f4f4f4; - border: 1px solid #ccc; - border-collapse: collapse; - border-spacing: 0; -} -.Key th, .tableHeader { - padding: 3px 9px; - margin: 0; - color: #fff; - text-align: center; - background: none repeat scroll 0 0 #666; -} -.Key td { - padding: 1px 5px !important; -} -.Key tr { - border-bottom: 1px solid #ccc; -} -.Key tr.even { - background-color: #dfdede; -} - -/* ======================================================================= -config_providers.tmpl -========================================================================== */ -#config-components > h2 { - border-bottom: 4px solid #ddd; -} -#providerOrderList, #service_order_list { - width: 250px; - padding-left: 20px; - list-style-type: none; -} -#providerOrderList li, #service_order_list li { - padding: 5px; - margin: 5px 0; - font-size: 14px; -} -#providerOrderList input, #service_order_list input { - margin: 0 2px; -} -.imgLink img { - padding: 0 2px 2px; -} -/* fix drop target height */ -#providerOrderList.ui-state-highlight, #service_order_list .ui-state-highlight { - height: 20px; - line-height: 18px; -} -h4.note { - float: left; - padding-right: 5px; - color: #000; -} -p.note { - color: #333; -} - -/* ======================================================================= -config_search.tmpl -========================================================================== */ -#no-torrents { - margin-top: 15px; - margin-bottom: 0; -} -.title-group { - position: relative; - padding: 15px; - border-bottom: 1px dotted #666; -} - -/* ======================================================================= -config_notifications.tmpl -========================================================================== */ -div.metadata-options-wrapper { - float: left; - width: 190px; -} -div.metadata-example-wrapper { - float: right; - width: 325px; -} -div.metadata-options { - padding: 7px; - overflow: auto; - background: #f5f1e4; - border: 1px solid #ccc; -} -div.metadata-options label { - display: block; - padding-left: 7px; - line-height: 20px; - color: #036; -} -div.metadata-options label:hover { - color: #fff; - background-color: #57442b; -} -div.metadata-example { - padding: 7px; -} -div.metadata-example label { - display: block; - line-height: 21px; - color: #000; -} -div.metadataDiv .disabled { - color: #ccc; -} - -/* ======================================================================= -global -- used all over -========================================================================== */ -.sickbeardTable { - width: 100%; - margin-right: auto; - margin-left: auto; - color: #000; - text-align: left; - background-color: #fff; - border-spacing: 0; -} -.sickbeardTable th, -.sickbeardTable td { - padding: 4px; - border-top: #fff 1px solid; - border-left: #fff 1px solid; -} -.sickbeardTable th:first-child, -.sickbeardTable td:first-child { - border-left: none; -} -.sickbeardTable th{ - color: #fff; - text-align: center; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); - background-color: #333; - border-collapse: collapse; -} -.sickbeardTable tfoot a { - color: #fff; - text-decoration: none; -} -tr.seasonheader { - padding: 0; - text-align: center; - background-color: #fff; -} -tr.seasonheader h2 { - display: inline; - margin: 0; - font-size: 22px; - line-height: 20px; - letter-spacing: 1px; - color: #000; -} -tr.seasonheader a { - text-decoration:none; -} -td.tvShow { - font-weight: bold; -} -td.tvShow a { - font-size: 14px; - color: #333; - text-decoration: none; -} -td.tvShow:hover a { - color:#000; -} -td.tvShow:hover { - cursor: pointer; - background-color: #cfcfcf !important; -} -/* TODO: convert float-* to pull-* */ -.float-left { - float: left; -} -.float-right { - float: right; -} -.align-left { - text-align: left; -} -.align-right { - text-align: right; -} -.nowrap { - white-space: nowrap; -} -.padding { - padding: 5px; -} -.alt { - background-color: #efefef; -} -div#summary { - padding: 10px; - margin: 10px; - background-color: #efefef; - border: 1px solid #dfdede; -} -h1 a:hover, h2 a:hover, span a:hover, div.h2footer a:hover, .notify-text a:hover { - color: #fff; - text-decoration: none; - background-color: #000; -} -/* used on displayShow and comingEp list */ -.plotInfo { - position: relative; - float: right; - margin-left: 8px; - font-weight: bold; - cursor: help; -} -div select option { - padding: 2px 10px; -} -span.path { - padding: 3px 6px; - color: #8b0000; - background-color: #f5f1e4; -} -h1.title { - padding-bottom: 4px; - margin-bottom: 15px; - font-family: "Helvetica", Verdana, sans-serif; - font-size: 24px; - font-weight: 400; - line-height: 30px; - text-align: left; - text-rendering: optimizeLegibility; - border-bottom: 1px solid #888; -} -.h2footer { - margin: -45px 5px 8px 0; - line-height: 18px; -} -.h2footer select { - margin-top: -6px; - margin-bottom: -6px; -} -.h2footer span { - padding: 3px 5px; -} -/* quality tags */ -span.quality { - display: inline-block; - padding: 2px 4px; - font: bold 1em/1.2em verdana, sans-serif; - color: #fff; - text-align: center; - background: none repeat scroll 0 0 #999; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -span.Custom { - background: none repeat scroll 0 0 #449; - /* purplish blue */ -} -span.HD { - background: none repeat scroll 0 0 #008fbb; - /* greenish blue */ -} -span.HD720p { - background: none repeat scroll 0 0 #494; - /* green */ -} -span.HD1080p { - background: none repeat scroll 0 0 #499; - /* blue */ -} -span.SD { - background: none repeat scroll 0 0 #944; - /* red */ -} -span.Any { - background: none repeat scroll 0 0 #444; - /* black */ -} -span.RawHD { - background: none repeat scroll 0 0 #999944; - /* dark orange */ -} -/* unused boolean tags */ -span.false { - color: #933; - /* red */ -} -span.true { - color: #696; - /* green */ -} - -/* ======================================================================= -Lib overrides -- global (except jui/bootstrap) -========================================================================== */ -/* pines notify related */ -div.ui-pnotify { - width: auto !important; - max-width: 550px; - min-width: 340px; -} -/* qTip2 related */ -.ui-tooltip-sb .ui-tooltip-titlebar a { - color: #222; - text-decoration: none; -} -.ui-tooltip, .qtip { - max-width: 500px !important; -} - -/* ======================================================================= -bootstrap overrides -========================================================================== */ -a { - color: #333; -} -img { - max-width: none; - /* fixes IE8 */ -} -input, textarea, select, .uneditable-input { - width: auto; - color: #000; -} -select:focus, -.btn:focus { - outline: none; -} -input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { - background-color: #efefef; - border-color: #ccc; -} -.btn:focus, -.btn:active { - border: 1px solid #4d90fe; - outline: none; - -moz-box-shadow: none; - box-shadow: none; -} -.btn-primary:focus, -.btn-warning:focus, -.btn-danger:focus, -.btn-success:focus, -.btn-info:focus, -.btn-inverse:focus { - border: 1px solid transparent; - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.75) inset; -} -.alert { - font-size: 15px; - text-align: center; -} -.navbar, label, legend, form, input, textarea, select, .uneditable-input { - margin-bottom: 0; -} -.navbar .nav .active > a, -.navbar .nav .active > a:hover { - background-color: transparent; -} -.navbar .nav > li > a { - padding: 7px 10px 9px; - color: #ddd; -} -.nav > li.active > a { - padding: 3px 10px 6px; - margin-top: 4px; - margin-bottom: 3px; - color: #fff; - background-color: #4E3D27 !important; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} -.navbar-inner { - min-height: 36px; - background-color: #614e35; - background-image: -ms-linear-gradient(top, #67543b, #57442b); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#67543b), to(#57442b)); - background-image: -webkit-linear-gradient(top, #67543b, #57442b); - background-image: -o-linear-gradient(top, #67543b, #57442b); - background-image: linear-gradient(top, #67543b, #57442b); - background-image: -moz-linear-gradient(top, #67543b, #57442b); - background-repeat: repeat-x; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#67543b', endColorstr='#57442b', GradientType=0); - -moz-box-shadow: none; - box-shadow: none; -} -.navbar .navbar-text { - line-height: 36px; -} -.navbar .divider-vertical { - height: 36px; - margin: 0 5px; - background-color: #57442b; - border-right: 1px solid #614e35; -} -.dropdown-menu li > a:hover, -.dropdown-menu .active > a, -.dropdown-menu .active > a:hover { - color: #fff; - text-decoration: none; - background-color: #333; -} -ul.nav .dropdown-menu .divider { - margin: 5px 1px; -} -/* -.dropdown-menu li > a:hover > [class^="icon-"], -.dropdown-menu li > a:hover > [class*=" icon-"] { - background-image: url("../images/glyphicons-halflings-white.png"); -} -*/ -/* ------------------------------------------ */ - -/* bootstrap mod to show menu on hover instead of click */ -.dropdown-menu .sub-menu { - position: absolute; - top: 0; - left: 100%; - margin-top: -1px; - visibility: hidden; -} -.dropdown-menu li:hover .sub-menu { - visibility: visible; -} -.dropdown:hover .dropdown-menu { - display: block; -} -.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu, .navbar .dropdown-menu { - margin-top: 0; -} -.navbar .sub-menu:before { - top: 10px; - left: -7px; - border-top: 7px solid transparent; - border-right: 7px solid rgba(0, 0, 0, 0.2); - border-bottom: 7px solid transparent; - border-left: none; -} -.navbar .sub-menu:after { - top: 11px; - left: -6px; - border-top: 6px solid transparent; - border-right: 6px solid #fff; - border-bottom: 6px solid transparent; - border-left: none; -} - -/* bootstrap-progressbar -- unused currently -.progress { - border: 1px solid #aaa; - background-color: #fcfcfc; - background-image: -moz-linear-gradient(top, #f9f9f9, #ffffff); - background-image: -ms-linear-gradient(top, #f9f9f9, #ffffff); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#ffffff)); - background-image: -webkit-linear-gradient(top, #f9f9f9, #ffffff); - background-image: -o-linear-gradient(top, #f9f9f9, #ffffff); - background-image: linear-gradient(top, #f9f9f9, #ffffff); - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#ffffff', GradientType=0); - margin-bottom: 0; -} -.progress-sb .bar { - background-color: #419ab5; - background-image: -moz-linear-gradient(top, #51a9c4, #2d88a1); - background-image: -ms-linear-gradient(top, #51a9c4, #2d88a1); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#51a9c4), to(#2d88a1)); - background-image: -webkit-linear-gradient(top, #51a9c4, #2d88a1); - background-image: -o-linear-gradient(top, #51a9c4, #2d88a1); - background-image: linear-gradient(top, #51a9c4, #2d88a1); - background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#51a9c4', endColorstr='#2d88a1', GradientType=0); -} -*/ diff --git a/data/css/superfish.css b/data/css/superfish.css new file mode 100644 index 0000000000000000000000000000000000000000..90a7564a596c63e284f3e9b3fff789ae27d97090 --- /dev/null +++ b/data/css/superfish.css @@ -0,0 +1,285 @@ +/* Variables *//* Mixins */ +/*** ESSENTIAL STYLES ***/ +.sf-menu ul { + background: #F5F1E4; + position: absolute; + top: -999em; + padding: 0; + -moz-border-radius-bottomleft: 5px; + -moz-border-radius-bottomright: 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border: 1px solid #ccc; + width: 10em; + /* left offset of submenus need to match (see below) */ + +} +.sf-menu ul li a { + padding-left: 28px; +} +.sf-menu ul li a img { + position: absolute; + margin-top: -1px; + margin-left: -22px; + z-index: 99; +} +.sf-menu ul li { + width: 100%; +} +.sf-menu li.spacer, +.sf-menu li.spacer:hover { + background-color: #57442B; + width: 15px; +} +.sf-menu .first { + margin-left: 0px; +} +.sf-menu .navIcon { + padding: 0.6em 1em 0.55em; +} +.sf-menu li:hover { + visibility: inherit; + /* fixes IE7 'sticky bug' */ + +} +.sf-menu li { + float: left; + position: relative; +} +.sf-menu a { + display: block; + position: relative; +} +.sf-menu li:hover ul, +.sf-menu li.sfHover ul { + left: 0; + top: 3.2em; + /* match top ul list item height */ + + z-index: 99; +} +ul.sf-menu li:hover li ul, +ul.sf-menu li.sfHover li ul { + top: -999em; +} +ul.sf-menu li li:hover ul, +ul.sf-menu li li.sfHover ul { + left: 10em; + /* match ul width */ + + top: 0; +} +ul.sf-menu li li:hover li ul, +ul.sf-menu li li.sfHover li ul { + top: -999em; +} +ul.sf-menu li li li:hover ul, +ul.sf-menu li li li.sfHover ul { + left: 10em; + /* match ul width */ + + top: 0; +} +.sf-menu li.current > a { + color: #bde433; +} +.sf-menu { + float: left; + /*margin-bottom: 1em;*/ + + line-height: 1em; +} +.sf-menu a { + border-right: 1px solid #ccc; + padding: .75em 1em; + text-decoration: none; +} +.sf-menu li a { + border: 1px solid transparent; + color: #FFFFFF; + display: block; + padding-bottom: 12px; + padding-top: 12px; + padding-left: 10px; + padding-right: 10px; + font-size: 15px; + font-weight: normal; + text-shadow: 1px 1px 0 #000; + text-transform: capitalize; +} +.sf-menu li a.log { + font-size: 11px; + padding-top: 10px; + padding-left: 15px; + padding-bottom: 11px; + line-height: 19px; + padding-right: 23px; +} +.sf-menu li a.config { + height: 28px; + width: 10px; +} +.sf-menu li a.config img { + left: -7px; + position: relative; + top: -14px; +} +.sf-menu li li a, +.sf-menu li li li a { + text-shadow: none; +} +.sf-menu a, +.sf-menu a:visited { + /* visited pseudo selector so IE6 applies text colour*/ + + color: #FFFFFF; +} +.sf-menu li { + display: block; + float: left; + margin: 8px 0 0; + text-align: center; +} +.sf-menu li li { + padding: 0; + margin: 0; + text-align: left; + /* alt row light brown */ +} +.sf-menu li li li { + background: #F5F1E4; + /* even row tan */ +} +.sf-menu li li a, +.sf-menu li li a:visited { + color: #000; +} +.sf-menu li li a:hover { + color: #343434; +} +.sf-menu li li li a, +.sf-menu li li li a:visited { + color: #000; +} +.sf-menu li li li a:hover { + color: #343434; +} +.sf-menu li:hover, +.sf-menu li.sfHover, +.sf-menu a:focus, +.sf-menu a:hover, +.sf-menu a:active { + outline: 0; +} +.sf-menu li a:hover { + background-image: -moz-linear-gradient(#777777, #555555) !important; + background-image: linear-gradient(#777777, #555555) !important; + background-image: -webkit-linear-gradient(#777777, #555555) !important; + background-image: -o-linear-gradient(#777777, #555555) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#777777, endColorstr=#555555) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#777777, endColorstr=#555555) !important; + border: 1px solid #777777; + border-radius: 3px 3px 3px 3px; + -moz-box-shadow: 0 1px 0 #888888 inset; + -webkit-box-shadow: 0 1px 0 #888888 inset; + -o-box-shadow: 0 1px 0 #888888 inset; + box-shadow: 0 1px 0 #888888 inset; +} +.sf-menu li ul li a { + font-size: 14px; + font-weight: normal; +} +.sf-menu li ul li a:hover { + background-image: -moz-linear-gradient(#555555, #333333) !important; + background-image: linear-gradient(#555555, #333333) !important; + background-image: -webkit-linear-gradient(#555555, #333333) !important; + background-image: -o-linear-gradient(#555555, #333333) !important; + filter: progid:dximagetransform.microsoft.gradient(startColorstr=#777777, endColorstr=#555555) !important; + -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#777777, endColorstr=#555555) !important; + color: #FFF !important; + text-shadow: none; + border: 1px solid transparent; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + -moz-box-shadow: none; + -webkit-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; +} +/*** arrows **/ +.sf-menu a.sf-with-ul { + padding-right: 1.8em; + min-width: 1px; + /* trigger IE7 hasLayout so spans position accurately */ + +} +.sf-sub-indicator { + position: absolute; + display: block; + right: .75em; + top: 1.05em; + /* IE6 only */ + + width: 10px; + height: 10px; + text-indent: -999em; + overflow: hidden; + /* + background: url('/images/arrows.png') no-repeat -10px -100px; /* 8-bit indexed alpha png. IE6 gets solid image only */ + +} +a > .sf-sub-indicator { + /* give all except IE6 the correct values */ + + top: 14px; + background-position: 0 -100px; + /* use translucent arrow for modern browsers*/ + +} +/* apply hovers to modern browsers */ +a:focus > .sf-sub-indicator, +a:hover > .sf-sub-indicator, +a:active > .sf-sub-indicator, +li:hover > a > .sf-sub-indicator, +li.sfHover > a > .sf-sub-indicator { + background-position: -10px -100px; + /* arrow hovers for modern browsers*/ + +} +/* point right for anchors in subs */ +.sf-menu ul .sf-sub-indicator { + background-position: -10px 0; +} +.sf-menu ul a > .sf-sub-indicator { + background-position: 0 0; +} +/* apply hovers to modern browsers */ +.sf-menu ul a:focus > .sf-sub-indicator, +.sf-menu ul a:hover > .sf-sub-indicator, +.sf-menu ul a:active > .sf-sub-indicator, +.sf-menu ul li:hover > a > .sf-sub-indicator, +.sf-menu ul li.sfHover > a > .sf-sub-indicator { + background-position: -10px 0; + /* arrow hovers for modern browsers*/ + +} +/*** shadows for all but IE6 ***/ +.sf-shadow ul { + /* + background: url('/images/shadow.png') no-repeat bottom right; +*/ + + padding: 0 8px 9px 0; + -moz-border-radius-bottomleft: 17px; + -moz-border-radius-topright: 17px; + -webkit-border-top-right-radius: 17px; + -webkit-border-bottom-left-radius: 17px; +} +.sf-shadow ul.sf-shadow-off { + background: transparent; +} diff --git a/data/css/superfish.less b/data/css/superfish.less new file mode 100644 index 0000000000000000000000000000000000000000..9000efed776b271422752511eea537f6b1cd4273 --- /dev/null +++ b/data/css/superfish.less @@ -0,0 +1,211 @@ +// Config +@import "config.less"; + +/*** ESSENTIAL STYLES ***/ + +.sf-menu ul { + background: #F5F1E4; + position: absolute; + top: -999em; + padding: 0; + border: 1px solid #ccc; + width: 10em; /* left offset of submenus need to match (see below) */ +} +.sf-menu ul li a {padding-left: 28px;} +.sf-menu ul li a img {position:absolute;margin-top:-2px;margin-left:-22px;z-index:99;} + +.sf-menu ul li { + width: 100%; +} +.sf-menu li.spacer,.sf-menu li.spacer:hover { + background-color:#57442B; + width:15px; +} +.sf-menu .first { +margin-left:0px; +} +.sf-menu .navIcon { + padding: 0.6em 1em 0.55em; +} +.sf-menu li:hover { + visibility: inherit; /* fixes IE7 'sticky bug' */ +} +.sf-menu li { + float: left; + position: relative; +} +.sf-menu a { + display: block; + position: relative; +} +.sf-menu li:hover ul, +.sf-menu li.sfHover ul { + left: 0; + top: 3.2em; /* match top ul list item height */ + z-index: 99; +} +ul.sf-menu li:hover li ul, +ul.sf-menu li.sfHover li ul { + top: -999em; +} +ul.sf-menu li li:hover ul, +ul.sf-menu li li.sfHover ul { + left: 10em; /* match ul width */ + top: 0; +} +ul.sf-menu li li:hover li ul, +ul.sf-menu li li.sfHover li ul { + top: -999em; +} +ul.sf-menu li li li:hover ul, +ul.sf-menu li li li.sfHover ul { + left: 10em; /* match ul width */ + top: 0; +} + +.sf-menu li.current > a { + color: @swatch-green; +} + + +.sf-menu { + float: left; + /*margin-bottom: 1em;*/ + line-height: 1em; +} +.sf-menu a { + border-right: 1px solid #ccc; + padding: .75em 1em; + text-decoration:none; +} +.sf-menu li a { + border: 1px solid transparent; + color: #FFFFFF; + display: block; + padding-bottom: 12px; + padding-top: 12px; + padding-left: 10px; + padding-right: 10px; + font-size: 15px; + font-weight: normal; + text-shadow: 1px 1px 0 #000; + text-transform: capitalize; +} +.sf-menu li a.log { + font-size: 11px; + padding-top: 10px; + padding-left: 15px; + padding-bottom: 11px; + line-height: 19px; + padding-right: 23px; +} + +.sf-menu li a.config { + height: 28px; + width: 10px; +} +.sf-menu li a.config img { + left: -7px; + position: relative; + top: -14px; +} +.sf-menu li li a, .sf-menu li li li a { + text-shadow: none; +} +.sf-menu a, .sf-menu a:visited { /* visited pseudo selector so IE6 applies text colour*/ + color: #FFFFFF; +} +.sf-menu li { display: block; + float: left; + margin: 8px 0 0; + text-align: center;} +.sf-menu li li { padding: 0; margin: 0; text-align: left; /* alt row light brown */ } +.sf-menu li li li { background: #F5F1E4; /* even row tan */ } + +.sf-menu li li a,.sf-menu li li a:visited { color: #000; } +.sf-menu li li a:hover { color: #343434; } + +.sf-menu li li li a,.sf-menu li li li a:visited { color: #000; } +.sf-menu li li li a:hover { color: #343434; } + + +.sf-menu li:hover, .sf-menu li.sfHover, +.sf-menu a:focus, .sf-menu a:hover, .sf-menu a:active { + outline: 0; +} +.sf-menu li a:hover { + //-moz-transition: color 0.2s ease-in 0s; + .gradient(#777777, #555555); + border: 1px solid #777777; + border-radius: 3px 3px 3px 3px; + .shadow(0 1px 0 #888888 inset); +} +.sf-menu li ul li a { + font-size: 14px; + font-weight: normal; +} +.sf-menu li ul li a:hover { + .gradient(#555555, #333333); + color: #FFF !important; + text-shadow: none; + border: 1px solid transparent; + .rounded(0); + .shadow(none); +} +/*** arrows **/ +.sf-menu a.sf-with-ul { + padding-right: 1.8em; + min-width: 1px; /* trigger IE7 hasLayout so spans position accurately */ +} +.sf-sub-indicator { + position: absolute; + display: block; + right: .75em; + top: 1.05em; /* IE6 only */ + width: 10px; + height: 10px; + text-indent: -999em; + overflow: hidden; +/* + background: url('/images/arrows.png') no-repeat -10px -100px; /* 8-bit indexed alpha png. IE6 gets solid image only */ + +} +a > .sf-sub-indicator { /* give all except IE6 the correct values */ + top: 14px; + background-position: 0 -100px; /* use translucent arrow for modern browsers*/ +} +/* apply hovers to modern browsers */ +a:focus > .sf-sub-indicator, +a:hover > .sf-sub-indicator, +a:active > .sf-sub-indicator, +li:hover > a > .sf-sub-indicator, +li.sfHover > a > .sf-sub-indicator { + background-position: -10px -100px; /* arrow hovers for modern browsers*/ +} + +/* point right for anchors in subs */ +.sf-menu ul .sf-sub-indicator { background-position: -10px 0; } +.sf-menu ul a > .sf-sub-indicator { background-position: 0 0; } +/* apply hovers to modern browsers */ +.sf-menu ul a:focus > .sf-sub-indicator, +.sf-menu ul a:hover > .sf-sub-indicator, +.sf-menu ul a:active > .sf-sub-indicator, +.sf-menu ul li:hover > a > .sf-sub-indicator, +.sf-menu ul li.sfHover > a > .sf-sub-indicator { + background-position: -10px 0; /* arrow hovers for modern browsers*/ +} + +/*** shadows for all but IE6 ***/ +.sf-shadow ul { +/* + background: url('/images/shadow.png') no-repeat bottom right; +*/ + padding: 0 8px 9px 0; + -moz-border-radius-bottomleft: 17px; + -moz-border-radius-topright: 17px; + -webkit-border-top-right-radius: 17px; + -webkit-border-bottom-left-radius: 17px; +} +.sf-shadow ul.sf-shadow-off { + background: transparent; +} \ No newline at end of file diff --git a/data/css/tablesorter.less b/data/css/tablesorter.less new file mode 100644 index 0000000000000000000000000000000000000000..da90e01e3e8a9e7640dd4a3f30b527b62391a9c8 --- /dev/null +++ b/data/css/tablesorter.less @@ -0,0 +1,91 @@ +// Config +@import "config.less"; + +/* SB Theme */ +table.tablesorter { + width: 100%; + margin-left:auto; + margin-right:auto; + text-align:left; + color: #000; + background-color: #fff; + border-spacing: 0; +} +table.tablesorter td { + font-size: 12px; + padding: 8px 10px; +} +/* remove extra border from left edge */ +table.tablesorter th:first-child, +table.tablesorter td:first-child { + border-left: none; +} +table.tablesorter th { + border-collapse: collapse; + .gradient(#555555,#333333); + color: #fff; +} +table.tablesorter .tablesorter-header { +/* background-image: url(../images/tablesorter/bg.gif); */ + background-repeat: no-repeat; + background-position: center right; + //padding: 4px 18px 4px 4px; + cursor: pointer; +} +table.tablesorter .tablesorter-header-inner { + //background: url(../images/tablesorter/bg.gif) no-repeat right center; + padding: 0px 18px 0px 4px; +} +table.tablesorter th.tablesorter-headerSortUp .tablesorter-header-inner { + background: url(../images/tablesorter/asc.gif) no-repeat right center; +} +table.tablesorter th.tablesorter-headerSortDown .tablesorter-header-inner { + background: url(../images/tablesorter/desc.gif) no-repeat right center; +} +table.tablesorter th.tablesorter-headerSortUp { + .gradient(#777777, #555555); + color: #FFFFFF; +/* background-image: url(../images/tablesorter/asc.gif); */ +} +table.tablesorter th.tablesorter-headerSortDown { + .gradient(#777777, #555555); + color: #FFFFFF; +/* background-image: url(../images/tablesorter/desc.gif); */ +} + +/* Zebra Widget - row alternating colors */ +table.tablesorter tr.odd td { + background-color: #F5F1E4; +} +table.tablesorter tr.even td { + background-color: #fbf9f3; +} +/* filter widget */ +table.tablesorter input.tablesorter-filter { + width: 98%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +table.tablesorter tr.tablesorter-filter, +table.tablesorter tr.tablesorter-filter td { + text-align: center; + background: #eee; +} +/* optional disabled input styling */ +table.tablesorter input.tablesorter-filter.disabled { + display: none; +} + + +/* xtra css for sb */ +.tablesorter-header-inner { + text-align: left; + white-space: nowrap; + padding: 0 2px; +} +tr.tablesorter-stickyHeader { + background-color: #fff; + padding: 2px 0; +} + diff --git a/data/css/token-input-facebook.css b/data/css/token-input-facebook.css new file mode 100644 index 0000000000000000000000000000000000000000..a166be82b30bec0d2289a24a4efef5ac26599154 --- /dev/null +++ b/data/css/token-input-facebook.css @@ -0,0 +1,122 @@ +/* Example tokeninput style #2: Facebook style */ +ul.token-input-list-facebook { + overflow: hidden; + height: auto !important; + height: 1%; + width: auto; + border: 1px solid #8496ba; + cursor: text; + font-size: 12px; + font-family: Verdana !important; + min-height: 1px; + z-index: 999; + margin: 0 !important; + padding: 0 !important; + background-color: #fff; + list-style-type: none; +/* clear: left; */ +} + +ul.token-input-list-facebook li input { + border: 0 !important; + width: 100px !important; + padding: 3px 8px !important; + background-color: white; + margin: 2px 0 !important; + -webkit-appearance: caret; +} + +li.token-input-token-facebook { + overflow: hidden; + height: auto !important; + height: 15px; + margin: 3px !important; + padding: 1px 3px !important; + background-color: #eff2f7; + color: #000; + cursor: default; + border: 1px solid #ccd5e4; + font-size: 11px !important; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + float: left; + white-space: nowrap; +} + +li.token-input-token-facebook p { + display: inline; + padding: 0 !important; + margin: 0 !important; +} + +li.token-input-token-facebook span { + color: #a6b3cf; + margin-left: 5px; + font-weight: bold; + cursor: pointer; +} + +li.token-input-selected-token-facebook { + background-color: #5670a6; + border: 1px solid #3b5998; + color: #fff; +} + +li.token-input-input-token-facebook { + float: left; + margin: 0; + padding: 0; + list-style-type: none; +} + +div.token-input-dropdown-facebook { + position: absolute; + width: auto; + background-color: #fff; + overflow: hidden; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; + cursor: default; + font-size: 11px; + font-family: Verdana; + z-index: 1; +} + +div.token-input-dropdown-facebook p { + margin: 0; + padding: 5px; + font-weight: bold; + color: #777; +} + +div.token-input-dropdown-facebook ul { + margin: 0; + padding: 0; +} + +div.token-input-dropdown-facebook ul li { + background-color: #fff; + padding: 3px; + margin: 0; + list-style-type: none; +} + +div.token-input-dropdown-facebook ul li.token-input-dropdown-item-facebook { + background-color: #fff; +} + +div.token-input-dropdown-facebook ul li.token-input-dropdown-item2-facebook { + background-color: #fff; +} + +div.token-input-dropdown-facebook ul li em { + font-weight: bold; + font-style: normal; +} + +div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { + background-color: #3b5998; + color: #fff; +} \ No newline at end of file diff --git a/data/css/token-input-mac.css b/data/css/token-input-mac.css new file mode 100644 index 0000000000000000000000000000000000000000..18522f05f3bdc2915a40dc62effdbcd378b7a6b9 --- /dev/null +++ b/data/css/token-input-mac.css @@ -0,0 +1,204 @@ +/* Example tokeninput style #2: Mac Style */ +fieldset.token-input-mac { + position: relative; + padding: 0; + margin: 5px 0; + background: #fff; + width: 400px; + border: 1px solid #A4BDEC; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +fieldset.token-input-mac.token-input-dropdown-mac { + border-radius: 10px 10px 0 0; + -moz-border-radius: 10px 10px 0 0; + -webkit-border-radius: 10px 10px 0 0; + box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); + -moz-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); + -webkit-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); +} + +ul.token-input-list-mac { + overflow: hidden; + height: auto !important; + height: 1%; + cursor: text; + font-size: 12px; + font-family: Verdana; + min-height: 1px; + z-index: 999; + margin: 0; + padding: 3px; + background: transparent; +} + +ul.token-input-list-mac.error { + border: 1px solid #C52020; +} + +ul.token-input-list-mac li { + list-style-type: none; +} + +li.token-input-token-mac p { + display: inline; + padding: 0; + margin: 0; +} + +li.token-input-token-mac span { + color: #a6b3cf; + margin-left: 5px; + font-weight: bold; + cursor: pointer; +} + +/* TOKENS */ + +li.token-input-token-mac { + font-family: "Lucida Grande", Arial, serif; + font-size: 9pt; + line-height: 12pt; + overflow: hidden; + height: 16px; + margin: 3px; + padding: 0 10px; + background: none; + background-color: #dee7f8; + color: #000; + cursor: default; + border: 1px solid #a4bdec; + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + float: left; +} + +li.token-input-highlighted-token-mac { + background-color: #bbcef1; + border: 1px solid #598bec; + color: #000; +} + +li.token-input-selected-token-mac { + background-color: #598bec; + border: 1px solid transparent; + color: #fff; +} + +li.token-input-highlighted-token-mac span.token-input-delete-token-mac { + color: #000; +} + +li.token-input-selected-token-mac span.token-input-delete-token-mac { + color: #fff; +} + +li.token-input-input-token-mac { + border: none; + background: transparent; + float: left; + padding: 0; + margin: 0; +} + +li.token-input-input-token-mac input { + border: 0; + width: 100px; + padding: 3px; + background-color: transparent; + margin: 0; +} + +div.token-input-dropdown-mac { + position: absolute; + border: 1px solid #A4BDEC; + border-top: none; + left: -1px; + right: -1px; + background-color: #fff; + overflow: hidden; + cursor: default; + font-size: 10pt; + font-family: "Lucida Grande", Arial, serif; + padding: 5px; + border-radius: 0 0 10px 10px; + -moz-border-radius: 0 0 10px 10px; + -webkit-border-radius: 0 0 10px 10px; + box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); + -moz-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); + -webkit-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); + clip:rect(0px, 1000px, 1000px, -10px); +} + +div.token-input-dropdown-mac p { + font-size: 8pt; + margin: 0; + padding: 0 5px; + font-style: italic; + color: #aaa; +} + +div.token-input-dropdown-mac h3.token-input-dropdown-category-mac { + font-family: "Lucida Grande", Arial, serif; + font-size: 10pt; + font-weight: bold; + border: none; + padding: 0 5px; + margin: 0; +} + +div.token-input-dropdown-mac ul { + margin: 0; + padding: 0; +} + +div.token-input-dropdown-mac ul li { + list-style-type: none; + cursor: pointer; + background: none; + background-color: #fff; + margin: 0; + padding: 0 0 0 25px; +} + +div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac { + background-color: #fff; +} + +div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac.odd { + background-color: #ECF4F9; + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; +} + +div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac span.token-input-dropdown-item-description-mac { + float: right; + font-size: 8pt; + font-style: italic; + padding: 0 10px 0 0; + color: #999; +} + +div.token-input-dropdown-mac ul li strong { + font-weight: bold; + text-decoration: underline; + font-style: none; +} + +div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac, +div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac.odd { + background-color: #598bec; + color: #fff; + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; +} + +div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac span.token-input-dropdown-item-description-mac, +div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac.odd span.token-input-dropdown-item-description-mac { + color: #fff; +} diff --git a/data/images/bg.gif b/data/images/bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..53f4abc902ed0178d4b83f95b984eab7c8781609 Binary files /dev/null and b/data/images/bg.gif differ diff --git a/data/images/changelog16.png b/data/images/changelog16.png new file mode 100644 index 0000000000000000000000000000000000000000..3f6807c06dc27b8d5a162160339010dff54fbb7b Binary files /dev/null and b/data/images/changelog16.png differ diff --git a/data/images/flags/ad.png b/data/images/flags/ad.png new file mode 100644 index 0000000000000000000000000000000000000000..625ca84f9ec596848d4b967b5556fda897ca7183 Binary files /dev/null and b/data/images/flags/ad.png differ diff --git a/data/images/flags/ae.png b/data/images/flags/ae.png new file mode 100644 index 0000000000000000000000000000000000000000..ef3a1ecfccdfe9cf7e9fc086a2c6c010f7977ed8 Binary files /dev/null and b/data/images/flags/ae.png differ diff --git a/data/images/flags/af.png b/data/images/flags/af.png new file mode 100644 index 0000000000000000000000000000000000000000..a4742e299f517aee16c248e40eccad39ac34c9e9 Binary files /dev/null and b/data/images/flags/af.png differ diff --git a/data/images/flags/ag.png b/data/images/flags/ag.png new file mode 100644 index 0000000000000000000000000000000000000000..556d5504dc28d89be22ec1883f12e8d8c07d5f41 Binary files /dev/null and b/data/images/flags/ag.png differ diff --git a/data/images/flags/ai.png b/data/images/flags/ai.png new file mode 100644 index 0000000000000000000000000000000000000000..74ed29d92616c86757d3c0ec04378301c8f591b4 Binary files /dev/null and b/data/images/flags/ai.png differ diff --git a/data/images/flags/al.png b/data/images/flags/al.png new file mode 100644 index 0000000000000000000000000000000000000000..92354cb6e257be2cade71cb825027ce8d9efc06d Binary files /dev/null and b/data/images/flags/al.png differ diff --git a/data/images/flags/am.png b/data/images/flags/am.png new file mode 100644 index 0000000000000000000000000000000000000000..344a2a86c43d52f490455d0fe582da93e07175b2 Binary files /dev/null and b/data/images/flags/am.png differ diff --git a/data/images/flags/an.png b/data/images/flags/an.png new file mode 100644 index 0000000000000000000000000000000000000000..633e4b89fded98256a8d142dfb60a8058f7e6b67 Binary files /dev/null and b/data/images/flags/an.png differ diff --git a/data/images/flags/ao.png b/data/images/flags/ao.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbd1d6d40d8665ed9b1001c490ce48befabb258 Binary files /dev/null and b/data/images/flags/ao.png differ diff --git a/data/images/flags/ar.png b/data/images/flags/ar.png new file mode 100644 index 0000000000000000000000000000000000000000..e5ef8f1fcddb9fa0b89c353430e9640c122445cc Binary files /dev/null and b/data/images/flags/ar.png differ diff --git a/data/images/flags/as.png b/data/images/flags/as.png new file mode 100644 index 0000000000000000000000000000000000000000..32f30e4ce4eedd22d4f09c4f3a46c52dd064f113 Binary files /dev/null and b/data/images/flags/as.png differ diff --git a/data/images/flags/at.png b/data/images/flags/at.png new file mode 100644 index 0000000000000000000000000000000000000000..0f15f34f2883c4b4360fc871d7105309f1533282 Binary files /dev/null and b/data/images/flags/at.png differ diff --git a/data/images/flags/au.png b/data/images/flags/au.png new file mode 100644 index 0000000000000000000000000000000000000000..a01389a745d51e16b01a9dc0a707572564a17625 Binary files /dev/null and b/data/images/flags/au.png differ diff --git a/data/images/flags/aw.png b/data/images/flags/aw.png new file mode 100644 index 0000000000000000000000000000000000000000..a3579c2d621069c8128d7cf16440d5e45a3ab3cd Binary files /dev/null and b/data/images/flags/aw.png differ diff --git a/data/images/flags/ax.png b/data/images/flags/ax.png new file mode 100644 index 0000000000000000000000000000000000000000..1eea80a7b739bea4a249dd10a3457010525f60da Binary files /dev/null and b/data/images/flags/ax.png differ diff --git a/data/images/flags/az.png b/data/images/flags/az.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee9fe5ced2610a60ff99e6cc4fbe80d7d53c624 Binary files /dev/null and b/data/images/flags/az.png differ diff --git a/data/images/flags/ba.png b/data/images/flags/ba.png new file mode 100644 index 0000000000000000000000000000000000000000..c77499249c9c54700885c84465bc9039a433a2c9 Binary files /dev/null and b/data/images/flags/ba.png differ diff --git a/data/images/flags/bb.png b/data/images/flags/bb.png new file mode 100644 index 0000000000000000000000000000000000000000..0df19c71d20d7fdc06e1cba01028983439b2bdae Binary files /dev/null and b/data/images/flags/bb.png differ diff --git a/data/images/flags/bd.png b/data/images/flags/bd.png new file mode 100644 index 0000000000000000000000000000000000000000..076a8bf87c0cedcce47099c6b74b59f2c9d1dbce Binary files /dev/null and b/data/images/flags/bd.png differ diff --git a/data/images/flags/be.png b/data/images/flags/be.png new file mode 100644 index 0000000000000000000000000000000000000000..d86ebc800a673e6cf357c33d00edef93e2df0787 Binary files /dev/null and b/data/images/flags/be.png differ diff --git a/data/images/flags/bf.png b/data/images/flags/bf.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5ce8fe1237a18d6809a5570024eb108cb14a3e Binary files /dev/null and b/data/images/flags/bf.png differ diff --git a/data/images/flags/bg.png b/data/images/flags/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..0469f0607dc76eb60327c29e04d9585f3ef25dc7 Binary files /dev/null and b/data/images/flags/bg.png differ diff --git a/data/images/flags/bh.png b/data/images/flags/bh.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8ce68761bbd8ee06f80c487c24d4493abfb52d Binary files /dev/null and b/data/images/flags/bh.png differ diff --git a/data/images/flags/bi.png b/data/images/flags/bi.png new file mode 100644 index 0000000000000000000000000000000000000000..5cc2e30cfc47452d5bef949628e955a522d59e50 Binary files /dev/null and b/data/images/flags/bi.png differ diff --git a/data/images/flags/bj.png b/data/images/flags/bj.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc8b458a4ca83a29117c1ab9e6cd1e60a717db2 Binary files /dev/null and b/data/images/flags/bj.png differ diff --git a/data/images/flags/bm.png b/data/images/flags/bm.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c7aead8dfdeb942752d40cead84182c94f3c94 Binary files /dev/null and b/data/images/flags/bm.png differ diff --git a/data/images/flags/bn.png b/data/images/flags/bn.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb09849e9b5712e9cdd8a2c25035da201535cf5 Binary files /dev/null and b/data/images/flags/bn.png differ diff --git a/data/images/flags/bo.png b/data/images/flags/bo.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7ba522aa7e948d581478432643c230eed1a658 Binary files /dev/null and b/data/images/flags/bo.png differ diff --git a/data/images/flags/br.png b/data/images/flags/br.png new file mode 100644 index 0000000000000000000000000000000000000000..9b1a5538b264a295021f4f717d4299bb8ed98d98 Binary files /dev/null and b/data/images/flags/br.png differ diff --git a/data/images/flags/bs.png b/data/images/flags/bs.png new file mode 100644 index 0000000000000000000000000000000000000000..639fa6cfa9c4792d03fb09fa197faf7ae549bfdf Binary files /dev/null and b/data/images/flags/bs.png differ diff --git a/data/images/flags/bt.png b/data/images/flags/bt.png new file mode 100644 index 0000000000000000000000000000000000000000..1d512dfff42db1ea3e7c59fa7dd69319e789ee12 Binary files /dev/null and b/data/images/flags/bt.png differ diff --git a/data/images/flags/bv.png b/data/images/flags/bv.png new file mode 100644 index 0000000000000000000000000000000000000000..160b6b5b79db15e623fa55e5774e5d160b933180 Binary files /dev/null and b/data/images/flags/bv.png differ diff --git a/data/images/flags/bw.png b/data/images/flags/bw.png new file mode 100644 index 0000000000000000000000000000000000000000..fcb103941523e24b03726fbbd88ef213dd476577 Binary files /dev/null and b/data/images/flags/bw.png differ diff --git a/data/images/flags/by.png b/data/images/flags/by.png new file mode 100644 index 0000000000000000000000000000000000000000..504774ec10efa9fdbedf75295ac88848f23a3908 Binary files /dev/null and b/data/images/flags/by.png differ diff --git a/data/images/flags/bz.png b/data/images/flags/bz.png new file mode 100644 index 0000000000000000000000000000000000000000..be63ee1c623af897a81d80c8e60506d2c9bc0a43 Binary files /dev/null and b/data/images/flags/bz.png differ diff --git a/data/images/flags/ca.png b/data/images/flags/ca.png new file mode 100644 index 0000000000000000000000000000000000000000..1f204193ae58c87efdd88d46700ccb48c2e1d0d8 Binary files /dev/null and b/data/images/flags/ca.png differ diff --git a/data/images/flags/cc.png b/data/images/flags/cc.png new file mode 100644 index 0000000000000000000000000000000000000000..aed3d3b4e4467c33717ab3e2f61596e06113f9bb Binary files /dev/null and b/data/images/flags/cc.png differ diff --git a/data/images/flags/cd.png b/data/images/flags/cd.png new file mode 100644 index 0000000000000000000000000000000000000000..5e489424884d2ec9e429f70d69af00edf242a077 Binary files /dev/null and b/data/images/flags/cd.png differ diff --git a/data/images/flags/cf.png b/data/images/flags/cf.png new file mode 100644 index 0000000000000000000000000000000000000000..da687bdce928e32e4a2bcbed2f3d2d97f42d340c Binary files /dev/null and b/data/images/flags/cf.png differ diff --git a/data/images/flags/cg.png b/data/images/flags/cg.png new file mode 100644 index 0000000000000000000000000000000000000000..a859792ef32a02b41503b5ab5f216191af397e02 Binary files /dev/null and b/data/images/flags/cg.png differ diff --git a/data/images/flags/ch.png b/data/images/flags/ch.png new file mode 100644 index 0000000000000000000000000000000000000000..242ec01aaf5ad351cb978a4eb650ad801a438b09 Binary files /dev/null and b/data/images/flags/ch.png differ diff --git a/data/images/flags/ci.png b/data/images/flags/ci.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2c62eb4d7dc192036af889b593b782dbe8abac Binary files /dev/null and b/data/images/flags/ci.png differ diff --git a/data/images/flags/ck.png b/data/images/flags/ck.png new file mode 100644 index 0000000000000000000000000000000000000000..746d3d6f758858c749523ac27c05c85930d10667 Binary files /dev/null and b/data/images/flags/ck.png differ diff --git a/data/images/flags/cl.png b/data/images/flags/cl.png new file mode 100644 index 0000000000000000000000000000000000000000..29c6d61bd4f16075228cdc6e526aafc3443029d7 Binary files /dev/null and b/data/images/flags/cl.png differ diff --git a/data/images/flags/cm.png b/data/images/flags/cm.png new file mode 100644 index 0000000000000000000000000000000000000000..f65c5bd5a79e2885060515be55f03a3ea4a15d95 Binary files /dev/null and b/data/images/flags/cm.png differ diff --git a/data/images/flags/cn.png b/data/images/flags/cn.png new file mode 100644 index 0000000000000000000000000000000000000000..89144146219e6fbec7eaa89e1bf4b073d299569e Binary files /dev/null and b/data/images/flags/cn.png differ diff --git a/data/images/flags/co.png b/data/images/flags/co.png new file mode 100644 index 0000000000000000000000000000000000000000..a118ff4a146fbd40ce865ea3c93b9d20ab3f14a0 Binary files /dev/null and b/data/images/flags/co.png differ diff --git a/data/images/flags/cr.png b/data/images/flags/cr.png new file mode 100644 index 0000000000000000000000000000000000000000..c7a3731794031667843f05ad3897a85c7c434877 Binary files /dev/null and b/data/images/flags/cr.png differ diff --git a/data/images/flags/cu.png b/data/images/flags/cu.png new file mode 100644 index 0000000000000000000000000000000000000000..083f1d611c94a535e02954711486da244ec3c5d0 Binary files /dev/null and b/data/images/flags/cu.png differ diff --git a/data/images/flags/cv.png b/data/images/flags/cv.png new file mode 100644 index 0000000000000000000000000000000000000000..a63f7eaf63c028615b2ded5878b5e14a7dbe962f Binary files /dev/null and b/data/images/flags/cv.png differ diff --git a/data/images/flags/cx.png b/data/images/flags/cx.png new file mode 100644 index 0000000000000000000000000000000000000000..48e31adbf4cc0074f40e95f87c1f103b91fe270e Binary files /dev/null and b/data/images/flags/cx.png differ diff --git a/data/images/flags/cy.png b/data/images/flags/cy.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1ad6c07886e6963db439afff55d7056e3c5cd4 Binary files /dev/null and b/data/images/flags/cy.png differ diff --git a/data/images/flags/cz.png b/data/images/flags/cz.png new file mode 100644 index 0000000000000000000000000000000000000000..c8403dd21fd15f46d501a766a7a97733462f3b22 Binary files /dev/null and b/data/images/flags/cz.png differ diff --git a/data/images/flags/dj.png b/data/images/flags/dj.png new file mode 100644 index 0000000000000000000000000000000000000000..582af364f8a9cb680628beae33cc9a2dbe0559f4 Binary files /dev/null and b/data/images/flags/dj.png differ diff --git a/data/images/flags/dk.png b/data/images/flags/dk.png new file mode 100644 index 0000000000000000000000000000000000000000..e2993d3c59ae78855f777c158a6aae6c1fb5c843 Binary files /dev/null and b/data/images/flags/dk.png differ diff --git a/data/images/flags/dm.png b/data/images/flags/dm.png new file mode 100644 index 0000000000000000000000000000000000000000..5fbffcba3cb0f20016c9717614127b89db4c9664 Binary files /dev/null and b/data/images/flags/dm.png differ diff --git a/data/images/flags/do.png b/data/images/flags/do.png new file mode 100644 index 0000000000000000000000000000000000000000..5a04932d87963bcb063497b1179cee12f407e18a Binary files /dev/null and b/data/images/flags/do.png differ diff --git a/data/images/flags/dz.png b/data/images/flags/dz.png new file mode 100644 index 0000000000000000000000000000000000000000..335c2391d39090d6b40a409870a74326665589c2 Binary files /dev/null and b/data/images/flags/dz.png differ diff --git a/data/images/flags/ec.png b/data/images/flags/ec.png new file mode 100644 index 0000000000000000000000000000000000000000..0caa0b1e785295d003869330fc4e073dce07e7f6 Binary files /dev/null and b/data/images/flags/ec.png differ diff --git a/data/images/flags/ee.png b/data/images/flags/ee.png new file mode 100644 index 0000000000000000000000000000000000000000..0c82efb7dde983e6ab0f6bebb3b2eb326ce3874a Binary files /dev/null and b/data/images/flags/ee.png differ diff --git a/data/images/flags/eg.png b/data/images/flags/eg.png new file mode 100644 index 0000000000000000000000000000000000000000..8a3f7a10b5757b006948ea4436fb242d02dc9a4e Binary files /dev/null and b/data/images/flags/eg.png differ diff --git a/data/images/flags/eh.png b/data/images/flags/eh.png new file mode 100644 index 0000000000000000000000000000000000000000..90a1195b47a6f12c70d06cb0bd0e4ea88d7bfb03 Binary files /dev/null and b/data/images/flags/eh.png differ diff --git a/data/images/flags/er.png b/data/images/flags/er.png new file mode 100644 index 0000000000000000000000000000000000000000..13065ae99ccace42df97be8b594049f9f40dcc4f Binary files /dev/null and b/data/images/flags/er.png differ diff --git a/data/images/flags/et.png b/data/images/flags/et.png new file mode 100644 index 0000000000000000000000000000000000000000..2e893fa056c3d27448b6b9b6579486439ac6e490 Binary files /dev/null and b/data/images/flags/et.png differ diff --git a/data/images/flags/fam.png b/data/images/flags/fam.png new file mode 100644 index 0000000000000000000000000000000000000000..cf50c759eb28b5962720aa1ce0617a29003e477d Binary files /dev/null and b/data/images/flags/fam.png differ diff --git a/data/images/flags/fj.png b/data/images/flags/fj.png new file mode 100644 index 0000000000000000000000000000000000000000..cee998892eb316c3293ef2d52afec9218bdbbc03 Binary files /dev/null and b/data/images/flags/fj.png differ diff --git a/data/images/flags/fk.png b/data/images/flags/fk.png new file mode 100644 index 0000000000000000000000000000000000000000..ceaeb27decb3f138ab5b385491c092557b79da92 Binary files /dev/null and b/data/images/flags/fk.png differ diff --git a/data/images/flags/fm.png b/data/images/flags/fm.png new file mode 100644 index 0000000000000000000000000000000000000000..066bb247389893b9ac33893fe346732ef394d8d6 Binary files /dev/null and b/data/images/flags/fm.png differ diff --git a/data/images/flags/fo.png b/data/images/flags/fo.png new file mode 100644 index 0000000000000000000000000000000000000000..cbceb809eb9b96d5d8ae231a53c4f4a98f0fcba9 Binary files /dev/null and b/data/images/flags/fo.png differ diff --git a/data/images/flags/ga.png b/data/images/flags/ga.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0d434363abd6766f9e8a8c8c9ad7275d23702a Binary files /dev/null and b/data/images/flags/ga.png differ diff --git a/data/images/flags/gb.png b/data/images/flags/gb.png new file mode 100644 index 0000000000000000000000000000000000000000..ff701e19f6d2c0658fb23b1d94124cba4ce60851 Binary files /dev/null and b/data/images/flags/gb.png differ diff --git a/data/images/flags/gd.png b/data/images/flags/gd.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab57f5489bb9ebb6450cb27f4efe0cfb466144e Binary files /dev/null and b/data/images/flags/gd.png differ diff --git a/data/images/flags/ge.png b/data/images/flags/ge.png new file mode 100644 index 0000000000000000000000000000000000000000..728d97078df1d07241ae605dff2f2cac463be72e Binary files /dev/null and b/data/images/flags/ge.png differ diff --git a/data/images/flags/gf.png b/data/images/flags/gf.png new file mode 100644 index 0000000000000000000000000000000000000000..8332c4ec23c853944c29b02d7b32a88033f48a71 Binary files /dev/null and b/data/images/flags/gf.png differ diff --git a/data/images/flags/gh.png b/data/images/flags/gh.png new file mode 100644 index 0000000000000000000000000000000000000000..4e2f8965914ddd3bd6be97674d2e40a9a3f7d26f Binary files /dev/null and b/data/images/flags/gh.png differ diff --git a/data/images/flags/gi.png b/data/images/flags/gi.png new file mode 100644 index 0000000000000000000000000000000000000000..e76797f62fedcbfca8c83c51951680d6a6e9081f Binary files /dev/null and b/data/images/flags/gi.png differ diff --git a/data/images/flags/gl.png b/data/images/flags/gl.png new file mode 100644 index 0000000000000000000000000000000000000000..ef12a73bf9628ff5a67b81bd980d9c5d2b2c0f05 Binary files /dev/null and b/data/images/flags/gl.png differ diff --git a/data/images/flags/gm.png b/data/images/flags/gm.png new file mode 100644 index 0000000000000000000000000000000000000000..0720b667aff506d7892c5c301af04e6bbf932751 Binary files /dev/null and b/data/images/flags/gm.png differ diff --git a/data/images/flags/gn.png b/data/images/flags/gn.png new file mode 100644 index 0000000000000000000000000000000000000000..ea660b01faefde01ad2527a6abcf7d1a5c1b0526 Binary files /dev/null and b/data/images/flags/gn.png differ diff --git a/data/images/flags/gp.png b/data/images/flags/gp.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb086d0012637103c0bebca861c10116ed3d527 Binary files /dev/null and b/data/images/flags/gp.png differ diff --git a/data/images/flags/gq.png b/data/images/flags/gq.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe20a28de06f3e6e520cea360cfc57586a5bec3 Binary files /dev/null and b/data/images/flags/gq.png differ diff --git a/data/images/flags/gr.png b/data/images/flags/gr.png new file mode 100644 index 0000000000000000000000000000000000000000..8651ade7cbe030e85efc811a844d8f366c97a50c Binary files /dev/null and b/data/images/flags/gr.png differ diff --git a/data/images/flags/gs.png b/data/images/flags/gs.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef0bf598d9aa7c12264551d5db06f44307911d1 Binary files /dev/null and b/data/images/flags/gs.png differ diff --git a/data/images/flags/gt.png b/data/images/flags/gt.png new file mode 100644 index 0000000000000000000000000000000000000000..c43a70d36424b66f1627216ad988cd23a4be9285 Binary files /dev/null and b/data/images/flags/gt.png differ diff --git a/data/images/flags/gu.png b/data/images/flags/gu.png new file mode 100644 index 0000000000000000000000000000000000000000..92f37c05330243ce2eae41bcd9a368c66d656875 Binary files /dev/null and b/data/images/flags/gu.png differ diff --git a/data/images/flags/gw.png b/data/images/flags/gw.png new file mode 100644 index 0000000000000000000000000000000000000000..b37bcf06bf20520555542c58534333e92022d929 Binary files /dev/null and b/data/images/flags/gw.png differ diff --git a/data/images/flags/gy.png b/data/images/flags/gy.png new file mode 100644 index 0000000000000000000000000000000000000000..22cbe2f5914953f1cdea98b7b0979b327ced9582 Binary files /dev/null and b/data/images/flags/gy.png differ diff --git a/data/images/flags/hk.png b/data/images/flags/hk.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c380ca9d84d30674f05b95c2f645b500626c07 Binary files /dev/null and b/data/images/flags/hk.png differ diff --git a/data/images/flags/hm.png b/data/images/flags/hm.png new file mode 100644 index 0000000000000000000000000000000000000000..a01389a745d51e16b01a9dc0a707572564a17625 Binary files /dev/null and b/data/images/flags/hm.png differ diff --git a/data/images/flags/hn.png b/data/images/flags/hn.png new file mode 100644 index 0000000000000000000000000000000000000000..96f838859fd2aed975f5f4134050fdbc0486ce1e Binary files /dev/null and b/data/images/flags/hn.png differ diff --git a/data/images/flags/ht.png b/data/images/flags/ht.png new file mode 100644 index 0000000000000000000000000000000000000000..416052af772d719132c152e26649635a97a63a94 Binary files /dev/null and b/data/images/flags/ht.png differ diff --git a/data/images/flags/id.png b/data/images/flags/id.png new file mode 100644 index 0000000000000000000000000000000000000000..c6bc0fafac79403c97c64ba0228d35f250d05b57 Binary files /dev/null and b/data/images/flags/id.png differ diff --git a/data/images/flags/ie.png b/data/images/flags/ie.png new file mode 100644 index 0000000000000000000000000000000000000000..26baa31e182ddd14106e67de1ac092a7da8e4899 Binary files /dev/null and b/data/images/flags/ie.png differ diff --git a/data/images/flags/il.png b/data/images/flags/il.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca772d0b79b255872cde2fb29060bbbbad950f2 Binary files /dev/null and b/data/images/flags/il.png differ diff --git a/data/images/flags/in.png b/data/images/flags/in.png new file mode 100644 index 0000000000000000000000000000000000000000..e4d7e81a98d705da8d7054e77e7d311805659678 Binary files /dev/null and b/data/images/flags/in.png differ diff --git a/data/images/flags/io.png b/data/images/flags/io.png new file mode 100644 index 0000000000000000000000000000000000000000..3e74b6a316477b90cce8b5f2111f911b1c640950 Binary files /dev/null and b/data/images/flags/io.png differ diff --git a/data/images/flags/iq.png b/data/images/flags/iq.png new file mode 100644 index 0000000000000000000000000000000000000000..878a351403a9a33fd9ae3af1ffd54739545f364d Binary files /dev/null and b/data/images/flags/iq.png differ diff --git a/data/images/flags/ir.png b/data/images/flags/ir.png new file mode 100644 index 0000000000000000000000000000000000000000..c5fd136aee534ecb59914e336cad18d18ead2a4a Binary files /dev/null and b/data/images/flags/ir.png differ diff --git a/data/images/flags/is.png b/data/images/flags/is.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f6d0f06675a9570c2c6e696ee51282097c3876 Binary files /dev/null and b/data/images/flags/is.png differ diff --git a/data/images/flags/jm.png b/data/images/flags/jm.png new file mode 100644 index 0000000000000000000000000000000000000000..7be119e03d203695325568174b72522124bb2f12 Binary files /dev/null and b/data/images/flags/jm.png differ diff --git a/data/images/flags/jo.png b/data/images/flags/jo.png new file mode 100644 index 0000000000000000000000000000000000000000..11bd4972b6d5f134045d4e8ce134601ea9b5654f Binary files /dev/null and b/data/images/flags/jo.png differ diff --git a/data/images/flags/jp.png b/data/images/flags/jp.png new file mode 100644 index 0000000000000000000000000000000000000000..325fbad3ffd3075a4a84d8d898ad26ef7d3e0d56 Binary files /dev/null and b/data/images/flags/jp.png differ diff --git a/data/images/flags/ke.png b/data/images/flags/ke.png new file mode 100644 index 0000000000000000000000000000000000000000..51879adf17c0c29167225a81645cf1123dda84a0 Binary files /dev/null and b/data/images/flags/ke.png differ diff --git a/data/images/flags/kg.png b/data/images/flags/kg.png new file mode 100644 index 0000000000000000000000000000000000000000..0a818f67ea37e1bf1398b3e2f92a52e331abf4e7 Binary files /dev/null and b/data/images/flags/kg.png differ diff --git a/data/images/flags/kh.png b/data/images/flags/kh.png new file mode 100644 index 0000000000000000000000000000000000000000..30f6bb1b9b6c5bf355f67a17531fa73beafa6639 Binary files /dev/null and b/data/images/flags/kh.png differ diff --git a/data/images/flags/ki.png b/data/images/flags/ki.png new file mode 100644 index 0000000000000000000000000000000000000000..2dcce4b33ffe1f40d490cb1a2e03efe22ea56155 Binary files /dev/null and b/data/images/flags/ki.png differ diff --git a/data/images/flags/km.png b/data/images/flags/km.png new file mode 100644 index 0000000000000000000000000000000000000000..812b2f56c5a2a6af805d9edd67d549952d5278ca Binary files /dev/null and b/data/images/flags/km.png differ diff --git a/data/images/flags/kn.png b/data/images/flags/kn.png new file mode 100644 index 0000000000000000000000000000000000000000..febd5b486f3f90056637b23caa26d838fbadd7d0 Binary files /dev/null and b/data/images/flags/kn.png differ diff --git a/data/images/flags/kp.png b/data/images/flags/kp.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d509aa874809a323ea99f3b37ece8a02201f77 Binary files /dev/null and b/data/images/flags/kp.png differ diff --git a/data/images/flags/kr.png b/data/images/flags/kr.png new file mode 100644 index 0000000000000000000000000000000000000000..9c0a78eb942da568f9cdac7190c17e23cceda7ed Binary files /dev/null and b/data/images/flags/kr.png differ diff --git a/data/images/flags/kw.png b/data/images/flags/kw.png new file mode 100644 index 0000000000000000000000000000000000000000..96546da328ab142ab0c7370511cbdbeb9a20efaf Binary files /dev/null and b/data/images/flags/kw.png differ diff --git a/data/images/flags/ky.png b/data/images/flags/ky.png new file mode 100644 index 0000000000000000000000000000000000000000..15c5f8e4775b2b68e0360c1f4ff1f37e61611276 Binary files /dev/null and b/data/images/flags/ky.png differ diff --git a/data/images/flags/kz.png b/data/images/flags/kz.png new file mode 100644 index 0000000000000000000000000000000000000000..45a8c887424cff6eb0471f5a1535139b965e241e Binary files /dev/null and b/data/images/flags/kz.png differ diff --git a/data/images/flags/la.png b/data/images/flags/la.png new file mode 100644 index 0000000000000000000000000000000000000000..e28acd018a21b62d2cc4b76eec7bbe1f714dfc6c Binary files /dev/null and b/data/images/flags/la.png differ diff --git a/data/images/flags/lb.png b/data/images/flags/lb.png new file mode 100644 index 0000000000000000000000000000000000000000..d0d452bf868e0cd6417f518f1dbe695f191ef392 Binary files /dev/null and b/data/images/flags/lb.png differ diff --git a/data/images/flags/lc.png b/data/images/flags/lc.png new file mode 100644 index 0000000000000000000000000000000000000000..a47d065541b0d998da832e1981b479097a9b36aa Binary files /dev/null and b/data/images/flags/lc.png differ diff --git a/data/images/flags/li.png b/data/images/flags/li.png new file mode 100644 index 0000000000000000000000000000000000000000..6469909c013eb9b752ca001694620a229f5792c7 Binary files /dev/null and b/data/images/flags/li.png differ diff --git a/data/images/flags/lk.png b/data/images/flags/lk.png new file mode 100644 index 0000000000000000000000000000000000000000..088aad6db9515dc659152b18ffdf60c269768777 Binary files /dev/null and b/data/images/flags/lk.png differ diff --git a/data/images/flags/lr.png b/data/images/flags/lr.png new file mode 100644 index 0000000000000000000000000000000000000000..89a5bc7e70711575c1ee3b83cc2be7f0e1fb29c5 Binary files /dev/null and b/data/images/flags/lr.png differ diff --git a/data/images/flags/ls.png b/data/images/flags/ls.png new file mode 100644 index 0000000000000000000000000000000000000000..33fdef101f74e38e2422bb85dc8a31bbf1da326b Binary files /dev/null and b/data/images/flags/ls.png differ diff --git a/data/images/flags/lt.png b/data/images/flags/lt.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ef0da0919b1e77ca91232de0cdf0d99dc8d68f Binary files /dev/null and b/data/images/flags/lt.png differ diff --git a/data/images/flags/lu.png b/data/images/flags/lu.png new file mode 100644 index 0000000000000000000000000000000000000000..4cabba98ae70837922beadc41453b5f848f03854 Binary files /dev/null and b/data/images/flags/lu.png differ diff --git a/data/images/flags/lv.png b/data/images/flags/lv.png new file mode 100644 index 0000000000000000000000000000000000000000..49b69981085ff54568907cd51a56a1e5d8b01ada Binary files /dev/null and b/data/images/flags/lv.png differ diff --git a/data/images/flags/ly.png b/data/images/flags/ly.png new file mode 100644 index 0000000000000000000000000000000000000000..b163a9f8a0660fc223c2648b22ed6a074fe28b21 Binary files /dev/null and b/data/images/flags/ly.png differ diff --git a/data/images/flags/ma.png b/data/images/flags/ma.png new file mode 100644 index 0000000000000000000000000000000000000000..f386770280b92a96a02b13032e056c3adfebfa18 Binary files /dev/null and b/data/images/flags/ma.png differ diff --git a/data/images/flags/mc.png b/data/images/flags/mc.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa830f121ab8ee0107d03251a03fee7cbcf790b Binary files /dev/null and b/data/images/flags/mc.png differ diff --git a/data/images/flags/md.png b/data/images/flags/md.png new file mode 100644 index 0000000000000000000000000000000000000000..4e92c189044b7ec02a5b7a3a9460e1d01b354801 Binary files /dev/null and b/data/images/flags/md.png differ diff --git a/data/images/flags/me.png b/data/images/flags/me.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7253558ab939481a85cc06dcc4d73503afb9f0 Binary files /dev/null and b/data/images/flags/me.png differ diff --git a/data/images/flags/mg.png b/data/images/flags/mg.png new file mode 100644 index 0000000000000000000000000000000000000000..d2715b3d0e11b3a92c4f33cfad6b4f3488d0310d Binary files /dev/null and b/data/images/flags/mg.png differ diff --git a/data/images/flags/mh.png b/data/images/flags/mh.png new file mode 100644 index 0000000000000000000000000000000000000000..fb523a8c39d40401b9abcfb144a73cbb2d76b286 Binary files /dev/null and b/data/images/flags/mh.png differ diff --git a/data/images/flags/mk.png b/data/images/flags/mk.png new file mode 100644 index 0000000000000000000000000000000000000000..db173aaff21955d9aed640beb344986335a1d164 Binary files /dev/null and b/data/images/flags/mk.png differ diff --git a/data/images/flags/ml.png b/data/images/flags/ml.png new file mode 100644 index 0000000000000000000000000000000000000000..2cec8ba440b76ab6ebef1bba4bcb924f6ba40eaf Binary files /dev/null and b/data/images/flags/ml.png differ diff --git a/data/images/flags/mm.png b/data/images/flags/mm.png new file mode 100644 index 0000000000000000000000000000000000000000..f464f67ffb4c7108d217a9f526acb17786641284 Binary files /dev/null and b/data/images/flags/mm.png differ diff --git a/data/images/flags/mn.png b/data/images/flags/mn.png new file mode 100644 index 0000000000000000000000000000000000000000..9396355db45a8ee040c790782209868acaad4b85 Binary files /dev/null and b/data/images/flags/mn.png differ diff --git a/data/images/flags/mo.png b/data/images/flags/mo.png new file mode 100644 index 0000000000000000000000000000000000000000..deb801dda2457f619d53bc176cc889d362cfa032 Binary files /dev/null and b/data/images/flags/mo.png differ diff --git a/data/images/flags/mp.png b/data/images/flags/mp.png new file mode 100644 index 0000000000000000000000000000000000000000..298d588b14b9b19e04c26ab36266ace317b81d59 Binary files /dev/null and b/data/images/flags/mp.png differ diff --git a/data/images/flags/mq.png b/data/images/flags/mq.png new file mode 100644 index 0000000000000000000000000000000000000000..010143b3867f21e7791b8254e806b325c13b2895 Binary files /dev/null and b/data/images/flags/mq.png differ diff --git a/data/images/flags/mr.png b/data/images/flags/mr.png new file mode 100644 index 0000000000000000000000000000000000000000..319546b100864f32c26f29b54b87fe1aee73af21 Binary files /dev/null and b/data/images/flags/mr.png differ diff --git a/data/images/flags/ms.png b/data/images/flags/ms.png new file mode 100644 index 0000000000000000000000000000000000000000..d4cbb433d8f9fe49f06585dc46ee15593e3e621c Binary files /dev/null and b/data/images/flags/ms.png differ diff --git a/data/images/flags/mt.png b/data/images/flags/mt.png new file mode 100644 index 0000000000000000000000000000000000000000..00af94871de66cd0fbf0ca8e46dc436d66e2f713 Binary files /dev/null and b/data/images/flags/mt.png differ diff --git a/data/images/flags/mu.png b/data/images/flags/mu.png new file mode 100644 index 0000000000000000000000000000000000000000..b7fdce1bdd7d174a894a4a075743695301d32450 Binary files /dev/null and b/data/images/flags/mu.png differ diff --git a/data/images/flags/mv.png b/data/images/flags/mv.png new file mode 100644 index 0000000000000000000000000000000000000000..5073d9ec47c3b98e18bd3cd8499433d463ab8e67 Binary files /dev/null and b/data/images/flags/mv.png differ diff --git a/data/images/flags/mw.png b/data/images/flags/mw.png new file mode 100644 index 0000000000000000000000000000000000000000..13886e9f8bf65186eb96071d4399fbe077ec92a3 Binary files /dev/null and b/data/images/flags/mw.png differ diff --git a/data/images/flags/mx.png b/data/images/flags/mx.png new file mode 100644 index 0000000000000000000000000000000000000000..5bc58ab3e3552b74d990d28a0f500e9eb6209dfe Binary files /dev/null and b/data/images/flags/mx.png differ diff --git a/data/images/flags/my.png b/data/images/flags/my.png new file mode 100644 index 0000000000000000000000000000000000000000..9034cbab2c02704b65fba6ecc4a7a1c1d053b6c5 Binary files /dev/null and b/data/images/flags/my.png differ diff --git a/data/images/flags/mz.png b/data/images/flags/mz.png new file mode 100644 index 0000000000000000000000000000000000000000..76405e063d43f2f3b5b9cae4f76d9f1c73cea25b Binary files /dev/null and b/data/images/flags/mz.png differ diff --git a/data/images/flags/na.png b/data/images/flags/na.png new file mode 100644 index 0000000000000000000000000000000000000000..63358c67df905515b49cf50cd766834dea8c18ce Binary files /dev/null and b/data/images/flags/na.png differ diff --git a/data/images/flags/nc.png b/data/images/flags/nc.png new file mode 100644 index 0000000000000000000000000000000000000000..2cad28378232e91848d9a2c8bd9d72a9e6a635f8 Binary files /dev/null and b/data/images/flags/nc.png differ diff --git a/data/images/flags/ne.png b/data/images/flags/ne.png new file mode 100644 index 0000000000000000000000000000000000000000..d85f424f38da0678471ef4b3dc697675118bc7e0 Binary files /dev/null and b/data/images/flags/ne.png differ diff --git a/data/images/flags/nf.png b/data/images/flags/nf.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bcdda12ca7b07b3d16bd88c759db2c82c88884 Binary files /dev/null and b/data/images/flags/nf.png differ diff --git a/data/images/flags/ng.png b/data/images/flags/ng.png new file mode 100644 index 0000000000000000000000000000000000000000..3eea2e020756c41abf81f765659a864c174f89db Binary files /dev/null and b/data/images/flags/ng.png differ diff --git a/data/images/flags/ni.png b/data/images/flags/ni.png new file mode 100644 index 0000000000000000000000000000000000000000..3969aaaaee470644115aa805cc344d032d2faa29 Binary files /dev/null and b/data/images/flags/ni.png differ diff --git a/data/images/flags/np.png b/data/images/flags/np.png new file mode 100644 index 0000000000000000000000000000000000000000..aeb058b7ea8b5d88519dadc69cfe7cdba77a587f Binary files /dev/null and b/data/images/flags/np.png differ diff --git a/data/images/flags/nr.png b/data/images/flags/nr.png new file mode 100644 index 0000000000000000000000000000000000000000..705fc337ccd50d4d49709597d5bd4b946c0d8a32 Binary files /dev/null and b/data/images/flags/nr.png differ diff --git a/data/images/flags/nu.png b/data/images/flags/nu.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ce4aedda9bea0553b43c8d3d849eba6b3d2cd1 Binary files /dev/null and b/data/images/flags/nu.png differ diff --git a/data/images/flags/nz.png b/data/images/flags/nz.png new file mode 100644 index 0000000000000000000000000000000000000000..10d6306d17429012904035e4097bf93a8d205971 Binary files /dev/null and b/data/images/flags/nz.png differ diff --git a/data/images/flags/om.png b/data/images/flags/om.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffba7e8c43f160bb0d9634fb9e6cb4093741340 Binary files /dev/null and b/data/images/flags/om.png differ diff --git a/data/images/flags/pa.png b/data/images/flags/pa.png new file mode 100644 index 0000000000000000000000000000000000000000..9b2ee9a780955566cc7dc2f59ce175f32d3731a0 Binary files /dev/null and b/data/images/flags/pa.png differ diff --git a/data/images/flags/pb.png b/data/images/flags/pb.png new file mode 100644 index 0000000000000000000000000000000000000000..9b1a5538b264a295021f4f717d4299bb8ed98d98 Binary files /dev/null and b/data/images/flags/pb.png differ diff --git a/data/images/flags/pe.png b/data/images/flags/pe.png new file mode 100644 index 0000000000000000000000000000000000000000..62a04977fb2b29b96d01ffef3b88b6bf2ff05862 Binary files /dev/null and b/data/images/flags/pe.png differ diff --git a/data/images/flags/pf.png b/data/images/flags/pf.png new file mode 100644 index 0000000000000000000000000000000000000000..771a0f652254b4e891fc73910aab38967864da54 Binary files /dev/null and b/data/images/flags/pf.png differ diff --git a/data/images/flags/pg.png b/data/images/flags/pg.png new file mode 100644 index 0000000000000000000000000000000000000000..10d6233496c10e52ead975c5a504459fad68ffb8 Binary files /dev/null and b/data/images/flags/pg.png differ diff --git a/data/images/flags/ph.png b/data/images/flags/ph.png new file mode 100644 index 0000000000000000000000000000000000000000..b89e15935d9daf25173f89a36d8111824fda5db5 Binary files /dev/null and b/data/images/flags/ph.png differ diff --git a/data/images/flags/pk.png b/data/images/flags/pk.png new file mode 100644 index 0000000000000000000000000000000000000000..e9df70ca4d63a979e6bcea2399263c081ce5eaeb Binary files /dev/null and b/data/images/flags/pk.png differ diff --git a/data/images/flags/pm.png b/data/images/flags/pm.png new file mode 100644 index 0000000000000000000000000000000000000000..ba91d2c7a0de26e554979f6351d42a1a4e22de3b Binary files /dev/null and b/data/images/flags/pm.png differ diff --git a/data/images/flags/pn.png b/data/images/flags/pn.png new file mode 100644 index 0000000000000000000000000000000000000000..aa9344f575bc92f4c1a5043e6e7d0a8b239daa64 Binary files /dev/null and b/data/images/flags/pn.png differ diff --git a/data/images/flags/pr.png b/data/images/flags/pr.png new file mode 100644 index 0000000000000000000000000000000000000000..82d9130da452fc294baa03a349ec2e71259a80af Binary files /dev/null and b/data/images/flags/pr.png differ diff --git a/data/images/flags/ps.png b/data/images/flags/ps.png new file mode 100644 index 0000000000000000000000000000000000000000..f5f547762ed3a7f556b1cb8b12fb80ed17fe1c4e Binary files /dev/null and b/data/images/flags/ps.png differ diff --git a/data/images/flags/pw.png b/data/images/flags/pw.png new file mode 100644 index 0000000000000000000000000000000000000000..6178b254a5dd2d91eeaa2a2adf124b6dba0af27f Binary files /dev/null and b/data/images/flags/pw.png differ diff --git a/data/images/flags/py.png b/data/images/flags/py.png new file mode 100644 index 0000000000000000000000000000000000000000..cb8723c06408828ce68a932ff472daabecc64139 Binary files /dev/null and b/data/images/flags/py.png differ diff --git a/data/images/flags/qa.png b/data/images/flags/qa.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4c621fa7181fb14c46325a76a16422653aafc7 Binary files /dev/null and b/data/images/flags/qa.png differ diff --git a/data/images/flags/re.png b/data/images/flags/re.png new file mode 100644 index 0000000000000000000000000000000000000000..8332c4ec23c853944c29b02d7b32a88033f48a71 Binary files /dev/null and b/data/images/flags/re.png differ diff --git a/data/images/flags/ro.png b/data/images/flags/ro.png new file mode 100644 index 0000000000000000000000000000000000000000..57e74a6510dd6a4b29668db181cb94727d1eb4b7 Binary files /dev/null and b/data/images/flags/ro.png differ diff --git a/data/images/flags/rs.png b/data/images/flags/rs.png new file mode 100644 index 0000000000000000000000000000000000000000..9439a5b605d82713decf23aba9c63c8d719cc200 Binary files /dev/null and b/data/images/flags/rs.png differ diff --git a/data/images/flags/rw.png b/data/images/flags/rw.png new file mode 100644 index 0000000000000000000000000000000000000000..535649178a885355c836b5c838d096ec3ce8d365 Binary files /dev/null and b/data/images/flags/rw.png differ diff --git a/data/images/flags/sa.png b/data/images/flags/sa.png new file mode 100644 index 0000000000000000000000000000000000000000..b4641c7e8b0dd79aafaa73babdb525d3d2dc6a8e Binary files /dev/null and b/data/images/flags/sa.png differ diff --git a/data/images/flags/sb.png b/data/images/flags/sb.png new file mode 100644 index 0000000000000000000000000000000000000000..a9937ccf091a3faecacbd5101c6630ea0d0b16d8 Binary files /dev/null and b/data/images/flags/sb.png differ diff --git a/data/images/flags/sc.png b/data/images/flags/sc.png new file mode 100644 index 0000000000000000000000000000000000000000..39ee37184e09c39dd05425db127288def220abb7 Binary files /dev/null and b/data/images/flags/sc.png differ diff --git a/data/images/flags/sd.png b/data/images/flags/sd.png new file mode 100644 index 0000000000000000000000000000000000000000..eaab69eb78776f8593b41c8fdc3fd65a86119a0a Binary files /dev/null and b/data/images/flags/sd.png differ diff --git a/data/images/flags/se.png b/data/images/flags/se.png new file mode 100644 index 0000000000000000000000000000000000000000..1994653dac1fc1c6ee3c9fcb35c8af97f16eefc7 Binary files /dev/null and b/data/images/flags/se.png differ diff --git a/data/images/flags/sg.png b/data/images/flags/sg.png new file mode 100644 index 0000000000000000000000000000000000000000..dd34d6121073fffcb2fcb5b9402b3e6361cded35 Binary files /dev/null and b/data/images/flags/sg.png differ diff --git a/data/images/flags/sh.png b/data/images/flags/sh.png new file mode 100644 index 0000000000000000000000000000000000000000..4b1d2a29107be96413eb86e64a75ac7a3ba5793d Binary files /dev/null and b/data/images/flags/sh.png differ diff --git a/data/images/flags/si.png b/data/images/flags/si.png new file mode 100644 index 0000000000000000000000000000000000000000..bb1476ff5fe8e0d3af4fc6bd11e513d95fd9cccd Binary files /dev/null and b/data/images/flags/si.png differ diff --git a/data/images/flags/sj.png b/data/images/flags/sj.png new file mode 100644 index 0000000000000000000000000000000000000000..160b6b5b79db15e623fa55e5774e5d160b933180 Binary files /dev/null and b/data/images/flags/sj.png differ diff --git a/data/images/flags/sk.png b/data/images/flags/sk.png new file mode 100644 index 0000000000000000000000000000000000000000..7ccbc8274ad8f76f28960b83f2bba2a619029d87 Binary files /dev/null and b/data/images/flags/sk.png differ diff --git a/data/images/flags/sm.png b/data/images/flags/sm.png new file mode 100644 index 0000000000000000000000000000000000000000..3df2fdcf8c0b0cc9b581f704c466db1f15c0d422 Binary files /dev/null and b/data/images/flags/sm.png differ diff --git a/data/images/flags/sn.png b/data/images/flags/sn.png new file mode 100644 index 0000000000000000000000000000000000000000..eabb71db4e8275a5bfb7b1b8f3a8374d50da95db Binary files /dev/null and b/data/images/flags/sn.png differ diff --git a/data/images/flags/so.png b/data/images/flags/so.png new file mode 100644 index 0000000000000000000000000000000000000000..4a1ea4b29b3f541f047dead7c202fd3b566575a9 Binary files /dev/null and b/data/images/flags/so.png differ diff --git a/data/images/flags/sr.png b/data/images/flags/sr.png new file mode 100644 index 0000000000000000000000000000000000000000..5eff9271d28cf8bf1cb85378600c4fa4997faa33 Binary files /dev/null and b/data/images/flags/sr.png differ diff --git a/data/images/flags/st.png b/data/images/flags/st.png new file mode 100644 index 0000000000000000000000000000000000000000..2978557b19d7d4283aa9a00ca78dcdd2580edc7a Binary files /dev/null and b/data/images/flags/st.png differ diff --git a/data/images/flags/sy.png b/data/images/flags/sy.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ce30dcb79b443ebc1615fe4889cc26e2d762b1 Binary files /dev/null and b/data/images/flags/sy.png differ diff --git a/data/images/flags/sz.png b/data/images/flags/sz.png new file mode 100644 index 0000000000000000000000000000000000000000..914ee861d419bc6c1e8a7ac432e96deea7504d3a Binary files /dev/null and b/data/images/flags/sz.png differ diff --git a/data/images/flags/tc.png b/data/images/flags/tc.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc1156bec3389e54d3c5bb8339901773a881e68 Binary files /dev/null and b/data/images/flags/tc.png differ diff --git a/data/images/flags/td.png b/data/images/flags/td.png new file mode 100644 index 0000000000000000000000000000000000000000..667f21fd9d552df546386174e506a6b5b606a258 Binary files /dev/null and b/data/images/flags/td.png differ diff --git a/data/images/flags/tf.png b/data/images/flags/tf.png new file mode 100644 index 0000000000000000000000000000000000000000..80529a4361941e01d1def5d581bf2847cf99fef6 Binary files /dev/null and b/data/images/flags/tf.png differ diff --git a/data/images/flags/tg.png b/data/images/flags/tg.png new file mode 100644 index 0000000000000000000000000000000000000000..3aa00ad4dface0a9c23744ab451cec0443f187bf Binary files /dev/null and b/data/images/flags/tg.png differ diff --git a/data/images/flags/th.png b/data/images/flags/th.png new file mode 100644 index 0000000000000000000000000000000000000000..dd8ba91719ba641502bc7ffda16c25dc71b2066c Binary files /dev/null and b/data/images/flags/th.png differ diff --git a/data/images/flags/tj.png b/data/images/flags/tj.png new file mode 100644 index 0000000000000000000000000000000000000000..617bf6455f69849b7f66f43ff36093bbcb07fc3d Binary files /dev/null and b/data/images/flags/tj.png differ diff --git a/data/images/flags/tk.png b/data/images/flags/tk.png new file mode 100644 index 0000000000000000000000000000000000000000..67b8c8cb5191080a1cf33125cfd05efe0b9a76e0 Binary files /dev/null and b/data/images/flags/tk.png differ diff --git a/data/images/flags/tl.png b/data/images/flags/tl.png new file mode 100644 index 0000000000000000000000000000000000000000..77da181e9c57a490c90a99ec08a8718ea8fc0835 Binary files /dev/null and b/data/images/flags/tl.png differ diff --git a/data/images/flags/tm.png b/data/images/flags/tm.png new file mode 100644 index 0000000000000000000000000000000000000000..828020ecd0f6fc73348373c9e7a235fdced09de7 Binary files /dev/null and b/data/images/flags/tm.png differ diff --git a/data/images/flags/tn.png b/data/images/flags/tn.png new file mode 100644 index 0000000000000000000000000000000000000000..183cdd3dc98c6957bde83f375a431e543a3ce9e4 Binary files /dev/null and b/data/images/flags/tn.png differ diff --git a/data/images/flags/to.png b/data/images/flags/to.png new file mode 100644 index 0000000000000000000000000000000000000000..f89b8ba755f5609dc761384fe0656f73c854031e Binary files /dev/null and b/data/images/flags/to.png differ diff --git a/data/images/flags/tt.png b/data/images/flags/tt.png new file mode 100644 index 0000000000000000000000000000000000000000..2a11c1e20ac7f5a4761049adf5e326654b94069b Binary files /dev/null and b/data/images/flags/tt.png differ diff --git a/data/images/flags/tv.png b/data/images/flags/tv.png new file mode 100644 index 0000000000000000000000000000000000000000..28274c5fb40e5d3bacd7c05d9a1b8017eeaffa6c Binary files /dev/null and b/data/images/flags/tv.png differ diff --git a/data/images/flags/tw.png b/data/images/flags/tw.png new file mode 100644 index 0000000000000000000000000000000000000000..f31c654c99c023dbed9a7070103c4542326c4464 Binary files /dev/null and b/data/images/flags/tw.png differ diff --git a/data/images/flags/tz.png b/data/images/flags/tz.png new file mode 100644 index 0000000000000000000000000000000000000000..c00ff7961424da8dabee61bfb53158c537e935e1 Binary files /dev/null and b/data/images/flags/tz.png differ diff --git a/data/images/flags/ua.png b/data/images/flags/ua.png new file mode 100644 index 0000000000000000000000000000000000000000..09563a21941f2a94c937d43aceda1aa546246302 Binary files /dev/null and b/data/images/flags/ua.png differ diff --git a/data/images/flags/ug.png b/data/images/flags/ug.png new file mode 100644 index 0000000000000000000000000000000000000000..33f4affadee432c0d4f499fd7e04736a29c48b06 Binary files /dev/null and b/data/images/flags/ug.png differ diff --git a/data/images/flags/um.png b/data/images/flags/um.png new file mode 100644 index 0000000000000000000000000000000000000000..c1dd9654b0705371876d3e3d06f950be02de2a73 Binary files /dev/null and b/data/images/flags/um.png differ diff --git a/data/images/flags/us.png b/data/images/flags/us.png new file mode 100644 index 0000000000000000000000000000000000000000..10f451fe85c41c6c9a06d543a57114ae2f87ecc1 Binary files /dev/null and b/data/images/flags/us.png differ diff --git a/data/images/flags/uy.png b/data/images/flags/uy.png new file mode 100644 index 0000000000000000000000000000000000000000..31d948a067fe02d067a8c2e69f28cca446bc7c57 Binary files /dev/null and b/data/images/flags/uy.png differ diff --git a/data/images/flags/uz.png b/data/images/flags/uz.png new file mode 100644 index 0000000000000000000000000000000000000000..fef5dc1709d69d32f6535fa9694069a56097adc9 Binary files /dev/null and b/data/images/flags/uz.png differ diff --git a/data/images/flags/va.png b/data/images/flags/va.png new file mode 100644 index 0000000000000000000000000000000000000000..b31eaf225d6fd770e0557c2baf8747c91ce88983 Binary files /dev/null and b/data/images/flags/va.png differ diff --git a/data/images/flags/vc.png b/data/images/flags/vc.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa17b0612bd318a649571fbc4f68e4512c65c5b Binary files /dev/null and b/data/images/flags/vc.png differ diff --git a/data/images/flags/ve.png b/data/images/flags/ve.png new file mode 100644 index 0000000000000000000000000000000000000000..00c90f9aff09fb1b6d697c6a5680df01f37cad60 Binary files /dev/null and b/data/images/flags/ve.png differ diff --git a/data/images/flags/vg.png b/data/images/flags/vg.png new file mode 100644 index 0000000000000000000000000000000000000000..415690798657a5921fd007b8ae85a5e5d414e7fa Binary files /dev/null and b/data/images/flags/vg.png differ diff --git a/data/images/flags/vi.png b/data/images/flags/vi.png new file mode 100644 index 0000000000000000000000000000000000000000..ed26915a3238534bf8f1249b75dd9ddde10db65a Binary files /dev/null and b/data/images/flags/vi.png differ diff --git a/data/images/flags/vn.png b/data/images/flags/vn.png new file mode 100644 index 0000000000000000000000000000000000000000..ec7cd48a3468a511e27c49a69194b0ef5564e615 Binary files /dev/null and b/data/images/flags/vn.png differ diff --git a/data/images/flags/vu.png b/data/images/flags/vu.png new file mode 100644 index 0000000000000000000000000000000000000000..b3397bc63d718b344e604266259134e653925c9d Binary files /dev/null and b/data/images/flags/vu.png differ diff --git a/data/images/flags/wf.png b/data/images/flags/wf.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9558734f0482439b2292a01f768639a287ac25 Binary files /dev/null and b/data/images/flags/wf.png differ diff --git a/data/images/flags/ws.png b/data/images/flags/ws.png new file mode 100644 index 0000000000000000000000000000000000000000..c16950802ea95b40a4e024be6cce870b1991f40e Binary files /dev/null and b/data/images/flags/ws.png differ diff --git a/data/images/flags/ye.png b/data/images/flags/ye.png new file mode 100644 index 0000000000000000000000000000000000000000..468dfad03867903f825e82de35934f3191e5f638 Binary files /dev/null and b/data/images/flags/ye.png differ diff --git a/data/images/flags/yt.png b/data/images/flags/yt.png new file mode 100644 index 0000000000000000000000000000000000000000..c298f378beee6b170a6909fd4f73ffbeb5997cff Binary files /dev/null and b/data/images/flags/yt.png differ diff --git a/data/images/flags/za.png b/data/images/flags/za.png new file mode 100644 index 0000000000000000000000000000000000000000..57c58e2119f402072640ca758657798b621f3fb1 Binary files /dev/null and b/data/images/flags/za.png differ diff --git a/data/images/flags/zm.png b/data/images/flags/zm.png new file mode 100644 index 0000000000000000000000000000000000000000..c25b07beef894408ae11c3be294d6e0eeb28c0bb Binary files /dev/null and b/data/images/flags/zm.png differ diff --git a/data/images/flags/zw.png b/data/images/flags/zw.png new file mode 100644 index 0000000000000000000000000000000000000000..53c97259b9b3e31c2f612e78344d035281682fa7 Binary files /dev/null and b/data/images/flags/zw.png differ diff --git a/data/images/imdb.png b/data/images/imdb.png new file mode 100644 index 0000000000000000000000000000000000000000..961177009aabef915420ef26fde96679d31d7c68 Binary files /dev/null and b/data/images/imdb.png differ diff --git a/data/images/like.png b/data/images/like.png new file mode 100644 index 0000000000000000000000000000000000000000..92460f753888ffa65f349b86047fd6c31cb7dd63 Binary files /dev/null and b/data/images/like.png differ diff --git a/data/images/menu/system18.png b/data/images/menu/system18.png index ecb1061db5c0a0dc71792f908d482c5e6b76467d..fc3bbb0f47ba6dce2e9a5309879f6377edcfdc56 100644 Binary files a/data/images/menu/system18.png and b/data/images/menu/system18.png differ diff --git a/data/images/network/cinemax.png b/data/images/network/cinemax.png new file mode 100644 index 0000000000000000000000000000000000000000..c00865c89088c7e7834b2691a6cf86891fc54a18 Binary files /dev/null and b/data/images/network/cinemax.png differ diff --git a/data/images/network/city.png b/data/images/network/city.png new file mode 100644 index 0000000000000000000000000000000000000000..979582dbb49462d5b3cc1703179e1ad3650f493d Binary files /dev/null and b/data/images/network/city.png differ diff --git a/data/images/notifiers/email.png b/data/images/notifiers/email.png new file mode 100644 index 0000000000000000000000000000000000000000..ee3fa70755bc66a9710193c44c35e5816b42d46e Binary files /dev/null and b/data/images/notifiers/email.png differ diff --git a/data/images/notifiers/pushalot.png b/data/images/notifiers/pushalot.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d9008c1e6078ebc2275fe1b28961fa1143ee70 Binary files /dev/null and b/data/images/notifiers/pushalot.png differ diff --git a/data/images/notifiers/synologynotifier.png b/data/images/notifiers/synologynotifier.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec497da27b874b8ae375c3080ac43002d4ebba0 Binary files /dev/null and b/data/images/notifiers/synologynotifier.png differ diff --git a/data/images/providers/dailytvtorrents.gif b/data/images/providers/dailytvtorrents.gif new file mode 100644 index 0000000000000000000000000000000000000000..fe5f50730a21c226d51ae321722da929ff8b3474 Binary files /dev/null and b/data/images/providers/dailytvtorrents.gif differ diff --git a/data/images/providers/iptorrents.png b/data/images/providers/iptorrents.png new file mode 100644 index 0000000000000000000000000000000000000000..d767a681647508057e3c756d7b8c4811a6a551d7 Binary files /dev/null and b/data/images/providers/iptorrents.png differ diff --git a/data/images/providers/thepiratebay.png b/data/images/providers/thepiratebay.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f1c3ef06d8c4069787d445d14d35ececb7785d Binary files /dev/null and b/data/images/providers/thepiratebay.png differ diff --git a/data/images/providers/thepiratebay_.png b/data/images/providers/thepiratebay_.png new file mode 100644 index 0000000000000000000000000000000000000000..681ab05476dbce5c0ff55c1f450b15fbd7ab1f6f Binary files /dev/null and b/data/images/providers/thepiratebay_.png differ diff --git a/data/images/ratings/0.png b/data/images/ratings/0.png new file mode 100644 index 0000000000000000000000000000000000000000..1b8c2eeabfb68dc841baaeabec7fc23dd5aa9396 Binary files /dev/null and b/data/images/ratings/0.png differ diff --git a/data/images/ratings/1.png b/data/images/ratings/1.png new file mode 100644 index 0000000000000000000000000000000000000000..e1e14ed2dc6d8c00c46b08941debf92bade4b686 Binary files /dev/null and b/data/images/ratings/1.png differ diff --git a/data/images/ratings/10.png b/data/images/ratings/10.png new file mode 100644 index 0000000000000000000000000000000000000000..54a95807568ecc5225263b9f9055677dd658b177 Binary files /dev/null and b/data/images/ratings/10.png differ diff --git a/data/images/ratings/2.png b/data/images/ratings/2.png new file mode 100644 index 0000000000000000000000000000000000000000..1c347ed01c54913661ca0b8703c2d56c97b24fb4 Binary files /dev/null and b/data/images/ratings/2.png differ diff --git a/data/images/ratings/3.png b/data/images/ratings/3.png new file mode 100644 index 0000000000000000000000000000000000000000..388722e064884f85679131ac9b12ddab724619b5 Binary files /dev/null and b/data/images/ratings/3.png differ diff --git a/data/images/ratings/4.png b/data/images/ratings/4.png new file mode 100644 index 0000000000000000000000000000000000000000..5376d114f0759e4b0ddebf97a617cea55742ad34 Binary files /dev/null and b/data/images/ratings/4.png differ diff --git a/data/images/ratings/5.png b/data/images/ratings/5.png new file mode 100644 index 0000000000000000000000000000000000000000..e8ed6bcd244bb6bde8b3b98be636ec9843555bf9 Binary files /dev/null and b/data/images/ratings/5.png differ diff --git a/data/images/ratings/6.png b/data/images/ratings/6.png new file mode 100644 index 0000000000000000000000000000000000000000..e39d578b96b2ee545a805d751327278953d37ab5 Binary files /dev/null and b/data/images/ratings/6.png differ diff --git a/data/images/ratings/7.png b/data/images/ratings/7.png new file mode 100644 index 0000000000000000000000000000000000000000..af0568535e42a4cc8bc61eac790c643a1e847057 Binary files /dev/null and b/data/images/ratings/7.png differ diff --git a/data/images/ratings/8.png b/data/images/ratings/8.png new file mode 100644 index 0000000000000000000000000000000000000000..82b8b52f9aae1d0db2d125c768a4fd8f8531d634 Binary files /dev/null and b/data/images/ratings/8.png differ diff --git a/data/images/ratings/9.png b/data/images/ratings/9.png new file mode 100644 index 0000000000000000000000000000000000000000..d91d2bcafdfe7903c418457facbf46ef6b9b505a Binary files /dev/null and b/data/images/ratings/9.png differ diff --git a/data/images/search32.png b/data/images/search32.png new file mode 100644 index 0000000000000000000000000000000000000000..38046cd6798de09edf78005c05183b05e4b7a7aa Binary files /dev/null and b/data/images/search32.png differ diff --git a/data/images/subtitles/itasa.png b/data/images/subtitles/itasa.png new file mode 100644 index 0000000000000000000000000000000000000000..908a52adae06ff1de52837cdd35b7942a7ec43ce Binary files /dev/null and b/data/images/subtitles/itasa.png differ diff --git a/data/images/tablesorter/asc.gif b/data/images/tablesorter/asc.gif new file mode 100644 index 0000000000000000000000000000000000000000..fa3fe40113a4482d2d3ff9b39c9056067477999e Binary files /dev/null and b/data/images/tablesorter/asc.gif differ diff --git a/data/images/tablesorter/bg.gif b/data/images/tablesorter/bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..40c6a65aa2862b0939e2707b4d9c1174225f21c1 Binary files /dev/null and b/data/images/tablesorter/bg.gif differ diff --git a/data/images/tablesorter/desc.gif b/data/images/tablesorter/desc.gif new file mode 100644 index 0000000000000000000000000000000000000000..88c6971d61ea3eecd468cd38c296346ef8b21bd1 Binary files /dev/null and b/data/images/tablesorter/desc.gif differ diff --git a/data/images/xbmc-notify.png b/data/images/xbmc-notify.png new file mode 100644 index 0000000000000000000000000000000000000000..a657e4f8fce5ef385e2ffa247737a2cdc2f77c9d Binary files /dev/null and b/data/images/xbmc-notify.png differ diff --git a/data/interfaces/default/apiBuilder.tmpl b/data/interfaces/default/apiBuilder.tmpl index 1234074cb6a26fa88ccae02debd4ecb9638c474c..6b1bc5a6fd8b16bfa242b72a8f099079ba29dbbb 100644 --- a/data/interfaces/default/apiBuilder.tmpl +++ b/data/interfaces/default/apiBuilder.tmpl @@ -1,28 +1,25 @@ -<!DOCTYPE HTML> -<html> - <head> - <meta charset="utf-8"> - <title>Sick Beard - API Builder</title> - <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <meta name="viewport" content="width=device-width"> - <meta name="robots" content="noindex"> - - <script type="text/javascript" charset="utf-8"> - <!-- - sbRoot = "$sbRoot"; - //--> - </script> - <script type="text/javascript" src="$sbRoot/js/lib/jquery-1.7.2.min.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/apibuilder.js?$sbPID"></script> - - <style type="text/css"> - <!-- - #apibuilder select { padding: 2px 2px 2px 6px; display: block; float: left; margin: auto 8px 4px auto; } - #apibuilder select option { padding: 1px 6px; line-height: 1.2em; } - #apibuilder .disabled { color: #ccc; } - #apibuilder .action { background-color: #efefef; } - --> - </style> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>API Builder</title> +<script type="text/javascript" charset="utf-8"> +<!-- +sbRoot = "$sbRoot"; +//--> +</script> +<script type="text/javascript" src="$sbRoot/js/lib/jquery-1.8.3.min.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/apibuilder.js?$sbPID"></script> + +<style type="text/css"> +<!-- +#apibuilder select { padding: 2px 2px 2px 6px; display: block; float: left; margin: auto 8px 4px auto; +} +#apibuilder select option { padding: 1px 6px; line-height: 1.2em; } +#apibuilder .disabled { color: #ccc; } +#apibuilder .action { background-color: #efefef; } +--> +</style> <script type="text/javascript"> var hide_empty_list=true; @@ -93,8 +90,8 @@ addList("sb.setdefaults-status", "Archived", "&status=archived", "sb.setdefaults addList("sb.setdefaults-status", "Ignored", "&status=ignored", "sb.setdefaults-opt"); addOption("sb.setdefaults-opt", "Optional Param", "", 1); -addList("sb.setdefaults-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); -addList("sb.setdefaults-opt", "Use Season Folder", "&flatten_folders=0", "quality"); +addList("sb.setdefaults-opt", "No Season Folder", "&season_folder=0", "quality"); +addList("sb.setdefaults-opt", "Use Season Folder", "&season_folder=1", "quality"); addOption("shows", "Optional Param", "", 1); addOption("shows", "Show Only Paused", "&paused=1"); @@ -111,8 +108,8 @@ addList("show.addexisting-tvdbid", "101501 (Ancient Aliens)", "&tvdbid=101501", addList("show.addexisting-tvdbid", "80348 (Chuck)", "&tvdbid=80348", "show.addexisting-opt"); addOption("show.addexisting-opt", "Optional Param", "", 1); -addList("show.addexisting-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); -addList("show.addexisting-opt", "Use Season Folder", "&flatten_folders=0", "quality"); +addList("show.addexisting-opt", "No Season Folder", "&season_folder=0", "quality"); +addList("show.addexisting-opt", "Use Season Folder", "&season_folder=1", "quality"); addList("show.addnew", "101501 (Ancient Aliens)", "&tvdbid=101501", "show.addnew-loc"); addList("show.addnew", "80348 (Chuck)", "&tvdbid=80348", "show.addnew-loc"); @@ -130,8 +127,8 @@ addList("show.addnew-status", "Archived", "&status=archived", "show.addnew-opt") addList("show.addnew-status", "Ignored", "&status=ignored", "show.addnew-opt"); addOption("show.addnew-opt", "Optional Param", "", 1); -addList("show.addnew-opt", "Flatten (No Season Folder)", "&flatten_folders=1", "quality"); -addList("show.addnew-opt", "Use Season Folder", "&flatten_folders=0", "quality"); +addList("show.addnew-opt", "No Season Folder", "&season_folder=0", "quality"); +addList("show.addnew-opt", "Use Season Folder", "&season_folder=1", "quality"); addOptGroup("sb.searchtvdb", "Search by Name"); addList("sb.searchtvdb", "Lost", "&name=Lost", "sb.searchtvdb-lang"); @@ -268,25 +265,15 @@ addList("episode.setstatus", "$curShow.name", "&tvdbid=$curShow.tvdbid", "episod // build out each show's season+episode list for episode.setstatus cmd #for $curShow in $episodeSQLResults: - #set $curSeason = -1 #for $curShowSeason in $episodeSQLResults[$curShow]: - #if $curShowSeason.season != $curSeason and $curShowSeason.season != 0: - // insert just the season as the ep number is now optional - addList("episode.setstatus-$curShow", "Season $curShowSeason.season", "&season=$curShowSeason.season", "episode-status-$curShow"); - #end if - #set $curSeason = int($curShowSeason.season) addList("episode.setstatus-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-status-$curShow"); #end for -addList("episode-status-$curShow", "Wanted", "&status=wanted", "force"); -addList("episode-status-$curShow", "Skipped", "&status=skipped", "force"); -addList("episode-status-$curShow", "Archived", "&status=archived", "force"); -addList("episode-status-$curShow", "Ignored", "&status=ignored", "force"); +addOption("episode-status-$curShow", "Wanted", "&status=wanted"); +addOption("episode-status-$curShow", "Skipped", "&status=skipped"); +addOption("episode-status-$curShow", "Archived", "&status=archived"); +addOption("episode-status-$curShow", "Ignored", "&status=ignored"); #end for -addOption("force", "Optional Param", "", 1); -addOption("force", "Replace Downloaded EP", "&force=1"); -addOption("force", "Skip Downloaded EP", "&force=0"); - addOption("future", "Optional Param", "", 1); addList("future", "Sort by Date", "&sort=date", "future-type"); addList("future", "Sort by Network", "&sort=network", "future-type"); @@ -375,8 +362,8 @@ addOption("show.pause-opt", "Pause", "&pause=1"); <select name="sixthlevel"><option></option></select> <select name="seventhlevel"><option></option></select> <div style="float: left; "> - <input type="button" value="Reset" onclick="resetListGroup('api',1)" /> - <input type="button" value="Go" onclick="goListGroup(this.form['apikey'].value, this.form['seventhlevel'].value, this.form['sixthlevel'].value, this.form['fifthlevel'].value, this.form['forthlevel'].value, this.form['thirdlevel'].value, this.form['secondlevel'].value, this.form['firstlevel'].value)" /> + <input class="btn" type="button" value="Reset" onclick="resetListGroup('api',1)" /> + <input class="btn" type="button" value="Go" onclick="goListGroup(this.form['apikey'].value, this.form['seventhlevel'].value, this.form['sixthlevel'].value, this.form['fifthlevel'].value, this.form['forthlevel'].value, this.form['thirdlevel'].value, this.form['secondlevel'].value, this.form['firstlevel'].value)" /> </div> </td> </tr> diff --git a/data/interfaces/default/comingEpisodes.tmpl b/data/interfaces/default/comingEpisodes.tmpl index e2a1561746d0980311481d76a71cbb3d52dd2751..05b64d7c1f4b4a12d2a377c4cc626de39a69dc18 100644 --- a/data/interfaces/default/comingEpisodes.tmpl +++ b/data/interfaces/default/comingEpisodes.tmpl @@ -1,283 +1,353 @@ -#import sickbeard -#import datetime -#from sickbeard.common import * -#set global $title="Coming Episodes" - -#set global $sbPath=".." - -#set global $topmenu="comingEpisodes" -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") -#set $sort = $sickbeard.COMING_EPS_SORT -<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> - -<div class="h2footer align-right"> - <b>Key:</b> - <span class="listing-overdue">Past</span> - <span class="listing-current">Current</span> - <span class="listing-default">Future</span> - <span class="listing-toofar">Distant</span> - <!-- <span class="listing-unknown">Unknown</span> //--> -</div> - -#if $layout == 'list': -<!-- start list view //--> -<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> -<script type="text/javascript" charset="utf-8"> -<!-- -\$.tablesorter.addParser({ - id: 'loadingNames', - is: function(s) { - return false; - }, - format: function(s) { - if (s.indexOf('Loading...') == 0) - return s.replace('Loading...','000'); - return (s || '').replace(/^(The|A)\s/i,''); - }, - type: 'text' -}); -\$.tablesorter.addParser({ - id: 'quality', - is: function(s) { - return false; - }, - format: function(s) { - return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7); - }, - type: 'numeric' -}); - - -\$(document).ready(function(){ - -#set $sort_codes = {'date': 0, 'show': 1, 'network': 4} -#if $sort not in $sort_codes: - $sort = 'date' -#end if - sortList = [[$sort_codes[$sort],0]]; - - \$("#showListTable:has(tbody tr)").tablesorter({ - widgets: ['stickyHeaders'], - sortList: sortList, - textExtraction: { - 5: function(node) { return \$(node).find("span").text().toLowerCase(); } - }, - headers: { - 0: { sorter: 'isoDate' }, - 1: { sorter: 'loadingNames' }, - 2: { sorter: false }, - 3: { sorter: false }, - 4: { sorter: 'loadingNames' }, - 5: { sorter: 'quality' }, - 6: { sorter: false }, - 7: { sorter: false } - } - }); - - \$('#sbRoot').ajaxEpSearch(); - -}); -//--> -</script> - - #set $show_div = "listing-default" - -<input type="hidden" id="sbRoot" value="$sbRoot" /> - -<br/> - <table id="showListTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> - <thead><tr><th>Airdate</th><th>Show</th><th class="nowrap">Next Ep</th><th>Next Ep Name</th><th>Network</th><th>Quality</th><th>tvDB</th><th>Search</th></tr></thead> - <tbody> - - #for $cur_result in $sql_results: - - #if int($cur_result["paused"]) and not $sickbeard.COMING_EPS_DISPLAY_PAUSED: - #continue - #end if - - #set $cur_ep_airdate = int($cur_result["airdate"]) - #if $cur_ep_airdate < $today: - #set $show_div = "listing-overdue" - #elif $cur_ep_airdate >= $next_week: - #set $show_div = "listing-toofar" - #elif $cur_ep_airdate >= $today and $cur_ep_airdate < $next_week: - #if $cur_ep_airdate == $today: - #set $show_div = "listing-current" - #else: - #set $show_div = "listing-default" - #end if - #end if - - <!-- start $cur_result["show_name"] //--> - <tr class="$show_div"> - <td align="center" class="nowrap">$datetime.date.fromordinal(int($cur_result["airdate"]))</td> - <td class="tvShow"><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"]</a> - #if int($cur_result["paused"]): - <span class="pause">[paused]</span> - #end if - </td> - <td class="nowrap" align="center"><%="%02ix%02i" % (int(cur_result["season"]), int(cur_result["episode"])) %></td> - <td> - #if $cur_result["description"] != "" and $cur_result["description"] != None: - <img alt="" src="$sbRoot/images/info32.png" height="16" width="16" class="plotInfo" id="plot_info_<%=str(cur_result["showid"])+"_"+str(cur_result["season"])+"_"+str(cur_result["episode"])%>" /> - #end if - $cur_result["name"] - </td> - <td align="center">$cur_result["network"]</td> - <td align="center"> -#if int($cur_result["quality"]) in $qualityPresets: - <span class="quality $qualityPresetStrings[int($cur_result["quality"])]">$qualityPresetStrings[int($cur_result["quality"])]</span> -#else: - <span class="quality Custom">Custom</span> -#end if - </td> - <td align="center"><a href="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}"><img alt="[info]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a></td> - <td align="center"> - <a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&season=$cur_result["season"]&episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="forceUpdate epSearch"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search16.png" id="forceUpdateImage-${cur_result["showid"]}" /></a> - </td> - </tr> - <!-- end $cur_result["show_name"] //--> - #end for - </tbody> - </table> -<!-- end list view //--> -#else: -<!-- start non list view //--> -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - \$('#sbRoot').ajaxEpSearch({'size': 16, 'loadingImage': 'loading16_333333.gif'}); - \$(".epSummary").hide(); - \$(".epSummaryTrigger").click(function() { - \$(this).next(".epSummary").slideToggle('normal', function() { - \$(this).prev(".epSummaryTrigger").attr('src', function(i, src) { - return \$(this).next(".epSummary").is(':visible') ? src.replace('plus','minus') : src.replace('minus','plus'); - }); - }); - }); -}); -//--> -</script> - - #set $cur_segment = None - #set $too_late_header = False - #set $missed_header = False - #set $show_div = "epListing listing-default" - - #if $sort == "show": - <br/><br/> - #end if - - #for $cur_result in $sql_results: - <!-- start $cur_result["show_name"] //--> - - #if int($cur_result["paused"]) and not $sickbeard.COMING_EPS_DISPLAY_PAUSED: - #continue - #end if - - #if $sort == "network": - #if $cur_result["network"] and $cur_segment != $cur_result["network"]: - <br /><h2 class="network">$cur_result["network"]</h2> - #set $cur_ep_airdate = int($cur_result["airdate"]) - #if $cur_ep_airdate < $today: - #set $show_div = "epListing listing-overdue" - #elif $cur_ep_airdate >= $next_week: - #set $show_div = "epListing listing-toofar" - #elif $cur_ep_airdate >= $today and $cur_ep_airdate < $next_week: - #if $cur_ep_airdate == $today: - #set $show_div = "epListing listing-current" - #else: - #set $show_div = "epListing listing-default" - #end if - #end if - #set $cur_segment = $cur_result["network"] - #end if - #elif $sort == "date": - #set $cur_ep_airdate = int($cur_result["airdate"]) - #if $cur_segment != $cur_ep_airdate: - #if $cur_ep_airdate < $today and not $missed_header: - <br /><h2 class="day">Missed</h2> - #set $missed_header = True - #set $show_div = "epListing listing-overdue" - #elif $cur_ep_airdate >= $next_week and not $too_late_header: - <br /><h2 class="day">Later</h2> - #set $show_div = "epListing listing-toofar" - #set $too_late_header = True - #elif $cur_ep_airdate >= $today and $cur_ep_airdate < $next_week: - #if $cur_ep_airdate == $today: - <br /><h2 class="day">$datetime.date.fromordinal($cur_ep_airdate).strftime("%A").decode($sickbeard.SYS_ENCODING) <span style="font-size: 12px;">[today]</span></h2> - #set $show_div = "epListing listing-current" - #else: - <br /><h2 class="day">$datetime.date.fromordinal($cur_ep_airdate).strftime("%A").decode($sickbeard.SYS_ENCODING)</h2> - #set $show_div = "epListing listing-default" - #end if - #end if - #set $cur_segment = $cur_ep_airdate - #end if - #end if - - <div class="$show_div" id="listing-${cur_result["showid"]}"> - <div class="tvshowDiv"> - <table width="100%" border="0" cellpadding="0" cellspacing="0"> - <tr> - <th #if $layout == 'banner' then "" else "colspan=\"2\""#> - <span class="tvshowTitle"><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"] - #if int($cur_result["paused"]): - <span class="pause">[paused]</span> - #end if - </a></span> - <span class="tvshowTitleIcons"> - <a href="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a> - <span><a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&season=$cur_result["season"]&episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="epSearch forceUpdate"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search16.png" id="forceUpdateImage-${cur_result["showid"]}" /></a></span> - </span> - </th> - </tr> - <tr> - <th #if $layout == 'banner' then "class=\"nobg\"" else "rowspan=\"2\""# style="background-color: #efefef;"> - <a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}"><img alt="" class="#if $layout == 'banner' then "bannerThumb" else "posterThumb"#" src="$sbRoot/showPoster/?show=${cur_result["showid"]}&which=$layout" /></a> - </th> -#if $layout == 'banner': - </tr> - <tr> -#end if - <td class="nextEp"> - <span class="title">Next Episode:</span> <span><%=str(cur_result["season"])+"x"+"%02i" % int(cur_result["episode"]) %> - $cur_result["name"] ($datetime.date.fromordinal(int($cur_result["airdate"])))</span> - </td> - </tr> - <tr> - <td style="vertical-align: top;"> - <span class="title">Airs:</span> <span>$cur_result["airs"] on $cur_result["network"]</span> -#if int($cur_result["quality"]) in $qualityPresets: - [<span class="title">$qualityPresetStrings[int($cur_result["quality"])]</span>]<br /> -#else: - [<span class="title">Custom</span>]<br /> -#end if - #if $cur_result["description"] == '': - <div class="epSummary" style="margin-left:0;display:block !important;">[There is no summary added for this episode]</div> - #else: - <img class="epSummaryTrigger" src="$sbRoot/images/plus.png" height="16" width="16" alt="" title="Toggle Summary" /><div class="epSummary">$cur_result["description"]</div> - #end if - <br/> - </td> - </tr> - </table> - </div> - </div> - - <!-- end $cur_result["show_name"] //--> - #end for - - <br/> - -<!-- end non list view //--> -#end if - -<script type="text/javascript" charset="utf-8"> -<!-- -window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes -//--> -</script> - -<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import datetime +#from sickbeard.common import * +#set global $title="" +#set global $header="Coming Episodes" + +#set global $sbPath=".." + +#set global $topmenu="comingEpisodes" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#set $sort = $sickbeard.COMING_EPS_SORT +<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<style type="text/css"> +.sort_data {display:none} +</style> + +<div class="h2footer align-right"> + <b>Key:</b> + <span class="listing_overdue">Missed</span> + <span class="listing_current">Current</span> + <span class="listing_default">Future</span> + <span class="listing_toofar">Distant</span> + <a class="btn btn-mini btn-inverse forceBacklog" href="webcal://$sbHost:$sbHttpPort/calendar"> + <i class="icon-calendar icon-white"></i>Subscribe</a> + <!-- <span class="listing_unknown">Unknown</span> //--> +</div> + +#if $layout == 'list': +<!-- start list view //--> +<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> +<script type="text/javascript" charset="utf-8"> +<!-- +\$.tablesorter.addParser({ + id: 'loadingNames', + is: function(s) { + return false; + }, + format: function(s) { + if (s.indexOf('Loading...') == 0) + return s.replace('Loading...','000'); + #if not $sickbeard.SORT_ARTICLE: + return (s || '').replace(/^(The|A|An|Le|La|Un|Une)\s/i,''); + #else: + return (s || ''); + #end if + }, + type: 'text' +}); +\$.tablesorter.addParser({ + id: 'quality', + is: function(s) { + return false; + }, + format: function(s) { + return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7); + }, + type: 'numeric' +}); +\$.tablesorter.addParser({ + id: 'cDate', + is: function(s) { + return false; + }, + format: function(s) { + return s; + }, + type: 'numeric' +}); + +\$(document).ready(function(){ + +#set $sort_codes = {'date': 0, 'show': 1, 'network': 4} +#if $sort not in $sort_codes: + $sort = 'date' +#end if + sortList = [[$sort_codes[$sort],0]]; + + \$("#showListTable:has(tbody tr)").tablesorter({ + widgets: ['stickyHeaders'], + sortList: sortList, + textExtraction: { + 0: function(node) { return \$(node).find("span").text().toLowerCase(); }, + 4: function(node) { return \$(node).find("img").attr("alt"); }, + 5: function(node) { return \$(node).find("span").text().toLowerCase(); } + }, + headers: { + 0: { sorter: 'cDate' }, + 1: { sorter: 'loadingNames' }, + 2: { sorter: false }, + 3: { sorter: false }, + 4: { sorter: 'loadingNames' }, + 5: { sorter: 'quality' }, + 6: { sorter: false }, + 7: { sorter: false }, + 8: { sorter: false } + } + }); + + \$('#sbRoot').ajaxEpSearch(); + +}); +//--> +</script> + + #set $show_div = "listing_default" + +<input type="hidden" id="sbRoot" value="$sbRoot" /> + + <table id="showListTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> + <thead><tr><th>Airdate</th><th>Show</th><th class="nowrap">Next Ep</th><th>Next Ep Name</th><th>Network</th><th>Quality</th><th>IMDb</th><th>tvDB</th><th>Search</th></tr></thead> + <tbody> + + #for $cur_result in $sql_results: + + #if int($cur_result["paused"]) and not $sickbeard.COMING_EPS_DISPLAY_PAUSED: + #continue + #end if + + #set $cur_ep_airdate = $cur_result["localtime"].date() + #set $cur_ep_enddate = $cur_result["localtime"] + datetime.timedelta(minutes=$cur_result["runtime"]) + #if $cur_ep_enddate < $today: + #set $show_div = "listing_overdue" + #elif $cur_ep_airdate >= $next_week.date(): + #set $show_div = "listing_toofar" + #elif $cur_ep_airdate >= $today.date() and $cur_ep_airdate < $next_week.date(): + #if $cur_ep_airdate == $today.date(): + #set $show_div = "listing_current" + #else: + #set $show_div = "listing_default" + #end if + #end if + + <!-- start $cur_result["show_name"] //--> + <tr class="$show_div"> + <td align="center" class="nowrap">$cur_result["localtime"].strftime('%c').decode($sickbeard.SYS_ENCODING)<span class="sort_data">$time.mktime($cur_result["localtime"].timetuple())</span></td> + <td class="tvShow"><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"]</a> + #if int($cur_result["paused"]): + <span class="pause">[paused]</span> + #end if + </td> + <td class="nowrap" align="center" style="color: #555555; font-size: 16px;"><b><%="%02ix%02i" % (int(cur_result["season"]), int(cur_result["episode"])) %></b></td> + <td> + #if $cur_result["description"] != "" and $cur_result["description"] != None: + <img alt="" src="$sbRoot/images/info32.png" height="16" width="16" class="plotInfo" id="plot_info_<%=str(cur_result["showid"])+"_"+str(cur_result["season"])+"_"+str(cur_result["episode"])%>" style="padding-top: 6px;"/> + #end if + $cur_result["name"] + </td> + <td align="center">$cur_result["network"]</td> + <td align="center"> +#if int($cur_result["quality"]) in $qualityPresets: + <span class="quality $qualityPresetStrings[int($cur_result["quality"])]">$qualityPresetStrings[int($cur_result["quality"])]</span> +#else: + <span class="quality Custom">Custom</span> +#end if + </td> + <td align="center"> + #if $cur_result["imdb_id"]: + <a href="http://www.imdb.com/title/${cur_result["imdb_id"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.imdb.com/title/${cur_result["imdb_id"]}"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" /> + #end if + </td> + <td align="center"><a href="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a></td> + <td align="center"> + <a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&season=$cur_result["season"]&episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="forceUpdate epSearch"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a> + </td> + </tr> + <!-- end $cur_result["show_name"] //--> + #end for + </tbody> + </table> +<!-- end list view //--> +#else: +<!-- start non list view //--> +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + \$('#sbRoot').ajaxEpSearch({'size': 16, 'loadingImage': 'loading16.gif'}); + \$(".ep_summary").hide(); + \$(".ep_summaryTrigger").click(function() { + \$(this).next(".ep_summary").slideToggle('normal', function() { + \$(this).prev(".ep_summaryTrigger").attr('src', function(i, src) { + return \$(this).next(".ep_summary").is(':visible') ? src.replace('plus','minus') : src.replace('minus','plus'); + }); + }); + }); +}); +//--> +</script> + + #set $cur_segment = None + #set $too_late_header = False + #set $missed_header = False + #set $today_header = False + #set $show_div = "ep_listing listing_default" + + #if $sort == "show": + <br/><br/> + #end if + + #for $cur_result in $sql_results: + <!-- start $cur_result["show_name"] //--> + + #if int($cur_result["paused"]) and not $sickbeard.COMING_EPS_DISPLAY_PAUSED: + #continue + #end if + + #if $sort == "network": + #if $cur_result["network"] and $cur_segment != $cur_result["network"]: + <h1 class="network">$cur_result["network"]</h1> + #set $cur_segment = $cur_result["network"] + #end if + #set $cur_ep_airdate = $cur_result["localtime"].date() + #set $cur_ep_enddate = $cur_result["localtime"] + datetime.timedelta(minutes=$cur_result["runtime"]) + #if $cur_ep_enddate < $today: + #set $show_div = "ep_listing listing_overdue" + #elif $cur_ep_airdate >= $next_week.date(): + #set $show_div = "ep_listing listing_toofar" + #elif $cur_ep_enddate >= $today and $cur_ep_airdate < $next_week.date(): + #if $cur_ep_airdate == $today.date(): + #set $show_div = "ep_listing listing_current" + #else: + #set $show_div = "ep_listing listing_default" + #end if + #end if + #elif $sort == "date": + #set $cur_ep_airdate = $cur_result["localtime"].date() + #set $cur_ep_enddate = $cur_result["localtime"] + datetime.timedelta(minutes=$cur_result["runtime"]) + #if $cur_segment != $cur_ep_airdate: + #if $cur_ep_enddate < $today and $cur_ep_airdate != $today.date() and not $missed_header: + <br /><h1 class="day">Missed</h1> + #set $missed_header = True + #elif $cur_ep_airdate >= $next_week.date() and not $too_late_header: + <br /><h1 class="day">Later</h1> + #set $too_late_header = True + #elif $cur_ep_enddate >= $today and $cur_ep_airdate < $next_week.date(): + #if $cur_ep_airdate == $today.date(): + <br /><h1 class="day">$datetime.date.fromordinal($cur_ep_airdate.toordinal).strftime("%A").decode($sickbeard.SYS_ENCODING).capitalize() <span style="font-size: 12px;">[today]</span></h1> + #set $today_header = True + #else: + <br /><h1 class="day">$datetime.date.fromordinal($cur_ep_airdate.toordinal).strftime("%A").decode($sickbeard.SYS_ENCODING).capitalize()</h1> + #end if + #end if + #set $cur_segment = $cur_ep_airdate + #end if + #if $cur_ep_airdate == $today.date() and not $today_header: + <br /><h1 class="day">$datetime.date.fromordinal($cur_ep_airdate.toordinal).strftime("%A").decode($sickbeard.SYS_ENCODING).capitalize() <span style="font-size: 12px;">[today]</span></h1> + #set $today_header = True + #end if + #if $cur_ep_enddate < $today: + #set $show_div = "ep_listing listing_overdue" + #elif $cur_ep_airdate >= $next_week.date(): + #set $show_div = "ep_listing listing_toofar" + #elif $cur_ep_enddate >= $today and $cur_ep_airdate < $next_week.date(): + #if $cur_ep_airdate == $today.date(): + #set $show_div = "ep_listing listing_current" + #else: + #set $show_div = "ep_listing listing_default" + #end if + #end if + #elif $sort == "show": + #set $cur_ep_airdate = $cur_result["localtime"].date() + #set $cur_ep_enddate = $cur_result["localtime"] + datetime.timedelta(minutes=$cur_result["runtime"]) + #if $cur_ep_enddate < $today: + #set $show_div = "ep_listing listing_overdue" + #elif $cur_ep_airdate >= $next_week.date(): + #set $show_div = "ep_listing listing_toofar" + #elif $cur_ep_enddate >= $today and $cur_ep_airdate < $next_week.date(): + #if $cur_ep_airdate == $today.date(): + #set $show_div = "ep_listing listing_current" + #else: + #set $show_div = "ep_listing listing_default" + #end if + #end if + #end if + + <div class="$show_div" id="listing_${cur_result["showid"]}"> + <div class="tvshowDiv"> + <table width="100%" border="0" cellpadding="0" cellspacing="0"> + <tr> + <th #if $layout == 'banner' then "class=\"nobg\"" else "rowspan=\"2\""# style="background-color: #efefef;" valign="top"> + <a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}"><img alt="" class="#if $layout == 'banner' then "bannerThumb" else "posterThumb"#" src="$sbRoot/showPoster/?show=${cur_result["showid"]}&which=#if $layout == 'poster_thumb' then "poster_thumb" else $layout#" /></a> + </th> +#if $layout == 'banner': + </tr> + <tr> +#end if + <td class="next_episode"> + <div class="clearfix"> + <h2 class="tvshowTitle"><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"] + #if int($cur_result["paused"]): + <span class="pause">[paused]</span> + #end if + </a></h2> + <span class="tvshowTitleIcons"> + #if $cur_result["imdb_id"]: + <a href="http://www.imdb.com/title/${cur_result["imdb_id"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.imdb.com/title/${cur_result["imdb_id"]}"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" /> + #end if + <a href="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://thetvdb.com/?tab=series&id=${cur_result["showid"]}"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a> + <span><a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&season=$cur_result["season"]&episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="epSearch forceUpdate"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a></span> + </span> + </div> + <span class="title">Next Episode:</span> <span><%=str(cur_result["season"])+"x"+"%02i" % int(cur_result["episode"]) %> - $cur_result["name"] ($datetime.date.fromordinal(int($cur_result["airdate"])))</span> + <div class="clearfix"> + <span class="title">Airs:</span> <span>$cur_result["localtime_string"].decode($sickbeard.SYS_ENCODING) on $cur_result["network"]</span> + </div> + <div class="clearfix"> + <span class="title">Quality:</span> + #if int($cur_result["quality"]) in $qualityPresets: + <span class="quality $qualityPresetStrings[int($cur_result["quality"])]">$qualityPresetStrings[int($cur_result["quality"])]</span> + #else: + <span class="quality Custom">Custom</span> + #end if + </div> + </td> + </tr> + <tr> + <td style="vertical-align: top;"> + <div> + #if $cur_result["description"] == '': + <span class="title">Plot:</span> + <img class="ep_summaryTrigger" src="$sbRoot/images/plus.png" height="16" width="16" alt="" title="Toggle Summary" /><div class="ep_summary">[There is no summary added for this episode]</div> + #else: + <span class="title">Plot:</span> + <img class="ep_summaryTrigger" src="$sbRoot/images/plus.png" height="16" width="16" alt="" title="Toggle Summary" /><div class="ep_summary">$cur_result["description"]</div> + #end if + </div> + </td> + </tr> + </table> + </div> + </div> + + <!-- end $cur_result["show_name"] //--> + #end for + + +<!-- end non list view //--> +#end if + +<script type="text/javascript" charset="utf-8"> +<!-- +window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes +//--> +</script> + +<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config.tmpl b/data/interfaces/default/config.tmpl index c88f3fe588f4842f951dda462bc2032c912bccd9..c03cebfb7c28d2a1b64b82251291f6245f8f4906 100644 --- a/data/interfaces/default/config.tmpl +++ b/data/interfaces/default/config.tmpl @@ -2,47 +2,34 @@ #from sickbeard import db #import os.path #set global $title="Configuration" +#set global $header="Configuration" #set global $sbPath=".." #set global $topmenu="config"# #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if <div id="summary" class="align-left"> - <table class="infoTable"> - <tr> - <td class="infoTableHeader">SB Version: </td> - <td> - ($sickbeard.version.SICKBEARD_VERSION) -- $sickbeard.versionCheckScheduler.action.install_type : - #if $sickbeard.versionCheckScheduler.action.install_type != 'win' and not $sickbeard.versionCheckScheduler.action.updater._cur_commit_hash: - $sickbeard.versionCheckScheduler.action.updater._find_installed_version() - #end if - #if $sickbeard.versionCheckScheduler.action.updater._cur_commit_hash: - $sickbeard.versionCheckScheduler.action.updater._cur_commit_hash - #end if - </td> - </tr> - <tr><td class="infoTableHeader">SB Config file: </td><td>$sickbeard.CONFIG_FILE</td></tr> - <tr><td class="infoTableHeader">SB Database file: </td><td>$db.dbFilename()</td></tr> - <tr><td class="infoTableHeader">SB Cache Dir: </td><td>$sickbeard.CACHE_DIR</td></tr> - <tr><td class="infoTableHeader">SB Arguments: </td><td>$sickbeard.MY_ARGS</td></tr> - <tr><td class="infoTableHeader">SB Web Root: </td><td>$sickbeard.WEB_ROOT</td></tr> - <tr><td class="infoTableHeader">Python Version: </td><td>$sys.version[:120]</td></tr> - <tr class="infoTableSeperator"><td class="infoTableHeader"><i class="icon16-sb"></i> Homepage </td><td><a href="http://www.sickbeard.com/">http://www.sickbeard.com/</a></td></tr> - <tr><td class="infoTableHeader"><i class="icon16-web"></i> Forums </td><td><a href="http://sickbeard.com/forums/">http://sickbeard.com/forums/</a></td></tr> - <tr><td class="infoTableHeader"><i class="icon16-github"></i> Source </td><td><a href="https://github.com/sarakha63/Sick-Beard/">https://github.com/sarakha63/Sick-Beard/</a></td></tr> - <tr><td class="infoTableHeader"><i class="icon16-win"></i> Bug Tracker &<br/> Windows Builds </td><td><a href="http://code.google.com/p/sickbeard/">http://code.google.com/p/sickbeard/</a></td></tr> - <tr><td class="infoTableHeader"><i class="icon16-mirc"></i> Internet Relay Chat </td><td><a href="irc://irc.freenode.net/#sickbeard"><i>#sickbeard</i> on <i>irc.freenode.net</i></a></td></tr> - </table> +<table class="infoTable" cellspacing="1" border="0" cellpadding="0"> + <tr><td class="infoTableHeader">SB Version: </td><td class="infoTableCell">($sickbeard.version.SICKBEARD_VERSION) <!-- – build.date //--> + </td></tr> + <tr><td class="infoTableHeader">SB Config file: </td><td class="infoTableCell">$sickbeard.CONFIG_FILE</td></tr> + <tr><td class="infoTableHeader">SB Database file: </td><td class="infoTableCell">$db.dbFilename()</td></tr> + <tr><td class="infoTableHeader">SB Cache Dir: </td><td class="infoTableCell">$sickbeard.CACHE_DIR</td></tr> + <tr><td class="infoTableHeader">SB Arguments: </td><td class="infoTableCell">$sickbeard.MY_ARGS</td></tr> + <tr><td class="infoTableHeader">SB Web Root: </td><td class="infoTableCell">$sickbeard.WEB_ROOT</td></tr> + <tr><td class="infoTableHeader">Python Version: </td><td class="infoTableCell">$sys.version[:120]</td></tr> + <tr class="infoTableSeperator"><td class="infoTableHeader"><i class="icon16-sb"></i> Homepage </td><td><a href="http://www.sickbeard.com/">http://www.sickbeard.com/</a></td></tr> + <tr><td class="infoTableHeader"><i class="icon16-web"></i> Forums </td><td><a href="http://sickbeard.com/forums/viewtopic.php?f=9&t=2386">http://sickbeard.com/forums/</a></td></tr> + <tr><td class="infoTableHeader"><i class="icon16-github"></i> Source </td><td><a href="https://github.com/sarakha63/Sick-Beard/">https://github.com/sarakha63/Sick-Beard/</a></td></tr> + <tr><td class="infoTableHeader"><i class="icon16-win"></i> Bug Tracker &<br/> Windows Builds </td><td><a href="http://code.google.com/p/sickbeard/">http://code.google.com/p/sickbeard/</a></td></tr> + <tr><td class="infoTableHeader"><i class="icon16-mirc"></i> Internet Relay Chat </td><td><a href="irc://irc.freenode.net/#sickbeard"><i>#sickbeard</i> on <i>irc.freenode.net</i></a></td></tr> +</table> </div> -<div class="container padding" style="width: 600px;"> - <table class="infoTable"> - <tr> - <td><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L2EWZMTRPW2SE" onclick="window.open(this.href); return false;"><img src="$sbRoot/images/paypal/btn_donateCC_LG.gif" alt="[donate]" /></a></td> - <td>Sick Beard VO/VF is free, but you can contribute by giving a <b><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L2EWZMTRPW2SE" onclick="window.open(this.href); return false;">donation</a></b>.</td> - </tr> - </table> -</div> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_general.tmpl b/data/interfaces/default/config_general.tmpl index 5ab25dbab583d714e74d4a0989acdfd62c3f8d45..a6b5e8bf8f56f48db5b2f2d703cf750ab7f8f3ef 100644 --- a/data/interfaces/default/config_general.tmpl +++ b/data/interfaces/default/config_general.tmpl @@ -11,21 +11,30 @@ #set global $topmenu="config"# #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if <script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> <div id="config"> <div id="config-content"> -<h5>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></h5> <form id="configForm" action="saveGeneral" method="post"> - <div id="config-components"> - + + <ul> + <li><a href="#core-component-group1">Misc</a></li> + <li><a href="#core-component-group2">Web Interface</a></li> + <li><a href="#core-component-group4">Api</a></li> + </ul> + <div id="core-component-group1" class="component-group clearfix"> - + <div class="component-group-desc"> <h3>Misc</h3> + <p>Startup options.</p> <p><b>Some options may require a manual restart to take effect.</b></p> </div> @@ -37,6 +46,14 @@ <span class="component-desc">Should Sick Beard open its home page when started?</span> </label> </div> + + <div class="field-pair"> + <input type="checkbox" name="update_shows_on_start" id="update_shows_on_start" #if $sickbeard.UPDATE_SHOWS_ON_START then "checked=\"checked\"" else ""#/> + <label class="clearfix" for="update_shows_on_start"> + <span class="component-title">Update Shows on Start</span> + <span class="component-desc">Should Sick Beard update shows info when started?</span> + </label> + </div> <div class="field-pair"> <input type="checkbox" name="version_notify" id="version_notify" #if $sickbeard.VERSION_NOTIFY then "checked=\"checked\"" else ""#/> @@ -51,10 +68,10 @@ </div> <div class="field-pair"> - <input type="checkbox" name="display_posters" id="display_posters" #if $sickbeard.DISPLAY_POSTERS then "checked=\"checked\"" else ""#/> - <label class="clearfix" for="display_posters"> - <span class="component-title">Display Posters</span> - <span class="component-desc">Display Posters in main view.</span> + <input type="checkbox" name="sort_article" id="sort_article" #if $sickbeard.SORT_ARTICLE then "checked=\"checked\"" else ""#/> + <label class="clearfix" for="sort_article"> + <span class="component-title">Sort articles</span> + <span class="component-desc">Include articles (The, A, An, Le, La ...) when sorting show lists.</span> </label> </div> @@ -186,7 +203,7 @@ <label class="nocheck clearfix" for="api_key"> <span class="component-title">API Key</span> <input type="text" name="api_key" id="api_key" value="$sickbeard.API_KEY" size="35" readonly="readonly" /> - <input type="button" class="btn" id="generate_new_apikey" value="Generate"> + <input class="btn" class="btn" type="button" id="generate_new_apikey" value="Generate"> </label> <label class="nocheck clearfix"> <span class="component-title"> </span> @@ -198,20 +215,22 @@ <input type="submit" class="btn config_submitter" value="Save Changes" /> </fieldset> </div><!-- /component-group4 //--> - - <div class="component-group-save"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><br /> - + <br/> + <small class="float-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b> </small> + <input type="submit" class="btn config_submitter button" value="Save Changes" /> + </div><!-- /config-components --> - + </form> </div></div> +<div class="clearfix"></div> + <script type="text/javascript" charset="utf-8"> <!-- jQuery('#log_dir').fileBrowser({ title: 'Select Log Directory' }); + jQuery('#config-components').tabs(); //--> </script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_notifications.tmpl b/data/interfaces/default/config_notifications.tmpl index 54882b1a3820e58556625e7018ce030ff1131b13..895c48d48d57c7ea80e6ab48891de173fb54a337 100755 --- a/data/interfaces/default/config_notifications.tmpl +++ b/data/interfaces/default/config_notifications.tmpl @@ -9,23 +9,27 @@ <script type="text/javascript" src="$sbRoot/js/configNotifications.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> - +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if <div id="config"> <div id="config-content"> <form id="configForm" action="saveNotifications" method="post"> - <div id="config-components" class="notifier"> - - - <br /> - <h2>Home Theater</h2> - <br /> - + <div id="config-components"> + <ul> + <li><a href="#tabs-1">Home Theater</a></li> + <li><a href="#tabs-2">Devices</a></li> + <li><a href="#tabs-3">Online</a></li> + </ul> + + <div id="tabs-1"> <div class="component-group clearfix"> + <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/xbmc.png" alt="" title="XBMC" /> - <h3><a href="http://xbmc.org/" onclick="window.open(this.href, '_blank'); return false;">XBMC</a></h3> + <h3><a href="http://xbmc.org/" onclick="window.open(this.href, '_blank'); return false;"> <img class="notifier-icon" src="$sbRoot/images/notifiers/xbmc.png" alt="" title="XBMC" /> XBMC </a></h3> <p>A free and open source cross-platform media center and home entertainment system software with a 10-foot user interface designed for the living-room TV.</p> - <p>For SickBeard to communicate with XBMC you must turn on the <a href="http://wiki.xbmc.org/index.php?title=Webserver" onclick="window.open(this.href, '_blank'); return false;">XBMC Webserver</a>.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> @@ -62,23 +66,23 @@ <input type="checkbox" name="xbmc_update_library" id="xbmc_update_library" #if $sickbeard.XBMC_UPDATE_LIBRARY then "checked=\"checked\"" else ""# /> <label class="clearfix" for="xbmc_update_library"> <span class="component-title">Update Library</span> - <span class="component-desc">Update library (show's path) when we finish a download?</span> + <span class="component-desc">Update XBMC library when we finish a download?</span> </label> </div> <div class="field-pair"> <input type="checkbox" name="xbmc_update_full" id="xbmc_update_full" #if $sickbeard.XBMC_UPDATE_FULL then "checked=\"checked\"" else ""# /> <label class="clearfix" for="xbmc_update_full"> <span class="component-title">Full Library Update</span> - <span class="component-desc">Fall back to a full library update if per-show fails?</span> + <span class="component-desc">Do a full library update if per-show fails?</span> </label> </div> <div class="field-pair"> <input type="checkbox" name="xbmc_update_onlyfirst" id="xbmc_update_onlyfirst" #if $sickbeard.XBMC_UPDATE_ONLYFIRST then "checked=\"checked\"" else ""# /> <label class="clearfix" for="xbmc_update_onlyfirst"> <span class="component-title">Only Update First Host</span> - <span class="component-desc">Only send library update to the first host?</span> + <span class="component-desc">Only send library update to the first host that is active?</span> </label> - </div> + </div> <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">XBMC IP:Port</span> @@ -90,8 +94,12 @@ </label> <label class="nocheck clearfix"> <span class="component-title"> </span> - <span class="component-desc">(multiple host strings must be separated by commas)</span> + <span class="component-desc">(multiple host strings must be separated by commas,</span> </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">the library updates will only be sent to the first host listed)</span> + </label> </div> <div class="field-pair"> <label class="nocheck clearfix"> @@ -114,18 +122,18 @@ </label> </div> <div class="testNotification" id="testXBMC-result">Click below to test.</div> - <input type="button" class="btn" value="Test XBMC" id="testXBMC" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test XBMC" id="testXBMC" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_xbmc //--> </fieldset> + </div><!-- /xbmc component-group //--> <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/plex.png" alt="" title="Plex Media Server" /> - <h3><a href="http://www.plexapp.com/" onclick="window.open(this.href, '_blank'); return false;">Plex Media Server</a></h3> + <h3><a href="http://www.plexapp.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/plex.png" alt="" title="Plex Media Server"/> Plex Media Server </a></h3> <p>Experience your media on a visually stunning, easy to use interface on your Mac connected to your TV. Your media library has never looked this good!</p> </div> <fieldset class="component-group-list"> @@ -185,10 +193,6 @@ <span class="component-title"> </span> <span class="component-desc">Host running Plex Client (eg. 192.168.1.100:3000)</span> </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">(multiple host strings must be separated by commas)</span> - </label> </div> <div class="field-pair"> <label class="nocheck clearfix"> @@ -211,8 +215,8 @@ </label> </div> <div class="testNotification" id="testPLEX-result">Click below to test.</div> - <input type="button" class="btn" value="Test Plex Media Clients" id="testPLEX" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Plex Media Server" id="testPLEX" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_plex --> </fieldset> @@ -221,8 +225,7 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/nmj.png" alt="" title="Networked Media Jukebox" /> - <h3><a href="http://www.popcornhour.com/" onclick="window.open(this.href, '_blank'); return false;">NMJ</a></h3> + <h3><a href="http://www.popcornhour.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/nmj.png" alt="" title="Networked Media Jukebox"/> NMJ </a></h3> <p>The Networked Media Jukebox, or NMJ, is the official media jukebox interface made available for the Popcorn Hour 200-series.</p> </div> <fieldset class="component-group-list"> @@ -248,7 +251,7 @@ <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">Get Settings</span> - <input type="button" class="btn" value="Get Settings" id="settingsNMJ" /> + <input class="btn" type="button" value="Get Settings" id="settingsNMJ" /> </label> <label class="nocheck clearfix"> <span class="component-title"> </span> @@ -276,18 +279,17 @@ </label> </div> <div class="testNotification" id="testNMJ-result">Click below to test.</div> - <input type="button" class="btn" value="Test NMJ" id="testNMJ" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test NMJ" id="testNMJ" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_nmj //--> </fieldset> </div><!-- /nmj component-group //--> - <div class="component-group clearfix"> + <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/nmj.png" alt="" title="Networked Media Jukebox V2" /> - <h3><a href="http://www.popcornhour.com/" onclick="window.open(this.href, '_blank'); return false;">NMJv2</a></h3> - <p>The Networked Media Jukebox, or NMJv2, is the official media jukebox interface made available for the Popcorn Hour 300 & 400-series.</p> + <h3><a href="http://www.popcornhour.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/nmj.png" alt="" title="Networked Media Jukebox v2"/> NMJv2 </a></h3> + <p>The Networked Media Jukebox, or NMJv2, is the official media jukebox interface made available for the Popcorn Hour 300 & 400-series.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> @@ -309,34 +311,35 @@ <span class="component-desc">IP of Popcorn 300/400-series (eg. 192.168.1.100)</span> </label> </div> - <div class="field-pair"> + <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">Database Location</span> - <span class="component-desc"> - <input type="radio" name="nmjv2_dbloc" value="local" class="radio" id="nmjv2_dbloc_a" #if $sickbeard.NMJv2_DBLOC=="local" then "checked=\"checked\"" else ""# />PCH Local Media<br /> - </span> + <span class="component-desc"> + <INPUT TYPE="radio" NAME="nmjv2_dbloc" VALUE="local" class="radio" id="NMJV2_DBLOC_A" #if $sickbeard.NMJv2_DBLOC=="local" then "checked=\"checked\"" else ""# />PCH Local Media<br /> + </span> </label> - <label class="nocheck clearfix"> + <label class="nocheck clearfix"> <span class="component-title"> </span> <span class="component-desc"> - <input type="radio" name="nmjv2_dbloc" value="network" class="radio" id="nmjv2_dbloc_b" #if $sickbeard.NMJv2_DBLOC=="network" then "checked=\"checked\"" else ""# />PCH Network Media<br /> - </span> + <INPUT TYPE="radio" NAME="nmjv2_dbloc" VALUE="network" class="radio" id="NMJV2_DBLOC_B" #if $sickbeard.NMJv2_DBLOC=="network" then "checked=\"checked\"" else ""#/>PCH Network Media + </span> </label> - <label class="nocheck clearfix"> + </label> + <label class="nocheck clearfix"> <span class="component-title">Database Instance</span> <span class="component-desc"> - <select id="NMJv2db_instance"> - <option value="0">#1 </option> - <option value="1">#2 </option> - <option value="2">#3 </option> - <option value="3">#4 </option> - <option value="4">#5 </option> - <option value="5">#6 </option> - <option value="6">#7 </option> - </select> - </span> - </label> - <label class="nocheck clearfix"> + <select id="NMJv2db_instance"> + <option value="0">#1 </option> + <option value="1">#2 </option> + <option value="2">#3 </option> + <option value="3">#4 </option> + <option value="4">#5 </option> + <option value="5">#6 </option> + <option value="6">#7 </option> + </select> + </span> + </label> + <label class="nocheck clearfix"> <span class="component-title"> </span> <span class="component-desc">Adjust this value if the wrong database is selected.</span> </label> @@ -344,7 +347,7 @@ <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">Find Database</span> - <input type="button" class="btn" value="Find Database" id="settingsNMJv2" /> + <input type="button" value="Find Database" id="settingsNMJv2" /> </label> <label class="nocheck clearfix"> <span class="component-title"> </span> @@ -360,25 +363,24 @@ <span class="component-desc">Automatically filled via the 'Find Database' buttons.</span> </label> </div> - <div class="testNotification" id="testNMJv2-result">Click below to test.</div> - <input type="button" class="btn" value="Test NMJv2" id="testNMJv2" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <div class="testNotification" id="testNMJv2-result">Click below to test.</div> + <input type="button" value="Test NMJv2" id="testNMJv2" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_nmjv2 //--> </fieldset> </div><!-- /nmjv2 component-group //--> - + <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/synoindex.png" alt="" title="Synology Indexer" /> - <h3><a href="http://synology.com/" onclick="window.open(this.href, '_blank'); return false;">Synology Indexer</a></h3> + <h3><a href="http://synology.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/synoindex.png" alt="" title="Synology Indexer"/> Synology Indexer </a></h3> <p>Synology Indexer is the daemon running on the Synology NAS to build its media database.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_synoindex" id="use_synoindex" #if $sickbeard.USE_SYNOINDEX then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_synoindex" id="use_synoindex" #if $sickbeard.USE_SYNOINDEX then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_synoindex"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send notifications to the synoindex daemon?<br /><br /> @@ -391,25 +393,68 @@ </div> <div id="content_use_synoindex"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><!-- /content_use_pytivo //--> + <input type="submit" class="config_submitter btn" value="Save Changes" /> + </div><!-- /content_use_synoindex //--> </fieldset> </div><!-- /synoindex component-group //--> + + + <div class="component-group clearfix"> + <div class="component-group-desc"> + <h3><a href="http://synology.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/synologynotifier.png" alt="" title="Synology Indexer"/> Synology Notifier </a></h3> + <p>Synology Notifier is the notification system of Synology DSM</p> + </div> - - + <fieldset class="component-group-list"> + <div class="field-pair"> + <input type="checkbox" class="enabler" name="use_synologynotifier" id="use_synologynotifier" #if $sickbeard.USE_SYNOLOGYNOTIFIER then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="use_synologynotifier"> + <span class="component-title">Enable</span> + <span class="component-desc">Should Sick Beard send notifications to the Synology Notifier?<br /><br /> + </span> + </label> + <label class="nocheck clearfix" for="use_synologynotifier"> + <span class="component-title"> </span> + <span class="component-desc"><b>Note:</b> Requires SB to be running on your Synology DSM.</span> + </label> + </div> + <div id="content_use_synologynotifier"> + <div class="field-pair"> + <input type="checkbox" name="synologynotifier_notify_onsnatch" id="synologynotifier_notify_onsnatch" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="synologynotifier_notify_onsnatch"> + <span class="component-title">Notify on Snatch</span> + <span class="component-desc">Send notification when we start a download?</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" name="synologynotifier_notify_ondownload" id="synologynotifier_notify_ondownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="synologynotifier_notify_ondownload"> + <span class="component-title">Notify on Download</span> + <span class="component-desc">Send notification when we finish a download?</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" name="synologynotifier_notify_onsubtitledownload" id="synologynotifier_notify_onsubtitledownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="synologynotifier_notify_onsubtitledownload"> + <span class="component-title">Notify on Subtitle Download</span> + <span class="component-desc">Send notification when we finish a subtitle download?</span> + </label> + </div> + <input type="submit" class="config_submitter btn" value="Save Changes" /> + </div> + </fieldset> + </div><!-- /synology notifier component-group //--> <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/pytivo.png" alt="" title="pyTivo" /> - <h3><a href="http://pytivo.sourceforge.net/wiki/index.php/PyTivo" onclick="window.open(this.href, '_blank'); return false;">pyTivo</a></h3> + <h3><a href="http://pytivo.sourceforge.net/wiki/index.php/PyTivo" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/pytivo.png" alt="" title="pyTivo"/> pyTivo </a></h3> <p>pyTivo is both an HMO and GoBack server. This notifier will load the completed downloads to your Tivo.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_pytivo" id="use_pytivo" #if $sickbeard.USE_PYTIVO then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_pytivo" id="use_pytivo" #if $sickbeard.USE_PYTIVO then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_pytivo"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send notifications to pyTivo?<br /><br /></span> @@ -448,35 +493,27 @@ </label> <label class="nocheck clearfix"> <span class="component-title"> </span> - <span class="component-desc">Messages & Settings > Account & System Information > System Information > DVR name</span> + <span class="component-desc">Messages & Settings > Account & System Information > System Information > DVR name</span> </label> </div> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_pytivo //--> </fieldset> </div><!-- /component-group //--> - <div class="component-group-save"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><br /> - - <br /> - <h2>Devices</h2> - <br /> + </div> - - - - <div class="component-group clearfix"> + + <div id="tabs-2"> + <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/growl.png" alt="" title="Growl" /> - <h3><a href="http://growl.info/" onclick="window.open(this.href, '_blank'); return false;">Growl</a></h3> + <h3><a href="http://growl.info/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/growl.png" alt="" title="Growl"/> Growl </a></h3> <p>A cross-platform unobtrusive global notification system.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_growl" id="use_growl" #if $sickbeard.USE_GROWL then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_growl" id="use_growl" #if $sickbeard.USE_GROWL then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_growl"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send Growl notifications?</span> @@ -485,7 +522,7 @@ <div id="content_use_growl"> <div class="field-pair"> - <input type="checkbox" name="growl_notify_onsnatch" id="growl_notify_onsnatch" #if $sickbeard.GROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <input type="checkbox" name="growl_notify_onsnatch" id="growl_notify_onsnatch" #if $sickbeard.GROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="growl_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> <span class="component-desc">Send notification when we start a download?</span> @@ -530,8 +567,8 @@ </label> </div> <div class="testNotification" id="testGrowl-result">Click below to register and test Growl, this is required for Growl notifications to work.</div> - <input type="button" class="btn" value="Register Growl" id="testGrowl" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Register Growl" id="testGrowl" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_growl //--> </fieldset> @@ -540,13 +577,12 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/prowl.png" alt="Prowl" title="Prowl" /> - <h3><a href="http://www.prowlapp.com/" onclick="window.open(this.href, '_blank'); return false;">Prowl</a></h3> + <h3><a href="http://www.prowlapp.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/prowl.png" alt="Prowl" title="Prowl"/> Prowl</a></h3> <p>A Growl client for iOS.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_prowl" id="use_prowl" #if $sickbeard.USE_PROWL then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_prowl" id="use_prowl" #if $sickbeard.USE_PROWL then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_prowl"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send Prowl notifications?</span> @@ -555,7 +591,7 @@ <div id="content_use_prowl"> <div class="field-pair"> - <input type="checkbox" name="prowl_notify_onsnatch" id="prowl_notify_onsnatch" #if $sickbeard.PROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <input type="checkbox" name="prowl_notify_onsnatch" id="prowl_notify_onsnatch" #if $sickbeard.PROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="prowl_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> <span class="component-desc">Send notification when we start a download?</span> @@ -582,12 +618,12 @@ </label> <label class="nocheck clearfix"> <span class="component-title"> </span> - <span class="component-desc">Get your key at: <a href="https://www.prowlapp.com/api_settings.php" onclick="window.open(this.href, '_blank'); return false;">https://prowlapp.com/api_settings.php</a></span> + <span class="component-desc">Get your key at: <a href="https://www.prowlapp.com/api_settings.php" onclick="window.open(this.href, '_blank'); return false;">https://www.prowlapp.com/api_settings.php</a></span> </label> </div> <div class="field-pair"> <label class="nocheck clearfix"> - <span class="component-title">Prowl Priority:</span> + <span class="component-title">Prowl priority:</span> <select id="prowl_priority" name="prowl_priority"> <option value="-2" #if $sickbeard.PROWL_PRIORITY == "-2" then 'selected="selected"' else ""#>Very Low</option> <option value="-1" #if $sickbeard.PROWL_PRIORITY == "-1" then 'selected="selected"' else ""#>Moderate</option> @@ -602,8 +638,8 @@ </label> </div> <div class="testNotification" id="testProwl-result">Click below to test.</div> - <input type="button" class="btn" value="Test Prowl" id="testProwl" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Prowl" id="testProwl" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_prowl //--> </fieldset> @@ -612,8 +648,7 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/notifo.png" alt="" title="Notifo" /> - <h3><a href="http://notifo.com/" onclick="window.open(this.href, '_blank'); return false;">Notifo</a></h3> + <h3><a href="http://notifo.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/notifo.png" alt="" title="Notifo"/> Notifo </a></h3> <p>A platform for push-notifications to either mobile or desktop clients</p> </div> <fieldset class="component-group-list"> @@ -664,12 +699,12 @@ </label> <label class="nocheck clearfix"> <span class="component-title"> </span> - <span class="component-desc">Get your key at: <a href="http://notifo.com/user/settings" onclick="window.open(this.href, '_blank'); return false;">http://notifo.com/user/settings</a></span> + <span class="component-desc">Get your API key at: <a href="http://notifo.com/user/settings" onclick="window.open(this.href, '_blank'); return false;">http://notifo.com/user/settings</a></span> </label> </div> <div class="testNotification" id="testNotifo-result">Click below to test.</div> - <input type="button" class="btn" value="Test Notifo" id="testNotifo" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Notifo" id="testNotifo" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_notifo //--> </fieldset> @@ -678,13 +713,12 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/libnotify.png" alt="" title="Libnotify" /> - <h3><a href="http://library.gnome.org/devel/libnotify/" onclick="window.open(this.href, '_blank'); return false;">Libnotify</a></h3> + <h3><a href="http://library.gnome.org/devel/libnotify/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/libnotify.png" alt="" title="Libnotify"/> Libnotify </a></h3> <p>The standard desktop notification API for Linux/*nix systems. This notifier will only function if the pynotify module is installed (Ubuntu/Debian package <a href="apt:python-notify">python-notify</a>).</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_libnotify" id="use_libnotify" #if $sickbeard.USE_LIBNOTIFY then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_libnotify" id="use_libnotify" #if $sickbeard.USE_LIBNOTIFY then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_libnotify"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send Libnotify notifications?</span> @@ -693,7 +727,7 @@ <div id="content_use_libnotify"> <div class="field-pair"> - <input type="checkbox" name="libnotify_notify_onsnatch" id="libnotify_notify_onsnatch" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <input type="checkbox" name="libnotify_notify_onsnatch" id="libnotify_notify_onsnatch" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="libnotify_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> <span class="component-desc">Send notification when we start a download?</span> @@ -714,8 +748,8 @@ </label> </div> <div class="testNotification" id="testLibnotify-result">Click below to test.</div> - <input type="button" class="btn" value="Test Libnotify" id="testLibnotify" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Libnotify" id="testLibnotify" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_libnotify //--> </fieldset> @@ -724,8 +758,7 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/pushover.png" alt="" title="Pushover" /> - <h3><a href="http://pushover.net/" onclick="window.open(this.href, '_blank'); return false;">Pushover</a></h3> + <h3><a href="http://pushover.net/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/pushover.png" alt="" title="Pushover"/> Pushover </a></h3> <p>Pushover makes it easy to send real-time notifications to your Android and iOS devices.</p> </div> <fieldset class="component-group-list"> @@ -761,7 +794,7 @@ </div> <div class="field-pair"> <label class="nocheck clearfix"> - <span class="component-title">Pushover User Key</span> + <span class="component-title">Pushover Key</span> <input type="text" name="pushover_userkey" id="pushover_userkey" value="$sickbeard.PUSHOVER_USERKEY" size="35" /> </label> <label class="nocheck clearfix"> @@ -770,8 +803,8 @@ </label> </div> <div class="testNotification" id="testPushover-result">Click below to test.</div> - <input type="button" class="btn" value="Test Pushover" id="testPushover" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Pushover" id="testPushover" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_pushover //--> </fieldset> @@ -779,8 +812,7 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/boxcar.png" alt="" title="Boxcar" /> - <h3><a href="http://boxcar.io/" onclick="window.open(this.href, '_blank'); return false;">Boxcar</a></h3> + <h3><a href="http://boxcar.io/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/boxcar.png" alt="" title="Boxcar"/> Boxcar </a></h3> <p>Read your messages where and when you want them! A subscription will be send if needed.</p> </div> <fieldset class="component-group-list"> @@ -825,8 +857,8 @@ </label> </div> <div class="testNotification" id="testBoxcar-result">Click below to test.</div> - <input type="button" class="btn" value="Test Boxcar" id="testBoxcar" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Boxcar" id="testBoxcar" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_boxcar //--> </fieldset> @@ -835,13 +867,12 @@ <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/nma.png" alt="" title="NMA" /> - <h3><a href="http://nma.usk.bz" onclick="window.open(this.href, '_blank'); return false;">Notify My Android</a></h3> + <h3><a href="http://nma.usk.bz" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/nma.png" alt="" title="NMA"/> Notify My Android </a></h3> <p>Notify My Android is a Prowl-like Android App and API that offers an easy way to send notifications from your application directly to your Android device.</p> </div> <fieldset class="component-group-list"> <div class="field-pair"> - <input type="checkbox" class="enabler" name="use_nma" id="use_nma" #if $sickbeard.USE_NMA then "checked=\"checked\"" else ""# /> + <input type="checkbox" class="enabler" name="use_nma" id="use_nma" #if $sickbeard.USE_NMA then "checked=\"checked\"" else ""# /> <label class="clearfix" for="use_nma"> <span class="component-title">Enable</span> <span class="component-desc">Should Sick Beard send NMA notifications?</span> @@ -850,7 +881,7 @@ <div id="content_use_nma"> <div class="field-pair"> - <input type="checkbox" name="nma_notify_onsnatch" id="nma_notify_onsnatch" #if $sickbeard.NMA_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <input type="checkbox" name="nma_notify_onsnatch" id="nma_notify_onsnatch" #if $sickbeard.NMA_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="nma_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> <span class="component-desc">Send notification when we start a download?</span> @@ -873,11 +904,11 @@ <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">NMA API key:</span> - <input type="text" name="nma_api" id="nma_api" value="$sickbeard.NMA_API" size="35" /> + <input type="text" name="nma_api" id="nma_api" value="$sickbeard.NMA_API" size="55" /> </label> <label class="nocheck clearfix"> <span class="component-title"> </span> - <span class="component-desc">Multiple keys (maxium of 5) must be seperated by commas</span> + <span class="component-desc">Multiple keys must be seperated by a , (comma). Up to a max of 5</span> </label> </div> <div class="field-pair"> @@ -904,24 +935,66 @@ </fieldset> </div><!-- /nma component-group //--> - <div class="component-group-save"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><br /> - - <br /> - <h2>Online</h2> - <br /> - - - - - - - <div class="component-group clearfix"> <div class="component-group-desc"> - <img class="notifier-icon" src="$sbRoot/images/notifiers/twitter.png" alt="" title="Twitter" /> - <h3><a href="http://www.twitter.com/" onclick="window.open(this.href, '_blank'); return false;">Twitter</a></h3> + <h3><a href="https://pushalot.com" onclick="window.open(this.href, '_blank'); return false;"><img class="notifier-icon" src="$sbRoot/images/notifiers/pushalot.png" alt="" title="Pushalot" /> Pushalot </a></h3> + <p>Pushalot is a platform for receiving custom push notifications to connected devices running Windows Phone or Windows 8.</p> + </div> + <fieldset class="component-group-list"> + <div class="field-pair"> + <input type="checkbox" class="enabler" name="use_pushalot" id="use_pushalot" #if $sickbeard.USE_PUSHALOT then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="use_pushalot"> + <span class="component-title">Enable</span> + <span class="component-desc">Should Sick Beard send notifications through Pushalot?</span> + </label> + </div> + + <div id="content_use_pushalot"> + <div class="field-pair"> + <input type="checkbox" name="pushalot_notify_onsnatch" id="pushalot_notify_onsnatch" #if $sickbeard.PUSHALOT_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="pushalot_notify_onsnatch"> + <span class="component-title">Notify on Snatch</span> + <span class="component-desc">Send notification when we start a download?</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" name="pushalot_notify_ondownload" id="pushalot_notify_ondownload" #if $sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="pushalot_notify_ondownload"> + <span class="component-title">Notify on Download</span> + <span class="component-desc">Send notification when we finish a download?</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" name="pushalot_notify_onsubtitledownload" id="pushalot_notify_onsubtitledownload" #if $sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="pushalot_notify_onsubtitledownload"> + <span class="component-title">Notify on Subtitle Download</span> + <span class="component-desc">Send notification when we finish a subtitle download?</span> + </label> + </div> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Pushalot Authorization Token</span> + <input type="text" name="pushalot_authorizationtoken" id="pushalot_authorizationtoken" value="$sickbeard.PUSHALOT_AUTHORIZATIONTOKEN" size="35" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Authorization Token of your Pushalot account</span> + </label> + </div> + <div class="testNotification" id="testPushalot-result">Click below to test.</div> + <input type="button" class="btn" value="Test Pushalot" id="testPushalot" /> + <input type="submit" class="btn config_submitter" value="Save Changes" /> + </div><!-- /content_use_pushalot //--> + + </fieldset> + </div><!-- /pushalot component-group //--> + + </div> + + <div id="tabs-3"> + <div class="component-group clearfix"> + <div class="component-group-desc"> + <h3><a href="http://www.twitter.com/" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/notifiers/twitter.png" alt="" title="Twitter"/> Twitter </a></h3> <p>A social networking and microblogging service, enabling its users to send and read other users' messages called tweets.</p> </div> <fieldset class="component-group-list"> @@ -939,7 +1012,7 @@ <div id="content_use_twitter"> <div class="field-pair"> - <input type="checkbox" name="twitter_notify_onsnatch" id="twitter_notify_onsnatch" #if $sickbeard.TWITTER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> + <input type="checkbox" name="twitter_notify_onsnatch" id="twitter_notify_onsnatch" #if $sickbeard.TWITTER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="twitter_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> <span class="component-desc">Send notification when we start a download?</span> @@ -964,8 +1037,8 @@ <span class="component-title">Step One</span> </label> <label class="nocheck clearfix"> - <span class="component-desc">Click the "Request Authorization" button.<br/> This will open a new page containing an auth key.<br/> Note: if nothing happens check your popup blocker.<br/></span> - <input type="button" class="btn" value="Request Authorization" id="twitterStep1" /> + <span style="font-size: 11px;">Click the "Request Authorization" button.<br/> This will open a new page containing an auth key.<br/> <b>Note:</b> if nothing happens check your popup blocker.<br/></span> + <input class="btn" type="button" value="Request Authorization" id="twitterStep1" /> </label> </div> <div class="field-pair"> @@ -973,9 +1046,11 @@ <span class="component-title">Step Two</span> </label> <label class="nocheck clearfix"> - <span class="component-desc">Enter the key Twitter gave you below, and click "Verify Key".<br/></span> - <input type="text" id="twitter_key" value="" size="35" /> - <input type="button" class="btn" value="Verify Key" id="twitterStep2" /> + <span style="font-size: 11px;">Enter the key Twitter gave you below, and click "Verify Key".<br/></span> + <input type="text" id="twitter_key" value="" size="35" /><br /> + </label> + <label class="nocheck clearfix"> + <input class="btn" type="button" value="Verify Key" id="twitterStep2" /> </label> </div> <div class="field-pair"> @@ -984,8 +1059,8 @@ </label> </div> <div class="testNotification" id="testTwitter-result">Click below to test.</div> - <input type="button" class="btn" value="Test Twitter" id="testTwitter" /> - <input type="submit" class="btn config_submitter" value="Save Changes" /> + <input class="btn" type="button" value="Test Twitter" id="testTwitter" /> + <input type="submit" class="config_submitter btn" value="Save Changes" /> </div><!-- /content_use_twitter //--> </fieldset> @@ -1047,29 +1122,29 @@ </label> </div> <div id="content_trakt_use_watchlist"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Watchlist Add Method:</span> - <select id="trakt_method_add" name="trakt_method_add"> - <option value="0" #if $sickbeard.TRAKT_METHOD_ADD == "0" then 'selected="selected"' else ""#>Skip All</option> - <option value="1" #if $sickbeard.TRAKT_METHOD_ADD == "1" then 'selected="selected"' else ""#>Download Pilot Only</option> - <option value="2" #if $sickbeard.TRAKT_METHOD_ADD == "2" then 'selected="selected"' else ""#>Get whole show</option> - <option value="3" #if $sickbeard.TRAKT_METHOD_ADD == "3" then 'selected="selected"' else ""#>Only get 3 first episodes</option> - </select> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Method to decide which episodes to download in new show</span> - </label> - </div> - <div class="field-pair"> - <input type="checkbox" name="trakt_remove_watchlist" id="trakt_remove_watchlist" #if $sickbeard.TRAKT_REMOVE_WATCHLIST then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="trakt_remove_watchlist"> - <span class="component-title">Remove from Watchlist:</span> - <span class="component-desc">Remove an episode from the watchlist after it is downloaded</span> - </label> - </div> - + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Watchlist Add Method:</span> + <select id="trakt_method_add" name="trakt_method_add"> + <option value="0" #if $sickbeard.TRAKT_METHOD_ADD == "0" then 'selected="selected"' else ""#>Skip All</option> + <option value="1" #if $sickbeard.TRAKT_METHOD_ADD == "1" then 'selected="selected"' else ""#>Download Pilot Only</option> + <option value="2" #if $sickbeard.TRAKT_METHOD_ADD == "2" then 'selected="selected"' else ""#>Get whole show</option> + <option value="3" #if $sickbeard.TRAKT_METHOD_ADD == "3" then 'selected="selected"' else ""#>Only get 3 first episodes</option> + </select> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Method to decide which episodes to download in new show</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" name="trakt_remove_watchlist" id="trakt_remove_watchlist" #if $sickbeard.TRAKT_REMOVE_WATCHLIST then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="trakt_remove_watchlist"> + <span class="component-title">Remove from Watchlist:</span> + <span class="component-desc">Remove an episode from the watchlist after it is downloaded</span> + </label> + </div> + </div> <div class="testNotification" id="testTrakt-result">Click below to test.</div> <input type="button" class="btn" value="Test Trakt" id="testTrakt" /> @@ -1079,8 +1154,7 @@ </fieldset> </div><!-- /trakt component-group //--> - - <div class="component-group clearfix"> + <div class="component-group clearfix"> <div class="component-group-desc"> <img class="notifier-icon" src="$sbRoot/images/notifiers/mail.png" alt="" title="Mail" /> <h3>Mail</h3> @@ -1096,7 +1170,7 @@ </div> <div id="content_use_mail"> - <div class="field-pair"> + <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">Send E-Mail From</span> <input type="text" name="mail_from" id="mail_from" value="$sickbeard.MAIL_FROM" size="35" /> @@ -1106,7 +1180,7 @@ <span class="component-desc"> </span> </label> </div> - <div class="field-pair"> + <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">Send E-Mail To</span> <input type="text" name="mail_to" id="mail_to" value="$sickbeard.MAIL_TO" size="35" /> @@ -1116,7 +1190,7 @@ <span class="component-desc"> </span> </label> </div> - <div class="field-pair"> + <div class="field-pair"> <label class="nocheck clearfix"> <span class="component-title">SMTP Server</span> <input type="text" name="mail_server" id="mail_server" value="$sickbeard.MAIL_SERVER" size="35" /> @@ -1126,7 +1200,7 @@ <span class="component-desc"> </span> </label> </div> - <div class="field-pair"> + <div class="field-pair"> <input type="checkbox" name="mail_ssl" id="mail_ssl" #if $sickbeard.MAIL_SSL then "checked=\"checked\"" else ""# /> <label class="clearfix" for="mail_ssl"> <span class="component-title">Enable SSL</span> @@ -1152,8 +1226,8 @@ <span class="component-title"> </span> <span class="component-desc"> </span> </label> - </div> - <div class="field-pair"> + </div> + <div class="field-pair"> <input type="checkbox" name="mail_notify_onsnatch" id="mail_notify_onsnatch" #if $sickbeard.MAIL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# /> <label class="clearfix" for="mail_notify_onsnatch"> <span class="component-title">Notify on Snatch</span> @@ -1168,17 +1242,19 @@ </fieldset> </div><!-- /mail component-group //--> - - - <div class="component-group-save"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><br /> - - </div><!-- /config-components //--> </form> + + <br/><input type="submit" class="config_submitter btn" value="Save Changes" /><br/> + </div> + </div> -<div class="clearfix"></div> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +<div class="clearfix"></div> +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#config-components').tabs(); +//--> +</script> +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_postProcessing.tmpl b/data/interfaces/default/config_postProcessing.tmpl index 787c8b49592054b857593f448bab757cdfccc1e6..603e71faa03251a8bee11366a226176fbe7cc3c1 100644 --- a/data/interfaces/default/config_postProcessing.tmpl +++ b/data/interfaces/default/config_postProcessing.tmpl @@ -16,20 +16,28 @@ <script type="text/javascript" src="$sbRoot/js/configPostProcessing.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> - +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if <div id="config"> <div id="config-content"> -<h5>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></h5> -<form id="configForms" action="savePostProcessing" method="post"> +<form id="configForm" action="savePostProcessing" method="post"> <div id="config-components"> - - <div class="component-group clearfix"> + <ul> + <li><a href="#core-component-group3">Post-Processing</a></li> + <li><a href="#core-component-group4">Episode Naming</a></li> + <li><a href="#core-component-group2">Metadata</a></li> + </ul> + + <div id="core-component-group3" class="component-group clearfix"> <div class="component-group-desc"> - <h3>NZB Post-Processing</h3> - <p>Settings that dictate how Sick Beard should process completed NZB downloads.</p> + <h3>Post-Processing</h3> + <p>Settings that dictate how Sick Beard should process completed downloads.</p> </div> <fieldset class="component-group-list"> @@ -52,6 +60,35 @@ </label> </div> + + <div class="field-pair"> + <input type="checkbox" name="keep_processed_dir" id="keep_processed_dir" #if $sickbeard.KEEP_PROCESSED_DIR == True then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="keep_processed_dir"> + <span class="component-title">Keep Original Files</span> + <span class="component-desc">Keep original files after they've been processed?</span> + </label> + </div> + + <div class="field-pair"> + <input type="checkbox" name="move_associated_files" id="move_associated_files" #if $sickbeard.MOVE_ASSOCIATED_FILES == True then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="move_associated_files"> + <span class="component-title">Move Associated Files</span> + <span class="component-desc">Move srr/srt/sfv/etc files with the episode when processed?</span> + </label> + <label class="nocheck clearfix" for="move_associated_files"> + <span class="component-title"> </span> + <span class="component-desc"><b>NOTE:</b> <i>.nfo</i> will be renamed to <i>.nfo-orig</i> when moved.</span> + </label> + </div> + + <div class="field-pair"> + <input type="checkbox" name="rename_episodes" id="rename_episodes" #if $sickbeard.RENAME_EPISODES == True then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="rename_episodes"> + <span class="component-title">Rename Episodes</span> + <span class="component-desc">Rename episode using the naming settings below?</span> + </label> + </div> + <div class="field-pair"> <input type="checkbox" name="process_automatically" id="process_automatically" #if $sickbeard.PROCESS_AUTOMATICALLY == True then "checked=\"checked\"" else ""# /> <label class="clearfix" for="process_automatically"> @@ -61,7 +98,6 @@ <label class="nocheck clearfix" for="process_automatically"> <span class="component-title"> </span> <span class="component-desc"><b>NOTE:</b> Do not use if you use sabToSickbeard w/ SABnzbd+!</span> - <span class="component-desc"><b>You must restart Sickbeard once everything is configured to start the thread</b></span> </label> </div> @@ -69,10 +105,7 @@ <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> </fieldset> - </div><!-- /component-pp //--> - - <div class="component-group clearfix"> - + <div class="component-group-desc"> <h3>Torrent Post-Processing</h3> <p>Settings that dictate how Sick Beard should process completed torrent downloads.</p> @@ -109,51 +142,80 @@ </div><!-- /component-pp //--> <div class="component-group clearfix"> + </div><!-- /component-group3 //--> + + <div id="core-component-group2" class="component-group clearfix"> <div class="component-group-desc"> - <h3>Common Post-Processing options</h3> - <p>Settings that dictate how Sick Beard should process completed downloads.</p> + <h3>Metadata</h3> + <p>The data associated to the data. These are files associated to a TV show in the form of images and text that, when supported, will enhance the viewing experience.</p> </div> <fieldset class="component-group-list"> - <div class="field-pair"> - <input type="checkbox" name="keep_processed_dir" id="keep_processed_dir" #if $sickbeard.KEEP_PROCESSED_DIR == True then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="keep_processed_dir"> - <span class="component-title">Keep Original Files</span> - <span class="component-desc">Keep original files after they've been processed?</span> + <label class="clearfix"> + <span class="component-title jumbo">Metadata Type:</span> + <span class="component-desc"> + #set $m_dict = $metadata.get_metadata_generator_dict() + <select id="metadataType"> + #for ($cur_name, $cur_generator) in $m_dict.items(): + <option value="$GenericMetadata.makeID($cur_name)">$cur_name</option> + #end for + </select> + </span> </label> + <span>Toggle the metadata options that you wish to be created. <b>Multiple targets may be used.</b></span> </div> - <div class="field-pair"> - <input type="checkbox" name="move_associated_files" id="move_associated_files" #if $sickbeard.MOVE_ASSOCIATED_FILES == True then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="move_associated_files"> - <span class="component-title">Move Associated Files</span> - <span class="component-desc">Move srr/srt/sfv/etc files with the episode when processed?</span> - </label> - <label class="nocheck clearfix" for="move_associated_files"> - <span class="component-title"> </span> - <span class="component-desc"><b>NOTE:</b> <i>.nfo</i> will be renamed to <i>.nfo-orig</i> when moved.</span> - </label> + <div id="metadataLegend"> + <div style="width: 190px; float: left;">Create:</div> + <div style="width: 260px; float: left;">Results:</div> </div> + <div class="clearfix"></div> - <div class="field-pair"> - <input type="checkbox" name="rename_episodes" id="rename_episodes" #if $sickbeard.RENAME_EPISODES == True then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="rename_episodes"> - <span class="component-title">Rename Episodes</span> - <span class="component-desc">Rename episode using the naming settings below?</span> +#for ($cur_name, $cur_generator) in $m_dict.items(): +#set $cur_metadata_inst = $sickbeard.metadata_provider_dict[$cur_generator.name] +#set $cur_id = $GenericMetadata.makeID($cur_name) +<div class="metadataDiv" id="$cur_id"> + <div class="metadata_options"> + <label for="${cur_id}_show_metadata"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_show_metadata" #if $cur_metadata_inst.show_metadata then "checked=\"checked\"" else ""#/> Show Metadata</label> + <label for="${cur_id}_episode_metadata"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_episode_metadata" #if $cur_metadata_inst.episode_metadata then "checked=\"checked\"" else ""#/> Episode Metadata</label> + <label for="${cur_id}_fanart"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_fanart" #if $cur_metadata_inst.fanart then "checked=\"checked\"" else ""#/> Show Fanart Image</label> + <label for="${cur_id}_poster"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_poster" #if $cur_metadata_inst.poster then "checked=\"checked\"" else ""#/> Show Folder Image</label> + <label for="${cur_id}_episode_thumbnails"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_episode_thumbnails" #if $cur_metadata_inst.episode_thumbnails then "checked=\"checked\"" else ""#/> Episode Thumbnail</label> + <label for="${cur_id}_season_thumbnails"><input type="checkbox" class="metadata_checkbox" id="${cur_id}_season_thumbnails" #if $cur_metadata_inst.season_thumbnails then "checked=\"checked\"" else ""#/> Season Thumbnail</label> + </div> + <div class="metadata_example"> + <label for="${cur_id}_show_metadata"><span id="${cur_id}_eg_show_metadata">$cur_metadata_inst.eg_show_metadata</span></label> + <label for="${cur_id}_episode_metadata"><span id="${cur_id}_eg_episode_metadata">$cur_metadata_inst.eg_episode_metadata</span></label> + <label for="${cur_id}_fanart"><span id="${cur_id}_eg_fanart">$cur_metadata_inst.eg_fanart</span></label> + <label for="${cur_id}_poster"><span id="${cur_id}_eg_poster">$cur_metadata_inst.eg_poster</span></label> + <label for="${cur_id}_episode_thumbnails"><span id="${cur_id}_eg_episode_thumbnails">$cur_metadata_inst.eg_episode_thumbnails</span></label> + <label for="${cur_id}_season_thumbnails"><span id="${cur_id}_eg_season_thumbnails">$cur_metadata_inst.eg_season_thumbnails</span></label> + </div> + <input type="hidden" name="${cur_id}_data" id="${cur_id}_data" value="$cur_metadata_inst.get_config()" /> +</div> +#end for + + <div class="clearfix" style="clear:left;"></div> + + <div class="field-pair clearfix"> + <input type="checkbox" name="use_banner" id="use_banner" #if $sickbeard.USE_BANNER then "checked=checked" else ""#/> + <label class="clearfix" for="use_banner"> + <span class="component-title">Use Banners</span> + <span class="component-desc">Use banners instead of posters for your Show Folder Images</span> </label> </div> - <div class="clearfix"></div> <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> </fieldset> - </div><!-- /component-pp //--> - - <div class="component-group clearfix"> + </div><!-- /component-group2 //--> + + <div id="core-component-group4" class="component-group clearfix"> + <div class="component-group-desc"> - <h3>Naming</h3> + <h3>Episode Naming</h3> <p>How Sick Beard will name and sort your episodes.</p> </div> @@ -185,8 +247,8 @@ </span> <span class="component-desc"> - <input type="text" size="45" name="naming_pattern" id="naming_pattern" class="custom-pattern" value="$sickbeard.NAMING_PATTERN" /> - <img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend" style="padding: 6px 0 0 3px;" /> + <input type="text" size="45" name="naming_pattern" id="naming_pattern" class="custom-pattern" value="$sickbeard.NAMING_PATTERN" style="font-size: 13px; height: 18px; margin-top: -8px;" /> + <img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend" style="padding: 0 0 0 3px; margin-top: -2px;" /> </span> </label> </div> @@ -281,12 +343,17 @@ <td>%RG</td> <td>RLSGROUP</td> </tr> + <tr> + <td class="align-right"><i class="icon-info-sign" title="If episode is proper/repack add 'proper' to name."></i> <b>Release Type:</b></td> + <td>%RT</td> + <td>PROPER</td> + </tr> </tbody> </table> <br/> </div> </div> - + <div class="field-pair"> <label class="nocheck clearfix" for="naming_multi_ep"> <span class="component-title">Multi-Episode Style:</span> @@ -301,7 +368,7 @@ </div> <div id="naming_example_div"> - <h3>Sample:</h3> + <h2>Sample:</h2> <div class="example"> <span class="jumbo" id="naming_example"> </span> </div> @@ -309,13 +376,15 @@ </div> <div id="naming_example_multi_div"> - <h3>Multi-EP sample:</h3> + <h2>Multi-EP sample:</h2> <div class="example"> <span class="jumbo" id="naming_example_multi"> </span> </div> - <br/><br /> + <br/> </div> + + <div class="field-pair clearfix"> <input type="checkbox" class="enabler" id="naming_custom_abd" name="naming_custom_abd" #if $sickbeard.NAMING_CUSTOM_ABD then "checked=\"checked\"" else ""#/> <label class="clearfix" for="naming_custom_abd"> @@ -351,8 +420,8 @@ </span> <span class="component-desc"> - <input type="text" size="45" name="naming_abd_pattern" id="naming_abd_pattern" class="custom-pattern" value="$sickbeard.NAMING_ABD_PATTERN" /> - <img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend" style="padding: 6px 0 0 3px;" /> + <input type="text" size="45" name="naming_abd_pattern" id="naming_abd_pattern" class="custom-pattern" value="$sickbeard.NAMING_ABD_PATTERN" style="font-size: 13px; height: 18px; margin-top: -8px"/> + <img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend" style="padding: 0 0 0 3px; margin-top: -2px;" /> </span> </label> </div> @@ -472,14 +541,19 @@ <td>%RG</td> <td>RLSGROUP</td> </tr> + <tr> + <td class="align-right"><i class="icon-info-sign" title="If episode is proper/repack add 'proper' to name."></i> <b>Release Type:</b></td> + <td>%RT</td> + <td>PROPER</td> + </tr> </tbody> </table> <br/> </div> </div><!-- /naming_abd_custom --> - + <div id="naming_abd_example_div"> - <h3>Sample:</h3> + <h2>Sample:</h2> <div class="example"> <span class="jumbo" id="naming_abd_example"> </span> </div> @@ -494,84 +568,18 @@ </fieldset> </div><!-- /component-naming //--> - <div class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Metadata</h3> - <p>The data associated to the data. These are files associated to a TV show in the form of images and text that, when supported, will enhance the viewing experience.</p> - </div> - - <fieldset class="component-group-list"> - <div class="field-pair clearfix"> - <label class="clearfix"> - <span class="component-title jumbo">Metadata Type:</span> - <span class="component-desc"> - #set $m_dict = $metadata.get_metadata_generator_dict() - <select id="metadataType" class="input-medium" > - #for ($cur_name, $cur_generator) in $m_dict.items(): - <option value="$GenericMetadata.makeID($cur_name)">$cur_name</option> - #end for - </select> - </span> - </label> - <span>Toggle the metadata options that you wish to be created. <b>Multiple targets may be used.</b></span> - </div> - -#for ($cur_name, $cur_generator) in $m_dict.items(): -#set $cur_metadata_inst = $sickbeard.metadata_provider_dict[$cur_generator.name] -#set $cur_id = $GenericMetadata.makeID($cur_name) -<div class="metadataDiv clearfix" id="$cur_id"> - <div class="metadata-options-wrapper"> - <h4>Create:</h4> - <div class="metadata-options"> - <label for="${cur_id}_show_metadata" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_show_metadata" #if $cur_metadata_inst.show_metadata then "checked=\"checked\"" else ""#/> Show Metadata</label> - <label for="${cur_id}_episode_metadata" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_episode_metadata" #if $cur_metadata_inst.episode_metadata then "checked=\"checked\"" else ""#/> Episode Metadata</label> - <label for="${cur_id}_fanart" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_fanart" #if $cur_metadata_inst.fanart then "checked=\"checked\"" else ""#/> Show Fanart Image</label> - <label for="${cur_id}_poster" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_poster" #if $cur_metadata_inst.poster then "checked=\"checked\"" else ""#/> Show Folder Image</label> - <label for="${cur_id}_episode_thumbnails" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_episode_thumbnails" #if $cur_metadata_inst.episode_thumbnails then "checked=\"checked\"" else ""#/> Episode Thumbnail</label> - <label for="${cur_id}_season_thumbnails" class="clearfix"><input type="checkbox" class="float-left metadata_checkbox" id="${cur_id}_season_thumbnails" #if $cur_metadata_inst.season_thumbnails then "checked=\"checked\"" else ""#/> Season Thumbnail</label> - </div> - </div> - <div class="metadata-example-wrapper"> - <h4>Results:</h4> - <div class="metadata-example"> - <label for="${cur_id}_show_metadata"><span id="${cur_id}_eg_show_metadata">$cur_metadata_inst.eg_show_metadata</span></label> - <label for="${cur_id}_episode_metadata"><span id="${cur_id}_eg_episode_metadata">$cur_metadata_inst.eg_episode_metadata</span></label> - <label for="${cur_id}_fanart"><span id="${cur_id}_eg_fanart">$cur_metadata_inst.eg_fanart</span></label> - <label for="${cur_id}_poster"><span id="${cur_id}_eg_poster">$cur_metadata_inst.eg_poster</span></label> - <label for="${cur_id}_episode_thumbnails"><span id="${cur_id}_eg_episode_thumbnails">$cur_metadata_inst.eg_episode_thumbnails</span></label> - <label for="${cur_id}_season_thumbnails"><span id="${cur_id}_eg_season_thumbnails">$cur_metadata_inst.eg_season_thumbnails</span></label> - </div> - </div> - - <input type="hidden" name="${cur_id}_data" id="${cur_id}_data" value="$cur_metadata_inst.get_config()" /> -</div> -#end for - - <div class="field-pair clearfix"> - <input type="checkbox" name="use_banner" id="use_banner" #if $sickbeard.USE_BANNER then "checked=checked" else ""#/> - <label class="clearfix" for="use_banner"> - <span class="component-title">Use Banners</span> - <span class="component-desc">Use banners instead of posters for 'Show Folder Image'</span> - </label> - </div> - - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - - </fieldset> - </div><!-- /component-metadata //--> - - <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </div><!-- /config-components --> + <br/> + <small class="float-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b> </small> + <input type="submit" class="btn config_submitter button" value="Save Changes" /><br/> </form> </div></div> +<div class="clearfix"></div> <script type="text/javascript" charset="utf-8"> <!-- + jQuery('#config-components').tabs(); jQuery('#tv_download_dir').fileBrowser({ title: 'Select TV Download Directory' }); - jQuery('#torrent_download_dir').fileBrowser({ title: 'Select Torrent finished downloads Directory' }); //--> </script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_providers.tmpl b/data/interfaces/default/config_providers.tmpl index 9c91fef323f75bb33659694578d6ae91657fe03f..ff4af4c99d210d82f4b90cee4a0c1b94147e432b 100755 --- a/data/interfaces/default/config_providers.tmpl +++ b/data/interfaces/default/config_providers.tmpl @@ -1,296 +1,313 @@ -#import sickbeard -#from sickbeard.providers.generic import GenericProvider -#set global $title="Config - Search Providers" -#set global $header="Search Providers" - -#set global $sbPath="../.." - -#set global $topmenu="config"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/configProviders.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> -#if $sickbeard.USE_NZBS -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ -#for $curNewznabProvider in $sickbeard.newznabProviderList: - \$(this).addProvider('$curNewznabProvider.getID()', '$curNewznabProvider.name', '$curNewznabProvider.url', '$curNewznabProvider.key', $int($curNewznabProvider.default)); -#end for -}); -//--> -</script> -#end if - -<div id="config"> -<div id="config-content"> - -<form id="configForm" action="saveProviders" method="post"> - - <div id="config-components"> - - <div id="core-component-group1" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Provider Priorities</h3> - <p>Check off and drag the providers into the order you want them to be used.</p> - <p>At least one provider is required but two are recommended.</p> - - #if not $sickbeard.USE_NZBS or not $sickbeard.USE_TORRENTS: - <blockquote style="margin: 20px 0;">NZB/Torrent providers can be toggled in <b><a href="$sbRoot/config/search">Search Settings</a></b></blockquote> - #else: - <br/> - #end if - - <div> - <h4 class="note">*</h4><p class="note">Provider does not support backlog searches at this time.</p> - <h4 class="note">**</h4><p class="note">Provider supports <b>limited</b> backlog searches, all episodes/qualities may not be available.</p> - </div> - </div> - - <fieldset class="component-group-list"> - <ul id="providerOrderList"> - #for $curProvider in $sickbeard.providers.sortedProviderList(): - #if $curProvider.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: - #continue - #elif $curProvider.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: - #continue - #end if - #set $curName = $curProvider.getID() - <li class="ui-state-default" id="$curName"> - <input type="checkbox" id="enable_$curName" class="checkbox provider_enabler" #if $curProvider.isEnabled() then "checked=\"checked\"" else ""#/> - <a href="$curProvider.url" class="imgLink" target="_new"><img src="$sbRoot/images/providers/$curProvider.imageName()" alt="" title="$curProvider.name" width="16" height="16" /></a> - $curProvider.name - #if not $curProvider.supportsBacklog then "*" else ""# - #if $curProvider.name == "EZRSS" then "**" else ""# - <span class="ui-icon ui-icon-arrowthick-2-n-s pull-right"></span> - </li> - #end for - </ul> - <input type="hidden" name="provider_order" id="provider_order" value="<%=" ".join([x.getID()+':'+str(int(x.isEnabled())) for x in sickbeard.providers.sortedProviderList()])%>"/> - <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </fieldset> - </div><!-- /component-group1 //--> - - <div id="core-component-group2" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Configure Built-In Providers</h3> - <p>Check with provider's website on how to obtain an API key if needed.</p> - </div> - - <fieldset class="component-group-list"> - <div class="field-pair"> - <label class="clearfix" for="editAProvider"> - <span class="component-title jumbo">Configure Provider:</span> - <span class="component-desc"> - #set $provider_config_list = [] - #for $cur_provider in ("nzbs_r_us", "omgwtfnzbs", "tvtorrents", "torrentleech", "btn", "binnewz", "t411", "piratebay","gks"): - #set $cur_provider_obj = $sickbeard.providers.getProviderClass($cur_provider) - #if $cur_provider_obj.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: - #continue - #elif $cur_provider_obj.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: - #continue - #end if - $provider_config_list.append($cur_provider_obj) - #end for - - #if $provider_config_list: - <select id="editAProvider" class="input-medium" > - #for $cur_provider in $provider_config_list + [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth and $sickbeard.USE_NZBS]: - <option value="$cur_provider.getID()">$cur_provider.name</option> - #end for - </select> - #else: - No providers available to configure. - #end if - </span> - </label> - </div> - - -<!-- start div for editing providers //--> -#for $curNewznabProvider in [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth]: - <div class="providerDiv" id="${curNewznabProvider.getID()}Div"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">$curNewznabProvider.name URL</span> - <input class="component-desc" type="text" id="${curNewznabProvider.getID()}_url" value="$curNewznabProvider.url" size="40" disabled/> - </label> - </div> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">$curNewznabProvider.name API Key</span> - <input class="component-desc newznab_key" type="text" id="${curNewznabProvider.getID()}_hash" value="$curNewznabProvider.key" size="40" /> - </label> - </div> - </div><!-- /${curNewznabProvider.getID()}Div //--> -#end for - - <div class="providerDiv" id="nzbs_r_usDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">NZBs'R'US User ID</span> - <input class="component-desc" type="text" name="nzbs_r_us_uid" value="$sickbeard.NZBSRUS_UID" size="10" /> - </label> - </div> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">NZBs'R'US API Key</span> - <input class="component-desc" type="text" name="nzbs_r_us_hash" value="$sickbeard.NZBSRUS_HASH" size="40" /> - </label> - </div> - </div><!-- /nzbs_r_usDiv //--> - - <div class="providerDiv" id="omgwtfnzbsDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">omgwtfnzbs User ID</span> - <input class="component-desc" type="text" name="omgwtfnzbs_uid" value="$sickbeard.OMGWTFNZBS_UID" size="10" /> - </label> - </div> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">omgwtfnzbs API Key</span> - <input class="component-desc" type="text" name="omgwtfnzbs_key" value="$sickbeard.OMGWTFNZBS_KEY" size="40" /> - </label> - </div> - </div><!-- /omgwtfnzbsDiv //--> - - <div class="providerDiv" id="binnewzDiv"> - <p> - Nothing to set up for this provider - </p> - </div><!-- /binnewzDiv //--> - - <div class="providerDiv" id="tvtorrentsDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">TvTorrents Digest:</span> - <input class="component-desc" type="text" name="tvtorrents_digest" value="$sickbeard.TVTORRENTS_DIGEST" size="40" /> - </label> - </div> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">TvTorrents Hash:</span> - <input class="component-desc" type="text" name="tvtorrents_hash" value="$sickbeard.TVTORRENTS_HASH" size="40" /> - </label> - </div> - </div><!-- /torrentleechDiv //--> - - <div class="providerDiv" id="torrentleechDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">TorrentLeech RSS key (enable in profile):</span> - <input class="component-desc" type="text" name="torrentleech_key" value="$sickbeard.TORRENTLEECH_KEY" size="40" /> - </label> - </div> - </div><!-- /torrentleechDiv //--> - - <div class="providerDiv" id="t411Div"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">T411 User name:</span> - <input class="component-desc" type="text" name="t411_username" value="$sickbeard.T411_USERNAME" size="40" /> - </label> - </div> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">T411 Password:</span> - <input class="component-desc" type="text" name="t411_password" value="$sickbeard.T411_PASSWORD" size="40" /> - </label> - </div> - </div><!-- /t411Div //--> - - <div class="providerDiv" id="btnDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">BTN API Key:</span> - <input class="component-desc" type="text" name="btn_api_key" value="$sickbeard.BTN_API_KEY" size="40" /> - </label> - </div> - </div><!-- /btnDiv //--> - - <div class="providerDiv" id="gksDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">GKS RSS Key:</span> - <input class="component-desc" type="text" name="gks_key" value="$sickbeard.GKS_KEY" size="40" /> - </label> - </div> - </div><!-- /gksDiv //--> - -<!-- end div for editing providers --> - - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - - </fieldset> - </div><!-- /component-group2 //--> -#if $sickbeard.USE_NZBS: - - <div id="core-component-group3" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Configure Custom Newznab Providers</h3> - <p>Add and setup custom Newznab providers.</p> - <p>Some built-in Newznab providers are already available above.</p> - </div> - - <fieldset class="component-group-list"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title jumbo">Select Provider:</span> - <span class="component-desc"> - <input type="hidden" name="newznab_string" id="newznab_string" /> - <select id="editANewznabProvider"> - <option value="addNewznab">-- add new provider --</option> - </select> - </span> - </label> - </div> - -<div class="newznabProviderDiv" id="addNewznab"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Provider Name</span> - <input class="component-desc" type="text" id="newznab_name" size="40" /> - </label> - </div> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Site URL</span> - <input class="component-desc" type="text" id="newznab_url" size="40" /> - </label> - </div> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">API Key</span> - <input class="component-desc" type="text" id="newznab_key" size="40" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">(leave blank if not required)</span> - </label> - </div> - <div id="newznab_add_div"> - <input type="button" class="btn newznab_save" id="newznab_add" value="Add" /> - </div> - <div id="newznab_update_div" style="display: none;"> - <input type="button" class="btn btn-danger newznab_delete" id="newznab_delete" value="Delete" /> - </div> -</div> - - </fieldset> - </div><!-- /component-group3 //--> -#end if - <div class="component-group-save"> - <input type="submit" class="btn config_submitter" value="Save Changes" /> - </div><br /> - - </div><!-- /config-components //--> - -</form> -</div></div> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard.providers.generic import GenericProvider +#set global $title="Config - Providers" +#set global $header="Search Providers" + +#set global $sbPath="../.." + +#set global $topmenu="config"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<script type="text/javascript" src="$sbRoot/js/configProviders.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + +var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; +#for $curNewznabProvider in $sickbeard.newznabProviderList: +\$(this).addProvider('$curNewznabProvider.getID()', '$curNewznabProvider.name', '$curNewznabProvider.url', '$curNewznabProvider.key', $int($curNewznabProvider.default), show_nzb_providers); +#end for +}); +//--> +</script> + +<div id="config"> +<div id="config-content"> + +<form id="configForm" action="saveProviders" method="post"> + + <div id="config-components"> + <ul> + <li><a href="#core-component-group1">Provider Priorities</a></li> + <li><a href="#core-component-group2">Configure Built-In Providers</a></li> + #if $sickbeard.USE_NZBS + <li><a href="#core-component-group3">Configure Custom Newznab Providers</a></li> + #end if + </ul> + + <div id="core-component-group1" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Provider Priorities</h3> + <p>Check off and drag the providers into the order you want them to be used.</p> + <p>At least one provider is required but two are recommended.</p> + + #if not $sickbeard.USE_NZBS or not $sickbeard.USE_TORRENTS: + <blockquote style="margin: 20px 0;">NZB/Torrent providers can be toggled in <b><a href="$sbRoot/config/search">Search Settings</a></b></blockquote> + #else: + <br/> + #end if + + <div> + <h4 class="note">*</h4><p class="note">Provider does not support backlog searches at this time.</p> + <h4 class="note">**</h4><p class="note">Provider supports <b>limited</b> backlog searches, all episodes/qualities may not be available.</p> + <h4 class="note">!</h4><p class="note">Provider is <b>NOT WORKING</b>.</p> + </div> + </div> + + <fieldset class="component-group-list" style="margin-left: 50px; margin-top:36px"> + <ul id="provider_order_list"> + #for $curProvider in $sickbeard.providers.sortedProviderList(): + #if $curProvider.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: + #continue + #elif $curProvider.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: + #continue + #end if + #set $curName = $curProvider.getID() + <li class="ui-state-default" id="$curName"> + <input type="checkbox" id="enable_$curName" class="provider_enabler" #if $curProvider.isEnabled() then "checked=\"checked\"" else ""#/> + <a href="$curProvider.url" class="imgLink" target="_new"><img src="$sbRoot/images/providers/$curProvider.imageName()" alt="$curProvider.name" title="$curProvider.name" width="16" height="16" /></a> + $curProvider.name + #if not $curProvider.supportsBacklog then "*" else ""# + #if $curProvider.name == "EZRSS" or $curProvider.name == "DailyTvTorrents" then "**" else ""# + #if $curProvider.name == "DailyTvTorrents" then "!" else "" + <span class="ui-icon ui-icon-arrowthick-2-n-s pull-right"></span> + </li> + #end for + </ul> + <input type="hidden" name="provider_order" id="provider_order" value="<%=" ".join([x.getID()+':'+str(int(x.isEnabled())) for x in sickbeard.providers.sortedProviderList()])%>"/> + <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </fieldset> + </div><!-- /component-group1 //--> + + <div id="core-component-group2" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Configure Built-In<br />Providers</h3> + <p>Check with provider's website on how to obtain an API key if needed.</p> + </div> + + <fieldset class="component-group-list"> + <div class="field-pair"> + <label class="clearfix" for="editAProvider"> + <span class="component-title jumbo">Configure Provider:</span> + <span class="component-desc"> + #set $provider_config_list = [] + #for $cur_provider in ("nzbs_r_us", "omgwtfnzbs", "tvtorrents", "torrentleech", "btn", "binnewz", "t411", "piratebay","gks"): + #set $cur_provider_obj = $sickbeard.providers.getProviderClass($cur_provider) + #if $cur_provider_obj.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: + #continue + #elif $cur_provider_obj.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: + #continue + #end if + $provider_config_list.append($cur_provider_obj) + #end for + + #if $provider_config_list: + <select id="editAProvider"> + #for $cur_provider in $provider_config_list + [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth and $sickbeard.USE_NZBS]: + <option value="$cur_provider.getID()">$cur_provider.name</option> + #end for + </select> + #else: + No providers available to configure. + #end if + </span> + </label> + </div> + + +<!-- start div for editing providers //--> +#for $curNewznabProvider in [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth]: + <div class="providerDiv" id="${curNewznabProvider.getID()}Div"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">$curNewznabProvider.name URL</span> + <input class="component-desc" type="text" id="${curNewznabProvider.getID()}_url" value="$curNewznabProvider.url" size="40" disabled/> + </label> + </div> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">$curNewznabProvider.name API Key</span> + <input class="component-desc newznab_key" type="text" id="${curNewznabProvider.getID()}_hash" value="$curNewznabProvider.key" size="40" /> + </label> + </div> + </div><!-- /${curNewznabProvider.getID()}Div //--> +#end for + + <div class="providerDiv" id="nzbs_r_usDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">NZBs'R'US User ID</span> + <input class="component-desc" type="text" name="nzbs_r_us_uid" value="$sickbeard.NZBSRUS_UID" size="10" /> + </label> + </div> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">NZBs'R'US API Key</span> + <input class="component-desc" type="text" name="nzbs_r_us_hash" value="$sickbeard.NZBSRUS_HASH" size="40" /> + </label> + </div> + </div><!-- /nzbs_r_usDiv //--> + + <div class="providerDiv" id="omgwtfnzbsDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">omgwtfnzbs User ID</span> + <input class="component-desc" type="text" name="omgwtfnzbs_uid" value="$sickbeard.OMGWTFNZBS_UID" size="10" /> + </label> + </div> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">omgwtfnzbs API Key</span> + <input class="component-desc" type="text" name="omgwtfnzbs_key" value="$sickbeard.OMGWTFNZBS_KEY" size="40" /> + </label> + </div> + </div><!-- /omgwtfnzbsDiv //--> + + <div class="providerDiv" id="binnewzDiv"> +<p> +Nothing to set up for this provider +</p> + </div><!-- /binnewzDiv //--> + + <div class="providerDiv" id="tvtorrentsDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">TvTorrents Digest:</span> + <input class="component-desc" type="text" name="tvtorrents_digest" value="$sickbeard.TVTORRENTS_DIGEST" size="40" /> + </label> + </div> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">TvTorrents Hash:</span> + <input class="component-desc" type="text" name="tvtorrents_hash" value="$sickbeard.TVTORRENTS_HASH" size="40" /> + </label> + </div> + </div><!-- /torrentleechDiv //--> + + <div class="providerDiv" id="torrentleechDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">TorrentLeech RSS key (enable in profile):</span> + <input class="component-desc" type="text" name="torrentleech_key" value="$sickbeard.TORRENTLEECH_KEY" size="40" /> + </label> + </div> + </div><!-- /torrentleechDiv //--> + + <div class="providerDiv" id="t411Div"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">T411 User name:</span> + <input class="component-desc" type="text" name="t411_username" value="$sickbeard.T411_USERNAME" size="40" /> + </label> + </div> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">T411 Password:</span> + <input class="component-desc" type="text" name="t411_password" value="$sickbeard.T411_PASSWORD" size="40" /> + </label> + </div> + </div><!-- /t411Div //--> + + <div class="providerDiv" id="btnDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">BTN API Key:</span> + <input class="component-desc" type="text" name="btn_api_key" value="$sickbeard.BTN_API_KEY" size="40" /> + </label> + </div> + </div><!-- /btnDiv //--> + +<div class="providerDiv" id="gksDiv"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title">GKS RSS Key:</span> + <input class="component-desc" type="text" name="gks_key" value="$sickbeard.GKS_KEY" size="40" /> + </label> + </div> + </div><!-- /gksDiv //--> + +<!-- end div for editing providers --> + + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + + </fieldset> + </div><!-- /component-group2 //--> + +#if $sickbeard.USE_NZBS + <div id="core-component-group3" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Configure Custom<br />Newznab Providers</h3> + <p>Add and setup custom Newznab providers.</p> + <p>Some built-in Newznab providers are already available above.</p> + </div> + + <fieldset class="component-group-list"> + <div class="field-pair"> + <label class="clearfix"> + <span class="component-title jumbo">Select Provider:</span> + <span class="component-desc"> + <input type="hidden" name="newznab_string" id="newznab_string" /> + <select id="editANewznabProvider"> + <option value="addNewznab">-- add new provider --</option> + </select> + </span> + </label> + </div> + +<div class="newznabProviderDiv" id="addNewznab"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Provider Name</span> + <input class="component-desc" type="text" id="newznab_name" size="40" /> + </label> + </div> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Site URL</span> + <input class="component-desc" type="text" id="newznab_url" size="40" /> + </label> + </div> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">API Key</span> + <input class="component-desc" type="text" id="newznab_key" size="40" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">(leave blank if not required)</span> + </label> + </div> + <div id="newznab_add_div"> + <input class="btn" type="button" class="newznab_save" id="newznab_add" value="Add" /> + </div> + <div id="newznab_update_div" style="display: none;"> + <input class="btn btn-danger newznab_delete" type="button" class="newznab_delete" id="newznab_delete" value="Delete" /> + </div> +</div> + + </fieldset> + </div><!-- /component-group3 //--> +#end if + + <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + + </div><!-- /config-components //--> + +</form> +</div></div> +<div class="clearfix"></div> +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#config-components').tabs(); +//--> +</script> +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_search.tmpl b/data/interfaces/default/config_search.tmpl index d95c6a6de22c4269b57f73db0e448118ec75b128..3bfd7bdd53c10172477ec4087de2fd774eaa9f27 100644 --- a/data/interfaces/default/config_search.tmpl +++ b/data/interfaces/default/config_search.tmpl @@ -1,427 +1,454 @@ -#import sickbeard -#set global $title="Config - Search Settings" -#set global $header="Search Settings" - -#set global $sbPath="../.." - -#set global $topmenu="config"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/configSearch.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> - -<div id="config"> -<div id="config-content"> -<h5>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></h5> - -<form id="configForm" action="saveSearch" method="post"> - - <div id="config-components"> - - <div id="core-component-group1" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Episode Search</h3> - <p>Settings that dictate how and when episode searching works with <a href="$sbRoot/config/providers">Providers</a>.</p> - </div> - - <fieldset class="component-group-list"> - <div class="field-pair"> - <input type="checkbox" name="download_propers" id="download_propers" #if $sickbeard.DOWNLOAD_PROPERS == True then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="download_propers"> - <span class="component-title">Download Propers</span> - <span class="component-desc">Replace original download with "Proper/Repack" if nuked?</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Search Frequency</span> - <input type="number" name="search_frequency" value="$sickbeard.SEARCH_FREQUENCY" size="5" min="10" class="input-small" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Time in minutes between searches (eg. 60)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Usenet Retention</span> - <input type="number" name="usenet_retention" value="$sickbeard.USENET_RETENTION" size="5" min="0" class="input-small" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Age limit in days for usenet articles to be used. (eg. 500)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Ignore Words</span> - <input type="text" name="ignore_words" value="$sickbeard.IGNORE_WORDS" size="45"/> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">List of words to ignore during search (comma separated without spaces) (eg. german,french,core2hd,dutch,swedish)</span> - </label> - </div> - - <div class="clearfix"></div> - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - - </fieldset> - </div><!-- /component-group1 //--> - - <div id="core-component-group2" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>NZB Search</h3> - <p>Settings that dictate how Sick Beard handles NZB search results.</p> - </div> - - <fieldset class="component-group-list"> - - <div class="field-pair"> - <input type="checkbox" name="use_nzbs" class="enabler" id="use_nzbs" #if $sickbeard.USE_NZBS then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="use_nzbs"> - <span class="component-title">Search NZBs</span> - <span class="component-desc">Should Sick Beard search for NZB files?</span> - </label> - </div> - - <div id="content_use_nzbs"> - <div class="field-pair"> - <label class="clearfix" for="nzb_method"> - <span class="component-title jumbo">NZB Method:</span> - <span class="component-desc"> - <select name="nzb_method" id="nzb_method" class="input-medium" > - #set $nzb_method_text = {'blackhole': "Black Hole", 'sabnzbd': "SABnzbd", 'nzbget': "NZBget"} - #for $curAction in ('sabnzbd', 'blackhole', 'nzbget'): - #if $sickbeard.NZB_METHOD == $curAction: - #set $nzb_method = "selected=\"selected\"" - #else - #set $nzb_method = "" - #end if - <option value="$curAction" $nzb_method>$nzb_method_text[$curAction]</option> - #end for - </select> - </span> - </label> - </div> - - <div id="blackhole_settings"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">NZB Black Hole</span> - <input type="text" name="nzb_dir" id="nzb_dir" value="$sickbeard.NZB_DIR" size="35" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">The directory where Sick Beard should store your <i>NZB</i> files.</span> - </label> - </div> - </div> - - <div id="sabnzbd_settings"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">SABnzbd URL</span> - <input type="text" name="sab_host" value="$sickbeard.SAB_HOST" size="45" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">URL to your SABnzbd+ install</span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">(eg. http://localhost:8000/)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">SABnzbd Username</span> - <input type="text" name="sab_username" value="$sickbeard.SAB_USERNAME" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Username of your SABnzbd+ server (blank for none)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">SABnzbd Password</span> - <input type="password" name="sab_password" value="$sickbeard.SAB_PASSWORD" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Password of your SABnzbd+ server (blank for none)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">SABnzbd API Key</span> - <input type="text" name="sab_apikey" value="$sickbeard.SAB_APIKEY" size="45" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">SABnzbd+ Config -> General -> API Key.</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">SABnzbd Category</span> - <input type="text" name="sab_category" value="$sickbeard.SAB_CATEGORY" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Category for downloads to go into (eg. TV)</span> - </label> - </div> - </div> - - <div id="nzbget_settings"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">NZBget HOST:PORT</span> - <input type="text" name="nzbget_host" value="$sickbeard.NZBGET_HOST" size="45" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Hostname and portnumber of the NZBget RPC (not NZBgetweb!)</span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">(eg. localhost:6789)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">NZBget Password</span> - <input type="password" name="nzbget_password" value="$sickbeard.NZBGET_PASSWORD" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Password found in nzbget.conf (by default tegbzn6789)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">NZBget Category</span> - <input type="text" name="nzbget_category" value="$sickbeard.NZBGET_CATEGORY" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Category for downloads to go into (eg. TV)</span> - </label> - </div> - </div> - - <div class="clearfix"></div> - - <div class="testNotification" id="testSABnzbd-result">Click below to test.</div> - <input type="button" value="Test SABnzbd" id="testSABnzbd" class="btn test-button"/> - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - - </div><!-- /content_use_nzbs //--> - - </fieldset> - </div><!-- /component-group2 //--> - - - <div id="core-component-group3" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Torrent Search</h3> - <p>Settings that dictate how Sick Beard handles Torrent search results.</p> - </div> - - <fieldset class="component-group-list"> - - <div class="field-pair"> - <input type="checkbox" name="use_torrents" class="enabler" id="use_torrents" #if $sickbeard.USE_TORRENTS == True then "checked=\"checked\"" else ""# /> - <label class="clearfix" for="use_torrents"> - <span class="component-title">Search Torrents</span> - <span class="component-desc">Should Sick Beard search for torrent files?</span> - </label> - </div> - - <div id="content_use_torrents"> - <div class="field-pair"> - <label class="clearfix" for="torrent_method"> - <span class="component-title jumbo">TORRENT Method</span> - <span class="component-desc"> - <select name="torrent_method" id="torrent_method"> - #set $torrent_method_text = {'blackhole': "Black hole", 'utorrent': "uTorrent", 'transmission': "Transmission", 'deluge': "Deluge", 'download_station': "Synology DS"} - #for $curAction in ('blackhole', 'utorrent', 'transmission', 'deluge', 'download_station'): - #if $sickbeard.TORRENT_METHOD == $curAction: - #set $torrent_method = "selected=\"selected\"" - #else - #set $torrent_method = "" - #end if - <option value="$curAction" $torrent_method>$torrent_method_text[$curAction]</option> - #end for - </select> - </label> - - <div id="t_blackhole_settings"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">TORRENT Black Hole</span> - <input type="text" name="torrent_dir" id="torrent_dir" value="$sickbeard.TORRENT_DIR" size="35" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">The directory where Sick Beard should store your <i>Torrent</i> files.</span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc"><b>NOTE:</b> This method will not working for ThePirateBay Provider</span> - </label> - </div> - - <div class="clearfix"></div> - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </div><!-- /content_use_torrents //--> - </div> - - <div id="torrent_settings"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title" id="host_desc">Torrent HOST</span> - <input type="text" name="torrent_host" id="torrent_host" value="$sickbeard.TORRENT_HOST" size="45" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">URL to your Torrent Client</span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">(eg. http://localhost:8000/)</span> - </label> - </div> - - <div class="field-pair" id="Torrent_username"> - <label class="nocheck clearfix"> - <span class="component-title" id="username_desc">Torrent Username</span> - <input type="text" name="torrent_username" id="torrent_username" value="$sickbeard.TORRENT_USERNAME" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Username of your Torrent Client (blank for none)</span> - </label> - </div> - - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title" id="password_desc">Torrent Password</span> - <input type="password" name="torrent_password" id="torrent_password" value="$sickbeard.TORRENT_PASSWORD" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Password of your Torrent Client (blank for none)</span> - </label> - </div> - - <div class="field-pair" id="Torrent_Label"> - <label class="nocheck clearfix"> - <span class="component-title" id="label_desc">Torrent Label</span> - <input type="text" name="torrent_label" id="torrent_label" value="$sickbeard.TORRENT_LABEL" size="20" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Add a specific label to Torrent</span> - </label> - <label class="nocheck clearfix" id="deluge_label_warning"> - <span class="component-title"> </span> - <span class="component-desc"><b>Note:</b> Label plugin must be enabled in Deluge client</span> - </label> - </div> - - <div class="field-pair" id="Torrent_Path"> - <label class="nocheck clearfix"> - <span class="component-title" id="directory_desc">Torrent Directory</span> - <input type="text" name="torrent_path" id="torrent_path" value="$sickbeard.TORRENT_PATH" size="35" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Where should it save the downloaded files? (blank for default)</span> - </label> - </div> - - <div class="field-pair" id="Torrent_Ratio"> - <label class="nocheck clearfix"> - <span class="component-title" id="torrent_ratio_desc">Torrent Ratio</span> - <input type="number" step="0.1" name="torrent_ratio" id="torrent_ratio" value="$sickbeard.TORRENT_RATIO" size="2" /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Stop transfer when reaching ratio (blank for default)</span> - </label> - </div> - - <div class="field-pair" id="Torrent_Paused"> - <label class="nocheck clearfix"> - <span class="component-title" id="torrent_paused_desc">Start Torrent Paused</span> - <input type="checkbox" name="torrent_paused" class="enabler" id="torrent_paused" #if $sickbeard.TORRENT_PAUSED == True then "checked=\"checked\"" else ""# /> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">Don't start downloading when torrent is added?</span> - </label> - </div> - - <div class="testNotification" id="testTorrent-result">Click below to test.</div> - <input class="btn" type="button" value="Test Connection" id="testTorrent" class="btn test-button"/> - <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </div> - </div> - </div><!-- /component-group3 //--> - <div id="core-component-group4" class="component-group clearfix"> - <div class="field-pair"> - <label class="clearfix" for="prefered_method"> - <span class="component-title jumbo">Preferred Method</span> - <span class="component-desc"> - <select name="prefered_method" id="preferred_method"> - #set $prefered_method_text = {'torrent': "Torrent", 'nzb': "Newsgroup"} - #for $curAction in ('torrent', 'nzb'): - #if $sickbeard.PREFERED_METHOD == $curAction: - #set $prefered_method = "selected=\"selected\"" - #else - #set $prefered_method = "" - #end if - <option value="$curAction" $prefered_method>$prefered_method_text[$curAction]</option> - #end for - </select> - </fieldset> - </div><!-- /component-group4 //--> - - <div class="component-group-save"> - <input type="submit" class="btn config_submitter button" value="Save Changes" /><br/> - </div><br /> - - </div><!-- /config-components //--> - -</form> - -</div></div> - -<script type="text/javascript" charset="utf-8"> -<!-- - - jQuery('#nzb_dir').fileBrowser({ title: 'Select NZB Black Hole/Watch Directory' }); - jQuery('#torrent_dir').fileBrowser({ title: 'Select Torrent Black Hole/Watch Directory' }); - jQuery('#torrent_path').fileBrowser({ title: 'Select Torrent Download Directory' }); - jQuery('#tv_download_dir').fileBrowser({ title: 'Select TV Download Directory' }); - -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import clients +#set global $title="Config - Episode Search" +#set global $header="Search Options" + +#set global $sbPath="../.." + +#set global $topmenu="config"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" src="$sbRoot/js/configSearch.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<div id="config"> +<div id="config-content"> + +<form id="configForm" action="saveSearch" method="post"> + + <div id="config-components"> + <ul> + <li><a href="#core-component-group1">Episode Search</a></li> + <li><a href="#core-component-group2">NZB Search</a></li> + <li><a href="#core-component-group3">Torrent Search</a></li> + <li><a href="#core-component-group4">Preferred Method</a></li> + </ul> + + + <div id="core-component-group1" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Episode Search</h3> + <p>Settings that dictate how and when episode searching works with <a href="$sbRoot/config/providers">Providers</a>.</p> + </div> + + <fieldset class="component-group-list"> + <div class="field-pair"> + <input type="checkbox" name="download_propers" id="download_propers" #if $sickbeard.DOWNLOAD_PROPERS == True then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="download_propers"> + <span class="component-title">Download Propers</span> + <span class="component-desc">Replace original download with "Proper/Repack" if nuked?</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Search Frequency</span> + <input type="text" name="search_frequency" value="$sickbeard.SEARCH_FREQUENCY" size="5" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Time in minutes between searches (eg. 60)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Usenet Retention</span> + <input type="text" name="usenet_retention" value="$sickbeard.USENET_RETENTION" size="5" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Age limit in days for usenet articles to be used. (eg. 500)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Ignore Words</span> + <input type="text" name="ignore_words" value="$sickbeard.IGNORE_WORDS" size="45" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Comma separated words to check in episode search.</span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Results containing any word in the list won't be snatched.</span> + </label> + </div> + + <div class="clearfix"></div> + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + + </fieldset> + </div><!-- /component-group1 //--> + + <div id="core-component-group2" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>NZB Search</h3> + <p>Settings that dictate how Sick Beard handles NZB search results.</p> + </div> + + <fieldset class="component-group-list"> + + <div class="field-pair"> + <input type="checkbox" name="use_nzbs" class="enabler" id="use_nzbs" #if $sickbeard.USE_NZBS then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="use_nzbs"> + <span class="component-title">Search NZBs</span> + <span class="component-desc">Should Sick Beard search for NZB files?</span> + </label> + </div> + + <div id="content_use_nzbs"> + <div class="field-pair"> + <label class="clearfix" for="nzb_method"> + <span class="component-title jumbo">NZB Method:</span> + <span class="component-desc"> + <select name="nzb_method" id="nzb_method"> + #set $nzb_method_text = {'blackhole': "Black hole", 'sabnzbd': "SABnzbd", 'nzbget': "NZBget"} + #for $curAction in ('sabnzbd', 'blackhole', 'nzbget'): + #if $sickbeard.NZB_METHOD == $curAction: + #set $nzb_method = "selected=\"selected\"" + #else + #set $nzb_method = "" + #end if + <option value="$curAction" $nzb_method>$nzb_method_text[$curAction]</option> + #end for + </select> + </span> + </label> + </div> + + <div id="blackhole_settings"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">NZB Black Hole</span> + <input type="text" name="nzb_dir" id="nzb_dir" value="$sickbeard.NZB_DIR" size="35" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">The directory where Sick Beard should store your <i>NZB</i> files.</span> + </label> + </div> + </div> + + <div id="sabnzbd_settings"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">SABnzbd URL</span> + <input type="text" id="sab_host" name="sab_host" value="$sickbeard.SAB_HOST" size="45" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">URL to your SABnzbd+ install</span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">(eg. http://localhost:8000/)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">SABnzbd Username</span> + <input type="text" name="sab_username" id="sab_username" value="$sickbeard.SAB_USERNAME" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Username of your SABnzbd+ server (blank for none)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">SABnzbd Password</span> + <input type="password" name="sab_password" id="sab_password" value="$sickbeard.SAB_PASSWORD" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Password of your SABnzbd+ server (blank for none)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">SABnzbd API Key</span> + <input type="text" name="sab_apikey" id="sab_apikey" value="$sickbeard.SAB_APIKEY" size="45" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">SABnzbd+ Config -> General -> API Key.</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">SABnzbd Category</span> + <input type="text" name="sab_category" id="sab_category" value="$sickbeard.SAB_CATEGORY" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Category for downloads to go into (eg. TV)</span> + </label> + </div> + </div> + + <div id="nzbget_settings"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">NZBget HOST:PORT</span> + <input type="text" name="nzbget_host" id="nzbget_host" value="$sickbeard.NZBGET_HOST" size="45" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Hostname and portnumber of the NZBget RPC (not NZBgetweb!)</span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">(eg. localhost:6789)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">NZBget Password</span> + <input type="password" name="nzbget_password" id="nzbget_password" value="$sickbeard.NZBGET_PASSWORD" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Password found in nzbget.conf (by default tegbzn6789)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">NZBget Category</span> + <input type="text" name="nzbget_category" id="nzbget_category" value="$sickbeard.NZBGET_CATEGORY" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Category for downloads to go into (eg. TV)</span> + </label> + </div> + </div> + + <div class="clearfix"></div> + + <div class="testNotification" id="testSABnzbd-result">Click below to test.</div> + <input class="btn" type="button" value="Test SABnzbd" id="testSABnzbd" class="btn test-button"/> + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + + </div><!-- /content_use_nzbs //--> + + </fieldset> + </div><!-- /component-group2 //--> + + <div id="core-component-group3" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Torrent Search</h3> + <p>Settings that dictate how Sick Beard handles Torrent search results.</p> + </div> + + <fieldset class="component-group-list"> + + <div class="field-pair"> + <input type="checkbox" name="use_torrents" class="enabler" id="use_torrents" #if $sickbeard.USE_TORRENTS == True then "checked=\"checked\"" else ""# /> + <label class="clearfix" for="use_torrents"> + <span class="component-title">Search Torrents</span> + <span class="component-desc">Should Sick Beard search for torrent files?</span> + </label> + </div> + + <div id="content_use_torrents"> + <div class="field-pair"> + <label class="clearfix" for="torrent_method"> + <span class="component-title jumbo">TORRENT Method</span> + <span class="component-desc"> + <select name="torrent_method" id="torrent_method"> + #set $torrent_method_text = {'blackhole': "Black hole", 'utorrent': "uTorrent", 'transmission': "Transmission", 'deluge': "Deluge", 'download_station': "Synology DS"} + #for $curAction in ('blackhole', 'utorrent', 'transmission', 'deluge', 'download_station'): + #if $sickbeard.TORRENT_METHOD == $curAction: + #set $torrent_method = "selected=\"selected\"" + #else + #set $torrent_method = "" + #end if + <option value="$curAction" $torrent_method>$torrent_method_text[$curAction]</option> + #end for + </select> + </label> + + <div id="t_blackhole_settings"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">TORRENT Black Hole</span> + <input type="text" name="torrent_dir" id="torrent_dir" value="$sickbeard.TORRENT_DIR" size="35" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">The directory where Sick Beard should store your <i>Torrent</i> files.</span> + </label> + </div> + + <div class="clearfix"></div> + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </div><!-- /content_use_torrents //--> + </div> + + <div id="torrent_settings"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title" id="host_desc">Torrent HOST</span> + <input type="text" name="torrent_host" id="torrent_host" value="$sickbeard.TORRENT_HOST" size="45" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">URL to your Torrent Client</span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">(eg. http://localhost:8000/)</span> + </label> + </div> + + <div class="field-pair" id="Torrent_username"> + <label class="nocheck clearfix"> + <span class="component-title" id="username_desc">Torrent Username</span> + <input type="text" name="torrent_username" id="torrent_username" value="$sickbeard.TORRENT_USERNAME" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Username of your Torrent Client (blank for none)</span> + </label> + </div> + + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title" id="password_desc">Torrent Password</span> + <input type="password" name="torrent_password" id="torrent_password" value="$sickbeard.TORRENT_PASSWORD" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Password of your Torrent Client (blank for none)</span> + </label> + </div> + + <div class="field-pair" id="Torrent_Label"> + <label class="nocheck clearfix"> + <span class="component-title" id="label_desc">Torrent Label</span> + <input type="text" name="torrent_label" id="torrent_label" value="$sickbeard.TORRENT_LABEL" size="20" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Add a specific label to Torrent</span> + </label> + <label class="nocheck clearfix" id="deluge_label_warning"> + <span class="component-title"> </span> + <span class="component-desc"><b>Note:</b> Label plugin must be enabled in Deluge client</span> + </label> + </div> + + <div class="field-pair" id="Torrent_Path"> + <label class="nocheck clearfix"> + <span class="component-title" id="directory_desc">Torrent Directory</span> + <input type="text" name="torrent_path" id="torrent_path" value="$sickbeard.TORRENT_PATH" size="35" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Where should it save the downloaded files? (blank for default)</span> + </label> + </div> + + <div class="field-pair" id="Torrent_Ratio"> + <label class="nocheck clearfix"> + <span class="component-title" id="torrent_ratio_desc">Torrent Ratio</span> + <input type="number" step="0.1" name="torrent_ratio" id="torrent_ratio" value="$sickbeard.TORRENT_RATIO" size="2" /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Stop transfer when reaching ratio (blank for default)</span> + </label> + </div> + + <div class="field-pair" id="Torrent_Paused"> + <label class="nocheck clearfix"> + <span class="component-title" id="torrent_paused_desc">Start Torrent Paused</span> + <input type="checkbox" name="torrent_paused" class="enabler" id="torrent_paused" #if $sickbeard.TORRENT_PAUSED == True then "checked=\"checked\"" else ""# /> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">Don't start downloading when torrent is added?</span> + </label> + </div> + + <div class="testNotification" id="testTorrent-result">Click below to test.</div> + <input class="btn" type="button" value="Test Connection" id="testTorrent" class="btn test-button"/> + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </div> + </div> + </fieldset> + </div><!-- /component-group3 //--> + <div id="core-component-group4" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Preferred Method</h3> + <p>Settings that dictate how Sick Beard will choose best result among NZBs and Torrents</p> + </div> + <div class="field-pair"> + <label class="clearfix" for="prefered_method"> + <span class="component-title jumbo">Preferred Method</span> + <span class="component-desc"> + <select name="prefered_method" id="preferred_method"> + #set $prefered_method_text = {'torrent': "Torrent", 'nzb': "Newsgroup"} + #for $curAction in ('torrent', 'nzb'): + #if $sickbeard.PREFERED_METHOD == $curAction: + #set $prefered_method = "selected=\"selected\"" + #else + #set $prefered_method = "" + #end if + <option value="$curAction" $prefered_method>$prefered_method_text[$curAction]</option> + #end for + </select> + <div class="clearfix"></div> + <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </fieldset> + + </div><!-- /component-group4 //--> + + <!--<div class="title-group clearfix" id="no-torrents"> + <div class="ui-corner-all config_message">Note: Sick Beard works better with Usenet than with Torrents, <a href="http://www.sickbeard.com/usenet.html" target="_blank">here's why</a>.</div> + </div> //--> + +</div><br /> + + <br/> + <small class="float-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b> </small> + <input type="submit" class="btn config_submitter button" value="Save Changes" /><br/> + + + </div><!-- /config-components //--> + +</form> + +</div></div> +<div class="clearfix"></div> + +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#config-components').tabs(); + jQuery('#nzb_dir').fileBrowser({ title: 'Select NZB Black Hole/Watch Directory' }); + jQuery('#torrent_dir').fileBrowser({ title: 'Select Torrent Black Hole/Watch Directory' }); + jQuery('#torrent_path').fileBrowser({ title: 'Select Torrent Download Directory' }); + jQuery('#tv_download_dir').fileBrowser({ title: 'Select TV Download Directory' }); + +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/config_subtitles.tmpl b/data/interfaces/default/config_subtitles.tmpl index 2108656eded07d61c8a55f97744a36ed14936c8c..736ce42642bea5a2427a99676e9d306f05855d6b 100644 --- a/data/interfaces/default/config_subtitles.tmpl +++ b/data/interfaces/default/config_subtitles.tmpl @@ -1,153 +1,161 @@ -#from sickbeard import subtitles -#import sickbeard - -#set global $title="Config - Subtitles" -#set global $header="Subtitles" - -#set global $sbPath="../.." - -#set global $topmenu="config" -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/configSubtitles.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/config.js"></script> -<script type="text/javascript" src="$sbRoot/js/lib/jquery.tokeninput.js"></script> -<link rel="stylesheet" type="text/css" href="$sbRoot/css/token-input.css" /> - -<script type="text/javascript"> - \$(document).ready(function() { - \$("#subtitles_languages").tokenInput( - [ - <%=",\r\n".join("{id: \"" + lang[2] + "\", name: \"" + lang[3] + "\"}" for lang in subtitles.subtitleLanguageFilter())%> - ], - { - method: "POST", - hintText: "Write to search a language and select it", - preventDuplicates: true, - prePopulate: - - [ - <%= - ",\r\n".join("{id: \"" + lang + "\", name: \"" + subtitles.getLanguageName(lang) + "\"}" for lang in sickbeard.SUBTITLES_LANGUAGES) if sickbeard.SUBTITLES_LANGUAGES != '' else '' - %> - ] - } - ); - }); -</script> - - - -<div id="config"> -<div id="config-content"> - -<form id="configForm" action="saveSubtitles" method="post"> - - <div id="config-components"> - - <div id="core-component-group4" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Subtitles Search</h3> - <p>Settings that dictate how Sick Beard handles subtitles search results.</p> - </div> - - <fieldset class="component-group-list"> - <div class="field-pair"> - <input type="checkbox" class="enabler" #if $sickbeard.USE_SUBTITLES then " checked=\"checked\"" else ""# id="use_subtitles" name="use_subtitles"> - <label for="use_subtitles" class="clearfix"> - <span class="component-title">Search Subtitles</span> - </label> - </div> - <div id="content_use_subtitles"> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Subtitle Languages</span> - <input type="text" id="subtitles_languages" name="subtitles_languages" style="border: 0px none"/> - </label> - </div> - <div class="field-pair"> - <label class="nocheck clearfix"> - <span class="component-title">Subtitle Directory</span> - <input type="text" size="35" value="$sickbeard.SUBTITLES_DIR" id="subtitles_dir" name="subtitles_dir"> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc">The directory where Sick Beard should store your <i>Subtitles</i> files.</span> - </label> - <label class="nocheck clearfix"> - <span class="component-title"> </span> - <span class="component-desc"><b>NOTE:</b> Leave empty if you want store subtitle in episode path.</span> - </label> - </div> - <div class="field-pair"> - <input type="checkbox" class="enabler" #if $sickbeard.SUBTITLES_DIR_SUB then " checked=\"checked\"" else ""# id="subtitles_dir_sub" name="subtitles_dir_sub"> - <label for="subtitles_dir_subs" class="clearfix"> - <span class="component-title">Use Subs Folder</span> - <span class="component-desc">Use Subs folder in episode path (usefull for Xbmc Subtitles plugin users) ? Do not use if you use a custom dir</span> - </label> - </div> - <div class="field-pair"> - <input type="checkbox" class="enabler" #if $sickbeard.SUBSNOLANG then " checked=\"checked\"" else ""# id="subsnolang" name="subsnolang"> - <label for="subsnolang" class="clearfix"> - <span class="component-title">Create Sub Files without language code</span> - <span class="component-desc">Create a sub file without language code next to the language coded one (usefull for players which needs exactly same name between video and srt files</span> - </label> - </div> - <div class="field-pair"> - <input type="checkbox" name="subtitles_history" id="subtitles_history" #if $sickbeard.SUBTITLES_HISTORY then " checked=\"checked\"" else ""#/> - <label class="clearfix" for="subtitles_history"> - <span class="component-title">Subtitles History</span> - <span class="component-desc">Log downloaded Subtitle on History page?</span> - </label> - </div> - <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </div> - </fieldset> - </div><!-- /component-group1 //--> - - <div id="core-component-group2" class="component-group clearfix"> - - <div class="component-group-desc"> - <h3>Subtitle Plugins</h3> - <p>Check off and drag the plugins into the order you want them to be used.</p> - <p class="note">At least one plugin is required.</p> - <p class="note"><span style="color: #654B24; font-size: 16px;">*</span> Web-scraping plugin</p> - </div> - - <fieldset class="component-group-list"> - <ul id="service_order_list"> - #for $curService in $sickbeard.subtitles.sortedServiceList(): - #set $curName = $curService.id - <li class="ui-state-default" id="$curName"> - <input type="checkbox" id="enable_$curName" class="service_enabler" #if $curService.enabled then "checked=\"checked\"" else ""#/> - <a href="$curService.url" class="imgLink" target="_new"> - <img src="$sbRoot/images/subtitles/$curService.image" alt="$curService.name" title="$curService.name" width="16" height="16" /> - </a> - $curService.name.capitalize() - #if not $curService.api_based then "*" else ""# - <span class="ui-icon ui-icon-arrowthick-2-n-s pull-right"></span> - </li> - #end for - </ul> - <input type="hidden" name="service_order" id="service_order" value="<%=" ".join([x.get('id')+':'+str(int(x.get('enabled'))) for x in sickbeard.subtitles.sortedServiceList()])%>"/> - - <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - </fieldset> - - </div><!-- /component-group2 //--> - - <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> - - </div><!-- /config-components //--> - -</form> -</div></div> -<script type="text/javascript" charset="utf-8"> -<!-- - - jQuery('#subtitles_dir').fileBrowser({ title: 'Select Subtitles Download Directory' }); -//--> -</script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#from sickbeard import subtitles +#import sickbeard + +#set global $title="Config - Subtitles" +#set global $header="Subtitles" + +#set global $sbPath="../.." + +#set global $topmenu="config" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" src="$sbRoot/js/configSubtitles.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/config.js"></script> +<script type="text/javascript" src="$sbRoot/js/lib/jquery.tokeninput.js"></script> +<link rel="stylesheet" type="text/css" href="$sbRoot/css/token-input.css" /> + +<script type="text/javascript"> + \$(document).ready(function() { + \$("#subtitles_languages").tokenInput( + [ + <%=",\r\n".join("{id: \"" + lang[2] + "\", name: \"" + lang[3] + "\"}" for lang in subtitles.subtitleLanguageFilter())%> + ], + { + method: "POST", + hintText: "Write to search a language and select it", + preventDuplicates: true, + prePopulate: + + [ + <%= + ",\r\n".join("{id: \"" + lang + "\", name: \"" + subtitles.getLanguageName(lang) + "\"}" for lang in sickbeard.SUBTITLES_LANGUAGES) if sickbeard.SUBTITLES_LANGUAGES != '' else '' + %> + ] + } + ); + }); +</script> + +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<div id="config"> +<div id="config-content"> + +<form id="configForm" action="saveSubtitles" method="post"> + + <div id="config-components"> + <ul> + <li><a href="#core-component-group4">Subtitles Search</a></li> + <li><a href="#core-component-group2">Subtitles Plugin</a></li> + </ul> + + <div id="core-component-group4" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Subtitles Search</h3> + <p>Settings that dictate how Sick Beard handles subtitles search results.</p> + </div> + + <fieldset class="component-group-list"> + <div class="field-pair"> + <input type="checkbox" class="enabler" #if $sickbeard.USE_SUBTITLES then " checked=\"checked\"" else ""# id="use_subtitles" name="use_subtitles"> + <label for="use_subtitles" class="clearfix"> + <span class="component-title">Search Subtitles</span> + </label> + </div> + <div id="content_use_subtitles"> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Subtitle Languages</span> + <input type="text" id="subtitles_languages" name="subtitles_languages" style="border: 0px none"/> + </label> + </div> + <div class="field-pair"> + <label class="nocheck clearfix"> + <span class="component-title">Subtitle Directory</span> + <input type="text" size="35" value="$sickbeard.SUBTITLES_DIR" id="subtitles_dir" name="subtitles_dir"> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">The directory where Sick Beard should store your <i>Subtitles</i> files.</span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc"><b>NOTE:</b> Leave empty if you want store subtitle in episode path.</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" class="enabler" #if $sickbeard.SUBTITLES_DIR_SUB then " checked=\"checked\"" else ""# id="subtitles_dir_sub" name="subtitles_dir_sub"> + <label for="subtitles_dir_subs" class="clearfix"> + <span class="component-title">Use Subs Folder</span> + <span class="component-desc">For XBMC users. Do not use if you use a custom dir</span> + </label> + </div> + <div class="field-pair"> + <input type="checkbox" class="enabler" #if $sickbeard.SUBSNOLANG then " checked=\"checked\"" else ""# id="subsnolang" name="subsnolang"> + <label for="subsnolang" class="clearfix"> + <span class="component-title">Create Sub Files without language code</span> + <span class="component-desc">Create a sub file without language code next to the language coded one (usefull for players which needs same name between video and srt files)</span> + </label> + </div> +<div class="field-pair"> + <input type="checkbox" name="subtitles_history" id="subtitles_history" #if $sickbeard.SUBTITLES_HISTORY then " checked=\"checked\"" else ""#/> + <label class="clearfix" for="subtitles_history"> + <span class="component-title">Subtitles History</span> + <span class="component-desc">Log downloaded Subtitle on History page?</span> + </label> + </div> + <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </div> + </fieldset> + </div><!-- /component-group1 //--> + + <div id="core-component-group2" class="component-group clearfix"> + + <div class="component-group-desc"> + <h3>Subtitle Plugins</h3> + <p>Check off and drag the plugins into the order you want them to be used.</p> + <p class="note">At least one plugin is required.</p> + <p class="note"><span style="color: #654B24; font-size: 16px;">*</span> Web-scraping plugin</p> + </div> + + <fieldset class="component-group-list" style="margin-left: 50px; margin-top:36px"> + <ul id="service_order_list"> + #for $curService in $sickbeard.subtitles.sortedServiceList(): + #set $curName = $curService.id + <li class="ui-state-default" id="$curName"> + <input type="checkbox" id="enable_$curName" class="service_enabler" #if $curService.enabled then "checked=\"checked\"" else ""#/> + <a href="$curService.url" class="imgLink" target="_new"> + <img src="$sbRoot/images/subtitles/$curService.image" alt="$curService.name" title="$curService.name" width="16" height="16" /> + </a> + $curService.name.capitalize() + #if not $curService.api_based then "*" else ""# + <span class="ui-icon ui-icon-arrowthick-2-n-s pull-right"></span> + </li> + #end for + </ul> + <input type="hidden" name="service_order" id="service_order" value="<%=" ".join([x.get('id')+':'+str(int(x.get('enabled'))) for x in sickbeard.subtitles.sortedServiceList()])%>"/> + + <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + </fieldset> + </div><!-- /component-group2 //--> + + <br/><input type="submit" class="btn config_submitter" value="Save Changes" /><br/> + + </div><!-- /config-components //--> + +</form> +</div></div> +<div class="clearfix"></div> +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#config-components').tabs(); + jQuery('#subtitles_dir').fileBrowser({ title: 'Select Subtitles Download Directory' }); +//--> +</script> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/displayShow.tmpl b/data/interfaces/default/displayShow.tmpl index 1000c6247dad272fd8eaa3fa741a793db624fd4f..1c87c6dd64a68b84e3303775ce8a5fcfae726b16 100644 --- a/data/interfaces/default/displayShow.tmpl +++ b/data/interfaces/default/displayShow.tmpl @@ -1,292 +1,359 @@ -#import sickbeard -#from sickbeard import subtitles -#import sickbeard.helpers -#from sickbeard import common -#from sickbeard.common import * -#from sickbeard import db -#import subliminal -#import os.path, os -#import datetime -#set global $title=$show.name -#set $myDB = $db.DBConnection() -#set $today = str($datetime.date.today().toordinal()) -#set $tvid =str($show.tvdbid) -#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") -#set $curfr = [x[1] for x in $fr if int(x[0]) == $show.tvdbid] -#if len($curfr) != 0: - #set $lfr = $curfr[0] -#else - #set $lfr = 0 -#end if -#set $en = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'en' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") -#set $curen = [x[1] for x in $en if int(x[0]) == $show.tvdbid] -#if len($curen) != 0: - #set $leng = $curen[0] -#else - #set $leng = 0 -#end if -#set $no = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = '' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") -#set $curno = [x[1] for x in $no if int(x[0]) == $show.tvdbid] -#if len($curno) != 0: - #set $lno = $curno[0] -#else - #set $lno = 0 -#end if -#set $manq = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE location = '' AND season != 0 and episode != 0 AND (airdate <= "+$today+" and airdate != 1) and showid="+$tvid+" GROUP BY showid") -#set $curmanq = [x[1] for x in $manq if int(x[0]) == $show.tvdbid] -#if len($curmanq) != 0: - #set $lmanq = $curmanq[0] -#else - #set $lmanq = 0 -#end if -#set $subs= $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE subtitles <> '' and showid="+$tvid+" GROUP BY showid") -#set $cursubs = [x[1] for x in $subs if int(x[0]) == $show.tvdbid] -#if len($cursubs) != 0: - #set $lsubs = $cursubs[0] -#else - #set $lsubs = 0 -#end if -#set global $header = '<a href="http://thetvdb.com/?tab=series&id=%d" target="_new">%s</a>' % ($show.tvdbid, $show.name) - -#set global $topmenu="manageShows"# -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/lib/jquery.bookmarkscroll.js?$sbPID"></script> - - -<div class="h2footer align-right"> -#if (len($seasonResults) > 14): - <select id="seasonJump"> - <option value="jump">Jump to Season</option> - #for $seasonNum in $seasonResults: - <option value="#season-$seasonNum["season"]">#if int($seasonNum["season"]) == 0 then "Specials" else "Season " + str($seasonNum["season"])#</option> - #end for - </select> -#else: - <b>Season:</b> - #for $seasonNum in $seasonResults: - #if int($seasonNum["season"]) == 0: - <a href="#season-$seasonNum["season"]">Specials</a> - #else: - <a href="#season-$seasonNum["season"]">${str($seasonNum["season"])}</a> - #end if - #if $seasonNum != $seasonResults[-1]: - <span class="separator">|</span> - #end if - #end for -#end if -</div><br/> - -#if $show_message: - <div class="alert alert-info"> - $show_message - </div> -#end if - -<input type="hidden" id="sbRoot" value="$sbRoot" /> - -<script type="text/javascript" src="$sbRoot/js/displayShow.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/ajaxEpSubtitles.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/ajaxHisttrunc.js?$sbPID"></script> - -<div class="align-left"><b>Change Show:</b> -<div class="navShow"><img id="prevShow" width="16" height="18" src="$sbRoot/images/prev.gif" alt="<<" title="Prev Show" /></div> -<select id="pickShow"> -#for $curShow in $sortedShowList: -<option value="$curShow.tvdbid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option> -#end for -</select> -<div class="navShow"><img id="nextShow" width="16" height="18" src="$sbRoot/images/next.gif" alt=">>" title="Next Show" /></div> -</div> - -<div id="summary" class="align-left"> -<table> -<div class="float-left" style="margin-right: 20px"> -<a href="$sbRoot/showPoster/?show=$show.tvdbid&which=poster" rel="dialog" title="$show.name"><img src="$sbRoot/showPoster/?show=$show.tvdbid&which=poster"#if $sickbeard.USE_SUBTITLES then 'height="320"' else 'height="300"' # alt=""/></a> -</div> -#if $show.network and $show.airs: - <tr><td class="showLegend">Airs: </td><td>$show.airs on $show.network</td></tr> -#else if $show.network: - <tr><td class="showLegend">Airs: </td><td>$show.network</td></tr> -#else if $show.airs: - <tr><td class="showLegend">Airs: </td><td>$show.airs</td></tr> -#end if - <tr><td class="showLegend">Status: </td><td>$show.status</td></tr> -#if $showLoc[1]: - <tr><td class="showLegend">Location: </td><td>$showLoc[0]</td></tr> -#else: - <tr><td class="showLegend"><span style="color: red;">Location: </span></td><td><span style="color: red;">$showLoc[0]</span> (dir is missing)</td></tr> -#end if -#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality)) - <tr><td class="showLegend">Quality: </td><td> -#if $show.quality in $qualityPresets: -<span class="quality $qualityPresetStrings[$show.quality]">$qualityPresetStrings[$show.quality]</span> -#else: -#if $anyQualities: -initially download: <b><%=", ".join([Quality.qualityStrings[x] for x in sorted(anyQualities)])%></b> #if $bestQualities then " + " else ""# -#end if -#if $bestQualities: -replace with: <b><%=", ".join([Quality.qualityStrings[x] for x in sorted(bestQualities)])%></b> -#end if -#end if - </td></tr> - <tr><td class="showLegend">Metadata language:</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="" /> $show.lang</td></tr> - <tr> - <td class="showLegend">Desired Audio language:</td> - <td> - <img src="$sbRoot/images/flags/${show.audio_lang}.png" alt="$show.audio_lang" width="16" /> $show.audio_lang - </td> - </tr> - <tr><td class="showLegend">Custom Search Names:</td><td>$show.custom_search_names</td></tr> - #if $sickbeard.USE_SUBTITLES - <tr><td class="showLegend">Subtitles: </td><td><img src="$sbRoot/images/#if int($show.subtitles) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> -#end if - <tr><td class="showLegend">Flatten Folders: </td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> - <tr><td class="showLegend">Paused: </td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> - <tr><td class="showLegend">Air-by-Date: </td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> -<div class="float-left"> - - <tr><td class="showLegend">French Episodes : </td><td>$lfr</td></tr> - <tr><td class="showLegend">English Episodes : </td><td>$leng</td></tr> - <tr><td class="showLegend">Unknown Episodes : </td><td>$lno</td></tr> - <tr><td class="showLegend">Missing Episodes : </td><td>$lmanq</td></tr> - <tr><td class="showLegend">Downloaded Subtitles : </td><td>$lsubs</td></tr> - </div> -</table> -</div> - -<div class="float-left"> -Change selected episodes to -<select id="statusSelect"> -#for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED] + sorted($Quality.DOWNLOADED): -#if $curStatus == $DOWNLOADED: -#continue -#end if -<option value="$curStatus">$statusStrings[$curStatus]</option> -#end for -</select> -<input type="hidden" id="showID" value="$show.tvdbid" /> -<input type="button" class="btn" id="changeStatus" value="Go" /><br /> -<br /> -<br /> -</div> -<div class="float-right"> -Change Audio of selected episodes to -<select id="audioSelect"> -#for $k,$v in $common.showLanguages.iteritems(): - #if $k!="": - <option value="$k" - #if $show.audio_lang == $k: - selected - #end if - #end if - >$v</option> -#end for -</select> -<input type="hidden" id="showID" value="$show.tvdbid" /> -<input type="button" class="btn" id="changeAudio" value="Go" /><br /> -<br /> -<br /> -</div> -#set $curSeason = -1 - -<div class="float-right clearfix" id="checkboxControls"> - <div style="padding-bottom: 5px;"> - <label for="wanted" class="checkbox inline wanted"><input type="checkbox" id="wanted" checked="checked" /> Wanted: <b>$epCounts[$Overview.WANTED]</b></label> - <label for="qual" class="checkbox inline qual"><input type="checkbox" id="qual" checked="checked" /> Low Quality: <b>$epCounts[$Overview.QUAL]</b></label> - <label for="good" class="checkbox inline good"><input type="checkbox" id="good" checked="checked" /> Downloaded: <b>$epCounts[$Overview.GOOD]</b></label> - <label for="skipped" class="checkbox inline skipped"><input type="checkbox" id="skipped" checked="checked" /> Skipped: <b>$epCounts[$Overview.SKIPPED]</b></label> - </div> - <div class="pull-right"> - <button class="btn btn-mini seriesCheck"><a href="#" onclick="return false;">Select Filtered Episodes</a></button> - <button class="btn btn-mini clearAll"><a href="#" onclick="return false;">Clear All</a></button> - </div> -</div> - -<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> - -#for $epResult in $sqlResults: - - #if int($epResult["season"]) != $curSeason: - <tr><td colspan="11"><a name="season-$epResult["season"]"></a></td></tr> - <tr class="seasonheader" id="season-$epResult["season"]"> - <td colspan="9"> - <h2>#if int($epResult["season"]) == 0 then "Specials" else "Season "+str($epResult["season"])#</h2> - </td> - </tr> - <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Audio</th>#if $sickbeard.USE_SUBTITLES and $show.subtitles then "<th>Subs</th>" else ""#<th>Status</th><th>Search</th><th>Hist</th></tr> - #set $curSeason = int($epResult["season"]) - #end if - - #set $epStr = str($epResult["season"]) + "x" + str($epResult["episode"]) - #set $epLoc = $epResult["location"] - <tr class="$Overview.overviewStrings[$epCats[$epStr]] season-$curSeason"> - <td width="1%"> -#if int($epResult["status"]) != $UNAIRED - <input type="checkbox" class="epCheck" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" /> -#end if - </td> - <td align="center"><img src="$sbRoot/images/#if $epResult["hasnfo"] == 1 then "nfo.gif\" alt=\"Y" else "nfo-no.gif\" alt=\"N"#" width="23" height="11" /></td> - <td align="center"><img src="$sbRoot/images/#if $epResult["hastbn"] == 1 then "tbn.gif\" alt=\"Y" else "tbn-no.gif\" alt=\"N"#" width="23" height="11" /></td> - <td align="center">$epResult["episode"]</td> - <td> - $epResult["name"] - #if $epResult["description"] != "" and $epResult["description"] != None: - <img src="$sbRoot/images/info32.png" height="16" class="plotInfo" alt="" id="plot_info_$show.tvdbid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>" /> - #end if - </td> - <td align="center" class="nowrap">#if int($epResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($epResult["airdate"]))#</td> - <td> -#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()): -$epLoc[len($show._location)+1:] -#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location): -$epLoc -#end if - - </td> - <td align="center" class="audio_langs_column"> - #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult["status"])) - #if $epResult["audio_langs"] == "" and $curStatus in [$DOWNLOADED, $SNATCHED, $ARCHIVED] - <img src="$sbRoot/images/flags/unknown.png" alt="" width="20" /> - #else - <img src="$sbRoot/images/flags/${epResult["audio_langs"]}.png" alt="$epResult["audio_langs"]" width="16" /> - #end if - </td> -</small> - </td> -#if $sickbeard.USE_SUBTITLES and $show.subtitles: - <td id="subtitles_column" class="subtitles_column" align="center"> - #if $epResult["subtitles"]: - #for $sub_lang in subliminal.language.language_list($epResult["subtitles"].split(',')): - #if sub_lang.alpha2 != "" - <img src="$sbRoot/images/flags/${sub_lang.alpha2}.png" width="16" height="11" alt="${sub_lang}" /> - #end if - #end for - #end if - </td> -#end if - #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult["status"])) - #if $curQuality != Quality.NONE: - <td class="status_column" align="center">$statusStrings[$curStatus] <span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td> -#else: - <td class="status_column" align="center">$statusStrings[$curStatus]</td> -#end if - <td align="center"> - #if int($epResult["season"]) != 0: - <a class="epSearch" href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" height="16" alt="search" title="Manual Search" /></a> - #end if - #if $sickbeard.USE_SUBTITLES and $show.subtitles and len(set(str($epResult["subtitles"]).split(',')).intersection(set($subtitles.wantedLanguages()))) < len($subtitles.wantedLanguages()) and $epResult["location"] - <a class="epSubtitlesSearch" href="searchEpisodeSubtitles?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/closed_captioning.png" height="16" alt="search subtitles" title="Search Subtitles" /></a> - #end if - </td> - <td align="center"> - <a class="histTrunc" href="trunchistory?epid=$epResult["episode_id"]"><img src="$sbRoot/images/corbeille.png" height="16" alt="trunc" title="Trunc Downloaded links History" /></a> - </td> - </tr> - -#end for -</table><br /> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import subtitles +#import sickbeard.helpers +#from sickbeard.common import * +#import subliminal +#from sickbeard import db +#from sickbeard import common +#import os.path, os +#import datetime +#set global $title=$show.name +#set $myDB = $db.DBConnection() +#set $today = str($datetime.date.today().toordinal()) +#set $tvid =str($show.tvdbid) +#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") +#set $curfr = [x[1] for x in $fr if int(x[0]) == $show.tvdbid] +#if len($curfr) != 0: + #set $lfr = $curfr[0] +#else + #set $lfr = 0 +#end if +#set $en = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'en' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") +#set $curen = [x[1] for x in $en if int(x[0]) == $show.tvdbid] +#if len($curen) != 0: + #set $leng = $curen[0] +#else + #set $leng = 0 +#end if +#set $no = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = '' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" and showid="+$tvid+" GROUP BY showid") +#set $curno = [x[1] for x in $no if int(x[0]) == $show.tvdbid] +#if len($curno) != 0: + #set $lno = $curno[0] +#else + #set $lno = 0 +#end if +#set $manq = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE location = '' AND season != 0 and episode != 0 AND (airdate <= "+$today+" and airdate != 1) and showid="+$tvid+" GROUP BY showid") +#set $curmanq = [x[1] for x in $manq if int(x[0]) == $show.tvdbid] +#if len($curmanq) != 0: + #set $lmanq = $curmanq[0] +#else + #set $lmanq = 0 +#end if +#set $subs= $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE subtitles <> '' and showid="+$tvid+" GROUP BY showid") +#set $cursubs = [x[1] for x in $subs if int(x[0]) == $show.tvdbid] +#if len($cursubs) != 0: + #set $lsubs = $cursubs[0] +#else + #set $lsubs = 0 +#end if +##set global $header = '<a></a>' % +#set global $topmenu="manageShows"# +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" src="$sbRoot/js/lib/jquery.bookmarkscroll.js?$sbPID"></script> + + +#if $show_message: + <div id="show_message" class="ui-corner-all">$show_message</div><br /> +#end if + +<input type="hidden" id="sbRoot" value="$sbRoot" /> + +<script type="text/javascript" src="$sbRoot/js/displayShow.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/ajaxEpSubtitles.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/ajaxHisttrunc.js?$sbPID"></script> + +<div class="navShows"> +</br> +<div class="align-right"><b>Change Show:</b> +<div class="navShow"><img id="prevShow" src="$sbRoot/images/prev.gif" alt="<<" title="Prev Show" /></div> +<select id="pickShow"> +#for $curShow in $sortedShowList: +<option value="$curShow.tvdbid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option> +#end for +</select> +<div class="navShow"><img id="nextShow" src="$sbRoot/images/next.gif" alt=">>" title="Next Show" /></div> +</div></div> + + + +<div class="showInfo"> +<h1 class="title"><a>$show.name#if $show.imdbid: + <tr><td class="showLegend"></td><td><img src="$sbRoot/images/ratings/${$int($round($float($show.imdb_info['rating'])))}.png" width="70" height="16" alt="$show.imdb_info['rating']" title="$show.imdb_info['rating']"/></td></tr> + #else: + <tr><td class="showLegend">No Rating</td><td></td></tr> + #end if + </td></tr></a></h1> +<span class="headerInfo" style="color: #b7b7b7; line-height: 16px;"> +#if not $show.imdbid + ($show.startyear) - $show.runtime min + #if $show.genre: + - $show.genre[1:-1].replace('|',' | ') + #end if + <span class="tvshowLink" style="vertical-align: text-top"> + <a href="http://www.thetvdb.com/?tab=series&id=$show.tvdbid" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&id=$show.tvdbid"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" style="margin-top: -1px;"/></a> + </span> +#else + <img src="$sbRoot/images/flags/${$show.imdb_info['country_codes']}.png" width="16" height="11" style="margin-top: 3px; margin-left: 3px" /> ($show.imdb_info['year']) - $show.imdb_info['runtimes'] min - $show.imdb_info['genres'].replace('|',' | ') + <span class="tvshowLink" style="vertical-align: text-top"> + <a href="http://www.imdb.com/title/$show.imdbid" onclick="window.open(this.href, '_blank'); return false;" title="http://www.imdb.com/title/$show.imdbid"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" style="margin-top: -1px;"/> + <a href="http://www.thetvdb.com/?tab=series&id=$show.tvdbid" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&id=$show.tvdbid"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" style="margin-top: -1px;"/></a> + </span> + +#end if +</span> + +##There is a special/season_0?## +#if int($seasonResults[-1]["season"]) == 0: + #set $season_special=1 +#else: + #set $season_special=0 +#end if + +#if not $sickbeard.DISPLAY_SHOW_SPECIALS and $season_special: + $seasonResults.pop(-1) +#end if + +<div class="seasonList"> +<span> +#if (len($seasonResults) > 14): + <select id="seasonJump"> + <option value="jump">Jump to Season</option> + #for $seasonNum in $seasonResults: + <option value="#season-$seasonNum["season"]">#if int($seasonNum["season"]) == 0 then "Specials" else "Season " + str($seasonNum["season"])#</option> + #end for + </select> +#else: + <b>Season:</b> + #for $seasonNum in $seasonResults: + #if int($seasonNum["season"]) == 0: + <a href="#season-$seasonNum["season"]">Specials</a> + #else: + <a href="#season-$seasonNum["season"]">${str($seasonNum["season"])}</a> + #end if + #if $seasonNum != $seasonResults[-1]: + <span class="separator">|</span> + #end if + #end for +#end if +</span> + +<span class="float-right"> +#if $season_special: + <b>Display Specials:</b> + #if sickbeard.DISPLAY_SHOW_SPECIALS: + <a class="inner" href="$sbRoot/toggleDisplayShowSpecials/?show=$show.tvdbid">Hide</a> + #else: + <a class="inner" href="$sbRoot/toggleDisplayShowSpecials/?show=$show.tvdbid">Show</a> + #end if +#end if +</span> +</div> +<div id="summary" class="align-left"> +<table class="infoTable" cellspacing="0" border="0" cellpadding="0"> +<tr> +<td width="75%"> +<table> +#if $show.network and $show.airs: + <tr><td class="showLegend">Airs: </td><td>$show.airs on $show.network</td></tr> +#else if $show.network: + <tr><td class="showLegend">Airs: </td><td>$show.network</td></tr> +#else if $show.airs: + <tr><td class="showLegend">Airs: </td><td>$show.airs</td></tr> +#end if + <tr><td class="showLegend">Status: </td><td>$show.status</td></tr> +#if $showLoc[1]: + <tr><td class="showLegend">Location: </td><td>$showLoc[0]</td></tr> +#else: + <tr><td class="showLegend"><span style="color: red;">Location: </span></td><td><span style="color: red;">$showLoc[0]</span> (dir is missing)</td></tr> +#end if +#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality)) + <tr><td class="showLegend">Quality: </td><td> +#if $show.quality in $qualityPresets: +<span class="quality $qualityPresetStrings[$show.quality]">$qualityPresetStrings[$show.quality]</span> +#else: +#if $anyQualities: +<i>Initial:</i> <b><%=", ".join([Quality.qualityStrings[x] for x in sorted(anyQualities)])%></b> #if $bestQualities then " </br> " else ""# +#end if +#if $bestQualities: +<i>Replace with:</i> <b><%=", ".join([Quality.qualityStrings[x] for x in sorted(bestQualities)])%></b> +#end if +#end if + + <tr><td class="showLegend">Info Language:</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="$show.lang" title="$show.lang" /></td></tr> + <tr><td class="showLegend">Audio Language:</td><td><img src="$sbRoot/images/flags/${show.audio_lang}.png" width="16" height="11" alt="$show.audio_lang" title="$show.lang" /></td></tr> + <tr><td class="showLegend">Custom Names :</td><td>$show.custom_search_names</td></tr> + #if $show.imdbid: + <tr><td class="showLegend">Rating :</td><td><img src="$sbRoot/images/ratings/${$int($round($float($show.imdb_info['rating'])))}.png" width="70" height="16" alt="$show.imdb_info['rating']" title="$show.imdb_info['rating']"/></td></tr> + #else: + <tr><td class="showLegend">No Rating</td><td></td></tr> + #end if + </td></tr> + </table> + <td style="float: right;"> + <table> + <tr><td class="showLegend">Flat Folders: </td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + <tr><td class="showLegend">Paused: </td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + + <tr><td class="showLegend">Air-by-Date: </td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + #if $sickbeard.USE_SUBTITLES + <tr><td class="showLegend">Subtitles: </td><td><img src="$sbRoot/images/#if int($show.subtitles) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> +#end if + <tr><td class="showLegend"><img src="$sbRoot/images/flags/fr.png" width="16" height="11" alt="fr" title="fr" /> episodes:</td><td>$lfr</td></tr> + <tr><td class="showLegend"><img src="$sbRoot/images/flags/en.png" width="16" height="11" alt="en" title="en" /> episodes:</td><td>$leng</td></tr> + <tr><td class="showLegend"><img src="$sbRoot/images/flags/unknown.png" width="16" height="11" alt="uno" title="uno" /> episodes:</td><td>$lno</td></tr> + <tr><td class="showLegend"><img src="$sbRoot/images/no16.png" width="10" height="10" alt="no" title="no" /> episodes:</td><td>$lmanq</td></tr> + <tr><td class="showLegend"><img src="$sbRoot/images/closed_captioning.png" width="12" height="12" alt="no" title="no" /> downloaded:</td><td>$lsubs</td></tr> + +</table> + +</td> +</tr> +</table> + +</div> +</div> +<div class="tvshowImg"><a href="$sbRoot/showPoster/?show=$curShow.tvdbid&which=poster" rel="dialog" title="$show.name"><img src="$sbRoot/showPoster/?show=$show.tvdbid&which=poster" class="posterThumb" alt=""/></a> +</div> + +#set $curSeason = -1 +#set $odd = 0 + +<div style="clear:both;" class="clearfix"></div> + +<div class="float-left"> + Change selected episodes to + <select id="statusSelect"> + #for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED] + sorted($Quality.DOWNLOADED): + #if $curStatus == $DOWNLOADED: + #continue + #end if + <option value="$curStatus">$statusStrings[$curStatus]</option> + #end for + </select> + <input type="hidden" id="showID" value="$show.tvdbid" /> + <input class="btn" type="button" id="changeStatus" value="Go" /> +</div> + + <div class="float-right clearfix" id="checkboxControls" style="margin-top: 3px;"> + <div style="padding-bottom: 3px;"> + <label for="wanted"><span class="wanted">Wanted: <b>$epCounts[$Overview.WANTED]</b> <input type="checkbox" id="wanted" checked="checked" /></span></label> + <label for="qual"><span class="qual">Low Quality: <b>$epCounts[$Overview.QUAL]</b> <input type="checkbox" id="qual" checked="checked" /></span></label> + <label for="good"><span class="good">Downloaded: <b>$epCounts[$Overview.GOOD]</b> <input type="checkbox" id="good" checked="checked" /></span></label> + <label for="skipped"><span class="skipped">Skipped: <b>$epCounts[$Overview.SKIPPED]</b> <input type="checkbox" id="skipped" checked="checked" /></span></label> + </div> + <div class="pull-right"> + <button class="btn btn-mini seriesCheck" style="line-height: 8px;"><a>Select Filtered Episodes</a></button> + <button class="btn btn-mini clearAll" style="line-height: 8px;"><a>Clear All</a></button> + </div> + </div> +<br /> +<div class="float-left"> + Change Audio of selected episodes to + <select id="audioSelect"> + #for $k,$v in $common.showLanguages.iteritems(): + #if $k!="": + <option value="$k" + #if $show.audio_lang == $k: + selected + #end if + #end if + >$v</option> + #end for + </select> + <input type="hidden" id="showID" value="$show.tvdbid" /> + <input type="button" class="btn" id="changeAudio" value="Go" /> +</div> + +<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> + +#for $epResult in $sqlResults: + #if not $sickbeard.DISPLAY_SHOW_SPECIALS and int($epResult["season"]) == 0: + #continue + #end if + + #if int($epResult["season"]) != $curSeason: + <tr><td colspan="9" style="height: 0px; padding:0; margin:0;"><a name="season-$epResult["season"]"></a></td></tr> + <tr class="seasonheader" id="season-$epResult["season"]" > + <td colspan="9"> + <h2>#if int($epResult["season"]) == 0 then "Specials" else "Season "+str($epResult["season"])#</h2> + </td> + </tr> + <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Audio</th>#if $sickbeard.USE_SUBTITLES and $show.subtitles then "<th>Subs</th>" else ""#<th>Status</th><th>Search</th><th>Hist</th></tr> + #set $curSeason = int($epResult["season"]) + #end if + + #set $epStr = str($epResult["season"]) + "x" + str($epResult["episode"]) + #set $epLoc = $epResult["location"] + <tr class="$Overview.overviewStrings[$epCats[$epStr]] season-$curSeason"> + <td width="1%"> +#if int($epResult["status"]) != $UNAIRED + <input type="checkbox" class="epCheck" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" /> +#end if + </td> + <td align="center"><img src="$sbRoot/images/#if $epResult["hasnfo"] == 1 then "nfo.gif\" alt=\"Y" else "nfo-no.gif\" alt=\"N"#" width="23" height="11" /></td> + <td align="center"><img src="$sbRoot/images/#if $epResult["hastbn"] == 1 then "tbn.gif\" alt=\"Y" else "tbn-no.gif\" alt=\"N"#" width="23" height="11" /></td> + <td align="center">$epResult["episode"]</td> + <td class="title"> + #if $epResult["description"] != "" and $epResult["description"] != None: + <img style="padding-top: 3px;" src="$sbRoot/images/info32.png" height="16" class="plotInfo" alt="" id="plot_info_$show.tvdbid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>" /> + #end if + $epResult["name"] +</td> + <td align="center" class="nowrap">#if int($epResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($epResult["airdate"]))#</td> + <td class="filename"><small> +#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()): + #set $epLoc = os.path.basename($epLoc[len($show._location)+1:]) +#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location): + #set $epLoc = os.path.basename($epLoc) +#end if +$epLoc + </td> + <td align="center" class="audio_langs_column"> + #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult["status"])) + #if $epResult["audio_langs"] == "" and $curStatus in [$DOWNLOADED, $SNATCHED, $ARCHIVED] + <img src="$sbRoot/images/flags/unknown.png" alt="" width="20" /> + #else + <img src="$sbRoot/images/flags/${epResult["audio_langs"]}.png" alt="$epResult["audio_langs"]" width="16" /> + #end if + </td> +</small> + </td> +#if $sickbeard.USE_SUBTITLES and $show.subtitles: + <td id="subtitles_column" class="subtitles_column" align="left"> + #if $epResult["subtitles"]: + #for $sub_lang in subliminal.language.language_list($epResult["subtitles"].split(',')): + #if sub_lang.alpha2 != "" + <img src="$sbRoot/images/flags/${sub_lang.alpha2}.png" width="16" height="11" alt="${sub_lang}" /> + #end if + #end for + #end if + </td> +#end if +#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult["status"])) +#if $curQuality != Quality.NONE: + <td class="status_column">$statusStrings[$curStatus] <span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td> +#else: + <td class="status_column">$statusStrings[$curStatus]</td> +#end if + <td align="center"> + #if int($epResult["season"]) != 0: + <a class="epSearch" href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" height="16" alt="search" title="Manual Search" /></a> + #end if +#if $sickbeard.USE_SUBTITLES and $show.subtitles and len(set(str($epResult["subtitles"]).split(',')).intersection(set($subtitles.wantedLanguages()))) < len($subtitles.wantedLanguages()) and $epResult["location"] + <a class="epSubtitlesSearch" href="searchEpisodeSubtitles?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/closed_captioning.png" height="16" alt="search subtitles" title="Search Subtitles" /></a> + #end if + </td> + <td align="center"> + <a class="histTrunc" href="trunchistory?epid=$epResult["episode_id"]"><img src="$sbRoot/images/corbeille.png" height="16" alt="trunc" title="Trunc Downloaded links History" /></a> + </td> + </tr> + +#end for +</table><br /> + + + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/editShow.tmpl b/data/interfaces/default/editShow.tmpl index b7f7885354f7697878b96fda83d041091d85d721..760cd706422663a9ac77aed040ac13bcb9b25b11 100644 --- a/data/interfaces/default/editShow.tmpl +++ b/data/interfaces/default/editShow.tmpl @@ -1,98 +1,167 @@ -#import sickbeard -#from sickbeard import common -#from sickbeard import exceptions -#set global $title="Edit " + $show.name -#set global $header=$show.name - -#set global $sbPath=".." - -#set global $topmenu="home" -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - - \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { - var resultStr = ''; - - if (data.results.length == 0) { - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${show.lang}.png)"'; - resultStr = '<option value="$show.lang" selected="selected" + flag>$show.lang</option>'; - } else { - var current_lang_added = false; - \$.each(data.results, function(index, obj) { - - if (obj == "$show.lang") { - selected = ' selected="selected"'; - current_lang_added = true; - } - else { - selected = ''; - } - - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; - resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; - }); - if (!current_lang_added) - resultStr += '<option value="$show.lang" selected="selected">$show.lang</option>'; - - } - \$('#tvdbLangSelect').html(resultStr) - - }); - -}); -//--> -</script> - - -<form action="editShow" method="post"> -<input type="hidden" name="show" value="$show.tvdbid" /> -Location: <input type="text" name="location" id="location" value="$show._location" size="50" /><br /> -<br /> -Quality: -#set $qualities = $common.Quality.splitQuality(int($show.quality)) -#set global $anyQualities = $qualities[0] -#set global $bestQualities = $qualities[1] -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") -<br /> -Metadata Language: <select name="tvdbLang" id="tvdbLangSelect"></select><br /> -Note: This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> -<br /> -<br /> -Desired Audio language: <select name="audio_lang" id="showLangSelect"> -#for $k,$v in $common.showLanguages.iteritems(): - #if $k!="": - <option value="$k" - #if $show.audio_lang == $k: - selected - #end if - #end if - >$v</option> -#end for -</select> -<br /> -<br /> -Custom Search Names: <input type="text" name="custom_search_names" id="custom_search_names" value="$show.custom_search_names" size="50" /><br /> -<strong>Note:</strong> Custom names used to find show. Define some custom names if show can't be found. Custom names should be separated by ",". Keep empty to use default show name (based on metadata language)<br /> -<br /> -Flatten files (no folders): <input type="checkbox" name="flatten_folders" #if $show.flatten_folders == 1 and not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> -Paused: <input type="checkbox" name="paused" #if $show.paused == 1 then "checked=\"checked\"" else ""# /><br /><br /> -Download subtitles: <input type="checkbox" name="subtitles"#if $show.subtitles == 1 and $sickbeard.USE_SUBTITLES then " checked=\"checked\"" else ""##if not $sickbeard.USE_SUBTITLES then " disabled=\"disabled\"" else ""#/><br /><br /> -Air by date: -<input type="checkbox" name="air_by_date" #if $show.air_by_date == 1 then "checked=\"checked\"" else ""# /><br /> -(check this if the show is released as Show.03.02.2010 rather than Show.S02E03) -<br /><br /> -<input class="btn" type="submit" value="Submit" /> -</form> - -<script type="text/javascript" charset="utf-8"> -<!-- - jQuery('#location').fileBrowser({ title: 'Select Show Location' }); -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import common +#from sickbeard import exceptions +#from sickbeard import scene_exceptions +#set global $title="Edit " + $show.name +#set global $header=$show.name + +#set global $sbPath=".." + +#set global $topmenu="home" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + + \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { + var resultStr = ''; + + if (data.results.length == 0) { + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${show.lang}.png)"'; + resultStr = '<option value="$show.lang" selected="selected" + flag>$show.lang</option>'; + } else { + var current_lang_added = false; + \$.each(data.results, function(index, obj) { + + if (obj == "$show.lang") { + selected = ' selected="selected"'; + current_lang_added = true; + } + else { + selected = ''; + } + + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; + resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; + }); + if (!current_lang_added) + resultStr += '<option value="$show.lang" selected="selected">$show.lang</option>'; + + } + \$('#tvdbLangSelect').html(resultStr) + + }); + +}); +//--> +</script> + + +<form action="editShow" method="post"> +<img src="$sbRoot/showPoster/?show=$show.tvdbid&which=banner" class="imgHomeWrapperbig imgHomeWrapperRoundedbig bannerbig" alt="$show.tvdbid" title="$show.name"/> +<br /> +<br /> +<br /> +<input type="hidden" name="show" value="$show.tvdbid" /> +<b>Location:</b> <input type="text" name="location" id="location" value="$show._location" size="50" /><br /> +<br /> +<b>Quality:</b> +#set $qualities = $common.Quality.splitQuality(int($show.quality)) +#set global $anyQualities = $qualities[0] +#set global $bestQualities = $qualities[1] +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") +<br /> + +<b>Info Language:</b> <select name="tvdbLang" id="tvdbLangSelect"></select><br /> +Note: This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> +This <b>DOES NOT</b> allow Sick Beard to download non-english TV episodes!<br /> +<br /> +Desired Audio language: <select name="audio_lang" id="showLangSelect"> +#for $k,$v in $common.showLanguages.iteritems(): +#if $k!="": +<option value="$k" +#if $show.audio_lang == $k: +selected +#end if +#end if +>$v</option> +#end for +</select> +<br /> +<br /> +Custom Search Names: <input type="text" name="custom_search_names" id="custom_search_names" value="$show.custom_search_names" size="50" /><br /> +<strong>Note:</strong> Custom names used to find show. Define some custom names if show can't be found. Custom names should be separated by ",". Keep empty to use default show name (based on metadata language)<br /> +<br /> +<b>Flatten files (no folders):</b> <input type="checkbox" name="flatten_folders" #if $show.flatten_folders == 1 and not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> +<b>Paused:</b> <input type="checkbox" name="paused" #if $show.paused == 1 then "checked=\"checked\"" else ""# /><br /><br /> +<b>Subtitles:</b> <input type="checkbox" name="subtitles"#if $show.subtitles == 1 and $sickbeard.USE_SUBTITLES then " checked=\"checked\"" else ""##if not $sickbeard.USE_SUBTITLES then " disabled=\"disabled\"" else ""#/><br /><br /> + +<b>Air by date: </b> +<input type="checkbox" name="air_by_date" #if $show.air_by_date == 1 then "checked=\"checked\"" else ""# /><br /> +(check this if the show is released as Show.03.02.2010 rather than Show.S02E03) +<br /><br /> +<input type="submit" id="submit" value="Submit" class="btn btn-primary" /> +</form> + +<script type="text/javascript" charset="utf-8"> +<!-- + var all_exceptions = new Array; + + jQuery('#location').fileBrowser({ title: 'Select Show Location' }); + + \$('#submit').click(function(){ + all_exceptions = [] + + \$("#exceptions_list option").each ( function() { + all_exceptions.push( \$(this).val() ); + }); + + \$("#exceptions_list").val(all_exceptions); + + }); + + \$('#addSceneName').click(function() { + var scene_ex = \$('#SceneName').val() + var option = \$("<option>") + all_exceptions = [] + + \$("#exceptions_list option").each ( function() { + all_exceptions.push( \$(this).val() ) + }); + + \$('#SceneName').val('') + + if (jQuery.inArray(scene_ex, all_exceptions) > -1 || (scene_ex == '')) + return + + \$("#SceneException").show() + + option.attr("value",scene_ex) + option.html(scene_ex) + return option.appendTo('#exceptions_list'); + }); + + \$('#removeSceneName').click(function() { + \$('#exceptions_list option:selected').remove(); + + \$(this).toggle_SceneException() + }); + + $.fn.toggle_SceneException = function() { + all_exceptions = [] + + \$("#exceptions_list option").each ( function() { + all_exceptions.push( \$(this).val() ); + }); + + if (all_exceptions == '') + \$("#SceneException").hide(); + else + \$("#SceneException").show(); + } + + \$(this).toggle_SceneException(); + +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/errorlogs.tmpl b/data/interfaces/default/errorlogs.tmpl index 3b0dc039d2b4925965a521d1ceb5e008a54c366b..91dbcf061cbc894259f86b0e4b7a94a592f17164 100644 --- a/data/interfaces/default/errorlogs.tmpl +++ b/data/interfaces/default/errorlogs.tmpl @@ -1,25 +1,30 @@ -#import sickbeard -#from sickbeard import classes -#from sickbeard.common import * -#set global $title="Logs & Errors" - -#set global $sbPath = ".." - -#set global $topmenu="errorlogs"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<div class="align-left"><pre> -#for $curError in $classes.ErrorViewer.errors[:30]: -$curError.time $curError.message -#end for -</pre> -</div> - -<script type="text/javascript" charset="utf-8"> -<!-- -window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import classes +#from sickbeard.common import * +#set global $header="Logs & Errors" +#set global $title="" + +#set global $sbPath = ".." + +#set global $topmenu="errorlogs"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<div class="align-left"><pre> +#for $curError in $classes.ErrorViewer.errors[:30]: +$curError.time $curError.message +#end for +</pre> +</div> + +<script type="text/javascript" charset="utf-8"> +<!-- +window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/genericMessage.tmpl b/data/interfaces/default/genericMessage.tmpl index 47553c8853fa3d281da651200c9ca156b2e61230..acbaaaa1018b07a7226d37a9df6e5fa2a5a2a226 100644 --- a/data/interfaces/default/genericMessage.tmpl +++ b/data/interfaces/default/genericMessage.tmpl @@ -1,14 +1,14 @@ -#import sickbeard -#set global $title="" - -#set global $sbPath="../.." - - -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<h2>$subject</h2> -$message - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#set global $title="" + +#set global $sbPath="../.." + + +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<h2>$subject</h2> +$message + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/history.tmpl b/data/interfaces/default/history.tmpl index 06f4a1083dd0ec158a5cf238de4316de175eb21f..759101f2c91baa6b37ee903592a092b163c971c4 100644 --- a/data/interfaces/default/history.tmpl +++ b/data/interfaces/default/history.tmpl @@ -1,93 +1,96 @@ -#import sickbeard -#import os.path -#import datetime -#import re -#from sickbeard import history -#from sickbeard import providers -#from sickbeard.providers import generic -#from sickbeard.common import * -#set global $title="History" - -#set global $sbPath=".." - -#set global $topmenu="history"# -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript"> -<!-- -\$(document).ready(function() -{ - \$("#historyTable:has(tbody tr)").tablesorter({ - widgets: ['zebra', 'stickyHeaders', 'filter'], - sortList: [[0,1]], - textExtraction: { - 4: function(node) { return \$(node).find("span").text().toLowerCase(); } - } - }); - \$('#limit').change(function(){ - url = '$sbRoot/history/?limit='+\$(this).val() - window.location.href = url - }); -}); -//--> -</script> - -<div class="h2footer align-right"><b>Limit:</b> - <select name="limit" id="limit"> - <option value="100" #if $limit == "100" then "selected=\"selected\"" else ""#>100</option> - <option value="250" #if $limit == "250" then "selected=\"selected\"" else ""#>250</option> - <option value="500" #if $limit == "500" then "selected=\"selected\"" else ""#>500</option> - <option value="0" #if $limit == "0" then "selected=\"selected\"" else ""#>All</option> - </select> -</div><br/> - -<table id="historyTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> - <thead><tr><th class="nowrap">Time</th><th>Episode</th><th>Action</th><th>Provider</th><th>Quality</th></tr></thead> - <tbody> -#for $hItem in $historyResults: -#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem["action"])) - <tr> - <td class="nowrap">$datetime.datetime.strptime(str($hItem["date"]), $history.dateFormat)</td> - - <td><a href="$sbRoot/home/displayShow?show=$hItem["showid"]#season-$hItem["season"]">$hItem["show_name"] - <%=str(hItem["season"]) +"x"+ "%02i" % int(hItem["episode"]) %></a></td> - <td align="center" #if $curStatus == SUBTITLED then 'class="subtitles_column"' else ''#><span style="cursor: help;" title="$os.path.basename($hItem["resource"])">$statusStrings[$curStatus]</span> - #if $curStatus == SUBTITLED: - <img width="16" height="11" src="/images/flags/<%= hItem["resource"][len(hItem["resource"])-6:len(hItem["resource"])-4]+'.png'%>"> - #end if - </td><td align="center"> - #if $curStatus == DOWNLOADED and $str($hItem["provider"]) == '-1': - #set $match = $re.search("\-(\w+)\.\w{3}\Z", $os.path.basename($hItem["resource"])) - #if $match: - #if $match.group(1).upper() in ("X264", "720P"): - #set $match = $re.search("(\w+)\-.*\-"+$match.group(1)+"\.\w{3}\Z", $os.path.basename($hItem["resource"]), re.IGNORECASE) - #if $match: - $match.group(1).upper() - #end if - #else: - $match.group(1).upper() - #end if - #end if - #elif $curStatus == DOWNLOADED: - $hItem["provider"] - #else - #if $len($hItem["provider"]) > 0 - #if $curStatus == SNATCHED: - #set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem["provider"])) - #if $provider != None: - <img src="$sbRoot/images/providers/<%=provider.imageName()%>" width="16" height="16" alt="$provider.name" title="$provider.name"/> - #else: - <img src="$sbRoot/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider"/> - #end if - #else: - <img src="$sbRoot/images/subtitles/<%=hItem["provider"]+'.png' %>" width="16" height="16" alt="$hItem["provider"]" title="<%=hItem["provider"].capitalize()%>"/> - #end if - #end if - #end if - </td> - <td align="center"><span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td> - </tr> -#end for - </tbody> -</table> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import os.path +#import datetime +#import re +#from sickbeard import history +#from sickbeard import providers +#from sickbeard.providers import generic +#from sickbeard.common import * +#set global $title="" +#set global $header="History" + +#set global $sbPath=".." + +#set global $topmenu="history"# +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript"> +<!-- +\$(document).ready(function() +{ + \$("#historyTable:has(tbody tr)").tablesorter({ + widgets: ['zebra', 'filter'], + sortList: [[0,1]], + textExtraction: { + 4: function(node) { return \$(node).find("span").text().toLowerCase(); } + } + }); + \$('#limit').change(function(){ + url = '$sbRoot/history/?limit='+\$(this).val() + window.location.href = url + }); +}); +//--> +</script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<div class="h2footer align-right"><b>Limit:</b> + <select name="limit" id="limit"> + <option value="100" #if $limit == "100" then "selected=\"selected\"" else ""#>100</option> + <option value="250" #if $limit == "250" then "selected=\"selected\"" else ""#>250</option> + <option value="500" #if $limit == "500" then "selected=\"selected\"" else ""#>500</option> + <option value="0" #if $limit == "0" then "selected=\"selected\"" else ""#>All</option> + </select> +</div> + +<table id="historyTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> + <thead><tr><th class="nowrap">Time</th><th>Episode</th><th>Action</th><th>Provider</th><th>Quality</th></tr></thead> + <tbody> +#for $hItem in $historyResults: +#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem["action"])) + <tr> + <td class="nowrap">$datetime.datetime.strptime(str($hItem["date"]), $history.dateFormat)</td> + <td width="35%"><a style="color: #000000; text-align: center;" href="$sbRoot/home/displayShow?show=$hItem["showid"]#season-$hItem["season"]">$hItem["show_name"] - <%=str(hItem["season"]) +"x"+ "%02i" % int(hItem["episode"]) %>#if "proper" in $hItem["resource"].lower or "repack" in $hItem["resource"].lower then ' <span class="quality Proper">Proper</span>' else ""#</a></td> + <td align="center" #if $curStatus == SUBTITLED then 'class="subtitles_column"' else ''#><span style="cursor: help;" title="$os.path.basename($hItem["resource"])">$statusStrings[$curStatus]</span> + #if $curStatus == SUBTITLED: + <img width="16" height="11" src="/images/flags/<%= hItem["resource"][len(hItem["resource"])-6:len(hItem["resource"])-4]+'.png'%>"> + #end if + </td> + <td align="center"> + #if $curStatus == DOWNLOADED: + #set $match = $re.search("\-(\w+)\.\w{3}\Z", $os.path.basename($hItem["resource"])) + #if $match + #if $match.group(1).upper() in ("X264", "720P"): + #set $match = $re.search("(\w+)\-.*\-"+$match.group(1)+"\.\w{3}\Z", $os.path.basename($hItem["resource"]), re.IGNORECASE) + #if $match + <i>$match.group(1).upper()</i> + #end if + #else: + <i>$match.group(1).upper()</i> + #end if + #end if + #else + #if $hItem["provider"] > 0 + #if $curStatus == SNATCHED: + #set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem["provider"])) + #if $provider != None: + <img src="$sbRoot/images/providers/<%=provider.imageName()%>" width="16" height="16" alt="$provider.name" title="$provider.name"/> + #else: + <img src="$sbRoot/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider"/> + #end if + #else: + <img src="$sbRoot/images/subtitles/<%=hItem["provider"]+'.png' %>" width="16" height="16" alt="$hItem["provider"]" title="<%=hItem["provider"].capitalize()%>"/> + #end if + #end if + #end if + </td> + <td align="center"><span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td> + </tr> +#end for + </tbody> +</table> + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home.tmpl b/data/interfaces/default/home.tmpl index c6495a31c1978f41ab373a4ce195fea28136f425..49b1d20c64ccd36bf3ba5f8d8121e87665b0ba96 100644 --- a/data/interfaces/default/home.tmpl +++ b/data/interfaces/default/home.tmpl @@ -1,230 +1,292 @@ -#import sickbeard -#import datetime -#from sickbeard.common import * -#from sickbeard import db - -#set global $title="Home" -#set global $header="Show List" - -#set global $sbPath = ".." - -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -#set $myDB = $db.DBConnection() -#set $today = str($datetime.date.today().toordinal()) -#set $downloadedEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE (status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") OR (status IN ("+",".join([str(x) for x in $Quality.SNATCHED + $Quality.SNATCHED_PROPER])+") AND location != '')) AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") -#set $allEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]])+")) AND airdate <= "+$today+" AND status != "+str($IGNORED)+" GROUP BY showid") -#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") - -<script type="text/javascript" charset="utf-8"> -<!-- - -\$.tablesorter.addParser({ - id: 'loadingNames', - is: function(s) { - return false; - }, - format: function(s) { - if (s.indexOf('Loading...') == 0) - return s.replace('Loading...','000'); - return (s || '').replace(/^(The|A)\s/i,''); - }, - type: 'text' -}); - -\$.tablesorter.addParser({ - id: 'quality', - is: function(s) { - return false; - }, - format: function(s) { - return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7); - }, - type: 'numeric' -}); - -\$.tablesorter.addParser({ - id: 'eps', - is: function(s) { - return false; - }, - format: function(s) { - match = s.match(/^(.*)/); - - if (match == null || match[1] == "?") - return -10; - - var nums = match[1].split(" / "); - - if (parseInt(nums[0]) === 0) - return parseInt(nums[1]); - - var finalNum = parseInt((nums[0]/nums[1])*1000)*100; - if (finalNum > 0) - finalNum += parseInt(nums[0]); - - return finalNum; - }, - type: 'numeric' -}); - -\$(document).ready(function(){ - - \$("#showListTable:has(tbody tr)").tablesorter({ - sortList: [[7,1],[1,0]], - textExtraction: { - 2: function(node) { return \$(node).find("img").attr("alt"); }, - 3: function(node) { return \$(node).find("span").text().toLowerCase(); }, - 6: function(node) { return \$(node).find("img").attr("alt"); }, - 7: function(node) { return \$(node).find("img").attr("alt"); } - }, - widgets: ['saveSort', 'zebra', 'stickyHeaders'], - headers: { - 0: { sorter: 'isoDate' }, - 1: { sorter: 'loadingNames' }, - 3: { sorter: 'quality' }, - 4: { sorter: 'eps' }, - 5: { sorter: 'eps' } - } - }); - -}); -//--> -</script> - -<table id="showListTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> - - <thead><tr><th class="nowrap">Next Ep</th><th>Show</th><th>Network</th><th>Quality</th><th>Downloads</th><th>French</th><th>Active</th><th>Audio</th><th>Status</th></tr></thead> - <tfoot> - <tr> - <th rowspan="1" colspan="1" align="center"><a href="$sbRoot/home/addShows/">Add Show</a></th> - <th rowspan="1" colspan="8"></th> - </tr> - </tfoot> - <tbody> - -#for $curLoadingShow in $sickbeard.showQueueScheduler.action.loadingShowList: - - #if $curLoadingShow.show != None and $curLoadingShow.show in $sickbeard.showList: - #continue - #end if - - <tr> - <td align="center">(loading)</td> - <td> - #if $curLoadingShow.show == None: - Loading... ($curLoadingShow.show_name) - #else: - <a href="displayShow?show=$curLoadingShow.show.tvdbid">$curLoadingShow.show.name</a> - #end if - </td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - </tr> -#end for - -#set $myShowList = $list($sickbeard.showList) -$myShowList.sort(lambda x, y: cmp(x.name, y.name)) -#for $curShow in $myShowList: -#set $curEp = $curShow.nextEpisode() - -#set $curShowDownloads = [x[1] for x in $downloadedEps if int(x[0]) == $curShow.tvdbid] -#set $curfr = [x[1] for x in $fr if int(x[0]) == $curShow.tvdbid] -#set $curShowAll = [x[1] for x in $allEps if int(x[0]) == $curShow.tvdbid] -#if len($curShowAll) != 0: - #if len($curShowDownloads) != 0: - #set $dlStat = str($curShowDownloads[0])+" / "+str($curShowAll[0]) - #set $nom = $curShowDownloads[0] - #set $den = $curShowAll[0] - #else - #set $dlStat = "0 / "+str($curShowAll[0]) - #set $nom = 0 - #set $den = $curShowAll[0] - #end if -#else - #set $dlStat = "?" - #set $nom = 0 - #set $den = 1 -#end if -#if len($curShowDownloads) != 0: - #if len($curfr) != 0: - #set $frStat = str($curfr[0])+" / "+str($curShowDownloads[0]) - #set $nomfr = $curfr[0] - #set $denfr = $curShowDownloads[0] - #else - #set $frStat = "0 / "+str($curShowDownloads[0]) - #set $nomfr = 0 - #set $denfr = $curShowDownloads[0] - #end if -#else - #set $frStat = "0 / 0" - #set $nomfr = 0 - #set $denfr = 1 -#end if - - - <tr> - <td align="center" class="nowrap">#if len($curEp) != 0 then $curEp[0].airdate else ""#</td> - <td class="tvShow"> - #if $sickbeard.DISPLAY_POSTERS: - <img height="75" src="$sbRoot/showPoster/?show=$curShow.tvdbid&which=poster" alt="$curShow.name"/> - #end if - <a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a> - </td> - <td align="center"> - #if $curShow.network: - <img id="network" width="60" height="30" src="$sbRoot/images/network/${curShow.network.lower()}.png" alt="$curShow.network" title="$curShow.network" /> - #else: - <img id="network" width="60" height="30" src="$sbRoot/images/network/nonetwork.png" alt="No Network" title="No Network" /> - #end if - </td> -#if $curShow.quality in $qualityPresets: - <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> -#else: - <td align="center"><span class="quality Custom">Custom</span></td> -#end if - <td align="center"><span style="display: none;">$dlStat</span><div id="progressbar$curShow.tvdbid" style="position:relative;"></div> - <script type="text/javascript"> - <!-- - \$(function() { - \$("\#progressbar$curShow.tvdbid").progressbar({ - value: parseInt($nom) * 100 / parseInt($den) - }); - \$("\#progressbar$curShow.tvdbid").append( "<div class='progressbarText'>$dlStat</div>" ) - }); - //--> - </script> - </td> - <td align="center"><span style="display: none;">$frStat</span><div id="progressbar2$curShow.tvdbid" style="position:relative;"></div> - <script type="text/javascript"> - <!-- - \$(function() { - \$("\#progressbar2$curShow.tvdbid").progressbar({ - value: parseInt($nomfr) * 100 / parseInt($denfr) - }); - \$("\#progressbar2$curShow.tvdbid").append( "<div class='progressbarText'>$frStat</div>" ) - }); - //--> - </script> - </td> - <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 0 and $curShow.status != "Ended" then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> - <td align="center"> - <img src="$sbRoot/images/flags/${curShow.audio_lang}.png" alt="$curShow.audio_lang" width="16" /> - </td> - <td align="center">$curShow.status</td> - </tr> - - -#end for -</tbody> -</table> - -<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import datetime +#from sickbeard.common import * +#from sickbeard import db + +#set global $title="Home" +#set global $header="Show List" + +#set global $sbPath = ".." + +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +#set $myDB = $db.DBConnection() +#set $today = str($datetime.date.today().toordinal()) +#set $downloadedEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE (status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") OR (status IN ("+",".join([str(x) for x in $Quality.SNATCHED + $Quality.SNATCHED_PROPER])+") AND location != '')) AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $allEps = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]])+")) AND airdate <= "+$today+" AND status != "+str($IGNORED)+" GROUP BY showid") +#set $fr = $myDB.select("SELECT showid, COUNT(*) FROM tv_episodes WHERE audio_langs = 'fr' AND location != '' AND season != 0 and episode != 0 AND airdate <= "+$today+" GROUP BY showid") +#set $layout = $sickbeard.HOME_LAYOUT +#set $sort = $sickbeard.SORT_ARTICLE + +<script type="text/javascript" charset="utf-8"> +<!-- + +\$.tablesorter.addParser({ + id: 'loadingNames', + is: function(s) { + return false; + }, + format: function(s) { + if (s.indexOf('Loading...') == 0) + return s.replace('Loading...','000'); + #if $sort != 1: + return (s || '').replace(/^(The|A|An|Un|Une\Le|La)\s/i,''); + #else: + return (s || ''); + #end if + }, + type: 'text' +}); + +\$.tablesorter.addParser({ + id: 'quality', + is: function(s) { + return false; + }, + format: function(s) { + return s.replace('HD1080p',5).replace('HD720p',4).replace('HD',3).replace('SD',2).replace('Any',1).replace('Best',0).replace('Custom',7); + }, + type: 'numeric' +}); + +\$.tablesorter.addParser({ + id: 'eps', + is: function(s) { + return false; + }, + format: function(s) { + match = s.match(/^(.*)/); + + if (match == null || match[1] == "?") + return -10; + + var nums = match[1].split(" / "); + + if (parseInt(nums[0]) === 0) + return parseInt(nums[1]); + + var finalNum = parseInt((nums[0]/nums[1])*1000)*100; + if (finalNum > 0) + finalNum += parseInt(nums[0]); + + return finalNum; + }, + type: 'numeric' +}); + +\$(document).ready(function(){ + + \$("img#network").on('error', function(){ + \$(this).parent().text(\$(this).attr('alt')); + \$(this).remove(); + }); + + \$("#showListTable:has(tbody tr)").tablesorter({ + + sortList: [[7,0],[2,0]], + textExtraction: { + #if ( $layout == 'poster'): + 1: function(node) { return \$(node).find("img").attr("alt"); }, + #end if + #if ( $layout != 'simple'): + 3: function(node) { return \$(node).find("img").attr("alt"); }, + #end if + 4: function(node) { return \$(node).find("span").text(); }, + 7: function(node) { return \$(node).find("img").attr("alt"); }, + 8: function(node) { return \$(node).find("img").attr("alt"); } + }, + widgets: ['saveSort', 'zebra'], + headers: { + 0: { sorter: 'isoDate' }, + #if ($layout == 'poster'): + 1: {sorter: 'loadingNames'}, + #end if + 2: { sorter: 'loadingNames' }, + 4: { sorter: 'quality' }, + 5: { sorter: 'eps' }, + 6: { sorter: 'eps' }, + } + }); + +}); + +//--> +</script> + +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<div id="HomeLayout" style="float: right; margin-top: -25px;"> + <span><b>Layout:</b> + <a class="inner" href="$sbRoot/setHomeLayout/?layout=poster" style="font-size: 12px;">Poster</a> · + <a class="inner" href="$sbRoot/setHomeLayout/?layout=banner" style="font-size: 12px;">Banner</a> · + <a class="inner" href="$sbRoot/setHomeLayout/?layout=simple" style="font-size: 12px;">Simple</a> + </span> +</div> + +<table id="showListTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> + + <thead><tr><th class="nowrap">Next Ep</th>#if $layout=="poster" then "<th>Poster</th>" else "<th style='display: none;'></th>"#<th>Show</th><th>Network</th><th>Quality</th><th>Downloads</th><th>French</th><th>Active</th><th>Audio</th><th>Status</th></tr></thead> + <tfoot> + <tr> + <th rowspan="1" colspan="1" align="center"><a href="$sbRoot/home/addShows/">  Add Show</a></th> + <th rowspan="1" colspan="9"></th> + </tr> + </tfoot> + <tbody> + +#for $curLoadingShow in $sickbeard.showQueueScheduler.action.loadingShowList: + + #if $curLoadingShow.show != None and $curLoadingShow.show in $sickbeard.showList: + #continue + #end if + + <tr> + <td align="center">(loading)</td> + <td></td> + <td> + #if $curLoadingShow.show == None: + Loading... ($curLoadingShow.show_name) + #else: + <a href="displayShow?show=$curLoadingShow.show.tvdbid">$curLoadingShow.show.name</a> + #end if + </td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + </tr> +#end for + +#set $myShowList = $list($sickbeard.showList) +$myShowList.sort(lambda x, y: cmp(x.name, y.name)) +#for $curShow in $myShowList: +#set $curEp = $curShow.nextEpisode() + +#set $curShowDownloads = [x[1] for x in $downloadedEps if int(x[0]) == $curShow.tvdbid] +#set $curfr = [x[1] for x in $fr if int(x[0]) == $curShow.tvdbid] +#set $curShowAll = [x[1] for x in $allEps if int(x[0]) == $curShow.tvdbid] +#if len($curShowAll) != 0: + #if len($curShowDownloads) != 0: + #set $dlStat = str($curShowDownloads[0])+" / "+str($curShowAll[0]) + #set $nom = $curShowDownloads[0] + #set $den = $curShowAll[0] + #else + #set $dlStat = "0 / "+str($curShowAll[0]) + #set $nom = 0 + #set $den = $curShowAll[0] + #end if +#else + #set $dlStat = "?" + #set $nom = 0 + #set $den = 1 +#end if +#if len($curShowDownloads) != 0: + #if len($curfr) != 0: + #set $frStat = str($curfr[0])+" / "+str($curShowDownloads[0]) + #set $nomfr = $curfr[0] + #set $denfr = $curShowDownloads[0] + #else + #set $frStat = "0 / "+str($curShowDownloads[0]) + #set $nomfr = 0 + #set $denfr = $curShowDownloads[0] + #end if +#else + #set $frStat = "0 / 0" + #set $nomfr = 0 + #set $denfr = 1 +#end if + +#set $which_thumb = $layout+"_thumb" + + <tr> + <td align="center" class="nowrap" style="color: #555555;font-weight:bold;">#if len($curEp) != 0 then $curEp[0].airdate else ""#</td> + #if $layout == 'poster': + <td> + <div class="imgHomeWrapper $layout"> + <a href="$sbRoot/showPoster/?show=$curShow.tvdbid&which=$layout" rel="dialog" title="$curShow.name"> + <img src="$sbRoot/showPoster/?show=$curShow.tvdbid&which=$which_thumb" class="$layout" alt="$curShow.name"/> + </a> + </div> + </td> + <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> + #else if $layout == 'banner': + <td style="display: none;"/> + <td> + <span style="display: none;">$curShow.name</span> + <div class="imgHomeWrapper imgHomeWrapperRounded $layout"> + <a href="$sbRoot/home/displayShow?show=$curShow.tvdbid"> + <img src="$sbRoot/showPoster/?show=$curShow.tvdbid&which=$layout" class="$layout" alt="$curShow.name" title="$curShow.name"/> + </div> + </td> + #else if $layout == 'simple': + <td style="display: none;"/> + <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> + #end if + <!--<td align="center">$curShow.network</td>//--> + <td align="center" style="color: #555555;font-weight:bold;"> + #if $layout != 'simple': + #if $curShow.network: + <img id="network" width="44.8" height="22.4" src="$sbRoot/images/network/${curShow.network.lower()}.png" alt="$curShow.network" title="$curShow.network" /> + #else: + <img id="network" width="44.8" height="22.4" src="$sbRoot/images/network/nonetwork.png" alt="No Network" title="No Network" /> + #end if + #else: + $curShow.network + #end if + </td> + +#if $curShow.quality in $qualityPresets: + <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> +#else: + <td align="center"><span class="quality Custom">Custom</span></td> +#end if + <td align="center"><span style="display: none;">$dlStat</span><div id="progressbar$curShow.tvdbid" style="position:relative;"></div> + <script type="text/javascript"> + <!-- + \$(function() { + \$("\#progressbar$curShow.tvdbid").progressbar({ + value: parseInt($nom) * 100 / parseInt($den) / 1000 }); + \$("\#progressbar$curShow.tvdbid").append( "<div class='progressbarText'>$dlStat</div>" ) + var progressBarWidth = parseInt($nom) * 100 / parseInt($den) + "%" + \$("\#progressbar$curShow.tvdbid > .ui-progressbar-value").animate({ width: progressBarWidth }, 1000); + }); + //--> + </script> + </td> + <td align="center"><span style="display: none;">$frStat</span><div id="progressbar2$curShow.tvdbid" style="position:relative;"></div> + <script type="text/javascript"> + <!-- + \$(function() { + \$("\#progressbar2$curShow.tvdbid").progressbar({ + value: parseInt($nomfr) * 100 / parseInt($denfr)/1000 + }); + \$("\#progressbar2$curShow.tvdbid").append( "<div class='progressbarText'>$frStat</div>" ) + var progressBar2Width = parseInt($nom) * 100 / parseInt($den) + "%" + \$("\#progressbar2$curShow.tvdbid > .ui-progressbar-value").animate({ width: progressBar2Width }, 1000); + }); + //--> + </script> + </td> + <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 0 and $curShow.status != "Ended" then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> + <td align="center"> + <img src="$sbRoot/images/flags/${curShow.audio_lang}.png" alt="$curShow.audio_lang" width="16" /> + </td> + <td align="center" style="color: #555555; font-weight: bold;">$curShow.status</td> + </tr> + + +#end for +</tbody> +</table> + +<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home_addExistingShow.tmpl b/data/interfaces/default/home_addExistingShow.tmpl index 38c4fc6d0f65bef78a714f04b04d6cd9406d8290..8db39002d8bbc8459cdc6f4785858783cb9952d8 100644 --- a/data/interfaces/default/home_addExistingShow.tmpl +++ b/data/interfaces/default/home_addExistingShow.tmpl @@ -1,62 +1,67 @@ -#import os.path -#import sickbeard -#from sickbeard.common import * -#set global $title="Existing Show" - -#set global $sbPath="../.." - -#set global $statpath="../.."# -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> - -<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/addExistingShow.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> - -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - \$( "#tabs" ).tabs({ - collapsible: true, - selected: #if $sickbeard.ROOT_DIRS then '-1' else '0'# - }); -}); -//--> -</script> - -<div id="tabs"> - <ul> - <li><a href="#tabs-1">Manage Directories</a></li> - <li><a href="#tabs-2">Customize Options</a></li> - </ul> - <div id="tabs-1"> - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_rootDirs.tmpl") - </div> - <div id="tabs-2"> - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") - </div> -</div> -<br /> - -<p class="align-left">Sick Beard can add existing shows, using the current options, by using locally stored NFO/XML metadata to eliminate user interaction.<br /> -If you would rather have Sick Beard prompt you to customize each show, then use the checkbox below.</p> -<p class="align-left"> - <input class="cb" type="checkbox" name="promptForSettings" id="promptForSettings" /> <label for="promptForSettings">Prompt me to set settings for each show</label> -</p> - -<hr /> - -<h4>Displaying folders within these directories which aren't already added to Sick Beard:</h4><br/> -<ul id="rootDirStaticList"><li></li></ul> - -<br /> -<div id="tableDiv"></div> -<br /> - -<input class="btn btn-primary" type="button" value="Submit" id="submitShowDirs" /> -</form> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import os.path +#import sickbeard +#from sickbeard.common import * +#set global $title="Existing Show" +#set global $header="Existing Show" + +#set global $sbPath="../.." + +#set global $statpath="../.."# +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> + +<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/addExistingShow.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> + +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + \$( "#tabs" ).tabs({ + collapsible: true, + selected: #if $sickbeard.ROOT_DIRS then '-1' else '0'# + }); +}); +//--> +</script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<div id="tabs"> + <ul> + <li><a href="#tabs-1">Manage Directories</a></li> + <li><a href="#tabs-2">Customize Options</a></li> + </ul> + <div id="tabs-1"> + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_rootDirs.tmpl") + </div> + <div id="tabs-2"> + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") + </div> +</div> +<br /> + +<p class="align-left">Sick Beard can add existing shows, using the current options, by using locally stored NFO/XML metadata to eliminate user interaction.<br /> +If you would rather have Sick Beard prompt you to customize each show, then use the checkbox below.</p> +<br /> +<p class="align-left"><input type="checkbox" name="promptForSettings" id="promptForSettings" /> <label for="promptForSettings">Prompt me to set settings for each show</label></p> +<br /> +<hr /> +<br /> +<h4>Displaying folders within these directories which aren't already added to Sick Beard:</h4> +<br /> +<ul id="rootDirStaticList"><li></li></ul> + +<br /> +<div id="tableDiv"></div> +<br /> +<input class="btn btn-primary" type="button" value="Submit" id="submitShowDirs" /> +</form> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home_addShows.tmpl b/data/interfaces/default/home_addShows.tmpl index 25f952eae1c00bb32bfb275cbe45662d98271fdd..74fc2b54e142eb642c44c553953810e5dd1053ce 100644 --- a/data/interfaces/default/home_addShows.tmpl +++ b/data/interfaces/default/home_addShows.tmpl @@ -1,36 +1,54 @@ -#import os.path -#import urllib -#import sickbeard -#set global $title="Add Show" - -#set global $sbPath="../.." - -#set global $statpath="../.."# -#set global $topmenu="home"# -#import os.path - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<div id="addShowPortal"> - - <a href="$sbRoot/home/addShows/newShow" id="btnNewShow" class="btn btn-large"> - <div class="button"><img src="$sbRoot/images/add-new32.png" alt="Add New Show"/></div> - <div class="buttontext"> - <h2>Add New Show</h2> - <p>For shows that you haven't downloaded yet, this option finds a show on theTVDB.com, creates a directory for its episodes, and adds it to Sick Beard.</p> - </div> - </a> - - <br/><br/> - - <a href="$sbRoot/home/addShows/existingShows" id="btnExistingShow" class="btn btn-large"> - <div class="button"><img src="$sbRoot/images/add-existing32.png" alt="Add Existing Shows"/></div> - <div class="buttontext"> - <h2>Add Existing Shows</h2> - <p>Use this option to add shows that already have a folder created on your hard drive. Sick Beard will scan your existing metadata/episodes and add the show accordingly.</p> - </div> - </a> - -</div> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import os.path +#import urllib +#import sickbeard +#set global $title="" +#set global $header="Add Show" + +#set global $sbPath="../.." + +#set global $statpath="../.."# +#set global $topmenu="home"# +#import os.path + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + + \$('#btnNewShow').click(function() { + window.location = '$sbRoot/home/addShows/newShow'; + }); + \$('#btnExistingShow').click(function() { + window.location = '$sbRoot/home/addShows/existingShows'; + }); + +}); +//--> +</script> + +<div id="addShowPortal" class="clearfix"> + + <button id="btnNewShow" class="ui-button"> + <div class="button"><img src="$sbRoot/images/add-new32.png" alt="Add New Show"/></div> + <div class="buttontext"> + <h2>Add New Show</h2> + <p>For shows that you haven't downloaded yet, this option finds a show on theTVDB.com, creates a directory for its episodes, and adds it to Sick Beard.</p> + </div> + </button> + + <button id="btnExistingShow" class="ui-button"> + <div class="button"><img src="$sbRoot/images/add-existing32.png" alt="Add Existing Shows"/></div> + <div class="buttontext"> + <h2>Add Existing Shows</h2> + <p>Use this option to add shows that already have a folder created on your hard drive. Sick Beard will scan your existing metadata/episodes and add the show accordingly.</p> + </div> + </button> + +</div> + +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home_massAddTable.tmpl b/data/interfaces/default/home_massAddTable.tmpl index d299fa301020f5b1059189d252864aedcf488809..fc47705f7c6cbf0c60ad9eeb053dd53b0884c9e1 100644 --- a/data/interfaces/default/home_massAddTable.tmpl +++ b/data/interfaces/default/home_massAddTable.tmpl @@ -1,26 +1,26 @@ -<table id="addRootDirTable" class="tablesorter"> - <thead><tr><th width="1%"><input type="checkbox" id="checkAll" checked=checked></th><th>Directory</th><th width="20%">Show Name (tvshow.nfo)</td></tr></thead> - <tfoot> - <tr> - <th rowspan="1" colspan="3" align="left"><a href="#" style="padding-left: 10px;" class="showManage pull-left">Manage Directories</a></th> - </tr> - </tfoot> - <tbody> -#for $curDir in $dirList: -#if $curDir['added_already']: -#continue -#end if - -#set $show_id = $curDir['dir'] -#if $curDir['existing_info'][0]: -#set $show_id = $show_id + '|' + $str($curDir['existing_info'][0]) + '|' + $curDir['existing_info'][1] -#end if - <tr> - <td><input type="checkbox" id="$show_id" class="dirCheck" checked=checked></td> - <td><label for="$show_id">$curDir['display_dir']</label></td> - <td>#if $curDir['existing_info'][0] and $curDir['existing_info'][1] then '<a href="http://thetvdb.com/?tab=series&id='+$str($curDir['existing_info'][0])+'">'+$curDir['existing_info'][1]+'</a>' else "?"#</td> - </tr> -#end for - </tbody> -</tbody> -</table> +<table id="addRootDirTable" class="sickbeardTable tablesorter"> + <thead><tr><th width="1%"><input type="checkbox" id="checkAll" checked=checked></th><th>Directory</th><th width="20%">Show Name (tvshow.nfo)</td></tr></thead> + <tfoot> + <tr> + <th rowspan="1" colspan="3" align="left"><a href="#" style="padding-left: 10px;" class="showManage">Manage Directories</a></th> + </tr> + </tfoot> + <tbody> +#for $curDir in $dirList: +#if $curDir['added_already']: +#continue +#end if + +#set $show_id = $curDir['dir'] +#if $curDir['existing_info'][0]: +#set $show_id = $show_id + '|' + $str($curDir['existing_info'][0]) + '|' + $curDir['existing_info'][1] +#end if + <tr> + <td><input type="checkbox" id="$show_id" class="dirCheck" checked=checked></td> + <td><label for="$show_id">$curDir['display_dir']</label></td> + <td>#if $curDir['existing_info'][0] and $curDir['existing_info'][1] then '<a href="http://thetvdb.com/?tab=series&id='+$str($curDir['existing_info'][0])+'">'+$curDir['existing_info'][1]+'</a>' else "?"#</td> + </tr> +#end for + </tbody> +</tbody> +</table> diff --git a/data/interfaces/default/home_newShow.tmpl b/data/interfaces/default/home_newShow.tmpl index 729a731a928732bc8d730c965adb52cc18e574f4..8929b6b09cccc3fd474cbaf07e9bd8d4e007bd12 100644 --- a/data/interfaces/default/home_newShow.tmpl +++ b/data/interfaces/default/home_newShow.tmpl @@ -1,6 +1,7 @@ #import os.path #import sickbeard -#set global $title="New Show" +#set global $header="New Show" +#set global $title="" #set global $sbPath="../.." @@ -10,11 +11,16 @@ #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") -<script type="text/javascript" src="$sbRoot/js/lib/formwizard.js?$sbPID"></script> +<link rel="stylesheet" type="text/css" href="$sbRoot/css/formwizard.css?$sbPID" /> +<script type="text/javascript" src="$sbRoot/js/formwizard.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/newShow.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> - +<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script> + #if $varExists('header') + <h1 class="header">$header</h1> + #else + <h1 class="title">$title</h1> + #end if <div id="displayText">aoeu</div> <br /> @@ -30,17 +36,18 @@ <input type="hidden" name="whichSeries" value="$provided_tvdb_id" /> <input type="hidden" id="providedName" value="$provided_tvdb_name" /> #else: - <input type="text" id="nameToSearch" value="$default_show_name" /> - <select name="tvdbLang" id="tvdbLangSelect"> + <input type="text" id="nameToSearch" value="$default_show_name" style="margin-top: 1px;" /> + <select name="tvdbLang" id="tvdbLangSelect" style="height: 26px;margin-top: 1px;"> <option value="fr" selected="selected">fr</option> </select><b>*</b> - <input type="button" id="searchName" value="Search" class="btn" /><br /><br /> - + <input class="btn" type="button" id="searchName" value="Search" style="margin-top: -3px;" /><br /><br /> + <b>*</b> This will only affect the language of the retrieved metadata file contents and episode filenames.<br /> This <b>DOES NOT</b> allow Sick Beard to download non-english TV episodes!<br /> <br /> - <div id="searchResults" style="max-height: 225px; overflow: auto;"><br/></div> + <div id="searchResults" style="height: 225px; overflow: auto;"><br/></div> #end if + </div> </fieldset> @@ -59,10 +66,9 @@ <fieldset class="sectionwrap"> <legend class="legendStep">Customize options</legend> - - <div class="stepDiv"> - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") - </div> + <div class="stepDiv"> + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_addShowOptions.tmpl") + </div> </fieldset> #for $curNextDir in $other_shows: @@ -82,4 +88,4 @@ <script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#include $os.path.join($sickbeard.PROG_DIR,"data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/home_postprocess.tmpl b/data/interfaces/default/home_postprocess.tmpl index eedeac157786baa65eae6bcd7d7cd148a43dfea7..0f8bdfce1914193f3fad8b7c786e3fd60ac8bb35 100644 --- a/data/interfaces/default/home_postprocess.tmpl +++ b/data/interfaces/default/home_postprocess.tmpl @@ -1,21 +1,26 @@ -#import sickbeard -#set global $title="Post Processing" - -#set global $sbPath="../.." - -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<form name="processForm" method="post" action="processEpisode"> -Enter the folder containing the episode: <input type="text" name="dir" id="episodeDir" size="50" /> <input type="submit" class="btn" value="Process" /> -</form> -<br /> - -<script type="text/javascript" charset="utf-8"> -<!-- - jQuery('#episodeDir').fileBrowser({ title: 'Select Unprocessed Episode Folder', key: 'postprocessPath' }); -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#set global $header="Post Processing" +#set global $title="" + +#set global $sbPath="../.." + +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<form name="processForm" method="post" action="processEpisode"> +Enter the folder containing the episode: <input type="text" name="dir" id="episodeDir" size="50" /> <input class="btn" type="submit" value="Process" /> +</form> +<br /> + +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#episodeDir').fileBrowser({ title: 'Select Unprocessed Episode Folder', key: 'postprocessPath' }); +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/inc_addShowOptions.tmpl b/data/interfaces/default/inc_addShowOptions.tmpl index 66b720b7354c37146dc611574d09365ba6205992..58bb82c58442fa94accd8fb624f4925ef433ac3c 100644 --- a/data/interfaces/default/inc_addShowOptions.tmpl +++ b/data/interfaces/default/inc_addShowOptions.tmpl @@ -1,63 +1,61 @@ -#import sickbeard -#from sickbeard import common -#from sickbeard.common import * -#from sickbeard import subtitles - - #if $sickbeard.USE_SUBTITLES: - <div class="field-pair alt"> - <input type="checkbox" name="subtitles" id="subtitles" #if $sickbeard.SUBTITLES_DEFAULT then "checked=\"checked\"" else ""# /> - <label for="subtitles" class="clearfix"> - <span class="component-title">Subtitles</span> - <span class="component-desc">Download subtitles for this show?</span> - </label> - </div> - #end if - - <div class="field-pair"> - <label for="statusSelect" class="nocheck clearfix"> - <span class="component-title"> - <select name="defaultStatus" id="statusSelect"> - #for $curStatus in [$SKIPPED, $WANTED, $ARCHIVED, $IGNORED]: - <option value="$curStatus" #if $sickbeard.STATUS_DEFAULT == $curStatus then 'selected="selected"' else ''#>$statusStrings[$curStatus]</option> - #end for - </select> - </span> - <span class="component-desc">Set the initial status of missing episodes</span> - </label> - </div> - - <div class="field-pair alt"> - <input class="cb" type="checkbox" name="flatten_folders" id="flatten_folders" #if $sickbeard.FLATTEN_FOLDERS_DEFAULT then "checked=\"checked\"" else ""# /> - <label for="flatten_folders" class="clearfix"> - <span class="component-title">Flatten Folders</span> - <span class="component-desc">Disregard sub-folders?</span> - </label> - </div> - - - #set $qualities = $Quality.splitQuality($sickbeard.QUALITY_DEFAULT) - #set global $anyQualities = $qualities[0] - #set global $bestQualities = $qualities[1] - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") - <div class="field-pair"> - <label for="showLangSelect" class="nocheck clearfix"> - <span class="component-title"> - <select name="audio_lang" id="showLangSelect"> - #for $k,$v in $common.showLanguages.iteritems(): - #if k!="": - <option value="$k" - #if $sickbeard.AUDIO_SHOW_DEFAULT == $k then 'selected="selected"' else ''#>$v</option> - #end if - #end for - </select> - </span> - <span class="component-desc">Set the desired audio language of the show (this can be changed later)</span> - </label> - </div> - - <div class="field-pair alt"> - <label for="saveDefaultsButton" class="nocheck clearfix"> - <span class="component-title"><input class="btn" type="button" id="saveDefaultsButton" value="Save Defaults" disabled="disabled" /></span> - <span class="component-desc">Persist current values as the defaults</span> - </label> - </div> +#import sickbeard +#from sickbeard import common +#from sickbeard.common import * +#from sickbeard import subtitles + + #if $sickbeard.USE_SUBTITLES: + <div class="field-pair alt"> + <input type="checkbox" name="subtitles" id="subtitles" #if $sickbeard.SUBTITLES_DEFAULT then "checked=\"checked\"" else ""# /> + <label for="subtitles" class="clearfix"> + <span class="component-title">Subtitles</span> + <span class="component-desc">Download subtitles for this show?</span> + </label> + </div> + #end if + + <div class="field-pair"> + <label for="statusSelect" class="nocheck clearfix"> + <span class="component-title"> + <select name="defaultStatus" id="statusSelect"> + #for $curStatus in [$SKIPPED, $WANTED, $ARCHIVED, $IGNORED]: + <option value="$curStatus" #if $sickbeard.STATUS_DEFAULT == $curStatus then 'selected="selected"' else ''#>$statusStrings[$curStatus]</option> + #end for + </select> + </span> + <span class="component-desc">Set the initial status of missing episodes</span> + </label> + </div> + + <div class="field-pair alt"> + <input class="cb" type="checkbox" name="flatten_folders" id="flatten_folders" #if $sickbeard.FLATTEN_FOLDERS_DEFAULT then "checked=\"checked\"" else ""# /> + <label for="flatten_folders" class="clearfix"> + <span class="component-title">Flatten Folders</span> + <span class="component-desc">Disregard sub-folders?</span> + </label> + </div> + + #set $qualities = $Quality.splitQuality($sickbeard.QUALITY_DEFAULT) + #set global $anyQualities = $qualities[0] + #set global $bestQualities = $qualities[1] + #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_qualityChooser.tmpl") + <div class="field-pair"> + <label for="showLangSelect" class="nocheck clearfix"> + <span class="component-title"> + <select name="audio_lang" id="showLangSelect"> + #for $k,$v in $common.showLanguages.iteritems(): + #if k!="": + <option value="$k" + #if $sickbeard.AUDIO_SHOW_DEFAULT == $k then 'selected="selected"' else ''#>$v</option> + #end if + #end for + </select> + </span> + <span class="component-desc">Set the desired audio language of the show (this can be changed later)</span> + </label> + </div> + <div class="field-pair alt"> + <label for="saveDefaultsButton" class="nocheck clearfix"> + <span class="component-title"><input class="btn" type="button" id="saveDefaultsButton" value="Save Defaults" disabled="disabled" /></span> + <span class="component-desc">Persist current values as the defaults</span> + </label> + </div> diff --git a/data/interfaces/default/inc_bottom.tmpl b/data/interfaces/default/inc_bottom.tmpl index 6989c20b157a9be398774e99bc245fed7dc120b8..d4264115b69d1e5d9467bc8e065e48d157b7b4b0 100644 --- a/data/interfaces/default/inc_bottom.tmpl +++ b/data/interfaces/default/inc_bottom.tmpl @@ -2,25 +2,31 @@ #import datetime #from sickbeard import db #from sickbeard.common import * - </div> <!-- /content --> -</div> <!-- /contentWrapper --> - -<footer> - <div class="container footer"> - #set $myDB = $db.DBConnection() - #set $today = str($datetime.date.today().toordinal()) - #set $numShows = len($sickbeard.showList) - #set $numGoodShows = len([x for x in $sickbeard.showList if x.paused == 0 and x.status != "Ended"]) - #set $numDLEpisodes = $myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") AND season != 0 and episode != 0 AND airdate <= " + $today + "")[0][0] - #set $numEpisodes = $myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]]) + ")) AND airdate <= " + $today + " AND status != " + str($IGNORED) + "")[0][0] - <b>$numShows shows</b> ($numGoodShows active) | <b>$numDLEpisodes/$numEpisodes</b> episodes downloaded - <br /> - <b>Search</b>: <%=str(sickbeard.currentSearchScheduler.timeLeft()).split('.')[0]%> | - <b>Backlog</b>: $sickbeard.backlogSearchScheduler.nextRun().strftime("%a %b %d").decode($sickbeard.SYS_ENCODING) - <br /> - Modifed by sarakha63 with the help of mozvip, based on midgetspy's work - </div> <!-- /container footer --> -</footer> + </div> +</div> +<div class="footer clearfix"> +<div class="meta" style="float:left;font-size: 12px;"> +#set $myDB = $db.DBConnection() +#set $today = str($datetime.date.today().toordinal()) +#set $numShows = len($sickbeard.showList) +#set $numGoodShows = len([x for x in $sickbeard.showList if x.paused == 0 and x.status != "Ended"]) +#set $numDLEpisodes = $myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE status IN ("+",".join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]])+") AND season != 0 and episode != 0 AND airdate <= "+$today+"")[0][0] +#set $numEpisodes = $myDB.select("SELECT COUNT(*) FROM tv_episodes WHERE season != 0 and episode != 0 AND (airdate != 1 OR status IN ("+",".join([str(x) for x in ($Quality.DOWNLOADED + $Quality.SNATCHED + $Quality.SNATCHED_PROPER) + [$ARCHIVED]])+")) AND airdate <= "+$today+" AND status != "+str($IGNORED)+"")[0][0] +<b>$numShows shows</b> ($numGoodShows active) | <b>$numDLEpisodes/$numEpisodes</b> episodes downloaded | +<b>Search</b>: <%=str(sickbeard.currentSearchScheduler.timeLeft()).split('.')[0]%> | +<!--<b>Update</b>: <a%a=str(sickbeard.updateScheduler.timeLeft()).split('.')[0]%> | //--> +<b>Backlog</b>: $sickbeard.backlogSearchScheduler.nextRun().strftime("%a %b %d").decode($sickbeard.SYS_ENCODING) +</div> +<ul style="float:right;"> + <li> + <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L2EWZMTRPW2SE" target="_blank"><img src="$sbRoot/images/like.png">Like this interface!</a> +</li> + <li><a href="https://github.com/sarakha63/Sick-Beard/commits/master" onclick="window.open(this.href); return false;"><img src="$sbRoot/images/menu/github-16.png" alt="" title="git" /></i>Changelog</a></li> + <li><a href="$sbRoot/manage/manageSearches/forceVersionCheck"><img src="$sbRoot/images/menu/update16.png" alt="" width="16" height="16" />Update Check</a></li> + <li><a href="$sbRoot/home/restart/?pid=$sbPID" class="confirm"><img src="$sbRoot/images/menu/restart16.png" alt="" width="16" height="16" />Restart</a></li> + <li><a href="$sbRoot/home/shutdown/?pid=$sbPID" class="confirm"><img src="$sbRoot/images/menu/shutdown16.png" alt="" width="16" height="16" />Shutdown</a></li> +</ul> +</div> </body> </html> diff --git a/data/interfaces/default/inc_qualityChooser.tmpl b/data/interfaces/default/inc_qualityChooser.tmpl index 5956b97b24d4c8f31c856d25e02dac53b5be10c1..6a0e91e6f5668a25707ea9ae0e43f9c0d3c71b9b 100644 --- a/data/interfaces/default/inc_qualityChooser.tmpl +++ b/data/interfaces/default/inc_qualityChooser.tmpl @@ -1,47 +1,47 @@ -#import sickbeard -#from sickbeard.common import Quality, qualityPresets, qualityPresetStrings - -<div class="field-pair"> - <label for="qualityPreset" class="nocheck clearfix"> -#set $overall_quality = $Quality.combineQualities($anyQualities, $bestQualities) -<span class="component-title"> -#set $selected = None -<select id="qualityPreset"> -<option value="0">Custom</option> -#for $curPreset in sorted($qualityPresets, reverse=True): -<option value="$curPreset" #if $curPreset == $overall_quality then "selected=\"selected\"" else ""# #if $qualityPresetStrings[$curPreset].endswith("0p") then "style=\"padding-left: 15px;\"" else ""#>$qualityPresetStrings[$curPreset]</option> -#end for -</select> -</span> -<span class="component-desc">Preferred quality of episodes to be download</span> - - </label> -</div> - -<div id="customQualityWrapper"> - <div id="customQuality"> - <div class="component-group-desc"> - <p>One of the <b>Initial</b> quality selections must be obtained before Sick Beard will attempt to process the <b>Archive</b> selections.</p> - </div> - - <div style="padding-right: 40px; text-align: center;" class="float-left"> - <h4>Initial</h4> - #set $anyQualityList = filter(lambda x: x > $Quality.NONE, $Quality.qualityStrings) - <select id="anyQualities" name="anyQualities" multiple="multiple" size="$len($anyQualityList)"> - #for $curQuality in sorted($anyQualityList): - <option value="$curQuality" #if $curQuality in $anyQualities then "selected=\"selected\"" else ""#>$Quality.qualityStrings[$curQuality]</option> - #end for - </select> - </div> - - <div style="text-align: center;" class="float-left"> - <h4>Archive</h4> - #set $bestQualityList = filter(lambda x: x > $Quality.SDTV and x < $Quality.UNKNOWN, $Quality.qualityStrings) - <select id="bestQualities" name="bestQualities" multiple="multiple" size="$len($bestQualityList)"> - #for $curQuality in sorted($bestQualityList): - <option value="$curQuality" #if $curQuality in $bestQualities then "selected=\"selected\"" else ""#>$Quality.qualityStrings[$curQuality]</option> - #end for - </select> - </div> - </div> -</div><br /> +#import sickbeard +#from sickbeard.common import Quality, qualityPresets, qualityPresetStrings + +<div class="field-pair"> + <label for="qualityPreset" class="nocheck clearfix"> +#set $overall_quality = $Quality.combineQualities($anyQualities, $bestQualities) +<span class="component-title"> +#set $selected = None +<select id="qualityPreset"> +<option value="0">Custom</option> +#for $curPreset in sorted($qualityPresets): +<option value="$curPreset" #if $curPreset == $overall_quality then "selected=\"selected\"" else ""# #if $qualityPresetStrings[$curPreset].endswith("0p") then "style=\"padding-left: 15px;\"" else ""#>$qualityPresetStrings[$curPreset]</option> +#end for +</select> +</span> +<span class="component-desc">Preferred quality of episodes to be download</span> + + </label> +</div> + +<div id="customQualityWrapper"> + <div id="customQuality"> + <div class="component-group-desc"> + <p>One of the <b>Initial</b> quality selections must be obtained before Sick Beard will attempt to process the <b>Archive</b> selections.</p> + </div> + + <div style="padding-right: 40px; text-align: center;" class="float-left"> + <h4>Initial</h4> + #set $anyQualityList = filter(lambda x: x > $Quality.NONE, $Quality.qualityStrings) + <select id="anyQualities" name="anyQualities" multiple="multiple" size="$len($anyQualityList)"> + #for $curQuality in sorted($anyQualityList): + <option value="$curQuality" #if $curQuality in $anyQualities then "selected=\"selected\"" else ""#>$Quality.qualityStrings[$curQuality]</option> + #end for + </select> + </div> + + <div style="text-align: center;" class="float-left"> + <h4>Archive</h4> + #set $bestQualityList = filter(lambda x: x > $Quality.SDTV and x < $Quality.UNKNOWN, $Quality.qualityStrings) + <select id="bestQualities" name="bestQualities" multiple="multiple" size="$len($bestQualityList)"> + #for $curQuality in sorted($bestQualityList): + <option value="$curQuality" #if $curQuality in $bestQualities then "selected=\"selected\"" else ""#>$Quality.qualityStrings[$curQuality]</option> + #end for + </select> + </div> + </div> +</div> diff --git a/data/interfaces/default/inc_rootDirs.tmpl b/data/interfaces/default/inc_rootDirs.tmpl index 27f995daab9316e69ab4f0630c3f52321a9963a5..fcd2ed5cb9cb82e872201e26f321de5faf94ce98 100644 --- a/data/interfaces/default/inc_rootDirs.tmpl +++ b/data/interfaces/default/inc_rootDirs.tmpl @@ -1,28 +1,28 @@ -#import sickbeard -<span id="sampleRootDir"></span> - -#if $sickbeard.ROOT_DIRS: -#set $backend_pieces = $sickbeard.ROOT_DIRS.split('|') -#set $backend_default = 'rd-' + $backend_pieces[0] -#set $backend_dirs = $backend_pieces[1:] -#else: -#set $backend_default = '' -#set $backend_dirs = [] -#end if - -<input type="hidden" id="whichDefaultRootDir" value="$backend_default" /> -<div style="padding: 10px 0 5px;"> - <select name="rootDir" id="rootDirs" size="6" style="min-width: 500px;"> - #for $cur_dir in $backend_dirs: - <option value="$cur_dir">$cur_dir</option> - #end for - </select> -</div> -<div id="rootDirsControls" style="text-align: center; width: 500px;"> - <input class="btn" type="button" id="addRootDir" value="New" /> - <input class="btn" type="button" id="editRootDir" value="Edit" /> - <input class="btn" type="button" id="deleteRootDir" value="Delete" /> - <input class="btn" type="button" id="defaultRootDir" value="Set as Default *" /> -</div> -<input type="text" style="display: none" id="rootDirText" /> -<br /> +#import sickbeard +<span id="sampleRootDir"></span> + +#if $sickbeard.ROOT_DIRS: +#set $backend_pieces = $sickbeard.ROOT_DIRS.split('|') +#set $backend_default = 'rd-' + $backend_pieces[0] +#set $backend_dirs = $backend_pieces[1:] +#else: +#set $backend_default = '' +#set $backend_dirs = [] +#end if + +<input type="hidden" id="whichDefaultRootDir" value="$backend_default" /> +<div style="padding: 10px 0 5px;"> + <select name="rootDir" id="rootDirs" size="6" style="min-width: 500px;"> + #for $cur_dir in $backend_dirs: + <option value="$cur_dir">$cur_dir</option> + #end for + </select> +</div> +<div id="rootDirsControls" style="text-align: center; width: 500px;"> + <input class="btn" type="button" id="addRootDir" value="New" /> + <input class="btn" type="button" id="editRootDir" value="Edit" /> + <input class="btn" type="button" id="deleteRootDir" value="Delete" /> + <input class="btn" type="button" id="defaultRootDir" value="Set as Default *" /> +</div> +<input type="text" style="display: none" id="rootDirText" /> +<br /> diff --git a/data/interfaces/default/inc_top.tmpl b/data/interfaces/default/inc_top.tmpl index e219b876f6538d2e32cca87d03d3a417dd00c3f5..2842e68db241a7bf70bd63e8370328eb02c2b18d 100644 --- a/data/interfaces/default/inc_top.tmpl +++ b/data/interfaces/default/inc_top.tmpl @@ -1,72 +1,51 @@ #import sickbeard.version -<!DOCTYPE HTML> -<html> - <head> - <meta charset="utf-8"> - <title>Sick Beard - $sickbeard.version.SICKBEARD_VERSION - $title</title> - <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <meta name="robots" content="noindex"> - <!--[if lt IE 9]> - <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> - <![endif]--> +#import sickbeard +#import urllib + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + <meta name="robots" content="noindex"> + <title>Sick Beard - alpha $sickbeard.version.SICKBEARD_VERSION - $title</title> + <!--[if lt IE 9]> + <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> + <![endif]--> + + <link rel="shortcut icon" href="$sbRoot/images/ico/favicon.ico"> + <link rel="apple-touch-icon-precomposed" sizes="144x144" href="$sbRoot/images/ico/apple-touch-icon-144x144-precomposed.png"> + <link rel="apple-touch-icon-precomposed" sizes="114x114" href="$sbRoot/images/ico/apple-touch-icon-114x114-precomposed.png"> + <link rel="apple-touch-icon-precomposed" sizes="72x72" href="$sbRoot/images/ico/apple-touch-icon-72x72-precomposed.png"> + <link rel="apple-touch-icon-precomposed" href="$sbRoot/images/ico/apple-touch-icon-57x57-precomposed.png"> + + <link rel="stylesheet" type="text/css" href="$sbRoot/css/default.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/browser.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/comingEpisodes.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/config.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery.pnotify.default.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery-ui-1.8.17.custom.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/superfish.css?$sbPID" /> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/tablesorter.css?$sbPID"/> + <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery.qtip2.css?$sbPID"/> + <link rel="stylesheet" type="text/css" media="only screen and (max-device-width: 480px)" href="$sbRoot/css/iphone.css?$sbPID" /> - <link rel="shortcut icon" href="$sbRoot/images/ico/favicon.ico"> - <link rel="apple-touch-icon-precomposed" sizes="144x144" href="$sbRoot/images/ico/apple-touch-icon-144x144-precomposed.png"> - <link rel="apple-touch-icon-precomposed" sizes="114x114" href="$sbRoot/images/ico/apple-touch-icon-114x114-precomposed.png"> - <link rel="apple-touch-icon-precomposed" sizes="72x72" href="$sbRoot/images/ico/apple-touch-icon-72x72-precomposed.png"> - <link rel="apple-touch-icon-precomposed" href="$sbRoot/images/ico/apple-touch-icon-57x57-precomposed.png"> - - <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/bootstrap.css?$sbPID"/> - <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery.pnotify.default.css?$sbPID" /> - <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery-ui-1.8.24.custom.css?$sbPID" /> - <link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/jquery.qtip.css?$sbPID"/> - <link rel="stylesheet" type="text/css" href="$sbRoot/css/style.css?$sbPID"/> <style type="text/css"> <!-- -#contentWrapper { background: url("$sbRoot/images/bg.png") repeat fixed 0 0 transparent; } - -[class^="icon-"], [class*=" icon-"] { background-image: url("$sbRoot/images/glyphicons-halflings.png"); } -.icon-white { background-image: url("$sbRoot/images/glyphicons-halflings-white.png"); } -.dropdown-menu li > a:hover > [class^="icon-"], .dropdown-menu li > a:hover > [class*=" icon-"] { background-image: url("$sbRoot/images/glyphicons-halflings-white.png"); } -[class^="icon16-"], [class*=" icon16-"] { background-image: url("$sbRoot/images/glyphicons-config.png"); } - -.ui-autocomplete-loading { background: white url("$sbRoot/images/loading16.gif") right center no-repeat; } -.browserDialog.busy .ui-dialog-buttonpane { background: url("$sbRoot/images/loading.gif") 10px 50% no-repeat !important; } -.ui-dialog, .ui-dialog-buttonpane { background: #eceadf url("$sbRoot/css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png") 50% 50% repeat !important; } -.ui-accordion-content, .ui-tabs-panel { background: #ededed !important; background-image: none !important; } - -.ui-widget-content { border: 1px solid #aaaaaa; background: #dcdcdc url("$sbRoot/css/lib/images/ui-bg_highlight-soft_75_dcdcdc_1x100.png") 50% top repeat-x; color: #222222; } -.ui-widget-header { border: 1px solid #aaaaaa; background: #ffffff url("$sbRoot/css/lib/images/ui-bg_flat_0_ffffff_40x100.png") 50% 50% repeat-x; color: #222222; font-weight: bold; } - -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aaaaaa; background: #efefef url("$sbRoot/css/lib/images/ui-bg_highlight-soft_75_efefef_1x100.png") 50% 50% repeat-x; font-weight: bold; color: #222222; } -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dddddd url("$sbRoot/css/lib/images/ui-bg_highlight-soft_75_dddddd_1x100.png") 50% 50% repeat-x; font-weight: bold; color: #222222; } -.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #dfdfdf url("$sbRoot/css/lib/images/ui-bg_inset-soft_75_dfdfdf_1x100.png") 50% 50% repeat-x; font-weight: bold; color: #140f06; } - -.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #aaaaaa; background: #fbf9ee url("$sbRoot/css/lib/images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x; color: #363636; } -.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #aaaaaa; background: #fef1ec url("$sbRoot/css/lib/images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x; color: #8c291d; } - -.ui-icon { width: 16px; height: 16px; background-image: url("$sbRoot/css/lib/images/ui-icons_222222_256x240.png"); } -.ui-widget-content .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_222222_256x240.png"); } -.ui-widget-header .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_222222_256x240.png"); } -.ui-state-default .ui-icon { background-image: url("$sbRoot/css/lib/images/ui-icons_8c291d_256x240.png"); } -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_222222_256x240.png"); } -.ui-state-active .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_8c291d_256x240.png"); } -.ui-state-highlight .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_2e83ff_256x240.png"); } -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url("$sbRoot/css/lib/images/ui-icons_cd0a0a_256x240.png"); } - -.ui-widget-overlay { background: #aaaaaa url("$sbRoot/css/lib/images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x; opacity: .35;filter:Alpha(Opacity=35); } -.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #000000 url("$sbRoot/css/lib/images/ui-bg_flat_0_000000_40x100.png") 50% 50% repeat-x; opacity: .35;filter:Alpha(Opacity=35); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; } - +.sf-sub-indicator { background: url("$sbRoot/images/arrows.png") no-repeat -10px -100px; } +.sf-shadow ul { background: url("$sbRoot/images/shadow.png") no-repeat bottom right; } #if $sickbeard.NEWEST_VERSION_STRING: .ui-pnotify { top: 30px !important; } #end if //--> </style> - <script type="text/javascript" src="$sbRoot/js/lib/jquery-1.7.2.min.js?$sbPID"></script> + <script type="text/javascript" src="$sbRoot/js/lib/jquery-1.8.3.min.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery-ui-1.8.24.custom.min.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/lib/bootstrap.min.js?$sbPID"></script> + <script type="text/javascript" src="$sbRoot/js/lib/superfish-1.4.8.js?$sbPID"></script> + <script type="text/javascript" src="$sbRoot/js/lib/supersubs-0.2b.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.cookie.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.cookiejar.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.json-2.2.min.js?$sbPID"></script> @@ -74,9 +53,10 @@ <script type="text/javascript" src="$sbRoot/js/lib/jquery.tablesorter-2.1.19.min.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.tablesorter.widgets.min.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.qtip-2012-04-26.min.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/lib/jquery.pnotify-1.2.0.min.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/lib/jquery.expand-1.3.8.js?$sbPID"></script> + <script type="text/javascript" src="$sbRoot/js/lib/jquery.pnotify-1.0.2.min.js"></script> + <script type="text/javascript" src="$sbRoot/js/lib/jquery.expand-1.3.8.js?$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/lib/jquery.form-2.92.js?$sbPID"></script> + <script type="text/javascript" src="$sbRoot/js/script.js?$sbPID"></script> <script type="text/javascript" charset="utf-8"> <!-- @@ -85,29 +65,94 @@ top_image_html = '<img src="$sbRoot/images/top.gif" style="width:31px; height:11px" alt="Jump to top" />'; //--> </script> - <script type="text/javascript" src="$sbRoot/js/lib/jquery.scrolltopcontrol-1.1.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/browser.js?$sbPID"></script> - <script type="text/javascript" src="$sbRoot/js/ajaxNotifications.js?$sbPID"></script> - <script type="text/javascript"> - <!-- - \$(document).ready(function(){ - \$("#NAV$topmenu").addClass("active"); + <script type="text/javascript" src="$sbRoot/js/lib/jquery.scrolltopcontrol-1.1.js"></script> + <script type="text/javascript" src="$sbRoot/js/browser.js"></script> + <script type="text/javascript" src="$sbRoot/js/ajaxNotifications.js"></script> + +<script type="text/javascript"> +<!-- + function initActions() { + \$("#SubMenu a[href^='/home/restart/']").addClass('btn').html('<span class="ui-icon ui-icon-power pull-left"></span> Restart </a>'); + \$("#SubMenu a[href^='/home/shutdown/']").addClass('btn').html('<span class="ui-icon ui-icon-power pull-left"></span> Shutdown </a>'); + \$("#SubMenu a:contains('Edit')").addClass('btn').html('<span class="ui-icon ui-icon-pencil pull-left"></span> Edit </a>'); + \$("#SubMenu a:contains('Delete')").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Delete </a>'); + \$("#SubMenu a:contains('Clear History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear History </a>'); + \$("#SubMenu a:contains('Trim History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trim History </a>'); + \$("#SubMenu a:contains('Trunc Episode Links')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode Links </a>'); + \$("#SubMenu a[href='/errorlogs/clearerrors']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors </a>'); + \$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan </a>'); + \$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Backlog Overview </a>'); + \$("#SubMenu a[href='/home/updatePLEX/']").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Update PLEX </a>'); + \$("#SubMenu a:contains('Force')").addClass('btn').html('<span class="ui-icon ui-icon-transfer-e-w pull-left"></span> Force Full Update </a>'); + \$("#SubMenu a:contains('Rename')").addClass('btn').html('<span class="ui-icon ui-icon-tag pull-left"></span> Preview Rename </a>'); + \$("#SubMenu a[href='/config/subtitles/']").addClass('btn').html('<span class="ui-icon ui-icon-comment pull-left"></span> Search Subtitles </a>'); + \$("#SubMenu a[href^='/home/subtitleShow']").addClass('btn').html('<span class="ui-icon ui-icon-comment pull-left"></span> Download Subtitles </a>'); + \$("#SubMenu a:contains('Settings')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Settings </a>'); + \$("#SubMenu a:contains('Provider')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Providers </a>'); + \$("#SubMenu a:contains('General')").addClass('btn').html('<span class="ui-icon ui-icon-gear pull-left"></span> General </a>'); + \$("#SubMenu a:contains('Episode Status')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Episode Status Management </a>'); + \$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Missed Subtitles </a>'); + \$("#SubMenu a[href='/home/addShows/']").addClass('btn').html('<span class="ui-icon ui-icon-video pull-left"></span> Add Show </a>'); + \$("#SubMenu a:contains('Processing')").addClass('btn').html('<span class="ui-icon ui-icon-folder-open pull-left"></span> Post-Processing </a>'); + \$("#SubMenu a:contains('Manage Searches')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Manage Searches </a>'); + \$("#SubMenu a:contains('Notification')").addClass('btn').html('<span class="ui-icon ui-icon-note pull-left"></span> Notification </a>'); + \$("#SubMenu a:contains('Update show in XBMC')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Update show in XBMC </a>'); + \$("#SubMenu a[href='/home/updateXBMC/']").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Update XBMC </a>'); + + } + \$(document).ready(function(){ + + initActions(); + \$("ul.sf-menu").supersubs({ + minWidth: 12, // minimum width of sub-menus in em units + maxWidth: 27, // maximum width of sub-menus in em units + extraWidth: 1 // extra width can ensure lines don't sometimes turn over + // due to slight rounding differences and font-family + }).superfish({ + delay: 150, + disableHI: true, + animation: {opacity:'show',height:'show'}, + speed: 'fast', + dropShadows: false + }); + \$("#MainMenu.sf-menu ul li a").mouseover(function(){ + imgsrc = \$(this).children("img").attr("src"); + if(imgsrc!=null) { + matches = imgsrc.match(/_over/); + if (!matches) { + imgsrcON = imgsrc.replace(/.png$/ig,"_over.png"); + \$(this).children("img").attr("src", imgsrcON); + } + \$("#MainMenu.sf-menu ul li a").mouseout(function(){ + \$(this).children("img").attr("src", imgsrc); + }); + } + }); + \$("#MainMenu.sf-menu ul li img").each(function() { + rollsrc = \$(this).attr("src"); + rollON = rollsrc.replace(/.png$/ig,"_over.png"); + \$("<img>").attr("src", rollON); + }); - \$("a.confirm").bind("click",function(e) { - e.preventDefault(); - var target = \$( this ).attr('href'); - if ( confirm("Are you sure you want to " + \$(this).text() + "?") ) - location.href = target; - return false; - }); + \$("#NAV$topmenu").addClass("current"); + \$("a.confirm").bind("click",function(e) { + \$('#MainMenu.sf-menu').hideSuperfishUl(); + e.preventDefault(); + var target = \$( this ).attr('href'); + if ( confirm("Are you sure you want to " + \$(this).text() + "?") ) + location.href = target; + return false; }); - //--> - </script> + + }); + + +//--> +</script> </head> <body> - <header> #if $sickbeard.NEWEST_VERSION_STRING: <div id="upgrade-notification"> <div> @@ -117,140 +162,74 @@ <div id="header-fix"></div> #end if <div id="header"> + <div class="wrapper"> <a name="top"></a> - <span id="logo"><a href="$sbRoot/home/" title="Sick Beard homepage"><img alt="Sick Beard" src="$sbRoot/images/sickbeard.png" width="150" height="90" /></a></span> - <span id="versiontext"><a>$sickbeard.version.SICKBEARD_VERSION</a></span> -</div> - - <div class="navbar"> - <div class="navbar-inner"> - <div class="container"> - <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </a> - - <!-- - <a class="brand" href="$sbRoot/home/" title="Sick Beard"><img alt="Sick Beard" src="$sbRoot/images/sickbeard.png" width="150" height="72" /></a> - --> - - <div class="nav-collapse"> - <ul class="nav"> - <li class="divider-vertical"></li> - <li id="NAVsystem" class="dropdown"> - <a data-toggle="dropdown" class="dropdown-toggle" href="#" onclick="return false;"><img src="$sbRoot/images/menu/system18.png" alt="" width="18" height="18" /> <b class="caret"></b></a> - <ul class="dropdown-menu"> - <li><a href="https://github.com/sarakha63/Sick-Beard/commits/master" onclick="window.open(this.href); return false;"><img class="notifier-icon" src="$sbRoot/images/menu/github-16.png" alt="" title="git" /></i> See Changelog</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/manage/manageSearches/forceVersionCheck"><i class="icon-check"></i> Force Version Check</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/home/restart/?pid=$sbPID" class="confirm"><i class="icon-repeat"></i> Restart</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/home/shutdown/?pid=$sbPID" class="confirm"><i class="icon-off"></i> Shutdown</a></li> - </ul> - </li> - <li class="divider-vertical"></li> - <li id="NAVhome" class="dropdown"> - <a data-toggle="dropdown" class="dropdown-toggle disabled" href="$sbRoot/home/">Home <b class="caret"></b></a> - <ul class="dropdown-menu"> - <li><a href="$sbRoot/home/"><i class="icon-home"></i> Home</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/home/addShows/"><i class="icon-plus-sign"></i> Add Shows</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/home/postprocess/"><i class="icon-random"></i> Manual Post-Processing</a></li> - </ul> - </li> - <li class="divider-vertical"></li> - <li id="NAVcomingEpisodes"> - <a href="$sbRoot/comingEpisodes/">Coming Episodes</a> - </li> - <li class="divider-vertical"></li> - <li id="NAVhistory"> - <a href="$sbRoot/history/">History</a> - </li> - <li class="divider-vertical"></li> - <li id="NAVmanage" class="dropdown"> - <a data-toggle="dropdown" class="dropdown-toggle disabled" href="$sbRoot/manage/">Manage <b class="caret"></b></a> - <ul class="dropdown-menu"> - <li><a href="$sbRoot/manage/"><i class="icon-tasks"></i> Mass Update</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/manage/backlogOverview/"><i class="icon-retweet"></i> Backlog Overview</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/manage/manageSearches/"><i class="icon-search"></i> Manage Searches</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/manage/episodeStatuses/"><i class="icon-list-alt"></i> Episode Status Management</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/manage/subtitleMissed/"><i class="icon-list-alt"></i> Manage Missed Subtitles</a></li> - <li class="divider"></li> - </ul> - </li> - <li class="divider-vertical"></li> - <li id="NAVconfig" class="dropdown"> - <a data-toggle="dropdown" class="dropdown-toggle disabled" href="$sbRoot/config/">Config <b class="caret"></b></a> - <ul class="dropdown-menu"> - <li><a href="$sbRoot/config/"><i class="icon-question-sign"></i> Help & Info</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/general/"><i class="icon-cog"></i> General</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/search/"><i class="icon-cog"></i> Search Settings</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/providers/"><i class="icon-cog"></i> Search Providers</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/postProcessing/"><i class="icon-cog"></i> Post Processing</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/subtitles/"><i class="icon-cog"></i> Subtitles Settings</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/config/notifications/"><i class="icon-cog"></i> Notifications</a></li> - </ul> - </li> - <li class="divider-vertical"></li> - <li id="NAVerrorlogs" class="dropdown"> - <a data-toggle="dropdown" class="dropdown-toggle disabled" href="$sbRoot/errorlogs/">$logPageTitle <b class="caret"></b></a> - <ul class="dropdown-menu"> - <li><a href="$sbRoot/errorlogs/"><i class="icon-file"></i> View Log (Errors)</a></li> - <li class="divider"></li> - <li><a href="$sbRoot/errorlogs/viewlog/"><i class="icon-file"></i> View Log</a></li> - </ul> - </li> - <li class="divider-vertical"></li> + <span id="logo"><a href="$sbRoot/home/" title="Sick Beard homepage"><img alt="Sick Beard" src="$sbRoot/images/sickbeard.png" width="150" /></a></span> + <span id="versiontext">$sickbeard.version.SICKBEARD_VERSION</span> + + <ul id="MainMenu" class="sf-menu"> + <li id="NAVhome"><a href="$sbRoot/home/">Show list</a> + <ul> + <li><a href="$sbRoot/home/addShows/"><img src="$sbRoot/images/menu/addshow16.png" alt="" width="16" height="16" />Add Shows</a></li> + <li><a href="$sbRoot/home/postprocess/"><img src="$sbRoot/images/menu/postprocess16.png" alt="" width="16" height="16" />Manual Post-Processing</a></li> </ul> - <ul class="nav pull-right"> - <li> - <a id="navDonate" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L2EWZMTRPW2SE" onclick="window.open(this.href); return false;"><img src="$sbRoot/images/paypal/btn_donate_LG.gif" alt="[donate]" height="26" width="92" /></a> - </li> + </li> + <li id="NAVcomingEpisodes"><a href="$sbRoot/comingEpisodes/">Coming Episodes</a></li> + <li id="NAVhistory"><a href="$sbRoot/history/">History</a></li> + <li id="NAVmanage"><a href="$sbRoot/manage/">Manage</a> + <ul> + <li><a href="$sbRoot/manage/"><img src="$sbRoot/images/menu/manage16.png" alt="" width="16" height="16" />Mass Update</a></li> + <li><a href="$sbRoot/manage/backlogOverview/"><img src="$sbRoot/images/menu/backlog_view16.png" alt="" width="16" height="16" />Backlog Overview</a></li> + <li><a href="$sbRoot/manage/manageSearches/"><img src="$sbRoot/images/menu/managesearches16.png" alt="" width="16" height="16" />Manage Searches</a></li> + <li><a href="$sbRoot/manage/episodeStatuses/"><img src="$sbRoot/images/menu/backlog16.png" alt="" width="16" height="16" />Episode Status Management</a></li> + #if $sickbeard.USE_SUBTITLES: + <li><a href="$sbRoot/manage/subtitleMissed/"><img src="$sbRoot/images/menu/backlog16.png" alt="" width="16" height="16" />Missed Subtitle Management</a></li> + #end if </ul> - </div><!-- /nav-collapse --> - - </div><!-- /container --> - </div><!-- /navbar-inner --> - </div><!-- /navbar --> - -#if $varExists('submenu'): - <div id="SubMenu"> - <span> - #set $first = True - #for $menuItem in $submenu: - #if 'requires' not in $menuItem or $menuItem.requires(): - #if type($menuItem.path) == dict: - #if $first then "" else "</span>| <span>"#<b>$menuItem.title</b> - #set $first = False - #set $inner_first = True - #for $cur_link in $menuItem.path: - #if $inner_first then "" else "· "#<a class="inner" href="$sbRoot/$menuItem.path[$cur_link]">$cur_link</a> - #set $inner_first = False - #end for - #else - #if $first then "" else "| "#<a href="$sbRoot/$menuItem.path" #if 'confirm' in $menuItem then "class=\"confirm\"" else "" #>$menuItem.title</a> - #set $first = False - #end if - #end if - #end for - </span> - </div> -#end if - </header> - + </li> + <li id="NAVerrorlogs"><a href="$sbRoot/errorlogs/" class="log">$logPageTitle</a> + <ul> + <li><a href="$sbRoot/errorlogs/"><img src="$sbRoot/images/menu/viewlog_errors16.png" alt="" width="16" height="16" />View Log (Errors)</a></li> + <li><a href="$sbRoot/errorlogs/viewlog/"><img src="$sbRoot/images/menu/viewlog16.png" alt="" width="16" height="16" />View Log</a></li> + </ul> + </li> + <li id="NAVconfig"><a href="$sbRoot/config/" class="config"><img src="$sbRoot/images/menu/system18.png" alt="" /></a> + <ul> + <li><a href="$sbRoot/config/"><i class="icon-question-sign" style=" margin-left: -21px;margin-right: 8px;position: absolute;"></i>Help & Info</a></li> + <li><a href="$sbRoot/config/general/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />General</a></li> + <li><a href="$sbRoot/config/search/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Settings</a></li> + <li><a href="$sbRoot/config/providers/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Providers</a></li> + <li><a href="$sbRoot/config/subtitles/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Subtitles Settings</a></li> + <li><a href="$sbRoot/config/postProcessing/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Post Processing</a></li> + <li><a href="$sbRoot/config/notifications/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Notifications</a></li> + </ul> + </li> + + <li id="donate"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L2EWZMTRPW2SE" onclick="window.open(this.href); return false;"><img src="$sbRoot/images/paypal/btn_donate_LG.gif" alt="[donate]" /></a></li> + </ul> <div id="contentWrapper"> <div id="content"> - <h1 class="title">#if $varExists('header') then $header else $title#</h1> + #if $varExists('submenu'): + <div id="SubMenu"> + <span> + #set $first = True + #for $menuItem in $submenu: + #if 'requires' not in $menuItem or $menuItem.requires(): + #if type($menuItem.path) == dict: + #if $first then "" else "</span><span>"#<b>$menuItem.title</b> + #set $first = False + #set $inner_first = True + #for $cur_link in $menuItem.path: + #if $inner_first then "" else "· "#<a class="inner" href="$sbRoot/$menuItem.path[$cur_link]">$cur_link</a> + #set $inner_first = False + #end for + #else + #if $first then "" else ""#<a href="$sbRoot/$menuItem.path" #if 'confirm' in $menuItem then "class=\"confirm\"" else "" #>$menuItem.title</a> + #set $first = False + #end if + #end if + #end for + </span> + </div> + #end if + \ No newline at end of file diff --git a/data/interfaces/default/manage.tmpl b/data/interfaces/default/manage.tmpl index 10b6ca242e950e500d4dd74bf62a50390ee2016e..7459921ed62b51bace2005c520b1134608408864 100644 --- a/data/interfaces/default/manage.tmpl +++ b/data/interfaces/default/manage.tmpl @@ -1,164 +1,173 @@ -#import sickbeard -#from sickbeard.common import * -#set global $title="Mass Update" - -#set global $sbPath="../.." - -#set global $topmenu="manage"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" charset="utf-8"> -<!-- -\$.tablesorter.addParser({ - id: 'showNames', - is: function(s) { - return false; - }, - format: function(s) { - return (s || '').replace(/^(The|A)\s/i,''); - }, - type: 'text' -}); -\$.tablesorter.addParser({ - id: 'quality', - is: function(s) { - return false; - }, - format: function(s) { - return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7); - }, - type: 'numeric' -}); - -\$(document).ready(function() -{ - \$("#massUpdateTable:has(tbody tr)").tablesorter({ - sortList: [[4,0]], - textExtraction: { - 1: function(node) { return \$(node).find("img").attr("alt"); }, - 2: function(node) { return \$(node).find("img").attr("alt"); }, - 3: function(node) { return \$(node).find("img").attr("alt"); }, - 5: function(node) { return \$(node).find("span").text().toLowerCase(); }, - 6: function(node) { return \$(node).find("img").attr("alt"); } - }, - widgets: ['zebra', 'stickyHeaders'], - headers: { - 0: { sorter: false}, - 4: { sorter: 'showNames'}, - 5: { sorter: 'quality'}, - 8: { sorter: false}, - 9: { sorter: false}, - 10: { sorter: false}, - 11: { sorter: false} - #if $sickbeard.USE_SUBTITLES - , 12: { sorter: false} -#end if - } - }); -}); -//--> -</script> -<script type="text/javascript" src="$sbRoot/js/massUpdate.js?$sbPID"></script> - -<form name="massUpdateForm" method="post" action="massUpdate"> - -<table id="massUpdateTable" class="tablesorter" cellspacing="1" border="0" cellpadding="0"> - <thead> - <tr> - <th width="1%">Edit<br/> - <input type="checkbox" class="bulkCheck" id="editCheck" /> - </th> - <th class="nowrap">Meta</th> - <th class="nowrap">Audio</th> - <th>Paused</th> - <th class="nowrap">Name</th> - <th>Quality</th> - <th>Flatten</th> - <th>Status</th> - <th width="1%">Update<br/> - <input type="checkbox" class="bulkCheck" id="updateCheck" /> - </th> - <th width="1%">Rescan<br/> - <input type="checkbox" class="bulkCheck" id="refreshCheck" /> - </th> - <th width="1%">Rename<br/> - <input type="checkbox" class="bulkCheck" id="renameCheck" /> - </th> - #if $sickbeard.USE_SUBTITLES: - <th width="1%">Sub<br/><input type="checkbox" class="bulkCheck" id="subtitleCheck" /></th> -#end if -<!-- <th>Force Metadata Regen <input type="checkbox" class="bulkCheck" id="metadataCheck" /></th>//--> - <th width="1%">Delete<br/> - <input type="checkbox" class="bulkCheck" id="deleteCheck" /> - </th> - </tr> - </thead> -<tfoot> - <tr> - <td rowspan="1" colspan="1" class="align-left"><input type="button" class="btn" value="Edit Selected" id="submitMassEdit" /></td> - <td rowspan="1" colspan="#if $sickbeard.USE_SUBTITLES then 12 else 11#" class="align-right"><input type="button" class="btn btn-primary" value="Submit" id="submitMassUpdate" /></td> - </tr> -</tfoot> - <tbody> -#set $myShowList = $sickbeard.showList -$myShowList.sort(lambda x, y: cmp(x.name, y.name)) -#for $curShow in $myShowList: -#set $curEp = $curShow.nextEpisode() -#set $curUpdate_disabled = "" -#set $curRefresh_disabled = "" -#set $curRename_disabled = "" -#set $curSubtitle_disabled = "" -#set $curDelete_disabled = "" - -#if $sickbeard.showQueueScheduler.action.isBeingUpdated($curShow) or $sickbeard.showQueueScheduler.action.isInUpdateQueue($curShow): - #set $curUpdate_disabled = "disabled=\"disabled\" " -#end if -#set $curUpdate = "<input type=\"checkbox\" class=\"updateCheck\" id=\"update-"+str($curShow.tvdbid)+"\" "+$curUpdate_disabled+"/>" -#if $sickbeard.showQueueScheduler.action.isBeingRefreshed($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow): - #set $curRefresh_disabled = "disabled=\"disabled\" " -#end if -#set $curRefresh = "<input type=\"checkbox\" class=\"refreshCheck\" id=\"refresh-"+str($curShow.tvdbid)+"\" "+$curRefresh_disabled+"/>" -#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow): - #set $curRename = "disabled=\"disabled\" " -#end if -#set $curRename = "<input type=\"checkbox\" class=\"renameCheck\" id=\"rename-"+str($curShow.tvdbid)+"\" "+$curRename_disabled+"/>" -#if not $curShow.subtitles or $sickbeard.showQueueScheduler.action.isBeingSubtitled($curShow) or $sickbeard.showQueueScheduler.action.isInSubtitleQueue($curShow): - #set $curSubtitle_disabled = "disabled=\"disabled\" " -#end if -#set $curSubtitle = "<input type=\"checkbox\" class=\"subtitleCheck\" id=\"subtitle-"+str($curShow.tvdbid)+"\" "+$curSubtitle_disabled+"/>" -#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow): - #set $curDelete = "disabled=\"disabled\" " -#end if -#set $curDelete = "<input type=\"checkbox\" class=\"deleteCheck\" id=\"delete-"+str($curShow.tvdbid)+"\" "+$curDelete_disabled+"/>" - - <tr> - <td align="center"><input type="checkbox" class="editCheck" id="edit-$curShow.tvdbid" /></td> - <td align="center"><img src="$sbRoot/images/flags/${curShow.lang}.png" width="16" height="11" alt="$curShow.lang" /></td> - <td align="center"><img src="$sbRoot/images/flags/${curShow.audio_lang}.png" alt="$curShow.audio_lang" width="16" /></td> - <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> - <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> -#if $curShow.quality in $qualityPresets: - <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> -#else: - <td align="center"><span class="quality Custom">Custom</span></td> -#end if - <td align="center"><img src="$sbRoot/images/#if int($curShow.flatten_folders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> - <td align="center">$curShow.status</td> - <td align="center">$curUpdate</td> - <td align="center">$curRefresh</td> - <td align="center">$curRename</td> - #if $sickbeard.USE_SUBTITLES: - <td align="center">$curSubtitle</td> -#end if -<!-- <td align="center"><input type="checkbox" class="metadataCheck" id="metadata-$curShow.tvdbid" /></td>//--> - <td align="center">$curDelete</td> - </tr> -#end for -</tbody> -</table> - -</form> - -<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard.common import * +#set global $title="Mass Update" +#set global $header="Mass Update" + +#set global $sbPath="../.." + +#set global $topmenu="manage" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" charset="utf-8"> +<!-- +\$.tablesorter.addParser({ + id: 'showNames', + is: function(s) { + return false; + }, + format: function(s) { + #if not $sickbeard.SORT_ARTICLE: + return (s || '').replace(/^(The|A|An|Un|Une|Le|La)\s/i,''); + #else: + return (s || ''); + #end if + }, + type: 'text' +}); +\$.tablesorter.addParser({ + id: 'quality', + is: function(s) { + return false; + }, + format: function(s) { + return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7); + }, + type: 'numeric' +}); + +\$(document).ready(function() +{ + \$("#massUpdateTable:has(tbody tr)").tablesorter({ + sortList: [[4,0]], + textExtraction: { + 1: function(node) { return \$(node).find("img").attr("alt"); }, + 2: function(node) { return \$(node).find("img").attr("alt"); }, + 3: function(node) { return \$(node).find("img").attr("alt"); }, + 5: function(node) { return \$(node).find("span").text().toLowerCase(); }, + 6: function(node) { return \$(node).find("img").attr("alt"); } + }, + widgets: ['zebra', 'stickyHeaders'], + headers: { + 0: { sorter: false}, + 4: { sorter: 'showNames'}, + 5: { sorter: 'quality'}, + 8: { sorter: false}, + 9: { sorter: false}, + 10: { sorter: false}, + 11: { sorter: false} + #if $sickbeard.USE_SUBTITLES + , 12: { sorter: false} +#end if + } + }); +}); +//--> +</script> +<script type="text/javascript" src="$sbRoot/js/massUpdate.js?$sbPID"></script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<form name="massUpdateForm" method="post" action="massUpdate"> + +<table id="massUpdateTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> + <thead> + <tr> + <th width="1%">Edit<br/> + <input type="checkbox" class="bulkCheck" id="editCheck" /> + </th> + <th class="nowrap">Meta</th> + <th class="nowrap">Audio</th> + <th>Paused</th> + <th class="nowrap">Name</th> + <th>Quality</th> + <th>Flatten</th> + <th>Status</th> + <th width="1%">Update<br/> + <input type="checkbox" class="bulkCheck" id="updateCheck" /> + </th> + <th width="1%">Rescan<br/> + <input type="checkbox" class="bulkCheck" id="refreshCheck" /> + </th> + <th width="1%">Rename<br/> + <input type="checkbox" class="bulkCheck" id="renameCheck" /> + </th> + #if $sickbeard.USE_SUBTITLES: + <th width="1%">Sub<br/><input type="checkbox" class="bulkCheck" id="subtitleCheck" /></th> +#end if +<!-- <th>Force Metadata Regen <input type="checkbox" class="bulkCheck" id="metadataCheck" /></th>//--> + <th width="1%">Delete<br/> + <input type="checkbox" class="bulkCheck" id="deleteCheck" /> + </th> + </tr> + </thead> +<tfoot> + <tr> + <td rowspan="1" colspan="1" class="align-left"><input type="button" class="btn" value="Edit Selected" id="submitMassEdit" /></td> + <td rowspan="1" colspan="#if $sickbeard.USE_SUBTITLES then 12 else 11#" class="align-right"><input type="button" class="btn btn-primary" value="Submit" id="submitMassUpdate" /></td> + </tr> +</tfoot> + <tbody> +#set $myShowList = $sickbeard.showList +$myShowList.sort(lambda x, y: cmp(x.name, y.name)) +#for $curShow in $myShowList: +#set $curEp = $curShow.nextEpisode() +#set $curUpdate_disabled = "" +#set $curRefresh_disabled = "" +#set $curRename_disabled = "" +#set $curSubtitle_disabled = "" +#set $curDelete_disabled = "" + +#if $sickbeard.showQueueScheduler.action.isBeingUpdated($curShow) or $sickbeard.showQueueScheduler.action.isInUpdateQueue($curShow): + #set $curUpdate_disabled = "disabled=\"disabled\" " +#end if +#set $curUpdate = "<input type=\"checkbox\" class=\"updateCheck\" id=\"update-"+str($curShow.tvdbid)+"\" "+$curUpdate_disabled+"/>" +#if $sickbeard.showQueueScheduler.action.isBeingRefreshed($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow): + #set $curRefresh_disabled = "disabled=\"disabled\" " +#end if +#set $curRefresh = "<input type=\"checkbox\" class=\"refreshCheck\" id=\"refresh-"+str($curShow.tvdbid)+"\" "+$curRefresh_disabled+"/>" +#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow): + #set $curRename = "disabled=\"disabled\" " +#end if +#set $curRename = "<input type=\"checkbox\" class=\"renameCheck\" id=\"rename-"+str($curShow.tvdbid)+"\" "+$curRename_disabled+"/>" +#if not $curShow.subtitles or $sickbeard.showQueueScheduler.action.isBeingSubtitled($curShow) or $sickbeard.showQueueScheduler.action.isInSubtitleQueue($curShow): + #set $curSubtitle_disabled = "disabled=\"disabled\" " +#end if +#set $curSubtitle = "<input type=\"checkbox\" class=\"subtitleCheck\" id=\"subtitle-"+str($curShow.tvdbid)+"\" "+$curSubtitle_disabled+"/>" +#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow): + #set $curDelete = "disabled=\"disabled\" " +#end if +#set $curDelete = "<input type=\"checkbox\" class=\"deleteCheck\" id=\"delete-"+str($curShow.tvdbid)+"\" "+$curDelete_disabled+"/>" + + <tr> + <td align="center"><input type="checkbox" class="editCheck" id="edit-$curShow.tvdbid" /></td> + <td align="center"><img src="$sbRoot/images/flags/${curShow.lang}.png" width="16" height="11" alt="$curShow.lang" /></td> + <td align="center"><img src="$sbRoot/images/flags/${curShow.audio_lang}.png" alt="$curShow.audio_lang" width="16" /></td> + <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> + <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td> +#if $curShow.quality in $qualityPresets: + <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td> +#else: + <td align="center"><span class="quality Custom">Custom</span></td> +#end if + <td align="center"><img src="$sbRoot/images/#if int($curShow.flatten_folders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> + <td align="center">$curShow.status</td> + <td align="center">$curUpdate</td> + <td align="center">$curRefresh</td> + <td align="center">$curRename</td> + #if $sickbeard.USE_SUBTITLES: + <td align="center">$curSubtitle</td> +#end if +<!-- <td align="center"><input type="checkbox" class="metadataCheck" id="metadata-$curShow.tvdbid" /></td>//--> + <td align="center">$curDelete</td> + </tr> +#end for +</tbody> +</table> + +</form> + +<script type="text/javascript" src="$sbRoot/js/tableClick.js?$sbPID"></script> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/manage_backlogOverview.tmpl b/data/interfaces/default/manage_backlogOverview.tmpl index 2fd271629d7d04071b39b77fe03be0895f07e562..81a4964d84094fd6556e2c90d74f8b70ebf998f0 100644 --- a/data/interfaces/default/manage_backlogOverview.tmpl +++ b/data/interfaces/default/manage_backlogOverview.tmpl @@ -1,68 +1,96 @@ -#import sickbeard -#import datetime -#from sickbeard.common import * -#set global $title="Backlog Overview" - -#set global $sbPath=".." - -#set global $topmenu="manage"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -#set $totalWanted = 0 -#set $totalQual = 0 - -#for $curShow in $sickbeard.showList: -#set $totalWanted = $totalWanted + $showCounts[$curShow.tvdbid][$Overview.WANTED] -#set $totalQual = $totalQual + $showCounts[$curShow.tvdbid][$Overview.QUAL] -#end for - -<div class="h2footer align-right"> - <span class="wanted nowrap">Wanted: <b>$totalWanted</b></span> - <span class="qual nowrap">Low Quality: <b>$totalQual</b></span> -</div><br/> - -<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> - -#for $curShow in sorted($sickbeard.showList, key=operator.attrgetter('name')): - -#if $showCounts[$curShow.tvdbid][$Overview.QUAL]+$showCounts[$curShow.tvdbid][$Overview.WANTED] == 0: -#continue -#end if - - <tr> - <td colspan="3" class="align-left"> - <br/> - <h2 class="backlogShow"> - <a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a> - </h2> - <div class="float-right clearfix"> - <label for="wanted" class="checkbox inline wanted">Wanted: <b>$showCounts[$curShow.tvdbid][$Overview.WANTED]</b></label> - <label for="qual" class="checkbox inline qual">Low Quality: <b>$showCounts[$curShow.tvdbid][$Overview.QUAL]</b></label> - <a class="btn btn-mini btn-inverse forceBacklog" href="$sbRoot/manage/backlogShow?tvdb_id=$curShow.tvdbid"><i class="icon-play-circle icon-white"></i> Force Backlog</a> - </div> - </td> - </tr> - - <tr><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th></tr> - -#for $curResult in $showSQLResults[$curShow.tvdbid]: -#set $whichStr = $str($curResult["season"]) + "x" + $str($curResult["episode"]) -#set $overview = $showCats[$curShow.tvdbid][$whichStr] -#if $overview not in ($Overview.QUAL, $Overview.WANTED): -#continue -#end if - <tr class="$Overview.overviewStrings[$showCats[$curShow.tvdbid][$whichStr]]"> - <td align="center">$whichStr</td> - <td>$curResult["name"]</td> - <td align="center" class="nowrap">#if int($curResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($curResult["airdate"]))#</td> - </tr> - -#end for - -#end for - - -</table><br /> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import datetime +#from sickbeard.common import * +#set global $title="Backlog Overview" +#set global $header="Backlog Overview" + +#set global $sbPath=".." + +#set global $topmenu="manage"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript"> +<!-- +\$(document).ready(function() +{ + \$('#pickShow').change(function(){ + var id = \$(this).val(); + if (id) { + \$('html,body').animate({scrollTop: \$("#show-"+id).offset().top -25},'slow'); + } + }); +}); +//--> +</script> + +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +#set $totalWanted = 0 +#set $totalQual = 0 + +#for $curShow in $sickbeard.showList: +#set $totalWanted = $totalWanted + $showCounts[$curShow.tvdbid][$Overview.WANTED] +#set $totalQual = $totalQual + $showCounts[$curShow.tvdbid][$Overview.QUAL] +#end for + +<div class="h2footer align-right"> + <span class="wanted nowrap">Wanted: <b>$totalWanted</b></span> + <span class="qual nowrap">Low Quality: <b>$totalQual</b></span> +</div><br/> + +<div class="float-left"> +Jump to Show + <select id="pickShow"> + #for $curShow in sorted($sickbeard.showList, key=operator.attrgetter('name')): + #if $showCounts[$curShow.tvdbid][$Overview.QUAL]+$showCounts[$curShow.tvdbid][$Overview.WANTED] != 0: + <option value="$curShow.tvdbid">$curShow.name</option> + #end if + #end for +</select> +</div> + +<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> + +#for $curShow in sorted($sickbeard.showList, key=operator.attrgetter('name')): + +#if $showCounts[$curShow.tvdbid][$Overview.QUAL]+$showCounts[$curShow.tvdbid][$Overview.WANTED] == 0: +#continue +#end if + + <tr class="seasonheader" id="show-$curShow.tvdbid"> + <td colspan="3" class="align-left"> + <br/><h2><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></h2> + <div class="float-right"> + <span class="wanted nowrap">Wanted: <b>$showCounts[$curShow.tvdbid][$Overview.WANTED]</b></span> + <span class="qual nowrap">Low Quality: <b>$showCounts[$curShow.tvdbid][$Overview.QUAL]</b></span> + <a class="btn btn-mini btn-inverse forceBacklog" href="$sbRoot/manage/backlogShow?tvdb_id=$curShow.tvdbid"><i class="icon-play-circle icon-white"></i> Force Backlog</a> + </div> + </td> + </tr> + + <tr><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th></tr> + +#for $curResult in $showSQLResults[$curShow.tvdbid]: +#set $whichStr = $str($curResult["season"]) + "x" + $str($curResult["episode"]) +#set $overview = $showCats[$curShow.tvdbid][$whichStr] +#if $overview not in ($Overview.QUAL, $Overview.WANTED): +#continue +#end if + <tr class="$Overview.overviewStrings[$showCats[$curShow.tvdbid][$whichStr]]"> + <td align="center">$whichStr</td> + <td>$curResult["name"]</td> + <td align="center" class="nowrap">#if int($curResult["airdate"]) == 1 then "never" else $datetime.date.fromordinal(int($curResult["airdate"]))#</td> + </tr> + +#end for + +#end for + + +</table> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/manage_episodeStatuses.tmpl b/data/interfaces/default/manage_episodeStatuses.tmpl index 0fb282ddb5dc8405b4994813178326c6347dfcd5..4d987460caaad55c50438bb47382635715f486b2 100644 --- a/data/interfaces/default/manage_episodeStatuses.tmpl +++ b/data/interfaces/default/manage_episodeStatuses.tmpl @@ -1,72 +1,82 @@ -#import sickbeard -#import datetime -#from sickbeard import common -#set global $title="Episode Overview" - -#set global $sbPath=".." - -#set global $topmenu="manage"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -#if not $whichStatus or ($whichStatus and not $ep_counts): - -#if $whichStatus: -<h3>None of your episodes have status $common.statusStrings[$int($whichStatus)]</h3> -<br /> -#end if - -<form action="$sbRoot/manage/episodeStatuses" method="get"> -Manage episodes with status <select name="whichStatus"> -#for $curStatus in [$common.SKIPPED, $common.SNATCHED, $common.WANTED, $common.ARCHIVED, $common.IGNORED]: -<option value="$curStatus">$common.statusStrings[$curStatus]</option> -#end for -</select> -<input class="btn" type="submit" value="Manage" /> -</form> - -#else - -<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?$sbPID"></script> - -<form action="$sbRoot/manage/changeEpisodeStatuses" method="post"> -<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus" /> - -<h3>Shows containing $common.statusStrings[$int($whichStatus)] episodes</h3> - -<br /> - -#if $whichStatus in ($common.ARCHIVED, $common.IGNORED, $common.SNATCHED): -#set $row_class = "good" -#else -#set $row_class = $common.Overview.overviewStrings[$whichStatus] -#end if -<input type="hidden" id="row_class" value="$row_class" /> - -Set checked shows/episodes to <select name="newStatus"> -#set $statusList = [$common.SKIPPED, $common.WANTED, $common.ARCHIVED, $common.IGNORED] -#if $int($whichStatus) in $statusList -$statusList.remove($int($whichStatus)) -#end if - -#for $curStatus in $statusList: -<option value="$curStatus">$common.statusStrings[$curStatus]</option> -#end for -</select> -<input class="btn" type="submit" value="Go" /> - -<br /><br /><br /> - -<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> -#for $cur_tvdb_id in $sorted_show_ids: - <tr id="$cur_tvdb_id"> - <th><input type="checkbox" class="allCheck" id="allCheck-$cur_tvdb_id" name="$cur_tvdb_id-all" checked="checked" /></th> - <th colspan="2" style="width: 100%; text-align: left;"><a class="whitelink" href="$sbRoot/home/displayShow?show=$cur_tvdb_id">$show_names[$cur_tvdb_id]</a> ($ep_counts[$cur_tvdb_id]) <input type="button" class="btn get_more_eps" id="$cur_tvdb_id" value="Expand" /></th> - </tr> -#end for -</table> -</form> - -#end if - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#import datetime +#from sickbeard import common +#set global $title="Episode Overview" +#set global $header="Episode Overview" + +#set global $sbPath=".." + +#set global $topmenu="manage"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +#if not $whichStatus or ($whichStatus and not $ep_counts): + +#if $whichStatus: +<h2>None of your episodes have status $common.statusStrings[$int($whichStatus)]</h2> +<br /> +#end if + +<form action="$sbRoot/manage/episodeStatuses" method="get"> +Manage episodes with status <select name="whichStatus"> +#for $curStatus in [$common.SKIPPED, $common.SNATCHED, $common.WANTED, $common.ARCHIVED, $common.IGNORED]: +<option value="$curStatus">$common.statusStrings[$curStatus]</option> +#end for +</select> +<input class="btn" type="submit" value="Manage" /> +</form> + +#else + +<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?$sbPID"></script> + +<form action="$sbRoot/manage/changeEpisodeStatuses" method="post"> +<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus" /> + +<h2>Shows containing $common.statusStrings[$int($whichStatus)] episodes</h2> + +<br /> + +#if $whichStatus in ($common.ARCHIVED, $common.IGNORED, $common.SNATCHED): +#set $row_class = "good" +#else +#set $row_class = $common.Overview.overviewStrings[$whichStatus] +#end if +<input type="hidden" id="row_class" value="$row_class" /> + +Set checked shows/episodes to <select name="newStatus"> +#set $statusList = [$common.SKIPPED, $common.WANTED, $common.ARCHIVED, $common.IGNORED] +#if $int($whichStatus) in $statusList +$statusList.remove($int($whichStatus)) +#end if + +#for $curStatus in $statusList: +<option value="$curStatus">$common.statusStrings[$curStatus]</option> +#end for +</select> +<input class="btn" type="submit" value="Go" /> + +<div> + <button type="button" class="btn btn-mini selectAllShows" style="line-height: 8px;"><a>Select all</a></button> + <button type="button" class="btn btn-mini unselectAllShows" style="line-height: 8px;"><a>Clear all</a></button> +</div> +<br /> + +<table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> +#for $cur_tvdb_id in $sorted_show_ids: + <tr id="$cur_tvdb_id"> + <th><input type="checkbox" class="allCheck" id="allCheck-$cur_tvdb_id" name="$cur_tvdb_id-all" checked="checked" /></th> + <th colspan="2" style="width: 100%; text-align: left;"><a class="whitelink" href="$sbRoot/home/displayShow?show=$cur_tvdb_id">$show_names[$cur_tvdb_id]</a> ($ep_counts[$cur_tvdb_id]) <input type="button" class="get_more_eps btn" id="$cur_tvdb_id" value="Expand" /></th> + </tr> +#end for +</table> +</form> + +#end if + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/manage_manageSearches.tmpl b/data/interfaces/default/manage_manageSearches.tmpl index e8e19a0f4d6701d15ccd55a5e2ecb46a816fe42f..164b4af74ea6f26241c48b985a4515492cd15d89 100644 --- a/data/interfaces/default/manage_manageSearches.tmpl +++ b/data/interfaces/default/manage_manageSearches.tmpl @@ -2,7 +2,7 @@ #import datetime #from sickbeard.common import * #set global $title="Manage Searches" - +#set global $header="Manage Searches" #set global $sbPath=".." #set global $topmenu="manage"# @@ -10,7 +10,11 @@ #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") <script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script> - + #if $varExists('header') + <h1 class="header">$header</h1> + #else + <h1 class="title">$title</h1> + #end if <h3>Backlog Search:</h3> <a class="btn" href="$sbRoot/manage/manageSearches/pauseBacklog?paused=#if $backlogPaused then "0" else "1"#"><i class="#if $backlogPaused then "icon-play" else "icon-pause"#"></i> #if $backlogPaused then "Unpause" else "Pause"#</a> @@ -35,6 +39,4 @@ In Progress<br /> <a class="btn" href="$sbRoot/manage/manageSearches/forceVersionCheck"><i class="icon-check"></i> Force Check</a> <br /> -<br /> - #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/manage_massEdit.tmpl b/data/interfaces/default/manage_massEdit.tmpl index 96789c19e479a2a474f46fbb1be884e927dff671..7a9b44b543a1ce1e7d164355eb848f3eb7a87b2a 100644 --- a/data/interfaces/default/manage_massEdit.tmpl +++ b/data/interfaces/default/manage_massEdit.tmpl @@ -1,173 +1,185 @@ -#import sickbeard -#from sickbeard import common -#from sickbeard import exceptions -#set global $title="Mass Edit" -#set global $header="Mass Edit" - -#set global $sbPath=".." - -#set global $topmenu="manage"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") -#if $quality_value != None: -#set $initial_quality = int($quality_value) -#else: -#set $initial_quality = $common.SD -#end if -#set $anyQualities, $bestQualities = $common.Quality.splitQuality($initial_quality) -<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/massEdit.js?$sbPID"></script> -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - - \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { - var resultStr = ''; - - if (data.results.length == 0) { - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${lang_value}.png)"'; - resultStr = '<option value="$lang_value" selected="selected" + flag>$lang_value</option>'; - } else { - var current_lang_added = false; - \$.each(data.results, function(index, obj) { - - if (obj == "$lang_value") { - selected = ' selected="selected"'; - current_lang_added = true; - } - else { - selected = ''; - } - - flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; - resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; - }); - if (!current_lang_added) - resultStr += '<option value="$lang_value" selected="selected">$lang_value</option>'; - - } - \$('#tvdbLangSelect').html(resultStr) - - }); - -}); -//--> -</script> -<form action="massEditSubmit" method="post"> -<input type="hidden" name="toEdit" value="$showList" /> - -<div class="optionWrapper clearfix"> -<span class="selectTitle">Root Directories <span class="separator">*</span></span><br /> - #for $cur_dir in $root_dir_list: - #set $cur_index = $root_dir_list.index($cur_dir) - <div style="padding: 6px 0 3px 25px;"> - <input class="btn edit_root_dir" type="button" id="edit_root_dir_$cur_index" value="Edit" /> - $cur_dir => <span id="display_new_root_dir_$cur_index">$cur_dir</span> - </div> - <input type="hidden" name="orig_root_dir_$cur_index" value="$cur_dir" /> - <input type="text" style="display: none" name="new_root_dir_$cur_index" id="new_root_dir_$cur_index" class="new_root_dir" value="$cur_dir" /> - #end for - -</div> - -<div class="optionWrapper clearfix alt container"> -<span class="selectTitle">Quality</span> - <div class="selectChoices"> - <select id="qualityPreset" name="quality_preset"> - <option value="keep">< keep ></option> - #set $selected = None - <option value="0" #if $quality_value != None and $quality_value not in $common.qualityPresets then "selected=\"selected\"" else ""#>Custom</option> - #for $curPreset in sorted($common.qualityPresets): - <option value="$curPreset" #if $quality_value == $curPreset then "selected=\"selected\"" else ""#>$common.qualityPresetStrings[$curPreset]</option> - #end for - </select> - </div> - - <div id="customQuality"> - <div style="width: 50%; text-align: center;" class="float-left"> - <h4>Inital</h4> - #set $anyQualityList = filter(lambda x: x > $common.Quality.NONE, $common.Quality.qualityStrings) - <select id="anyQualities" name="anyQualities" multiple="multiple" size="len($anyQualityList)"> - #for $curQuality in sorted($anyQualityList): - <option value="$curQuality" #if $curQuality in $anyQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option> - #end for - </select> - </div> - <div style="width: 50%; text-align: center;" class="float-left"> - <h4>Archive</h4> - #set $bestQualityList = filter(lambda x: x > $common.Quality.SDTV and x < $common.Quality.UNKNOWN, $common.Quality.qualityStrings) - <select id="bestQualities" name="bestQualities" multiple="multiple" size="len($bestQualityList)"> - #for $curQuality in sorted($bestQualityList): - <option value="$curQuality" #if $curQuality in $bestQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option> - #end for - </select> - </div> - </div> -</div> - -<div class="optionWrapper clearfix"> -<span class="selectTitle">Flatten Folders <span class="separator">*</span></span> - <div class="selectChoices"> - <select id="edit_flatten_folders" name="flatten_folders"> - <option value="keep">< keep ></option> - <option value="enable" #if $flatten_folders_value then "selected=\"selected\"" else ""#>enable</option> - <option value="disable" #if $flatten_folders_value == False then "selected=\"selected\"" else ""#>disable</option> - </select> - </div> -</div> - -<div class="optionWrapper clearfix alt"> - <span class="selectTitle">Paused</span> - <div class="selectChoices"> - <select id="edit_paused" name="paused"> - <option value="keep">< keep ></option> - <option value="enable" #if $paused_value then "selected=\"selected\"" else ""#>enable</option> - <option value="disable" #if $paused_value == False then "selected=\"selected\"" else ""#>disable</option> - </select> - </div><br /> -</div> -<div class="optionWrapper clearfix"> - <span class="selectTitle">Subtitles</span> - <div class="selectChoices"> - <select id="edit_subtitles" name="subtitles"> - <option value="keep">< keep ></option> - <option value="enable" #if $subtitles_value then "selected=\"selected\"" else ""#>enable</option> - <option value="disable" #if $subtitles_value == False then "selected=\"selected\"" else ""#>disable</option> - </select> - </div><br /> -</div> - -<div class="optionWrapper clearfix alt"> - <span class="selectTitle">Metadata Language <span class="separator">*</span></span> - <div class="selectChoices"> - <select name="tvdbLang" id="tvdbLangSelect"></select><br /> - </div><br /> -</div> - -<div class="optionWrapper clearfix"> - <span class="selectTitle">Desired Audio Language</span> - <div class="selectChoices"> - <select name="audioLang" id="showLangSelect"> - #for $k,$v in $common.showLanguages.iteritems(): - #if $k!="": - <option value="$k" #if $audio_value == $k then "selected=\"selected\"" else ""# - #end if - >$v</option> - #end for - </select> - </div><br /> -</div> - -<div class="optionWrapper clearfix"> - <br /><span class="separator" style="font-size: 1.2em; font-weight: 700;">*</span> - Changing these settings will cause the selected shows to be refreshed. - Be sure the selected show are not being refreshed -</div> -<div class="optionWrapper clearfix" style="text-align: center;"> - <input class="btn btn-primary" type="submit" value="Submit" /><br /> -</div> - -</form> -<br /> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import common +#from sickbeard import exceptions +#set global $title="" +#set global $header="Mass Edit" + +#set global $sbPath=".." + +#set global $topmenu="manage"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +#if $quality_value != None: +#set $initial_quality = int($quality_value) +#else: +#set $initial_quality = $common.SD +#end if +#set $anyQualities, $bestQualities = $common.Quality.splitQuality($initial_quality) +<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/massEdit.js?$sbPID"></script> +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + + \$.getJSON('$sbRoot/home/addShows/getTVDBLanguages', {}, function(data) { + var resultStr = ''; + + if (data.results.length == 0) { + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/${lang_value}.png)"'; + resultStr = '<option value="$lang_value" selected="selected" + flag>$lang_value</option>'; + } else { + var current_lang_added = false; + \$.each(data.results, function(index, obj) { + + if (obj == "$lang_value") { + selected = ' selected="selected"'; + current_lang_added = true; + } + else { + selected = ''; + } + + flag = ' class="flag" style="background-image:url($sbRoot/images/flags/' + obj + '.png);"'; + resultStr += '<option value="' + obj + '"' + selected + flag + '>' + obj + '</option>'; + }); + if (!current_lang_added) + resultStr += '<option value="$lang_value" selected="selected">$lang_value</option>'; + + } + \$('#tvdbLangSelect').html(resultStr) + + }); + +}); +//--> +</script> + +<form action="massEditSubmit" method="post"> +<input type="hidden" name="toEdit" value="$showList" /> + +<div class="optionWrapper"> +<span class="selectTitle">Root Directories <span class="separator">*</span></span><br /> + #for $cur_dir in $root_dir_list: + #set $cur_index = $root_dir_list.index($cur_dir) + <div style="padding: 6px 0 3px 25px;"> + <input class="btn edit_root_dir" type="button" class="edit_root_dir" id="edit_root_dir_$cur_index" value="Edit" /> + $cur_dir => <span id="display_new_root_dir_$cur_index">$cur_dir</span> + </div> + <input type="hidden" name="orig_root_dir_$cur_index" value="$cur_dir" /> + <input type="text" style="display: none" name="new_root_dir_$cur_index" id="new_root_dir_$cur_index" class="new_root_dir" value="$cur_dir" /> + #end for + +</div> + +<div class="optionWrapper alt"> +<span class="selectTitle">Quality</span> + <div class="selectChoices"> + <select id="qualityPreset" name="quality_preset"> + <option value="keep">< keep ></option> + #set $selected = None + <option value="0" #if $quality_value != None and $quality_value not in $common.qualityPresets then "selected=\"selected\"" else ""#>Custom</option> + #for $curPreset in sorted($common.qualityPresets): + <option value="$curPreset" #if $quality_value == $curPreset then "selected=\"selected\"" else ""#>$common.qualityPresetStrings[$curPreset]</option> + #end for + </select> + </div><br /> + + <div id="customQuality"> + <div style="width: 50%; text-align: center;" class="float-left"> + <h3>Inital</h3> + #set $anyQualityList = filter(lambda x: x > $common.Quality.NONE, $common.Quality.qualityStrings) + <select id="anyQualities" name="anyQualities" multiple="multiple" size="len($anyQualityList)"> + #for $curQuality in sorted($anyQualityList): + <option value="$curQuality" #if $curQuality in $anyQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option> + #end for + </select> + </div> + <div style="width: 50%; text-align: center;" class="float-left"> + <h3>Archive</h3> + #set $bestQualityList = filter(lambda x: x > $common.Quality.SDTV, $common.Quality.qualityStrings) + <select id="bestQualities" name="bestQualities" multiple="multiple" size="len($bestQualityList)"> + #for $curQuality in sorted($bestQualityList): + <option value="$curQuality" #if $curQuality in $bestQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option> + #end for + </select> + </div> + <br /> + </div> +</div> + +<div class="optionWrapper clearfix"> +<span class="selectTitle">Flatten Folders <span class="separator">*</span></span> + <div class="selectChoices"> + <select id="edit_flatten_folders" name="flatten_folders"> + <option value="keep">< keep ></option> + <option value="enable" #if $flatten_folders_value then "selected=\"selected\"" else ""#>enable</option> + <option value="disable" #if $flatten_folders_value == False then "selected=\"selected\"" else ""#>disable</option> + </select> + </div> +</div> + +<div class="optionWrapper alt"> + <span class="selectTitle">Paused</span> + <div class="selectChoices"> + <select id="edit_paused" name="paused"> + <option value="keep">< keep ></option> + <option value="enable" #if $paused_value then "selected=\"selected\"" else ""#>enable</option> + <option value="disable" #if $paused_value == False then "selected=\"selected\"" else ""#>disable</option> + </select> + </div><br /> +</div> + +<div class="optionWrapper"> +<span class="selectTitle">Subtitles<span class="separator"></span></span> + <div class="selectChoices"> + <select id="edit_subtitles" name="subtitles"> + <option value="keep">< keep ></option> + <option value="enable" #if $subtitles_value then "selected=\"selected\"" else ""#>enable</option> + <option value="disable" #if $subtitles_value == False then "selected=\"selected\"" else ""#>disable</option> + </select> + </div><br /> +</div> + +<div class="optionWrapper"> + <span class="selectTitle">Metadata Language <span class="separator">*</span></span> + <div class="selectChoices"> + <select name="tvdbLang" id="tvdbLangSelect"> + </select> + </div><br /> +</div> + +<div class="optionWrapper"> + <span class="selectTitle">Desired Audio Language</span> + <div class="selectChoices"> + <select name="audioLang" id="showLangSelect"> + <option value="keep">< keep ></option> + #for $k,$v in $common.showLanguages.iteritems(): + #if $k!="": + <option value="$k" #if $audio_value == $k then "selected=\"selected\"" else ""# + #end if + >$v</option> + #end for + </select> + </div><br /> +</div> + +<div class="optionWrapper"> + <br /><span class="separator" style="font-size: 1.2em; font-weight: 700;">*</span> + Changing these settings will cause the selected shows to be refreshed. + Be sure the selected show are not being refreshed. Blank for metadata will keep. +</div> + +<div class="optionWrapper" style="text-align: center;"> + <input type="submit" value="Submit" class="btn btn-primary" /><br /> +</div> + +</form> +<br /> + +<script type="text/javascript" charset="utf-8"> +<!-- + jQuery('#location').fileBrowser({ title: 'Select Show Location' }); +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/manage_subtitleMissed.tmpl b/data/interfaces/default/manage_subtitleMissed.tmpl index 44883eb5976ae0c617ccfdef0835ba895eae24da..137e097807ed7d5d8f16ca10405fce687a09d7d7 100644 --- a/data/interfaces/default/manage_subtitleMissed.tmpl +++ b/data/interfaces/default/manage_subtitleMissed.tmpl @@ -10,7 +10,11 @@ #set global $topmenu="manage"# #import os.path #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if #if $whichSubs: #set subsLanguage = $subliminal.language.Language($whichSubs) if not $whichSubs == 'all' else 'All' #end if @@ -41,11 +45,11 @@ subtitles <h2>Episodes without $subsLanguage subtitles.</h2> <br /> Download missed subtitles for selected episodes <input class="btn" type="submit" value="Go" /> -<div class="pull-left"> +<div> <button type="button" class="btn btn-mini selectAllShows" style="line-height: 8px;"><a>Select all</a></button> <button type="button" class="btn btn-mini unselectAllShows" style="line-height: 8px;"><a>Clear all</a></button> </div> -<br /><br /> +<br /> <table class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> #for $cur_tvdb_id in $sorted_show_ids: <tr id="$cur_tvdb_id"> diff --git a/data/interfaces/default/restart_bare.tmpl b/data/interfaces/default/restart_bare.tmpl index cb8267f99cbae49ea01adbae2362ad62bfd22f1f..70de6fca54c47c5e7a1b6f48f789c4ecc46608be 100644 --- a/data/interfaces/default/restart_bare.tmpl +++ b/data/interfaces/default/restart_bare.tmpl @@ -1,44 +1,44 @@ -<script type="text/javascript" charset="utf-8"> -<!-- -#try: - #set curSBHost = $sbHost - #set curSBHttpPort = $sbHttpPort - #set curSBHttpsEnabled = $sbHttpsEnabled -#except NameMapper.NotFound: - #set curSBHost = "localhost" - #set curSBHttpPort = $sickbeard.WEB_PORT - #set curSBHttpsEnabled = "False" -#end try -sbRoot = "$sbRoot"; -sbHttpPort = "$curSBHttpPort"; -sbHttpsEnabled = "$curSBHttpsEnabled"; -sbHost = "$curSBHost"; -//--> -</script> - -<script type="text/javascript" src="$sbRoot/js/lib/jquery-1.7.2.min.js?$sbPID"></script> -<script type="text/javascript" src="$sbRoot/js/restart.js?$sbPID"></script> - -<h3>Performing Restart</h3> -<br /> -<div id="shut_down_message"> -Waiting for Sick Beard to shut down: -<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="shut_down_loading" /> -<img src="$sbRoot/images/yes16.png" height="16" width="16" id="shut_down_success" style="display: none;" /> -</div> - -<div id="restart_message" style="display: none;"> -Waiting for Sick Beard to start again: -<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="restart_loading" /> -<img src="$sbRoot/images/yes16.png" height="16" width="16" id="restart_success" style="display: none;" /> -<img src="$sbRoot/images/no16.png" height="16" width="16" id="restart_failure" style="display: none;" /> -</div> - -<div id="refresh_message" style="display: none;"> -Loading the home page: -<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="refresh_loading" /> -</div> - -<div id="restart_fail_message" style="display: none;"> -Error: The restart has timed out, perhaps something prevented Sick Beard from starting again? -</div> +<script type="text/javascript" charset="utf-8"> +<!-- +#try: + #set curSBHost = $sbHost + #set curSBHttpPort = $sbHttpPort + #set curSBHttpsEnabled = $sbHttpsEnabled +#except NameMapper.NotFound: + #set curSBHost = "localhost" + #set curSBHttpPort = $sickbeard.WEB_PORT + #set curSBHttpsEnabled = "False" +#end try +sbRoot = "$sbRoot"; +sbHttpPort = "$curSBHttpPort"; +sbHttpsEnabled = "$curSBHttpsEnabled"; +sbHost = "$curSBHost"; +//--> +</script> + +<script type="text/javascript" src="$sbRoot/js/lib/jquery-1.8.3.min.js?$sbPID"></script> +<script type="text/javascript" src="$sbRoot/js/restart.js?$sbPID"></script> + +<h2>Performing Restart</h2> +<br /> +<div id="shut_down_message"> +Waiting for Sick Beard to shut down: +<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="shut_down_loading" /> +<img src="$sbRoot/images/yes16.png" height="16" width="16" id="shut_down_success" style="display: none;" /> +</div> + +<div id="restart_message" style="display: none;"> +Waiting for Sick Beard to start again: +<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="restart_loading" /> +<img src="$sbRoot/images/yes16.png" height="16" width="16" id="restart_success" style="display: none;" /> +<img src="$sbRoot/images/no16.png" height="16" width="16" id="restart_failure" style="display: none;" /> +</div> + +<div id="refresh_message" style="display: none;"> +Loading the home page: +<img src="$sbRoot/images/loading16.gif" height="16" width="16" id="refresh_loading" /> +</div> + +<div id="restart_fail_message" style="display: none;"> +Error: The restart has timed out, perhaps something prevented Sick Beard from starting again? +</div> diff --git a/data/interfaces/default/testRename.tmpl b/data/interfaces/default/testRename.tmpl index 5dc0bfc7d972beca54431ac7635e79e3c9176694..deead6529e1daac602a2cfbd4b9537b250be4ace 100644 --- a/data/interfaces/default/testRename.tmpl +++ b/data/interfaces/default/testRename.tmpl @@ -1,77 +1,83 @@ -#import sickbeard -#from sickbeard import common -#from sickbeard import exceptions -#set global $title="Test Rename" -#set global $header = '<a href="' + $sbRoot + '/home/displayShow?show=%d">%s</a>' % ($show.tvdbid, $show.name) -#set global $sbPath=".." - -#set global $topmenu="home"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<input type="hidden" id="showID" value="$show.tvdbid" /> - -<script type="text/javascript" src="$sbRoot/js/testRename.js"></script> - -<h3>Preview of the proposed name changes</h3> -<blockquote style="margin-bottom: 0; color: #3A87AD;"> -#if int($show.air_by_date) == 1 and $sickbeard.NAMING_CUSTOM_ABD: - $sickbeard.NAMING_ABD_PATTERN -#else - $sickbeard.NAMING_PATTERN -#end if -</blockquote> - -#set $curSeason = -1 -#set $odd = False -<table id="testRenameTable" class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> - -#for $cur_ep_obj in $ep_obj_list: -#set $curLoc = $cur_ep_obj.location[len($cur_ep_obj.show.location)+1:] -#set $curExt = $curLoc.split('.')[-1] -#set $newLoc = $cur_ep_obj.proper_path() + '.' + $curExt - -#if int($cur_ep_obj.season) != $curSeason: - <thead> - <tr class="seasonheader" id="season-$cur_ep_obj.season"> - <td colspan="4"> - <br/> - <h2>#if int($cur_ep_obj.season) == 0 then "Specials" else "Season "+str($cur_ep_obj.season)#</h2> - </td> - </tr> - <tr id="season-$cur_ep_obj.season-cols"> - <th width="1%"><input type="checkbox" class="seasonCheck" id="$cur_ep_obj.season" /></th> - <th class="nowrap">Episode</th> - <th class="nowrap">Old Location</th> - <th class="nowrap">New Location</th> - </tr> - </thead> -#set $curSeason = int($cur_ep_obj.season) -#end if - <tbody> -#set $odd = not $odd -#set $epStr = str($cur_ep_obj.season) + "x" + str($cur_ep_obj.episode) -#set $epList = sorted([cur_ep_obj.episode] + [x.episode for x in cur_ep_obj.relatedEps]) -#if len($epList) > 1: - #set $epList = [$min($epList), $max($epList)] -#end if - <tr class="season-$curSeason - #if $curLoc == $newLoc: - #if $odd then "odd" else "even"# - #else - wanted - #end if - "> - <td width="1%" valign="top"> - <input type="checkbox" class="epCheck" id="<%=str(cur_ep_obj.season) + 'x' + str(cur_ep_obj.episode)%>" name="<%=str(cur_ep_obj.season) + "x" + str(cur_ep_obj.episode) %>" /> - </td> - <td align="center" valign="top" class="nowrap"><%= "-".join(map(str, epList)) %></td> - <td width="50%" valign="top">$curLoc</td> - <td width="50%" valign="top">$newLoc</td> - </tr> - </tbody> - -#end for -</table><br /> -<input type="submit" value="Rename Selected" class="btn btn-danger"> -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import common +#from sickbeard import exceptions +#set global $title="Test Rename" +#set global $header = '<a style="color: #343434;" href="' + $sbRoot + '/home/displayShow?show=%d">%s</a>' % ($show.tvdbid, $show.name) +#set global $sbPath=".." + +#set global $topmenu="home"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if + +<input type="hidden" id="showID" value="$show.tvdbid" /> + +<script type="text/javascript" src="$sbRoot/js/testRename.js"></script> + +<h3>Preview of the proposed name changes</h3> +<blockquote style="margin-bottom: 0; color: #3A87AD;"> +#if int($show.air_by_date) == 1 and $sickbeard.NAMING_CUSTOM_ABD: + $sickbeard.NAMING_ABD_PATTERN +#else + $sickbeard.NAMING_PATTERN +#end if +</blockquote> + +#set $curSeason = -1 +#set $odd = False +<table id="testRenameTable" class="sickbeardTable" cellspacing="1" border="0" cellpadding="0"> + +#for $cur_ep_obj in $ep_obj_list: +#set $curLoc = $cur_ep_obj.location[len($cur_ep_obj.show.location)+1:] +#set $curExt = $curLoc.split('.')[-1] +#set $newLoc = $cur_ep_obj.proper_path() + '.' + $curExt + +#if int($cur_ep_obj.season) != $curSeason: + <thead> + <tr class="seasonheader" id="season-$cur_ep_obj.season"> + <td colspan="4"> + <br/> + <h2>#if int($cur_ep_obj.season) == 0 then "Specials" else "Season "+str($cur_ep_obj.season)#</h2> + </td> + </tr> + <tr id="season-$cur_ep_obj.season-cols"> + <th width="1%"><input type="checkbox" class="seasonCheck" id="$cur_ep_obj.season" /></th> + <th class="nowrap">Episode</th> + <th class="nowrap">Old Location</th> + <th class="nowrap">New Location</th> + </tr> + </thead> +#set $curSeason = int($cur_ep_obj.season) +#end if + <tbody> +#set $odd = not $odd +#set $epStr = str($cur_ep_obj.season) + "x" + str($cur_ep_obj.episode) +#set $epList = sorted([cur_ep_obj.episode] + [x.episode for x in cur_ep_obj.relatedEps]) +#if len($epList) > 1: + #set $epList = [$min($epList), $max($epList)] +#end if + <tr class="season-$curSeason + #if $curLoc == $newLoc: + #if $odd then "odd" else "even"# + #else + wanted + #end if + "> + <td width="1%" valign="top"> + <input type="checkbox" class="epCheck" id="<%=str(cur_ep_obj.season) + 'x' + str(cur_ep_obj.episode)%>" name="<%=str(cur_ep_obj.season) + "x" + str(cur_ep_obj.episode) %>" /> + </td> + <td align="center" valign="top" class="nowrap"><%= "-".join(map(str, epList)) %></td> + <td width="50%" valign="top">$curLoc</td> + <td width="50%" valign="top">$newLoc</td> + </tr> + </tbody> + +#end for +</table><br /> +<input type="submit" value="Rename Selected" class="btn btn-danger"> +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/interfaces/default/viewlogs.tmpl b/data/interfaces/default/viewlogs.tmpl index 261af794ca8cdffd35f1f5977659e94e21b43ada..95d72df14e438ecccd4d38062843edc0ee275e2f 100644 --- a/data/interfaces/default/viewlogs.tmpl +++ b/data/interfaces/default/viewlogs.tmpl @@ -1,44 +1,49 @@ -#import sickbeard -#from sickbeard import classes -#from sickbeard.common import * -#from sickbeard.logger import reverseNames -#set global $title="Log File" - -#set global $sbPath = ".." - -#set global $topmenu="errorlogs"# -#import os.path -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") - -<script type="text/javascript" charset="utf-8"> -<!-- -\$(document).ready(function(){ - \$('#minLevel').change(function(){ - url = '$sbRoot/errorlogs/viewlog/?minLevel='+\$(this).val() - window.location.href = url - }); -}); -//--> -</script> - -<div class="h2footer align-right"><b>Minimum logging level to display:</b> <select name="minLevel" id="minLevel"> -#set $levels = $reverseNames.keys() -$levels.sort(lambda x,y: cmp($reverseNames[$x], $reverseNames[$y])) -#for $level in $levels: -<option value="$reverseNames[$level]" #if $minLevel == $reverseNames[$level] then "selected=\"selected\"" else ""#>$level.title()</option> -#end for -</select> -</div> -<br /> -<div class="align-left"><pre> -$logLines -</pre> -</div> -<br /> -<script type="text/javascript" charset="utf-8"> -<!-- -window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes -//--> -</script> - -#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") +#import sickbeard +#from sickbeard import classes +#from sickbeard.common import * +#from sickbeard.logger import reverseNames +#set global $header="Log File" +#set global $title="" + +#set global $sbPath = ".." + +#set global $topmenu="errorlogs"# +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") + +<script type="text/javascript" charset="utf-8"> +<!-- +\$(document).ready(function(){ + \$('#minLevel').change(function(){ + url = '$sbRoot/errorlogs/viewlog/?minLevel='+\$(this).val() + window.location.href = url + }); +}); +//--> +</script> +#if $varExists('header') + <h1 class="header">$header</h1> +#else + <h1 class="title">$title</h1> +#end if +<div class="h2footer align-right"><b>Minimum logging level to display:</b> <select name="minLevel" id="minLevel"> +#set $levels = $reverseNames.keys() +$levels.sort(lambda x,y: cmp($reverseNames[$x], $reverseNames[$y])) +#for $level in $levels: +<option value="$reverseNames[$level]" #if $minLevel == $reverseNames[$level] then "selected=\"selected\"" else ""#>$level.title()</option> +#end for +</select> +</div> + +<div class="align-left"><pre> +$logLines +</pre> +</div> + +<script type="text/javascript" charset="utf-8"> +<!-- +window.setInterval( "location.reload(true)", 600000); // Refresh every 10 minutes +//--> +</script> + +#include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_bottom.tmpl") diff --git a/data/js/addExistingShow.js b/data/js/addExistingShow.js index 5c78e132669c26e939f2e4fa7103d722f42f5ca2..f41117d8fc53ee1888dbfb29d245bd03155e4e48 100644 --- a/data/js/addExistingShow.js +++ b/data/js/addExistingShow.js @@ -1,78 +1,78 @@ -$(document).ready(function() { - - $('#checkAll').live('click', function(){ - - var seasCheck = this; - - $('.dirCheck').each(function(){ - this.checked = seasCheck.checked; - }); - }); - - $('#submitShowDirs').click(function(){ - - var dirArr = new Array(); - - $('.dirCheck').each(function() { - - if (this.checked == true) { - dirArr.push(encodeURIComponent($(this).attr('id'))); - } - - }); - - if (dirArr.length == 0) - return false; - - url = sbRoot+'/home/addShows/addExistingShows?promptForSettings='+ ($('#promptForSettings').prop('checked') ? 'on' : 'off'); - - url += '&shows_to_add='+dirArr.join('&shows_to_add='); - - window.location.href = url; - }); - - - function loadContent() { - var url = ''; - $('.dir_check').each(function(i,w){ - if ($(w).is(':checked')) { - if (url.length) - url += '&' - url += 'rootDir=' + encodeURIComponent($(w).attr('id')); - } - }); - - $('#tableDiv').html('<img id="searchingAnim" src="'+sbRoot+'/images/loading32.gif" height="32" width="32" /> loading folders...'); - $.get(sbRoot+'/home/addShows/massAddTable', url, function(data) { - $('#tableDiv').html(data); - $("#addRootDirTable").tablesorter({ - //sortList: [[1,0]], - widgets: ['zebra'], - headers: { - 0: { sorter: false } - } - }); - }); - - } - - var last_txt = ''; - $('#rootDirText').change(function() { - if (last_txt == $('#rootDirText').val()) - return false; - else - last_txt = $('#rootDirText').val(); - $('#rootDirStaticList').html(''); - $('#rootDirs option').each(function(i, w) { - $('#rootDirStaticList').append('<li class="ui-state-default ui-corner-all"><input type="checkbox" class="cb dir_check" id="'+$(w).val()+'" checked=checked> <label for="'+$(w).val()+'"><b>'+$(w).val()+'</b></label></li>') - }); - loadContent(); - }); - - $('.dir_check').live('click', loadContent); - - $('.showManage').live('click', function() { - $( "#tabs" ).tabs( 'select', 0 ); - }); - +$(document).ready(function() { + + $('#checkAll').live('click', function(){ + + var seasCheck = this; + + $('.dirCheck').each(function(){ + this.checked = seasCheck.checked; + }); + }); + + $('#submitShowDirs').click(function(){ + + var dirArr = new Array(); + + $('.dirCheck').each(function() { + + if (this.checked == true) { + dirArr.push(encodeURIComponent($(this).attr('id'))); + } + + }); + + if (dirArr.length == 0) + return false; + + url = sbRoot+'/home/addShows/addExistingShows?promptForSettings='+ ($('#promptForSettings').prop('checked') ? 'on' : 'off'); + + url += '&shows_to_add='+dirArr.join('&shows_to_add='); + + window.location.href = url; + }); + + + function loadContent() { + var url = ''; + $('.dir_check').each(function(i,w){ + if ($(w).is(':checked')) { + if (url.length) + url += '&' + url += 'rootDir=' + encodeURIComponent($(w).attr('id')); + } + }); + + $('#tableDiv').html('<img id="searchingAnim" src="'+sbRoot+'/images/loading32.gif" height="32" width="32" /> loading folders...'); + $.get(sbRoot+'/home/addShows/massAddTable', url, function(data) { + $('#tableDiv').html(data); + $("#addRootDirTable").tablesorter({ + //sortList: [[1,0]], + widgets: ['zebra'], + headers: { + 0: { sorter: false } + } + }); + }); + + } + + var last_txt = ''; + $('#rootDirText').change(function() { + if (last_txt == $('#rootDirText').val()) + return false; + else + last_txt = $('#rootDirText').val(); + $('#rootDirStaticList').html(''); + $('#rootDirs option').each(function(i, w) { + $('#rootDirStaticList').append('<li class="ui-state-default ui-corner-all"><input type="checkbox" class="dir_check" id="'+$(w).val()+'" checked=checked> <label for="'+$(w).val()+'" style="color:black;"><b>'+$(w).val()+'</b></label></li>') + }); + loadContent(); + }); + + $('.dir_check').live('click', loadContent); + + $('.showManage').live('click', function() { + $( "#tabs" ).tabs( 'select', 0 ); + }); + }); \ No newline at end of file diff --git a/data/js/addShowOptions.js b/data/js/addShowOptions.js index 21ab36b3f65dc2b9dc4b6cdcbcde0589b71dfd1f..8d7ad002d4523bff08340be086cd57237602c677 100644 --- a/data/js/addShowOptions.js +++ b/data/js/addShowOptions.js @@ -1,27 +1,27 @@ -$(document).ready(function () { - - $('#saveDefaultsButton').click(function () { - var anyQualArray = []; - var bestQualArray = []; - $('#anyQualities option:selected').each(function (i, d) {anyQualArray.push($(d).val()); }); - $('#bestQualities option:selected').each(function (i, d) {bestQualArray.push($(d).val()); }); - - $.get(sbRoot + '/config/general/saveAddShowDefaults', {defaultStatus: $('#statusSelect').val(), - anyQualities: anyQualArray.join(','), - bestQualities: bestQualArray.join(','), - audio_lang: $('#showLangSelect').val(), - subtitles: $('#subtitles').prop('checked'), - defaultFlattenFolders: $('#flatten_folders').prop('checked')}); - $(this).attr('disabled', true); - $.pnotify({ - pnotify_title: 'Saved Defaults', - pnotify_text: 'Your "add show" defaults have been set to your current selections.', - pnotify_shadow: false - }); - }); - - $('#statusSelect, #qualityPreset, #flatten_folders, #anyQualities, #bestQualities ,#showLangSelect, #subtitles').change(function () { - $('#saveDefaultsButton').attr('disabled', false); - }); - +$(document).ready(function () { + + $('#saveDefaultsButton').click(function () { + var anyQualArray = []; + var bestQualArray = []; + $('#anyQualities option:selected').each(function (i, d) {anyQualArray.push($(d).val()); }); + $('#bestQualities option:selected').each(function (i, d) {bestQualArray.push($(d).val()); }); + + $.get(sbRoot + '/config/general/saveAddShowDefaults', {defaultStatus: $('#statusSelect').val(), + anyQualities: anyQualArray.join(','), + bestQualities: bestQualArray.join(','), + audio_lang: $('#showLangSelect').val(), + subtitles: $('#subtitles').prop('checked'), + defaultFlattenFolders: $('#flatten_folders').prop('checked')}); + $(this).attr('disabled', true); + $.pnotify({ + pnotify_title: 'Saved Defaults', + pnotify_text: 'Your "add show" defaults have been set to your current selections.', + pnotify_shadow: false + }); + }); + + $('#statusSelect, #qualityPreset, #flatten_folders, #anyQualities, #bestQualities ,#showLangSelect, #subtitles').change(function () { + $('#saveDefaultsButton').attr('disabled', false); + }); + }); \ No newline at end of file diff --git a/data/js/ajaxNotifications.js b/data/js/ajaxNotifications.js index 66d07b09152e9f2b5ccd4a2dea0733cac081558b..8f1a12a5130c140fb63d0276882f2f7a5f5eb5ab 100644 --- a/data/js/ajaxNotifications.js +++ b/data/js/ajaxNotifications.js @@ -1,26 +1,24 @@ var message_url = sbRoot + '/ui/get_messages'; $.pnotify.defaults.pnotify_width = "340px"; -$.pnotify.defaults.styling = "jqueryui"; $.pnotify.defaults.pnotify_history = false; $.pnotify.defaults.pnotify_delay = 4000; function check_notifications() { - $.getJSON(message_url, function (data) { - $.each(data, function (name, data) { + $.getJSON(message_url, function(data){ + $.each(data, function(name,data){ $.pnotify({ pnotify_type: data.type, pnotify_hide: data.type == 'notice', pnotify_title: data.title, - pnotify_text: data.message, - pnotify_shadow: false + pnotify_text: data.message }); }); }); - - setTimeout(check_notifications, 3000); + + setTimeout(check_notifications, 3000) } -$(document).ready(function () { +$(document).ready(function(){ check_notifications(); diff --git a/data/js/browser.js b/data/js/browser.js index 33091dba7998e361e4303a443c2800b191632906..2f5141ea7b1febfac06241b04980de7d0e49766b 100644 --- a/data/js/browser.js +++ b/data/js/browser.js @@ -1,178 +1,171 @@ -;(function($) { -"use strict"; - - $.Browser = { - defaults: { - title: 'Choose Directory', - url: sbRoot + '/browser/', - autocompleteURL: sbRoot + '/browser/complete' - } - }; - - var fileBrowserDialog, currentBrowserPath, currentRequest = null; - - function browse(path, endpoint) { - - if (currentBrowserPath === path) { - return; - } - - currentBrowserPath = path; - - if (currentRequest) { - currentRequest.abort(); - } - - fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy'); - - currentRequest = $.getJSON(endpoint, { path: path }, function (data) { - fileBrowserDialog.empty(); - var first_val = data[0]; - var i = 0; - var list, link = null; - data = $.grep(data, function (value) { - return i++ != 0; - }); - $('<h2>').text(first_val.current_path).appendTo(fileBrowserDialog); - list = $('<ul>').appendTo(fileBrowserDialog); - $.each(data, function (i, entry) { - link = $("<a href='javascript:void(0)' />").click(function () { browse(entry.path, endpoint); }).text(entry.name); - $('<span class="ui-icon ui-icon-folder-collapsed"></span>').prependTo(link); - link.hover( - function () {$("span", this).addClass("ui-icon-folder-open"); }, - function () {$("span", this).removeClass("ui-icon-folder-open"); } - ); - link.appendTo(list); - }); - $("a", list).wrap('<li class="ui-state-default ui-corner-all">'); - fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog'); - }); - } - - $.fn.nFileBrowser = function (callback, options) { - - options = $.extend({}, $.Browser.defaults, options); - - // make a fileBrowserDialog object if one doesn't exist already - if (!fileBrowserDialog) { - - // set up the jquery dialog - fileBrowserDialog = $('<div id="fileBrowserDialog" style="display:hidden"></div>').appendTo('body').dialog({ - dialogClass: 'browserDialog', - title: options.title, - position: ['center', 40], - minWidth: Math.min($(document).width() - 80, 650), - height: Math.min($(document).height() - 80, $(window).height() - 80), - maxHeight: Math.min($(document).height() - 80, $(window).height() - 80), - maxWidth: $(document).width() - 80, - modal: true, - autoOpen: false, - buttons: [ - { - text: "Ok", - "class": "btn btn-large", - click: function() { - // store the browsed path to the associated text field - callback(currentBrowserPath, options); - fileBrowserDialog.dialog("close"); - } - }, - { - text: "Cancel", - "class": "btn btn-large", - click: function() { - fileBrowserDialog.dialog("close"); - } - } - ] - }); - } - - // set up the browser and launch the dialog - var initialDir = ''; - if (options.initialDir) { - initialDir = options.initialDir; - } - browse(initialDir, options.url); - fileBrowserDialog.dialog('open'); - - return false; - }; - - $.fn.fileBrowser = function (options) { - options = $.extend({}, $.Browser.defaults, options); - // text field used for the result - options.field = $(this); - - if (options.field.autocomplete && options.autocompleteURL) { - var query = ''; - options.field.autocomplete({ - source: function (request, response) { - //keep track of user submitted search term - query = $.ui.autocomplete.escapeRegex(request.term); - $.ajax({ - url: options.autocompleteURL, - data: request, - dataType: "json", - success: function (data, item) { - //implement a startsWith filter for the results - var matcher = new RegExp("^" + query, "i"); - var a = $.grep(data, function (item, index) { - return matcher.test(item); - }); - response(a); - } - }); - }, - open: function (event, ui) { - $(".ui-autocomplete li.ui-menu-item a").removeClass("ui-corner-all"); - $(".ui-autocomplete li.ui-menu-item:odd a").addClass("ui-menu-item-alternate"); - } - }) - .data("autocomplete")._renderItem = function (ul, item) { - //highlight the matched search term from the item -- note that this is global and will match anywhere - var result_item = item.label; - var x = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", "gi"); - result_item = result_item.replace(x, function (FullMatch, n) { - return '<b>' + FullMatch + '</b>'; - }); - return $("<li></li>") - .data("item.autocomplete", item) - .append("<a class='nowrap'>" + result_item + "</a>") - .appendTo(ul); - }; - } - - var initialDir, path, callback, ls = false; - // if the text field is empty and we're given a key then populate it with the last browsed value from localStorage - try { ls = !!(localStorage.getItem); } catch (e) {} - if (ls && options.key) { - path = localStorage['fileBrowser-' + options.key]; - } - if (options.key && options.field.val().length === 0 && (path)) { - options.field.val(path); - } - - callback = function (path, options) { - // store the browsed path to the associated text field - options.field.val(path); - - // use a localStorage to remember for next time -- no ie6/7 - if (ls && options.key) { - localStorage['fileBrowser-' + options.key] = path; - } - - }; - - initialDir = options.field.val() || (options.key && path) || ''; - - options = $.extend(options, {initialDir: initialDir}); - - // append the browse button and give it a click behavior - return options.field.addClass('fileBrowserField').after($('<input type="button" value="Browse…" class="btn fileBrowser" />').click(function () { - $(this).nFileBrowser(callback, options); - return false; - })); - }; - -})(jQuery); +(function () { + + $.Browser = { + defaults: { + title: 'Choose Directory', + url: sbRoot + '/browser/', + autocompleteURL: sbRoot + '/browser/complete' + } + }; + + var fileBrowserDialog, currentBrowserPath, currentRequest = null; + + function browse(path, endpoint) { + + if (currentBrowserPath === path) { + return; + } + + currentBrowserPath = path; + + if (currentRequest) { + currentRequest.abort(); + } + + fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy'); + + currentRequest = $.getJSON(endpoint, { path: path }, function (data) { + fileBrowserDialog.empty(); + var first_val = data[0]; + var i = 0; + var list, link = null; + data = $.grep(data, function (value) { + return i++ != 0; + }); + $('<h1>').text(first_val.current_path).appendTo(fileBrowserDialog); + list = $('<ul>').appendTo(fileBrowserDialog); + $.each(data, function (i, entry) { + link = $("<a href='javascript:void(0)' />").click(function () { browse(entry.path, endpoint); }).text(entry.name); + $('<span class="ui-icon ui-icon-folder-collapsed"></span>').prependTo(link); + link.hover( + function () {$("span", this).addClass("ui-icon-folder-open"); }, + function () {$("span", this).removeClass("ui-icon-folder-open"); } + ); + link.appendTo(list); + }); + $("a", list).wrap('<li class="ui-state-default ui-corner-all">'); + fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog'); + }); + } + + $.fn.nFileBrowser = function (callback, options) { + + options = $.extend({}, $.Browser.defaults, options); + + // make a fileBrowserDialog object if one doesn't exist already + if (!fileBrowserDialog) { + + // set up the jquery dialog + fileBrowserDialog = $('<div id="fileBrowserDialog" style="display:hidden"></div>').appendTo('body').dialog({ + dialogClass: 'browserDialog', + title: options.title, + position: ['center', 40], + minWidth: Math.min($(document).width() - 80, 650), + minHeight: 320, + height: $(document).height() - 80, + modal: true, + autoOpen: false + }); + } + + // add the OK/Close buttons to the dialog + fileBrowserDialog.dialog('option', 'buttons', { + "Ok": function () { + // store the browsed path to the associated text field + //options.field.val(currentBrowserPath); + //alert(currentBrowserPath); + callback(currentBrowserPath, options); + fileBrowserDialog.dialog("close"); + }, + "Cancel": function () { + fileBrowserDialog.dialog("close"); + } + }); + + // set up the browser and launch the dialog + var initialDir = ''; + if (options.initialDir) { + initialDir = options.initialDir; + } + browse(initialDir, options.url); + fileBrowserDialog.dialog('open'); + + return false; + }; + + $.fn.fileBrowser = function (options) { + options = $.extend({}, $.Browser.defaults, options); + // text field used for the result + options.field = $(this); + + if (options.field.autocomplete && options.autocompleteURL) { + var query = ''; + options.field.autocomplete({ + source: function (request, response) { + //keep track of user submitted search term + query = $.ui.autocomplete.escapeRegex(request.term); + $.ajax({ + url: options.autocompleteURL, + data: request, + dataType: "json", + success: function (data, item) { + //implement a startsWith filter for the results + var matcher = new RegExp("^" + query, "i"); + var a = $.grep(data, function (item, index) { + return matcher.test(item); + }); + response(a); + } + }); + }, + open: function (event, ui) { + $(".ui-autocomplete li.ui-menu-item a").removeClass("ui-corner-all"); + $(".ui-autocomplete li.ui-menu-item:odd a").addClass("ui-menu-item-alternate"); + } + }) + .data("autocomplete")._renderItem = function (ul, item) { + //highlight the matched search term from the item -- note that this is global and will match anywhere + var result_item = item.label; + var x = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", "gi"); + result_item = result_item.replace(x, function (FullMatch, n) { + return '<b>' + FullMatch + '</b>'; + }); + return $("<li></li>") + .data("item.autocomplete", item) + .append("<a class='nowrap'>" + result_item + "</a>") + .appendTo(ul); + }; + } + + var initialDir, path, callback, ls = false; + // if the text field is empty and we're given a key then populate it with the last browsed value from localStorage + try { ls = !!(localStorage.getItem); } catch (e) {} + if (ls && options.key) { + path = localStorage['fileBrowser-' + options.key]; + } + if (options.key && options.field.val().length === 0 && (path)) { + options.field.val(path); + } + + callback = function (path, options) { + // store the browsed path to the associated text field + options.field.val(path); + + // use a localStorage to remember for next time -- no ie6/7 + if (ls && options.key) { + localStorage['fileBrowser-' + options.key] = path; + } + + }; + + initialDir = options.field.val() || (options.key && path) || ''; + + options = $.extend(options, {initialDir: initialDir}); + + // append the browse button and give it a click behavior + return options.field.addClass('fileBrowserField').after($('<input type="button" value="Browse…" class="btn fileBrowser" />').click(function () { + $(this).nFileBrowser(callback, options); + return false; + })); + }; +})(); diff --git a/data/js/config.js b/data/js/config.js index 541d8d2dc04fb42b25660338cdd48b885418905f..709d3ac7caa926f1909c136d52d552fbbb490cee 100644 --- a/data/js/config.js +++ b/data/js/config.js @@ -1,50 +1,49 @@ -$(document).ready(function () { - $(".enabler").each(function () { - if (!$(this).prop('checked')) { - $('#content_' + $(this).attr('id')).hide(); - } - }); - - $(".enabler").click(function () { - if ($(this).prop('checked')) { - $('#content_' + $(this).attr('id')).fadeIn("fast", "linear"); - } else { - $('#content_' + $(this).attr('id')).fadeOut("fast", "linear"); - } - }); - - // bind 'myForm' and provide a simple callback function - $('#configForm').ajaxForm({ - beforeSubmit: function () { - $('.config_submitter').each(function () { - $(this).attr("disabled", "disabled"); - $(this).after('<span><img src="' + sbRoot + '/images/loading16.gif"> Saving...</span>'); - $(this).hide(); - }); - }, - success: function () { - setTimeout('config_success()', 2000); - } - }); - - $('#api_key').click(function () { $('#api_key').select(); }); - $("#generate_new_apikey").click(function () { - $.get(sbRoot + '/config/general/generateKey', - function (data) { - if (data.error != undefined) { - alert(data.error); - return; - } - $('#api_key').val(data).select(); - }); - }); - -}); - -function config_success() { - $('.config_submitter').each(function () { - $(this).removeAttr("disabled"); - $(this).next().remove(); - $(this).show(); - }); -} \ No newline at end of file +$(document).ready(function(){ + $(".enabler").each(function(){ + if (!$(this).prop('checked')) + $('#content_'+$(this).attr('id')).hide(); + }); + + $(".enabler").click(function() { + if ($(this).prop('checked')) + $('#content_'+$(this).attr('id')).fadeIn("fast", "linear"); + else + $('#content_'+$(this).attr('id')).fadeOut("fast", "linear"); + }); + + // bind 'myForm' and provide a simple callback function + $('#configForm').ajaxForm({ + beforeSubmit: function(){ + $('.config_submitter').each(function(){ + $(this).attr("disabled", "disabled"); + $(this).after('<span><img src="'+sbRoot+'/images/loading16.gif"> Saving...</span>'); + $(this).hide(); + }); + }, + success: function(){ + setTimeout('config_success()', 2000) + } + }); + + $('#api_key').click(function(){ $('#api_key').select() }); + $("#generate_new_apikey").click(function(){ + $.get(sbRoot + '/config/general/generateKey', + function(data){ + if (data.error != undefined) { + alert(data.error); + return; + } + $('#api_key').val(data); + }); + }); + +}); + +function config_success(){ + $('.config_submitter').each(function(){ + $(this).removeAttr("disabled"); + $(this).next().remove(); + $(this).show(); + }); + $('#email_show').trigger('notify'); +} diff --git a/data/js/configPostProcessing.js b/data/js/configPostProcessing.js index 81e7e818f6a0503e7a57980c7253b8e4fe2523e1..43c5d61806c403046a8dbfc6d6bb8566fa567e87 100644 --- a/data/js/configPostProcessing.js +++ b/data/js/configPostProcessing.js @@ -1,296 +1,296 @@ -$(document).ready(function () { - - // http://stackoverflow.com/questions/2219924/idiomatic-jquery-delayed-event-only-after-a-short-pause-in-typing-e-g-timew - var typewatch = (function () { - var timer = 0; - return function (callback, ms) { - clearTimeout(timer); - timer = setTimeout(callback, ms); - }; - })(); - - function fill_examples() { - var pattern = $('#naming_pattern').val(); - var multi = $('#naming_multi_ep :selected').val(); - - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern}, - function (data) { - if (data) { - $('#naming_example').text(data + '.ext'); - $('#naming_example_div').show(); - } else { - $('#naming_example_div').hide(); - } - }); - - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi}, - function (data) { - if (data) { - $('#naming_example_multi').text(data + '.ext'); - $('#naming_example_multi_div').show(); - } else { - $('#naming_example_multi_div').hide(); - } - }); - - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi}, - function (data) { - if (data == "invalid") { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' - }); - $('#naming_pattern').qtip('toggle', true); - $('#naming_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-cream' - }); - $('#naming_pattern').qtip('toggle', true); - $('#naming_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-green' - }); - $('#naming_pattern').qtip('toggle', false); - $('#naming_pattern').css('background-color', '#FFFFFF'); - } - }); - - } - - function fill_abd_examples() { - if (!$('#naming_custom_abd').prop('checked')) { - $('#naming_abd_pattern').qtip('hide'); - return; - } - - var pattern = $('#naming_abd_pattern').val(); - - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, abd: 'True'}, - function (data) { - if (data) { - $('#naming_abd_example').text(data + '.ext'); - $('#naming_abd_example_div').show(); - } else { - $('#naming_abd_example_div').hide(); - } - }); - - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, abd: 'True'}, - function (data) { - if (data == "invalid") { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' - }); - $('#naming_abd_pattern').qtip('toggle', true); - $('#naming_abd_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' - }); - $('#naming_abd_pattern').qtip('toggle', true); - $('#naming_abd_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-green' - }); - $('#naming_abd_pattern').qtip('toggle', false); - $('#naming_abd_pattern').css('background-color', '#FFFFFF'); - } - }); - - } - - function setup_naming() { - // if it is a custom selection then show the text box - if ($('#name_presets :selected').val() == "Custom...") { - $('#naming_custom').show(); - } else { - $('#naming_custom').hide(); - $('#naming_pattern').val($('#name_presets :selected').attr('id')); - } - fill_examples(); - } - - function setup_abd_naming() { - // if it is a custom selection then show the text box - if ($('#name_abd_presets :selected').val() == "Custom...") { - $('#naming_abd_custom').show(); - } else { - $('#naming_abd_custom').hide(); - $('#naming_abd_pattern').val($('#name_abd_presets :selected').attr('id')); - } - fill_abd_examples(); - } - - $('#name_presets').change(function () { - setup_naming(); - }); - - $('#name_abd_presets').change(function () { - setup_abd_naming(); - }); - - $('#naming_custom_abd').change(function () { - setup_abd_naming(); - }); - - $('#naming_multi_ep').change(fill_examples); - $('#naming_pattern').focusout(fill_examples); - $('#naming_pattern').keyup(function () { - typewatch(function () { - fill_examples(); - }, 500); - }); - - $('#naming_abd_pattern').focusout(fill_examples); - $('#naming_abd_pattern').keyup(function () { - typewatch(function () { - fill_abd_examples(); - }, 500); - }); - - $('#show_naming_key').click(function () { - $('#naming_key').toggle(); - }); - $('#show_naming_abd_key').click(function () { - $('#naming_abd_key').toggle(); - }); - $('#do_custom').click(function () { - $('#naming_pattern').val($('#name_presets :selected').attr('id')); - $('#naming_custom').show(); - $('#naming_pattern').focus(); - }); - setup_naming(); - setup_abd_naming(); - - // -- start of metadata options div toggle code -- - $('#metadataType').change(function () { - $(this).showHideMetadata(); - }); - - $.fn.showHideMetadata = function () { - $('.metadataDiv').each(function () { - var targetName = $(this).attr('id'); - var selectedTarget = $('#metadataType :selected').val(); - - if (selectedTarget == targetName) { - $(this).show(); - } else { - $(this).hide(); - } - }); - }; - //initalize to show the div - $(this).showHideMetadata(); - // -- end of metadata options div toggle code -- - - $('.metadata_checkbox').click(function () { - $(this).refreshMetadataConfig(false); - }); - - $.fn.refreshMetadataConfig = function (first) { - - var cur_most = 0; - var cur_most_provider = ''; - - $('.metadataDiv').each(function () { - var generator_name = $(this).attr('id'); - - var config_arr = []; - var show_metadata = $("#" + generator_name + "_show_metadata").prop('checked'); - var episode_metadata = $("#" + generator_name + "_episode_metadata").prop('checked'); - var fanart = $("#" + generator_name + "_fanart").prop('checked'); - var poster = $("#" + generator_name + "_poster").prop('checked'); - var episode_thumbnails = $("#" + generator_name + "_episode_thumbnails").prop('checked'); - var season_thumbnails = $("#" + generator_name + "_season_thumbnails").prop('checked'); - - config_arr.push(show_metadata ? '1' : '0'); - config_arr.push(episode_metadata ? '1' : '0'); - config_arr.push(poster ? '1' : '0'); - config_arr.push(fanart ? '1' : '0'); - config_arr.push(episode_thumbnails ? '1' : '0'); - config_arr.push(season_thumbnails ? '1' : '0'); - - var cur_num = 0; - for (var i = 0; i < config_arr.length; i++) - cur_num += parseInt(config_arr[i]); - if (cur_num > cur_most) { - cur_most = cur_num; - cur_most_provider = generator_name; - } - - $("#" + generator_name + "_eg_show_metadata").attr('class', show_metadata ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_episode_metadata").attr('class', episode_metadata ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_poster").attr('class', poster ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_fanart").attr('class', fanart ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_episode_thumbnails").attr('class', episode_thumbnails ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_season_thumbnails").attr('class', season_thumbnails ? 'enabled' : 'disabled'); - $("#" + generator_name + "_data").val(config_arr.join('|')) - - }); - - if (cur_most_provider != '' && first) { - $('#metadataType option[value=' + cur_most_provider + ']').attr('selected', 'selected') - $(this).showHideMetadata(); - } - - } - - $(this).refreshMetadataConfig(true); - $('img[title]').qtip( { - position: { - viewport: $(window), - at: 'bottom center', - my: 'top right' - }, - style: { - tip: { - corner: true, - method: 'polygon' - }, - classes: 'ui-tooltip-shadow ui-tooltip-dark' - } - }); - $('i[title]').qtip( { - position: { - viewport: $(window), - at: 'top center', - my: 'bottom center', - }, - style: { - tip: { - corner: true, - method: 'polygon' - }, - classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-sb' - } - }); - $('.custom-pattern').qtip( { - content: 'validating...', - show: { - event: false, - ready: false - }, - hide: false, - position: { - viewport: $(window), - at: 'center left', - my: 'center right', - }, - style: { - tip: { - corner: true, - method: 'polygon' - }, - classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' - } - }); - +$(document).ready(function () { + + // http://stackoverflow.com/questions/2219924/idiomatic-jquery-delayed-event-only-after-a-short-pause-in-typing-e-g-timew + var typewatch = (function () { + var timer = 0; + return function (callback, ms) { + clearTimeout(timer); + timer = setTimeout(callback, ms); + }; + })(); + + function fill_examples() { + var pattern = $('#naming_pattern').val(); + var multi = $('#naming_multi_ep :selected').val(); + + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern}, + function (data) { + if (data) { + $('#naming_example').text(data + '.ext'); + $('#naming_example_div').show(); + } else { + $('#naming_example_div').hide(); + } + }); + + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi}, + function (data) { + if (data) { + $('#naming_example_multi').text(data + '.ext'); + $('#naming_example_multi_div').show(); + } else { + $('#naming_example_multi_div').hide(); + } + }); + + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi}, + function (data) { + if (data == "invalid") { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' + }); + $('#naming_pattern').qtip('toggle', true); + $('#naming_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' + }); + $('#naming_pattern').qtip('toggle', true); + $('#naming_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-green' + }); + $('#naming_pattern').qtip('toggle', false); + $('#naming_pattern').css('background-color', '#FFFFFF'); + } + }); + + } + + function fill_abd_examples() { + if (!$('#naming_custom_abd').prop('checked')) { + $('#naming_abd_pattern').qtip('hide'); + return; + } + + var pattern = $('#naming_abd_pattern').val(); + + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, abd: 'True'}, + function (data) { + if (data) { + $('#naming_abd_example').text(data + '.ext'); + $('#naming_abd_example_div').show(); + } else { + $('#naming_abd_example_div').hide(); + } + }); + + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, abd: 'True'}, + function (data) { + if (data == "invalid") { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' + }); + $('#naming_abd_pattern').qtip('toggle', true); + $('#naming_abd_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' + }); + $('#naming_abd_pattern').qtip('toggle', true); + $('#naming_abd_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-green' + }); + $('#naming_abd_pattern').qtip('toggle', false); + $('#naming_abd_pattern').css('background-color', '#FFFFFF'); + } + }); + + } + + function setup_naming() { + // if it is a custom selection then show the text box + if ($('#name_presets :selected').val() == "Custom...") { + $('#naming_custom').show(); + } else { + $('#naming_custom').hide(); + $('#naming_pattern').val($('#name_presets :selected').attr('id')); + } + fill_examples(); + } + + function setup_abd_naming() { + // if it is a custom selection then show the text box + if ($('#name_abd_presets :selected').val() == "Custom...") { + $('#naming_abd_custom').show(); + } else { + $('#naming_abd_custom').hide(); + $('#naming_abd_pattern').val($('#name_abd_presets :selected').attr('id')); + } + fill_abd_examples(); + } + + $('#name_presets').change(function () { + setup_naming(); + }); + + $('#name_abd_presets').change(function () { + setup_abd_naming(); + }); + + $('#naming_custom_abd').change(function () { + setup_abd_naming(); + }); + + $('#naming_multi_ep').change(fill_examples); + $('#naming_pattern').focusout(fill_examples); + $('#naming_pattern').keyup(function () { + typewatch(function () { + fill_examples(); + }, 500); + }); + + $('#naming_abd_pattern').focusout(fill_examples); + $('#naming_abd_pattern').keyup(function () { + typewatch(function () { + fill_abd_examples(); + }, 500); + }); + + $('#show_naming_key').click(function () { + $('#naming_key').toggle(); + }); + $('#show_naming_abd_key').click(function () { + $('#naming_abd_key').toggle(); + }); + $('#do_custom').click(function () { + $('#naming_pattern').val($('#name_presets :selected').attr('id')); + $('#naming_custom').show(); + $('#naming_pattern').focus(); + }); + setup_naming(); + setup_abd_naming(); + + // -- start of metadata options div toggle code -- + $('#metadataType').change(function () { + $(this).showHideMetadata(); + }); + + $.fn.showHideMetadata = function () { + $('.metadataDiv').each(function () { + var targetName = $(this).attr('id'); + var selectedTarget = $('#metadataType :selected').val(); + + if (selectedTarget == targetName) { + $(this).show(); + } else { + $(this).hide(); + } + }); + }; + //initalize to show the div + $(this).showHideMetadata(); + // -- end of metadata options div toggle code -- + + $('.metadata_checkbox').click(function () { + $(this).refreshMetadataConfig(false); + }); + + $.fn.refreshMetadataConfig = function (first) { + + var cur_most = 0; + var cur_most_provider = ''; + + $('.metadataDiv').each(function () { + var generator_name = $(this).attr('id'); + + var config_arr = []; + var show_metadata = $("#" + generator_name + "_show_metadata").prop('checked'); + var episode_metadata = $("#" + generator_name + "_episode_metadata").prop('checked'); + var fanart = $("#" + generator_name + "_fanart").prop('checked'); + var poster = $("#" + generator_name + "_poster").prop('checked'); + var episode_thumbnails = $("#" + generator_name + "_episode_thumbnails").prop('checked'); + var season_thumbnails = $("#" + generator_name + "_season_thumbnails").prop('checked'); + + config_arr.push(show_metadata ? '1' : '0'); + config_arr.push(episode_metadata ? '1' : '0'); + config_arr.push(poster ? '1' : '0'); + config_arr.push(fanart ? '1' : '0'); + config_arr.push(episode_thumbnails ? '1' : '0'); + config_arr.push(season_thumbnails ? '1' : '0'); + + var cur_num = 0; + for (var i = 0; i < config_arr.length; i++) + cur_num += parseInt(config_arr[i]); + if (cur_num > cur_most) { + cur_most = cur_num; + cur_most_provider = generator_name; + } + + $("#" + generator_name + "_eg_show_metadata").attr('class', show_metadata ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_episode_metadata").attr('class', episode_metadata ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_poster").attr('class', poster ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_fanart").attr('class', fanart ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_episode_thumbnails").attr('class', episode_thumbnails ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_season_thumbnails").attr('class', season_thumbnails ? 'enabled' : 'disabled'); + $("#" + generator_name + "_data").val(config_arr.join('|')) + + }); + + if (cur_most_provider != '' && first) { + $('#metadataType option[value=' + cur_most_provider + ']').attr('selected', 'selected') + $(this).showHideMetadata(); + } + + } + + $(this).refreshMetadataConfig(true); + $('img[title]').qtip( { + position: { + viewport: $(window), + at: 'bottom center', + my: 'top right' + }, + style: { + tip: { + corner: true, + method: 'polygon' + }, + classes: 'ui-tooltip-shadow ui-tooltip-dark' + } + }); + $('i[title]').qtip( { + position: { + viewport: $(window), + at: 'top center', + my: 'bottom center', + }, + style: { + tip: { + corner: true, + method: 'polygon' + }, + classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-sb' + } + }); + $('.custom-pattern').qtip( { + content: 'validating...', + show: { + event: false, + ready: false + }, + hide: false, + position: { + viewport: $(window), + at: 'center left', + my: 'center right', + }, + style: { + tip: { + corner: true, + method: 'polygon' + }, + classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-red' + } + }); + }); \ No newline at end of file diff --git a/data/js/configProviders.js b/data/js/configProviders.js index 2824378f7c7ce33020b8a083c1a3e1034581fccc..b5be74c6ef75db0050083e7083038876c5c91ab6 100644 --- a/data/js/configProviders.js +++ b/data/js/configProviders.js @@ -13,7 +13,7 @@ $(document).ready(function(){ }); } - $.fn.addProvider = function (id, name, url, key, isDefault) { + $.fn.addProvider = function (id, name, url, key, isDefault, showProvider) { if (url.match('/$') == null) url = url + '/' @@ -27,11 +27,11 @@ $(document).ready(function(){ $(this).populateNewznabSection(); } - if ($('#providerOrderList > #'+id).length == 0) { + if ($('#provider_order_list > #'+id).length == 0 && showProvider != false) { var toAdd = '<li class="ui-state-default" id="'+id+'"> <input type="checkbox" id="enable_'+id+'" class="provider_enabler" CHECKED> <a href="'+url+'" class="imgLink" target="_new"><img src="'+sbRoot+'/images/providers/newznab.gif" alt="'+name+'" width="16" height="16"></a> '+name+'</li>' - $('#providerOrderList').append(toAdd); - $('#providerOrderList').sortable("refresh"); + $('#provider_order_list').append(toAdd); + $('#provider_order_list').sortable("refresh"); } $(this).makeNewznabProviderString(); @@ -55,7 +55,7 @@ $(document).ready(function(){ delete newznabProviders[id]; $(this).populateNewznabSection(); - $('#providerOrderList > #'+id).remove(); + $('#provider_order_list > #'+id).remove(); $(this).makeNewznabProviderString(); @@ -112,7 +112,7 @@ $(document).ready(function(){ } $.fn.refreshProviderList = function() { - var idArr = $("#providerOrderList").sortable('toArray'); + var idArr = $("#provider_order_list").sortable('toArray'); var finalArr = new Array(); $.each(idArr, function(key, val) { var checked = + $('#enable_'+val).prop('checked') ? '1' : '0'; @@ -140,9 +140,6 @@ $(document).ready(function(){ var selectedProvider = $('#editANewznabProvider :selected').val(); - if (selectedProvider == "addNewznab") - return; - var url = $('#newznab_url').val(); var key = $('#newznab_key').val(); @@ -199,13 +196,13 @@ $(document).ready(function(){ $(this).showHideProviders(); - $("#providerOrderList").sortable({ + $("#provider_order_list").sortable({ placeholder: 'ui-state-highlight', update: function (event, ui) { $(this).refreshProviderList(); } }); - $("#providerOrderList").disableSelection(); + $("#provider_order_list").disableSelection(); }); \ No newline at end of file diff --git a/data/js/configSearch.js b/data/js/configSearch.js index 11f1ffafc348bb6d456845e94496f4eef3d5210a..c615e8f99cd7b54ef17abe3b5b9182579ebf640c 100644 --- a/data/js/configSearch.js +++ b/data/js/configSearch.js @@ -1,135 +1,133 @@ -$(document).ready(function(){ - var loading = '<img src="'+sbRoot+'/images/loading16.gif" height="16" width="16" />'; - - function toggle_torrent_title(){ - if ($('#use_torrents').prop('checked')) - $('#no-torrents').show(); - else - $('#no-torrents').hide(); - } - - $.fn.nzb_method_handler = function() { - - var selectedProvider = $('#nzb_method :selected').val(); - - if (selectedProvider == "blackhole") { - $('#blackhole_settings').show(); - $('#sabnzbd_settings').hide(); - $('#testSABnzbd').hide(); - $('#testSABnzbd-result').hide(); - $('#nzbget_settings').hide(); - } else if (selectedProvider == "nzbget") { - $('#blackhole_settings').hide(); - $('#sabnzbd_settings').hide(); - $('#testSABnzbd').hide(); - $('#testSABnzbd-result').hide(); - $('#nzbget_settings').show(); - } else { - $('#blackhole_settings').hide(); - $('#sabnzbd_settings').show(); - $('#testSABnzbd').show(); - $('#testSABnzbd-result').show(); - $('#nzbget_settings').hide(); - } - - } - - $.fn.torrent_method_handler = function() { - - var selectedProvider = $('#torrent_method :selected').val(); - - if (selectedProvider == "blackhole") { - $('#t_blackhole_settings').show(); - $('#torrent_settings').hide(); - } else if (selectedProvider == "utorrent") { - $('#t_blackhole_settings').hide(); - $('#torrent_settings').show(); - $('#Torrent_username').show() - $('#Torrent_Path').hide(); - $('#Torrent_Ratio').hide(); - $('#Torrent_Label').show() - $('#host_desc').text('uTorrent Host'); - $('#username_desc').text('uTorrent Username'); - $('#password_desc').text('uTorrent Password'); - $('#label_desc').text('uTorrent Label'); - } else if (selectedProvider == "transmission"){ - $('#t_blackhole_settings').hide(); - $('#torrent_settings').show(); - $('#Torrent_username').show(); - $('#Torrent_Path').show(); - $('#Torrent_Ratio').show(); - $('#Torrent_Label').hide(); - $('#host_desc').html('Transmission Host'); - $('#username_desc').text('Transmission Username'); - $('#password_desc').text('Transmission Password'); - $('#directory_desc').text('Transmission Directory'); - } else if (selectedProvider == "deluge"){ - $('#t_blackhole_settings').hide(); - $('#torrent_settings').show(); - $('#Torrent_Label').show(); - $('#Torrent_username').hide(); - $('#Torrent_Path').show(); - $('#Torrent_Ratio').show(); - $('#host_desc').text('Deluge Host'); - $('#username_desc').text('Deluge Username'); - $('#password_desc').text('Deluge Password'); - $('#label_desc').text('Deluge Label'); - $('#directory_desc').text('Deluge Directory'); - } else if (selectedProvider == "download_station"){ - $('#t_blackhole_settings').hide(); - $('#torrent_settings').show(); - $('#Torrent_Label').hide(); - $('#Torrent_username').show(); - $('#Torrent_Paused').hide(); - $('#Torrent_Path').hide(); - $('#Torrent_Ratio').hide(); - $('#Torrent_High_Bandwidth').hide(); - $('#host_desc').text('Synology Host'); - $('#username_desc').text('Synology Username'); - $('#password_desc').text('Synology Password'); - $('#label_desc').text('Synology Label'); - $('#directory_desc').text('Synology Directory'); - } - } - - $('#nzb_method').change($(this).nzb_method_handler); - - $(this).nzb_method_handler(); - - $('#testSABnzbd').click(function(){ - $('#testSABnzbd-result').html(loading); - var sab_host = $("input=[name='sab_host']").val(); - var sab_username = $("input=[name='sab_username']").val(); - var sab_password = $("input=[name='sab_password']").val(); - var sab_apiKey = $("input=[name='sab_apikey']").val(); - - $.get(sbRoot+"/home/testSABnzbd", {'host': sab_host, 'username': sab_username, 'password': sab_password, 'apikey': sab_apiKey}, - function (data){ $('#testSABnzbd-result').html(data); }); - }); - - - $('#torrent_method').change($(this).torrent_method_handler); - - $(this).torrent_method_handler(); - - $('#use_torrents').click(function(){ - toggle_torrent_title(); - - }); - - - $('#testTorrent').click(function(){ - $('#testTorrent-result').html(loading); - var torrent_method = $('#torrent_method :selected').val(); - var torrent_host = $('#torrent_host').val(); - var torrent_username = $('#torrent_username').val(); - var torrent_password = $('#torrent_password').val(); - - $.get(sbRoot+"/home/testTorrent", {'torrent_method': torrent_method, 'host': torrent_host, 'username': torrent_username, 'password': torrent_password}, - function (data){ $('#testTorrent-result').html(data); }); - }); - $('#prefered_method').change($(this).prefered_method_handler); - - $(this).prefered_method_handler(); - -}); +$(document).ready(function(){ + var loading = '<img src="'+sbRoot+'/images/loading16.gif" height="16" width="16" />'; + + function toggle_torrent_title(){ + if ($('#use_torrents').prop('checked')) + $('#no-torrents').show(); + else + $('#no-torrents').hide(); + } + + $.fn.nzb_method_handler = function() { + + var selectedProvider = $('#nzb_method :selected').val(); + + if (selectedProvider == "blackhole") { + $('#blackhole_settings').show(); + $('#sabnzbd_settings').hide(); + $('#testSABnzbd').hide(); + $('#testSABnzbd-result').hide(); + $('#nzbget_settings').hide(); + } else if (selectedProvider == "nzbget") { + $('#blackhole_settings').hide(); + $('#sabnzbd_settings').hide(); + $('#testSABnzbd').hide(); + $('#testSABnzbd-result').hide(); + $('#nzbget_settings').show(); + } else { + $('#blackhole_settings').hide(); + $('#sabnzbd_settings').show(); + $('#testSABnzbd').show(); + $('#testSABnzbd-result').show(); + $('#nzbget_settings').hide(); + } + + } + + $.fn.torrent_method_handler = function() { + + var selectedProvider = $('#torrent_method :selected').val(); + + if (selectedProvider == "blackhole") { + $('#t_blackhole_settings').show(); + $('#torrent_settings').hide(); + } else if (selectedProvider == "utorrent") { + $('#t_blackhole_settings').hide(); + $('#torrent_settings').show(); + $('#Torrent_username').show() + $('#Torrent_Path').hide(); + $('#Torrent_Ratio').hide(); + $('#Torrent_Label').show() + $('#host_desc').text('uTorrent Host'); + $('#username_desc').text('uTorrent Username'); + $('#password_desc').text('uTorrent Password'); + $('#label_desc').text('uTorrent Label'); + } else if (selectedProvider == "transmission"){ + $('#t_blackhole_settings').hide(); + $('#torrent_settings').show(); + $('#Torrent_username').show(); + $('#Torrent_Path').show(); + $('#Torrent_Ratio').show(); + $('#Torrent_Label').hide(); + $('#host_desc').html('Transmission Host'); + $('#username_desc').text('Transmission Username'); + $('#password_desc').text('Transmission Password'); + $('#directory_desc').text('Transmission Directory'); + } else if (selectedProvider == "deluge"){ + $('#t_blackhole_settings').hide(); + $('#torrent_settings').show(); + $('#Torrent_Label').show(); + $('#Torrent_username').hide(); + $('#Torrent_Path').show(); + $('#Torrent_Ratio').show(); + $('#host_desc').text('Deluge Host'); + $('#username_desc').text('Deluge Username'); + $('#password_desc').text('Deluge Password'); + $('#label_desc').text('Deluge Label'); + $('#directory_desc').text('Deluge Directory'); + } else if (selectedProvider == "download_station"){ + $('#t_blackhole_settings').hide(); + $('#torrent_settings').show(); + $('#Torrent_Label').hide(); + $('#Torrent_username').show(); + $('#Torrent_Paused').hide(); + $('#Torrent_Path').hide(); + $('#Torrent_Ratio').hide(); + $('#Torrent_High_Bandwidth').hide(); + $('#host_desc').text('Synology Host'); + $('#username_desc').text('Synology Username'); + $('#password_desc').text('Synology Password'); + $('#label_desc').text('Synology Label'); + $('#directory_desc').text('Synology Directory'); + } + } + + $('#nzb_method').change($(this).nzb_method_handler); + + $(this).nzb_method_handler(); + + $('#testSABnzbd').click(function(){ + $('#testSABnzbd-result').html(loading); + var sab_host = $('#sab_host').val(); + var sab_username = $('#sab_username').val(); + var sab_password = $('#sab_password').val(); + var sab_apiKey = $('#sab_apikey').val(); + + $.get(sbRoot+"/home/testSABnzbd", {'host': sab_host, 'username': sab_username, 'password': sab_password, 'apikey': sab_apiKey}, + function (data){ $('#testSABnzbd-result').html(data); }); + }); + + + $('#torrent_method').change($(this).torrent_method_handler); + + $(this).torrent_method_handler(); + + $('#use_torrents').click(function(){ + toggle_torrent_title(); + }); + + $('#testTorrent').click(function(){ + $('#testTorrent-result').html(loading); + var torrent_method = $('#torrent_method :selected').val(); + var torrent_host = $('#torrent_host').val(); + var torrent_username = $('#torrent_username').val(); + var torrent_password = $('#torrent_password').val(); + + $.get(sbRoot+"/home/testTorrent", {'torrent_method': torrent_method, 'host': torrent_host, 'username': torrent_username, 'password': torrent_password}, + function (data){ $('#testTorrent-result').html(data); }); + }); + $('#prefered_method').change($(this).prefered_method_handler); + + $(this).prefered_method_handler(); + +}); diff --git a/data/js/displayShow.js b/data/js/displayShow.js index 1c84a7c83ed46b5e2e2ac0d0650f78b7d14299f5..16255a4d48ed4a46b6c1b57a535fcecc520fb3fa 100644 --- a/data/js/displayShow.js +++ b/data/js/displayShow.js @@ -1,194 +1,185 @@ -$(document).ready(function(){ - - $('#sbRoot').ajaxEpSearch({'colorRow': true}); - $('#sbRoot').ajaxEpSubtitlesSearch(); - $('#sbRoot').ajaxHisttrunc(); - - $('#seasonJump').change(function() { - var id = $(this).val(); - if (id && id != 'jump') { - $('html,body').animate({scrollTop: $(id).offset().top},'slow'); - location.hash = id; - } - $(this).val('jump'); - }); - - $("#prevShow").click(function(){ - var show = $('#pickShow option:selected'); - if (show.prev('option').length < 1){ - show.parent().children('option:last').attr('selected', 'selected'); - } else{ - show.prev('option').attr('selected', 'selected'); - }; - $("#pickShow").change(); - }); - - $("#nextShow").click(function(){ - var show = $('#pickShow option:selected'); - if (show.next('option').length < 1){ - show.parent().children('option:first').attr('selected', 'selected'); - } else{ - show.next('option').attr('selected', 'selected'); - }; - $("#pickShow").change(); - }); - - $('#changeStatus').click(function(){ - var sbRoot = $('#sbRoot').val() - var epArr = new Array() - - $('.epCheck').each(function() { - - if (this.checked == true) { - epArr.push($(this).attr('id')) - } - - }); - - if (epArr.length == 0) - return false - - url = sbRoot+'/home/setStatus?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&status='+$('#statusSelect').attr('value') - window.location.href = url - - }); - - $('#changeAudio').click(function(){ - var sbRoot = $('#sbRoot').val() - var epArr = new Array() - - $('.epCheck').each(function() { - - if (this.checked == true) { - epArr.push($(this).attr('id')) - } - - }); - - if (epArr.length == 0) - return false - - url = sbRoot+'/home/setAudio?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&audio_langs='+$('#audioSelect').attr('value') - window.location.href = url - - }); - - $('.seasonCheck').click(function(){ - var seasCheck = this; - var seasNo = $(seasCheck).attr('id'); - - $('.epCheck:visible').each(function(){ - var epParts = $(this).attr('id').split('x') - - if (epParts[0] == seasNo) { - this.checked = seasCheck.checked - } - }); - }); - - var lastCheck = null; - $('.epCheck').click(function(event) { - - if(!lastCheck || !event.shiftKey) { - lastCheck = this; - return; - } - - var check = this; - var found = 0; - - $('.epCheck').each(function() { - switch (found) { - case 2: return false; - case 1: this.checked = lastCheck.checked; - } - - if (this == check || this == lastCheck) - found++; - }); - - lastClick = this; - }); - - // selects all visible episode checkboxes. - $('.seriesCheck').click(function(){ - $('.epCheck:visible').each(function(){ - this.checked = true - }); - $('.seasonCheck:visible').each(function(){ - this.checked = true - }) - }); - - // clears all visible episode checkboxes and the season selectors - $('.clearAll').click(function(){ - $('.epCheck:visible').each(function(){ - this.checked = false - }); - $('.seasonCheck:visible').each(function(){ - this.checked = false - }); - }); - - // handle the show selection dropbox - $('#pickShow').change(function(){ - var sbRoot = $('#sbRoot').val() - var val = $(this).attr('value') - if (val == 0) - return - url = sbRoot+'/home/displayShow?show='+val - window.location.href = url - }); - - // show/hide different types of rows when the checkboxes are changed - $("#checkboxControls input").change(function(e){ - var whichClass = $(this).attr('id') - $(this).showHideRows(whichClass) - return - $('tr.'+whichClass).each(function(i){ - $(this).toggle(); - }); - }); - - // initially show/hide all the rows according to the checkboxes - $("#checkboxControls input").each(function(e){ - var status = this.checked; - $("tr."+$(this).attr('id')).each(function(e){ - if (status) { - $(this).show(); - } else { - $(this).hide(); - } - }); - }); - - $.fn.showHideRows = function(whichClass){ - - var status = $('#checkboxControls > input, #'+whichClass).prop('checked') - $("tr."+whichClass).each(function(e){ - if (status) { - $(this).show(); - } else { - $(this).hide(); - } - }); - - // hide season headers with no episodes under them - $('tr.seasonheader').each(function(){ - var numRows = 0 - var seasonNo = $(this).attr('id') - $('tr.'+seasonNo+' :visible').each(function(){ - numRows++ - }) - if (numRows == 0) { - $(this).hide() - $('#'+seasonNo+'-cols').hide() - } else { - $(this).show() - $('#'+seasonNo+'-cols').show() - } - - }); - } - -}); +$(document).ready(function(){ + + $('#sbRoot').ajaxEpSearch({'colorRow': true}); + + $('#sbRoot').ajaxEpSubtitlesSearch(); + $('#sbRoot').ajaxHisttrunc(); + + $('#seasonJump').change(function() { + var id = $(this).val(); + if (id && id != 'jump') { + $('html,body').animate({scrollTop: $(id).offset().top},'slow'); + location.hash = id; + } + $(this).val('jump'); + }); + + $("#prevShow").click(function(){ + $('#pickShow option:selected').prev('option').attr('selected', 'selected'); + $("#pickShow").change(); + }); + + $("#nextShow").click(function(){ + $('#pickShow option:selected').next('option').attr('selected', 'selected'); + $("#pickShow").change(); + }); + + $('#changeStatus').click(function(){ + var sbRoot = $('#sbRoot').val() + var epArr = new Array() + + $('.epCheck').each(function() { + + if (this.checked == true) { + epArr.push($(this).attr('id')) + } + + }); + + if (epArr.length == 0) + return false + + url = sbRoot+'/home/setStatus?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&status='+$('#statusSelect').attr('value') + window.location.href = url + + }); + + $('#changeAudio').click(function(){ + var sbRoot = $('#sbRoot').val() + var epArr = new Array() + + $('.epCheck').each(function() { + + if (this.checked == true) { + epArr.push($(this).attr('id')) + } + + }); + + if (epArr.length == 0) + return false + + url = sbRoot+'/home/setAudio?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&audio_langs='+$('#audioSelect').attr('value') + window.location.href = url + + }); + + $('.seasonCheck').click(function(){ + var seasCheck = this; + var seasNo = $(seasCheck).attr('id'); + + $('.epCheck:visible').each(function(){ + var epParts = $(this).attr('id').split('x') + + if (epParts[0] == seasNo) { + this.checked = seasCheck.checked + } + }); + }); + + var lastCheck = null; + $('.epCheck').click(function(event) { + + if(!lastCheck || !event.shiftKey) { + lastCheck = this; + return; + } + + var check = this; + var found = 0; + + $('.epCheck').each(function() { + switch (found) { + case 2: return false; + case 1: this.checked = lastCheck.checked; + } + + if (this == check || this == lastCheck) + found++; + }); + + lastClick = this; + }); + + // selects all visible episode checkboxes. + $('.seriesCheck').click(function(){ + $('.epCheck:visible').each(function(){ + this.checked = true + }); + $('.seasonCheck:visible').each(function(){ + this.checked = true + }) + }); + + // clears all visible episode checkboxes and the season selectors + $('.clearAll').click(function(){ + $('.epCheck:visible').each(function(){ + this.checked = false + }); + $('.seasonCheck:visible').each(function(){ + this.checked = false + }); + }); + + // handle the show selection dropbox + $('#pickShow').change(function(){ + var sbRoot = $('#sbRoot').val() + var val = $(this).attr('value') + if (val == 0) + return + url = sbRoot+'/home/displayShow?show='+val + window.location.href = url + }); + + // show/hide different types of rows when the checkboxes are changed + $("#checkboxControls input").change(function(e){ + var whichClass = $(this).attr('id') + $(this).showHideRows(whichClass) + return + $('tr.'+whichClass).each(function(i){ + $(this).toggle(); + }); + }); + + // initially show/hide all the rows according to the checkboxes + $("#checkboxControls input").each(function(e){ + var status = this.checked; + $("tr."+$(this).attr('id')).each(function(e){ + if (status) { + $(this).show(); + } else { + $(this).hide(); + } + }); + }); + + $.fn.showHideRows = function(whichClass){ + + var status = $('#checkboxControls > input, #'+whichClass).prop('checked') + $("tr."+whichClass).each(function(e){ + if (status) { + $(this).show(); + } else { + $(this).hide(); + } + }); + + // hide season headers with no episodes under them + $('tr.seasonheader').each(function(){ + var numRows = 0 + var seasonNo = $(this).attr('id') + $('tr.'+seasonNo+' :visible').each(function(){ + numRows++ + }) + if (numRows == 0) { + $(this).hide() + $('#'+seasonNo+'-cols').hide() + } else { + $(this).show() + $('#'+seasonNo+'-cols').show() + } + + }); + } + +}); diff --git a/data/js/fancybox/blank.gif b/data/js/fancybox/blank.gif new file mode 100644 index 0000000000000000000000000000000000000000..35d42e808f0a8017b8d52a06be2f8fec0b466a66 Binary files /dev/null and b/data/js/fancybox/blank.gif differ diff --git a/data/js/fancybox/fancy_close.png b/data/js/fancybox/fancy_close.png new file mode 100644 index 0000000000000000000000000000000000000000..07035307ad435f8f2f8eedf0bce50f7ec8a858c2 Binary files /dev/null and b/data/js/fancybox/fancy_close.png differ diff --git a/data/js/fancybox/fancy_loading.png b/data/js/fancybox/fancy_loading.png new file mode 100644 index 0000000000000000000000000000000000000000..2503017960b3972499d3aa92f89953935ae40934 Binary files /dev/null and b/data/js/fancybox/fancy_loading.png differ diff --git a/data/js/fancybox/fancy_nav_left.png b/data/js/fancybox/fancy_nav_left.png new file mode 100644 index 0000000000000000000000000000000000000000..ebaa6a4fd34e51575a01da366312c20618985cbc Binary files /dev/null and b/data/js/fancybox/fancy_nav_left.png differ diff --git a/data/js/fancybox/fancy_nav_right.png b/data/js/fancybox/fancy_nav_right.png new file mode 100644 index 0000000000000000000000000000000000000000..873294e969db9160f5ddd4e1ab498ff60b080e3f Binary files /dev/null and b/data/js/fancybox/fancy_nav_right.png differ diff --git a/data/js/fancybox/fancy_shadow_e.png b/data/js/fancybox/fancy_shadow_e.png new file mode 100644 index 0000000000000000000000000000000000000000..2eda0893649371f8d92b92976d8542cdd1b601ed Binary files /dev/null and b/data/js/fancybox/fancy_shadow_e.png differ diff --git a/data/js/fancybox/fancy_shadow_n.png b/data/js/fancybox/fancy_shadow_n.png new file mode 100644 index 0000000000000000000000000000000000000000..69aa10e233b039077e7dc600177ddb1eb46217e3 Binary files /dev/null and b/data/js/fancybox/fancy_shadow_n.png differ diff --git a/data/js/fancybox/fancy_shadow_ne.png b/data/js/fancybox/fancy_shadow_ne.png new file mode 100644 index 0000000000000000000000000000000000000000..79f6980a3ba5c43de120d963dbba2516b8f27ac7 Binary files /dev/null and b/data/js/fancybox/fancy_shadow_ne.png differ diff --git a/data/js/fancybox/fancy_shadow_nw.png b/data/js/fancybox/fancy_shadow_nw.png new file mode 100644 index 0000000000000000000000000000000000000000..7182cd938ae98e7e28c65a0bc55df576042ff9f5 Binary files /dev/null and b/data/js/fancybox/fancy_shadow_nw.png differ diff --git a/data/js/fancybox/fancy_shadow_s.png b/data/js/fancybox/fancy_shadow_s.png new file mode 100644 index 0000000000000000000000000000000000000000..d8858bfb78efb8d7268736920efa1eae8873f89c Binary files /dev/null and b/data/js/fancybox/fancy_shadow_s.png differ diff --git a/data/js/fancybox/fancy_shadow_se.png b/data/js/fancybox/fancy_shadow_se.png new file mode 100644 index 0000000000000000000000000000000000000000..541e3ffd3e88224b34a4d2097c66a780e6060aeb Binary files /dev/null and b/data/js/fancybox/fancy_shadow_se.png differ diff --git a/data/js/fancybox/fancy_shadow_sw.png b/data/js/fancybox/fancy_shadow_sw.png new file mode 100644 index 0000000000000000000000000000000000000000..b451689fa7b57b7432820e4c06d0864c143c79ab Binary files /dev/null and b/data/js/fancybox/fancy_shadow_sw.png differ diff --git a/data/js/fancybox/fancy_shadow_w.png b/data/js/fancybox/fancy_shadow_w.png new file mode 100644 index 0000000000000000000000000000000000000000..8a4e4a887f18384a204563c0048c9cd1328f7faa Binary files /dev/null and b/data/js/fancybox/fancy_shadow_w.png differ diff --git a/data/js/fancybox/fancy_title_left.png b/data/js/fancybox/fancy_title_left.png new file mode 100644 index 0000000000000000000000000000000000000000..6049223d1ec6af46e100499c01f6489c9e2c6240 Binary files /dev/null and b/data/js/fancybox/fancy_title_left.png differ diff --git a/data/js/fancybox/fancy_title_main.png b/data/js/fancybox/fancy_title_main.png new file mode 100644 index 0000000000000000000000000000000000000000..8044271f29b5d4e4471570e75cdce90bf9a1497c Binary files /dev/null and b/data/js/fancybox/fancy_title_main.png differ diff --git a/data/js/fancybox/fancy_title_over.png b/data/js/fancybox/fancy_title_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f458f4bb8770466e44ba97dd8fe1f2936090db Binary files /dev/null and b/data/js/fancybox/fancy_title_over.png differ diff --git a/data/js/fancybox/fancy_title_right.png b/data/js/fancybox/fancy_title_right.png new file mode 100644 index 0000000000000000000000000000000000000000..e36d9db2a7c6e570aec993d3665cbc13620115e2 Binary files /dev/null and b/data/js/fancybox/fancy_title_right.png differ diff --git a/data/js/fancybox/fancybox-x.png b/data/js/fancybox/fancybox-x.png new file mode 100644 index 0000000000000000000000000000000000000000..c2130f8698f682d68b1550bffedecfe19eaa1a81 Binary files /dev/null and b/data/js/fancybox/fancybox-x.png differ diff --git a/data/js/fancybox/fancybox-y.png b/data/js/fancybox/fancybox-y.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef399b9908976fc36f760fad7876a4d9c38e006 Binary files /dev/null and b/data/js/fancybox/fancybox-y.png differ diff --git a/data/js/fancybox/fancybox.png b/data/js/fancybox/fancybox.png new file mode 100644 index 0000000000000000000000000000000000000000..65e14f68fd83b87f75c22c0c074e7b20bf20a133 Binary files /dev/null and b/data/js/fancybox/fancybox.png differ diff --git a/data/js/fancybox/jquery.easing-1.3.pack.js b/data/js/fancybox/jquery.easing-1.3.pack.js new file mode 100644 index 0000000000000000000000000000000000000000..9028179e7bde19e3c0fdbf9f422c35c4a0d95034 --- /dev/null +++ b/data/js/fancybox/jquery.easing-1.3.pack.js @@ -0,0 +1,72 @@ +/* + * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ + * + * Uses the built in easing capabilities added In jQuery 1.1 + * to offer multiple easing options + * + * TERMS OF USE - jQuery Easing + * + * Open source under the BSD License. + * + * Copyright © 2008 George McGinley Smith + * 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. + * + * Neither the name of the author nor the names of contributors may 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 OWNER 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. + * +*/ + +// t: current time, b: begInnIng value, c: change In value, d: duration +eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('h.i[\'1a\']=h.i[\'z\'];h.O(h.i,{y:\'D\',z:9(x,t,b,c,d){6 h.i[h.i.y](x,t,b,c,d)},17:9(x,t,b,c,d){6 c*(t/=d)*t+b},D:9(x,t,b,c,d){6-c*(t/=d)*(t-2)+b},13:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t+b;6-c/2*((--t)*(t-2)-1)+b},X:9(x,t,b,c,d){6 c*(t/=d)*t*t+b},U:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t+1)+b},R:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t+b;6 c/2*((t-=2)*t*t+2)+b},N:9(x,t,b,c,d){6 c*(t/=d)*t*t*t+b},M:9(x,t,b,c,d){6-c*((t=t/d-1)*t*t*t-1)+b},L:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t+b;6-c/2*((t-=2)*t*t*t-2)+b},K:9(x,t,b,c,d){6 c*(t/=d)*t*t*t*t+b},J:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t*t*t+1)+b},I:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t*t+b;6 c/2*((t-=2)*t*t*t*t+2)+b},G:9(x,t,b,c,d){6-c*8.C(t/d*(8.g/2))+c+b},15:9(x,t,b,c,d){6 c*8.n(t/d*(8.g/2))+b},12:9(x,t,b,c,d){6-c/2*(8.C(8.g*t/d)-1)+b},Z:9(x,t,b,c,d){6(t==0)?b:c*8.j(2,10*(t/d-1))+b},Y:9(x,t,b,c,d){6(t==d)?b+c:c*(-8.j(2,-10*t/d)+1)+b},W:9(x,t,b,c,d){e(t==0)6 b;e(t==d)6 b+c;e((t/=d/2)<1)6 c/2*8.j(2,10*(t-1))+b;6 c/2*(-8.j(2,-10*--t)+2)+b},V:9(x,t,b,c,d){6-c*(8.o(1-(t/=d)*t)-1)+b},S:9(x,t,b,c,d){6 c*8.o(1-(t=t/d-1)*t)+b},Q:9(x,t,b,c,d){e((t/=d/2)<1)6-c/2*(8.o(1-t*t)-1)+b;6 c/2*(8.o(1-(t-=2)*t)+1)+b},P:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6-(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b},H:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6 a*8.j(2,-10*t)*8.n((t*d-s)*(2*8.g)/p)+c+b},T:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d/2)==2)6 b+c;e(!p)p=d*(.3*1.5);e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);e(t<1)6-.5*(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b;6 a*8.j(2,-10*(t-=1))*8.n((t*d-s)*(2*8.g)/p)*.5+c+b},F:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*(t/=d)*t*((s+1)*t-s)+b},E:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*((t=t/d-1)*t*((s+1)*t+s)+1)+b},16:9(x,t,b,c,d,s){e(s==u)s=1.l;e((t/=d/2)<1)6 c/2*(t*t*(((s*=(1.B))+1)*t-s))+b;6 c/2*((t-=2)*t*(((s*=(1.B))+1)*t+s)+2)+b},A:9(x,t,b,c,d){6 c-h.i.v(x,d-t,0,c,d)+b},v:9(x,t,b,c,d){e((t/=d)<(1/2.k)){6 c*(7.q*t*t)+b}m e(t<(2/2.k)){6 c*(7.q*(t-=(1.5/2.k))*t+.k)+b}m e(t<(2.5/2.k)){6 c*(7.q*(t-=(2.14/2.k))*t+.11)+b}m{6 c*(7.q*(t-=(2.18/2.k))*t+.19)+b}},1b:9(x,t,b,c,d){e(t<d/2)6 h.i.A(x,t*2,0,c,d)*.5+b;6 h.i.v(x,t*2-d,0,c,d)*.5+c*.5+b}});',62,74,'||||||return||Math|function|||||if|var|PI|jQuery|easing|pow|75|70158|else|sin|sqrt||5625|asin|||undefined|easeOutBounce|abs||def|swing|easeInBounce|525|cos|easeOutQuad|easeOutBack|easeInBack|easeInSine|easeOutElastic|easeInOutQuint|easeOutQuint|easeInQuint|easeInOutQuart|easeOutQuart|easeInQuart|extend|easeInElastic|easeInOutCirc|easeInOutCubic|easeOutCirc|easeInOutElastic|easeOutCubic|easeInCirc|easeInOutExpo|easeInCubic|easeOutExpo|easeInExpo||9375|easeInOutSine|easeInOutQuad|25|easeOutSine|easeInOutBack|easeInQuad|625|984375|jswing|easeInOutBounce'.split('|'),0,{})) + +/* + * + * TERMS OF USE - EASING EQUATIONS + * + * Open source under the BSD License. + * + * Copyright © 2001 Robert Penner + * 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. + * + * Neither the name of the author nor the names of contributors may 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 OWNER 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. + * + */ diff --git a/data/js/fancybox/jquery.fancybox-1.3.4.css b/data/js/fancybox/jquery.fancybox-1.3.4.css new file mode 100644 index 0000000000000000000000000000000000000000..8fe0fbd5a52d002a659876ccb528987f387883f3 --- /dev/null +++ b/data/js/fancybox/jquery.fancybox-1.3.4.css @@ -0,0 +1,359 @@ +/* + * FancyBox - jQuery Plugin + * Simple and fancy lightbox alternative + * + * Examples and documentation at: http://fancybox.net + * + * Copyright (c) 2008 - 2010 Janis Skarnelis + * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. + * + * Version: 1.3.4 (11/11/2010) + * Requires: jQuery v1.3+ + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ + +#fancybox-loading { + position: fixed; + top: 50%; + left: 50%; + width: 40px; + height: 40px; + margin-top: -20px; + margin-left: -20px; + cursor: pointer; + overflow: hidden; + z-index: 1104; + display: none; +} + +#fancybox-loading div { + position: absolute; + top: 0; + left: 0; + width: 40px; + height: 480px; + background-image: url('fancybox.png'); +} + +#fancybox-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 1100; + display: none; +} + +#fancybox-tmp { + padding: 0; + margin: 0; + border: 0; + overflow: auto; + display: none; +} + +#fancybox-wrap { + position: absolute; + top: 0; + left: 0; + padding: 20px; + z-index: 1101; + outline: none; + display: none; +} + +#fancybox-outer { + position: relative; + width: 100%; + height: 100%; + background: #fff; +} + +#fancybox-content { + width: 0; + height: 0; + padding: 0; + outline: none; + position: relative; + overflow: hidden; + z-index: 1102; + border: 0px solid #fff; +} + +#fancybox-hide-sel-frame { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + z-index: 1101; +} + +#fancybox-close { + position: absolute; + top: -15px; + right: -15px; + width: 30px; + height: 30px; + background: transparent url('fancybox.png') -40px 0px; + cursor: pointer; + z-index: 1103; + display: none; +} + +#fancybox-error { + color: #444; + font: normal 12px/20px Arial; + padding: 14px; + margin: 0; +} + +#fancybox-img { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + border: none; + outline: none; + line-height: 0; + vertical-align: top; +} + +#fancybox-frame { + width: 100%; + height: 100%; + border: none; + display: block; +} + +#fancybox-left, #fancybox-right { + position: absolute; + bottom: 0px; + height: 100%; + width: 35%; + cursor: pointer; + outline: none; + background: transparent url('blank.gif'); + z-index: 1102; + display: none; +} + +#fancybox-left { + left: 0px; +} + +#fancybox-right { + right: 0px; +} + +#fancybox-left-ico, #fancybox-right-ico { + position: absolute; + top: 50%; + left: -9999px; + width: 30px; + height: 30px; + margin-top: -15px; + cursor: pointer; + z-index: 1102; + display: block; +} + +#fancybox-left-ico { + background-image: url('fancybox.png'); + background-position: -40px -30px; +} + +#fancybox-right-ico { + background-image: url('fancybox.png'); + background-position: -40px -60px; +} + +#fancybox-left:hover, #fancybox-right:hover { + visibility: visible; /* IE6 */ +} + +#fancybox-left:hover span { + left: 20px; +} + +#fancybox-right:hover span { + left: auto; + right: 20px; +} + +.fancybox-bg { + position: absolute; + padding: 0; + margin: 0; + border: 0; + width: 20px; + height: 20px; + z-index: 1001; +} + +#fancybox-bg-n { + top: -20px; + left: 0; + width: 100%; + background-image: url('fancybox-x.png'); +} + +#fancybox-bg-ne { + top: -20px; + right: -20px; + background-image: url('fancybox.png'); + background-position: -40px -162px; +} + +#fancybox-bg-e { + top: 0; + right: -20px; + height: 100%; + background-image: url('fancybox-y.png'); + background-position: -20px 0px; +} + +#fancybox-bg-se { + bottom: -20px; + right: -20px; + background-image: url('fancybox.png'); + background-position: -40px -182px; +} + +#fancybox-bg-s { + bottom: -20px; + left: 0; + width: 100%; + background-image: url('fancybox-x.png'); + background-position: 0px -20px; +} + +#fancybox-bg-sw { + bottom: -20px; + left: -20px; + background-image: url('fancybox.png'); + background-position: -40px -142px; +} + +#fancybox-bg-w { + top: 0; + left: -20px; + height: 100%; + background-image: url('fancybox-y.png'); +} + +#fancybox-bg-nw { + top: -20px; + left: -20px; + background-image: url('fancybox.png'); + background-position: -40px -122px; +} + +#fancybox-title { + font-family: Helvetica; + font-size: 12px; + z-index: 1102; +} + +.fancybox-title-inside { + padding-bottom: 10px; + text-align: center; + color: #333; + background: #fff; + position: relative; +} + +.fancybox-title-outside { + padding-top: 10px; + color: #fff; +} + +.fancybox-title-over { + position: absolute; + bottom: 0; + left: 0; + color: #FFF; + text-align: left; +} + +#fancybox-title-over { + padding: 10px; + background-image: url('fancy_title_over.png'); + display: block; +} + +.fancybox-title-float { + position: absolute; + left: 0; + bottom: -20px; + height: 32px; +} + +#fancybox-title-float-wrap { + border: none; + border-collapse: collapse; + width: auto; +} + +#fancybox-title-float-wrap td { + border: none; + white-space: nowrap; +} + +#fancybox-title-float-left { + padding: 0 0 0 15px; + background: url('fancybox.png') -40px -90px no-repeat; +} + +#fancybox-title-float-main { + color: #FFF; + line-height: 29px; + font-weight: bold; + padding: 0 0 3px 0; + background: url('fancybox-x.png') 0px -40px; +} + +#fancybox-title-float-right { + padding: 0 0 0 15px; + background: url('fancybox.png') -55px -90px no-repeat; +} + +/* IE6 */ + +.fancybox-ie6 #fancybox-close { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_close.png', sizingMethod='scale'); } + +.fancybox-ie6 #fancybox-left-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_left.png', sizingMethod='scale'); } +.fancybox-ie6 #fancybox-right-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_right.png', sizingMethod='scale'); } + +.fancybox-ie6 #fancybox-title-over { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_over.png', sizingMethod='scale'); zoom: 1; } +.fancybox-ie6 #fancybox-title-float-left { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_left.png', sizingMethod='scale'); } +.fancybox-ie6 #fancybox-title-float-main { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_main.png', sizingMethod='scale'); } +.fancybox-ie6 #fancybox-title-float-right { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_right.png', sizingMethod='scale'); } + +.fancybox-ie6 #fancybox-bg-w, .fancybox-ie6 #fancybox-bg-e, .fancybox-ie6 #fancybox-left, .fancybox-ie6 #fancybox-right, #fancybox-hide-sel-frame { + height: expression(this.parentNode.clientHeight + "px"); +} + +#fancybox-loading.fancybox-ie6 { + position: absolute; margin-top: 0; + top: expression( (-20 + (document.documentElement.clientHeight ? document.documentElement.clientHeight/2 : document.body.clientHeight/2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )) + 'px'); +} + +#fancybox-loading.fancybox-ie6 div { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_loading.png', sizingMethod='scale'); } + +/* IE6, IE7, IE8 */ + +.fancybox-ie .fancybox-bg { background: transparent !important; } + +.fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_n.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_ne.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_e.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_se.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_s.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_sw.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_w.png', sizingMethod='scale'); } +.fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_nw.png', sizingMethod='scale'); } \ No newline at end of file diff --git a/data/js/fancybox/jquery.fancybox-1.3.4.js b/data/js/fancybox/jquery.fancybox-1.3.4.js new file mode 100644 index 0000000000000000000000000000000000000000..a8520051dc780846e034c9c5ef7a72ce107e9c6c --- /dev/null +++ b/data/js/fancybox/jquery.fancybox-1.3.4.js @@ -0,0 +1,1156 @@ +/* + * FancyBox - jQuery Plugin + * Simple and fancy lightbox alternative + * + * Examples and documentation at: http://fancybox.net + * + * Copyright (c) 2008 - 2010 Janis Skarnelis + * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. + * + * Version: 1.3.4 (11/11/2010) + * Requires: jQuery v1.3+ + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ + +;(function($) { + var tmp, loading, overlay, wrap, outer, content, close, title, nav_left, nav_right, + + selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [], + + ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i, + + loadingTimer, loadingFrame = 1, + + titleHeight = 0, titleStr = '', start_pos, final_pos, busy = false, fx = $.extend($('<div/>')[0], { prop: 0 }), + + isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest, + + /* + * Private methods + */ + + _abort = function() { + loading.hide(); + + imgPreloader.onerror = imgPreloader.onload = null; + + if (ajaxLoader) { + ajaxLoader.abort(); + } + + tmp.empty(); + }, + + _error = function() { + if (false === selectedOpts.onError(selectedArray, selectedIndex, selectedOpts)) { + loading.hide(); + busy = false; + return; + } + + selectedOpts.titleShow = false; + + selectedOpts.width = 'auto'; + selectedOpts.height = 'auto'; + + tmp.html( '<p id="fancybox-error">The requested content cannot be loaded.<br />Please try again later.</p>' ); + + _process_inline(); + }, + + _start = function() { + var obj = selectedArray[ selectedIndex ], + href, + type, + title, + str, + emb, + ret; + + _abort(); + + selectedOpts = $.extend({}, $.fn.fancybox.defaults, (typeof $(obj).data('fancybox') == 'undefined' ? selectedOpts : $(obj).data('fancybox'))); + + ret = selectedOpts.onStart(selectedArray, selectedIndex, selectedOpts); + + if (ret === false) { + busy = false; + return; + } else if (typeof ret == 'object') { + selectedOpts = $.extend(selectedOpts, ret); + } + + title = selectedOpts.title || (obj.nodeName ? $(obj).attr('title') : obj.title) || ''; + + if (obj.nodeName && !selectedOpts.orig) { + selectedOpts.orig = $(obj).children("img:first").length ? $(obj).children("img:first") : $(obj); + } + + if (title === '' && selectedOpts.orig && selectedOpts.titleFromAlt) { + title = selectedOpts.orig.attr('alt'); + } + + href = selectedOpts.href || (obj.nodeName ? $(obj).attr('href') : obj.href) || null; + + if ((/^(?:javascript)/i).test(href) || href == '#') { + href = null; + } + + if (selectedOpts.type) { + type = selectedOpts.type; + + if (!href) { + href = selectedOpts.content; + } + + } else if (selectedOpts.content) { + type = 'html'; + + } else if (href) { + if (href.match(imgRegExp)) { + type = 'image'; + + } else if (href.match(swfRegExp)) { + type = 'swf'; + + } else if ($(obj).hasClass("iframe")) { + type = 'iframe'; + + } else if (href.indexOf("#") === 0) { + type = 'inline'; + + } else { + type = 'ajax'; + } + } + + if (!type) { + _error(); + return; + } + + if (type == 'inline') { + obj = href.substr(href.indexOf("#")); + type = $(obj).length > 0 ? 'inline' : 'ajax'; + } + + selectedOpts.type = type; + selectedOpts.href = href; + selectedOpts.title = title; + + if (selectedOpts.autoDimensions) { + if (selectedOpts.type == 'html' || selectedOpts.type == 'inline' || selectedOpts.type == 'ajax') { + selectedOpts.width = 'auto'; + selectedOpts.height = 'auto'; + } else { + selectedOpts.autoDimensions = false; + } + } + + if (selectedOpts.modal) { + selectedOpts.overlayShow = true; + selectedOpts.hideOnOverlayClick = false; + selectedOpts.hideOnContentClick = false; + selectedOpts.enableEscapeButton = false; + selectedOpts.showCloseButton = false; + } + + selectedOpts.padding = parseInt(selectedOpts.padding, 10); + selectedOpts.margin = parseInt(selectedOpts.margin, 10); + + tmp.css('padding', (selectedOpts.padding + selectedOpts.margin)); + + $('.fancybox-inline-tmp').unbind('fancybox-cancel').bind('fancybox-change', function() { + $(this).replaceWith(content.children()); + }); + + switch (type) { + case 'html' : + tmp.html( selectedOpts.content ); + _process_inline(); + break; + + case 'inline' : + if ( $(obj).parent().is('#fancybox-content') === true) { + busy = false; + return; + } + + $('<div class="fancybox-inline-tmp" />') + .hide() + .insertBefore( $(obj) ) + .bind('fancybox-cleanup', function() { + $(this).replaceWith(content.children()); + }).bind('fancybox-cancel', function() { + $(this).replaceWith(tmp.children()); + }); + + $(obj).appendTo(tmp); + + _process_inline(); + break; + + case 'image': + busy = false; + + $.fancybox.showActivity(); + + imgPreloader = new Image(); + + imgPreloader.onerror = function() { + _error(); + }; + + imgPreloader.onload = function() { + busy = true; + + imgPreloader.onerror = imgPreloader.onload = null; + + _process_image(); + }; + + imgPreloader.src = href; + break; + + case 'swf': + selectedOpts.scrolling = 'no'; + + str = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"><param name="movie" value="' + href + '"></param>'; + emb = ''; + + $.each(selectedOpts.swf, function(name, val) { + str += '<param name="' + name + '" value="' + val + '"></param>'; + emb += ' ' + name + '="' + val + '"'; + }); + + str += '<embed src="' + href + '" type="application/x-shockwave-flash" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"' + emb + '></embed></object>'; + + tmp.html(str); + + _process_inline(); + break; + + case 'ajax': + busy = false; + + $.fancybox.showActivity(); + + selectedOpts.ajax.win = selectedOpts.ajax.success; + + ajaxLoader = $.ajax($.extend({}, selectedOpts.ajax, { + url : href, + data : selectedOpts.ajax.data || {}, + error : function(XMLHttpRequest, textStatus, errorThrown) { + if ( XMLHttpRequest.status > 0 ) { + _error(); + } + }, + success : function(data, textStatus, XMLHttpRequest) { + var o = typeof XMLHttpRequest == 'object' ? XMLHttpRequest : ajaxLoader; + if (o.status == 200) { + if ( typeof selectedOpts.ajax.win == 'function' ) { + ret = selectedOpts.ajax.win(href, data, textStatus, XMLHttpRequest); + + if (ret === false) { + loading.hide(); + return; + } else if (typeof ret == 'string' || typeof ret == 'object') { + data = ret; + } + } + + tmp.html( data ); + _process_inline(); + } + } + })); + + break; + + case 'iframe': + _show(); + break; + } + }, + + _process_inline = function() { + var + w = selectedOpts.width, + h = selectedOpts.height; + + if (w.toString().indexOf('%') > -1) { + w = parseInt( ($(window).width() - (selectedOpts.margin * 2)) * parseFloat(w) / 100, 10) + 'px'; + + } else { + w = w == 'auto' ? 'auto' : w + 'px'; + } + + if (h.toString().indexOf('%') > -1) { + h = parseInt( ($(window).height() - (selectedOpts.margin * 2)) * parseFloat(h) / 100, 10) + 'px'; + + } else { + h = h == 'auto' ? 'auto' : h + 'px'; + } + + tmp.wrapInner('<div style="width:' + w + ';height:' + h + ';overflow: ' + (selectedOpts.scrolling == 'auto' ? 'auto' : (selectedOpts.scrolling == 'yes' ? 'scroll' : 'hidden')) + ';position:relative;"></div>'); + + selectedOpts.width = tmp.width(); + selectedOpts.height = tmp.height(); + + _show(); + }, + + _process_image = function() { + selectedOpts.width = imgPreloader.width; + selectedOpts.height = imgPreloader.height; + + $("<img />").attr({ + 'id' : 'fancybox-img', + 'src' : imgPreloader.src, + 'alt' : selectedOpts.title + }).appendTo( tmp ); + + _show(); + }, + + _show = function() { + var pos, equal; + + loading.hide(); + + if (wrap.is(":visible") && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { + $.event.trigger('fancybox-cancel'); + + busy = false; + return; + } + + busy = true; + + $(content.add( overlay )).unbind(); + + $(window).unbind("resize.fb scroll.fb"); + $(document).unbind('keydown.fb'); + + if (wrap.is(":visible") && currentOpts.titlePosition !== 'outside') { + wrap.css('height', wrap.height()); + } + + currentArray = selectedArray; + currentIndex = selectedIndex; + currentOpts = selectedOpts; + + if (currentOpts.overlayShow) { + overlay.css({ + 'background-color' : currentOpts.overlayColor, + 'opacity' : currentOpts.overlayOpacity, + 'cursor' : currentOpts.hideOnOverlayClick ? 'pointer' : 'auto', + 'height' : $(document).height() + }); + + if (!overlay.is(':visible')) { + if (isIE6) { + $('select:not(#fancybox-tmp select)').filter(function() { + return this.style.visibility !== 'hidden'; + }).css({'visibility' : 'hidden'}).one('fancybox-cleanup', function() { + this.style.visibility = 'inherit'; + }); + } + + overlay.show(); + } + } else { + overlay.hide(); + } + + final_pos = _get_zoom_to(); + + _process_title(); + + if (wrap.is(":visible")) { + $( close.add( nav_left ).add( nav_right ) ).hide(); + + pos = wrap.position(), + + start_pos = { + top : pos.top, + left : pos.left, + width : wrap.width(), + height : wrap.height() + }; + + equal = (start_pos.width == final_pos.width && start_pos.height == final_pos.height); + + content.fadeTo(currentOpts.changeFade, 0.3, function() { + var finish_resizing = function() { + content.html( tmp.contents() ).fadeTo(currentOpts.changeFade, 1, _finish); + }; + + $.event.trigger('fancybox-change'); + + content + .empty() + .removeAttr('filter') + .css({ + 'border-width' : currentOpts.padding, + 'width' : final_pos.width - currentOpts.padding * 2, + 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 + }); + + if (equal) { + finish_resizing(); + + } else { + fx.prop = 0; + + $(fx).animate({prop: 1}, { + duration : currentOpts.changeSpeed, + easing : currentOpts.easingChange, + step : _draw, + complete : finish_resizing + }); + } + }); + + return; + } + + wrap.removeAttr("style"); + + content.css('border-width', currentOpts.padding); + + if (currentOpts.transitionIn == 'elastic') { + start_pos = _get_zoom_from(); + + content.html( tmp.contents() ); + + wrap.show(); + + if (currentOpts.opacity) { + final_pos.opacity = 0; + } + + fx.prop = 0; + + $(fx).animate({prop: 1}, { + duration : currentOpts.speedIn, + easing : currentOpts.easingIn, + step : _draw, + complete : _finish + }); + + return; + } + + if (currentOpts.titlePosition == 'inside' && titleHeight > 0) { + title.show(); + } + + content + .css({ + 'width' : final_pos.width - currentOpts.padding * 2, + 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 + }) + .html( tmp.contents() ); + + wrap + .css(final_pos) + .fadeIn( currentOpts.transitionIn == 'none' ? 0 : currentOpts.speedIn, _finish ); + }, + + _format_title = function(title) { + if (title && title.length) { + if (currentOpts.titlePosition == 'float') { + return '<table id="fancybox-title-float-wrap" cellpadding="0" cellspacing="0"><tr><td id="fancybox-title-float-left"></td><td id="fancybox-title-float-main">' + title + '</td><td id="fancybox-title-float-right"></td></tr></table>'; + } + + return '<div id="fancybox-title-' + currentOpts.titlePosition + '">' + title + '</div>'; + } + + return false; + }, + + _process_title = function() { + titleStr = currentOpts.title || ''; + titleHeight = 0; + + title + .empty() + .removeAttr('style') + .removeClass(); + + if (currentOpts.titleShow === false) { + title.hide(); + return; + } + + titleStr = $.isFunction(currentOpts.titleFormat) ? currentOpts.titleFormat(titleStr, currentArray, currentIndex, currentOpts) : _format_title(titleStr); + + if (!titleStr || titleStr === '') { + title.hide(); + return; + } + + title + .addClass('fancybox-title-' + currentOpts.titlePosition) + .html( titleStr ) + .appendTo( 'body' ) + .show(); + + switch (currentOpts.titlePosition) { + case 'inside': + title + .css({ + 'width' : final_pos.width - (currentOpts.padding * 2), + 'marginLeft' : currentOpts.padding, + 'marginRight' : currentOpts.padding + }); + + titleHeight = title.outerHeight(true); + + title.appendTo( outer ); + + final_pos.height += titleHeight; + break; + + case 'over': + title + .css({ + 'marginLeft' : currentOpts.padding, + 'width' : final_pos.width - (currentOpts.padding * 2), + 'bottom' : currentOpts.padding + }) + .appendTo( outer ); + break; + + case 'float': + title + .css('left', parseInt((title.width() - final_pos.width - 40)/ 2, 10) * -1) + .appendTo( wrap ); + break; + + default: + title + .css({ + 'width' : final_pos.width - (currentOpts.padding * 2), + 'paddingLeft' : currentOpts.padding, + 'paddingRight' : currentOpts.padding + }) + .appendTo( wrap ); + break; + } + + title.hide(); + }, + + _set_navigation = function() { + if (currentOpts.enableEscapeButton || currentOpts.enableKeyboardNav) { + $(document).bind('keydown.fb', function(e) { + if (e.keyCode == 27 && currentOpts.enableEscapeButton) { + e.preventDefault(); + $.fancybox.close(); + + } else if ((e.keyCode == 37 || e.keyCode == 39) && currentOpts.enableKeyboardNav && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') { + e.preventDefault(); + $.fancybox[ e.keyCode == 37 ? 'prev' : 'next'](); + } + }); + } + + if (!currentOpts.showNavArrows) { + nav_left.hide(); + nav_right.hide(); + return; + } + + if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex !== 0) { + nav_left.show(); + } + + if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex != (currentArray.length -1)) { + nav_right.show(); + } + }, + + _finish = function () { + if (!$.support.opacity) { + content.get(0).style.removeAttribute('filter'); + wrap.get(0).style.removeAttribute('filter'); + } + + if (selectedOpts.autoDimensions) { + content.css('height', 'auto'); + } + + wrap.css('height', 'auto'); + + if (titleStr && titleStr.length) { + title.show(); + } + + if (currentOpts.showCloseButton) { + close.show(); + } + + _set_navigation(); + + if (currentOpts.hideOnContentClick) { + content.bind('click', $.fancybox.close); + } + + if (currentOpts.hideOnOverlayClick) { + overlay.bind('click', $.fancybox.close); + } + + $(window).bind("resize.fb", $.fancybox.resize); + + if (currentOpts.centerOnScroll) { + $(window).bind("scroll.fb", $.fancybox.center); + } + + if (currentOpts.type == 'iframe') { + $('<iframe id="fancybox-frame" name="fancybox-frame' + new Date().getTime() + '" frameborder="0" hspace="0" ' + ($.browser.msie ? 'allowtransparency="true""' : '') + ' scrolling="' + selectedOpts.scrolling + '" src="' + currentOpts.href + '"></iframe>').appendTo(content); + } + + wrap.show(); + + busy = false; + + $.fancybox.center(); + + currentOpts.onComplete(currentArray, currentIndex, currentOpts); + + _preload_images(); + }, + + _preload_images = function() { + var href, + objNext; + + if ((currentArray.length -1) > currentIndex) { + href = currentArray[ currentIndex + 1 ].href; + + if (typeof href !== 'undefined' && href.match(imgRegExp)) { + objNext = new Image(); + objNext.src = href; + } + } + + if (currentIndex > 0) { + href = currentArray[ currentIndex - 1 ].href; + + if (typeof href !== 'undefined' && href.match(imgRegExp)) { + objNext = new Image(); + objNext.src = href; + } + } + }, + + _draw = function(pos) { + var dim = { + width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10), + height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10), + + top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10), + left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10) + }; + + if (typeof final_pos.opacity !== 'undefined') { + dim.opacity = pos < 0.5 ? 0.5 : pos; + } + + wrap.css(dim); + + content.css({ + 'width' : dim.width - currentOpts.padding * 2, + 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2 + }); + }, + + _get_viewport = function() { + return [ + $(window).width() - (currentOpts.margin * 2), + $(window).height() - (currentOpts.margin * 2), + $(document).scrollLeft() + currentOpts.margin, + $(document).scrollTop() + currentOpts.margin + ]; + }, + + _get_zoom_to = function () { + var view = _get_viewport(), + to = {}, + resize = currentOpts.autoScale, + double_padding = currentOpts.padding * 2, + ratio; + + if (currentOpts.width.toString().indexOf('%') > -1) { + to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10); + } else { + to.width = currentOpts.width + double_padding; + } + + if (currentOpts.height.toString().indexOf('%') > -1) { + to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10); + } else { + to.height = currentOpts.height + double_padding; + } + + if (resize && (to.width > view[0] || to.height > view[1])) { + if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') { + ratio = (currentOpts.width ) / (currentOpts.height ); + + if ((to.width ) > view[0]) { + to.width = view[0]; + to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10); + } + + if ((to.height) > view[1]) { + to.height = view[1]; + to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10); + } + + } else { + to.width = Math.min(to.width, view[0]); + to.height = Math.min(to.height, view[1]); + } + } + + to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10); + to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10); + + return to; + }, + + _get_obj_pos = function(obj) { + var pos = obj.offset(); + + pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0; + pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0; + + pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0; + pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0; + + pos.width = obj.width(); + pos.height = obj.height(); + + return pos; + }, + + _get_zoom_from = function() { + var orig = selectedOpts.orig ? $(selectedOpts.orig) : false, + from = {}, + pos, + view; + + if (orig && orig.length) { + pos = _get_obj_pos(orig); + + from = { + width : pos.width + (currentOpts.padding * 2), + height : pos.height + (currentOpts.padding * 2), + top : pos.top - currentOpts.padding - 20, + left : pos.left - currentOpts.padding - 20 + }; + + } else { + view = _get_viewport(); + + from = { + width : currentOpts.padding * 2, + height : currentOpts.padding * 2, + top : parseInt(view[3] + view[1] * 0.5, 10), + left : parseInt(view[2] + view[0] * 0.5, 10) + }; + } + + return from; + }, + + _animate_loading = function() { + if (!loading.is(':visible')){ + clearInterval(loadingTimer); + return; + } + + $('div', loading).css('top', (loadingFrame * -40) + 'px'); + + loadingFrame = (loadingFrame + 1) % 12; + }; + + /* + * Public methods + */ + + $.fn.fancybox = function(options) { + if (!$(this).length) { + return this; + } + + $(this) + .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {}))) + .unbind('click.fb') + .bind('click.fb', function(e) { + e.preventDefault(); + + if (busy) { + return; + } + + busy = true; + + $(this).blur(); + + selectedArray = []; + selectedIndex = 0; + + var rel = $(this).attr('rel') || ''; + + if (!rel || rel == '' || rel === 'nofollow') { + selectedArray.push(this); + + } else { + selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]"); + selectedIndex = selectedArray.index( this ); + } + + _start(); + + return; + }); + + return this; + }; + + $.fancybox = function(obj) { + var opts; + + if (busy) { + return; + } + + busy = true; + opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {}; + + selectedArray = []; + selectedIndex = parseInt(opts.index, 10) || 0; + + if ($.isArray(obj)) { + for (var i = 0, j = obj.length; i < j; i++) { + if (typeof obj[i] == 'object') { + $(obj[i]).data('fancybox', $.extend({}, opts, obj[i])); + } else { + obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts)); + } + } + + selectedArray = jQuery.merge(selectedArray, obj); + + } else { + if (typeof obj == 'object') { + $(obj).data('fancybox', $.extend({}, opts, obj)); + } else { + obj = $({}).data('fancybox', $.extend({content : obj}, opts)); + } + + selectedArray.push(obj); + } + + if (selectedIndex > selectedArray.length || selectedIndex < 0) { + selectedIndex = 0; + } + + _start(); + }; + + $.fancybox.showActivity = function() { + clearInterval(loadingTimer); + + loading.show(); + loadingTimer = setInterval(_animate_loading, 66); + }; + + $.fancybox.hideActivity = function() { + loading.hide(); + }; + + $.fancybox.next = function() { + return $.fancybox.pos( currentIndex + 1); + }; + + $.fancybox.prev = function() { + return $.fancybox.pos( currentIndex - 1); + }; + + $.fancybox.pos = function(pos) { + if (busy) { + return; + } + + pos = parseInt(pos); + + selectedArray = currentArray; + + if (pos > -1 && pos < currentArray.length) { + selectedIndex = pos; + _start(); + + } else if (currentOpts.cyclic && currentArray.length > 1) { + selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1; + _start(); + } + + return; + }; + + $.fancybox.cancel = function() { + if (busy) { + return; + } + + busy = true; + + $.event.trigger('fancybox-cancel'); + + _abort(); + + selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts); + + busy = false; + }; + + // Note: within an iframe use - parent.$.fancybox.close(); + $.fancybox.close = function() { + if (busy || wrap.is(':hidden')) { + return; + } + + busy = true; + + if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { + busy = false; + return; + } + + _abort(); + + $(close.add( nav_left ).add( nav_right )).hide(); + + $(content.add( overlay )).unbind(); + + $(window).unbind("resize.fb scroll.fb"); + $(document).unbind('keydown.fb'); + + content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank'); + + if (currentOpts.titlePosition !== 'inside') { + title.empty(); + } + + wrap.stop(); + + function _cleanup() { + overlay.fadeOut('fast'); + + title.empty().hide(); + wrap.hide(); + + $.event.trigger('fancybox-cleanup'); + + content.empty(); + + currentOpts.onClosed(currentArray, currentIndex, currentOpts); + + currentArray = selectedOpts = []; + currentIndex = selectedIndex = 0; + currentOpts = selectedOpts = {}; + + busy = false; + } + + if (currentOpts.transitionOut == 'elastic') { + start_pos = _get_zoom_from(); + + var pos = wrap.position(); + + final_pos = { + top : pos.top , + left : pos.left, + width : wrap.width(), + height : wrap.height() + }; + + if (currentOpts.opacity) { + final_pos.opacity = 1; + } + + title.empty().hide(); + + fx.prop = 1; + + $(fx).animate({ prop: 0 }, { + duration : currentOpts.speedOut, + easing : currentOpts.easingOut, + step : _draw, + complete : _cleanup + }); + + } else { + wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup); + } + }; + + $.fancybox.resize = function() { + if (overlay.is(':visible')) { + overlay.css('height', $(document).height()); + } + + $.fancybox.center(true); + }; + + $.fancybox.center = function() { + var view, align; + + if (busy) { + return; + } + + align = arguments[0] === true ? 1 : 0; + view = _get_viewport(); + + if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) { + return; + } + + wrap + .stop() + .animate({ + 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)), + 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding)) + }, typeof arguments[0] == 'number' ? arguments[0] : 200); + }; + + $.fancybox.init = function() { + if ($("#fancybox-wrap").length) { + return; + } + + $('body').append( + tmp = $('<div id="fancybox-tmp"></div>'), + loading = $('<div id="fancybox-loading"><div></div></div>'), + overlay = $('<div id="fancybox-overlay"></div>'), + wrap = $('<div id="fancybox-wrap"></div>') + ); + + outer = $('<div id="fancybox-outer"></div>') + .append('<div class="fancybox-bg" id="fancybox-bg-n"></div><div class="fancybox-bg" id="fancybox-bg-ne"></div><div class="fancybox-bg" id="fancybox-bg-e"></div><div class="fancybox-bg" id="fancybox-bg-se"></div><div class="fancybox-bg" id="fancybox-bg-s"></div><div class="fancybox-bg" id="fancybox-bg-sw"></div><div class="fancybox-bg" id="fancybox-bg-w"></div><div class="fancybox-bg" id="fancybox-bg-nw"></div>') + .appendTo( wrap ); + + outer.append( + content = $('<div id="fancybox-content"></div>'), + close = $('<a id="fancybox-close"></a>'), + title = $('<div id="fancybox-title"></div>'), + + nav_left = $('<a href="javascript:;" id="fancybox-left"><span class="fancy-ico" id="fancybox-left-ico"></span></a>'), + nav_right = $('<a href="javascript:;" id="fancybox-right"><span class="fancy-ico" id="fancybox-right-ico"></span></a>') + ); + + close.click($.fancybox.close); + loading.click($.fancybox.cancel); + + nav_left.click(function(e) { + e.preventDefault(); + $.fancybox.prev(); + }); + + nav_right.click(function(e) { + e.preventDefault(); + $.fancybox.next(); + }); + + if ($.fn.mousewheel) { + wrap.bind('mousewheel.fb', function(e, delta) { + if (busy) { + e.preventDefault(); + + } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) { + e.preventDefault(); + $.fancybox[ delta > 0 ? 'prev' : 'next'](); + } + }); + } + + if (!$.support.opacity) { + wrap.addClass('fancybox-ie'); + } + + if (isIE6) { + loading.addClass('fancybox-ie6'); + wrap.addClass('fancybox-ie6'); + + $('<iframe id="fancybox-hide-sel-frame" src="' + (/^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank' ) + '" scrolling="no" border="0" frameborder="0" tabindex="-1"></iframe>').prependTo(outer); + } + }; + + $.fn.fancybox.defaults = { + padding : 10, + margin : 40, + opacity : false, + modal : false, + cyclic : false, + scrolling : 'auto', // 'auto', 'yes' or 'no' + + width : 560, + height : 340, + + autoScale : true, + autoDimensions : true, + centerOnScroll : false, + + ajax : {}, + swf : { wmode: 'transparent' }, + + hideOnOverlayClick : true, + hideOnContentClick : false, + + overlayShow : true, + overlayOpacity : 0.7, + overlayColor : '#777', + + titleShow : true, + titlePosition : 'float', // 'float', 'outside', 'inside' or 'over' + titleFormat : null, + titleFromAlt : false, + + transitionIn : 'fade', // 'elastic', 'fade' or 'none' + transitionOut : 'fade', // 'elastic', 'fade' or 'none' + + speedIn : 300, + speedOut : 300, + + changeSpeed : 300, + changeFade : 'fast', + + easingIn : 'swing', + easingOut : 'swing', + + showCloseButton : true, + showNavArrows : true, + enableEscapeButton : true, + enableKeyboardNav : true, + + onStart : function(){}, + onCancel : function(){}, + onComplete : function(){}, + onCleanup : function(){}, + onClosed : function(){}, + onError : function(){} + }; + + $(document).ready(function() { + $.fancybox.init(); + }); + +})(jQuery); \ No newline at end of file diff --git a/data/js/fancybox/jquery.fancybox-1.3.4.pack.js b/data/js/fancybox/jquery.fancybox-1.3.4.pack.js new file mode 100644 index 0000000000000000000000000000000000000000..1373ed0838bc1be6841339fef703a07c1ee7267f --- /dev/null +++ b/data/js/fancybox/jquery.fancybox-1.3.4.pack.js @@ -0,0 +1,46 @@ +/* + * FancyBox - jQuery Plugin + * Simple and fancy lightbox alternative + * + * Examples and documentation at: http://fancybox.net + * + * Copyright (c) 2008 - 2010 Janis Skarnelis + * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. + * + * Version: 1.3.4 (11/11/2010) + * Requires: jQuery v1.3+ + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ + +;(function(b){var m,t,u,f,D,j,E,n,z,A,q=0,e={},o=[],p=0,d={},l=[],G=null,v=new Image,J=/\.(jpg|gif|png|bmp|jpeg)(.*)?$/i,W=/[^\.]\.(swf)\s*$/i,K,L=1,y=0,s="",r,i,h=false,B=b.extend(b("<div/>")[0],{prop:0}),M=b.browser.msie&&b.browser.version<7&&!window.XMLHttpRequest,N=function(){t.hide();v.onerror=v.onload=null;G&&G.abort();m.empty()},O=function(){if(false===e.onError(o,q,e)){t.hide();h=false}else{e.titleShow=false;e.width="auto";e.height="auto";m.html('<p id="fancybox-error">The requested content cannot be loaded.<br />Please try again later.</p>'); +F()}},I=function(){var a=o[q],c,g,k,C,P,w;N();e=b.extend({},b.fn.fancybox.defaults,typeof b(a).data("fancybox")=="undefined"?e:b(a).data("fancybox"));w=e.onStart(o,q,e);if(w===false)h=false;else{if(typeof w=="object")e=b.extend(e,w);k=e.title||(a.nodeName?b(a).attr("title"):a.title)||"";if(a.nodeName&&!e.orig)e.orig=b(a).children("img:first").length?b(a).children("img:first"):b(a);if(k===""&&e.orig&&e.titleFromAlt)k=e.orig.attr("alt");c=e.href||(a.nodeName?b(a).attr("href"):a.href)||null;if(/^(?:javascript)/i.test(c)|| +c=="#")c=null;if(e.type){g=e.type;if(!c)c=e.content}else if(e.content)g="html";else if(c)g=c.match(J)?"image":c.match(W)?"swf":b(a).hasClass("iframe")?"iframe":c.indexOf("#")===0?"inline":"ajax";if(g){if(g=="inline"){a=c.substr(c.indexOf("#"));g=b(a).length>0?"inline":"ajax"}e.type=g;e.href=c;e.title=k;if(e.autoDimensions)if(e.type=="html"||e.type=="inline"||e.type=="ajax"){e.width="auto";e.height="auto"}else e.autoDimensions=false;if(e.modal){e.overlayShow=true;e.hideOnOverlayClick=false;e.hideOnContentClick= +false;e.enableEscapeButton=false;e.showCloseButton=false}e.padding=parseInt(e.padding,10);e.margin=parseInt(e.margin,10);m.css("padding",e.padding+e.margin);b(".fancybox-inline-tmp").unbind("fancybox-cancel").bind("fancybox-change",function(){b(this).replaceWith(j.children())});switch(g){case "html":m.html(e.content);F();break;case "inline":if(b(a).parent().is("#fancybox-content")===true){h=false;break}b('<div class="fancybox-inline-tmp" />').hide().insertBefore(b(a)).bind("fancybox-cleanup",function(){b(this).replaceWith(j.children())}).bind("fancybox-cancel", +function(){b(this).replaceWith(m.children())});b(a).appendTo(m);F();break;case "image":h=false;b.fancybox.showActivity();v=new Image;v.onerror=function(){O()};v.onload=function(){h=true;v.onerror=v.onload=null;e.width=v.width;e.height=v.height;b("<img />").attr({id:"fancybox-img",src:v.src,alt:e.title}).appendTo(m);Q()};v.src=c;break;case "swf":e.scrolling="no";C='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'+e.width+'" height="'+e.height+'"><param name="movie" value="'+c+ +'"></param>';P="";b.each(e.swf,function(x,H){C+='<param name="'+x+'" value="'+H+'"></param>';P+=" "+x+'="'+H+'"'});C+='<embed src="'+c+'" type="application/x-shockwave-flash" width="'+e.width+'" height="'+e.height+'"'+P+"></embed></object>";m.html(C);F();break;case "ajax":h=false;b.fancybox.showActivity();e.ajax.win=e.ajax.success;G=b.ajax(b.extend({},e.ajax,{url:c,data:e.ajax.data||{},error:function(x){x.status>0&&O()},success:function(x,H,R){if((typeof R=="object"?R:G).status==200){if(typeof e.ajax.win== +"function"){w=e.ajax.win(c,x,H,R);if(w===false){t.hide();return}else if(typeof w=="string"||typeof w=="object")x=w}m.html(x);F()}}}));break;case "iframe":Q()}}else O()}},F=function(){var a=e.width,c=e.height;a=a.toString().indexOf("%")>-1?parseInt((b(window).width()-e.margin*2)*parseFloat(a)/100,10)+"px":a=="auto"?"auto":a+"px";c=c.toString().indexOf("%")>-1?parseInt((b(window).height()-e.margin*2)*parseFloat(c)/100,10)+"px":c=="auto"?"auto":c+"px";m.wrapInner('<div style="width:'+a+";height:"+c+ +";overflow: "+(e.scrolling=="auto"?"auto":e.scrolling=="yes"?"scroll":"hidden")+';position:relative;"></div>');e.width=m.width();e.height=m.height();Q()},Q=function(){var a,c;t.hide();if(f.is(":visible")&&false===d.onCleanup(l,p,d)){b.event.trigger("fancybox-cancel");h=false}else{h=true;b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");f.is(":visible")&&d.titlePosition!=="outside"&&f.css("height",f.height());l=o;p=q;d=e;if(d.overlayShow){u.css({"background-color":d.overlayColor, +opacity:d.overlayOpacity,cursor:d.hideOnOverlayClick?"pointer":"auto",height:b(document).height()});if(!u.is(":visible")){M&&b("select:not(#fancybox-tmp select)").filter(function(){return this.style.visibility!=="hidden"}).css({visibility:"hidden"}).one("fancybox-cleanup",function(){this.style.visibility="inherit"});u.show()}}else u.hide();i=X();s=d.title||"";y=0;n.empty().removeAttr("style").removeClass();if(d.titleShow!==false){if(b.isFunction(d.titleFormat))a=d.titleFormat(s,l,p,d);else a=s&&s.length? +d.titlePosition=="float"?'<table id="fancybox-title-float-wrap" cellpadding="0" cellspacing="0"><tr><td id="fancybox-title-float-left"></td><td id="fancybox-title-float-main">'+s+'</td><td id="fancybox-title-float-right"></td></tr></table>':'<div id="fancybox-title-'+d.titlePosition+'">'+s+"</div>":false;s=a;if(!(!s||s==="")){n.addClass("fancybox-title-"+d.titlePosition).html(s).appendTo("body").show();switch(d.titlePosition){case "inside":n.css({width:i.width-d.padding*2,marginLeft:d.padding,marginRight:d.padding}); +y=n.outerHeight(true);n.appendTo(D);i.height+=y;break;case "over":n.css({marginLeft:d.padding,width:i.width-d.padding*2,bottom:d.padding}).appendTo(D);break;case "float":n.css("left",parseInt((n.width()-i.width-40)/2,10)*-1).appendTo(f);break;default:n.css({width:i.width-d.padding*2,paddingLeft:d.padding,paddingRight:d.padding}).appendTo(f)}}}n.hide();if(f.is(":visible")){b(E.add(z).add(A)).hide();a=f.position();r={top:a.top,left:a.left,width:f.width(),height:f.height()};c=r.width==i.width&&r.height== +i.height;j.fadeTo(d.changeFade,0.3,function(){var g=function(){j.html(m.contents()).fadeTo(d.changeFade,1,S)};b.event.trigger("fancybox-change");j.empty().removeAttr("filter").css({"border-width":d.padding,width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2});if(c)g();else{B.prop=0;b(B).animate({prop:1},{duration:d.changeSpeed,easing:d.easingChange,step:T,complete:g})}})}else{f.removeAttr("style");j.css("border-width",d.padding);if(d.transitionIn=="elastic"){r=V();j.html(m.contents()); +f.show();if(d.opacity)i.opacity=0;B.prop=0;b(B).animate({prop:1},{duration:d.speedIn,easing:d.easingIn,step:T,complete:S})}else{d.titlePosition=="inside"&&y>0&&n.show();j.css({width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2}).html(m.contents());f.css(i).fadeIn(d.transitionIn=="none"?0:d.speedIn,S)}}}},Y=function(){if(d.enableEscapeButton||d.enableKeyboardNav)b(document).bind("keydown.fb",function(a){if(a.keyCode==27&&d.enableEscapeButton){a.preventDefault();b.fancybox.close()}else if((a.keyCode== +37||a.keyCode==39)&&d.enableKeyboardNav&&a.target.tagName!=="INPUT"&&a.target.tagName!=="TEXTAREA"&&a.target.tagName!=="SELECT"){a.preventDefault();b.fancybox[a.keyCode==37?"prev":"next"]()}});if(d.showNavArrows){if(d.cyclic&&l.length>1||p!==0)z.show();if(d.cyclic&&l.length>1||p!=l.length-1)A.show()}else{z.hide();A.hide()}},S=function(){if(!b.support.opacity){j.get(0).style.removeAttribute("filter");f.get(0).style.removeAttribute("filter")}e.autoDimensions&&j.css("height","auto");f.css("height","auto"); +s&&s.length&&n.show();d.showCloseButton&&E.show();Y();d.hideOnContentClick&&j.bind("click",b.fancybox.close);d.hideOnOverlayClick&&u.bind("click",b.fancybox.close);b(window).bind("resize.fb",b.fancybox.resize);d.centerOnScroll&&b(window).bind("scroll.fb",b.fancybox.center);if(d.type=="iframe")b('<iframe id="fancybox-frame" name="fancybox-frame'+(new Date).getTime()+'" frameborder="0" hspace="0" '+(b.browser.msie?'allowtransparency="true""':"")+' scrolling="'+e.scrolling+'" src="'+d.href+'"></iframe>').appendTo(j); +f.show();h=false;b.fancybox.center();d.onComplete(l,p,d);var a,c;if(l.length-1>p){a=l[p+1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}if(p>0){a=l[p-1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}},T=function(a){var c={width:parseInt(r.width+(i.width-r.width)*a,10),height:parseInt(r.height+(i.height-r.height)*a,10),top:parseInt(r.top+(i.top-r.top)*a,10),left:parseInt(r.left+(i.left-r.left)*a,10)};if(typeof i.opacity!=="undefined")c.opacity=a<0.5?0.5:a;f.css(c); +j.css({width:c.width-d.padding*2,height:c.height-y*a-d.padding*2})},U=function(){return[b(window).width()-d.margin*2,b(window).height()-d.margin*2,b(document).scrollLeft()+d.margin,b(document).scrollTop()+d.margin]},X=function(){var a=U(),c={},g=d.autoScale,k=d.padding*2;c.width=d.width.toString().indexOf("%")>-1?parseInt(a[0]*parseFloat(d.width)/100,10):d.width+k;c.height=d.height.toString().indexOf("%")>-1?parseInt(a[1]*parseFloat(d.height)/100,10):d.height+k;if(g&&(c.width>a[0]||c.height>a[1]))if(e.type== +"image"||e.type=="swf"){g=d.width/d.height;if(c.width>a[0]){c.width=a[0];c.height=parseInt((c.width-k)/g+k,10)}if(c.height>a[1]){c.height=a[1];c.width=parseInt((c.height-k)*g+k,10)}}else{c.width=Math.min(c.width,a[0]);c.height=Math.min(c.height,a[1])}c.top=parseInt(Math.max(a[3]-20,a[3]+(a[1]-c.height-40)*0.5),10);c.left=parseInt(Math.max(a[2]-20,a[2]+(a[0]-c.width-40)*0.5),10);return c},V=function(){var a=e.orig?b(e.orig):false,c={};if(a&&a.length){c=a.offset();c.top+=parseInt(a.css("paddingTop"), +10)||0;c.left+=parseInt(a.css("paddingLeft"),10)||0;c.top+=parseInt(a.css("border-top-width"),10)||0;c.left+=parseInt(a.css("border-left-width"),10)||0;c.width=a.width();c.height=a.height();c={width:c.width+d.padding*2,height:c.height+d.padding*2,top:c.top-d.padding-20,left:c.left-d.padding-20}}else{a=U();c={width:d.padding*2,height:d.padding*2,top:parseInt(a[3]+a[1]*0.5,10),left:parseInt(a[2]+a[0]*0.5,10)}}return c},Z=function(){if(t.is(":visible")){b("div",t).css("top",L*-40+"px");L=(L+1)%12}else clearInterval(K)}; +b.fn.fancybox=function(a){if(!b(this).length)return this;b(this).data("fancybox",b.extend({},a,b.metadata?b(this).metadata():{})).unbind("click.fb").bind("click.fb",function(c){c.preventDefault();if(!h){h=true;b(this).blur();o=[];q=0;c=b(this).attr("rel")||"";if(!c||c==""||c==="nofollow")o.push(this);else{o=b("a[rel="+c+"], area[rel="+c+"]");q=o.index(this)}I()}});return this};b.fancybox=function(a,c){var g;if(!h){h=true;g=typeof c!=="undefined"?c:{};o=[];q=parseInt(g.index,10)||0;if(b.isArray(a)){for(var k= +0,C=a.length;k<C;k++)if(typeof a[k]=="object")b(a[k]).data("fancybox",b.extend({},g,a[k]));else a[k]=b({}).data("fancybox",b.extend({content:a[k]},g));o=jQuery.merge(o,a)}else{if(typeof a=="object")b(a).data("fancybox",b.extend({},g,a));else a=b({}).data("fancybox",b.extend({content:a},g));o.push(a)}if(q>o.length||q<0)q=0;I()}};b.fancybox.showActivity=function(){clearInterval(K);t.show();K=setInterval(Z,66)};b.fancybox.hideActivity=function(){t.hide()};b.fancybox.next=function(){return b.fancybox.pos(p+ +1)};b.fancybox.prev=function(){return b.fancybox.pos(p-1)};b.fancybox.pos=function(a){if(!h){a=parseInt(a);o=l;if(a>-1&&a<l.length){q=a;I()}else if(d.cyclic&&l.length>1){q=a>=l.length?0:l.length-1;I()}}};b.fancybox.cancel=function(){if(!h){h=true;b.event.trigger("fancybox-cancel");N();e.onCancel(o,q,e);h=false}};b.fancybox.close=function(){function a(){u.fadeOut("fast");n.empty().hide();f.hide();b.event.trigger("fancybox-cleanup");j.empty();d.onClosed(l,p,d);l=e=[];p=q=0;d=e={};h=false}if(!(h||f.is(":hidden"))){h= +true;if(d&&false===d.onCleanup(l,p,d))h=false;else{N();b(E.add(z).add(A)).hide();b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");j.find("iframe").attr("src",M&&/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank");d.titlePosition!=="inside"&&n.empty();f.stop();if(d.transitionOut=="elastic"){r=V();var c=f.position();i={top:c.top,left:c.left,width:f.width(),height:f.height()};if(d.opacity)i.opacity=1;n.empty().hide();B.prop=1; +b(B).animate({prop:0},{duration:d.speedOut,easing:d.easingOut,step:T,complete:a})}else f.fadeOut(d.transitionOut=="none"?0:d.speedOut,a)}}};b.fancybox.resize=function(){u.is(":visible")&&u.css("height",b(document).height());b.fancybox.center(true)};b.fancybox.center=function(a){var c,g;if(!h){g=a===true?1:0;c=U();!g&&(f.width()>c[0]||f.height()>c[1])||f.stop().animate({top:parseInt(Math.max(c[3]-20,c[3]+(c[1]-j.height()-40)*0.5-d.padding)),left:parseInt(Math.max(c[2]-20,c[2]+(c[0]-j.width()-40)*0.5- +d.padding))},typeof a=="number"?a:200)}};b.fancybox.init=function(){if(!b("#fancybox-wrap").length){b("body").append(m=b('<div id="fancybox-tmp"></div>'),t=b('<div id="fancybox-loading"><div></div></div>'),u=b('<div id="fancybox-overlay"></div>'),f=b('<div id="fancybox-wrap"></div>'));D=b('<div id="fancybox-outer"></div>').append('<div class="fancybox-bg" id="fancybox-bg-n"></div><div class="fancybox-bg" id="fancybox-bg-ne"></div><div class="fancybox-bg" id="fancybox-bg-e"></div><div class="fancybox-bg" id="fancybox-bg-se"></div><div class="fancybox-bg" id="fancybox-bg-s"></div><div class="fancybox-bg" id="fancybox-bg-sw"></div><div class="fancybox-bg" id="fancybox-bg-w"></div><div class="fancybox-bg" id="fancybox-bg-nw"></div>').appendTo(f); +D.append(j=b('<div id="fancybox-content"></div>'),E=b('<a id="fancybox-close"></a>'),n=b('<div id="fancybox-title"></div>'),z=b('<a href="javascript:;" id="fancybox-left"><span class="fancy-ico" id="fancybox-left-ico"></span></a>'),A=b('<a href="javascript:;" id="fancybox-right"><span class="fancy-ico" id="fancybox-right-ico"></span></a>'));E.click(b.fancybox.close);t.click(b.fancybox.cancel);z.click(function(a){a.preventDefault();b.fancybox.prev()});A.click(function(a){a.preventDefault();b.fancybox.next()}); +b.fn.mousewheel&&f.bind("mousewheel.fb",function(a,c){if(h)a.preventDefault();else if(b(a.target).get(0).clientHeight==0||b(a.target).get(0).scrollHeight===b(a.target).get(0).clientHeight){a.preventDefault();b.fancybox[c>0?"prev":"next"]()}});b.support.opacity||f.addClass("fancybox-ie");if(M){t.addClass("fancybox-ie6");f.addClass("fancybox-ie6");b('<iframe id="fancybox-hide-sel-frame" src="'+(/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank")+'" scrolling="no" border="0" frameborder="0" tabindex="-1"></iframe>').prependTo(D)}}}; +b.fn.fancybox.defaults={padding:10,margin:40,opacity:false,modal:false,cyclic:false,scrolling:"auto",width:560,height:340,autoScale:true,autoDimensions:true,centerOnScroll:false,ajax:{},swf:{wmode:"transparent"},hideOnOverlayClick:true,hideOnContentClick:false,overlayShow:true,overlayOpacity:0.7,overlayColor:"#777",titleShow:true,titlePosition:"float",titleFormat:null,titleFromAlt:false,transitionIn:"fade",transitionOut:"fade",speedIn:300,speedOut:300,changeSpeed:300,changeFade:"fast",easingIn:"swing", +easingOut:"swing",showCloseButton:true,showNavArrows:true,enableEscapeButton:true,enableKeyboardNav:true,onStart:function(){},onCancel:function(){},onComplete:function(){},onCleanup:function(){},onClosed:function(){},onError:function(){}};b(document).ready(function(){b.fancybox.init()})})(jQuery); \ No newline at end of file diff --git a/data/js/fancybox/jquery.mousewheel-3.0.4.pack.js b/data/js/fancybox/jquery.mousewheel-3.0.4.pack.js new file mode 100644 index 0000000000000000000000000000000000000000..cb66588e29c237d80f5a944c7f0d47bc97dd9a39 --- /dev/null +++ b/data/js/fancybox/jquery.mousewheel-3.0.4.pack.js @@ -0,0 +1,14 @@ +/*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) +* Licensed under the MIT License (LICENSE.txt). +* +* Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. +* Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. +* Thanks to: Seamus Leahy for adding deltaX and deltaY +* +* Version: 3.0.4 +* +* Requires: 1.2.2+ +*/ + +(function(d){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),c=0,h=0,e=0;a=d.event.fix(b);a.type="mousewheel";if(a.wheelDelta)c=a.wheelDelta/120;if(a.detail)c=-a.detail/3;e=c;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){e=0;h=-1*c}if(b.wheelDeltaY!==undefined)e=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,c,h,e);return d.event.handle.apply(this,i)}var f=["DOMMouseScroll","mousewheel"];d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a= +f.length;a;)this.addEventListener(f[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=f.length;a;)this.removeEventListener(f[--a],g,false);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); \ No newline at end of file diff --git a/data/js/lib/formwizard.js b/data/js/formwizard.js similarity index 100% rename from data/js/lib/formwizard.js rename to data/js/formwizard.js diff --git a/data/js/lib/bootstrap.min.js b/data/js/lib/bootstrap.min.js deleted file mode 100644 index 8c06421e105c24445e14ed5d2a4322e2e06a3c41..0000000000000000000000000000000000000000 --- a/data/js/lib/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** -* Bootstrap.js by @fat & @mdo -* Copyright 2012 Twitter, Inc. -* http://www.apache.org/licenses/LICENSE-2.0.txt -*/ -!function(a){a(function(){"use strict",a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(a){return a||(this.paused=!0),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide");this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c);e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e,f,g;if(c.is(".disabled, :disabled"))return;return f=c.attr("data-target"),f||(f=c.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,"")),e=a(f),e.length||(e=c.parent()),g=e.hasClass("open"),d(),g||e.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('<div class="modal-backdrop '+d+'" />').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,a.proxy(f,this)):f.call(this)):b&&b()}function f(){this.$backdrop.remove(),this.$backdrop=null}function g(){var b=this;this.isShown&&this.options.keyboard?a(document).on("keyup.dismiss.modal",function(a){a.which==27&&b.hide()}):this.isShown||a(document).off("keyup.dismiss.modal")}"use strict";var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this))};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;a("body").addClass("modal-open"),this.isShown=!0,g.call(this),e.call(this,function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in"),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();var e=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,a("body").removeClass("modal-open"),g.call(this),this.$element.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?c.call(this):d.call(this)}},a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a(function(){a("body").on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({},e.data(),c.data());b.preventDefault(),e.modal(f)})})}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f;this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,this.options.trigger!="manual"&&(e=this.options.trigger=="hover"?"mouseenter":"focus",f=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(e,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f,this.options.selector,a.proxy(this.leave,this))),this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,b,this.$element.data()),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);if(!c.options.delay||!c.options.delay.show)return c.show();clearTimeout(this.timeout),c.hoverState="in",this.timeout=setTimeout(function(){c.hoverState=="in"&&c.show()},c.options.delay.show)},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);if(!c.options.delay||!c.options.delay.hide)return c.hide();clearTimeout(this.timeout),c.hoverState="out",this.timeout=setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide)},show:function(){var a,b,c,d,e,f,g;if(this.hasContent()&&this.enabled){a=this.tip(),this.setContent(),this.options.animation&&a.addClass("fade"),f=typeof this.options.placement=="function"?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,b=/in/.test(f),a.remove().css({top:0,left:0,display:"block"}).appendTo(b?this.$element:document.body),c=this.getPosition(b),d=a[0].offsetWidth,e=a[0].offsetHeight;switch(b?f.split(" ")[1]:f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}a.css(g).addClass(f).addClass("in")}},isHTML:function(a){return typeof a!="string"||a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3||/^(?:[^<]*<[\w\W]+>[^>]*$)/.exec(a)},setContent:function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.isHTML(b)?"html":"text"](b),a.removeClass("fade in top bottom left right")},hide:function(){function d(){var b=setTimeout(function(){c.off(a.support.transition.end).remove()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.remove()})}var b=this,c=this.tip();c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d():c.remove()},fixTitle:function(){var a=this.$element;(a.attr("title")||typeof a.attr("data-original-title")!="string")&&a.attr("data-original-title",a.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(b){return a.extend({},b?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||(typeof c.title=="function"?c.title.call(b[0]):c.title),a},tip:function(){return this.$tip=this.$tip||a(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()}},a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f=typeof c=="object"&&c;e||d.data("tooltip",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover",title:"",delay:0}}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.isHTML(b)?"html":"text"](b),a.find(".popover-content > *")[this.isHTML(c)?"html":"text"](c),a.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-content")||(typeof c.content=="function"?c.content.call(b[0]):c.content),a},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip}}),a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f=typeof c=="object"&&c;e||d.data("popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",content:"",template:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(window.jQuery),!function(a){function b(b,c){var d=a.proxy(this.process,this),e=a(b).is("body")?a(window):a(b),f;this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=e.on("scroll.scroll.data-api",d),this.selector=(this.options.target||(f=a(b).attr("href"))&&f.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=a("body"),this.refresh(),this.process()}"use strict",b.prototype={constructor:b,refresh:function(){var b=this,c;this.offsets=a([]),this.targets=a([]),c=this.$body.find(this.selector).map(function(){var b=a(this),c=b.data("target")||b.attr("href"),d=/^#\w/.test(c)&&a(c);return d&&c.length&&[[d.position().top,c]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},process:function(){var a=this.$scrollElement.scrollTop()+this.options.offset,b=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,c=b-this.$scrollElement.height(),d=this.offsets,e=this.targets,f=this.activeTarget,g;if(a>=c)return f!=(g=e.last()[0])&&this.activate(g);for(g=d.length;g--;)f!=e[g]&&a>=d[g]&&(!d[g+1]||a<=d[g+1])&&this.activate(e[g])},activate:function(b){var c,d;this.activeTarget=b,a(this.selector).parent(".active").removeClass("active"),d=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',c=a(d).parent("li").addClass("active"),c.parent(".dropdown-menu")&&(c=c.closest("li.dropdown").addClass("active")),c.trigger("activate")}},a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f=typeof c=="object"&&c;e||d.data("scrollspy",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a(function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active a").last()[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}},a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a(function(){a("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=a(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(a)).change(),this.hide()},updater:function(a){return a},show:function(){var b=a.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:b.top+b.height,left:b.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(b){var c=this,d,e;return this.query=this.$element.val(),this.query?(d=a.grep(this.source,function(a){return c.matcher(a)}),d=this.sorter(d),d.length?this.render(d.slice(0,this.options.items)).show():this.shown?this.hide():this):this.shown?this.hide():this},matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){var b=[],c=[],d=[],e;while(e=a.shift())e.toLowerCase().indexOf(this.query.toLowerCase())?~e.indexOf(this.query)?c.push(e):d.push(e):b.push(e);return b.concat(c,d)},highlighter:function(a){var b=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return a.replace(new RegExp("("+b+")","ig"),function(a,b){return"<strong>"+b+"</strong>"})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),(a.browser.webkit||a.browser.msie)&&this.$element.on("keydown",a.proxy(this.keypress,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this))},keyup:function(a){switch(a.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},keypress:function(a){if(!this.shown)return;switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:if(a.type!="keydown")break;a.preventDefault(),this.prev();break;case 40:if(a.type!="keydown")break;a.preventDefault(),this.next()}a.stopPropagation()},blur:function(a){var b=this;setTimeout(function(){b.hide()},150)},click:function(a){a.stopPropagation(),a.preventDefault(),this.select()},mouseenter:function(b){this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")}},a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f=typeof c=="object"&&c;e||d.data("typeahead",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>'},a.fn.typeahead.Constructor=b,a(function(){a("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;b.preventDefault(),c.typeahead(c.data())})})}(window.jQuery); \ No newline at end of file diff --git a/data/js/lib/jquery-1.7.2.min.js b/data/js/lib/jquery-1.7.2.min.js deleted file mode 100644 index 16ad06c5acaad09ee4d6e9d7c428506db028aeeb..0000000000000000000000000000000000000000 --- a/data/js/lib/jquery-1.7.2.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v1.7.2 jquery.com | jquery.org/license */ -(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( -a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f -.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/data/js/lib/jquery-1.8.3.min.js b/data/js/lib/jquery-1.8.3.min.js new file mode 100644 index 0000000000000000000000000000000000000000..38837795279c5eb281e98ce6017998b993026518 --- /dev/null +++ b/data/js/lib/jquery-1.8.3.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.3 jquery.com | jquery.org/license */ +(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r<i;r++)v.event.add(t,n,u[n][r])}o.data&&(o.data=v.extend({},o.data))}function Ot(e,t){var n;if(t.nodeType!==1)return;t.clearAttributes&&t.clearAttributes(),t.mergeAttributes&&t.mergeAttributes(e),n=t.nodeName.toLowerCase(),n==="object"?(t.parentNode&&(t.outerHTML=e.outerHTML),v.support.html5Clone&&e.innerHTML&&!v.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):n==="input"&&Et.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):n==="option"?t.selected=e.defaultSelected:n==="input"||n==="textarea"?t.defaultValue=e.defaultValue:n==="script"&&t.text!==e.text&&(t.text=e.text),t.removeAttribute(v.expando)}function Mt(e){return typeof e.getElementsByTagName!="undefined"?e.getElementsByTagName("*"):typeof e.querySelectorAll!="undefined"?e.querySelectorAll("*"):[]}function _t(e){Et.test(e.type)&&(e.defaultChecked=e.checked)}function Qt(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Jt.length;while(i--){t=Jt[i]+n;if(t in e)return t}return r}function Gt(e,t){return e=t||e,v.css(e,"display")==="none"||!v.contains(e.ownerDocument,e)}function Yt(e,t){var n,r,i=[],s=0,o=e.length;for(;s<o;s++){n=e[s];if(!n.style)continue;i[s]=v._data(n,"olddisplay"),t?(!i[s]&&n.style.display==="none"&&(n.style.display=""),n.style.display===""&&Gt(n)&&(i[s]=v._data(n,"olddisplay",nn(n.nodeName)))):(r=Dt(n,"display"),!i[s]&&r!=="none"&&v._data(n,"olddisplay",r))}for(s=0;s<o;s++){n=e[s];if(!n.style)continue;if(!t||n.style.display==="none"||n.style.display==="")n.style.display=t?i[s]||"":"none"}return e}function Zt(e,t,n){var r=Rt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function en(e,t,n,r){var i=n===(r?"border":"content")?4:t==="width"?1:0,s=0;for(;i<4;i+=2)n==="margin"&&(s+=v.css(e,n+$t[i],!0)),r?(n==="content"&&(s-=parseFloat(Dt(e,"padding"+$t[i]))||0),n!=="margin"&&(s-=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0)):(s+=parseFloat(Dt(e,"padding"+$t[i]))||0,n!=="padding"&&(s+=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0));return s}function tn(e,t,n){var r=t==="width"?e.offsetWidth:e.offsetHeight,i=!0,s=v.support.boxSizing&&v.css(e,"boxSizing")==="border-box";if(r<=0||r==null){r=Dt(e,t);if(r<0||r==null)r=e.style[t];if(Ut.test(r))return r;i=s&&(v.support.boxSizingReliable||r===e.style[t]),r=parseFloat(r)||0}return r+en(e,t,n||(s?"border":"content"),i)+"px"}function nn(e){if(Wt[e])return Wt[e];var t=v("<"+e+">").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write("<!doctype html><html><body>"),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u<a;u++)r=o[u],s=/^\+/.test(r),s&&(r=r.substr(1)||"*"),i=e[r]=e[r]||[],i[s?"unshift":"push"](n)}}function kn(e,n,r,i,s,o){s=s||n.dataTypes[0],o=o||{},o[s]=!0;var u,a=e[s],f=0,l=a?a.length:0,c=e===Sn;for(;f<l&&(c||!u);f++)u=a[f](n,r,i),typeof u=="string"&&(!c||o[u]?u=t:(n.dataTypes.unshift(u),u=kn(e,n,r,i,u,o)));return(c||!u)&&!o["*"]&&(u=kn(e,n,r,i,"*",o)),u}function Ln(e,n){var r,i,s=v.ajaxSettings.flatOptions||{};for(r in n)n[r]!==t&&((s[r]?e:i||(i={}))[r]=n[r]);i&&v.extend(!0,e,i)}function An(e,n,r){var i,s,o,u,a=e.contents,f=e.dataTypes,l=e.responseFields;for(s in l)s in r&&(n[l[s]]=r[s]);while(f[0]==="*")f.shift(),i===t&&(i=e.mimeType||n.getResponseHeader("content-type"));if(i)for(s in a)if(a[s]&&a[s].test(i)){f.unshift(s);break}if(f[0]in r)o=f[0];else{for(s in r){if(!f[0]||e.converters[s+" "+f[0]]){o=s;break}u||(u=s)}o=o||u}if(o)return o!==f[0]&&f.unshift(o),r[o]}function On(e,t){var n,r,i,s,o=e.dataTypes.slice(),u=o[0],a={},f=0;e.dataFilter&&(t=e.dataFilter(t,e.dataType));if(o[1])for(n in e.converters)a[n.toLowerCase()]=e.converters[n];for(;i=o[++f];)if(i!=="*"){if(u!=="*"&&u!==i){n=a[u+" "+i]||a["* "+i];if(!n)for(r in a){s=r.split(" ");if(s[1]===i){n=a[u+" "+s[0]]||a["* "+s[0]];if(n){n===!0?n=a[r]:a[r]!==!0&&(i=s[0],o.splice(f--,0,i));break}}}if(n!==!0)if(n&&e["throws"])t=n(t);else try{t=n(t)}catch(l){return{state:"parsererror",error:n?l:"No conversion from "+u+" to "+i}}}u=i}return{state:"success",data:t}}function Fn(){try{return new e.XMLHttpRequest}catch(t){}}function In(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function $n(){return setTimeout(function(){qn=t},0),qn=v.now()}function Jn(e,t){v.each(t,function(t,n){var r=(Vn[t]||[]).concat(Vn["*"]),i=0,s=r.length;for(;i<s;i++)if(r[i].call(e,t,n))return})}function Kn(e,t,n){var r,i=0,s=0,o=Xn.length,u=v.Deferred().always(function(){delete a.elem}),a=function(){var t=qn||$n(),n=Math.max(0,f.startTime+f.duration-t),r=n/f.duration||0,i=1-r,s=0,o=f.tweens.length;for(;s<o;s++)f.tweens[s].run(i);return u.notifyWith(e,[f,i,n]),i<1&&o?n:(u.resolveWith(e,[f]),!1)},f=u.promise({elem:e,props:v.extend({},t),opts:v.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:qn||$n(),duration:n.duration,tweens:[],createTween:function(t,n,r){var i=v.Tween(e,f.opts,t,n,f.opts.specialEasing[t]||f.opts.easing);return f.tweens.push(i),i},stop:function(t){var n=0,r=t?f.tweens.length:0;for(;n<r;n++)f.tweens[n].run(1);return t?u.resolveWith(e,[f,t]):u.rejectWith(e,[f,t]),this}}),l=f.props;Qn(l,f.opts.specialEasing);for(;i<o;i++){r=Xn[i].call(f,e,l,f.opts);if(r)return r}return Jn(f,l),v.isFunction(f.opts.start)&&f.opts.start.call(e,f),v.fx.timer(v.extend(a,{anim:f,queue:f.opts.queue,elem:e})),f.progress(f.opts.progress).done(f.opts.done,f.opts.complete).fail(f.opts.fail).always(f.opts.always)}function Qn(e,t){var n,r,i,s,o;for(n in e){r=v.camelCase(n),i=t[r],s=e[n],v.isArray(s)&&(i=s[1],s=e[n]=s[0]),n!==r&&(e[r]=s,delete e[n]),o=v.cssHooks[r];if(o&&"expand"in o){s=o.expand(s),delete e[r];for(n in s)n in e||(e[n]=s[n],t[n]=i)}else t[r]=i}}function Gn(e,t,n){var r,i,s,o,u,a,f,l,c,h=this,p=e.style,d={},m=[],g=e.nodeType&&Gt(e);n.queue||(l=v._queueHooks(e,"fx"),l.unqueued==null&&(l.unqueued=0,c=l.empty.fire,l.empty.fire=function(){l.unqueued||c()}),l.unqueued++,h.always(function(){h.always(function(){l.unqueued--,v.queue(e,"fx").length||l.empty.fire()})})),e.nodeType===1&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],v.css(e,"display")==="inline"&&v.css(e,"float")==="none"&&(!v.support.inlineBlockNeedsLayout||nn(e.nodeName)==="inline"?p.display="inline-block":p.zoom=1)),n.overflow&&(p.overflow="hidden",v.support.shrinkWrapBlocks||h.done(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t){s=t[r];if(Un.exec(s)){delete t[r],a=a||s==="toggle";if(s===(g?"hide":"show"))continue;m.push(r)}}o=m.length;if(o){u=v._data(e,"fxshow")||v._data(e,"fxshow",{}),"hidden"in u&&(g=u.hidden),a&&(u.hidden=!g),g?v(e).show():h.done(function(){v(e).hide()}),h.done(function(){var t;v.removeData(e,"fxshow",!0);for(t in d)v.style(e,t,d[t])});for(r=0;r<o;r++)i=m[r],f=h.createTween(i,g?u[i]:0),d[i]=u[i]||v.style(e,i),i in u||(u[i]=f.start,g&&(f.end=f.start,f.start=i==="width"||i==="height"?1:0))}}function Yn(e,t,n,r,i){return new Yn.prototype.init(e,t,n,r,i)}function Zn(e,t){var n,r={height:e},i=0;t=t?1:0;for(;i<4;i+=2-t)n=$t[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function tr(e){return v.isWindow(e)?e:e.nodeType===9?e.defaultView||e.parentWindow:!1}var n,r,i=e.document,s=e.location,o=e.navigator,u=e.jQuery,a=e.$,f=Array.prototype.push,l=Array.prototype.slice,c=Array.prototype.indexOf,h=Object.prototype.toString,p=Object.prototype.hasOwnProperty,d=String.prototype.trim,v=function(e,t){return new v.fn.init(e,t,n)},m=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,g=/\S/,y=/\s+/,b=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,w=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a<f;a++)if((e=arguments[a])!=null)for(n in e){r=u[n],i=e[n];if(u===i)continue;l&&i&&(v.isPlainObject(i)||(s=v.isArray(i)))?(s?(s=!1,o=r&&v.isArray(r)?r:[]):o=r&&v.isPlainObject(r)?r:{},u[n]=v.extend(l,o,i)):i!==t&&(u[n]=i)}return u},v.extend({noConflict:function(t){return e.$===v&&(e.$=a),t&&e.jQuery===v&&(e.jQuery=u),v},isReady:!1,readyWait:1,holdReady:function(e){e?v.readyWait++:v.ready(!0)},ready:function(e){if(e===!0?--v.readyWait:v.isReady)return;if(!i.body)return setTimeout(v.ready,1);v.isReady=!0;if(e!==!0&&--v.readyWait>0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s<o;)if(n.apply(e[s++],r)===!1)break}else if(u){for(i in e)if(n.call(e[i],i,e[i])===!1)break}else for(;s<o;)if(n.call(e[s],s,e[s++])===!1)break;return e},trim:d&&!d.call("\ufeff\u00a0")?function(e){return e==null?"":d.call(e)}:function(e){return e==null?"":(e+"").replace(b,"")},makeArray:function(e,t){var n,r=t||[];return e!=null&&(n=v.type(e),e.length==null||n==="string"||n==="function"||n==="regexp"||v.isWindow(e)?f.call(r,e):v.merge(r,e)),r},inArray:function(e,t,n){var r;if(t){if(c)return c.call(t,e,n);r=t.length,n=n?n<0?Math.max(0,r+n):n:0;for(;n<r;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,s=0;if(typeof r=="number")for(;s<r;s++)e[i++]=n[s];else while(n[s]!==t)e[i++]=n[s++];return e.length=i,e},grep:function(e,t,n){var r,i=[],s=0,o=e.length;n=!!n;for(;s<o;s++)r=!!t(e[s],s),n!==r&&i.push(e[s]);return i},map:function(e,n,r){var i,s,o=[],u=0,a=e.length,f=e instanceof v||a!==t&&typeof a=="number"&&(a>0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u<a;u++)i=n(e[u],u,r),i!=null&&(o[o.length]=i);else for(s in e)i=n(e[s],s,r),i!=null&&(o[o.length]=i);return o.concat.apply([],o)},guid:1,proxy:function(e,n){var r,i,s;return typeof n=="string"&&(r=e[n],n=e,e=r),v.isFunction(e)?(i=l.call(arguments,2),s=function(){return e.apply(n,i.concat(l.call(arguments)))},s.guid=e.guid=e.guid||v.guid++,s):t},access:function(e,n,r,i,s,o,u){var a,f=r==null,l=0,c=e.length;if(r&&typeof r=="object"){for(l in r)v.access(e,n,l,r[l],1,o,i);s=1}else if(i!==t){a=u===t&&v.isFunction(i),f&&(a?(a=n,n=function(e,t,n){return a.call(v(e),n)}):(n.call(e,i),n=null));if(n)for(;l<c;l++)n(e[l],r,a?i.call(e[l],l,n(e[l],r)):i,u);s=1}return s?e:f?n.call(e):c?n(e[0],r):o},now:function(){return(new Date).getTime()}}),v.ready.promise=function(t){if(!r){r=v.Deferred();if(i.readyState==="complete")setTimeout(v.ready,1);else if(i.addEventListener)i.addEventListener("DOMContentLoaded",A,!1),e.addEventListener("load",v.ready,!1);else{i.attachEvent("onreadystatechange",A),e.attachEvent("onload",v.ready);var n=!1;try{n=e.frameElement==null&&i.documentElement}catch(s){}n&&n.doScroll&&function o(){if(!v.isReady){try{n.doScroll("left")}catch(e){return setTimeout(o,50)}v.ready()}}()}}return r.promise(t)},v.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(e,t){O["[object "+t+"]"]=t.toLowerCase()}),n=v(i);var M={};v.Callbacks=function(e){e=typeof e=="string"?M[e]||_(e):v.extend({},e);var n,r,i,s,o,u,a=[],f=!e.once&&[],l=function(t){n=e.memory&&t,r=!0,u=s||0,s=0,o=a.length,i=!0;for(;a&&u<o;u++)if(a[u].apply(t[0],t[1])===!1&&e.stopOnFalse){n=!1;break}i=!1,a&&(f?f.length&&l(f.shift()):n?a=[]:c.disable())},c={add:function(){if(a){var t=a.length;(function r(t){v.each(t,function(t,n){var i=v.type(n);i==="function"?(!e.unique||!c.has(n))&&a.push(n):n&&n.length&&i!=="string"&&r(n)})})(arguments),i?o=a.length:n&&(s=t,l(n))}return this},remove:function(){return a&&v.each(arguments,function(e,t){var n;while((n=v.inArray(t,a,n))>-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t<r;t++)n[t]&&v.isFunction(n[t].promise)?n[t].promise().done(o(t,f,n)).fail(s.reject).progress(o(t,a,u)):--i}return i||s.resolveWith(f,n),s.promise()}}),v.support=function(){var t,n,r,s,o,u,a,f,l,c,h,p=i.createElement("div");p.setAttribute("className","t"),p.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="<table><tr><td></td><td>t</td></tr></table>",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="<div></div>",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i<s;i++)delete r[t[i]];if(!(n?B:v.isEmptyObject)(r))return}}if(!n){delete u[a].data;if(!B(u[a]))return}o?v.cleanData([e],!0):v.support.deleteExpando||u!=u.window?delete u[a]:u[a]=null},_data:function(e,t,n){return v.data(e,t,n,!0)},acceptData:function(e){var t=e.nodeName&&v.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),v.fn.extend({data:function(e,n){var r,i,s,o,u,a=this[0],f=0,l=null;if(e===t){if(this.length){l=v.data(a);if(a.nodeType===1&&!v._data(a,"parsedAttrs")){s=a.attributes;for(u=s.length;f<u;f++)o=s[f].name,o.indexOf("data-")||(o=v.camelCase(o.substring(5)),H(a,o,l[o]));v._data(a,"parsedAttrs",!0)}}return l}return typeof e=="object"?this.each(function(){v.data(this,e)}):(r=e.split(".",2),r[1]=r[1]?"."+r[1]:"",i=r[1]+"!",v.access(this,function(n){if(n===t)return l=this.triggerHandler("getData"+i,[r[0]]),l===t&&a&&(l=v.data(a,e),l=H(a,e,l)),l===t&&r[1]?this.data(r[0]):l;r[1]=n,this.each(function(){var t=v(this);t.triggerHandler("setData"+i,r),v.data(this,e,n),t.triggerHandler("changeData"+i,r)})},null,n,arguments.length>1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length<r?v.queue(this[0],e):n===t?this:this.each(function(){var t=v.queue(this,e,n);v._queueHooks(this,e),e==="fx"&&t[0]!=="inprogress"&&v.dequeue(this,e)})},dequeue:function(e){return this.each(function(){v.dequeue(this,e)})},delay:function(e,t){return e=v.fx?v.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,s=v.Deferred(),o=this,u=this.length,a=function(){--i||s.resolveWith(o,[o])};typeof e!="string"&&(n=e,e=t),e=e||"fx";while(u--)r=v._data(o[u],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(a));return a(),s.promise(n)}});var j,F,I,q=/[\t\r\n]/g,R=/\r/g,U=/^(?:button|input)$/i,z=/^(?:button|input|object|select|textarea)$/i,W=/^a(?:rea|)$/i,X=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,V=v.support.getSetAttribute;v.fn.extend({attr:function(e,t){return v.access(this,v.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n<r;n++){i=this[n];if(i.nodeType===1)if(!i.className&&t.length===1)i.className=e;else{s=" "+i.className+" ";for(o=0,u=t.length;o<u;o++)s.indexOf(" "+t[o]+" ")<0&&(s+=t[o]+" ");i.className=v.trim(s)}}}return this},removeClass:function(e){var n,r,i,s,o,u,a;if(v.isFunction(e))return this.each(function(t){v(this).removeClass(e.call(this,t,this.className))});if(e&&typeof e=="string"||e===t){n=(e||"").split(y);for(u=0,a=this.length;u<a;u++){i=this[u];if(i.nodeType===1&&i.className){r=(" "+i.className+" ").replace(q," ");for(s=0,o=n.length;s<o;s++)while(r.indexOf(" "+n[s]+" ")>=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n<r;n++)if(this[n].nodeType===1&&(" "+this[n].className+" ").replace(q," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a<u;a++){n=r[a];if((n.selected||a===i)&&(v.support.optDisabled?!n.disabled:n.getAttribute("disabled")===null)&&(!n.parentNode.disabled||!v.nodeName(n.parentNode,"optgroup"))){t=v(n).val();if(s)return t;o.push(t)}}return o},set:function(e,t){var n=v.makeArray(t);return v(e).find("option").each(function(){this.selected=v.inArray(v(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o<r.length;o++)i=r[o],i&&(n=v.propFix[i]||i,s=X.test(i),s||v.attr(e,i,""),e.removeAttribute(V?i:n),s&&n in e&&(e[n]=!1))}},attrHooks:{type:{set:function(e,t){if(U.test(e.nodeName)&&e.parentNode)v.error("type property can't be changed");else if(!v.support.radioValue&&t==="radio"&&v.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}},value:{get:function(e,t){return j&&v.nodeName(e,"button")?j.get(e,t):t in e?e.value:null},set:function(e,t,n){if(j&&v.nodeName(e,"button"))return j.set(e,t,n);e.value=t}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,s,o,u=e.nodeType;if(!e||u===3||u===8||u===2)return;return o=u!==1||!v.isXMLDoc(e),o&&(n=v.propFix[n]||n,s=v.propHooks[n]),r!==t?s&&"set"in s&&(i=s.set(e,r,n))!==t?i:e[n]=r:s&&"get"in s&&(i=s.get(e,n))!==null?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):z.test(e.nodeName)||W.test(e.nodeName)&&e.href?0:t}}}}),F={get:function(e,n){var r,i=v.prop(e,n);return i===!0||typeof i!="boolean"&&(r=e.getAttributeNode(n))&&r.nodeValue!==!1?n.toLowerCase():t},set:function(e,t,n){var r;return t===!1?v.removeAttr(e,n):(r=v.propFix[n]||n,r in e&&(e[r]=!0),e.setAttribute(n,n.toLowerCase())),n}},V||(I={name:!0,id:!0,coords:!0},j=v.valHooks.button={get:function(e,n){var r;return r=e.getAttributeNode(n),r&&(I[n]?r.value!=="":r.specified)?r.value:t},set:function(e,t,n){var r=e.getAttributeNode(n);return r||(r=i.createAttribute(n),e.setAttributeNode(r)),r.value=t+""}},v.each(["width","height"],function(e,t){v.attrHooks[t]=v.extend(v.attrHooks[t],{set:function(e,n){if(n==="")return e.setAttribute(t,"auto"),n}})}),v.attrHooks.contenteditable={get:j.get,set:function(e,t,n){t===""&&(t="false"),j.set(e,t,n)}}),v.support.hrefNormalized||v.each(["href","src","width","height"],function(e,n){v.attrHooks[n]=v.extend(v.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return r===null?t:r}})}),v.support.style||(v.attrHooks.style={get:function(e){return e.style.cssText.toLowerCase()||t},set:function(e,t){return e.style.cssText=t+""}}),v.support.optSelected||(v.propHooks.selected=v.extend(v.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),v.support.enctype||(v.propFix.enctype="encoding"),v.support.checkOn||v.each(["radio","checkbox"],function(){v.valHooks[this]={get:function(e){return e.getAttribute("value")===null?"on":e.value}}}),v.each(["radio","checkbox"],function(){v.valHooks[this]=v.extend(v.valHooks[this],{set:function(e,t){if(v.isArray(t))return e.checked=v.inArray(v(e).val(),t)>=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f<n.length;f++){l=J.exec(n[f])||[],c=l[1],h=(l[2]||"").split(".").sort(),g=v.event.special[c]||{},c=(s?g.delegateType:g.bindType)||c,g=v.event.special[c]||{},p=v.extend({type:c,origType:l[1],data:i,handler:r,guid:r.guid,selector:s,needsContext:s&&v.expr.match.needsContext.test(s),namespace:h.join(".")},d),m=a[c];if(!m){m=a[c]=[],m.delegateCount=0;if(!g.setup||g.setup.call(e,i,h,u)===!1)e.addEventListener?e.addEventListener(c,u,!1):e.attachEvent&&e.attachEvent("on"+c,u)}g.add&&(g.add.call(e,p),p.handler.guid||(p.handler.guid=r.guid)),s?m.splice(m.delegateCount++,0,p):m.push(p),v.event.global[c]=!0}e=null},global:{},remove:function(e,t,n,r,i){var s,o,u,a,f,l,c,h,p,d,m,g=v.hasData(e)&&v._data(e);if(!g||!(h=g.events))return;t=v.trim(Z(t||"")).split(" ");for(s=0;s<t.length;s++){o=J.exec(t[s])||[],u=a=o[1],f=o[2];if(!u){for(u in h)v.event.remove(e,u+t[s],n,r,!0);continue}p=v.event.special[u]||{},u=(r?p.delegateType:p.bindType)||u,d=h[u]||[],l=d.length,f=f?new RegExp("(^|\\.)"+f.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(c=0;c<d.length;c++)m=d[c],(i||a===m.origType)&&(!n||n.guid===m.guid)&&(!f||f.test(m.namespace))&&(!r||r===m.selector||r==="**"&&m.selector)&&(d.splice(c--,1),m.selector&&d.delegateCount--,p.remove&&p.remove.call(e,m));d.length===0&&l!==d.length&&((!p.teardown||p.teardown.call(e,f,g.handle)===!1)&&v.removeEvent(e,u,g.handle),delete h[u])}v.isEmptyObject(h)&&(delete g.handle,v.removeData(e,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(n,r,s,o){if(!s||s.nodeType!==3&&s.nodeType!==8){var u,a,f,l,c,h,p,d,m,g,y=n.type||n,b=[];if(Y.test(y+v.event.triggered))return;y.indexOf("!")>=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f<m.length&&!n.isPropagationStopped();f++)l=m[f][0],n.type=m[f][1],d=(v._data(l,"events")||{})[n.type]&&v._data(l,"handle"),d&&d.apply(l,r),d=h&&l[h],d&&v.acceptData(l)&&d.apply&&d.apply(l,r)===!1&&n.preventDefault();return n.type=y,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(s.ownerDocument,r)===!1)&&(y!=="click"||!v.nodeName(s,"a"))&&v.acceptData(s)&&h&&s[y]&&(y!=="focus"&&y!=="blur"||n.target.offsetWidth!==0)&&!v.isWindow(s)&&(c=s[h],c&&(s[h]=null),v.event.triggered=y,s[y](),v.event.triggered=t,c&&(s[h]=c)),n.result}return},dispatch:function(n){n=v.event.fix(n||e.event);var r,i,s,o,u,a,f,c,h,p,d=(v._data(this,"events")||{})[n.type]||[],m=d.delegateCount,g=l.call(arguments),y=!n.exclusive&&!n.namespace,b=v.event.special[n.type]||{},w=[];g[0]=n,n.delegateTarget=this;if(b.preDispatch&&b.preDispatch.call(this,n)===!1)return;if(m&&(!n.button||n.type!=="click"))for(s=n.target;s!=this;s=s.parentNode||this)if(s.disabled!==!0||n.type!=="click"){u={},f=[];for(r=0;r<m;r++)c=d[r],h=c.selector,u[h]===t&&(u[h]=c.needsContext?v(h,this).index(s)>=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r<w.length&&!n.isPropagationStopped();r++){a=w[r],n.currentTarget=a.elem;for(i=0;i<a.matches.length&&!n.isImmediatePropagationStopped();i++){c=a.matches[i];if(y||!n.namespace&&!c.namespace||n.namespace_re&&n.namespace_re.test(c.namespace))n.data=c.data,n.handleObj=c,o=((v.event.special[c.origType]||{}).handle||c.handler).apply(a.elem,g),o!==t&&(n.result=o,o===!1&&(n.preventDefault(),n.stopPropagation()))}}return b.postDispatch&&b.postDispatch.call(this,n),n.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return e.which==null&&(e.which=t.charCode!=null?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,s,o,u=n.button,a=n.fromElement;return e.pageX==null&&n.clientX!=null&&(r=e.target.ownerDocument||i,s=r.documentElement,o=r.body,e.pageX=n.clientX+(s&&s.scrollLeft||o&&o.scrollLeft||0)-(s&&s.clientLeft||o&&o.clientLeft||0),e.pageY=n.clientY+(s&&s.scrollTop||o&&o.scrollTop||0)-(s&&s.clientTop||o&&o.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?n.toElement:a),!e.which&&u!==t&&(e.which=u&1?1:u&2?3:u&4?2:0),e}},fix:function(e){if(e[v.expando])return e;var t,n,r=e,s=v.event.fixHooks[e.type]||{},o=s.props?this.props.concat(s.props):this.props;e=v.Event(r);for(t=o.length;t;)n=o[--t],e[n]=r[n];return e.target||(e.target=r.srcElement||i),e.target.nodeType===3&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,r):e},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(e,t,n){v.isWindow(this)&&(this.onbeforeunload=n)},teardown:function(e,t){this.onbeforeunload===t&&(this.onbeforeunload=null)}}},simulate:function(e,t,n,r){var i=v.extend(new v.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?v.event.trigger(i,null,t):v.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},v.event.handle=v.event.dispatch,v.removeEvent=i.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]=="undefined"&&(e[r]=null),e.detachEvent(r,n))},v.Event=function(e,t){if(!(this instanceof v.Event))return new v.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?tt:et):this.type=e,t&&v.extend(this,t),this.timeStamp=e&&e.timeStamp||v.now(),this[v.expando]=!0},v.Event.prototype={preventDefault:function(){this.isDefaultPrevented=tt;var e=this.originalEvent;if(!e)return;e.preventDefault?e.preventDefault():e.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=tt;var e=this.originalEvent;if(!e)return;e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=tt,this.stopPropagation()},isDefaultPrevented:et,isPropagationStopped:et,isImmediatePropagationStopped:et},v.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){v.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,s=e.handleObj,o=s.selector;if(!i||i!==r&&!v.contains(r,i))e.type=s.origType,n=s.handler.apply(this,arguments),e.type=t;return n}}}),v.support.submitBubbles||(v.event.special.submit={setup:function(){if(v.nodeName(this,"form"))return!1;v.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=v.nodeName(n,"input")||v.nodeName(n,"button")?n.form:t;r&&!v._data(r,"_submit_attached")&&(v.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),v._data(r,"_submit_attached",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&v.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){if(v.nodeName(this,"form"))return!1;v.event.remove(this,"._submit")}}),v.support.changeBubbles||(v.event.special.change={setup:function(){if($.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")v.event.add(this,"propertychange._change",function(e){e.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),v.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),v.event.simulate("change",this,e,!0)});return!1}v.event.add(this,"beforeactivate._change",function(e){var t=e.target;$.test(t.nodeName)&&!v._data(t,"_change_attached")&&(v.event.add(t,"change._change",function(e){this.parentNode&&!e.isSimulated&&!e.isTrigger&&v.event.simulate("change",this.parentNode,e,!0)}),v._data(t,"_change_attached",!0))})},handle:function(e){var t=e.target;if(this!==t||e.isSimulated||e.isTrigger||t.type!=="radio"&&t.type!=="checkbox")return e.handleObj.handler.apply(this,arguments)},teardown:function(){return v.event.remove(this,"._change"),!$.test(this.nodeName)}}),v.support.focusinBubbles||v.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){v.event.simulate(t,e.target,v.event.fix(e),!0)};v.event.special[t]={setup:function(){n++===0&&i.addEventListener(e,r,!0)},teardown:function(){--n===0&&i.removeEventListener(e,r,!0)}}}),v.fn.extend({on:function(e,n,r,i,s){var o,u;if(typeof e=="object"){typeof n!="string"&&(r=r||n,n=t);for(u in e)this.on(u,n,r,e[u],s);return this}r==null&&i==null?(i=n,r=n=t):i==null&&(typeof n=="string"?(i=r,r=t):(i=r,r=n,n=t));if(i===!1)i=et;else if(!i)return this;return s===1&&(o=i,i=function(e){return v().off(e),o.apply(this,arguments)},i.guid=o.guid||(o.guid=v.guid++)),this.each(function(){v.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,s;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,v(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if(typeof e=="object"){for(s in e)this.off(s,n,e[s]);return this}if(n===!1||typeof n=="function")r=n,n=t;return r===!1&&(r=et),this.each(function(){v.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},live:function(e,t,n){return v(this.context).on(e,this.selector,t,n),this},die:function(e,t){return v(this.context).off(e,this.selector||"**",t),this},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return arguments.length===1?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){v.event.trigger(e,t,this)})},triggerHandler:function(e,t){if(this[0])return v.event.trigger(e,t,this[0],!0)},toggle:function(e){var t=arguments,n=e.guid||v.guid++,r=0,i=function(n){var i=(v._data(this,"lastToggle"+e.guid)||0)%r;return v._data(this,"lastToggle"+e.guid,i+1),n.preventDefault(),t[i].apply(this,arguments)||!1};i.guid=n;while(r<t.length)t[r++].guid=n;return this.click(i)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),v.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){v.fn[t]=function(e,n){return n==null&&(n=e,e=null),arguments.length>0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u<a;u++)if(s=e[u])if(!n||n(s,r,i))o.push(s),f&&t.push(u);return o}function ct(e,t,n,r,i,s){return r&&!r[d]&&(r=ct(r)),i&&!i[d]&&(i=ct(i,s)),N(function(s,o,u,a){var f,l,c,h=[],p=[],d=o.length,v=s||dt(t||"*",u.nodeType?[u]:u,[]),m=e&&(s||!t)?lt(v,h,e,u,a):v,g=n?i||(s?e:d||r)?[]:o:m;n&&n(m,g,u,a);if(r){f=lt(g,p),r(f,[],u,a),l=f.length;while(l--)if(c=f[l])g[p[l]]=!(m[p[l]]=c)}if(s){if(i||e){if(i){f=[],l=g.length;while(l--)(c=g[l])&&f.push(m[l]=c);i(null,g=[],f,a)}l=g.length;while(l--)(c=g[l])&&(f=i?T.call(s,c):h[l])>-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a<s;a++)if(n=i.relative[e[a].type])h=[at(ft(h),n)];else{n=i.filter[e[a].type].apply(null,e[a].matches);if(n[d]){r=++a;for(;r<s;r++)if(i.relative[e[r].type])break;return ct(a>1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a<r&&ht(e.slice(a,r)),r<s&&ht(e=e.slice(r)),r<s&&e.join(""))}h.push(n)}return ft(h)}function pt(e,t){var r=t.length>0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r<i;r++)nt(e,t[r],n);return n}function vt(e,t,n,r,s){var o,u,f,l,c,h=ut(e),p=h.length;if(!r&&h.length===1){u=h[0]=h[0].slice(0);if(u.length>2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;t<n;t++)if(this[t]===e)return t;return-1},N=function(e,t){return e[d]=t==null||t,e},C=function(){var e={},t=[];return N(function(n,r){return t.push(n)>i.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="<a href='#'></a>",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="<select></select>";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="<a name='"+d+"'></a><div name='"+d+"'></div>",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:st(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:st(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}},f=y.compareDocumentPosition?function(e,t){return e===t?(l=!0,0):(!e.compareDocumentPosition||!t.compareDocumentPosition?e.compareDocumentPosition:e.compareDocumentPosition(t)&4)?-1:1}:function(e,t){if(e===t)return l=!0,0;if(e.sourceIndex&&t.sourceIndex)return e.sourceIndex-t.sourceIndex;var n,r,i=[],s=[],o=e.parentNode,u=t.parentNode,a=o;if(o===u)return ot(e,t);if(!o)return-1;if(!u)return 1;while(a)i.unshift(a),a=a.parentNode;a=u;while(a)s.unshift(a),a=a.parentNode;n=i.length,r=s.length;for(var f=0;f<n&&f<r;f++)if(i[f]!==s[f])return ot(i[f],s[f]);return f===n?ot(e,s[f],-1):ot(i[f],t,1)},[0,0].sort(f),h=!l,nt.uniqueSort=function(e){var t,n=[],r=1,i=0;l=h,e.sort(f);if(l){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e},nt.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},a=nt.compile=function(e,t){var n,r=[],i=[],s=A[d][e+" "];if(!s){t||(t=ut(e)),n=t.length;while(n--)s=ht(t[n]),s[d]?r.push(s):i.push(s);s=A(e,pt(i,r))}return s},g.querySelectorAll&&function(){var e,t=vt,n=/'|\\/g,r=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,i=[":focus"],s=[":active"],u=y.matchesSelector||y.mozMatchesSelector||y.webkitMatchesSelector||y.oMatchesSelector||y.msMatchesSelector;K(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="<p test=''></p>",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="<input type='hidden'/>",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t<n;t++)if(v.contains(u[t],this))return!0});o=this.pushStack("","find",e);for(t=0,n=this.length;t<n;t++){r=o.length,v.find(e,this[t],o);if(t>0)for(i=r;i<o.length;i++)for(s=0;s<r;s++)if(o[s]===o[i]){o.splice(i--,1);break}}return o},has:function(e){var t,n=v(e,this),r=n.length;return this.filter(function(){for(t=0;t<r;t++)if(v.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1),"not",e)},filter:function(e){return this.pushStack(ft(this,e,!0),"filter",e)},is:function(e){return!!e&&(typeof e=="string"?st.test(e)?v(e,this.context).index(this[0])>=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r<i;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&n.nodeType!==11){if(o?o.index(n)>-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/<tbody/i,gt=/<|&#?\w+;/,yt=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,wt=new RegExp("<(?:"+ct+")[\\s/>]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,Nt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X<div>","</div>"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1></$2>");try{for(;r<i;r++)n=this[r]||{},n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),n.innerHTML=e);n=0}catch(s){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){return ut(this[0])?this.length?this.pushStack(v(v.isFunction(e)?e():e),"replaceWith",e):this:v.isFunction(e)?this.each(function(t){var n=v(this),r=n.html();n.replaceWith(e.call(this,t,r))}):(typeof e!="string"&&(e=v(e).detach()),this.each(function(){var t=this.nextSibling,n=this.parentNode;v(this).remove(),t?v(t).before(e):v(n).append(e)}))},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=[].concat.apply([],e);var i,s,o,u,a=0,f=e[0],l=[],c=this.length;if(!v.support.checkClone&&c>1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a<c;a++)r.call(n&&v.nodeName(this[a],"table")?Lt(this[a],"tbody"):this[a],a===u?o:v.clone(o,!0,!0))}o=s=null,l.length&&v.each(l,function(e,t){t.src?v.ajax?v.ajax({url:t.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):v.error("no ajax"):v.globalEval((t.text||t.textContent||t.innerHTML||"").replace(Tt,"")),t.parentNode&&t.parentNode.removeChild(t)})}return this}}),v.buildFragment=function(e,n,r){var s,o,u,a=e[0];return n=n||i,n=!n.nodeType&&n[0]||n,n=n.ownerDocument||n,e.length===1&&typeof a=="string"&&a.length<512&&n===i&&a.charAt(0)==="<"&&!bt.test(a)&&(v.support.checkClone||!St.test(a))&&(v.support.html5Clone||!wt.test(a))&&(o=!0,s=v.fragments[a],u=s!==t),s||(s=n.createDocumentFragment(),v.clean(e,n,s,r),o&&(v.fragments[a]=u&&s)),{fragment:s,cacheable:o}},v.fragments={},v.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){v.fn[e]=function(n){var r,i=0,s=[],o=v(n),u=o.length,a=this.length===1&&this[0].parentNode;if((a==null||a&&a.nodeType===11&&a.childNodes.length===1)&&u===1)return o[t](this[0]),this;for(;i<u;i++)r=(i>0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1></$2>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]==="<table>"&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("<div>").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r<i;r++)n=e[r],Vn[n]=Vn[n]||[],Vn[n].unshift(t)},prefilter:function(e,t){t?Xn.unshift(e):Xn.push(e)}}),v.Tween=Yn,Yn.prototype={constructor:Yn,init:function(e,t,n,r,i,s){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=s||(v.cssNumber[n]?"":"px")},cur:function(){var e=Yn.propHooks[this.prop];return e&&e.get?e.get(this):Yn.propHooks._default.get(this)},run:function(e){var t,n=Yn.propHooks[this.prop];return this.options.duration?this.pos=t=v.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Yn.propHooks._default.set(this),this}},Yn.prototype.init.prototype=Yn.prototype,Yn.propHooks={_default:{get:function(e){var t;return e.elem[e.prop]==null||!!e.elem.style&&e.elem.style[e.prop]!=null?(t=v.css(e.elem,e.prop,!1,""),!t||t==="auto"?0:t):e.elem[e.prop]},set:function(e){v.fx.step[e.prop]?v.fx.step[e.prop](e):e.elem.style&&(e.elem.style[v.cssProps[e.prop]]!=null||v.cssHooks[e.prop])?v.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},Yn.propHooks.scrollTop=Yn.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},v.each(["toggle","show","hide"],function(e,t){var n=v.fn[t];v.fn[t]=function(r,i,s){return r==null||typeof r=="boolean"||!e&&v.isFunction(r)&&v.isFunction(i)?n.apply(this,arguments):this.animate(Zn(t,!0),r,i,s)}}),v.fn.extend({fadeTo:function(e,t,n,r){return this.filter(Gt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=v.isEmptyObject(e),s=v.speed(t,n,r),o=function(){var t=Kn(this,v.extend({},e),s);i&&t.stop(!0)};return i||s.queue===!1?this.each(o):this.queue(s.queue,o)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return typeof e!="string"&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=e!=null&&e+"queueHooks",s=v.timers,o=v._data(this);if(n)o[n]&&o[n].stop&&i(o[n]);else for(n in o)o[n]&&o[n].stop&&Wn.test(n)&&i(o[n]);for(n=s.length;n--;)s[n].elem===this&&(e==null||s[n].queue===e)&&(s[n].anim.stop(r),t=!1,s.splice(n,1));(t||!r)&&v.dequeue(this,e)})}}),v.each({slideDown:Zn("show"),slideUp:Zn("hide"),slideToggle:Zn("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){v.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),v.speed=function(e,t,n){var r=e&&typeof e=="object"?v.extend({},e):{complete:n||!n&&t||v.isFunction(e)&&e,duration:e,easing:n&&t||t&&!v.isFunction(t)&&t};r.duration=v.fx.off?0:typeof r.duration=="number"?r.duration:r.duration in v.fx.speeds?v.fx.speeds[r.duration]:v.fx.speeds._default;if(r.queue==null||r.queue===!0)r.queue="fx";return r.old=r.complete,r.complete=function(){v.isFunction(r.old)&&r.old.call(this),r.queue&&v.dequeue(this,r.queue)},r},v.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},v.timers=[],v.fx=Yn.prototype.init,v.fx.tick=function(){var e,n=v.timers,r=0;qn=v.now();for(;r<n.length;r++)e=n[r],!e()&&n[r]===e&&n.splice(r--,1);n.length||v.fx.stop(),qn=t},v.fx.timer=function(e){e()&&v.timers.push(e)&&!Rn&&(Rn=setInterval(v.fx.tick,v.fx.interval))},v.fx.interval=13,v.fx.stop=function(){clearInterval(Rn),Rn=null},v.fx.speeds={slow:600,fast:200,_default:400},v.fx.step={},v.expr&&v.expr.filters&&(v.expr.filters.animated=function(e){return v.grep(v.timers,function(t){return e===t.elem}).length});var er=/^(?:body|html)$/i;v.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){v.offset.setOffset(this,e,t)});var n,r,i,s,o,u,a,f={top:0,left:0},l=this[0],c=l&&l.ownerDocument;if(!c)return;return(r=c.body)===l?v.offset.bodyOffset(l):(n=c.documentElement,v.contains(n,l)?(typeof l.getBoundingClientRect!="undefined"&&(f=l.getBoundingClientRect()),i=tr(c),s=n.clientTop||r.clientTop||0,o=n.clientLeft||r.clientLeft||0,u=i.pageYOffset||n.scrollTop,a=i.pageXOffset||n.scrollLeft,{top:f.top+u-s,left:f.left+a-o}):f)},v.offset={bodyOffset:function(e){var t=e.offsetTop,n=e.offsetLeft;return v.support.doesNotIncludeMarginInBodyOffset&&(t+=parseFloat(v.css(e,"marginTop"))||0,n+=parseFloat(v.css(e,"marginLeft"))||0),{top:t,left:n}},setOffset:function(e,t,n){var r=v.css(e,"position");r==="static"&&(e.style.position="relative");var i=v(e),s=i.offset(),o=v.css(e,"top"),u=v.css(e,"left"),a=(r==="absolute"||r==="fixed")&&v.inArray("auto",[o,u])>-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file diff --git a/data/js/lib/jquery.bookmarkscroll.js b/data/js/lib/jquery.bookmarkscroll.js index d9bc8e6abad169ddff67a81975ebcdd8f4da6509..de49e54536ba58a469006a6383f1060fae809d1b 100644 --- a/data/js/lib/jquery.bookmarkscroll.js +++ b/data/js/lib/jquery.bookmarkscroll.js @@ -1,51 +1,51 @@ -//** Scrolling HTML Bookmarks script- (c) Dynamic Drive DHTML code library: http://www.dynamicdrive.com. -//** Available/ usage terms at http://www.dynamicdrive.com/ (April 11th, 09') -//** Updated Nov 10th, 09'- Fixed anchor jumping issue in IE7 - -var bookmarkscroll={ - setting: {duration:1000, yoffset:0}, //{duration_of_scroll_milliseconds, offset_from_target_element_to_rest} - topkeyword: '#top', //keyword used in your anchors and scrollTo() to cause script to scroll page to very top - - scrollTo:function(dest, options, hash){ - var $=jQuery, options=options || {} - var $dest=(typeof dest=="string" && dest.length>0)? (dest==this.topkeyword? 0 : $('#'+dest)) : (dest)? $(dest) : [] //get element based on id, topkeyword, or dom ref - if ($dest===0 || $dest.length==1 && (!options.autorun || options.autorun && Math.abs($dest.offset().top+(options.yoffset||this.setting.yoffset)-$(window).scrollTop())>5)){ - this.$body.animate({scrollTop: ($dest===0)? 0 : $dest.offset().top+(options.yoffset||this.setting.yoffset)}, (options.duration||this.setting.duration), function(){ - if ($dest!==0 && hash) - location.hash=hash - }) - } - }, - - urlparamselect:function(){ - var param=window.location.search.match(/scrollto=[\w\-_,]+/i) //search for scrollto=divid - return (param)? param[0].split('=')[1] : null - }, - - init:function(){ - jQuery(document).ready(function($){ - var mainobj=bookmarkscroll - mainobj.$body=(window.opera)? (document.compatMode=="CSS1Compat"? $('html') : $('body')) : $('html,body') - var urlselectid=mainobj.urlparamselect() //get div of page.htm?scrollto=divid - if (urlselectid) //if id defined - setTimeout(function(){mainobj.scrollTo(document.getElementById(urlselectid) || $('a[name='+urlselectid+']:eq(0)').get(0), {autorun:true})}, 100) - $('a[href^="#"]').each(function(){ //loop through links with "#" prefix - var hashvalue=this.getAttribute('href').match(/#\w+$/i) //filter links at least 1 character following "#" prefix - hashvalue=(hashvalue)? hashvalue[0].substring(1) : null //strip "#" from hashvalue - if (this.hash.length>1){ //if hash value is more than just "#" - var $bookmark=$('a[name='+this.hash.substr(1)+']:eq(0)') - if ($bookmark.length==1 || this.hash==mainobj.topkeyword){ //if HTML anchor with given ID exists or href==topkeyword - if ($bookmark.length==1 && !document.all) //non IE, or IE7+ - $bookmark.html('.').css({position:'absolute', fontSize:1, visibility:'hidden'}) - $(this).click(function(e){ - mainobj.scrollTo((this.hash==mainobj.topkeyword)? mainobj.topkeyword : $bookmark.get(0), {}, this.hash) - e.preventDefault() - }) - } - } - }) - }) - } -} - +//** Scrolling HTML Bookmarks script- (c) Dynamic Drive DHTML code library: http://www.dynamicdrive.com. +//** Available/ usage terms at http://www.dynamicdrive.com/ (April 11th, 09') +//** Updated Nov 10th, 09'- Fixed anchor jumping issue in IE7 + +var bookmarkscroll={ + setting: {duration:1000, yoffset:-50}, //{duration_of_scroll_milliseconds, offset_from_target_element_to_rest} + topkeyword: '#top', //keyword used in your anchors and scrollTo() to cause script to scroll page to very top + + scrollTo:function(dest, options, hash){ + var $=jQuery, options=options || {} + var $dest=(typeof dest=="string" && dest.length>0)? (dest==this.topkeyword? 0 : $('#'+dest)) : (dest)? $(dest) : [] //get element based on id, topkeyword, or dom ref + if ($dest===0 || $dest.length==1 && (!options.autorun || options.autorun && Math.abs($dest.offset().top+(options.yoffset||this.setting.yoffset)-$(window).scrollTop())>5)){ + this.$body.animate({scrollTop: ($dest===0)? 0 : $dest.offset().top+(options.yoffset||this.setting.yoffset)}, (options.duration||this.setting.duration), function(){ +//** if ($dest!==0 && hash) +//** location.hash=hash + }) + } + }, + + urlparamselect:function(){ + var param=window.location.search.match(/scrollto=[\w\-_,]+/i) //search for scrollto=divid + return (param)? param[0].split('=')[1] : null + }, + + init:function(){ + jQuery(document).ready(function($){ + var mainobj=bookmarkscroll + mainobj.$body=(window.opera)? (document.compatMode=="CSS1Compat"? $('html') : $('body')) : $('html,body') + var urlselectid=mainobj.urlparamselect() //get div of page.htm?scrollto=divid + if (urlselectid) //if id defined + setTimeout(function(){mainobj.scrollTo(document.getElementById(urlselectid) || $('a[name='+urlselectid+']:eq(0)').get(0), {autorun:true})}, 100) + $('a[href^="#"]').each(function(){ //loop through links with "#" prefix + var hashvalue=this.getAttribute('href').match(/#\w+$/i) //filter links at least 1 character following "#" prefix + hashvalue=(hashvalue)? hashvalue[0].substring(1) : null //strip "#" from hashvalue + if (this.hash.length>1){ //if hash value is more than just "#" + var $bookmark=$('a[name='+this.hash.substr(1)+']:eq(0)') + if ($bookmark.length==1 || this.hash==mainobj.topkeyword){ //if HTML anchor with given ID exists or href==topkeyword + if ($bookmark.length==1 && !document.all) //non IE, or IE7+ + $bookmark.html('.').css({position:'absolute', fontSize:1, visibility:'hidden'}) + $(this).click(function(e){ + mainobj.scrollTo((this.hash==mainobj.topkeyword)? mainobj.topkeyword : $bookmark.get(0), {}, this.hash) + e.preventDefault() + }) + } + } + }) + }) + } +} + bookmarkscroll.init() \ No newline at end of file diff --git a/data/js/lib/jquery.pnotify-1.0.2.min.js b/data/js/lib/jquery.pnotify-1.0.2.min.js new file mode 100644 index 0000000000000000000000000000000000000000..99bcfdc97cadb144ad58248bb4c3c888ff35695e --- /dev/null +++ b/data/js/lib/jquery.pnotify-1.0.2.min.js @@ -0,0 +1,34 @@ +/* + * jQuery Pines Notify (pnotify) Plugin 1.0.2 + * + * Copyright (c) 2009-2011 Hunter Perrin + * + * Triple license under the GPL, LGPL, and MPL: + * http://www.gnu.org/licenses/gpl.html + * http://www.gnu.org/licenses/lgpl.html + * http://www.mozilla.org/MPL/MPL-1.1.html + */ +(function(e){var n,j,g,k;e.extend({pnotify_remove_all:function(){var d=g.data("pnotify");d&&d.length&&e.each(d,function(){this.pnotify_remove&&this.pnotify_remove()})},pnotify_position_all:function(){j&&clearTimeout(j);j=null;var d=g.data("pnotify");d&&d.length&&(e.each(d,function(){var c=this.opts.pnotify_stack;if(c){if(!c.nextpos1)c.nextpos1=c.firstpos1;if(!c.nextpos2)c.nextpos2=c.firstpos2;if(!c.addpos2)c.addpos2=0;if(this.css("display")!="none"){var a,e,d={},b;switch(c.dir1){case "down":b="top"; +break;case "up":b="bottom";break;case "left":b="right";break;case "right":b="left"}a=parseInt(this.css(b));isNaN(a)&&(a=0);if(typeof c.firstpos1=="undefined")c.firstpos1=a,c.nextpos1=c.firstpos1;var f;switch(c.dir2){case "down":f="top";break;case "up":f="bottom";break;case "left":f="right";break;case "right":f="left"}e=parseInt(this.css(f));isNaN(e)&&(e=0);if(typeof c.firstpos2=="undefined")c.firstpos2=e,c.nextpos2=c.firstpos2;if(c.dir1=="down"&&c.nextpos1+this.height()>k.height()||c.dir1=="up"&& +c.nextpos1+this.height()>k.height()||c.dir1=="left"&&c.nextpos1+this.width()>k.width()||c.dir1=="right"&&c.nextpos1+this.width()>k.width())c.nextpos1=c.firstpos1,c.nextpos2+=c.addpos2+10,c.addpos2=0;if(c.animation&&c.nextpos2<e)switch(c.dir2){case "down":d.top=c.nextpos2+"px";break;case "up":d.bottom=c.nextpos2+"px";break;case "left":d.right=c.nextpos2+"px";break;case "right":d.left=c.nextpos2+"px"}else this.css(f,c.nextpos2+"px");switch(c.dir2){case "down":case "up":if(this.outerHeight(!0)>c.addpos2)c.addpos2= +this.height();break;case "left":case "right":if(this.outerWidth(!0)>c.addpos2)c.addpos2=this.width()}if(c.nextpos1)if(c.animation&&(a>c.nextpos1||d.top||d.bottom||d.right||d.left))switch(c.dir1){case "down":d.top=c.nextpos1+"px";break;case "up":d.bottom=c.nextpos1+"px";break;case "left":d.right=c.nextpos1+"px";break;case "right":d.left=c.nextpos1+"px"}else this.css(b,c.nextpos1+"px");(d.top||d.bottom||d.right||d.left)&&this.animate(d,{duration:500,queue:!1});switch(c.dir1){case "down":case "up":c.nextpos1+= +this.height()+10;break;case "left":case "right":c.nextpos1+=this.width()+10}}}}),e.each(d,function(){var c=this.opts.pnotify_stack;if(c)c.nextpos1=c.firstpos1,c.nextpos2=c.firstpos2,c.addpos2=0,c.animation=!0}))},pnotify:function(d){g||(g=e("body"));k||(k=e(window));var c,a;typeof d!="object"?(a=e.extend({},e.pnotify.defaults),a.pnotify_text=d):a=e.extend({},e.pnotify.defaults,d);if(a.pnotify_before_init&&a.pnotify_before_init(a)===!1)return null;var h,l=function(a,c){b.css("display","none");var d= +document.elementFromPoint(a.clientX,a.clientY);b.css("display","block");var f=e(d),g=f.css("cursor");b.css("cursor",g!="auto"?g:"default");if(!h||h.get(0)!=d)h&&(m.call(h.get(0),"mouseleave",a.originalEvent),m.call(h.get(0),"mouseout",a.originalEvent)),m.call(d,"mouseenter",a.originalEvent),m.call(d,"mouseover",a.originalEvent);m.call(d,c,a.originalEvent);h=f},b=e("<div />",{"class":"ui-pnotify "+a.pnotify_addclass,css:{display:"none"},mouseenter:function(o){a.pnotify_nonblock&&o.stopPropagation(); +a.pnotify_mouse_reset&&c=="out"&&(b.stop(!0),c="in",b.css("height","auto").animate({width:a.pnotify_width,opacity:a.pnotify_nonblock?a.pnotify_nonblock_opacity:a.pnotify_opacity},"fast"));a.pnotify_nonblock&&b.animate({opacity:a.pnotify_nonblock_opacity},"fast");a.pnotify_hide&&a.pnotify_mouse_reset&&b.pnotify_cancel_remove();a.pnotify_closer&&!a.pnotify_nonblock&&b.closer.show()},mouseleave:function(o){a.pnotify_nonblock&&o.stopPropagation();h=null;b.css("cursor","auto");a.pnotify_nonblock&&c!="out"&& +b.animate({opacity:a.pnotify_opacity},"fast");a.pnotify_hide&&a.pnotify_mouse_reset&&b.pnotify_queue_remove();b.closer.hide();e.pnotify_position_all()},mouseover:function(b){a.pnotify_nonblock&&b.stopPropagation()},mouseout:function(b){a.pnotify_nonblock&&b.stopPropagation()},mousemove:function(b){a.pnotify_nonblock&&(b.stopPropagation(),l(b,"onmousemove"))},mousedown:function(b){a.pnotify_nonblock&&(b.stopPropagation(),b.preventDefault(),l(b,"onmousedown"))},mouseup:function(b){a.pnotify_nonblock&& +(b.stopPropagation(),b.preventDefault(),l(b,"onmouseup"))},click:function(b){a.pnotify_nonblock&&(b.stopPropagation(),l(b,"onclick"))},dblclick:function(b){a.pnotify_nonblock&&(b.stopPropagation(),l(b,"ondblclick"))}});b.opts=a;if(a.pnotify_shadow&&!e.browser.msie)b.shadow_container=e("<div />",{"class":"ui-widget-shadow ui-corner-all ui-pnotify-shadow"}).prependTo(b);b.container=e("<div />",{"class":"ui-widget ui-widget-content ui-corner-all ui-pnotify-container "+(a.pnotify_type=="error"?"ui-state-error": +"ui-state-highlight")}).appendTo(b);b.pnotify_version="1.0.2";b.pnotify=function(c){var d=a;typeof c=="string"?a.pnotify_text=c:a=e.extend({},a,c);b.opts=a;if(a.pnotify_shadow!=d.pnotify_shadow)a.pnotify_shadow&&!e.browser.msie?b.shadow_container=e("<div />",{"class":"ui-widget-shadow ui-pnotify-shadow"}).prependTo(b):b.children(".ui-pnotify-shadow").remove();a.pnotify_addclass===!1?b.removeClass(d.pnotify_addclass):a.pnotify_addclass!==d.pnotify_addclass&&b.removeClass(d.pnotify_addclass).addClass(a.pnotify_addclass); +a.pnotify_title===!1?b.title_container.hide("fast"):a.pnotify_title!==d.pnotify_title&&b.title_container.html(a.pnotify_title).show(200);if(a.pnotify_text===!1)b.text_container.hide("fast");else if(a.pnotify_text!==d.pnotify_text){if(a.pnotify_insert_brs)a.pnotify_text=a.pnotify_text.replace(/\n/g,"<br />");b.text_container.html(a.pnotify_text).show(200)}b.pnotify_history=a.pnotify_history;a.pnotify_type!=d.pnotify_type&&b.container.toggleClass("ui-state-error ui-state-highlight");if(a.pnotify_notice_icon!= +d.pnotify_notice_icon&&a.pnotify_type=="notice"||a.pnotify_error_icon!=d.pnotify_error_icon&&a.pnotify_type=="error"||a.pnotify_type!=d.pnotify_type)if(b.container.find("div.ui-pnotify-icon").remove(),a.pnotify_error_icon&&a.pnotify_type=="error"||a.pnotify_notice_icon)e("<div />",{"class":"ui-pnotify-icon"}).append(e("<span />",{"class":a.pnotify_type=="error"?a.pnotify_error_icon:a.pnotify_notice_icon})).prependTo(b.container);a.pnotify_width!==d.pnotify_width&&b.animate({width:a.pnotify_width}); +a.pnotify_min_height!==d.pnotify_min_height&&b.container.animate({minHeight:a.pnotify_min_height});a.pnotify_opacity!==d.pnotify_opacity&&b.fadeTo(a.pnotify_animate_speed,a.pnotify_opacity);a.pnotify_hide?d.pnotify_hide||b.pnotify_queue_remove():b.pnotify_cancel_remove();b.pnotify_queue_position();return b};b.pnotify_queue_position=function(){j&&clearTimeout(j);j=setTimeout(e.pnotify_position_all,10)};b.pnotify_display=function(){b.parent().length||b.appendTo(g);a.pnotify_before_open&&a.pnotify_before_open(b)=== +!1||(b.pnotify_queue_position(),a.pnotify_animation=="fade"||a.pnotify_animation.effect_in=="fade"?b.show().fadeTo(0,0).hide():a.pnotify_opacity!=1&&b.show().fadeTo(0,a.pnotify_opacity).hide(),b.animate_in(function(){a.pnotify_after_open&&a.pnotify_after_open(b);b.pnotify_queue_position();a.pnotify_hide&&b.pnotify_queue_remove()}))};b.pnotify_remove=function(){if(b.timer)window.clearTimeout(b.timer),b.timer=null;a.pnotify_before_close&&a.pnotify_before_close(b)===!1||b.animate_out(function(){a.pnotify_after_close&& +a.pnotify_after_close(b)===!1||(b.pnotify_queue_position(),a.pnotify_remove&&b.detach())})};b.animate_in=function(d){c="in";var e;e=typeof a.pnotify_animation.effect_in!="undefined"?a.pnotify_animation.effect_in:a.pnotify_animation;e=="none"?(b.show(),d()):e=="show"?b.show(a.pnotify_animate_speed,d):e=="fade"?b.show().fadeTo(a.pnotify_animate_speed,a.pnotify_opacity,d):e=="slide"?b.slideDown(a.pnotify_animate_speed,d):typeof e=="function"?e("in",d,b):b.effect&&b.effect(e,{},a.pnotify_animate_speed, +d)};b.animate_out=function(d){c="out";var e;e=typeof a.pnotify_animation.effect_out!="undefined"?a.pnotify_animation.effect_out:a.pnotify_animation;e=="none"?(b.hide(),d()):e=="show"?b.hide(a.pnotify_animate_speed,d):e=="fade"?b.fadeOut(a.pnotify_animate_speed,d):e=="slide"?b.slideUp(a.pnotify_animate_speed,d):typeof e=="function"?e("out",d,b):b.effect&&b.effect(e,{},a.pnotify_animate_speed,d)};b.pnotify_cancel_remove=function(){b.timer&&window.clearTimeout(b.timer)};b.pnotify_queue_remove=function(){b.pnotify_cancel_remove(); +b.timer=window.setTimeout(function(){b.pnotify_remove()},isNaN(a.pnotify_delay)?0:a.pnotify_delay)};b.closer=e("<div />",{"class":"ui-pnotify-closer",css:{cursor:"pointer",display:"none"},click:function(){b.pnotify_remove();b.closer.hide()}}).append(e("<span />",{"class":"ui-icon ui-icon-circle-close"})).appendTo(b.container);if(a.pnotify_error_icon&&a.pnotify_type=="error"||a.pnotify_notice_icon)e("<div />",{"class":"ui-pnotify-icon"}).append(e("<span />",{"class":a.pnotify_type=="error"?a.pnotify_error_icon: +a.pnotify_notice_icon})).appendTo(b.container);b.title_container=e("<div />",{"class":"ui-pnotify-title",html:a.pnotify_title}).appendTo(b.container);a.pnotify_title===!1&&b.title_container.hide();if(a.pnotify_insert_brs&&typeof a.pnotify_text=="string")a.pnotify_text=a.pnotify_text.replace(/\n/g,"<br />");b.text_container=e("<div />",{"class":"ui-pnotify-text",html:a.pnotify_text}).appendTo(b.container);a.pnotify_text===!1&&b.text_container.hide();typeof a.pnotify_width=="string"&&b.css("width", +a.pnotify_width);typeof a.pnotify_min_height=="string"&&b.container.css("min-height",a.pnotify_min_height);b.pnotify_history=a.pnotify_history;var f=g.data("pnotify");if(f==null||typeof f!="object")f=[];f=a.pnotify_stack.push=="top"?e.merge([b],f):e.merge(f,[b]);g.data("pnotify",f);a.pnotify_after_init&&a.pnotify_after_init(b);if(a.pnotify_history){var i=g.data("pnotify_history");typeof i=="undefined"&&(i=e("<div />",{"class":"ui-pnotify-history-container ui-state-default ui-corner-bottom",mouseleave:function(){i.animate({top:"-"+ +n+"px"},{duration:100,queue:!1})}}).append(e("<div />",{"class":"ui-pnotify-history-header",text:"Redisplay"})).append(e("<button />",{"class":"ui-pnotify-history-all ui-state-default ui-corner-all",text:"All",mouseenter:function(){e(this).addClass("ui-state-hover")},mouseleave:function(){e(this).removeClass("ui-state-hover")},click:function(){e.each(f,function(){this.pnotify_history&&this.pnotify_display&&this.pnotify_display()});return!1}})).append(e("<button />",{"class":"ui-pnotify-history-last ui-state-default ui-corner-all", +text:"Last",mouseenter:function(){e(this).addClass("ui-state-hover")},mouseleave:function(){e(this).removeClass("ui-state-hover")},click:function(){for(var a=1;!f[f.length-a]||!f[f.length-a].pnotify_history||f[f.length-a].is(":visible");){if(f.length-a===0)return!1;a++}a=f[f.length-a];a.pnotify_display&&a.pnotify_display();return!1}})).appendTo(g),n=e("<span />",{"class":"ui-pnotify-history-pulldown ui-icon ui-icon-grip-dotted-horizontal",mouseenter:function(){i.animate({top:"0"},{duration:100,queue:!1})}}).appendTo(i).offset().top+ +2,i.css({top:"-"+n+"px"}),g.data("pnotify_history",i))}a.pnotify_stack.animation=!1;b.pnotify_display();return b}});var p=/^on/,q=/^(dbl)?click$|^mouse(move|down|up|over|out|enter|leave)$|^contextmenu$/,r=/^(focus|blur|select|change|reset)$|^key(press|down|up)$/,s=/^(scroll|resize|(un)?load|abort|error)$/,m=function(d,c){var a,d=d.toLowerCase();document.createEvent&&this.dispatchEvent?(d=d.replace(p,""),d.match(q)?(e(this).offset(),a=document.createEvent("MouseEvents"),a.initMouseEvent(d,c.bubbles, +c.cancelable,c.view,c.detail,c.screenX,c.screenY,c.clientX,c.clientY,c.ctrlKey,c.altKey,c.shiftKey,c.metaKey,c.button,c.relatedTarget)):d.match(r)?(a=document.createEvent("UIEvents"),a.initUIEvent(d,c.bubbles,c.cancelable,c.view,c.detail)):d.match(s)&&(a=document.createEvent("HTMLEvents"),a.initEvent(d,c.bubbles,c.cancelable)),a&&this.dispatchEvent(a)):(d.match(p)||(d="on"+d),a=document.createEventObject(c),this.fireEvent(d,a))};e.pnotify.defaults={pnotify_title:!1,pnotify_text:!1,pnotify_addclass:"", +pnotify_nonblock:!1,pnotify_nonblock_opacity:0.2,pnotify_history:!0,pnotify_width:"300px",pnotify_min_height:"16px",pnotify_type:"notice",pnotify_notice_icon:"ui-icon ui-icon-info",pnotify_error_icon:"ui-icon ui-icon-alert",pnotify_animation:"fade",pnotify_animate_speed:"slow",pnotify_opacity:1,pnotify_shadow:!1,pnotify_closer:!0,pnotify_hide:!0,pnotify_delay:8E3,pnotify_mouse_reset:!0,pnotify_remove:!0,pnotify_insert_brs:!0,pnotify_stack:{dir1:"down",dir2:"left",push:"bottom"}}})(jQuery); diff --git a/data/js/lib/jquery.selectboxes.min.js b/data/js/lib/jquery.selectboxes.min.js index 812e9bddca71e7e0582287c463f83218ec1ec24d..2ea85acc0152c98d2f86bd2fcd5e3dec916bc9e7 100644 --- a/data/js/lib/jquery.selectboxes.min.js +++ b/data/js/lib/jquery.selectboxes.min.js @@ -1,14 +1,14 @@ -/* - * - * Copyright (c) 2006-2008 Sam Collett (http://www.texotela.co.uk) - * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) - * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. - * - * Version 2.2.4 - * Demo: http://www.texotela.co.uk/code/jquery/select/ - * - * $LastChangedDate: 2008-06-17 17:27:25 +0100 (Tue, 17 Jun 2008) $ - * $Rev: 5727 $ - * - */ +/* + * + * Copyright (c) 2006-2008 Sam Collett (http://www.texotela.co.uk) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * Version 2.2.4 + * Demo: http://www.texotela.co.uk/code/jquery/select/ + * + * $LastChangedDate: 2008-06-17 17:27:25 +0100 (Tue, 17 Jun 2008) $ + * $Rev: 5727 $ + * + */ ;(function(h){h.fn.addOption=function(){var j=function(a,f,c,g){var d=document.createElement("option");d.value=f,d.text=c;var b=a.options;var e=b.length;if(!a.cache){a.cache={};for(var i=0;i<e;i++){a.cache[b[i].value]=i}}if(typeof a.cache[f]=="undefined")a.cache[f]=e;a.options[a.cache[f]]=d;if(g){d.selected=true}};var k=arguments;if(k.length==0)return this;var l=true;var m=false;var n,o,p;if(typeof(k[0])=="object"){m=true;n=k[0]}if(k.length>=2){if(typeof(k[1])=="boolean")l=k[1];else if(typeof(k[2])=="boolean")l=k[2];if(!m){o=k[0];p=k[1]}}this.each(function(){if(this.nodeName.toLowerCase()!="select")return;if(m){for(var a in n){j(this,a,n[a],l)}}else{j(this,o,p,l)}});return this};h.fn.ajaxAddOption=function(c,g,d,b,e){if(typeof(c)!="string")return this;if(typeof(g)!="object")g={};if(typeof(d)!="boolean")d=true;this.each(function(){var f=this;h.getJSON(c,g,function(a){h(f).addOption(a,d);if(typeof b=="function"){if(typeof e=="object"){b.apply(f,e)}else{b.call(f)}}})});return this};h.fn.removeOption=function(){var d=arguments;if(d.length==0)return this;var b=typeof(d[0]);var e,i;if(b=="string"||b=="object"||b=="function"){e=d[0];if(e.constructor==Array){var j=e.length;for(var k=0;k<j;k++){this.removeOption(e[k],d[1])}return this}}else if(b=="number")i=d[0];else return this;this.each(function(){if(this.nodeName.toLowerCase()!="select")return;if(this.cache)this.cache=null;var a=false;var f=this.options;if(!!e){var c=f.length;for(var g=c-1;g>=0;g--){if(e.constructor==RegExp){if(f[g].value.match(e)){a=true}}else if(f[g].value==e){a=true}if(a&&d[1]===true)a=f[g].selected;if(a){f[g]=null}a=false}}else{if(d[1]===true){a=f[i].selected}else{a=true}if(a){this.remove(i)}}});return this};h.fn.sortOptions=function(e){var i=h(this).selectedValues();var j=typeof(e)=="undefined"?true:!!e;this.each(function(){if(this.nodeName.toLowerCase()!="select")return;var c=this.options;var g=c.length;var d=[];for(var b=0;b<g;b++){d[b]={v:c[b].value,t:c[b].text}}d.sort(function(a,f){o1t=a.t.toLowerCase(),o2t=f.t.toLowerCase();if(o1t==o2t)return 0;if(j){return o1t<o2t?-1:1}else{return o1t>o2t?-1:1}});for(var b=0;b<g;b++){c[b].text=d[b].t;c[b].value=d[b].v}}).selectOptions(i,true);return this};h.fn.selectOptions=function(g,d){var b=g;var e=typeof(g);if(e=="object"&&b.constructor==Array){var i=this;h.each(b,function(){i.selectOptions(this,d)})};var j=d||false;if(e!="string"&&e!="function"&&e!="object")return this;this.each(function(){if(this.nodeName.toLowerCase()!="select")return this;var a=this.options;var f=a.length;for(var c=0;c<f;c++){if(b.constructor==RegExp){if(a[c].value.match(b)){a[c].selected=true}else if(j){a[c].selected=false}}else{if(a[c].value==b){a[c].selected=true}else if(j){a[c].selected=false}}}});return this};h.fn.copyOptions=function(g,d){var b=d||"selected";if(h(g).size()==0)return this;this.each(function(){if(this.nodeName.toLowerCase()!="select")return this;var a=this.options;var f=a.length;for(var c=0;c<f;c++){if(b=="all"||(b=="selected"&&a[c].selected)){h(g).addOption(a[c].value,a[c].text)}}});return this};h.fn.containsOption=function(g,d){var b=false;var e=g;var i=typeof(e);var j=typeof(d);if(i!="string"&&i!="function"&&i!="object")return j=="function"?this:b;this.each(function(){if(this.nodeName.toLowerCase()!="select")return this;if(b&&j!="function")return false;var a=this.options;var f=a.length;for(var c=0;c<f;c++){if(e.constructor==RegExp){if(a[c].value.match(e)){b=true;if(j=="function")d.call(a[c],c)}}else{if(a[c].value==e){b=true;if(j=="function")d.call(a[c],c)}}}});return j=="function"?this:b};h.fn.selectedValues=function(){var a=[];this.selectedOptions().each(function(){a[a.length]=this.value});return a};h.fn.selectedTexts=function(){var a=[];this.selectedOptions().each(function(){a[a.length]=this.text});return a};h.fn.selectedOptions=function(){return this.find("option:selected")}})(jQuery); \ No newline at end of file diff --git a/data/js/lib/jquery.tokeninput.js b/data/js/lib/jquery.tokeninput.js index 672be5221329f73fd771e6e99d2a2c0ca2ec31cb..ee3995e933ab43c487d1fcdf7632da2391aae07b 100644 --- a/data/js/lib/jquery.tokeninput.js +++ b/data/js/lib/jquery.tokeninput.js @@ -1,861 +1,861 @@ -/* - * jQuery Plugin: Tokenizing Autocomplete Text Entry - * Version 1.6.0 - * - * Copyright (c) 2009 James Smith (http://loopj.com) - * Licensed jointly under the GPL and MIT licenses, - * choose which one suits your project best! - * - */ - -(function ($) { -// Default settings -var DEFAULT_SETTINGS = { - // Search settings - method: "GET", - contentType: "json", - queryParam: "q", - searchDelay: 300, - minChars: 1, - propertyToSearch: "name", - jsonContainer: null, - - // Display settings - hintText: "Type in a search term", - noResultsText: "No results", - searchingText: "Searching...", - deleteText: "×", - animateDropdown: true, - - // Tokenization settings - tokenLimit: null, - tokenDelimiter: ",", - preventDuplicates: false, - - // Output settings - tokenValue: "id", - - // Prepopulation settings - prePopulate: null, - processPrePopulate: true, - - // Manipulation settings - idPrefix: "token-input-", - - // Formatters - resultsFormatter: function(item){ return "<li><img src='/images/flags/"+item["id"]+".png' /> " + item[this.propertyToSearch]+ "</li>" }, - tokenFormatter: function(item) { return "<li><img src='/images/flags/"+item["id"]+".png' /> <p>" + item[this.propertyToSearch] + "</p></li>" }, - flag: "flag", - - // Callbacks - onResult: null, - onAdd: null, - onDelete: null, - onReady: null -}; - -// Default classes to use when theming -var DEFAULT_CLASSES = { - tokenList: "token-input-list", - token: "token-input-token", - tokenDelete: "token-input-delete-token", - selectedToken: "token-input-selected-token", - highlightedToken: "token-input-highlighted-token", - dropdown: "token-input-dropdown", - dropdownItem: "token-input-dropdown-item", - dropdownItem2: "token-input-dropdown-item2", - selectedDropdownItem: "token-input-selected-dropdown-item", - inputToken: "token-input-input-token" -}; - -// Input box position "enum" -var POSITION = { - BEFORE: 0, - AFTER: 1, - END: 2 -}; - -// Keys "enum" -var KEY = { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - ESCAPE: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - NUMPAD_ENTER: 108, - COMMA: 188 -}; - -// Additional public (exposed) methods -var methods = { - init: function(url_or_data_or_function, options) { - var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); - - return this.each(function () { - $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); - }); - }, - clear: function() { - this.data("tokenInputObject").clear(); - return this; - }, - add: function(item) { - this.data("tokenInputObject").add(item); - return this; - }, - remove: function(item) { - this.data("tokenInputObject").remove(item); - return this; - }, - get: function() { - return this.data("tokenInputObject").getTokens(); - } -} - -// Expose the .tokenInput function to jQuery as a plugin -$.fn.tokenInput = function (method) { - // Method calling and initialization logic - if(methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else { - return methods.init.apply(this, arguments); - } -}; - -// TokenList class for each input -$.TokenList = function (input, url_or_data, settings) { - // - // Initialization - // - - // Configure the data source - if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { - // Set the url to query against - settings.url = url_or_data; - - // If the URL is a function, evaluate it here to do our initalization work - var url = computeURL(); - - // Make a smart guess about cross-domain if it wasn't explicitly specified - if(settings.crossDomain === undefined) { - if(url.indexOf("://") === -1) { - settings.crossDomain = false; - } else { - settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); - } - } - } else if(typeof(url_or_data) === "object") { - // Set the local data to search through - settings.local_data = url_or_data; - } - - // Build class names - if(settings.classes) { - // Use custom class names - settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); - } else if(settings.theme) { - // Use theme-suffixed default class names - settings.classes = {}; - $.each(DEFAULT_CLASSES, function(key, value) { - settings.classes[key] = value + "-" + settings.theme; - }); - } else { - settings.classes = DEFAULT_CLASSES; - } - - - // Save the tokens - var saved_tokens = []; - - // Keep track of the number of tokens in the list - var token_count = 0; - - // Basic cache to save on db hits - var cache = new $.TokenList.Cache(); - - // Keep track of the timeout, old vals - var timeout; - var input_val; - - // Create a new text input an attach keyup events - var input_box = $("<input type=\"text\" autocomplete=\"off\" >") - .css({ - outline: "none" - }) - .attr("id", settings.idPrefix + input.id) - .focus(function () { - if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { - show_dropdown_hint(); - } - }) - .blur(function () { - hide_dropdown(); - $(this).val(""); - }) - .bind("keyup keydown blur update", resize_input) - .keydown(function (event) { - var previous_token; - var next_token; - - switch(event.keyCode) { - case KEY.LEFT: - case KEY.RIGHT: - case KEY.UP: - case KEY.DOWN: - if(!$(this).val()) { - previous_token = input_token.prev(); - next_token = input_token.next(); - - if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { - // Check if there is a previous/next token and it is selected - if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { - deselect_token($(selected_token), POSITION.BEFORE); - } else { - deselect_token($(selected_token), POSITION.AFTER); - } - } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { - // We are moving left, select the previous token if it exists - select_token($(previous_token.get(0))); - } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { - // We are moving right, select the next token if it exists - select_token($(next_token.get(0))); - } - } else { - var dropdown_item = null; - - if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { - dropdown_item = $(selected_dropdown_item).next(); - } else { - dropdown_item = $(selected_dropdown_item).prev(); - } - - if(dropdown_item.length) { - select_dropdown_item(dropdown_item); - } - return false; - } - break; - - case KEY.BACKSPACE: - previous_token = input_token.prev(); - - if(!$(this).val().length) { - if(selected_token) { - delete_token($(selected_token)); - hidden_input.change(); - } else if(previous_token.length) { - select_token($(previous_token.get(0))); - } - - return false; - } else if($(this).val().length === 1) { - hide_dropdown(); - } else { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); - } - break; - - case KEY.TAB: - case KEY.ENTER: - case KEY.NUMPAD_ENTER: - case KEY.COMMA: - if(selected_dropdown_item) { - add_token($(selected_dropdown_item).data("tokeninput")); - hidden_input.change(); - return false; - } - break; - - case KEY.ESCAPE: - hide_dropdown(); - return true; - - default: - if(String.fromCharCode(event.which)) { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); - } - break; - } - }); - - // Keep a reference to the original input box - var hidden_input = $(input) - .hide() - .val("") - .focus(function () { - input_box.focus(); - }) - .blur(function () { - input_box.blur(); - }); - - // Keep a reference to the selected token and dropdown item - var selected_token = null; - var selected_token_index = 0; - var selected_dropdown_item = null; - - // The list to store the token items in - var token_list = $("<ul />") - .addClass(settings.classes.tokenList) - .click(function (event) { - var li = $(event.target).closest("li"); - if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { - toggle_select_token(li); - } else { - // Deselect selected token - if(selected_token) { - deselect_token($(selected_token), POSITION.END); - } - - // Focus input box - input_box.focus(); - } - }) - .mouseover(function (event) { - var li = $(event.target).closest("li"); - if(li && selected_token !== this) { - li.addClass(settings.classes.highlightedToken); - } - }) - .mouseout(function (event) { - var li = $(event.target).closest("li"); - if(li && selected_token !== this) { - li.removeClass(settings.classes.highlightedToken); - } - }) - .insertBefore(hidden_input); - - // The token holding the input box - var input_token = $("<li />") - .addClass(settings.classes.inputToken) - .appendTo(token_list) - .append(input_box); - - // The list to store the dropdown items in - var dropdown = $("<div>") - .addClass(settings.classes.dropdown) - .appendTo("body") - .hide(); - - // Magic element to help us resize the text input - var input_resizer = $("<tester/>") - .insertAfter(input_box) - .css({ - position: "absolute", - top: -9999, - left: -9999, - width: "auto", - fontSize: input_box.css("fontSize"), - fontFamily: input_box.css("fontFamily"), - fontWeight: input_box.css("fontWeight"), - letterSpacing: input_box.css("letterSpacing"), - whiteSpace: "nowrap" - }); - - // Pre-populate list if items exist - hidden_input.val(""); - var li_data = settings.prePopulate || hidden_input.data("pre"); - if(settings.processPrePopulate && $.isFunction(settings.onResult)) { - li_data = settings.onResult.call(hidden_input, li_data); - } - if(li_data && li_data.length) { - $.each(li_data, function (index, value) { - insert_token(value); - checkTokenLimit(); - }); - } - - // Initialization is done - if($.isFunction(settings.onReady)) { - settings.onReady.call(); - } - - // - // Public functions - // - - this.clear = function() { - token_list.children("li").each(function() { - if ($(this).children("input").length === 0) { - delete_token($(this)); - } - }); - } - - this.add = function(item) { - add_token(item); - } - - this.remove = function(item) { - token_list.children("li").each(function() { - if ($(this).children("input").length === 0) { - var currToken = $(this).data("tokeninput"); - var match = true; - for (var prop in item) { - if (item[prop] !== currToken[prop]) { - match = false; - break; - } - } - if (match) { - delete_token($(this)); - } - } - }); - } - - this.getTokens = function() { - return saved_tokens; - } - - // - // Private functions - // - - function checkTokenLimit() { - if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { - input_box.hide(); - hide_dropdown(); - return; - } - } - - function resize_input() { - if(input_val === (input_val = input_box.val())) {return;} - - // Enter new content into resizer and resize input accordingly - var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(/</g, '<').replace(/>/g, '>'); - input_resizer.html(escaped); - input_box.width(input_resizer.width() + 30); - } - - function is_printable_character(keycode) { - return ((keycode >= 48 && keycode <= 90) || // 0-1a-z - (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . - (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ - (keycode >= 219 && keycode <= 222)); // ( \ ) ' - } - - // Inner function to a token to the list - function insert_token(item) { - var this_token = settings.tokenFormatter(item); - this_token = $(this_token) - .addClass(settings.classes.token) - .insertBefore(input_token); - - // The 'delete token' button - $("<span>" + settings.deleteText + "</span>") - .addClass(settings.classes.tokenDelete) - .appendTo(this_token) - .click(function () { - delete_token($(this).parent()); - hidden_input.change(); - return false; - }); - - // Store data on the token - var token_data = {"id": item.id}; - token_data[settings.propertyToSearch] = item[settings.propertyToSearch]; - $.data(this_token.get(0), "tokeninput", item); - - // Save this token for duplicate checking - saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); - selected_token_index++; - - // Update the hidden input - update_hidden_input(saved_tokens, hidden_input); - - token_count += 1; - - // Check the token limit - if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { - input_box.hide(); - hide_dropdown(); - } - - return this_token; - } - - // Add a token to the token list based on user input - function add_token (item) { - var callback = settings.onAdd; - - // See if the token already exists and select it if we don't want duplicates - if(token_count > 0 && settings.preventDuplicates) { - var found_existing_token = null; - token_list.children().each(function () { - var existing_token = $(this); - var existing_data = $.data(existing_token.get(0), "tokeninput"); - if(existing_data && existing_data.id === item.id) { - found_existing_token = existing_token; - return false; - } - }); - - if(found_existing_token) { - select_token(found_existing_token); - input_token.insertAfter(found_existing_token); - input_box.focus(); - return; - } - } - - // Insert the new tokens - if(settings.tokenLimit == null || token_count < settings.tokenLimit) { - insert_token(item); - checkTokenLimit(); - } - - // Clear input box - input_box.val(""); - - // Don't show the help dropdown, they've got the idea - hide_dropdown(); - - // Execute the onAdd callback if defined - if($.isFunction(callback)) { - callback.call(hidden_input,item); - } - } - - // Select a token in the token list - function select_token (token) { - token.addClass(settings.classes.selectedToken); - selected_token = token.get(0); - - // Hide input box - input_box.val(""); - - // Hide dropdown if it is visible (eg if we clicked to select token) - hide_dropdown(); - } - - // Deselect a token in the token list - function deselect_token (token, position) { - token.removeClass(settings.classes.selectedToken); - selected_token = null; - - if(position === POSITION.BEFORE) { - input_token.insertBefore(token); - selected_token_index--; - } else if(position === POSITION.AFTER) { - input_token.insertAfter(token); - selected_token_index++; - } else { - input_token.appendTo(token_list); - selected_token_index = token_count; - } - - // Show the input box and give it focus again - input_box.focus(); - } - - // Toggle selection of a token in the token list - function toggle_select_token(token) { - var previous_selected_token = selected_token; - - if(selected_token) { - deselect_token($(selected_token), POSITION.END); - } - - if(previous_selected_token === token.get(0)) { - deselect_token(token, POSITION.END); - } else { - select_token(token); - } - } - - // Delete a token from the token list - function delete_token (token) { - // Remove the id from the saved list - var token_data = $.data(token.get(0), "tokeninput"); - var callback = settings.onDelete; - - var index = token.prevAll().length; - if(index > selected_token_index) index--; - - // Delete the token - token.remove(); - selected_token = null; - - // Show the input box and give it focus again - input_box.focus(); - - // Remove this token from the saved list - saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); - if(index < selected_token_index) selected_token_index--; - - // Update the hidden input - update_hidden_input(saved_tokens, hidden_input); - - token_count -= 1; - - if(settings.tokenLimit !== null) { - input_box - .show() - .val("") - .focus(); - } - - // Execute the onDelete callback if defined - if($.isFunction(callback)) { - callback.call(hidden_input,token_data); - } - } - - // Update the hidden input box value - function update_hidden_input(saved_tokens, hidden_input) { - var token_values = $.map(saved_tokens, function (el) { - return el[settings.tokenValue]; - }); - hidden_input.val(token_values.join(settings.tokenDelimiter)); - - } - - // Hide and clear the results dropdown - function hide_dropdown () { - dropdown.hide().empty(); - selected_dropdown_item = null; - } - - function show_dropdown() { - dropdown - .css({ - position: "absolute", - top: $(token_list).offset().top + $(token_list).outerHeight(), - left: $(token_list).offset().left, - zindex: 999 - }) - .show(); - } - - function show_dropdown_searching () { - if(settings.searchingText) { - dropdown.html("<p>"+settings.searchingText+"</p>"); - show_dropdown(); - } - } - - function show_dropdown_hint () { - if(settings.hintText) { - dropdown.html("<p>"+settings.hintText+"</p>"); - show_dropdown(); - } - } - - // Highlight the query part of the search term - function highlight_term(value, term) { - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>"); - } - - function find_value_and_highlight_term(template, value, term) { - return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); - } - - // Populate the results dropdown with some results - function populate_dropdown (query, results) { - if(results && results.length) { - dropdown.empty(); - var dropdown_ul = $("<ul>") - .appendTo(dropdown) - .mouseover(function (event) { - select_dropdown_item($(event.target).closest("li")); - }) - .mousedown(function (event) { - add_token($(event.target).closest("li").data("tokeninput")); - hidden_input.change(); - return false; - }) - .hide(); - - $.each(results, function(index, value) { - var this_li = settings.resultsFormatter(value); - - this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query); - - this_li = $(this_li).appendTo(dropdown_ul); - - if(index % 2) { - this_li.addClass(settings.classes.dropdownItem); - } else { - this_li.addClass(settings.classes.dropdownItem2); - } - - if(index === 0) { - select_dropdown_item(this_li); - } - - $.data(this_li.get(0), "tokeninput", value); - }); - - show_dropdown(); - - if(settings.animateDropdown) { - dropdown_ul.slideDown("fast"); - } else { - dropdown_ul.show(); - } - } else { - if(settings.noResultsText) { - dropdown.html("<p>"+settings.noResultsText+"</p>"); - show_dropdown(); - } - } - } - - // Highlight an item in the results dropdown - function select_dropdown_item (item) { - if(item) { - if(selected_dropdown_item) { - deselect_dropdown_item($(selected_dropdown_item)); - } - - item.addClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = item.get(0); - } - } - - // Remove highlighting from an item in the results dropdown - function deselect_dropdown_item (item) { - item.removeClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = null; - } - - // Do a search and show the "searching" dropdown if the input is longer - // than settings.minChars - function do_search() { - var query = input_box.val().toLowerCase(); - - if(query && query.length) { - if(selected_token) { - deselect_token($(selected_token), POSITION.AFTER); - } - - if(query.length >= settings.minChars) { - show_dropdown_searching(); - clearTimeout(timeout); - - timeout = setTimeout(function(){ - run_search(query); - }, settings.searchDelay); - } else { - hide_dropdown(); - } - } - } - - // Do the actual search - function run_search(query) { - var cache_key = query + computeURL(); - var cached_results = cache.get(cache_key); - if(cached_results) { - populate_dropdown(query, cached_results); - } else { - // Are we doing an ajax search or local data search? - if(settings.url) { - var url = computeURL(); - // Extract exisiting get params - var ajax_params = {}; - ajax_params.data = {}; - if(url.indexOf("?") > -1) { - var parts = url.split("?"); - ajax_params.url = parts[0]; - - var param_array = parts[1].split("&"); - $.each(param_array, function (index, value) { - var kv = value.split("="); - ajax_params.data[kv[0]] = kv[1]; - }); - } else { - ajax_params.url = url; - } - - // Prepare the request - ajax_params.data[settings.queryParam] = query; - ajax_params.type = settings.method; - ajax_params.dataType = settings.contentType; - if(settings.crossDomain) { - ajax_params.dataType = "jsonp"; - } - - // Attach the success callback - ajax_params.success = function(results) { - if($.isFunction(settings.onResult)) { - results = settings.onResult.call(hidden_input, results); - } - cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); - - // only populate the dropdown if the results are associated with the active search query - if(input_box.val().toLowerCase() === query) { - populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); - } - }; - - // Make the request - $.ajax(ajax_params); - } else if(settings.local_data) { - // Do the search through local data - var results = $.grep(settings.local_data, function (row) { - return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; - }); - - if($.isFunction(settings.onResult)) { - results = settings.onResult.call(hidden_input, results); - } - cache.add(cache_key, results); - populate_dropdown(query, results); - } - } - } - - // compute the dynamic URL - function computeURL() { - var url = settings.url; - if(typeof settings.url == 'function') { - url = settings.url.call(); - } - return url; - } -}; - -// Really basic cache for the results -$.TokenList.Cache = function (options) { - var settings = $.extend({ - max_size: 500 - }, options); - - var data = {}; - var size = 0; - - var flush = function () { - data = {}; - size = 0; - }; - - this.add = function (query, results) { - if(size > settings.max_size) { - flush(); - } - - if(!data[query]) { - size += 1; - } - - data[query] = results; - }; - - this.get = function (query) { - return data[query]; - }; -}; -}(jQuery)); +/* + * jQuery Plugin: Tokenizing Autocomplete Text Entry + * Version 1.6.0 + * + * Copyright (c) 2009 James Smith (http://loopj.com) + * Licensed jointly under the GPL and MIT licenses, + * choose which one suits your project best! + * + */ + +(function ($) { +// Default settings +var DEFAULT_SETTINGS = { + // Search settings + method: "GET", + contentType: "json", + queryParam: "q", + searchDelay: 300, + minChars: 1, + propertyToSearch: "name", + jsonContainer: null, + + // Display settings + hintText: "Type in a search term", + noResultsText: "No results", + searchingText: "Searching...", + deleteText: "×", + animateDropdown: true, + + // Tokenization settings + tokenLimit: null, + tokenDelimiter: ",", + preventDuplicates: false, + + // Output settings + tokenValue: "id", + + // Prepopulation settings + prePopulate: null, + processPrePopulate: true, + + // Manipulation settings + idPrefix: "token-input-", + + // Formatters + resultsFormatter: function(item){ return "<li><img src='/images/flags/"+item["id"]+".png' /> " + item[this.propertyToSearch]+ "</li>" }, + tokenFormatter: function(item) { return "<li><img src='/images/flags/"+item["id"]+".png' /> <p>" + item[this.propertyToSearch] + "</p></li>" }, + flag: "flag", + + // Callbacks + onResult: null, + onAdd: null, + onDelete: null, + onReady: null +}; + +// Default classes to use when theming +var DEFAULT_CLASSES = { + tokenList: "token-input-list", + token: "token-input-token", + tokenDelete: "token-input-delete-token", + selectedToken: "token-input-selected-token", + highlightedToken: "token-input-highlighted-token", + dropdown: "token-input-dropdown", + dropdownItem: "token-input-dropdown-item", + dropdownItem2: "token-input-dropdown-item2", + selectedDropdownItem: "token-input-selected-dropdown-item", + inputToken: "token-input-input-token" +}; + +// Input box position "enum" +var POSITION = { + BEFORE: 0, + AFTER: 1, + END: 2 +}; + +// Keys "enum" +var KEY = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + NUMPAD_ENTER: 108, + COMMA: 188 +}; + +// Additional public (exposed) methods +var methods = { + init: function(url_or_data_or_function, options) { + var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); + + return this.each(function () { + $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); + }); + }, + clear: function() { + this.data("tokenInputObject").clear(); + return this; + }, + add: function(item) { + this.data("tokenInputObject").add(item); + return this; + }, + remove: function(item) { + this.data("tokenInputObject").remove(item); + return this; + }, + get: function() { + return this.data("tokenInputObject").getTokens(); + } +} + +// Expose the .tokenInput function to jQuery as a plugin +$.fn.tokenInput = function (method) { + // Method calling and initialization logic + if(methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else { + return methods.init.apply(this, arguments); + } +}; + +// TokenList class for each input +$.TokenList = function (input, url_or_data, settings) { + // + // Initialization + // + + // Configure the data source + if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { + // Set the url to query against + settings.url = url_or_data; + + // If the URL is a function, evaluate it here to do our initalization work + var url = computeURL(); + + // Make a smart guess about cross-domain if it wasn't explicitly specified + if(settings.crossDomain === undefined) { + if(url.indexOf("://") === -1) { + settings.crossDomain = false; + } else { + settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); + } + } + } else if(typeof(url_or_data) === "object") { + // Set the local data to search through + settings.local_data = url_or_data; + } + + // Build class names + if(settings.classes) { + // Use custom class names + settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); + } else if(settings.theme) { + // Use theme-suffixed default class names + settings.classes = {}; + $.each(DEFAULT_CLASSES, function(key, value) { + settings.classes[key] = value + "-" + settings.theme; + }); + } else { + settings.classes = DEFAULT_CLASSES; + } + + + // Save the tokens + var saved_tokens = []; + + // Keep track of the number of tokens in the list + var token_count = 0; + + // Basic cache to save on db hits + var cache = new $.TokenList.Cache(); + + // Keep track of the timeout, old vals + var timeout; + var input_val; + + // Create a new text input an attach keyup events + var input_box = $("<input type=\"text\" autocomplete=\"off\" >") + .css({ + outline: "none" + }) + .attr("id", settings.idPrefix + input.id) + .focus(function () { + if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { + show_dropdown_hint(); + } + }) + .blur(function () { + hide_dropdown(); + $(this).val(""); + }) + .bind("keyup keydown blur update", resize_input) + .keydown(function (event) { + var previous_token; + var next_token; + + switch(event.keyCode) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + if(!$(this).val()) { + previous_token = input_token.prev(); + next_token = input_token.next(); + + if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { + // Check if there is a previous/next token and it is selected + if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { + deselect_token($(selected_token), POSITION.BEFORE); + } else { + deselect_token($(selected_token), POSITION.AFTER); + } + } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { + // We are moving left, select the previous token if it exists + select_token($(previous_token.get(0))); + } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { + // We are moving right, select the next token if it exists + select_token($(next_token.get(0))); + } + } else { + var dropdown_item = null; + + if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { + dropdown_item = $(selected_dropdown_item).next(); + } else { + dropdown_item = $(selected_dropdown_item).prev(); + } + + if(dropdown_item.length) { + select_dropdown_item(dropdown_item); + } + return false; + } + break; + + case KEY.BACKSPACE: + previous_token = input_token.prev(); + + if(!$(this).val().length) { + if(selected_token) { + delete_token($(selected_token)); + hidden_input.change(); + } else if(previous_token.length) { + select_token($(previous_token.get(0))); + } + + return false; + } else if($(this).val().length === 1) { + hide_dropdown(); + } else { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + + case KEY.TAB: + case KEY.ENTER: + case KEY.NUMPAD_ENTER: + case KEY.COMMA: + if(selected_dropdown_item) { + add_token($(selected_dropdown_item).data("tokeninput")); + hidden_input.change(); + return false; + } + break; + + case KEY.ESCAPE: + hide_dropdown(); + return true; + + default: + if(String.fromCharCode(event.which)) { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + } + }); + + // Keep a reference to the original input box + var hidden_input = $(input) + .hide() + .val("") + .focus(function () { + input_box.focus(); + }) + .blur(function () { + input_box.blur(); + }); + + // Keep a reference to the selected token and dropdown item + var selected_token = null; + var selected_token_index = 0; + var selected_dropdown_item = null; + + // The list to store the token items in + var token_list = $("<ul />") + .addClass(settings.classes.tokenList) + .click(function (event) { + var li = $(event.target).closest("li"); + if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { + toggle_select_token(li); + } else { + // Deselect selected token + if(selected_token) { + deselect_token($(selected_token), POSITION.END); + } + + // Focus input box + input_box.focus(); + } + }) + .mouseover(function (event) { + var li = $(event.target).closest("li"); + if(li && selected_token !== this) { + li.addClass(settings.classes.highlightedToken); + } + }) + .mouseout(function (event) { + var li = $(event.target).closest("li"); + if(li && selected_token !== this) { + li.removeClass(settings.classes.highlightedToken); + } + }) + .insertBefore(hidden_input); + + // The token holding the input box + var input_token = $("<li />") + .addClass(settings.classes.inputToken) + .appendTo(token_list) + .append(input_box); + + // The list to store the dropdown items in + var dropdown = $("<div>") + .addClass(settings.classes.dropdown) + .appendTo("body") + .hide(); + + // Magic element to help us resize the text input + var input_resizer = $("<tester/>") + .insertAfter(input_box) + .css({ + position: "absolute", + top: -9999, + left: -9999, + width: "auto", + fontSize: input_box.css("fontSize"), + fontFamily: input_box.css("fontFamily"), + fontWeight: input_box.css("fontWeight"), + letterSpacing: input_box.css("letterSpacing"), + whiteSpace: "nowrap" + }); + + // Pre-populate list if items exist + hidden_input.val(""); + var li_data = settings.prePopulate || hidden_input.data("pre"); + if(settings.processPrePopulate && $.isFunction(settings.onResult)) { + li_data = settings.onResult.call(hidden_input, li_data); + } + if(li_data && li_data.length) { + $.each(li_data, function (index, value) { + insert_token(value); + checkTokenLimit(); + }); + } + + // Initialization is done + if($.isFunction(settings.onReady)) { + settings.onReady.call(); + } + + // + // Public functions + // + + this.clear = function() { + token_list.children("li").each(function() { + if ($(this).children("input").length === 0) { + delete_token($(this)); + } + }); + } + + this.add = function(item) { + add_token(item); + } + + this.remove = function(item) { + token_list.children("li").each(function() { + if ($(this).children("input").length === 0) { + var currToken = $(this).data("tokeninput"); + var match = true; + for (var prop in item) { + if (item[prop] !== currToken[prop]) { + match = false; + break; + } + } + if (match) { + delete_token($(this)); + } + } + }); + } + + this.getTokens = function() { + return saved_tokens; + } + + // + // Private functions + // + + function checkTokenLimit() { + if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { + input_box.hide(); + hide_dropdown(); + return; + } + } + + function resize_input() { + if(input_val === (input_val = input_box.val())) {return;} + + // Enter new content into resizer and resize input accordingly + var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(/</g, '<').replace(/>/g, '>'); + input_resizer.html(escaped); + input_box.width(input_resizer.width() + 30); + } + + function is_printable_character(keycode) { + return ((keycode >= 48 && keycode <= 90) || // 0-1a-z + (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . + (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ + (keycode >= 219 && keycode <= 222)); // ( \ ) ' + } + + // Inner function to a token to the list + function insert_token(item) { + var this_token = settings.tokenFormatter(item); + this_token = $(this_token) + .addClass(settings.classes.token) + .insertBefore(input_token); + + // The 'delete token' button + $("<span>" + settings.deleteText + "</span>") + .addClass(settings.classes.tokenDelete) + .appendTo(this_token) + .click(function () { + delete_token($(this).parent()); + hidden_input.change(); + return false; + }); + + // Store data on the token + var token_data = {"id": item.id}; + token_data[settings.propertyToSearch] = item[settings.propertyToSearch]; + $.data(this_token.get(0), "tokeninput", item); + + // Save this token for duplicate checking + saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); + selected_token_index++; + + // Update the hidden input + update_hidden_input(saved_tokens, hidden_input); + + token_count += 1; + + // Check the token limit + if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { + input_box.hide(); + hide_dropdown(); + } + + return this_token; + } + + // Add a token to the token list based on user input + function add_token (item) { + var callback = settings.onAdd; + + // See if the token already exists and select it if we don't want duplicates + if(token_count > 0 && settings.preventDuplicates) { + var found_existing_token = null; + token_list.children().each(function () { + var existing_token = $(this); + var existing_data = $.data(existing_token.get(0), "tokeninput"); + if(existing_data && existing_data.id === item.id) { + found_existing_token = existing_token; + return false; + } + }); + + if(found_existing_token) { + select_token(found_existing_token); + input_token.insertAfter(found_existing_token); + input_box.focus(); + return; + } + } + + // Insert the new tokens + if(settings.tokenLimit == null || token_count < settings.tokenLimit) { + insert_token(item); + checkTokenLimit(); + } + + // Clear input box + input_box.val(""); + + // Don't show the help dropdown, they've got the idea + hide_dropdown(); + + // Execute the onAdd callback if defined + if($.isFunction(callback)) { + callback.call(hidden_input,item); + } + } + + // Select a token in the token list + function select_token (token) { + token.addClass(settings.classes.selectedToken); + selected_token = token.get(0); + + // Hide input box + input_box.val(""); + + // Hide dropdown if it is visible (eg if we clicked to select token) + hide_dropdown(); + } + + // Deselect a token in the token list + function deselect_token (token, position) { + token.removeClass(settings.classes.selectedToken); + selected_token = null; + + if(position === POSITION.BEFORE) { + input_token.insertBefore(token); + selected_token_index--; + } else if(position === POSITION.AFTER) { + input_token.insertAfter(token); + selected_token_index++; + } else { + input_token.appendTo(token_list); + selected_token_index = token_count; + } + + // Show the input box and give it focus again + input_box.focus(); + } + + // Toggle selection of a token in the token list + function toggle_select_token(token) { + var previous_selected_token = selected_token; + + if(selected_token) { + deselect_token($(selected_token), POSITION.END); + } + + if(previous_selected_token === token.get(0)) { + deselect_token(token, POSITION.END); + } else { + select_token(token); + } + } + + // Delete a token from the token list + function delete_token (token) { + // Remove the id from the saved list + var token_data = $.data(token.get(0), "tokeninput"); + var callback = settings.onDelete; + + var index = token.prevAll().length; + if(index > selected_token_index) index--; + + // Delete the token + token.remove(); + selected_token = null; + + // Show the input box and give it focus again + input_box.focus(); + + // Remove this token from the saved list + saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); + if(index < selected_token_index) selected_token_index--; + + // Update the hidden input + update_hidden_input(saved_tokens, hidden_input); + + token_count -= 1; + + if(settings.tokenLimit !== null) { + input_box + .show() + .val("") + .focus(); + } + + // Execute the onDelete callback if defined + if($.isFunction(callback)) { + callback.call(hidden_input,token_data); + } + } + + // Update the hidden input box value + function update_hidden_input(saved_tokens, hidden_input) { + var token_values = $.map(saved_tokens, function (el) { + return el[settings.tokenValue]; + }); + hidden_input.val(token_values.join(settings.tokenDelimiter)); + + } + + // Hide and clear the results dropdown + function hide_dropdown () { + dropdown.hide().empty(); + selected_dropdown_item = null; + } + + function show_dropdown() { + dropdown + .css({ + position: "absolute", + top: $(token_list).offset().top + $(token_list).outerHeight(), + left: $(token_list).offset().left, + zindex: 999 + }) + .show(); + } + + function show_dropdown_searching () { + if(settings.searchingText) { + dropdown.html("<p>"+settings.searchingText+"</p>"); + show_dropdown(); + } + } + + function show_dropdown_hint () { + if(settings.hintText) { + dropdown.html("<p>"+settings.hintText+"</p>"); + show_dropdown(); + } + } + + // Highlight the query part of the search term + function highlight_term(value, term) { + return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>"); + } + + function find_value_and_highlight_term(template, value, term) { + return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); + } + + // Populate the results dropdown with some results + function populate_dropdown (query, results) { + if(results && results.length) { + dropdown.empty(); + var dropdown_ul = $("<ul>") + .appendTo(dropdown) + .mouseover(function (event) { + select_dropdown_item($(event.target).closest("li")); + }) + .mousedown(function (event) { + add_token($(event.target).closest("li").data("tokeninput")); + hidden_input.change(); + return false; + }) + .hide(); + + $.each(results, function(index, value) { + var this_li = settings.resultsFormatter(value); + + this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query); + + this_li = $(this_li).appendTo(dropdown_ul); + + if(index % 2) { + this_li.addClass(settings.classes.dropdownItem); + } else { + this_li.addClass(settings.classes.dropdownItem2); + } + + if(index === 0) { + select_dropdown_item(this_li); + } + + $.data(this_li.get(0), "tokeninput", value); + }); + + show_dropdown(); + + if(settings.animateDropdown) { + dropdown_ul.slideDown("fast"); + } else { + dropdown_ul.show(); + } + } else { + if(settings.noResultsText) { + dropdown.html("<p>"+settings.noResultsText+"</p>"); + show_dropdown(); + } + } + } + + // Highlight an item in the results dropdown + function select_dropdown_item (item) { + if(item) { + if(selected_dropdown_item) { + deselect_dropdown_item($(selected_dropdown_item)); + } + + item.addClass(settings.classes.selectedDropdownItem); + selected_dropdown_item = item.get(0); + } + } + + // Remove highlighting from an item in the results dropdown + function deselect_dropdown_item (item) { + item.removeClass(settings.classes.selectedDropdownItem); + selected_dropdown_item = null; + } + + // Do a search and show the "searching" dropdown if the input is longer + // than settings.minChars + function do_search() { + var query = input_box.val().toLowerCase(); + + if(query && query.length) { + if(selected_token) { + deselect_token($(selected_token), POSITION.AFTER); + } + + if(query.length >= settings.minChars) { + show_dropdown_searching(); + clearTimeout(timeout); + + timeout = setTimeout(function(){ + run_search(query); + }, settings.searchDelay); + } else { + hide_dropdown(); + } + } + } + + // Do the actual search + function run_search(query) { + var cache_key = query + computeURL(); + var cached_results = cache.get(cache_key); + if(cached_results) { + populate_dropdown(query, cached_results); + } else { + // Are we doing an ajax search or local data search? + if(settings.url) { + var url = computeURL(); + // Extract exisiting get params + var ajax_params = {}; + ajax_params.data = {}; + if(url.indexOf("?") > -1) { + var parts = url.split("?"); + ajax_params.url = parts[0]; + + var param_array = parts[1].split("&"); + $.each(param_array, function (index, value) { + var kv = value.split("="); + ajax_params.data[kv[0]] = kv[1]; + }); + } else { + ajax_params.url = url; + } + + // Prepare the request + ajax_params.data[settings.queryParam] = query; + ajax_params.type = settings.method; + ajax_params.dataType = settings.contentType; + if(settings.crossDomain) { + ajax_params.dataType = "jsonp"; + } + + // Attach the success callback + ajax_params.success = function(results) { + if($.isFunction(settings.onResult)) { + results = settings.onResult.call(hidden_input, results); + } + cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); + + // only populate the dropdown if the results are associated with the active search query + if(input_box.val().toLowerCase() === query) { + populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); + } + }; + + // Make the request + $.ajax(ajax_params); + } else if(settings.local_data) { + // Do the search through local data + var results = $.grep(settings.local_data, function (row) { + return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; + }); + + if($.isFunction(settings.onResult)) { + results = settings.onResult.call(hidden_input, results); + } + cache.add(cache_key, results); + populate_dropdown(query, results); + } + } + } + + // compute the dynamic URL + function computeURL() { + var url = settings.url; + if(typeof settings.url == 'function') { + url = settings.url.call(); + } + return url; + } +}; + +// Really basic cache for the results +$.TokenList.Cache = function (options) { + var settings = $.extend({ + max_size: 500 + }, options); + + var data = {}; + var size = 0; + + var flush = function () { + data = {}; + size = 0; + }; + + this.add = function (query, results) { + if(size > settings.max_size) { + flush(); + } + + if(!data[query]) { + size += 1; + } + + data[query] = results; + }; + + this.get = function (query) { + return data[query]; + }; +}; +}(jQuery)); diff --git a/data/js/lib/superfish-1.4.8.js b/data/js/lib/superfish-1.4.8.js new file mode 100644 index 0000000000000000000000000000000000000000..419fa51b49b80b1bd02066bbd35c89f371cf44b5 --- /dev/null +++ b/data/js/lib/superfish-1.4.8.js @@ -0,0 +1,120 @@ + +/* + * Superfish v1.4.8 - jQuery menu widget + * Copyright (c) 2008 Joel Birch + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * CHANGELOG: http://users.tpg.com.au/j_birch/plugins/superfish/changelog.txt + */ + +;(function($){ + $.fn.superfish = function(op){ + + var sf = $.fn.superfish, + c = sf.c, + $arrow = $(['<span class="',c.arrowClass,'"> »</span>'].join('')), + over = function(){ + var $$ = $(this), menu = getMenu($$); + clearTimeout(menu.sfTimer); + $$.showSuperfishUl().siblings().hideSuperfishUl(); + }, + out = function(){ + var $$ = $(this), menu = getMenu($$), o = sf.op; + clearTimeout(menu.sfTimer); + menu.sfTimer=setTimeout(function(){ + o.retainPath=($.inArray($$[0],o.$path)>-1); + $$.hideSuperfishUl(); + if (o.$path.length && $$.parents(['li.',o.hoverClass].join('')).length<1){over.call(o.$path);} + },o.delay); + }, + getMenu = function($menu){ + var menu = $menu.parents(['ul.',c.menuClass,':first'].join(''))[0]; + sf.op = sf.o[menu.serial]; + return menu; + }, + addArrow = function($a){ $a.addClass(c.anchorClass).append($arrow.clone()); }; + + return this.each(function() { + var s = this.serial = sf.o.length; + var o = $.extend({},sf.defaults,op); + o.$path = $('li.'+o.pathClass,this).slice(0,o.pathLevels).each(function(){ + $(this).addClass([o.hoverClass,c.bcClass].join(' ')) + .filter('li:has(ul)').removeClass(o.pathClass); + }); + sf.o[s] = sf.op = o; + + $('li:has(ul)',this)[($.fn.hoverIntent && !o.disableHI) ? 'hoverIntent' : 'hover'](over,out).each(function() { + if (o.autoArrows) addArrow( $('>a:first-child',this) ); + }) + .not('.'+c.bcClass) + .hideSuperfishUl(); + + var $a = $('a',this); + $a.each(function(i){ + var $li = $a.eq(i).parents('li'); + $a.eq(i).focus(function(){over.call($li);}).blur(function(){out.call($li);}); + }); + o.onInit.call(this); + + }).each(function() { + var menuClasses = [c.menuClass]; + if (sf.op.dropShadows && !($.browser.msie && $.browser.version < 7)) menuClasses.push(c.shadowClass); + $(this).addClass(menuClasses.join(' ')); + }); + }; + + var sf = $.fn.superfish; + sf.o = []; + sf.op = {}; + sf.IE7fix = function(){ + var o = sf.op; + if ($.browser.msie && $.browser.version > 6 && o.dropShadows && o.animation.opacity!=undefined) + this.toggleClass(sf.c.shadowClass+'-off'); + }; + sf.c = { + bcClass : 'sf-breadcrumb', + menuClass : 'sf-js-enabled', + anchorClass : 'sf-with-ul', + arrowClass : 'sf-sub-indicator', + shadowClass : 'sf-shadow' + }; + sf.defaults = { + hoverClass : 'sfHover', + pathClass : 'overideThisToUse', + pathLevels : 1, + delay : 800, + animation : {opacity:'show'}, + speed : 'normal', + autoArrows : true, + dropShadows : true, + disableHI : false, // true disables hoverIntent detection + onInit : function(){}, // callback functions + onBeforeShow: function(){}, + onShow : function(){}, + onHide : function(){} + }; + $.fn.extend({ + hideSuperfishUl : function(){ + var o = sf.op, + not = (o.retainPath===true) ? o.$path : ''; + o.retainPath = false; + var $ul = $(['li.',o.hoverClass].join(''),this).add(this).not(not).removeClass(o.hoverClass) + .find('>ul').hide().css('visibility','hidden'); + o.onHide.call($ul); + return this; + }, + showSuperfishUl : function(){ + var o = sf.op, + sh = sf.c.shadowClass+'-off', + $ul = this.addClass(o.hoverClass) + .find('>ul:hidden').css('visibility','visible'); + sf.IE7fix.call($ul); + o.onBeforeShow.call($ul); + $ul.animate(o.animation,o.speed,function(){ sf.IE7fix.call($ul); o.onShow.call($ul); }); + return this; + } + }); +})(jQuery); diff --git a/data/js/lib/supersubs-0.2b.js b/data/js/lib/supersubs-0.2b.js new file mode 100644 index 0000000000000000000000000000000000000000..45221510fc96220fa36031d7bc95e5f4937ece71 --- /dev/null +++ b/data/js/lib/supersubs-0.2b.js @@ -0,0 +1,90 @@ + +/* + * Supersubs v0.2b - jQuery plugin + * Copyright (c) 2008 Joel Birch + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * + * This plugin automatically adjusts submenu widths of suckerfish-style menus to that of + * their longest list item children. If you use this, please expect bugs and report them + * to the jQuery Google Group with the word 'Superfish' in the subject line. + * + */ + +;(function($){ // $ will refer to jQuery within this closure + + $.fn.supersubs = function(options){ + var opts = $.extend({}, $.fn.supersubs.defaults, options); + // return original object to support chaining + return this.each(function() { + // cache selections + var $$ = $(this); + // support metadata + var o = $.meta ? $.extend({}, opts, $$.data()) : opts; + // get the font size of menu. + // .css('fontSize') returns various results cross-browser, so measure an em dash instead + var fontsize = $('<li id="menu-fontsize">—</li>').css({ + 'padding' : 0, + 'position' : 'absolute', + 'top' : '-999em', + 'width' : 'auto' + }).appendTo($$).width(); //clientWidth is faster, but was incorrect here + // remove em dash + $('#menu-fontsize').remove(); + // cache all ul elements + $ULs = $$.find('ul'); + // loop through each ul in menu + $ULs.each(function(i) { + // cache this ul + var $ul = $ULs.eq(i); + // get all (li) children of this ul + var $LIs = $ul.children(); + // get all anchor grand-children + var $As = $LIs.children('a'); + // force content to one line and save current float property + var liFloat = $LIs.css('white-space','nowrap').css('float'); + // remove width restrictions and floats so elements remain vertically stacked + var emWidth = $ul.add($LIs).add($As).css({ + 'float' : 'none', + 'width' : 'auto' + }) + // this ul will now be shrink-wrapped to longest li due to position:absolute + // so save its width as ems. Clientwidth is 2 times faster than .width() - thanks Dan Switzer + .end().end()[0].clientWidth / fontsize; + // add more width to ensure lines don't turn over at certain sizes in various browsers + emWidth += o.extraWidth; + // restrict to at least minWidth and at most maxWidth + if (emWidth > o.maxWidth) { emWidth = o.maxWidth; } + else if (emWidth < o.minWidth) { emWidth = o.minWidth; } + emWidth += 'em'; + // set ul to width in ems + $ul.css('width',emWidth); + // restore li floats to avoid IE bugs + // set li width to full width of this ul + // revert white-space to normal + $LIs.css({ + 'float' : liFloat, + 'width' : '100%', + 'white-space' : 'normal' + }) + // update offset position of descendant ul to reflect new width of parent + .each(function(){ + var $childUl = $('>ul',this); + var offsetDirection = $childUl.css('left')!==undefined ? 'left' : 'right'; + $childUl.css(offsetDirection,emWidth); + }); + }); + + }); + }; + // expose defaults + $.fn.supersubs.defaults = { + minWidth : 9, // requires em unit. + maxWidth : 25, // requires em unit. + extraWidth : 0 // extra width can ensure lines don't sometimes turn over due to slight browser differences in how they round-off values + }; + +})(jQuery); // plugin code ends diff --git a/data/js/manageEpisodeStatuses.js b/data/js/manageEpisodeStatuses.js index fbe0d9d39aa683b612c3aea7cf9fbddda07507d2..237a0906f7ccbe870223530bfc911c9d2dbb6eaa 100644 --- a/data/js/manageEpisodeStatuses.js +++ b/data/js/manageEpisodeStatuses.js @@ -1,47 +1,67 @@ -$(document).ready(function() { - - function make_row(tvdb_id, season, episode, name, checked) { - if (checked) - var checked = ' checked'; - else - var checked = ''; - - var row_class = $('#row_class').val(); - - var row = ''; - row += ' <tr class="'+row_class+'">'; - row += ' <td><input type="checkbox" class="'+tvdb_id+'-epcheck" name="'+tvdb_id+'-'+season+'x'+episode+'"'+checked+'></td>'; - row += ' <td>'+season+'x'+episode+'</td>'; - row += ' <td style="width: 100%">'+name+'</td>'; - row += ' </tr>' - - return row; - } - - $('.allCheck').click(function(){ - var tvdb_id = $(this).attr('id').split('-')[1]; - $('.'+tvdb_id+'-epcheck').prop('checked', $(this).prop('checked')); - }); - - $('.get_more_eps').click(function(){ - var cur_tvdb_id = $(this).attr('id'); - var checked = $('#allCheck-'+cur_tvdb_id).prop('checked'); - var last_row = $('tr#'+cur_tvdb_id); - - $.getJSON(sbRoot+'/manage/showEpisodeStatuses', - { - tvdb_id: cur_tvdb_id, - whichStatus: $('#oldStatus').val() - }, - function (data) { - $.each(data, function(season,eps){ - $.each(eps, function(episode, name) { - //alert(season+'x'+episode+': '+name); - last_row.after(make_row(cur_tvdb_id, season, episode, name, checked)); - }); - }); - }); - $(this).hide(); - }); - +$(document).ready(function() { + + function make_row(tvdb_id, season, episode, name, checked) { + if (checked) + var checked = ' checked'; + else + var checked = ''; + + var row_class = $('#row_class').val(); + + var row = ''; + row += ' <tr class="'+row_class+'">'; + row += ' <td><input type="checkbox" class="'+tvdb_id+'-epcheck" name="'+tvdb_id+'-'+season+'x'+episode+'"'+checked+'></td>'; + row += ' <td>'+season+'x'+episode+'</td>'; + row += ' <td style="width: 100%">'+name+'</td>'; + row += ' </tr>' + + return row; + } + + $('.allCheck').click(function(){ + var tvdb_id = $(this).attr('id').split('-')[1]; + $('.'+tvdb_id+'-epcheck').prop('checked', $(this).prop('checked')); + }); + + $('.get_more_eps').click(function(){ + var cur_tvdb_id = $(this).attr('id'); + var checked = $('#allCheck-'+cur_tvdb_id).prop('checked'); + var last_row = $('tr#'+cur_tvdb_id); + + $.getJSON(sbRoot+'/manage/showEpisodeStatuses', + { + tvdb_id: cur_tvdb_id, + whichStatus: $('#oldStatus').val() + }, + function (data) { + $.each(data, function(season,eps){ + $.each(eps, function(episode, name) { + //alert(season+'x'+episode+': '+name); + last_row.after(make_row(cur_tvdb_id, season, episode, name, checked)); + }); + }); + }); + $(this).hide(); + }); + + // selects all visible episode checkboxes. + $('.selectAllShows').click(function(){ + $('.allCheck').each(function(){ + this.checked = true; + }); + $('input[class*="-epcheck"]').each(function(){ + this.checked = true; + }); + }); + + // clears all visible episode checkboxes and the season selectors + $('.unselectAllShows').click(function(){ + $('.allCheck').each(function(){ + this.checked = false; + }); + $('input[class*="-epcheck"]').each(function(){ + this.checked = false; + }); + }); + }); \ No newline at end of file diff --git a/data/js/massEdit.js b/data/js/massEdit.js index d03d07762b32305bfb650c93085972d022a65fd3..1ddc541b3e395b876370dedb242c14f7cffeaa32 100644 --- a/data/js/massEdit.js +++ b/data/js/massEdit.js @@ -1,24 +1,25 @@ -$(document).ready(function () { - - function find_dir_index(which) { - var dir_parts = which.split('_'); - return dir_parts[dir_parts.length - 1]; - } - - function edit_root_dir(path, options) { - $('#new_root_dir_' + options.which_id).val(path); - $('#new_root_dir_' + options.which_id).change(); - } - - $('.new_root_dir').change(function () { - var cur_index = find_dir_index($(this).attr('id')); - $('#display_new_root_dir_' + cur_index).html('<b>' + $(this).val() + '</b>'); - }); - - $('.edit_root_dir').click(function () { - var cur_id = find_dir_index($(this).attr('id')); - var initial_dir = $("#new_root_dir_" + cur_id).val(); - $(this).nFileBrowser(edit_root_dir, {initialDir: initial_dir, which_id: cur_id, title: 'Select Show Location', autocomplete: false}); - }); - -}); +$(document).ready(function(){ + + function find_dir_index(which){ + var dir_parts = which.split('_'); + return dir_parts[dir_parts.length-1]; + } + + function edit_root_dir(path, options){ + $('#new_root_dir_'+options.which_id).val(path); + $('#new_root_dir_'+options.which_id).change(); + } + + $('.new_root_dir').change(function(){ + var cur_index = find_dir_index($(this).attr('id')); + $('#display_new_root_dir_'+cur_index).html('<b>'+$(this).val()+'</b>'); + }); + + $('.edit_root_dir').click(function(){ + var cur_id = find_dir_index($(this).attr('id')); + var initial_dir = $("#new_root_dir_"+cur_id).val(); + $(this).nFileBrowser(edit_root_dir, {initialDir: initial_dir, which_id: cur_id}); + + }); + +}); \ No newline at end of file diff --git a/data/js/massUpdate.js b/data/js/massUpdate.js index 2737ec93678e75807b3b1d1906690b292252d9c0..42469672eb0e1a50ac065eec2626f27849dd8ddf 100644 --- a/data/js/massUpdate.js +++ b/data/js/massUpdate.js @@ -1,116 +1,116 @@ -$(document).ready(function(){ - - $('#submitMassEdit').click(function(){ - var editArr = new Array() - - $('.editCheck').each(function() { - if (this.checked == true) { - editArr.push($(this).attr('id').split('-')[1]) - } - }); - - if (editArr.length == 0) - return - - url = 'massEdit?toEdit='+editArr.join('|') - window.location.href = url - }); - - - $('#submitMassUpdate').click(function(){ - - var updateArr = new Array() - var refreshArr = new Array() - var renameArr = new Array() - var subtitleArr = new Array() - var deleteArr = new Array() - var metadataArr = new Array() - - $('.updateCheck').each(function() { - if (this.checked == true) { - updateArr.push($(this).attr('id').split('-')[1]) - } - }); - - $('.refreshCheck').each(function() { - if (this.checked == true) { - refreshArr.push($(this).attr('id').split('-')[1]) - } - }); - - $('.renameCheck').each(function() { - if (this.checked == true) { - renameArr.push($(this).attr('id').split('-')[1]) - } - }); - - $('.subtitleCheck').each(function() { - if (this.checked == true) { - subtitleArr.push($(this).attr('id').split('-')[1]) - } - }); - - $('.deleteCheck').each(function() { - if (this.checked == true) { - deleteArr.push($(this).attr('id').split('-')[1]) - } - }); - -/* - $('.metadataCheck').each(function() { - if (this.checked == true) { - metadataArr.push($(this).attr('id').split('-')[1]) - } - }); -*/ - if (updateArr.length+refreshArr.length+renameArr.length+subtitleArr.length+deleteArr.length+metadataArr.length == 0) - return false - - url = 'massUpdate?toUpdate='+updateArr.join('|')+'&toRefresh='+refreshArr.join('|')+'&toRename='+renameArr.join('|')+'&toSubtitle='+subtitleArr.join('|')+'&toDelete='+deleteArr.join('|')+'&toMetadata='+metadataArr.join('|') - - window.location.href = url - - }); - - $('.bulkCheck').click(function(){ - - var bulkCheck = this; - var whichBulkCheck = $(bulkCheck).attr('id'); - - $('.'+whichBulkCheck).each(function(){ - if (!this.disabled) - this.checked = !this.checked - }); - }); - - ['.editCheck', '.updateCheck', '.refreshCheck', '.renameCheck', '.deleteCheck'].forEach(function(name) { - var lastCheck = null; - - $(name).click(function(event) { - - if(!lastCheck || !event.shiftKey) { - lastCheck = this; - return; - } - - var check = this; - var found = 0; - - $(name).each(function() { - switch (found) { - case 2: return false; - case 1: - if (!this.disabled) - this.checked = lastCheck.checked; - } - - if (this == check || this == lastCheck) - found++; - }); - - lastClick = this; - }); - - }); - -}); +$(document).ready(function(){ + + $('#submitMassEdit').click(function(){ + var editArr = new Array() + + $('.editCheck').each(function() { + if (this.checked == true) { + editArr.push($(this).attr('id').split('-')[1]) + } + }); + + if (editArr.length == 0) + return + + url = 'massEdit?toEdit='+editArr.join('|') + window.location.href = url + }); + + + $('#submitMassUpdate').click(function(){ + + var updateArr = new Array() + var refreshArr = new Array() + var renameArr = new Array() + var subtitleArr = new Array() + var deleteArr = new Array() + var metadataArr = new Array() + + $('.updateCheck').each(function() { + if (this.checked == true) { + updateArr.push($(this).attr('id').split('-')[1]) + } + }); + + $('.refreshCheck').each(function() { + if (this.checked == true) { + refreshArr.push($(this).attr('id').split('-')[1]) + } + }); + + $('.renameCheck').each(function() { + if (this.checked == true) { + renameArr.push($(this).attr('id').split('-')[1]) + } + }); + + $('.subtitleCheck').each(function() { + if (this.checked == true) { + subtitleArr.push($(this).attr('id').split('-')[1]) + } + }); + + $('.deleteCheck').each(function() { + if (this.checked == true) { + deleteArr.push($(this).attr('id').split('-')[1]) + } + }); + +/* + $('.metadataCheck').each(function() { + if (this.checked == true) { + metadataArr.push($(this).attr('id').split('-')[1]) + } + }); +*/ + if (updateArr.length+refreshArr.length+renameArr.length+subtitleArr.length+deleteArr.length+metadataArr.length == 0) + return false + + url = 'massUpdate?toUpdate='+updateArr.join('|')+'&toRefresh='+refreshArr.join('|')+'&toRename='+renameArr.join('|')+'&toSubtitle='+subtitleArr.join('|')+'&toDelete='+deleteArr.join('|')+'&toMetadata='+metadataArr.join('|') + + window.location.href = url + + }); + + $('.bulkCheck').click(function(){ + + var bulkCheck = this; + var whichBulkCheck = $(bulkCheck).attr('id'); + + $('.'+whichBulkCheck).each(function(){ + if (!this.disabled) + this.checked = !this.checked + }); + }); + + ['.editCheck', '.updateCheck', '.refreshCheck', '.renameCheck', '.deleteCheck'].forEach(function(name) { + var lastCheck = null; + + $(name).click(function(event) { + + if(!lastCheck || !event.shiftKey) { + lastCheck = this; + return; + } + + var check = this; + var found = 0; + + $(name).each(function() { + switch (found) { + case 2: return false; + case 1: + if (!this.disabled) + this.checked = lastCheck.checked; + } + + if (this == check || this == lastCheck) + found++; + }); + + lastClick = this; + }); + + }); + +}); diff --git a/data/js/plotTooltip.js b/data/js/plotTooltip.js index 801feeb998b35529fe922a4de04873678f4eac2e..545fdeb275eb4f3ed3116a0af4e3b16fb25b2ca7 100644 --- a/data/js/plotTooltip.js +++ b/data/js/plotTooltip.js @@ -1,41 +1,40 @@ -$(function () { - $('.plotInfo').each(function () { - match = $(this).attr("id").match(/^plot_info_(\d+)_(\d+)_(\d+)$/); - $(this).qtip({ - content: { - text: 'Loading...', - ajax: { - url: $("#sbRoot").val() + '/home/plotDetails', - type: 'GET', - data: { - show: match[1], - episode: match[3], - season: match[2] - }, - success: function (data, status) { - this.set('content.text', data); - } - } - }, - show: { - solo: true - }, - position: { - viewport: $(window), - my: 'left center', - effect: false, - adjust: { - y: -8, - x: 0 - } - }, - style: { - tip: { - corner: true, - method: 'polygon' - }, - classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-sb' - } - }); - }); -}); +$(function () { + $('.plotInfo').each(function () { + match = $(this).attr("id").match(/^plot_info_(\d+)_(\d+)_(\d+)$/); + $(this).qtip({ + content: { + text: 'Loading...', + ajax: { + url: $("#sbRoot").val() + '/home/plotDetails', + type: 'GET', + data: { + show: match[1], + episode: match[3], + season: match[2] + }, + success: function (data, status) { + this.set('content.text', data); + } + } + }, + show: { + solo: true + }, + position: { + viewport: $(window), + my: 'left center', + adjust: { + y: -10, + x: 2 + } + }, + style: { + tip: { + corner: true, + method: 'polygon' + }, + classes: 'ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-sb' + } + }); + }); +}); diff --git a/data/js/script.js b/data/js/script.js index e9576f9d2f1400e5be79978ed522f56efd90367e..b65614e30eae7cbc933c17d5cf96f2da8c53092a 100644 --- a/data/js/script.js +++ b/data/js/script.js @@ -1,8 +1,6 @@ function initHeader() { //settings var header = $("#header"); - var fadeSpeed = 100, fadeTo = 0.8, topDistance = 20; - var topbarME = function() { $(header).fadeTo(fadeSpeed,1); }, topbarML = function() { $(header).fadeTo(fadeSpeed,fadeTo); }; var inside = false; //do $(window).scroll(function() { diff --git a/data/js/tableClick.js b/data/js/tableClick.js index b16aea4794bdb52e0c3d5f33b2042630b35b89c9..439feb9384c13dfcc19d5b64720304cb337dfd0d 100644 --- a/data/js/tableClick.js +++ b/data/js/tableClick.js @@ -1,12 +1,12 @@ -$(document).ready(function () { - - $("table td.tvShow").live('click', function (e) { - if ((!$.browser.msie && e.button == 0) || ($.browser.msie && e.button == 1)) { - if (!e.shiftKey) { - var href = $(this).find("a").attr("href"); - if (href) { window.location = href; } - } - } - }); - -}); +$(document).ready(function(){ + + $("table.sickbeardTable td.tvShow").live('click', function(e) { + if( (!$.browser.msie && e.button == 0) || ($.browser.msie && e.button == 1) ) { + if(!e.shiftKey) { + var href = $(this).find("a").attr("href"); + if(href) { window.location = href; } + } + } + }); + +}); diff --git a/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz b/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz deleted file mode 100644 index 8bd4f96402be50779e4b2749688d077347a6eef0..0000000000000000000000000000000000000000 Binary files a/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz and /dev/null differ diff --git a/lib/dateutil/zoneinfo/zoneinfo-2013c.tar.gz b/lib/dateutil/zoneinfo/zoneinfo-2013c.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..9bed6f6987054cd62e474398a73c803fd295050a Binary files /dev/null and b/lib/dateutil/zoneinfo/zoneinfo-2013c.tar.gz differ diff --git a/lib/imdb/Character.py b/lib/imdb/Character.py new file mode 100644 index 0000000000000000000000000000000000000000..5a5239af79f7066a34c05e3f4ff3ce8805bab61d --- /dev/null +++ b/lib/imdb/Character.py @@ -0,0 +1,201 @@ +""" +Character module (imdb package). + +This module provides the Character class, used to store information about +a given character. + +Copyright 2007-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from copy import deepcopy + +from imdb.utils import analyze_name, build_name, flatten, _Container, cmpPeople + + +class Character(_Container): + """A Character. + + Every information about a character can be accessed as: + characterObject['information'] + to get a list of the kind of information stored in a + Character object, use the keys() method; some useful aliases + are defined (as "also known as" for the "akas" key); + see the keys_alias dictionary. + """ + # The default sets of information retrieved. + default_info = ('main', 'filmography', 'biography') + + # Aliases for some not-so-intuitive keys. + keys_alias = {'mini biography': 'biography', + 'bio': 'biography', + 'character biography': 'biography', + 'character biographies': 'biography', + 'biographies': 'biography', + 'character bio': 'biography', + 'aka': 'akas', + 'also known as': 'akas', + 'alternate names': 'akas', + 'personal quotes': 'quotes', + 'keys': 'keywords', + 'keyword': 'keywords'} + + keys_tomodify_list = ('biography', 'quotes') + + cmpFunct = cmpPeople + + def _init(self, **kwds): + """Initialize a Character object. + + *characterID* -- the unique identifier for the character. + *name* -- the name of the Character, if not in the data dictionary. + *myName* -- the nickname you use for this character. + *myID* -- your personal id for this character. + *data* -- a dictionary used to initialize the object. + *notes* -- notes about the given character. + *accessSystem* -- a string representing the data access system used. + *titlesRefs* -- a dictionary with references to movies. + *namesRefs* -- a dictionary with references to persons. + *charactersRefs* -- a dictionary with references to characters. + *modFunct* -- function called returning text fields. + """ + name = kwds.get('name') + if name and not self.data.has_key('name'): + self.set_name(name) + self.characterID = kwds.get('characterID', None) + self.myName = kwds.get('myName', u'') + + def _reset(self): + """Reset the Character object.""" + self.characterID = None + self.myName = u'' + + def set_name(self, name): + """Set the name of the character.""" + # XXX: convert name to unicode, if it's a plain string? + try: + d = analyze_name(name, canonical=0) + self.data.update(d) + except: + # TODO: catch only IMDbPYParserError and issue a warning. + pass + + def _additional_keys(self): + """Valid keys to append to the data.keys() list.""" + addkeys = [] + if self.data.has_key('name'): + addkeys += ['long imdb name'] + if self.data.has_key('headshot'): + addkeys += ['full-size headshot'] + return addkeys + + def _getitem(self, key): + """Handle special keys.""" + ## XXX: can a character have an imdbIndex? + if self.data.has_key('name'): + if key == 'long imdb name': + return build_name(self.data) + if key == 'full-size headshot' and self.data.has_key('headshot'): + return self._re_fullsizeURL.sub('', self.data.get('headshot', '')) + return None + + def getID(self): + """Return the characterID.""" + return self.characterID + + def __nonzero__(self): + """The Character is "false" if the self.data does not contain a name.""" + # XXX: check the name and the characterID? + if self.data.get('name'): return 1 + return 0 + + def __contains__(self, item): + """Return true if this Character was portrayed in the given Movie + or it was impersonated by the given Person.""" + from Movie import Movie + from Person import Person + if isinstance(item, Person): + for m in flatten(self.data, yieldDictKeys=1, scalar=Movie): + if item.isSame(m.currentRole): + return 1 + elif isinstance(item, Movie): + for m in flatten(self.data, yieldDictKeys=1, scalar=Movie): + if item.isSame(m): + return 1 + return 0 + + def isSameName(self, other): + """Return true if two character have the same name + and/or characterID.""" + if not isinstance(other, self.__class__): + return 0 + if self.data.has_key('name') and \ + other.data.has_key('name') and \ + build_name(self.data, canonical=0) == \ + build_name(other.data, canonical=0): + return 1 + if self.accessSystem == other.accessSystem and \ + self.characterID is not None and \ + self.characterID == other.characterID: + return 1 + return 0 + isSameCharacter = isSameName + + def __deepcopy__(self, memo): + """Return a deep copy of a Character instance.""" + c = Character(name=u'', characterID=self.characterID, + myName=self.myName, myID=self.myID, + data=deepcopy(self.data, memo), + notes=self.notes, accessSystem=self.accessSystem, + titlesRefs=deepcopy(self.titlesRefs, memo), + namesRefs=deepcopy(self.namesRefs, memo), + charactersRefs=deepcopy(self.charactersRefs, memo)) + c.current_info = list(self.current_info) + c.set_mod_funct(self.modFunct) + return c + + def __repr__(self): + """String representation of a Character object.""" + r = '<Character id:%s[%s] name:_%s_>' % (self.characterID, + self.accessSystem, + self.get('name')) + if isinstance(r, unicode): r = r.encode('utf_8', 'replace') + return r + + def __str__(self): + """Simply print the short name.""" + return self.get('name', u'').encode('utf_8', 'replace') + + def __unicode__(self): + """Simply print the short title.""" + return self.get('name', u'') + + def summary(self): + """Return a string with a pretty-printed summary for the character.""" + if not self: return u'' + s = u'Character\n=====\nName: %s\n' % \ + self.get('name', u'') + bio = self.get('biography') + if bio: + s += u'Biography: %s\n' % bio[0] + filmo = self.get('filmography') + if filmo: + a_list = [x.get('long imdb canonical title', u'') + for x in filmo[:5]] + s += u'Last movies with this character: %s.\n' % u'; '.join(a_list) + return s + + diff --git a/lib/imdb/Company.py b/lib/imdb/Company.py new file mode 100644 index 0000000000000000000000000000000000000000..5e05c840d3787d0cb8cbefc3cd40443f346c54d7 --- /dev/null +++ b/lib/imdb/Company.py @@ -0,0 +1,195 @@ +""" +company module (imdb package). + +This module provides the company class, used to store information about +a given company. + +Copyright 2008-2009 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from copy import deepcopy + +from imdb.utils import analyze_company_name, build_company_name, \ + flatten, _Container, cmpCompanies + + +class Company(_Container): + """A company. + + Every information about a company can be accessed as: + companyObject['information'] + to get a list of the kind of information stored in a + company object, use the keys() method; some useful aliases + are defined (as "also known as" for the "akas" key); + see the keys_alias dictionary. + """ + # The default sets of information retrieved. + default_info = ('main',) + + # Aliases for some not-so-intuitive keys. + keys_alias = { + 'distributor': 'distributors', + 'special effects company': 'special effects companies', + 'other company': 'miscellaneous companies', + 'miscellaneous company': 'miscellaneous companies', + 'other companies': 'miscellaneous companies', + 'misc companies': 'miscellaneous companies', + 'misc company': 'miscellaneous companies', + 'production company': 'production companies'} + + keys_tomodify_list = () + + cmpFunct = cmpCompanies + + def _init(self, **kwds): + """Initialize a company object. + + *companyID* -- the unique identifier for the company. + *name* -- the name of the company, if not in the data dictionary. + *myName* -- the nickname you use for this company. + *myID* -- your personal id for this company. + *data* -- a dictionary used to initialize the object. + *notes* -- notes about the given company. + *accessSystem* -- a string representing the data access system used. + *titlesRefs* -- a dictionary with references to movies. + *namesRefs* -- a dictionary with references to persons. + *charactersRefs* -- a dictionary with references to companies. + *modFunct* -- function called returning text fields. + """ + name = kwds.get('name') + if name and not self.data.has_key('name'): + self.set_name(name) + self.companyID = kwds.get('companyID', None) + self.myName = kwds.get('myName', u'') + + def _reset(self): + """Reset the company object.""" + self.companyID = None + self.myName = u'' + + def set_name(self, name): + """Set the name of the company.""" + # XXX: convert name to unicode, if it's a plain string? + # Company diverges a bit from other classes, being able + # to directly handle its "notes". AND THAT'S PROBABLY A BAD IDEA! + oname = name = name.strip() + notes = u'' + if name.endswith(')'): + fparidx = name.find('(') + if fparidx != -1: + notes = name[fparidx:] + name = name[:fparidx].rstrip() + if self.notes: + name = oname + d = analyze_company_name(name) + self.data.update(d) + if notes and not self.notes: + self.notes = notes + + def _additional_keys(self): + """Valid keys to append to the data.keys() list.""" + if self.data.has_key('name'): + return ['long imdb name'] + return [] + + def _getitem(self, key): + """Handle special keys.""" + ## XXX: can a company have an imdbIndex? + if self.data.has_key('name'): + if key == 'long imdb name': + return build_company_name(self.data) + return None + + def getID(self): + """Return the companyID.""" + return self.companyID + + def __nonzero__(self): + """The company is "false" if the self.data does not contain a name.""" + # XXX: check the name and the companyID? + if self.data.get('name'): return 1 + return 0 + + def __contains__(self, item): + """Return true if this company and the given Movie are related.""" + from Movie import Movie + if isinstance(item, Movie): + for m in flatten(self.data, yieldDictKeys=1, scalar=Movie): + if item.isSame(m): + return 1 + return 0 + + def isSameName(self, other): + """Return true if two company have the same name + and/or companyID.""" + if not isinstance(other, self.__class__): + return 0 + if self.data.has_key('name') and \ + other.data.has_key('name') and \ + build_company_name(self.data) == \ + build_company_name(other.data): + return 1 + if self.accessSystem == other.accessSystem and \ + self.companyID is not None and \ + self.companyID == other.companyID: + return 1 + return 0 + isSameCompany = isSameName + + def __deepcopy__(self, memo): + """Return a deep copy of a company instance.""" + c = Company(name=u'', companyID=self.companyID, + myName=self.myName, myID=self.myID, + data=deepcopy(self.data, memo), + notes=self.notes, accessSystem=self.accessSystem, + titlesRefs=deepcopy(self.titlesRefs, memo), + namesRefs=deepcopy(self.namesRefs, memo), + charactersRefs=deepcopy(self.charactersRefs, memo)) + c.current_info = list(self.current_info) + c.set_mod_funct(self.modFunct) + return c + + def __repr__(self): + """String representation of a Company object.""" + r = '<Company id:%s[%s] name:_%s_>' % (self.companyID, + self.accessSystem, + self.get('long imdb name')) + if isinstance(r, unicode): r = r.encode('utf_8', 'replace') + return r + + def __str__(self): + """Simply print the short name.""" + return self.get('name', u'').encode('utf_8', 'replace') + + def __unicode__(self): + """Simply print the short title.""" + return self.get('name', u'') + + def summary(self): + """Return a string with a pretty-printed summary for the company.""" + if not self: return u'' + s = u'Company\n=======\nName: %s\n' % \ + self.get('name', u'') + for k in ('distributor', 'production company', 'miscellaneous company', + 'special effects company'): + d = self.get(k, [])[:5] + if not d: continue + s += u'Last movies from this company (%s): %s.\n' % \ + (k, u'; '.join([x.get('long imdb title', u'') for x in d])) + return s + + diff --git a/lib/imdb/Movie.py b/lib/imdb/Movie.py new file mode 100644 index 0000000000000000000000000000000000000000..5cdcde6504accb88356ad1260833878fc8ddf7c2 --- /dev/null +++ b/lib/imdb/Movie.py @@ -0,0 +1,398 @@ +""" +Movie module (imdb package). + +This module provides the Movie class, used to store information about +a given movie. + +Copyright 2004-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from copy import deepcopy + +from imdb import linguistics +from imdb.utils import analyze_title, build_title, canonicalTitle, \ + flatten, _Container, cmpMovies + + +class Movie(_Container): + """A Movie. + + Every information about a movie can be accessed as: + movieObject['information'] + to get a list of the kind of information stored in a + Movie object, use the keys() method; some useful aliases + are defined (as "casting" for the "casting director" key); see + the keys_alias dictionary. + """ + # The default sets of information retrieved. + default_info = ('main', 'plot') + + # Aliases for some not-so-intuitive keys. + keys_alias = { + 'tv schedule': 'airing', + 'user rating': 'rating', + 'plot summary': 'plot', + 'plot summaries': 'plot', + 'directed by': 'director', + 'created by': 'creator', + 'writing credits': 'writer', + 'produced by': 'producer', + 'original music by': 'original music', + 'non-original music by': 'non-original music', + 'music': 'original music', + 'cinematography by': 'cinematographer', + 'cinematography': 'cinematographer', + 'film editing by': 'editor', + 'film editing': 'editor', + 'editing': 'editor', + 'actors': 'cast', + 'actresses': 'cast', + 'casting by': 'casting director', + 'casting': 'casting director', + 'art direction by': 'art direction', + 'set decoration by': 'set decoration', + 'costume design by': 'costume designer', + 'costume design': 'costume designer', + 'makeup department': 'make up', + 'makeup': 'make up', + 'make-up': 'make up', + 'production management': 'production manager', + 'production company': 'production companies', + 'second unit director or assistant director': + 'assistant director', + 'second unit director': 'assistant director', + 'sound department': 'sound crew', + 'costume and wardrobe department': 'costume department', + 'special effects by': 'special effects', + 'visual effects by': 'visual effects', + 'special effects company': 'special effects companies', + 'stunts': 'stunt performer', + 'other crew': 'miscellaneous crew', + 'misc crew': 'miscellaneous crew', + 'miscellaneouscrew': 'miscellaneous crew', + 'crewmembers': 'miscellaneous crew', + 'crew members': 'miscellaneous crew', + 'other companies': 'miscellaneous companies', + 'misc companies': 'miscellaneous companies', + 'miscellaneous company': 'miscellaneous companies', + 'misc company': 'miscellaneous companies', + 'other company': 'miscellaneous companies', + 'aka': 'akas', + 'also known as': 'akas', + 'country': 'countries', + 'production country': 'countries', + 'production countries': 'countries', + 'genre': 'genres', + 'runtime': 'runtimes', + 'lang': 'languages', + 'color': 'color info', + 'cover': 'cover url', + 'full-size cover': 'full-size cover url', + 'seasons': 'number of seasons', + 'language': 'languages', + 'certificate': 'certificates', + 'certifications': 'certificates', + 'certification': 'certificates', + 'miscellaneous links': 'misc links', + 'miscellaneous': 'misc links', + 'soundclips': 'sound clips', + 'videoclips': 'video clips', + 'photographs': 'photo sites', + 'distributor': 'distributors', + 'distribution': 'distributors', + 'distribution companies': 'distributors', + 'distribution company': 'distributors', + 'guest': 'guests', + 'guest appearances': 'guests', + 'tv guests': 'guests', + 'notable tv guest appearances': 'guests', + 'episodes cast': 'guests', + 'episodes number': 'number of episodes', + 'amazon review': 'amazon reviews', + 'merchandising': 'merchandising links', + 'merchandise': 'merchandising links', + 'sales': 'merchandising links', + 'faq': 'faqs', + 'parental guide': 'parents guide', + 'frequently asked questions': 'faqs'} + + keys_tomodify_list = ('plot', 'trivia', 'alternate versions', 'goofs', + 'quotes', 'dvd', 'laserdisc', 'news', 'soundtrack', + 'crazy credits', 'business', 'supplements', + 'video review', 'faqs') + + cmpFunct = cmpMovies + + def _init(self, **kwds): + """Initialize a Movie object. + + *movieID* -- the unique identifier for the movie. + *title* -- the title of the Movie, if not in the data dictionary. + *myTitle* -- your personal title for the movie. + *myID* -- your personal identifier for the movie. + *data* -- a dictionary used to initialize the object. + *currentRole* -- a Character instance representing the current role + or duty of a person in this movie, or a Person + object representing the actor/actress who played + a given character in a Movie. If a string is + passed, an object is automatically build. + *roleID* -- if available, the characterID/personID of the currentRole + object. + *roleIsPerson* -- when False (default) the currentRole is assumed + to be a Character object, otherwise a Person. + *notes* -- notes for the person referred in the currentRole + attribute; e.g.: '(voice)'. + *accessSystem* -- a string representing the data access system used. + *titlesRefs* -- a dictionary with references to movies. + *namesRefs* -- a dictionary with references to persons. + *charactersRefs* -- a dictionary with references to characters. + *modFunct* -- function called returning text fields. + """ + title = kwds.get('title') + if title and not self.data.has_key('title'): + self.set_title(title) + self.movieID = kwds.get('movieID', None) + self.myTitle = kwds.get('myTitle', u'') + + def _reset(self): + """Reset the Movie object.""" + self.movieID = None + self.myTitle = u'' + + def set_title(self, title): + """Set the title of the movie.""" + # XXX: convert title to unicode, if it's a plain string? + d_title = analyze_title(title) + self.data.update(d_title) + + def _additional_keys(self): + """Valid keys to append to the data.keys() list.""" + addkeys = [] + if self.data.has_key('title'): + addkeys += ['canonical title', 'long imdb title', + 'long imdb canonical title', + 'smart canonical title', + 'smart long imdb canonical title'] + if self.data.has_key('episode of'): + addkeys += ['long imdb episode title', 'series title', + 'canonical series title', 'episode title', + 'canonical episode title', + 'smart canonical series title', + 'smart canonical episode title'] + if self.data.has_key('cover url'): + addkeys += ['full-size cover url'] + return addkeys + + def guessLanguage(self): + """Guess the language of the title of this movie; returns None + if there are no hints.""" + lang = self.get('languages') + if lang: + lang = lang[0] + else: + country = self.get('countries') + if country: + lang = linguistics.COUNTRY_LANG.get(country[0]) + return lang + + def smartCanonicalTitle(self, title=None, lang=None): + """Return the canonical title, guessing its language. + The title can be forces with the 'title' argument (internally + used) and the language can be forced with the 'lang' argument, + otherwise it's auto-detected.""" + if title is None: + title = self.data.get('title', u'') + if lang is None: + lang = self.guessLanguage() + return canonicalTitle(title, lang=lang) + + def _getitem(self, key): + """Handle special keys.""" + if self.data.has_key('episode of'): + if key == 'long imdb episode title': + return build_title(self.data) + elif key == 'series title': + return self.data['episode of']['title'] + elif key == 'canonical series title': + ser_title = self.data['episode of']['title'] + return canonicalTitle(ser_title) + elif key == 'smart canonical series title': + ser_title = self.data['episode of']['title'] + return self.smartCanonicalTitle(ser_title) + elif key == 'episode title': + return self.data.get('title', u'') + elif key == 'canonical episode title': + return canonicalTitle(self.data.get('title', u'')) + elif key == 'smart canonical episode title': + return self.smartCanonicalTitle(self.data.get('title', u'')) + if self.data.has_key('title'): + if key == 'title': + return self.data['title'] + elif key == 'long imdb title': + return build_title(self.data) + elif key == 'canonical title': + return canonicalTitle(self.data['title']) + elif key == 'smart canonical title': + return self.smartCanonicalTitle(self.data['title']) + elif key == 'long imdb canonical title': + return build_title(self.data, canonical=1) + elif key == 'smart long imdb canonical title': + return build_title(self.data, canonical=1, + lang=self.guessLanguage()) + if key == 'full-size cover url' and self.data.has_key('cover url'): + return self._re_fullsizeURL.sub('', self.data.get('cover url', '')) + return None + + def getID(self): + """Return the movieID.""" + return self.movieID + + def __nonzero__(self): + """The Movie is "false" if the self.data does not contain a title.""" + # XXX: check the title and the movieID? + if self.data.has_key('title'): return 1 + return 0 + + def isSameTitle(self, other): + """Return true if this and the compared object have the same + long imdb title and/or movieID. + """ + # XXX: obsolete? + if not isinstance(other, self.__class__): return 0 + if self.data.has_key('title') and \ + other.data.has_key('title') and \ + build_title(self.data, canonical=0) == \ + build_title(other.data, canonical=0): + return 1 + if self.accessSystem == other.accessSystem and \ + self.movieID is not None and self.movieID == other.movieID: + return 1 + return 0 + isSameMovie = isSameTitle # XXX: just for backward compatiblity. + + def __contains__(self, item): + """Return true if the given Person object is listed in this Movie, + or if the the given Character is represented in this Movie.""" + from Person import Person + from Character import Character + from Company import Company + if isinstance(item, Person): + for p in flatten(self.data, yieldDictKeys=1, scalar=Person, + toDescend=(list, dict, tuple, Movie)): + if item.isSame(p): + return 1 + elif isinstance(item, Character): + for p in flatten(self.data, yieldDictKeys=1, scalar=Person, + toDescend=(list, dict, tuple, Movie)): + if item.isSame(p.currentRole): + return 1 + elif isinstance(item, Company): + for c in flatten(self.data, yieldDictKeys=1, scalar=Company, + toDescend=(list, dict, tuple, Movie)): + if item.isSame(c): + return 1 + return 0 + + def __deepcopy__(self, memo): + """Return a deep copy of a Movie instance.""" + m = Movie(title=u'', movieID=self.movieID, myTitle=self.myTitle, + myID=self.myID, data=deepcopy(self.data, memo), + currentRole=deepcopy(self.currentRole, memo), + roleIsPerson=self._roleIsPerson, + notes=self.notes, accessSystem=self.accessSystem, + titlesRefs=deepcopy(self.titlesRefs, memo), + namesRefs=deepcopy(self.namesRefs, memo), + charactersRefs=deepcopy(self.charactersRefs, memo)) + m.current_info = list(self.current_info) + m.set_mod_funct(self.modFunct) + return m + + def __repr__(self): + """String representation of a Movie object.""" + # XXX: add also currentRole and notes, if present? + if self.has_key('long imdb episode title'): + title = self.get('long imdb episode title') + else: + title = self.get('long imdb title') + r = '<Movie id:%s[%s] title:_%s_>' % (self.movieID, self.accessSystem, + title) + if isinstance(r, unicode): r = r.encode('utf_8', 'replace') + return r + + def __str__(self): + """Simply print the short title.""" + return self.get('title', u'').encode('utf_8', 'replace') + + def __unicode__(self): + """Simply print the short title.""" + return self.get('title', u'') + + def summary(self): + """Return a string with a pretty-printed summary for the movie.""" + if not self: return u'' + def _nameAndRole(personList, joiner=u', '): + """Build a pretty string with name and role.""" + nl = [] + for person in personList: + n = person.get('name', u'') + if person.currentRole: n += u' (%s)' % person.currentRole + nl.append(n) + return joiner.join(nl) + s = u'Movie\n=====\nTitle: %s\n' % \ + self.get('long imdb canonical title', u'') + genres = self.get('genres') + if genres: s += u'Genres: %s.\n' % u', '.join(genres) + director = self.get('director') + if director: + s += u'Director: %s.\n' % _nameAndRole(director) + writer = self.get('writer') + if writer: + s += u'Writer: %s.\n' % _nameAndRole(writer) + cast = self.get('cast') + if cast: + cast = cast[:5] + s += u'Cast: %s.\n' % _nameAndRole(cast) + runtime = self.get('runtimes') + if runtime: + s += u'Runtime: %s.\n' % u', '.join(runtime) + countries = self.get('countries') + if countries: + s += u'Country: %s.\n' % u', '.join(countries) + lang = self.get('languages') + if lang: + s += u'Language: %s.\n' % u', '.join(lang) + rating = self.get('rating') + if rating: + s += u'Rating: %s' % rating + nr_votes = self.get('votes') + if nr_votes: + s += u' (%s votes)' % nr_votes + s += u'.\n' + plot = self.get('plot') + if not plot: + plot = self.get('plot summary') + if plot: + plot = [plot] + if plot: + plot = plot[0] + i = plot.find('::') + if i != -1: + plot = plot[:i] + s += u'Plot: %s' % plot + return s + + diff --git a/lib/imdb/Person.py b/lib/imdb/Person.py new file mode 100644 index 0000000000000000000000000000000000000000..6e3e46231f795c41b34ab36170446f33d50d298a --- /dev/null +++ b/lib/imdb/Person.py @@ -0,0 +1,275 @@ +""" +Person module (imdb package). + +This module provides the Person class, used to store information about +a given person. + +Copyright 2004-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from copy import deepcopy + +from imdb.utils import analyze_name, build_name, normalizeName, \ + flatten, _Container, cmpPeople + + +class Person(_Container): + """A Person. + + Every information about a person can be accessed as: + personObject['information'] + to get a list of the kind of information stored in a + Person object, use the keys() method; some useful aliases + are defined (as "biography" for the "mini biography" key); + see the keys_alias dictionary. + """ + # The default sets of information retrieved. + default_info = ('main', 'filmography', 'biography') + + # Aliases for some not-so-intuitive keys. + keys_alias = {'biography': 'mini biography', + 'bio': 'mini biography', + 'aka': 'akas', + 'also known as': 'akas', + 'nick name': 'nick names', + 'nicks': 'nick names', + 'nickname': 'nick names', + 'miscellaneouscrew': 'miscellaneous crew', + 'crewmembers': 'miscellaneous crew', + 'misc': 'miscellaneous crew', + 'guest': 'notable tv guest appearances', + 'guests': 'notable tv guest appearances', + 'tv guest': 'notable tv guest appearances', + 'guest appearances': 'notable tv guest appearances', + 'spouses': 'spouse', + 'salary': 'salary history', + 'salaries': 'salary history', + 'otherworks': 'other works', + "maltin's biography": + "biography from leonard maltin's movie encyclopedia", + "leonard maltin's biography": + "biography from leonard maltin's movie encyclopedia", + 'real name': 'birth name', + 'where are they now': 'where now', + 'personal quotes': 'quotes', + 'mini-biography author': 'imdb mini-biography by', + 'biography author': 'imdb mini-biography by', + 'genre': 'genres', + 'portrayed': 'portrayed in', + 'keys': 'keywords', + 'trademarks': 'trade mark', + 'trade mark': 'trade mark', + 'trade marks': 'trade mark', + 'trademark': 'trade mark', + 'pictorials': 'pictorial', + 'magazine covers': 'magazine cover photo', + 'magazine-covers': 'magazine cover photo', + 'tv series episodes': 'episodes', + 'tv-series episodes': 'episodes', + 'articles': 'article', + 'keyword': 'keywords'} + + # 'nick names'??? + keys_tomodify_list = ('mini biography', 'spouse', 'quotes', 'other works', + 'salary history', 'trivia', 'trade mark', 'news', + 'books', 'biographical movies', 'portrayed in', + 'where now', 'interviews', 'article', + "biography from leonard maltin's movie encyclopedia") + + cmpFunct = cmpPeople + + def _init(self, **kwds): + """Initialize a Person object. + + *personID* -- the unique identifier for the person. + *name* -- the name of the Person, if not in the data dictionary. + *myName* -- the nickname you use for this person. + *myID* -- your personal id for this person. + *data* -- a dictionary used to initialize the object. + *currentRole* -- a Character instance representing the current role + or duty of a person in this movie, or a Person + object representing the actor/actress who played + a given character in a Movie. If a string is + passed, an object is automatically build. + *roleID* -- if available, the characterID/personID of the currentRole + object. + *roleIsPerson* -- when False (default) the currentRole is assumed + to be a Character object, otherwise a Person. + *notes* -- notes about the given person for a specific movie + or role (e.g.: the alias used in the movie credits). + *accessSystem* -- a string representing the data access system used. + *titlesRefs* -- a dictionary with references to movies. + *namesRefs* -- a dictionary with references to persons. + *modFunct* -- function called returning text fields. + *billingPos* -- position of this person in the credits list. + """ + name = kwds.get('name') + if name and not self.data.has_key('name'): + self.set_name(name) + self.personID = kwds.get('personID', None) + self.myName = kwds.get('myName', u'') + self.billingPos = kwds.get('billingPos', None) + + def _reset(self): + """Reset the Person object.""" + self.personID = None + self.myName = u'' + self.billingPos = None + + def _clear(self): + """Reset the dictionary.""" + self.billingPos = None + + def set_name(self, name): + """Set the name of the person.""" + # XXX: convert name to unicode, if it's a plain string? + d = analyze_name(name, canonical=1) + self.data.update(d) + + def _additional_keys(self): + """Valid keys to append to the data.keys() list.""" + addkeys = [] + if self.data.has_key('name'): + addkeys += ['canonical name', 'long imdb name', + 'long imdb canonical name'] + if self.data.has_key('headshot'): + addkeys += ['full-size headshot'] + return addkeys + + def _getitem(self, key): + """Handle special keys.""" + if self.data.has_key('name'): + if key == 'name': + return normalizeName(self.data['name']) + elif key == 'canonical name': + return self.data['name'] + elif key == 'long imdb name': + return build_name(self.data, canonical=0) + elif key == 'long imdb canonical name': + return build_name(self.data) + if key == 'full-size headshot' and self.data.has_key('headshot'): + return self._re_fullsizeURL.sub('', self.data.get('headshot', '')) + return None + + def getID(self): + """Return the personID.""" + return self.personID + + def __nonzero__(self): + """The Person is "false" if the self.data does not contain a name.""" + # XXX: check the name and the personID? + if self.data.has_key('name'): return 1 + return 0 + + def __contains__(self, item): + """Return true if this Person has worked in the given Movie, + or if the fiven Character was played by this Person.""" + from Movie import Movie + from Character import Character + if isinstance(item, Movie): + for m in flatten(self.data, yieldDictKeys=1, scalar=Movie): + if item.isSame(m): + return 1 + elif isinstance(item, Character): + for m in flatten(self.data, yieldDictKeys=1, scalar=Movie): + if item.isSame(m.currentRole): + return 1 + return 0 + + def isSameName(self, other): + """Return true if two persons have the same name and imdbIndex + and/or personID. + """ + if not isinstance(other, self.__class__): + return 0 + if self.data.has_key('name') and \ + other.data.has_key('name') and \ + build_name(self.data, canonical=1) == \ + build_name(other.data, canonical=1): + return 1 + if self.accessSystem == other.accessSystem and \ + self.personID and self.personID == other.personID: + return 1 + return 0 + isSamePerson = isSameName # XXX: just for backward compatiblity. + + def __deepcopy__(self, memo): + """Return a deep copy of a Person instance.""" + p = Person(name=u'', personID=self.personID, myName=self.myName, + myID=self.myID, data=deepcopy(self.data, memo), + currentRole=deepcopy(self.currentRole, memo), + roleIsPerson=self._roleIsPerson, + notes=self.notes, accessSystem=self.accessSystem, + titlesRefs=deepcopy(self.titlesRefs, memo), + namesRefs=deepcopy(self.namesRefs, memo), + charactersRefs=deepcopy(self.charactersRefs, memo)) + p.current_info = list(self.current_info) + p.set_mod_funct(self.modFunct) + p.billingPos = self.billingPos + return p + + def __repr__(self): + """String representation of a Person object.""" + # XXX: add also currentRole and notes, if present? + r = '<Person id:%s[%s] name:_%s_>' % (self.personID, self.accessSystem, + self.get('long imdb canonical name')) + if isinstance(r, unicode): r = r.encode('utf_8', 'replace') + return r + + def __str__(self): + """Simply print the short name.""" + return self.get('name', u'').encode('utf_8', 'replace') + + def __unicode__(self): + """Simply print the short title.""" + return self.get('name', u'') + + def summary(self): + """Return a string with a pretty-printed summary for the person.""" + if not self: return u'' + s = u'Person\n=====\nName: %s\n' % \ + self.get('long imdb canonical name', u'') + bdate = self.get('birth date') + if bdate: + s += u'Birth date: %s' % bdate + bnotes = self.get('birth notes') + if bnotes: + s += u' (%s)' % bnotes + s += u'.\n' + ddate = self.get('death date') + if ddate: + s += u'Death date: %s' % ddate + dnotes = self.get('death notes') + if dnotes: + s += u' (%s)' % dnotes + s += u'.\n' + bio = self.get('mini biography') + if bio: + s += u'Biography: %s\n' % bio[0] + director = self.get('director') + if director: + d_list = [x.get('long imdb canonical title', u'') + for x in director[:3]] + s += u'Last movies directed: %s.\n' % u'; '.join(d_list) + act = self.get('actor') or self.get('actress') + if act: + a_list = [x.get('long imdb canonical title', u'') + for x in act[:5]] + s += u'Last movies acted: %s.\n' % u'; '.join(a_list) + return s + + diff --git a/lib/imdb/__init__.py b/lib/imdb/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f93482da617fc063b0dedaf76f64bdbf678a144f --- /dev/null +++ b/lib/imdb/__init__.py @@ -0,0 +1,959 @@ +""" +imdb package. + +This package can be used to retrieve information about a movie or +a person from the IMDb database. +It can fetch data through different media (e.g.: the IMDb web pages, +a SQL database, etc.) + +Copyright 2004-2012 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +__all__ = ['IMDb', 'IMDbError', 'Movie', 'Person', 'Character', 'Company', + 'available_access_systems'] +__version__ = VERSION = '4.9' + +# Import compatibility module (importing it is enough). +import _compat + +import sys, os, ConfigParser, logging +from types import MethodType + +from imdb import Movie, Person, Character, Company +import imdb._logging +from imdb._exceptions import IMDbError, IMDbDataAccessError, IMDbParserError +from imdb.utils import build_title, build_name, build_company_name + +_aux_logger = logging.getLogger('imdbpy.aux') + + +# URLs of the main pages for movies, persons, characters and queries. +imdbURL_base = 'http://akas.imdb.com/' + +# NOTE: the urls below will be removed in a future version. +# please use the values in the 'urls' attribute +# of the IMDbBase subclass instance. +# http://akas.imdb.com/title/ +imdbURL_movie_base = '%stitle/' % imdbURL_base +# http://akas.imdb.com/title/tt%s/ +imdbURL_movie_main = imdbURL_movie_base + 'tt%s/' +# http://akas.imdb.com/name/ +imdbURL_person_base = '%sname/' % imdbURL_base +# http://akas.imdb.com/name/nm%s/ +imdbURL_person_main = imdbURL_person_base + 'nm%s/' +# http://akas.imdb.com/character/ +imdbURL_character_base = '%scharacter/' % imdbURL_base +# http://akas.imdb.com/character/ch%s/ +imdbURL_character_main = imdbURL_character_base + 'ch%s/' +# http://akas.imdb.com/company/ +imdbURL_company_base = '%scompany/' % imdbURL_base +# http://akas.imdb.com/company/co%s/ +imdbURL_company_main = imdbURL_company_base + 'co%s/' +# http://akas.imdb.com/keyword/%s/ +imdbURL_keyword_main = imdbURL_base + 'keyword/%s/' +# http://akas.imdb.com/chart/top +imdbURL_top250 = imdbURL_base + 'chart/top' +# http://akas.imdb.com/chart/bottom +imdbURL_bottom100 = imdbURL_base + 'chart/bottom' +# http://akas.imdb.com/find?%s +imdbURL_find = imdbURL_base + 'find?%s' + +# Name of the configuration file. +confFileName = 'imdbpy.cfg' + +class ConfigParserWithCase(ConfigParser.ConfigParser): + """A case-sensitive parser for configuration files.""" + def __init__(self, defaults=None, confFile=None, *args, **kwds): + """Initialize the parser. + + *defaults* -- defaults values. + *confFile* -- the file (or list of files) to parse.""" + ConfigParser.ConfigParser.__init__(self, defaults=defaults) + if confFile is None: + dotFileName = '.' + confFileName + # Current and home directory. + confFile = [os.path.join(os.getcwd(), confFileName), + os.path.join(os.getcwd(), dotFileName), + os.path.join(os.path.expanduser('~'), confFileName), + os.path.join(os.path.expanduser('~'), dotFileName)] + if os.name == 'posix': + sep = getattr(os.path, 'sep', '/') + # /etc/ and /etc/conf.d/ + confFile.append(os.path.join(sep, 'etc', confFileName)) + confFile.append(os.path.join(sep, 'etc', 'conf.d', + confFileName)) + else: + # etc subdirectory of sys.prefix, for non-unix systems. + confFile.append(os.path.join(sys.prefix, 'etc', confFileName)) + for fname in confFile: + try: + self.read(fname) + except (ConfigParser.MissingSectionHeaderError, + ConfigParser.ParsingError), e: + _aux_logger.warn('Troubles reading config file: %s' % e) + # Stop at the first valid file. + if self.has_section('imdbpy'): + break + + def optionxform(self, optionstr): + """Option names are case sensitive.""" + return optionstr + + def _manageValue(self, value): + """Custom substitutions for values.""" + if not isinstance(value, (str, unicode)): + return value + vlower = value.lower() + if vlower in self._boolean_states: + return self._boolean_states[vlower] + elif vlower == 'none': + return None + return value + + def get(self, section, option, *args, **kwds): + """Return the value of an option from a given section.""" + value = ConfigParser.ConfigParser.get(self, section, option, + *args, **kwds) + return self._manageValue(value) + + def items(self, section, *args, **kwds): + """Return a list of (key, value) tuples of items of the + given section.""" + if section != 'DEFAULT' and not self.has_section(section): + return [] + keys = ConfigParser.ConfigParser.options(self, section) + return [(k, self.get(section, k, *args, **kwds)) for k in keys] + + def getDict(self, section): + """Return a dictionary of items of the specified section.""" + return dict(self.items(section)) + + +def IMDb(accessSystem=None, *arguments, **keywords): + """Return an instance of the appropriate class. + The accessSystem parameter is used to specify the kind of + the preferred access system.""" + if accessSystem is None or accessSystem in ('auto', 'config'): + try: + cfg_file = ConfigParserWithCase(*arguments, **keywords) + # Parameters set by the code take precedence. + kwds = cfg_file.getDict('imdbpy') + if 'accessSystem' in kwds: + accessSystem = kwds['accessSystem'] + del kwds['accessSystem'] + else: + accessSystem = 'http' + kwds.update(keywords) + keywords = kwds + except Exception, e: + logging.getLogger('imdbpy').warn('Unable to read configuration' \ + ' file; complete error: %s' % e) + # It just LOOKS LIKE a bad habit: we tried to read config + # options from some files, but something is gone horribly + # wrong: ignore everything and pretend we were called with + # the 'http' accessSystem. + accessSystem = 'http' + if 'loggingLevel' in keywords: + imdb._logging.setLevel(keywords['loggingLevel']) + del keywords['loggingLevel'] + if 'loggingConfig' in keywords: + logCfg = keywords['loggingConfig'] + del keywords['loggingConfig'] + try: + import logging.config + logging.config.fileConfig(os.path.expanduser(logCfg)) + except Exception, e: + logging.getLogger('imdbpy').warn('unable to read logger ' \ + 'config: %s' % e) + if accessSystem in ('httpThin', 'webThin', 'htmlThin'): + logging.warn('httpThin was removed since IMDbPY 4.8') + accessSystem = 'http' + if accessSystem in ('http', 'web', 'html'): + from parser.http import IMDbHTTPAccessSystem + return IMDbHTTPAccessSystem(*arguments, **keywords) + elif accessSystem in ('mobile',): + from parser.mobile import IMDbMobileAccessSystem + return IMDbMobileAccessSystem(*arguments, **keywords) + elif accessSystem in ('local', 'files'): + # The local access system was removed since IMDbPY 4.2. + raise IMDbError('the local access system was removed since IMDbPY 4.2') + elif accessSystem in ('sql', 'db', 'database'): + try: + from parser.sql import IMDbSqlAccessSystem + except ImportError: + raise IMDbError('the sql access system is not installed') + return IMDbSqlAccessSystem(*arguments, **keywords) + else: + raise IMDbError('unknown kind of data access system: "%s"' \ + % accessSystem) + + +def available_access_systems(): + """Return the list of available data access systems.""" + asList = [] + # XXX: trying to import modules is a good thing? + try: + from parser.http import IMDbHTTPAccessSystem + asList.append('http') + except ImportError: + pass + try: + from parser.mobile import IMDbMobileAccessSystem + asList.append('mobile') + except ImportError: + pass + try: + from parser.sql import IMDbSqlAccessSystem + asList.append('sql') + except ImportError: + pass + return asList + + +# XXX: I'm not sure this is a good guess. +# I suppose that an argument of the IMDb function can be used to +# set a default encoding for the output, and then Movie, Person and +# Character objects can use this default encoding, returning strings. +# Anyway, passing unicode strings to search_movie(), search_person() +# and search_character() methods is always safer. +encoding = getattr(sys.stdin, 'encoding', '') or sys.getdefaultencoding() + +class IMDbBase: + """The base class used to search for a movie/person/character and + to get a Movie/Person/Character object. + + This class cannot directly fetch data of any kind and so you + have to search the "real" code into a subclass.""" + + # The name of the preferred access system (MUST be overridden + # in the subclasses). + accessSystem = 'UNKNOWN' + + # Top-level logger for IMDbPY. + _imdb_logger = logging.getLogger('imdbpy') + + # Whether to re-raise caught exceptions or not. + _reraise_exceptions = False + + def __init__(self, defaultModFunct=None, results=20, keywordsResults=100, + *arguments, **keywords): + """Initialize the access system. + If specified, defaultModFunct is the function used by + default by the Person, Movie and Character objects, when + accessing their text fields. + """ + # The function used to output the strings that need modification (the + # ones containing references to movie titles and person names). + self._defModFunct = defaultModFunct + # Number of results to get. + try: + results = int(results) + except (TypeError, ValueError): + results = 20 + if results < 1: + results = 20 + self._results = results + try: + keywordsResults = int(keywordsResults) + except (TypeError, ValueError): + keywordsResults = 100 + if keywordsResults < 1: + keywordsResults = 100 + self._keywordsResults = keywordsResults + self._reraise_exceptions = keywords.get('reraiseExceptions') or False + self.set_imdb_urls(keywords.get('imdbURL_base') or imdbURL_base) + + def set_imdb_urls(self, imdbURL_base): + """Set the urls used accessing the IMDb site.""" + imdbURL_base = imdbURL_base.strip().strip('"\'') + if not imdbURL_base.startswith('http://'): + imdbURL_base = 'http://%s' % imdbURL_base + if not imdbURL_base.endswith('/'): + imdbURL_base = '%s/' % imdbURL_base + # http://akas.imdb.com/title/ + imdbURL_movie_base='%stitle/' % imdbURL_base + # http://akas.imdb.com/title/tt%s/ + imdbURL_movie_main=imdbURL_movie_base + 'tt%s/' + # http://akas.imdb.com/name/ + imdbURL_person_base='%sname/' % imdbURL_base + # http://akas.imdb.com/name/nm%s/ + imdbURL_person_main=imdbURL_person_base + 'nm%s/' + # http://akas.imdb.com/character/ + imdbURL_character_base='%scharacter/' % imdbURL_base + # http://akas.imdb.com/character/ch%s/ + imdbURL_character_main=imdbURL_character_base + 'ch%s/' + # http://akas.imdb.com/company/ + imdbURL_company_base='%scompany/' % imdbURL_base + # http://akas.imdb.com/company/co%s/ + imdbURL_company_main=imdbURL_company_base + 'co%s/' + # http://akas.imdb.com/keyword/%s/ + imdbURL_keyword_main=imdbURL_base + 'keyword/%s/' + # http://akas.imdb.com/chart/top + imdbURL_top250=imdbURL_base + 'chart/top', + # http://akas.imdb.com/chart/bottom + imdbURL_bottom100=imdbURL_base + 'chart/bottom' + # http://akas.imdb.com/find?%s + imdbURL_find=imdbURL_base + 'find?%s' + self.urls = dict( + movie_base=imdbURL_movie_base, + movie_main=imdbURL_movie_main, + person_base=imdbURL_person_base, + person_main=imdbURL_person_main, + character_base=imdbURL_character_base, + character_main=imdbURL_character_main, + company_base=imdbURL_company_base, + company_main=imdbURL_company_main, + keyword_main=imdbURL_keyword_main, + top250=imdbURL_top250, + bottom100=imdbURL_bottom100, + find=imdbURL_find) + + def _normalize_movieID(self, movieID): + """Normalize the given movieID.""" + # By default, do nothing. + return movieID + + def _normalize_personID(self, personID): + """Normalize the given personID.""" + # By default, do nothing. + return personID + + def _normalize_characterID(self, characterID): + """Normalize the given characterID.""" + # By default, do nothing. + return characterID + + def _normalize_companyID(self, companyID): + """Normalize the given companyID.""" + # By default, do nothing. + return companyID + + def _get_real_movieID(self, movieID): + """Handle title aliases.""" + # By default, do nothing. + return movieID + + def _get_real_personID(self, personID): + """Handle name aliases.""" + # By default, do nothing. + return personID + + def _get_real_characterID(self, characterID): + """Handle character name aliases.""" + # By default, do nothing. + return characterID + + def _get_real_companyID(self, companyID): + """Handle company name aliases.""" + # By default, do nothing. + return companyID + + def _get_infoset(self, prefname): + """Return methods with the name starting with prefname.""" + infoset = [] + excludes = ('%sinfoset' % prefname,) + preflen = len(prefname) + for name in dir(self.__class__): + if name.startswith(prefname) and name not in excludes: + member = getattr(self.__class__, name) + if isinstance(member, MethodType): + infoset.append(name[preflen:].replace('_', ' ')) + return infoset + + def get_movie_infoset(self): + """Return the list of info set available for movies.""" + return self._get_infoset('get_movie_') + + def get_person_infoset(self): + """Return the list of info set available for persons.""" + return self._get_infoset('get_person_') + + def get_character_infoset(self): + """Return the list of info set available for characters.""" + return self._get_infoset('get_character_') + + def get_company_infoset(self): + """Return the list of info set available for companies.""" + return self._get_infoset('get_company_') + + def get_movie(self, movieID, info=Movie.Movie.default_info, modFunct=None): + """Return a Movie object for the given movieID. + + The movieID is something used to univocally identify a movie; + it can be the imdbID used by the IMDb web server, a file + pointer, a line number in a file, an ID in a database, etc. + + info is the list of sets of information to retrieve. + + If specified, modFunct will be the function used by the Movie + object when accessing its text fields (like 'plot').""" + movieID = self._normalize_movieID(movieID) + movieID = self._get_real_movieID(movieID) + movie = Movie.Movie(movieID=movieID, accessSystem=self.accessSystem) + modFunct = modFunct or self._defModFunct + if modFunct is not None: + movie.set_mod_funct(modFunct) + self.update(movie, info) + return movie + + get_episode = get_movie + + def _search_movie(self, title, results): + """Return a list of tuples (movieID, {movieData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_movie(self, title, results=None, _episodes=False): + """Return a list of Movie objects for a query for the given title. + The results argument is the maximum number of results to return.""" + if results is None: + results = self._results + try: + results = int(results) + except (ValueError, OverflowError): + results = 20 + # XXX: I suppose it will be much safer if the user provides + # an unicode string... this is just a guess. + if not isinstance(title, unicode): + title = unicode(title, encoding, 'replace') + if not _episodes: + res = self._search_movie(title, results) + else: + res = self._search_episode(title, results) + return [Movie.Movie(movieID=self._get_real_movieID(mi), + data=md, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for mi, md in res][:results] + + def _search_episode(self, title, results): + """Return a list of tuples (movieID, {movieData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_episode(self, title, results=None): + """Return a list of Movie objects for a query for the given title. + The results argument is the maximum number of results to return; + this method searches only for titles of tv (mini) series' episodes.""" + return self.search_movie(title, results=results, _episodes=True) + + def get_person(self, personID, info=Person.Person.default_info, + modFunct=None): + """Return a Person object for the given personID. + + The personID is something used to univocally identify a person; + it can be the imdbID used by the IMDb web server, a file + pointer, a line number in a file, an ID in a database, etc. + + info is the list of sets of information to retrieve. + + If specified, modFunct will be the function used by the Person + object when accessing its text fields (like 'mini biography').""" + personID = self._normalize_personID(personID) + personID = self._get_real_personID(personID) + person = Person.Person(personID=personID, + accessSystem=self.accessSystem) + modFunct = modFunct or self._defModFunct + if modFunct is not None: + person.set_mod_funct(modFunct) + self.update(person, info) + return person + + def _search_person(self, name, results): + """Return a list of tuples (personID, {personData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_person(self, name, results=None): + """Return a list of Person objects for a query for the given name. + + The results argument is the maximum number of results to return.""" + if results is None: + results = self._results + try: + results = int(results) + except (ValueError, OverflowError): + results = 20 + if not isinstance(name, unicode): + name = unicode(name, encoding, 'replace') + res = self._search_person(name, results) + return [Person.Person(personID=self._get_real_personID(pi), + data=pd, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for pi, pd in res][:results] + + def get_character(self, characterID, info=Character.Character.default_info, + modFunct=None): + """Return a Character object for the given characterID. + + The characterID is something used to univocally identify a character; + it can be the imdbID used by the IMDb web server, a file + pointer, a line number in a file, an ID in a database, etc. + + info is the list of sets of information to retrieve. + + If specified, modFunct will be the function used by the Character + object when accessing its text fields (like 'biography').""" + characterID = self._normalize_characterID(characterID) + characterID = self._get_real_characterID(characterID) + character = Character.Character(characterID=characterID, + accessSystem=self.accessSystem) + modFunct = modFunct or self._defModFunct + if modFunct is not None: + character.set_mod_funct(modFunct) + self.update(character, info) + return character + + def _search_character(self, name, results): + """Return a list of tuples (characterID, {characterData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_character(self, name, results=None): + """Return a list of Character objects for a query for the given name. + + The results argument is the maximum number of results to return.""" + if results is None: + results = self._results + try: + results = int(results) + except (ValueError, OverflowError): + results = 20 + if not isinstance(name, unicode): + name = unicode(name, encoding, 'replace') + res = self._search_character(name, results) + return [Character.Character(characterID=self._get_real_characterID(pi), + data=pd, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for pi, pd in res][:results] + + def get_company(self, companyID, info=Company.Company.default_info, + modFunct=None): + """Return a Company object for the given companyID. + + The companyID is something used to univocally identify a company; + it can be the imdbID used by the IMDb web server, a file + pointer, a line number in a file, an ID in a database, etc. + + info is the list of sets of information to retrieve. + + If specified, modFunct will be the function used by the Company + object when accessing its text fields (none, so far).""" + companyID = self._normalize_companyID(companyID) + companyID = self._get_real_companyID(companyID) + company = Company.Company(companyID=companyID, + accessSystem=self.accessSystem) + modFunct = modFunct or self._defModFunct + if modFunct is not None: + company.set_mod_funct(modFunct) + self.update(company, info) + return company + + def _search_company(self, name, results): + """Return a list of tuples (companyID, {companyData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_company(self, name, results=None): + """Return a list of Company objects for a query for the given name. + + The results argument is the maximum number of results to return.""" + if results is None: + results = self._results + try: + results = int(results) + except (ValueError, OverflowError): + results = 20 + if not isinstance(name, unicode): + name = unicode(name, encoding, 'replace') + res = self._search_company(name, results) + return [Company.Company(companyID=self._get_real_companyID(pi), + data=pd, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for pi, pd in res][:results] + + def _search_keyword(self, keyword, results): + """Return a list of 'keyword' strings.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def search_keyword(self, keyword, results=None): + """Search for existing keywords, similar to the given one.""" + if results is None: + results = self._keywordsResults + try: + results = int(results) + except (ValueError, OverflowError): + results = 100 + if not isinstance(keyword, unicode): + keyword = unicode(keyword, encoding, 'replace') + return self._search_keyword(keyword, results) + + def _get_keyword(self, keyword, results): + """Return a list of tuples (movieID, {movieData})""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def get_keyword(self, keyword, results=None): + """Return a list of movies for the given keyword.""" + if results is None: + results = self._keywordsResults + try: + results = int(results) + except (ValueError, OverflowError): + results = 100 + # XXX: I suppose it will be much safer if the user provides + # an unicode string... this is just a guess. + if not isinstance(keyword, unicode): + keyword = unicode(keyword, encoding, 'replace') + res = self._get_keyword(keyword, results) + return [Movie.Movie(movieID=self._get_real_movieID(mi), + data=md, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for mi, md in res][:results] + + def _get_top_bottom_movies(self, kind): + """Return the list of the top 250 or bottom 100 movies.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + # This method must return a list of (movieID, {movieDict}) + # tuples. The kind parameter can be 'top' or 'bottom'. + raise NotImplementedError('override this method') + + def get_top250_movies(self): + """Return the list of the top 250 movies.""" + res = self._get_top_bottom_movies('top') + return [Movie.Movie(movieID=self._get_real_movieID(mi), + data=md, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for mi, md in res] + + def get_bottom100_movies(self): + """Return the list of the bottom 100 movies.""" + res = self._get_top_bottom_movies('bottom') + return [Movie.Movie(movieID=self._get_real_movieID(mi), + data=md, modFunct=self._defModFunct, + accessSystem=self.accessSystem) for mi, md in res] + + def new_movie(self, *arguments, **keywords): + """Return a Movie object.""" + # XXX: not really useful... + if 'title' in keywords: + if not isinstance(keywords['title'], unicode): + keywords['title'] = unicode(keywords['title'], + encoding, 'replace') + elif len(arguments) > 1: + if not isinstance(arguments[1], unicode): + arguments[1] = unicode(arguments[1], encoding, 'replace') + return Movie.Movie(accessSystem=self.accessSystem, + *arguments, **keywords) + + def new_person(self, *arguments, **keywords): + """Return a Person object.""" + # XXX: not really useful... + if 'name' in keywords: + if not isinstance(keywords['name'], unicode): + keywords['name'] = unicode(keywords['name'], + encoding, 'replace') + elif len(arguments) > 1: + if not isinstance(arguments[1], unicode): + arguments[1] = unicode(arguments[1], encoding, 'replace') + return Person.Person(accessSystem=self.accessSystem, + *arguments, **keywords) + + def new_character(self, *arguments, **keywords): + """Return a Character object.""" + # XXX: not really useful... + if 'name' in keywords: + if not isinstance(keywords['name'], unicode): + keywords['name'] = unicode(keywords['name'], + encoding, 'replace') + elif len(arguments) > 1: + if not isinstance(arguments[1], unicode): + arguments[1] = unicode(arguments[1], encoding, 'replace') + return Character.Character(accessSystem=self.accessSystem, + *arguments, **keywords) + + def new_company(self, *arguments, **keywords): + """Return a Company object.""" + # XXX: not really useful... + if 'name' in keywords: + if not isinstance(keywords['name'], unicode): + keywords['name'] = unicode(keywords['name'], + encoding, 'replace') + elif len(arguments) > 1: + if not isinstance(arguments[1], unicode): + arguments[1] = unicode(arguments[1], encoding, 'replace') + return Company.Company(accessSystem=self.accessSystem, + *arguments, **keywords) + + def update(self, mop, info=None, override=0): + """Given a Movie, Person, Character or Company object with only + partial information, retrieve the required set of information. + + info is the list of sets of information to retrieve. + + If override is set, the information are retrieved and updated + even if they're already in the object.""" + # XXX: should this be a method of the Movie/Person/Character/Company + # classes? NO! What for instances created by external functions? + mopID = None + prefix = '' + if isinstance(mop, Movie.Movie): + mopID = mop.movieID + prefix = 'movie' + elif isinstance(mop, Person.Person): + mopID = mop.personID + prefix = 'person' + elif isinstance(mop, Character.Character): + mopID = mop.characterID + prefix = 'character' + elif isinstance(mop, Company.Company): + mopID = mop.companyID + prefix = 'company' + else: + raise IMDbError('object ' + repr(mop) + \ + ' is not a Movie, Person, Character or Company instance') + if mopID is None: + # XXX: enough? It's obvious that there are Characters + # objects without characterID, so I think they should + # just do nothing, when an i.update(character) is tried. + if prefix == 'character': + return + raise IMDbDataAccessError( \ + 'the supplied object has null movieID, personID or companyID') + if mop.accessSystem == self.accessSystem: + aSystem = self + else: + aSystem = IMDb(mop.accessSystem) + if info is None: + info = mop.default_info + elif info == 'all': + if isinstance(mop, Movie.Movie): + info = self.get_movie_infoset() + elif isinstance(mop, Person.Person): + info = self.get_person_infoset() + elif isinstance(mop, Character.Character): + info = self.get_character_infoset() + else: + info = self.get_company_infoset() + if not isinstance(info, (tuple, list)): + info = (info,) + res = {} + for i in info: + if i in mop.current_info and not override: + continue + if not i: + continue + self._imdb_logger.debug('retrieving "%s" info set', i) + try: + method = getattr(aSystem, 'get_%s_%s' % + (prefix, i.replace(' ', '_'))) + except AttributeError: + self._imdb_logger.error('unknown information set "%s"', i) + # Keeps going. + method = lambda *x: {} + try: + ret = method(mopID) + except Exception, e: + self._imdb_logger.critical('caught an exception retrieving ' \ + 'or parsing "%s" info set for mopID ' \ + '"%s" (accessSystem: %s)', + i, mopID, mop.accessSystem, exc_info=True) + ret = {} + # If requested by the user, reraise the exception. + if self._reraise_exceptions: + raise + keys = None + if 'data' in ret: + res.update(ret['data']) + if isinstance(ret['data'], dict): + keys = ret['data'].keys() + if 'info sets' in ret: + for ri in ret['info sets']: + mop.add_to_current_info(ri, keys, mainInfoset=i) + else: + mop.add_to_current_info(i, keys) + if 'titlesRefs' in ret: + mop.update_titlesRefs(ret['titlesRefs']) + if 'namesRefs' in ret: + mop.update_namesRefs(ret['namesRefs']) + if 'charactersRefs' in ret: + mop.update_charactersRefs(ret['charactersRefs']) + mop.set_data(res, override=0) + + def get_imdbMovieID(self, movieID): + """Translate a movieID in an imdbID (the ID used by the IMDb + web server); must be overridden by the subclass.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def get_imdbPersonID(self, personID): + """Translate a personID in a imdbID (the ID used by the IMDb + web server); must be overridden by the subclass.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def get_imdbCharacterID(self, characterID): + """Translate a characterID in a imdbID (the ID used by the IMDb + web server); must be overridden by the subclass.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def get_imdbCompanyID(self, companyID): + """Translate a companyID in a imdbID (the ID used by the IMDb + web server); must be overridden by the subclass.""" + # XXX: for the real implementation, see the method of the + # subclass, somewhere under the imdb.parser package. + raise NotImplementedError('override this method') + + def _searchIMDb(self, kind, ton): + """Search the IMDb akas server for the given title or name.""" + # The Exact Primary search system has gone AWOL, so we resort + # to the mobile search. :-/ + if not ton: + return None + aSystem = IMDb('mobile') + if kind == 'tt': + searchFunct = aSystem.search_movie + check = 'long imdb canonical title' + elif kind == 'nm': + searchFunct = aSystem.search_person + check = 'long imdb canonical name' + elif kind == 'char': + searchFunct = aSystem.search_character + check = 'long imdb canonical name' + elif kind == 'co': + # XXX: are [COUNTRY] codes included in the results? + searchFunct = aSystem.search_company + check = 'long imdb name' + try: + searchRes = searchFunct(ton) + except IMDbError: + return None + # When only one result is returned, assume it was from an + # exact match. + if len(searchRes) == 1: + return searchRes[0].getID() + for item in searchRes: + # Return the first perfect match. + if item[check] == ton: + return item.getID() + return None + + def title2imdbID(self, title): + """Translate a movie title (in the plain text data files format) + to an imdbID. + Try an Exact Primary Title search on IMDb; + return None if it's unable to get the imdbID.""" + return self._searchIMDb('tt', title) + + def name2imdbID(self, name): + """Translate a person name in an imdbID. + Try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID.""" + return self._searchIMDb('tt', name) + + def character2imdbID(self, name): + """Translate a character name in an imdbID. + Try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID.""" + return self._searchIMDb('char', name) + + def company2imdbID(self, name): + """Translate a company name in an imdbID. + Try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID.""" + return self._searchIMDb('co', name) + + def get_imdbID(self, mop): + """Return the imdbID for the given Movie, Person, Character or Company + object.""" + imdbID = None + if mop.accessSystem == self.accessSystem: + aSystem = self + else: + aSystem = IMDb(mop.accessSystem) + if isinstance(mop, Movie.Movie): + if mop.movieID is not None: + imdbID = aSystem.get_imdbMovieID(mop.movieID) + else: + imdbID = aSystem.title2imdbID(build_title(mop, canonical=0, + ptdf=1)) + elif isinstance(mop, Person.Person): + if mop.personID is not None: + imdbID = aSystem.get_imdbPersonID(mop.personID) + else: + imdbID = aSystem.name2imdbID(build_name(mop, canonical=1)) + elif isinstance(mop, Character.Character): + if mop.characterID is not None: + imdbID = aSystem.get_imdbCharacterID(mop.characterID) + else: + # canonical=0 ? + imdbID = aSystem.character2imdbID(build_name(mop, canonical=1)) + elif isinstance(mop, Company.Company): + if mop.companyID is not None: + imdbID = aSystem.get_imdbCompanyID(mop.companyID) + else: + imdbID = aSystem.company2imdbID(build_company_name(mop)) + else: + raise IMDbError('object ' + repr(mop) + \ + ' is not a Movie, Person or Character instance') + return imdbID + + def get_imdbURL(self, mop): + """Return the main IMDb URL for the given Movie, Person, + Character or Company object, or None if unable to get it.""" + imdbID = self.get_imdbID(mop) + if imdbID is None: + return None + if isinstance(mop, Movie.Movie): + url_firstPart = imdbURL_movie_main + elif isinstance(mop, Person.Person): + url_firstPart = imdbURL_person_main + elif isinstance(mop, Character.Character): + url_firstPart = imdbURL_character_main + elif isinstance(mop, Company.Company): + url_firstPart = imdbURL_company_main + else: + raise IMDbError('object ' + repr(mop) + \ + ' is not a Movie, Person, Character or Company instance') + return url_firstPart % imdbID + + def get_special_methods(self): + """Return the special methods defined by the subclass.""" + sm_dict = {} + base_methods = [] + for name in dir(IMDbBase): + member = getattr(IMDbBase, name) + if isinstance(member, MethodType): + base_methods.append(name) + for name in dir(self.__class__): + if name.startswith('_') or name in base_methods or \ + name.startswith('get_movie_') or \ + name.startswith('get_person_') or \ + name.startswith('get_company_') or \ + name.startswith('get_character_'): + continue + member = getattr(self.__class__, name) + if isinstance(member, MethodType): + sm_dict.update({name: member.__doc__}) + return sm_dict + diff --git a/lib/imdb/_compat.py b/lib/imdb/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..03b11564ea934a43ebb89fe01333d23acba851ff --- /dev/null +++ b/lib/imdb/_compat.py @@ -0,0 +1,72 @@ +""" +_compat module (imdb package). + +This module provides compatibility functions used by the imdb package +to deal with unusual environments. + +Copyright 2008-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +# TODO: now we're heavily using the 'logging' module, which was not +# present in Python 2.2. To work in a Symbian environment, we +# need to create a fake 'logging' module (its functions may call +# the 'warnings' module, or do nothing at all). + + +import os +# If true, we're working on a Symbian device. +if os.name == 'e32': + # Replace os.path.expandvars and os.path.expanduser, if needed. + def _noact(x): + """Ad-hoc replacement for IMDbPY.""" + return x + try: + os.path.expandvars + except AttributeError: + os.path.expandvars = _noact + try: + os.path.expanduser + except AttributeError: + os.path.expanduser = _noact + + # time.strptime is missing, on Symbian devices. + import time + try: + time.strptime + except AttributeError: + import re + _re_web_time = re.compile(r'Episode dated (\d+) (\w+) (\d+)') + _re_ptdf_time = re.compile(r'\((\d+)-(\d+)-(\d+)\)') + _month2digit = {'January': '1', 'February': '2', 'March': '3', + 'April': '4', 'May': '5', 'June': '6', 'July': '7', + 'August': '8', 'September': '9', 'October': '10', + 'November': '11', 'December': '12'} + def strptime(s, format): + """Ad-hoc strptime replacement for IMDbPY.""" + try: + if format.startswith('Episode'): + res = _re_web_time.findall(s)[0] + return (int(res[2]), int(_month2digit[res[1]]), int(res[0]), + 0, 0, 0, 0, 1, 0) + else: + res = _re_ptdf_time.findall(s)[0] + return (int(res[0]), int(res[1]), int(res[2]), + 0, 0, 0, 0, 1, 0) + except: + raise ValueError('error in IMDbPY\'s ad-hoc strptime!') + time.strptime = strptime + diff --git a/lib/imdb/_exceptions.py b/lib/imdb/_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..55788a207d1e802b4d56a19cd88e7c40f6be39cf --- /dev/null +++ b/lib/imdb/_exceptions.py @@ -0,0 +1,45 @@ +""" +_exceptions module (imdb package). + +This module provides the exception hierarchy used by the imdb package. + +Copyright 2004-2009 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import logging + + +class IMDbError(Exception): + """Base class for every exception raised by the imdb package.""" + _logger = logging.getLogger('imdbpy') + + def __init__(self, *args, **kwargs): + """Initialize the exception and pass the message to the log system.""" + # Every raised exception also dispatch a critical log. + self._logger.critical('%s exception raised; args: %s; kwds: %s', + self.__class__.__name__, args, kwargs, + exc_info=True) + Exception.__init__(self, *args, **kwargs) + +class IMDbDataAccessError(IMDbError): + """Exception raised when is not possible to access needed data.""" + pass + +class IMDbParserError(IMDbError): + """Exception raised when an error occurred parsing the data.""" + pass + diff --git a/lib/imdb/_logging.py b/lib/imdb/_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..2b8a286a0d74a60c3fff536dbc49d440615d03f5 --- /dev/null +++ b/lib/imdb/_logging.py @@ -0,0 +1,63 @@ +""" +_logging module (imdb package). + +This module provides the logging facilities used by the imdb package. + +Copyright 2009-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import logging + +LEVELS = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARNING, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + + +imdbpyLogger = logging.getLogger('imdbpy') +imdbpyStreamHandler = logging.StreamHandler() +imdbpyFormatter = logging.Formatter('%(asctime)s %(levelname)s [%(name)s]' \ + ' %(pathname)s:%(lineno)d: %(message)s') +imdbpyStreamHandler.setFormatter(imdbpyFormatter) +imdbpyLogger.addHandler(imdbpyStreamHandler) + +def setLevel(level): + """Set logging level for the main logger.""" + level = level.lower().strip() + imdbpyLogger.setLevel(LEVELS.get(level, logging.NOTSET)) + imdbpyLogger.log(imdbpyLogger.level, 'set logging threshold to "%s"', + logging.getLevelName(imdbpyLogger.level)) + + +#imdbpyLogger.setLevel(logging.DEBUG) + + +# It can be an idea to have a single function to log and warn: +#import warnings +#def log_and_warn(msg, args=None, logger=None, level=None): +# """Log the message and issue a warning.""" +# if logger is None: +# logger = imdbpyLogger +# if level is None: +# level = logging.WARNING +# if args is None: +# args = () +# #warnings.warn(msg % args, stacklevel=0) +# logger.log(level, msg % args) + diff --git a/lib/imdb/helpers.py b/lib/imdb/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..f22061429d8a7038ed4b1e43df5a8584cffe9600 --- /dev/null +++ b/lib/imdb/helpers.py @@ -0,0 +1,640 @@ +""" +helpers module (imdb package). + +This module provides functions not used directly by the imdb package, +but useful for IMDbPY-based programs. + +Copyright 2006-2012 Davide Alberani <da@erlug.linux.it> + 2012 Alberto Malagoli <albemala AT gmail.com> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +# XXX: find better names for the functions in this modules. + +import re +import difflib +from cgi import escape +import gettext +from gettext import gettext as _ +gettext.textdomain('imdbpy') + +# The modClearRefs can be used to strip names and titles references from +# the strings in Movie and Person objects. +from imdb.utils import modClearRefs, re_titleRef, re_nameRef, \ + re_characterRef, _tagAttr, _Container, TAGS_TO_MODIFY +from imdb import IMDb, imdbURL_movie_base, imdbURL_person_base, \ + imdbURL_character_base + +import imdb.locale +from imdb.linguistics import COUNTRY_LANG +from imdb.Movie import Movie +from imdb.Person import Person +from imdb.Character import Character +from imdb.Company import Company +from imdb.parser.http.utils import re_entcharrefssub, entcharrefs, \ + subXMLRefs, subSGMLRefs +from imdb.parser.http.bsouplxml.etree import BeautifulSoup + + +# An URL, more or less. +_re_href = re.compile(r'(http://.+?)(?=\s|$)', re.I) +_re_hrefsub = _re_href.sub + + +def makeCgiPrintEncoding(encoding): + """Make a function to pretty-print strings for the web.""" + def cgiPrint(s): + """Encode the given string using the %s encoding, and replace + chars outside the given charset with XML char references.""" % encoding + s = escape(s, quote=1) + if isinstance(s, unicode): + s = s.encode(encoding, 'xmlcharrefreplace') + return s + return cgiPrint + +# cgiPrint uses the latin_1 encoding. +cgiPrint = makeCgiPrintEncoding('latin_1') + +# Regular expression for %(varname)s substitutions. +re_subst = re.compile(r'%\((.+?)\)s') +# Regular expression for <if condition>....</if condition> clauses. +re_conditional = re.compile(r'<if\s+(.+?)\s*>(.+?)</if\s+\1\s*>') + + +def makeTextNotes(replaceTxtNotes): + """Create a function useful to handle text[::optional_note] values. + replaceTxtNotes is a format string, which can include the following + values: %(text)s and %(notes)s. + Portions of the text can be conditionally excluded, if one of the + values is absent. E.g.: <if notes>[%(notes)s]</if notes> will be replaced + with '[notes]' if notes exists, or by an empty string otherwise. + The returned function is suitable be passed as applyToValues argument + of the makeObject2Txt function.""" + def _replacer(s): + outS = replaceTxtNotes + if not isinstance(s, (unicode, str)): + return s + ssplit = s.split('::', 1) + text = ssplit[0] + # Used to keep track of text and note existence. + keysDict = {} + if text: + keysDict['text'] = True + outS = outS.replace('%(text)s', text) + if len(ssplit) == 2: + keysDict['notes'] = True + outS = outS.replace('%(notes)s', ssplit[1]) + else: + outS = outS.replace('%(notes)s', u'') + def _excludeFalseConditionals(matchobj): + # Return an empty string if the conditional is false/empty. + if matchobj.group(1) in keysDict: + return matchobj.group(2) + return u'' + while re_conditional.search(outS): + outS = re_conditional.sub(_excludeFalseConditionals, outS) + return outS + return _replacer + + +def makeObject2Txt(movieTxt=None, personTxt=None, characterTxt=None, + companyTxt=None, joiner=' / ', + applyToValues=lambda x: x, _recurse=True): + """"Return a function useful to pretty-print Movie, Person, + Character and Company instances. + + *movieTxt* -- how to format a Movie object. + *personTxt* -- how to format a Person object. + *characterTxt* -- how to format a Character object. + *companyTxt* -- how to format a Company object. + *joiner* -- string used to join a list of objects. + *applyToValues* -- function to apply to values. + *_recurse* -- if True (default) manage only the given object. + """ + # Some useful defaults. + if movieTxt is None: + movieTxt = '%(long imdb title)s' + if personTxt is None: + personTxt = '%(long imdb name)s' + if characterTxt is None: + characterTxt = '%(long imdb name)s' + if companyTxt is None: + companyTxt = '%(long imdb name)s' + def object2txt(obj, _limitRecursion=None): + """Pretty-print objects.""" + # Prevent unlimited recursion. + if _limitRecursion is None: + _limitRecursion = 0 + elif _limitRecursion > 5: + return u'' + _limitRecursion += 1 + if isinstance(obj, (list, tuple)): + return joiner.join([object2txt(o, _limitRecursion=_limitRecursion) + for o in obj]) + elif isinstance(obj, dict): + # XXX: not exactly nice, neither useful, I fear. + return joiner.join([u'%s::%s' % + (object2txt(k, _limitRecursion=_limitRecursion), + object2txt(v, _limitRecursion=_limitRecursion)) + for k, v in obj.items()]) + objData = {} + if isinstance(obj, Movie): + objData['movieID'] = obj.movieID + outs = movieTxt + elif isinstance(obj, Person): + objData['personID'] = obj.personID + outs = personTxt + elif isinstance(obj, Character): + objData['characterID'] = obj.characterID + outs = characterTxt + elif isinstance(obj, Company): + objData['companyID'] = obj.companyID + outs = companyTxt + else: + return obj + def _excludeFalseConditionals(matchobj): + # Return an empty string if the conditional is false/empty. + condition = matchobj.group(1) + proceed = obj.get(condition) or getattr(obj, condition, None) + if proceed: + return matchobj.group(2) + else: + return u'' + return matchobj.group(2) + while re_conditional.search(outs): + outs = re_conditional.sub(_excludeFalseConditionals, outs) + for key in re_subst.findall(outs): + value = obj.get(key) or getattr(obj, key, None) + if not isinstance(value, (unicode, str)): + if not _recurse: + if value: + value = unicode(value) + if value: + value = object2txt(value, _limitRecursion=_limitRecursion) + elif value: + value = applyToValues(unicode(value)) + if not value: + value = u'' + elif not isinstance(value, (unicode, str)): + value = unicode(value) + outs = outs.replace(u'%(' + key + u')s', value) + return outs + return object2txt + + +def makeModCGILinks(movieTxt, personTxt, characterTxt=None, + encoding='latin_1'): + """Make a function used to pretty-print movies and persons refereces; + movieTxt and personTxt are the strings used for the substitutions. + movieTxt must contains %(movieID)s and %(title)s, while personTxt + must contains %(personID)s and %(name)s and characterTxt %(characterID)s + and %(name)s; characterTxt is optional, for backward compatibility.""" + _cgiPrint = makeCgiPrintEncoding(encoding) + def modCGILinks(s, titlesRefs, namesRefs, characterRefs=None): + """Substitute movies and persons references.""" + if characterRefs is None: characterRefs = {} + # XXX: look ma'... more nested scopes! <g> + def _replaceMovie(match): + to_replace = match.group(1) + item = titlesRefs.get(to_replace) + if item: + movieID = item.movieID + to_replace = movieTxt % {'movieID': movieID, + 'title': unicode(_cgiPrint(to_replace), + encoding, + 'xmlcharrefreplace')} + return to_replace + def _replacePerson(match): + to_replace = match.group(1) + item = namesRefs.get(to_replace) + if item: + personID = item.personID + to_replace = personTxt % {'personID': personID, + 'name': unicode(_cgiPrint(to_replace), + encoding, + 'xmlcharrefreplace')} + return to_replace + def _replaceCharacter(match): + to_replace = match.group(1) + if characterTxt is None: + return to_replace + item = characterRefs.get(to_replace) + if item: + characterID = item.characterID + if characterID is None: + return to_replace + to_replace = characterTxt % {'characterID': characterID, + 'name': unicode(_cgiPrint(to_replace), + encoding, + 'xmlcharrefreplace')} + return to_replace + s = s.replace('<', '<').replace('>', '>') + s = _re_hrefsub(r'<a href="\1">\1</a>', s) + s = re_titleRef.sub(_replaceMovie, s) + s = re_nameRef.sub(_replacePerson, s) + s = re_characterRef.sub(_replaceCharacter, s) + return s + modCGILinks.movieTxt = movieTxt + modCGILinks.personTxt = personTxt + modCGILinks.characterTxt = characterTxt + return modCGILinks + +# links to the imdb.com web site. +_movieTxt = '<a href="' + imdbURL_movie_base + 'tt%(movieID)s">%(title)s</a>' +_personTxt = '<a href="' + imdbURL_person_base + 'nm%(personID)s">%(name)s</a>' +_characterTxt = '<a href="' + imdbURL_character_base + \ + 'ch%(characterID)s">%(name)s</a>' +modHtmlLinks = makeModCGILinks(movieTxt=_movieTxt, personTxt=_personTxt, + characterTxt=_characterTxt) +modHtmlLinksASCII = makeModCGILinks(movieTxt=_movieTxt, personTxt=_personTxt, + characterTxt=_characterTxt, + encoding='ascii') + + +everyentcharrefs = entcharrefs.copy() +for k, v in {'lt':u'<','gt':u'>','amp':u'&','quot':u'"','apos':u'\''}.items(): + everyentcharrefs[k] = v + everyentcharrefs['#%s' % ord(v)] = v +everyentcharrefsget = everyentcharrefs.get +re_everyentcharrefs = re.compile('&(%s|\#160|\#\d{1,5});' % + '|'.join(map(re.escape, everyentcharrefs))) +re_everyentcharrefssub = re_everyentcharrefs.sub + +def _replAllXMLRef(match): + """Replace the matched XML reference.""" + ref = match.group(1) + value = everyentcharrefsget(ref) + if value is None: + if ref[0] == '#': + return unichr(int(ref[1:])) + else: + return ref + return value + +def subXMLHTMLSGMLRefs(s): + """Return the given string with XML/HTML/SGML entity and char references + replaced.""" + return re_everyentcharrefssub(_replAllXMLRef, s) + + +def sortedSeasons(m): + """Return a sorted list of seasons of the given series.""" + seasons = m.get('episodes', {}).keys() + seasons.sort() + return seasons + + +def sortedEpisodes(m, season=None): + """Return a sorted list of episodes of the given series, + considering only the specified season(s) (every season, if None).""" + episodes = [] + seasons = season + if season is None: + seasons = sortedSeasons(m) + else: + if not isinstance(season, (tuple, list)): + seasons = [season] + for s in seasons: + eps_indx = m.get('episodes', {}).get(s, {}).keys() + eps_indx.sort() + for e in eps_indx: + episodes.append(m['episodes'][s][e]) + return episodes + + +# Idea and portions of the code courtesy of none none (dclist at gmail.com) +_re_imdbIDurl = re.compile(r'\b(nm|tt|ch|co)([0-9]{7})\b') +def get_byURL(url, info=None, args=None, kwds=None): + """Return a Movie, Person, Character or Company object for the given URL; + info is the info set to retrieve, args and kwds are respectively a list + and a dictionary or arguments to initialize the data access system. + Returns None if unable to correctly parse the url; can raise + exceptions if unable to retrieve the data.""" + if args is None: args = [] + if kwds is None: kwds = {} + ia = IMDb(*args, **kwds) + match = _re_imdbIDurl.search(url) + if not match: + return None + imdbtype = match.group(1) + imdbID = match.group(2) + if imdbtype == 'tt': + return ia.get_movie(imdbID, info=info) + elif imdbtype == 'nm': + return ia.get_person(imdbID, info=info) + elif imdbtype == 'ch': + return ia.get_character(imdbID, info=info) + elif imdbtype == 'co': + return ia.get_company(imdbID, info=info) + return None + + +# Idea and portions of code courtesy of Basil Shubin. +# Beware that these information are now available directly by +# the Movie/Person/Character instances. +def fullSizeCoverURL(obj): + """Given an URL string or a Movie, Person or Character instance, + returns an URL to the full-size version of the cover/headshot, + or None otherwise. This function is obsolete: the same information + are available as keys: 'full-size cover url' and 'full-size headshot', + respectively for movies and persons/characters.""" + if isinstance(obj, Movie): + coverUrl = obj.get('cover url') + elif isinstance(obj, (Person, Character)): + coverUrl = obj.get('headshot') + else: + coverUrl = obj + if not coverUrl: + return None + return _Container._re_fullsizeURL.sub('', coverUrl) + + +def keyToXML(key): + """Return a key (the ones used to access information in Movie and + other classes instances) converted to the style of the XML output.""" + return _tagAttr(key, '')[0] + + +def translateKey(key): + """Translate a given key.""" + return _(keyToXML(key)) + + +# Maps tags to classes. +_MAP_TOP_OBJ = { + 'person': Person, + 'movie': Movie, + 'character': Character, + 'company': Company +} + +# Tags to be converted to lists. +_TAGS_TO_LIST = dict([(x[0], None) for x in TAGS_TO_MODIFY.values()]) +_TAGS_TO_LIST.update(_MAP_TOP_OBJ) + +def tagToKey(tag): + """Return the name of the tag, taking it from the 'key' attribute, + if present.""" + keyAttr = tag.get('key') + if keyAttr: + if tag.get('keytype') == 'int': + keyAttr = int(keyAttr) + return keyAttr + return tag.name + + +def _valueWithType(tag, tagValue): + """Return tagValue, handling some type conversions.""" + tagType = tag.get('type') + if tagType == 'int': + tagValue = int(tagValue) + elif tagType == 'float': + tagValue = float(tagValue) + return tagValue + + +# Extra tags to get (if values were not already read from title/name). +_titleTags = ('imdbindex', 'kind', 'year') +_nameTags = ('imdbindex') +_companyTags = ('imdbindex', 'country') + +def parseTags(tag, _topLevel=True, _as=None, _infoset2keys=None, + _key2infoset=None): + """Recursively parse a tree of tags.""" + # The returned object (usually a _Container subclass, but it can + # be a string, an int, a float, a list or a dictionary). + item = None + if _infoset2keys is None: + _infoset2keys = {} + if _key2infoset is None: + _key2infoset = {} + name = tagToKey(tag) + firstChild = tag.find(recursive=False) + tagStr = (tag.string or u'').strip() + if not tagStr and name == 'item': + # Handles 'item' tags containing text and a 'notes' sub-tag. + tagContent = tag.contents[0] + if isinstance(tagContent, BeautifulSoup.NavigableString): + tagStr = (unicode(tagContent) or u'').strip() + tagType = tag.get('type') + infoset = tag.get('infoset') + if infoset: + _key2infoset[name] = infoset + _infoset2keys.setdefault(infoset, []).append(name) + # Here we use tag.name to avoid tags like <item title="company"> + if tag.name in _MAP_TOP_OBJ: + # One of the subclasses of _Container. + item = _MAP_TOP_OBJ[name]() + itemAs = tag.get('access-system') + if itemAs: + if not _as: + _as = itemAs + else: + itemAs = _as + item.accessSystem = itemAs + tagsToGet = [] + theID = tag.get('id') + if name == 'movie': + item.movieID = theID + tagsToGet = _titleTags + theTitle = tag.find('title', recursive=False) + if tag.title: + item.set_title(tag.title.string) + tag.title.extract() + else: + if name == 'person': + item.personID = theID + tagsToGet = _nameTags + theName = tag.find('long imdb canonical name', recursive=False) + if not theName: + theName = tag.find('name', recursive=False) + elif name == 'character': + item.characterID = theID + tagsToGet = _nameTags + theName = tag.find('name', recursive=False) + elif name == 'company': + item.companyID = theID + tagsToGet = _companyTags + theName = tag.find('name', recursive=False) + if theName: + item.set_name(theName.string) + if theName: + theName.extract() + for t in tagsToGet: + if t in item.data: + continue + dataTag = tag.find(t, recursive=False) + if dataTag: + item.data[tagToKey(dataTag)] = _valueWithType(dataTag, + dataTag.string) + if tag.notes: + item.notes = tag.notes.string + tag.notes.extract() + episodeOf = tag.find('episode-of', recursive=False) + if episodeOf: + item.data['episode of'] = parseTags(episodeOf, _topLevel=False, + _as=_as, _infoset2keys=_infoset2keys, + _key2infoset=_key2infoset) + episodeOf.extract() + cRole = tag.find('current-role', recursive=False) + if cRole: + cr = parseTags(cRole, _topLevel=False, _as=_as, + _infoset2keys=_infoset2keys, _key2infoset=_key2infoset) + item.currentRole = cr + cRole.extract() + # XXX: big assumption, here. What about Movie instances used + # as keys in dictionaries? What about other keys (season and + # episode number, for example?) + if not _topLevel: + #tag.extract() + return item + _adder = lambda key, value: item.data.update({key: value}) + elif tagStr: + if tag.notes: + notes = (tag.notes.string or u'').strip() + if notes: + tagStr += u'::%s' % notes + else: + tagStr = _valueWithType(tag, tagStr) + return tagStr + elif firstChild: + firstChildName = tagToKey(firstChild) + if firstChildName in _TAGS_TO_LIST: + item = [] + _adder = lambda key, value: item.append(value) + else: + item = {} + _adder = lambda key, value: item.update({key: value}) + else: + item = {} + _adder = lambda key, value: item.update({name: value}) + for subTag in tag(recursive=False): + subTagKey = tagToKey(subTag) + # Exclude dinamically generated keys. + if tag.name in _MAP_TOP_OBJ and subTagKey in item._additional_keys(): + continue + subItem = parseTags(subTag, _topLevel=False, _as=_as, + _infoset2keys=_infoset2keys, _key2infoset=_key2infoset) + if subItem: + _adder(subTagKey, subItem) + if _topLevel and name in _MAP_TOP_OBJ: + # Add information about 'info sets', but only to the top-level object. + item.infoset2keys = _infoset2keys + item.key2infoset = _key2infoset + item.current_info = _infoset2keys.keys() + return item + + +def parseXML(xml): + """Parse a XML string, returning an appropriate object (usually an + instance of a subclass of _Container.""" + xmlObj = BeautifulSoup.BeautifulStoneSoup(xml, + convertEntities=BeautifulSoup.BeautifulStoneSoup.XHTML_ENTITIES) + if xmlObj: + mainTag = xmlObj.find() + if mainTag: + return parseTags(mainTag) + return None + + +_re_akas_lang = re.compile('(?:[(])([a-zA-Z]+?)(?: title[)])') +_re_akas_country = re.compile('\(.*?\)') + +# akasLanguages, sortAKAsBySimilarity and getAKAsInLanguage code +# copyright of Alberto Malagoli (refactoring by Davide Alberani). +def akasLanguages(movie): + """Given a movie, return a list of tuples in (lang, AKA) format; + lang can be None, if unable to detect.""" + lang_and_aka = [] + akas = set((movie.get('akas') or []) + + (movie.get('akas from release info') or [])) + for aka in akas: + # split aka + aka = aka.encode('utf8').split('::') + # sometimes there is no countries information + if len(aka) == 2: + # search for something like "(... title)" where ... is a language + language = _re_akas_lang.search(aka[1]) + if language: + language = language.groups()[0] + else: + # split countries using , and keep only the first one (it's sufficient) + country = aka[1].split(',')[0] + # remove parenthesis + country = _re_akas_country.sub('', country).strip() + # given the country, get corresponding language from dictionary + language = COUNTRY_LANG.get(country) + else: + language = None + lang_and_aka.append((language, aka[0].decode('utf8'))) + return lang_and_aka + + +def sortAKAsBySimilarity(movie, title, _titlesOnly=True, _preferredLang=None): + """Return a list of movie AKAs, sorted by their similarity to + the given title. + If _titlesOnly is not True, similarity information are returned. + If _preferredLang is specified, AKAs in the given language will get + a higher score. + The return is a list of title, or a list of tuples if _titlesOnly is False.""" + language = movie.guessLanguage() + # estimate string distance between current title and given title + m_title = movie['title'].lower() + l_title = title.lower() + if isinstance(l_title, unicode): + l_title = l_title.encode('utf8') + scores = [] + score = difflib.SequenceMatcher(None, m_title.encode('utf8'), l_title).ratio() + # set original title and corresponding score as the best match for given title + scores.append((score, movie['title'], None)) + for language, aka in akasLanguages(movie): + # estimate string distance between current title and given title + m_title = aka.lower() + if isinstance(m_title, unicode): + m_title = m_title.encode('utf8') + score = difflib.SequenceMatcher(None, m_title, l_title).ratio() + # if current language is the same as the given one, increase score + if _preferredLang and _preferredLang == language: + score += 1 + scores.append((score, aka, language)) + scores.sort(reverse=True) + if _titlesOnly: + return [x[1] for x in scores] + return scores + + +def getAKAsInLanguage(movie, lang, _searchedTitle=None): + """Return a list of AKAs of a movie, in the specified language. + If _searchedTitle is given, the AKAs are sorted by their similarity + to it.""" + akas = [] + for language, aka in akasLanguages(movie): + if lang == language: + akas.append(aka) + if _searchedTitle: + scores = [] + if isinstance(_searchedTitle, unicode): + _searchedTitle = _searchedTitle.encode('utf8') + for aka in akas: + m_aka = aka + if isinstance(m_aka): + m_aka = m_aka.encode('utf8') + scores.append(difflib.SequenceMatcher(None, m_aka.lower(), + _searchedTitle.lower()), aka) + scores.sort(reverse=True) + akas = [x[1] for x in scores] + return akas + diff --git a/lib/imdb/imdbpy.cfg b/lib/imdb/imdbpy.cfg new file mode 100644 index 0000000000000000000000000000000000000000..407fdacc2e34fe9cdb6ac0b3ec7f1fe22ead7f04 --- /dev/null +++ b/lib/imdb/imdbpy.cfg @@ -0,0 +1,78 @@ +# +# IMDbPY configuration file. +# +# This file can be placed in many locations; the first file found is +# used, _ignoring_ the content of the others. +# +# Place it in one of the following directories (in order of precedence): +# +# - imdbpy.cfg in the current directory. +# - .imdbpy.cfg in the current directory. +# - imdbpy.cfg in the user's home directory. +# - .imdbpy.cfg in the user's home directory. +# - /etc/imdbpy.cfg Unix-like systems only. +# - /etc/conf.d/imdbpy.cfg Unix-like systems only. +# - sys.prefix + imdbpy.cfg for non-Unix (e.g.: C:\Python\etc\imdbpy.cfg) +# +# If this file is not found, 'http' access system is used by default. +# +# Lines starting with #, ; and // are considered comments and ignored. +# +# Some special values are replaced with Python equivalents (case insensitive): +# +# 0, off, false, no -> False +# 1, on, true, yes -> True +# none -> None +# +# Other options, like defaultModFunct, must be passed by the code. +# + +[imdbpy] +## Default. +accessSystem = mobile + +## Optional (options common to every data access system): +# Activate adult searches (on, by default). +#adultSearch = on +# Number of results for searches (20 by default). +#results = 20 +# Re-raise all caught exceptions (off, by default). +reraiseExceptions = on + +## Optional (options common to http and mobile data access systems): +# Proxy used to access the network. If it requires authentication, +# try with: http://username:password@server_address:port/ +#proxy = http://localhost:8080/ +# Cookies of the IMDb.com account +#cookie_id = string_representing_the_cookie_id +#cookie_uu = string_representing_the_cookie_uu +## Timeout for the connection to IMDb (30 seconds, by default). +#timeout = 30 +# Base url to access pages on the IMDb.com web server. +#imdbURL_base = http://akas.imdb.com/ + +## Parameters for the 'http' data access system. +# Parser to use; can be a single value or a list of value separated by +# a comma, to express order preference. Valid values: "lxml", "beautifulsoup" +#useModule = lxml,beautifulsoup + +## Parameters for the 'mobile' data access system. +#accessSystem = mobile + +## Parameters for the 'sql' data access system. +#accessSystem = sql +#uri = mysql://user:password@localhost/imdb +# ORM to use; can be a single value or a list of value separated by +# a comma, to express order preference. Valid values: "sqlobject", "sqlalchemy" +#useORM = sqlobject,sqlalchemy + +## Set the threshold for logging messages. +# Can be one of "debug", "info", "warning", "error", "critical" (default: +# "warning"). +loggingLevel = info + +## Path to a configuration file for the logging facility; +# see: http://docs.python.org/library/logging.html#configuring-logging +#loggingConfig = ~/.imdbpy-logger.cfg + + diff --git a/lib/imdb/linguistics.py b/lib/imdb/linguistics.py new file mode 100644 index 0000000000000000000000000000000000000000..da8829fd0f6c793e88a9305de322e13caa894a6c --- /dev/null +++ b/lib/imdb/linguistics.py @@ -0,0 +1,203 @@ +""" +linguistics module (imdb package). + +This module provides functions and data to handle in a smart way +languages and articles (in various languages) at the beginning of movie titles. + +Copyright 2009-2012 Davide Alberani <da@erlug.linux.it> + 2012 Alberto Malagoli <albemala AT gmail.com> + 2009 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +# List of generic articles used when the language of the title is unknown (or +# we don't have information about articles in that language). +# XXX: Managing titles in a lot of different languages, a function to recognize +# an initial article can't be perfect; sometimes we'll stumble upon a short +# word that is an article in some language, but it's not in another; in these +# situations we have to choose if we want to interpret this little word +# as an article or not (remember that we don't know what the original language +# of the title was). +# Example: 'en' is (I suppose) an article in Some Language. Unfortunately it +# seems also to be a preposition in other languages (French?). +# Running a script over the whole list of titles (and aliases), I've found +# that 'en' is used as an article only 376 times, and as another thing 594 +# times, so I've decided to _always_ consider 'en' as a non article. +# +# Here is a list of words that are _never_ considered as articles, complete +# with the cound of times they are used in a way or another: +# 'en' (376 vs 594), 'to' (399 vs 727), 'as' (198 vs 276), 'et' (79 vs 99), +# 'des' (75 vs 150), 'al' (78 vs 304), 'ye' (14 vs 70), +# 'da' (23 vs 298), "'n" (8 vs 12) +# +# I've left in the list 'i' (1939 vs 2151) and 'uno' (52 vs 56) +# I'm not sure what '-al' is, and so I've left it out... +# +# Generic list of articles in utf-8 encoding: +GENERIC_ARTICLES = ('the', 'la', 'a', 'die', 'der', 'le', 'el', + "l'", 'il', 'das', 'les', 'i', 'o', 'ein', 'un', 'de', 'los', + 'an', 'una', 'las', 'eine', 'den', 'het', 'gli', 'lo', 'os', + 'ang', 'oi', 'az', 'een', 'ha-', 'det', 'ta', 'al-', + 'mga', "un'", 'uno', 'ett', 'dem', 'egy', 'els', 'eines', + '\xc3\x8f', '\xc3\x87', '\xc3\x94\xc3\xaf', '\xc3\x8f\xc3\xa9') + + +# Lists of articles separated by language. If possible, the list should +# be sorted by frequency (not very important, but...) +# If you want to add a list of articles for another language, mail it +# it at imdbpy-devel@lists.sourceforge.net; non-ascii articles must be utf-8 +# encoded. +LANG_ARTICLES = { + 'English': ('the', 'a', 'an'), + 'Italian': ('la', 'le', "l'", 'il', 'i', 'un', 'una', 'gli', 'lo', "un'", + 'uno'), + 'Spanish': ('la', 'le', 'el', 'les', 'un', 'los', 'una', 'uno', 'unos', + 'unas'), + 'Portuguese': ('a', 'as', 'o', 'os', 'um', 'uns', 'uma', 'umas'), + 'Turkish': (), # Some languages doesn't have articles. +} +LANG_ARTICLESget = LANG_ARTICLES.get + + +# Maps a language to countries where it is the main language. +# If you want to add an entry for another language or country, mail it at +# imdbpy-devel@lists.sourceforge.net . +LANG_COUNTRIES = { + 'English': ('Canada', 'Swaziland', 'Ghana', 'St. Lucia', 'Liberia', 'Jamaica', 'Bahamas', 'New Zealand', 'Lesotho', 'Kenya', 'Solomon Islands', 'United States', 'South Africa', 'St. Vincent and the Grenadines', 'Fiji', 'UK', 'Nigeria', 'Australia', 'USA', 'St. Kitts and Nevis', 'Belize', 'Sierra Leone', 'Gambia', 'Namibia', 'Micronesia', 'Kiribati', 'Grenada', 'Antigua and Barbuda', 'Barbados', 'Malta', 'Zimbabwe', 'Ireland', 'Uganda', 'Trinidad and Tobago', 'South Sudan', 'Guyana', 'Botswana', 'United Kingdom', 'Zambia'), + 'Italian': ('Italy', 'San Marino', 'Vatican City'), + 'Spanish': ('Spain', 'Mexico', 'Argentina', 'Bolivia', 'Guatemala', 'Uruguay', 'Peru', 'Cuba', 'Dominican Republic', 'Panama', 'Costa Rica', 'Ecuador', 'El Salvador', 'Chile', 'Equatorial Guinea', 'Spain', 'Colombia', 'Nicaragua', 'Venezuela', 'Honduras', 'Paraguay'), + 'French': ('Cameroon', 'Burkina Faso', 'Dominica', 'Gabon', 'Monaco', 'France', "Cote d'Ivoire", 'Benin', 'Togo', 'Central African Republic', 'Mali', 'Niger', 'Congo, Republic of', 'Guinea', 'Congo, Democratic Republic of the', 'Luxembourg', 'Haiti', 'Chad', 'Burundi', 'Madagascar', 'Comoros', 'Senegal'), + 'Portuguese': ('Portugal', 'Brazil', 'Sao Tome and Principe', 'Cape Verde', 'Angola', 'Mozambique', 'Guinea-Bissau'), + 'German': ('Liechtenstein', 'Austria', 'West Germany', 'Switzerland', 'East Germany', 'Germany'), + 'Arabic': ('Saudi Arabia', 'Kuwait', 'Jordan', 'Oman', 'Yemen', 'United Arab Emirates', 'Mauritania', 'Lebanon', 'Bahrain', 'Libya', 'Palestinian State (proposed)', 'Qatar', 'Algeria', 'Morocco', 'Iraq', 'Egypt', 'Djibouti', 'Sudan', 'Syria', 'Tunisia'), + 'Turkish': ('Turkey', 'Azerbaijan'), + 'Swahili': ('Tanzania',), + 'Swedish': ('Sweden',), + 'Icelandic': ('Iceland',), + 'Estonian': ('Estonia',), + 'Romanian': ('Romania',), + 'Samoan': ('Samoa',), + 'Slovenian': ('Slovenia',), + 'Tok Pisin': ('Papua New Guinea',), + 'Palauan': ('Palau',), + 'Macedonian': ('Macedonia',), + 'Hindi': ('India',), + 'Dutch': ('Netherlands', 'Belgium', 'Suriname'), + 'Marshallese': ('Marshall Islands',), + 'Korean': ('Korea, North', 'Korea, South', 'North Korea', 'South Korea'), + 'Vietnamese': ('Vietnam',), + 'Danish': ('Denmark',), + 'Khmer': ('Cambodia',), + 'Lao': ('Laos',), + 'Somali': ('Somalia',), + 'Filipino': ('Philippines',), + 'Hungarian': ('Hungary',), + 'Ukrainian': ('Ukraine',), + 'Bosnian': ('Bosnia and Herzegovina',), + 'Georgian': ('Georgia',), + 'Lithuanian': ('Lithuania',), + 'Malay': ('Brunei',), + 'Tetum': ('East Timor',), + 'Norwegian': ('Norway',), + 'Armenian': ('Armenia',), + 'Russian': ('Russia',), + 'Slovak': ('Slovakia',), + 'Thai': ('Thailand',), + 'Croatian': ('Croatia',), + 'Turkmen': ('Turkmenistan',), + 'Nepali': ('Nepal',), + 'Finnish': ('Finland',), + 'Uzbek': ('Uzbekistan',), + 'Albanian': ('Albania', 'Kosovo'), + 'Hebrew': ('Israel',), + 'Bulgarian': ('Bulgaria',), + 'Greek': ('Cyprus', 'Greece'), + 'Burmese': ('Myanmar',), + 'Latvian': ('Latvia',), + 'Serbian': ('Serbia',), + 'Afar': ('Eritrea',), + 'Catalan': ('Andorra',), + 'Chinese': ('China', 'Taiwan'), + 'Czech': ('Czech Republic', 'Czechoslovakia'), + 'Bislama': ('Vanuatu',), + 'Japanese': ('Japan',), + 'Kinyarwanda': ('Rwanda',), + 'Amharic': ('Ethiopia',), + 'Persian': ('Afghanistan', 'Iran'), + 'Tajik': ('Tajikistan',), + 'Mongolian': ('Mongolia',), + 'Dzongkha': ('Bhutan',), + 'Urdu': ('Pakistan',), + 'Polish': ('Poland',), + 'Sinhala': ('Sri Lanka',), +} + +# Maps countries to their main language. +COUNTRY_LANG = {} +for lang in LANG_COUNTRIES: + for country in LANG_COUNTRIES[lang]: + COUNTRY_LANG[country] = lang + + +def toUnicode(articles): + """Convert a list of articles utf-8 encoded to unicode strings.""" + return tuple([art.decode('utf_8') for art in articles]) + + +def toDicts(articles): + """Given a list of utf-8 encoded articles, build two dictionary (one + utf-8 encoded and another one with unicode keys) for faster matches.""" + uArticles = toUnicode(articles) + return dict([(x, x) for x in articles]), dict([(x, x) for x in uArticles]) + + +def addTrailingSpace(articles): + """From the given list of utf-8 encoded articles, return two + lists (one utf-8 encoded and another one in unicode) where a space + is added at the end - if the last char is not ' or -.""" + _spArticles = [] + _spUnicodeArticles = [] + for article in articles: + if article[-1] not in ("'", '-'): + article += ' ' + _spArticles.append(article) + _spUnicodeArticles.append(article.decode('utf_8')) + return _spArticles, _spUnicodeArticles + + +# Caches. +_ART_CACHE = {} +_SP_ART_CACHE = {} + +def articlesDictsForLang(lang): + """Return dictionaries of articles specific for the given language, or the + default one if the language is not known.""" + if lang in _ART_CACHE: + return _ART_CACHE[lang] + artDicts = toDicts(LANG_ARTICLESget(lang, GENERIC_ARTICLES)) + _ART_CACHE[lang] = artDicts + return artDicts + + +def spArticlesForLang(lang): + """Return lists of articles (plus optional spaces) specific for the + given language, or the default one if the language is not known.""" + if lang in _SP_ART_CACHE: + return _SP_ART_CACHE[lang] + spArticles = addTrailingSpace(LANG_ARTICLESget(lang, GENERIC_ARTICLES)) + _SP_ART_CACHE[lang] = spArticles + return spArticles + diff --git a/lib/imdb/locale/__init__.py b/lib/imdb/locale/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9bc2e4668e8cf7d2545193f0f87812b6257f14b2 --- /dev/null +++ b/lib/imdb/locale/__init__.py @@ -0,0 +1,29 @@ +""" +locale package (imdb package). + +This package provides scripts and files for internationalization +of IMDbPY. + +Copyright 2009 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import gettext +import os + +LOCALE_DIR = os.path.dirname(__file__) + +gettext.bindtextdomain('imdbpy', LOCALE_DIR) diff --git a/lib/imdb/locale/generatepot.py b/lib/imdb/locale/generatepot.py new file mode 100644 index 0000000000000000000000000000000000000000..282f7d41e37390d208017235496fd489ee1999ec --- /dev/null +++ b/lib/imdb/locale/generatepot.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +""" +generatepot.py script. + +This script generates the imdbpy.pot file, from the DTD. + +Copyright 2009 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +import sys + +from datetime import datetime as dt + +DEFAULT_MESSAGES = { } + +ELEMENT_PATTERN = r"""<!ELEMENT\s+([^\s]+)""" +re_element = re.compile(ELEMENT_PATTERN) + +POT_HEADER_TEMPLATE = r"""# Gettext message file for imdbpy +msgid "" +msgstr "" +"Project-Id-Version: imdbpy\n" +"POT-Creation-Date: %(now)s\n" +"PO-Revision-Date: YYYY-MM-DD HH:MM+0000\n" +"Last-Translator: YOUR NAME <YOUR@EMAIL>\n" +"Language-Team: TEAM NAME <TEAM@EMAIL>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8\n" +"Domain: imdbpy\n" +""" + +if len(sys.argv) != 2: + print "Usage: %s dtd_file" % sys.argv[0] + sys.exit() + +dtdfilename = sys.argv[1] +dtd = open(dtdfilename).read() +elements = re_element.findall(dtd) +uniq = set(elements) +elements = list(uniq) + +print POT_HEADER_TEMPLATE % { + 'now': dt.strftime(dt.now(), "%Y-%m-%d %H:%M+0000") +} +for element in sorted(elements): + if element in DEFAULT_MESSAGES: + print '# Default: %s' % DEFAULT_MESSAGES[element] + else: + print '# Default: %s' % element.replace('-', ' ').capitalize() + print 'msgid "%s"' % element + print 'msgstr ""' + # use this part instead of the line above to generate the po file for English + #if element in DEFAULT_MESSAGES: + # print 'msgstr "%s"' % DEFAULT_MESSAGES[element] + #else: + # print 'msgstr "%s"' % element.replace('-', ' ').capitalize() + print + diff --git a/lib/imdb/locale/imdbpy-en.po b/lib/imdb/locale/imdbpy-en.po new file mode 100644 index 0000000000000000000000000000000000000000..3b3013c3a2a4880c6191833739cee1f523ce9df7 --- /dev/null +++ b/lib/imdb/locale/imdbpy-en.po @@ -0,0 +1,1257 @@ +# Gettext message file for imdbpy +msgid "" +msgstr "" +"Project-Id-Version: imdbpy\n" +"POT-Creation-Date: 2009-04-16 14:27+0000\n" +"PO-Revision-Date: YYYY-MM-DD HH:MM+0000\n" +"Last-Translator: YOUR NAME <YOUR@EMAIL>\n" +"Language-Team: TEAM NAME <TEAM@EMAIL>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8\n" +"Domain: imdbpy\n" + +# Default: Actor +msgid "actor" +msgstr "Actor" + +# Default: Actress +msgid "actress" +msgstr "Actress" + +# Default: Adaption +msgid "adaption" +msgstr "Adaption" + +# Default: Additional information +msgid "additional-information" +msgstr "Additional information" + +# Default: Admissions +msgid "admissions" +msgstr "Admissions" + +# Default: Agent address +msgid "agent-address" +msgstr "Agent address" + +# Default: Airing +msgid "airing" +msgstr "Airing" + +# Default: Akas +msgid "akas" +msgstr "Akas" + +# Default: All products +msgid "all-products" +msgstr "All products" + +# Default: Alternate language version of +msgid "alternate-language-version-of" +msgstr "Alternate language version of" + +# Default: Alternate versions +msgid "alternate-versions" +msgstr "Alternate versions" + +# Default: Amazon reviews +msgid "amazon-reviews" +msgstr "Amazon reviews" + +# Default: Analog left +msgid "analog-left" +msgstr "Analog left" + +# Default: Analog right +msgid "analog-right" +msgstr "Analog right" + +# Default: Animation department +msgid "animation-department" +msgstr "Animation department" + +# Default: Archive footage +msgid "archive-footage" +msgstr "Archive footage" + +# Default: Arithmetic mean +msgid "arithmetic-mean" +msgstr "Arithmetic mean" + +# Default: Art department +msgid "art-department" +msgstr "Art department" + +# Default: Art direction +msgid "art-direction" +msgstr "Art direction" + +# Default: Art director +msgid "art-director" +msgstr "Art director" + +# Default: Article +msgid "article" +msgstr "Article" + +# Default: Asin +msgid "asin" +msgstr "Asin" + +# Default: Aspect ratio +msgid "aspect-ratio" +msgstr "Aspect ratio" + +# Default: Assigner +msgid "assigner" +msgstr "Assigner" + +# Default: Assistant director +msgid "assistant-director" +msgstr "Assistant director" + +# Default: Auctions +msgid "auctions" +msgstr "Auctions" + +# Default: Audio noise +msgid "audio-noise" +msgstr "Audio noise" + +# Default: Audio quality +msgid "audio-quality" +msgstr "Audio quality" + +# Default: Award +msgid "award" +msgstr "Award" + +# Default: Awards +msgid "awards" +msgstr "Awards" + +# Default: Biographical movies +msgid "biographical-movies" +msgstr "Biographical movies" + +# Default: Biography +msgid "biography" +msgstr "Biography" + +# Default: Biography print +msgid "biography-print" +msgstr "Biography print" + +# Default: Birth date +msgid "birth-date" +msgstr "Birth date" + +# Default: Birth name +msgid "birth-name" +msgstr "Birth name" + +# Default: Birth notes +msgid "birth-notes" +msgstr "Birth notes" + +# Default: Body +msgid "body" +msgstr "Body" + +# Default: Book +msgid "book" +msgstr "Book" + +# Default: Books +msgid "books" +msgstr "Books" + +# Default: Bottom 100 rank +msgid "bottom-100-rank" +msgstr "Bottom 100 rank" + +# Default: Budget +msgid "budget" +msgstr "Budget" + +# Default: Business +msgid "business" +msgstr "Business" + +# Default: By arrangement with +msgid "by-arrangement-with" +msgstr "By arrangement with" + +# Default: Camera +msgid "camera" +msgstr "Camera" + +# Default: Camera and electrical department +msgid "camera-and-electrical-department" +msgstr "Camera and electrical department" + +# Default: Canonical episode title +msgid "canonical-episode-title" +msgstr "Canonical episode title" + +# Default: Canonical name +msgid "canonical-name" +msgstr "Canonical name" + +# Default: Canonical series title +msgid "canonical-series-title" +msgstr "Canonical series title" + +# Default: Canonical title +msgid "canonical-title" +msgstr "Canonical title" + +# Default: Cast +msgid "cast" +msgstr "Cast" + +# Default: Casting department +msgid "casting-department" +msgstr "Casting department" + +# Default: Casting director +msgid "casting-director" +msgstr "Casting director" + +# Default: Catalog number +msgid "catalog-number" +msgstr "Catalog number" + +# Default: Category +msgid "category" +msgstr "Category" + +# Default: Certificate +msgid "certificate" +msgstr "Certificate" + +# Default: Certificates +msgid "certificates" +msgstr "Certificates" + +# Default: Certification +msgid "certification" +msgstr "Certification" + +# Default: Channel +msgid "channel" +msgstr "Channel" + +# Default: Character +msgid "character" +msgstr "Character" + +# Default: Cinematographer +msgid "cinematographer" +msgstr "Cinematographer" + +# Default: Cinematographic process +msgid "cinematographic-process" +msgstr "Cinematographic process" + +# Default: Close captions teletext ld g +msgid "close-captions-teletext-ld-g" +msgstr "Close captions teletext ld g" + +# Default: Color info +msgid "color-info" +msgstr "Color info" + +# Default: Color information +msgid "color-information" +msgstr "Color information" + +# Default: Color rendition +msgid "color-rendition" +msgstr "Color rendition" + +# Default: Company +msgid "company" +msgstr "Company" + +# Default: Complete cast +msgid "complete-cast" +msgstr "Complete cast" + +# Default: Complete crew +msgid "complete-crew" +msgstr "Complete crew" + +# Default: Composer +msgid "composer" +msgstr "Composer" + +# Default: Connections +msgid "connections" +msgstr "Connections" + +# Default: Contrast +msgid "contrast" +msgstr "Contrast" + +# Default: Copyright holder +msgid "copyright-holder" +msgstr "Copyright holder" + +# Default: Costume department +msgid "costume-department" +msgstr "Costume department" + +# Default: Costume designer +msgid "costume-designer" +msgstr "Costume designer" + +# Default: Countries +msgid "countries" +msgstr "Countries" + +# Default: Country +msgid "country" +msgstr "Country" + +# Default: Courtesy of +msgid "courtesy-of" +msgstr "Courtesy of" + +# Default: Cover +msgid "cover" +msgstr "Cover" + +# Default: Cover url +msgid "cover-url" +msgstr "Cover url" + +# Default: Crazy credits +msgid "crazy-credits" +msgstr "Crazy credits" + +# Default: Creator +msgid "creator" +msgstr "Creator" + +# Default: Current role +msgid "current-role" +msgstr "Current role" + +# Default: Database +msgid "database" +msgstr "Database" + +# Default: Date +msgid "date" +msgstr "Date" + +# Default: Death date +msgid "death-date" +msgstr "Death date" + +# Default: Death notes +msgid "death-notes" +msgstr "Death notes" + +# Default: Demographic +msgid "demographic" +msgstr "Demographic" + +# Default: Description +msgid "description" +msgstr "Description" + +# Default: Dialogue intellegibility +msgid "dialogue-intellegibility" +msgstr "Dialogue intellegibility" + +# Default: Digital sound +msgid "digital-sound" +msgstr "Digital sound" + +# Default: Director +msgid "director" +msgstr "Director" + +# Default: Disc format +msgid "disc-format" +msgstr "Disc format" + +# Default: Disc size +msgid "disc-size" +msgstr "Disc size" + +# Default: Distributors +msgid "distributors" +msgstr "Distributors" + +# Default: Dvd +msgid "dvd" +msgstr "Dvd" + +# Default: Dvd features +msgid "dvd-features" +msgstr "Dvd features" + +# Default: Dvd format +msgid "dvd-format" +msgstr "Dvd format" + +# Default: Dvds +msgid "dvds" +msgstr "Dvds" + +# Default: Dynamic range +msgid "dynamic-range" +msgstr "Dynamic range" + +# Default: Edited from +msgid "edited-from" +msgstr "Edited from" + +# Default: Edited into +msgid "edited-into" +msgstr "Edited into" + +# Default: Editor +msgid "editor" +msgstr "Editor" + +# Default: Editorial department +msgid "editorial-department" +msgstr "Editorial department" + +# Default: Episode +msgid "episode" +msgstr "Episode" + +# Default: Episode of +msgid "episode-of" +msgstr "Episode of" + +# Default: Episode title +msgid "episode-title" +msgstr "Episode title" + +# Default: Episodes +msgid "episodes" +msgstr "Episodes" + +# Default: Episodes rating +msgid "episodes-rating" +msgstr "Episodes rating" + +# Default: Essays +msgid "essays" +msgstr "Essays" + +# Default: External reviews +msgid "external-reviews" +msgstr "External reviews" + +# Default: Faqs +msgid "faqs" +msgstr "Faqs" + +# Default: Featured in +msgid "featured-in" +msgstr "Featured in" + +# Default: Features +msgid "features" +msgstr "Features" + +# Default: Film negative format +msgid "film-negative-format" +msgstr "Film negative format" + +# Default: Filming dates +msgid "filming-dates" +msgstr "Filming dates" + +# Default: Filmography +msgid "filmography" +msgstr "Filmography" + +# Default: Followed by +msgid "followed-by" +msgstr "Followed by" + +# Default: Follows +msgid "follows" +msgstr "Follows" + +# Default: For +msgid "for" +msgstr "For" + +# Default: Frequency response +msgid "frequency-response" +msgstr "Frequency response" + +# Default: From +msgid "from" +msgstr "From" + +# Default: Full article link +msgid "full-article-link" +msgstr "Full article link" + +# Default: Genres +msgid "genres" +msgstr "Genres" + +# Default: Goofs +msgid "goofs" +msgstr "Goofs" + +# Default: Gross +msgid "gross" +msgstr "Gross" + +# Default: Group genre +msgid "group-genre" +msgstr "Group genre" + +# Default: Headshot +msgid "headshot" +msgstr "Headshot" + +# Default: Height +msgid "height" +msgstr "Height" + +# Default: Imdbindex +msgid "imdbindex" +msgstr "Imdbindex" + +# Default: Interview +msgid "interview" +msgstr "Interview" + +# Default: Interviews +msgid "interviews" +msgstr "Interviews" + +# Default: Introduction +msgid "introduction" +msgstr "Introduction" + +# Default: Item +msgid "item" +msgstr "Item" + +# Default: Keywords +msgid "keywords" +msgstr "Keywords" + +# Default: Kind +msgid "kind" +msgstr "Kind" + +# Default: Label +msgid "label" +msgstr "Label" + +# Default: Laboratory +msgid "laboratory" +msgstr "Laboratory" + +# Default: Language +msgid "language" +msgstr "Language" + +# Default: Languages +msgid "languages" +msgstr "Languages" + +# Default: Laserdisc +msgid "laserdisc" +msgstr "Laserdisc" + +# Default: Laserdisc title +msgid "laserdisc-title" +msgstr "Laserdisc title" + +# Default: Length +msgid "length" +msgstr "Length" + +# Default: Line +msgid "line" +msgstr "Line" + +# Default: Link +msgid "link" +msgstr "Link" + +# Default: Link text +msgid "link-text" +msgstr "Link text" + +# Default: Literature +msgid "literature" +msgstr "Literature" + +# Default: Locations +msgid "locations" +msgstr "Locations" + +# Default: Long imdb canonical name +msgid "long-imdb-canonical-name" +msgstr "Long imdb canonical name" + +# Default: Long imdb canonical title +msgid "long-imdb-canonical-title" +msgstr "Long imdb canonical title" + +# Default: Long imdb episode title +msgid "long-imdb-episode-title" +msgstr "Long imdb episode title" + +# Default: Long imdb name +msgid "long-imdb-name" +msgstr "Long imdb name" + +# Default: Long imdb title +msgid "long-imdb-title" +msgstr "Long imdb title" + +# Default: Magazine cover photo +msgid "magazine-cover-photo" +msgstr "Magazine cover photo" + +# Default: Make up +msgid "make-up" +msgstr "Make up" + +# Default: Master format +msgid "master-format" +msgstr "Master format" + +# Default: Median +msgid "median" +msgstr "Median" + +# Default: Merchandising links +msgid "merchandising-links" +msgstr "Merchandising links" + +# Default: Mini biography +msgid "mini-biography" +msgstr "Mini biography" + +# Default: Misc links +msgid "misc-links" +msgstr "Misc links" + +# Default: Miscellaneous companies +msgid "miscellaneous-companies" +msgstr "Miscellaneous companies" + +# Default: Miscellaneous crew +msgid "miscellaneous-crew" +msgstr "Miscellaneous crew" + +# Default: Movie +msgid "movie" +msgstr "Movie" + +# Default: Mpaa +msgid "mpaa" +msgstr "Mpaa" + +# Default: Music department +msgid "music-department" +msgstr "Music department" + +# Default: Name +msgid "name" +msgstr "Name" + +# Default: News +msgid "news" +msgstr "News" + +# Default: Newsgroup reviews +msgid "newsgroup-reviews" +msgstr "Newsgroup reviews" + +# Default: Nick names +msgid "nick-names" +msgstr "Nick names" + +# Default: Notes +msgid "notes" +msgstr "Notes" + +# Default: Novel +msgid "novel" +msgstr "Novel" + +# Default: Number +msgid "number" +msgstr "Number" + +# Default: Number of chapter stops +msgid "number-of-chapter-stops" +msgstr "Number of chapter stops" + +# Default: Number of episodes +msgid "number-of-episodes" +msgstr "Number of episodes" + +# Default: Number of seasons +msgid "number-of-seasons" +msgstr "Number of seasons" + +# Default: Number of sides +msgid "number-of-sides" +msgstr "Number of sides" + +# Default: Number of votes +msgid "number-of-votes" +msgstr "Number of votes" + +# Default: Official retail price +msgid "official-retail-price" +msgstr "Official retail price" + +# Default: Official sites +msgid "official-sites" +msgstr "Official sites" + +# Default: Opening weekend +msgid "opening-weekend" +msgstr "Opening weekend" + +# Default: Original air date +msgid "original-air-date" +msgstr "Original air date" + +# Default: Original music +msgid "original-music" +msgstr "Original music" + +# Default: Original title +msgid "original-title" +msgstr "Original title" + +# Default: Other literature +msgid "other-literature" +msgstr "Other literature" + +# Default: Other works +msgid "other-works" +msgstr "Other works" + +# Default: Parents guide +msgid "parents-guide" +msgstr "Parents guide" + +# Default: Performed by +msgid "performed-by" +msgstr "Performed by" + +# Default: Person +msgid "person" +msgstr "Person" + +# Default: Photo sites +msgid "photo-sites" +msgstr "Photo sites" + +# Default: Pictorial +msgid "pictorial" +msgstr "Pictorial" + +# Default: Picture format +msgid "picture-format" +msgstr "Picture format" + +# Default: Plot +msgid "plot" +msgstr "Plot" + +# Default: Plot outline +msgid "plot-outline" +msgstr "Plot outline" + +# Default: Portrayed in +msgid "portrayed-in" +msgstr "Portrayed in" + +# Default: Pressing plant +msgid "pressing-plant" +msgstr "Pressing plant" + +# Default: Printed film format +msgid "printed-film-format" +msgstr "Printed film format" + +# Default: Printed media reviews +msgid "printed-media-reviews" +msgstr "Printed media reviews" + +# Default: Producer +msgid "producer" +msgstr "Producer" + +# Default: Production companies +msgid "production-companies" +msgstr "Production companies" + +# Default: Production country +msgid "production-country" +msgstr "Production country" + +# Default: Production dates +msgid "production-dates" +msgstr "Production dates" + +# Default: Production design +msgid "production-design" +msgstr "Production design" + +# Default: Production designer +msgid "production-designer" +msgstr "Production designer" + +# Default: Production manager +msgid "production-manager" +msgstr "Production manager" + +# Default: Production process protocol +msgid "production-process-protocol" +msgstr "Production process protocol" + +# Default: Quality of source +msgid "quality-of-source" +msgstr "Quality of source" + +# Default: Quality program +msgid "quality-program" +msgstr "Quality program" + +# Default: Quote +msgid "quote" +msgstr "Quote" + +# Default: Quotes +msgid "quotes" +msgstr "Quotes" + +# Default: Rating +msgid "rating" +msgstr "Rating" + +# Default: Recommendations +msgid "recommendations" +msgstr "Recommendations" + +# Default: Referenced in +msgid "referenced-in" +msgstr "Referenced in" + +# Default: References +msgid "references" +msgstr "References" + +# Default: Region +msgid "region" +msgstr "Region" + +# Default: Release country +msgid "release-country" +msgstr "Release country" + +# Default: Release date +msgid "release-date" +msgstr "Release date" + +# Default: Release dates +msgid "release-dates" +msgstr "Release dates" + +# Default: Remade as +msgid "remade-as" +msgstr "Remade as" + +# Default: Remake of +msgid "remake-of" +msgstr "Remake of" + +# Default: Rentals +msgid "rentals" +msgstr "Rentals" + +# Default: Result +msgid "result" +msgstr "Result" + +# Default: Review +msgid "review" +msgstr "Review" + +# Default: Review author +msgid "review-author" +msgstr "Review author" + +# Default: Review kind +msgid "review-kind" +msgstr "Review kind" + +# Default: Runtime +msgid "runtime" +msgstr "Runtime" + +# Default: Runtimes +msgid "runtimes" +msgstr "Runtimes" + +# Default: Salary history +msgid "salary-history" +msgstr "Salary history" + +# Default: Screenplay teleplay +msgid "screenplay-teleplay" +msgstr "Screenplay teleplay" + +# Default: Season +msgid "season" +msgstr "Season" + +# Default: Second unit director or assistant director +msgid "second-unit-director-or-assistant-director" +msgstr "Second unit director or assistant director" + +# Default: Self +msgid "self" +msgstr "Self" + +# Default: Series animation department +msgid "series-animation-department" +msgstr "Series animation department" + +# Default: Series art department +msgid "series-art-department" +msgstr "Series art department" + +# Default: Series assistant directors +msgid "series-assistant-directors" +msgstr "Series assistant directors" + +# Default: Series camera department +msgid "series-camera-department" +msgstr "Series camera department" + +# Default: Series casting department +msgid "series-casting-department" +msgstr "Series casting department" + +# Default: Series cinematographers +msgid "series-cinematographers" +msgstr "Series cinematographers" + +# Default: Series costume department +msgid "series-costume-department" +msgstr "Series costume department" + +# Default: Series editorial department +msgid "series-editorial-department" +msgstr "Series editorial department" + +# Default: Series editors +msgid "series-editors" +msgstr "Series editors" + +# Default: Series make up department +msgid "series-make-up-department" +msgstr "Series make up department" + +# Default: Series miscellaneous +msgid "series-miscellaneous" +msgstr "Series miscellaneous" + +# Default: Series music department +msgid "series-music-department" +msgstr "Series music department" + +# Default: Series producers +msgid "series-producers" +msgstr "Series producers" + +# Default: Series production designers +msgid "series-production-designers" +msgstr "Series production designers" + +# Default: Series production managers +msgid "series-production-managers" +msgstr "Series production managers" + +# Default: Series sound department +msgid "series-sound-department" +msgstr "Series sound department" + +# Default: Series special effects department +msgid "series-special-effects-department" +msgstr "Series special effects department" + +# Default: Series stunts +msgid "series-stunts" +msgstr "Series stunts" + +# Default: Series title +msgid "series-title" +msgstr "Series title" + +# Default: Series transportation department +msgid "series-transportation-department" +msgstr "Series transportation department" + +# Default: Series visual effects department +msgid "series-visual-effects-department" +msgstr "Series visual effects department" + +# Default: Series writers +msgid "series-writers" +msgstr "Series writers" + +# Default: Series years +msgid "series-years" +msgstr "Series years" + +# Default: Set decoration +msgid "set-decoration" +msgstr "Set decoration" + +# Default: Sharpness +msgid "sharpness" +msgstr "Sharpness" + +# Default: Similar to +msgid "similar-to" +msgstr "Similar to" + +# Default: Sound clips +msgid "sound-clips" +msgstr "Sound clips" + +# Default: Sound crew +msgid "sound-crew" +msgstr "Sound crew" + +# Default: Sound encoding +msgid "sound-encoding" +msgstr "Sound encoding" + +# Default: Sound mix +msgid "sound-mix" +msgstr "Sound mix" + +# Default: Soundtrack +msgid "soundtrack" +msgstr "Soundtrack" + +# Default: Spaciality +msgid "spaciality" +msgstr "Spaciality" + +# Default: Special effects +msgid "special-effects" +msgstr "Special effects" + +# Default: Special effects companies +msgid "special-effects-companies" +msgstr "Special effects companies" + +# Default: Special effects department +msgid "special-effects-department" +msgstr "Special effects department" + +# Default: Spin off +msgid "spin-off" +msgstr "Spin off" + +# Default: Spin off from +msgid "spin-off-from" +msgstr "Spin off from" + +# Default: Spoofed in +msgid "spoofed-in" +msgstr "Spoofed in" + +# Default: Spoofs +msgid "spoofs" +msgstr "Spoofs" + +# Default: Spouse +msgid "spouse" +msgstr "Spouse" + +# Default: Status of availablility +msgid "status-of-availablility" +msgstr "Status of availablility" + +# Default: Studio +msgid "studio" +msgstr "Studio" + +# Default: Studios +msgid "studios" +msgstr "Studios" + +# Default: Stunt performer +msgid "stunt-performer" +msgstr "Stunt performer" + +# Default: Stunts +msgid "stunts" +msgstr "Stunts" + +# Default: Subtitles +msgid "subtitles" +msgstr "Subtitles" + +# Default: Supplement +msgid "supplement" +msgstr "Supplement" + +# Default: Supplements +msgid "supplements" +msgstr "Supplements" + +# Default: Synopsis +msgid "synopsis" +msgstr "Synopsis" + +# Default: Taglines +msgid "taglines" +msgstr "Taglines" + +# Default: Tech info +msgid "tech-info" +msgstr "Tech info" + +# Default: Thanks +msgid "thanks" +msgstr "Thanks" + +# Default: Time +msgid "time" +msgstr "Time" + +# Default: Title +msgid "title" +msgstr "Title" + +# Default: Titles in this product +msgid "titles-in-this-product" +msgstr "Titles in this product" + +# Default: To +msgid "to" +msgstr "To" + +# Default: Top 250 rank +msgid "top-250-rank" +msgstr "Top 250 rank" + +# Default: Trade mark +msgid "trade-mark" +msgstr "Trade mark" + +# Default: Transportation department +msgid "transportation-department" +msgstr "Transportation department" + +# Default: Trivia +msgid "trivia" +msgstr "Trivia" + +# Default: Under license from +msgid "under-license-from" +msgstr "Under license from" + +# Default: Unknown link +msgid "unknown-link" +msgstr "Unknown link" + +# Default: Upc +msgid "upc" +msgstr "Upc" + +# Default: Version of +msgid "version-of" +msgstr "Version of" + +# Default: Vhs +msgid "vhs" +msgstr "Vhs" + +# Default: Video artifacts +msgid "video-artifacts" +msgstr "Video artifacts" + +# Default: Video clips +msgid "video-clips" +msgstr "Video clips" + +# Default: Video noise +msgid "video-noise" +msgstr "Video noise" + +# Default: Video quality +msgid "video-quality" +msgstr "Video quality" + +# Default: Video standard +msgid "video-standard" +msgstr "Video standard" + +# Default: Visual effects +msgid "visual-effects" +msgstr "Visual effects" + +# Default: Votes +msgid "votes" +msgstr "Votes" + +# Default: Votes distribution +msgid "votes-distribution" +msgstr "Votes distribution" + +# Default: Weekend gross +msgid "weekend-gross" +msgstr "Weekend gross" + +# Default: Where now +msgid "where-now" +msgstr "Where now" + +# Default: With +msgid "with" +msgstr "With" + +# Default: Writer +msgid "writer" +msgstr "Writer" + +# Default: Written by +msgid "written-by" +msgstr "Written by" + +# Default: Year +msgid "year" +msgstr "Year" + +# Default: Zshops +msgid "zshops" +msgstr "Zshops" + diff --git a/lib/imdb/locale/imdbpy-it.po b/lib/imdb/locale/imdbpy-it.po new file mode 100644 index 0000000000000000000000000000000000000000..17cfce469b491b409c76b0ceb96132ca90582f7c --- /dev/null +++ b/lib/imdb/locale/imdbpy-it.po @@ -0,0 +1,1300 @@ +# Gettext message file for imdbpy +msgid "" +msgstr "" +"Project-Id-Version: imdbpy\n" +"POT-Creation-Date: 2010-03-18 14:35+0000\n" +"PO-Revision-Date: 2009-07-03 13:00+0000\n" +"Last-Translator: Davide Alberani <da@erlug.linux.it>\n" +"Language-Team: Davide Alberani <da@erlug.linux.it>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language-Code: it\n" +"Language-Name: Italian\n" +"Preferred-Encodings: utf-8\n" +"Domain: imdbpy\n" + +# Default: Actor +msgid "actor" +msgstr "Attore" + +# Default: Actress +msgid "actress" +msgstr "Attrice" + +# Default: Adaption +msgid "adaption" +msgstr "Adattamento" + +# Default: Additional information +msgid "additional-information" +msgstr "Ulteriori informazioni" + +# Default: Admissions +msgid "admissions" +msgstr "Biglietti venduti" + +# Default: Agent address +msgid "agent-address" +msgstr "Indirizzo dell'agente" + +# Default: Airing +msgid "airing" +msgstr "In onda" + +# Default: Akas +msgid "akas" +msgstr "Alias" + +# Default: Akas from release info +msgid "akas-from-release-info" +msgstr "Alias dalle informazioni di rilascio" + +# Default: All products +msgid "all-products" +msgstr "Tutti i prodotti" + +# Default: Alternate language version of +msgid "alternate-language-version-of" +msgstr "Versione in altra lingua di" + +# Default: Alternate versions +msgid "alternate-versions" +msgstr "Versioni alternative" + +# Default: Amazon reviews +msgid "amazon-reviews" +msgstr "Recensione di Amazon" + +# Default: Analog left +msgid "analog-left" +msgstr "Analogico sinistro" + +# Default: Analog right +msgid "analog-right" +msgstr "Analogico destro" + +# Default: Animation department +msgid "animation-department" +msgstr "Dipartimento animazione" + +# Default: Archive footage +msgid "archive-footage" +msgstr "Materiale d'archivio" + +# Default: Arithmetic mean +msgid "arithmetic-mean" +msgstr "Media aritmetica" + +# Default: Art department +msgid "art-department" +msgstr "Dipartimento artistico" + +# Default: Art direction +msgid "art-direction" +msgstr "Direzione artistica" + +# Default: Art director +msgid "art-director" +msgstr "Direttore artistico" + +# Default: Article +msgid "article" +msgstr "Articolo" + +# Default: Asin +msgid "asin" +msgstr "Asin" + +# Default: Aspect ratio +msgid "aspect-ratio" +msgstr "Rapporto d'aspetto" + +# Default: Assigner +msgid "assigner" +msgstr "Assegnatario" + +# Default: Assistant director +msgid "assistant-director" +msgstr "Assistente regista" + +# Default: Auctions +msgid "auctions" +msgstr "Aste" + +# Default: Audio noise +msgid "audio-noise" +msgstr "Rumore audio" + +# Default: Audio quality +msgid "audio-quality" +msgstr "Qualità audio" + +# Default: Award +msgid "award" +msgstr "Premio" + +# Default: Awards +msgid "awards" +msgstr "Premi" + +# Default: Biographical movies +msgid "biographical-movies" +msgstr "Film biografici" + +# Default: Biography +msgid "biography" +msgstr "Biografia" + +# Default: Biography print +msgid "biography-print" +msgstr "Biografia" + +# Default: Birth date +msgid "birth-date" +msgstr "Data di nascita" + +# Default: Birth name +msgid "birth-name" +msgstr "Nome di nascita" + +# Default: Birth notes +msgid "birth-notes" +msgstr "Note di nascita" + +# Default: Body +msgid "body" +msgstr "Corpo" + +# Default: Book +msgid "book" +msgstr "Libro" + +# Default: Books +msgid "books" +msgstr "Libri" + +# Default: Bottom 100 rank +msgid "bottom-100-rank" +msgstr "Posizione nella bottom 100" + +# Default: Budget +msgid "budget" +msgstr "Bilancio" + +# Default: Business +msgid "business" +msgstr "Affari" + +# Default: By arrangement with +msgid "by-arrangement-with" +msgstr "Arrangiamento con" + +# Default: Camera +msgid "camera" +msgstr "Cinepresa" + +# Default: Camera and electrical department +msgid "camera-and-electrical-department" +msgstr "Cinepresa e dipartimento elettrico" + +# Default: Canonical episode title +msgid "canonical-episode-title" +msgstr "Titolo dell'episodio in forma canonica" + +# Default: Canonical name +msgid "canonical-name" +msgstr "Nome in forma canonica" + +# Default: Canonical series title +msgid "canonical-series-title" +msgstr "Titolo della serie in forma canonica" + +# Default: Canonical title +msgid "canonical-title" +msgstr "Titolo in forma canonica" + +# Default: Cast +msgid "cast" +msgstr "Cast" + +# Default: Casting department +msgid "casting-department" +msgstr "Casting" + +# Default: Casting director +msgid "casting-director" +msgstr "Direttore del casting" + +# Default: Catalog number +msgid "catalog-number" +msgstr "Numero di catalogo" + +# Default: Category +msgid "category" +msgstr "Categoria" + +# Default: Certificate +msgid "certificate" +msgstr "Certificazione" + +# Default: Certificates +msgid "certificates" +msgstr "Certificazioni" + +# Default: Certification +msgid "certification" +msgstr "Certificazioni" + +# Default: Channel +msgid "channel" +msgstr "Canale" + +# Default: Character +msgid "character" +msgstr "Personaggio" + +# Default: Cinematographer +msgid "cinematographer" +msgstr "Fotografia" + +# Default: Cinematographic process +msgid "cinematographic-process" +msgstr "Processo cinematografico" + +# Default: Close captions teletext ld g +msgid "close-captions-teletext-ld-g" +msgstr "" + +# Default: Color info +msgid "color-info" +msgstr "Colore" + +# Default: Color information +msgid "color-information" +msgstr "Informazioni sul colore" + +# Default: Color rendition +msgid "color-rendition" +msgstr "Resa dei colori" + +# Default: Company +msgid "company" +msgstr "Compagnia" + +# Default: Complete cast +msgid "complete-cast" +msgstr "Cast completo" + +# Default: Complete crew +msgid "complete-crew" +msgstr "Troupe completa" + +# Default: Composer +msgid "composer" +msgstr "Compositore" + +# Default: Connections +msgid "connections" +msgstr "Collegamenti" + +# Default: Contrast +msgid "contrast" +msgstr "Contrasto" + +# Default: Copyright holder +msgid "copyright-holder" +msgstr "Detentore dei diritti d'autore" + +# Default: Costume department +msgid "costume-department" +msgstr "Dipartimento costumi" + +# Default: Costume designer +msgid "costume-designer" +msgstr "Costumista" + +# Default: Countries +msgid "countries" +msgstr "Paesi" + +# Default: Country +msgid "country" +msgstr "Paese" + +# Default: Courtesy of +msgid "courtesy-of" +msgstr "Cortesia di" + +# Default: Cover +msgid "cover" +msgstr "Copertina" + +# Default: Cover url +msgid "cover-url" +msgstr "Locandina" + +# Default: Crazy credits +msgid "crazy-credits" +msgstr "Titoli pazzi" + +# Default: Creator +msgid "creator" +msgstr "Creatore" + +# Default: Current role +msgid "current-role" +msgstr "Ruolo" + +# Default: Database +msgid "database" +msgstr "Database" + +# Default: Date +msgid "date" +msgstr "Data" + +# Default: Death date +msgid "death-date" +msgstr "Data di morte" + +# Default: Death notes +msgid "death-notes" +msgstr "Note di morte" + +# Default: Demographic +msgid "demographic" +msgstr "Spaccato demografico" + +# Default: Description +msgid "description" +msgstr "Descrizione" + +# Default: Dialogue intellegibility +msgid "dialogue-intellegibility" +msgstr "Comprensibilità dei dialoghi" + +# Default: Digital sound +msgid "digital-sound" +msgstr "Suono digitale" + +# Default: Director +msgid "director" +msgstr "Regista" + +# Default: Disc format +msgid "disc-format" +msgstr "Formato del disco" + +# Default: Disc size +msgid "disc-size" +msgstr "Dimensione del disco" + +# Default: Distributors +msgid "distributors" +msgstr "Distributori" + +# Default: Dvd +msgid "dvd" +msgstr "Dvd" + +# Default: Dvd features +msgid "dvd-features" +msgstr "Caratteristiche del DVD" + +# Default: Dvd format +msgid "dvd-format" +msgstr "Formato del DVD" + +# Default: Dvds +msgid "dvds" +msgstr "Dvd" + +# Default: Dynamic range +msgid "dynamic-range" +msgstr "Intervallo dinamico" + +# Default: Edited from +msgid "edited-from" +msgstr "Tratto da" + +# Default: Edited into +msgid "edited-into" +msgstr "Montato in" + +# Default: Editor +msgid "editor" +msgstr "Editore" + +# Default: Editorial department +msgid "editorial-department" +msgstr "Dipartimento editoriale" + +# Default: Episode +msgid "episode" +msgstr "Episodio" + +# Default: Episode of +msgid "episode-of" +msgstr "Episodio di" + +# Default: Episode title +msgid "episode-title" +msgstr "Titolo dell'episodio" + +# Default: Episodes +msgid "episodes" +msgstr "Episodi" + +# Default: Episodes rating +msgid "episodes-rating" +msgstr "Voto degli episodi" + +# Default: Essays +msgid "essays" +msgstr "Saggi" + +# Default: External reviews +msgid "external-reviews" +msgstr "Recensioni esterne" + +# Default: Faqs +msgid "faqs" +msgstr "Domande ricorrenti" + +# Default: Feature +msgid "feature" +msgstr "Caratteristica" + +# Default: Featured in +msgid "featured-in" +msgstr "Ripreso in" + +# Default: Features +msgid "features" +msgstr "Caratteristiche" + +# Default: Film negative format +msgid "film-negative-format" +msgstr "Formato del negativo" + +# Default: Filming dates +msgid "filming-dates" +msgstr "Data delle riprese" + +# Default: Filmography +msgid "filmography" +msgstr "Filmografia" + +# Default: Followed by +msgid "followed-by" +msgstr "Seguito da" + +# Default: Follows +msgid "follows" +msgstr "Segue" + +# Default: For +msgid "for" +msgstr "Per" + +# Default: Frequency response +msgid "frequency-response" +msgstr "Frequenze di risposta" + +# Default: From +msgid "from" +msgstr "Da" + +# Default: Full article link +msgid "full-article-link" +msgstr "Collegamento all'articolo completo" + +# Default: Full size cover url +msgid "full-size-cover-url" +msgstr "URL della copertina nelle dimensioni originali" + +# Default: Full size headshot +msgid "full-size-headshot" +msgstr "Ritratto nelle dimensioni originali" + +# Default: Genres +msgid "genres" +msgstr "Generi" + +# Default: Goofs +msgid "goofs" +msgstr "Errori" + +# Default: Gross +msgid "gross" +msgstr "Lordo" + +# Default: Group genre +msgid "group-genre" +msgstr "" + +# Default: Headshot +msgid "headshot" +msgstr "Foto" + +# Default: Height +msgid "height" +msgstr "Altezza" + +# Default: Imdbindex +msgid "imdbindex" +msgstr "" + +# Default: In development +msgid "in-development" +msgstr "In sviluppo" + +# Default: Interview +msgid "interview" +msgstr "Intervista" + +# Default: Interviews +msgid "interviews" +msgstr "Interviste" + +# Default: Introduction +msgid "introduction" +msgstr "Introduzione" + +# Default: Item +msgid "item" +msgstr "Elemento" + +# Default: Keywords +msgid "keywords" +msgstr "Parole chiave" + +# Default: Kind +msgid "kind" +msgstr "Tipo" + +# Default: Label +msgid "label" +msgstr "Etichetta" + +# Default: Laboratory +msgid "laboratory" +msgstr "Laboratorio" + +# Default: Language +msgid "language" +msgstr "Lingua" + +# Default: Languages +msgid "languages" +msgstr "Lingue" + +# Default: Laserdisc +msgid "laserdisc" +msgstr "Laserdisc" + +# Default: Laserdisc title +msgid "laserdisc-title" +msgstr "Titolo del laserdisc" + +# Default: Length +msgid "length" +msgstr "Durata" + +# Default: Line +msgid "line" +msgstr "Battuta" + +# Default: Link +msgid "link" +msgstr "Collegamento" + +# Default: Link text +msgid "link-text" +msgstr "Testo del link" + +# Default: Literature +msgid "literature" +msgstr "Letteratura" + +# Default: Locations +msgid "locations" +msgstr "Luoghi" + +# Default: Long imdb canonical name +msgid "long-imdb-canonical-name" +msgstr "Nome canonico IMDb lungo" + +# Default: Long imdb canonical title +msgid "long-imdb-canonical-title" +msgstr "Titolo canonico IMDb lungo" + +# Default: Long imdb episode title +msgid "long-imdb-episode-title" +msgstr "Titolo dell'episodio canonico IMDb lungo" + +# Default: Long imdb name +msgid "long-imdb-name" +msgstr "Nome IMDb lungo" + +# Default: Long imdb title +msgid "long-imdb-title" +msgstr "Titolo IMDb lungo" + +# Default: Magazine cover photo +msgid "magazine-cover-photo" +msgstr "Foto di copertina" + +# Default: Make up +msgid "make-up" +msgstr "Trucco" + +# Default: Master format +msgid "master-format" +msgstr "Formato del master" + +# Default: Median +msgid "median" +msgstr "Mediana" + +# Default: Merchandising links +msgid "merchandising-links" +msgstr "Collegamenti al merchandising" + +# Default: Mini biography +msgid "mini-biography" +msgstr "Biografia" + +# Default: Misc links +msgid "misc-links" +msgstr "Altri collegamenti" + +# Default: Miscellaneous companies +msgid "miscellaneous-companies" +msgstr "Altre compagnie" + +# Default: Miscellaneous crew +msgid "miscellaneous-crew" +msgstr "Altra troupe" + +# Default: Movie +msgid "movie" +msgstr "Film" + +# Default: Mpaa +msgid "mpaa" +msgstr "Visto MPAA" + +# Default: Music department +msgid "music-department" +msgstr "Dipartimento musicale" + +# Default: Name +msgid "name" +msgstr "Nome" + +# Default: News +msgid "news" +msgstr "Notizie" + +# Default: Newsgroup reviews +msgid "newsgroup-reviews" +msgstr "Recensioni dai gruppi di discussione" + +# Default: Nick names +msgid "nick-names" +msgstr "Soprannomi" + +# Default: Notes +msgid "notes" +msgstr "Note" + +# Default: Novel +msgid "novel" +msgstr "Novella" + +# Default: Number +msgid "number" +msgstr "Numero" + +# Default: Number of chapter stops +msgid "number-of-chapter-stops" +msgstr "Numero di interruzioni di capitolo" + +# Default: Number of episodes +msgid "number-of-episodes" +msgstr "Numero di episodi" + +# Default: Number of seasons +msgid "number-of-seasons" +msgstr "Numero di stagioni" + +# Default: Number of sides +msgid "number-of-sides" +msgstr "Numero di lati" + +# Default: Number of votes +msgid "number-of-votes" +msgstr "Numero di voti" + +# Default: Official retail price +msgid "official-retail-price" +msgstr "Prezzo ufficiale al pubblico" + +# Default: Official sites +msgid "official-sites" +msgstr "Siti ufficiali" + +# Default: Opening weekend +msgid "opening-weekend" +msgstr "Weekend d'apertura" + +# Default: Original air date +msgid "original-air-date" +msgstr "Data della prima messa in onda" + +# Default: Original music +msgid "original-music" +msgstr "Musica originale" + +# Default: Original title +msgid "original-title" +msgstr "Titolo originale" + +# Default: Other literature +msgid "other-literature" +msgstr "Altre opere letterarie" + +# Default: Other works +msgid "other-works" +msgstr "Altri lavori" + +# Default: Parents guide +msgid "parents-guide" +msgstr "Guida per i genitori" + +# Default: Performed by +msgid "performed-by" +msgstr "Eseguito da" + +# Default: Person +msgid "person" +msgstr "Persona" + +# Default: Photo sites +msgid "photo-sites" +msgstr "Siti con fotografie" + +# Default: Pictorial +msgid "pictorial" +msgstr "Ritratto" + +# Default: Picture format +msgid "picture-format" +msgstr "Formato dell'immagine" + +# Default: Plot +msgid "plot" +msgstr "Trama" + +# Default: Plot outline +msgid "plot-outline" +msgstr "Trama in breve" + +# Default: Portrayed in +msgid "portrayed-in" +msgstr "Rappresentato in" + +# Default: Pressing plant +msgid "pressing-plant" +msgstr "Impianto di stampa" + +# Default: Printed film format +msgid "printed-film-format" +msgstr "Formato della pellicola" + +# Default: Printed media reviews +msgid "printed-media-reviews" +msgstr "Recensioni su carta stampata" + +# Default: Producer +msgid "producer" +msgstr "Produttore" + +# Default: Production companies +msgid "production-companies" +msgstr "Compagnie di produzione" + +# Default: Production country +msgid "production-country" +msgstr "Paese di produzione" + +# Default: Production dates +msgid "production-dates" +msgstr "Date di produzione" + +# Default: Production design +msgid "production-design" +msgstr "Design di produzione" + +# Default: Production designer +msgid "production-designer" +msgstr "Designer di produzione" + +# Default: Production manager +msgid "production-manager" +msgstr "Manager di produzione" + +# Default: Production process protocol +msgid "production-process-protocol" +msgstr "Controllo del processo di produzione" + +# Default: Quality of source +msgid "quality-of-source" +msgstr "Qualità dell'originale" + +# Default: Quality program +msgid "quality-program" +msgstr "Programma di Qualità " + +# Default: Quote +msgid "quote" +msgstr "Citazione" + +# Default: Quotes +msgid "quotes" +msgstr "Citazioni" + +# Default: Rating +msgid "rating" +msgstr "Voto" + +# Default: Recommendations +msgid "recommendations" +msgstr "Raccomandazioni" + +# Default: Referenced in +msgid "referenced-in" +msgstr "Citato in" + +# Default: References +msgid "references" +msgstr "Cita" + +# Default: Region +msgid "region" +msgstr "Regione" + +# Default: Release country +msgid "release-country" +msgstr "Paese d'uscita" + +# Default: Release date +msgid "release-date" +msgstr "Data d'uscita" + +# Default: Release dates +msgid "release-dates" +msgstr "Date d'uscita" + +# Default: Remade as +msgid "remade-as" +msgstr "Rifatto come" + +# Default: Remake of +msgid "remake-of" +msgstr "Rifacimento di" + +# Default: Rentals +msgid "rentals" +msgstr "Noleggi" + +# Default: Result +msgid "result" +msgstr "Risultato" + +# Default: Review +msgid "review" +msgstr "Recensione" + +# Default: Review author +msgid "review-author" +msgstr "Autore della recensione" + +# Default: Review kind +msgid "review-kind" +msgstr "Tipo di recensione" + +# Default: Runtime +msgid "runtime" +msgstr "Durata" + +# Default: Runtimes +msgid "runtimes" +msgstr "Durate" + +# Default: Salary history +msgid "salary-history" +msgstr "Stipendi" + +# Default: Screenplay teleplay +msgid "screenplay-teleplay" +msgstr "" + +# Default: Season +msgid "season" +msgstr "Stagione" + +# Default: Second unit director or assistant director +msgid "second-unit-director-or-assistant-director" +msgstr "Regista della seconda unità o aiuto regista" + +# Default: Self +msgid "self" +msgstr "Se stesso" + +# Default: Series animation department +msgid "series-animation-department" +msgstr "Dipartimento animazione della serie" + +# Default: Series art department +msgid "series-art-department" +msgstr "Dipartimento artistico della serie" + +# Default: Series assistant directors +msgid "series-assistant-directors" +msgstr "Assistenti registi della serie" + +# Default: Series camera department +msgid "series-camera-department" +msgstr "" + +# Default: Series casting department +msgid "series-casting-department" +msgstr "" + +# Default: Series cinematographers +msgid "series-cinematographers" +msgstr "" + +# Default: Series costume department +msgid "series-costume-department" +msgstr "" + +# Default: Series editorial department +msgid "series-editorial-department" +msgstr "" + +# Default: Series editors +msgid "series-editors" +msgstr "" + +# Default: Series make up department +msgid "series-make-up-department" +msgstr "" + +# Default: Series miscellaneous +msgid "series-miscellaneous" +msgstr "" + +# Default: Series music department +msgid "series-music-department" +msgstr "" + +# Default: Series producers +msgid "series-producers" +msgstr "" + +# Default: Series production designers +msgid "series-production-designers" +msgstr "" + +# Default: Series production managers +msgid "series-production-managers" +msgstr "" + +# Default: Series sound department +msgid "series-sound-department" +msgstr "Dipartimento sonoro della serie" + +# Default: Series special effects department +msgid "series-special-effects-department" +msgstr "Dipartimento effetti speciali della serie" + +# Default: Series stunts +msgid "series-stunts" +msgstr "Controfigure della serie" + +# Default: Series title +msgid "series-title" +msgstr "Titolo della serie" + +# Default: Series transportation department +msgid "series-transportation-department" +msgstr "" + +# Default: Series visual effects department +msgid "series-visual-effects-department" +msgstr "" + +# Default: Series writers +msgid "series-writers" +msgstr "Scrittori della serie" + +# Default: Series years +msgid "series-years" +msgstr "Anni della serie" + +# Default: Set decoration +msgid "set-decoration" +msgstr "Decorazione del set" + +# Default: Sharpness +msgid "sharpness" +msgstr "" + +# Default: Similar to +msgid "similar-to" +msgstr "Simile a" + +# Default: Smart canonical episode title +msgid "smart-canonical-episode-title" +msgstr "Titolo canonico intelligente dell'episodio" + +# Default: Smart canonical series title +msgid "smart-canonical-series-title" +msgstr "Titolo canonico intelligente della serie" + +# Default: Smart canonical title +msgid "smart-canonical-title" +msgstr "Titolo canonico intelligente" + +# Default: Smart long imdb canonical title +msgid "smart-long-imdb-canonical-title" +msgstr "Titolo canonico lungo intelligente" + +# Default: Sound clips +msgid "sound-clips" +msgstr "" + +# Default: Sound crew +msgid "sound-crew" +msgstr "" + +# Default: Sound encoding +msgid "sound-encoding" +msgstr "Codifica sonora" + +# Default: Sound mix +msgid "sound-mix" +msgstr "Mix audio" + +# Default: Soundtrack +msgid "soundtrack" +msgstr "Colonna sonora" + +# Default: Spaciality +msgid "spaciality" +msgstr "Specialità " + +# Default: Special effects +msgid "special-effects" +msgstr "Effetti speciali" + +# Default: Special effects companies +msgid "special-effects-companies" +msgstr "Compagnie di effetti speciali" + +# Default: Special effects department +msgid "special-effects-department" +msgstr "Dipartimento effetti speciali" + +# Default: Spin off +msgid "spin-off" +msgstr "Derivati" + +# Default: Spin off from +msgid "spin-off-from" +msgstr "Deriva da" + +# Default: Spoofed in +msgid "spoofed-in" +msgstr "Preso in giro in" + +# Default: Spoofs +msgid "spoofs" +msgstr "Prende in giro" + +# Default: Spouse +msgid "spouse" +msgstr "Coniuge" + +# Default: Status of availablility +msgid "status-of-availablility" +msgstr "Disponibilità " + +# Default: Studio +msgid "studio" +msgstr "Studio" + +# Default: Studios +msgid "studios" +msgstr "Studi" + +# Default: Stunt performer +msgid "stunt-performer" +msgstr "" + +# Default: Stunts +msgid "stunts" +msgstr "Stuntman" + +# Default: Subtitles +msgid "subtitles" +msgstr "Sottotitoli" + +# Default: Supplement +msgid "supplement" +msgstr "Extra" + +# Default: Supplements +msgid "supplements" +msgstr "Extra" + +# Default: Synopsis +msgid "synopsis" +msgstr "Compendio della trama" + +# Default: Taglines +msgid "taglines" +msgstr "Slogan" + +# Default: Tech info +msgid "tech-info" +msgstr "Informazioni tecniche" + +# Default: Thanks +msgid "thanks" +msgstr "Ringraziamenti" + +# Default: Time +msgid "time" +msgstr "Tempo" + +# Default: Title +msgid "title" +msgstr "Titolo" + +# Default: Titles in this product +msgid "titles-in-this-product" +msgstr "Titoli in questo prodotto" + +# Default: To +msgid "to" +msgstr "A" + +# Default: Top 250 rank +msgid "top-250-rank" +msgstr "Posizione nella top 250" + +# Default: Trade mark +msgid "trade-mark" +msgstr "Marchio registrato" + +# Default: Transportation department +msgid "transportation-department" +msgstr "Dipartimento trasporti" + +# Default: Trivia +msgid "trivia" +msgstr "Frivolezze" + +# Default: Tv +msgid "tv" +msgstr "Tv" + +# Default: Under license from +msgid "under-license-from" +msgstr "Sotto licenza da" + +# Default: Unknown link +msgid "unknown-link" +msgstr "Collegamento sconosciuto" + +# Default: Upc +msgid "upc" +msgstr "" + +# Default: Version of +msgid "version-of" +msgstr "Versione di" + +# Default: Vhs +msgid "vhs" +msgstr "VHS" + +# Default: Video +msgid "video" +msgstr "Video" + +# Default: Video artifacts +msgid "video-artifacts" +msgstr "Imperfezioni video" + +# Default: Video clips +msgid "video-clips" +msgstr "Video clips" + +# Default: Video noise +msgid "video-noise" +msgstr "Rumore video" + +# Default: Video quality +msgid "video-quality" +msgstr "Qualità video" + +# Default: Video standard +msgid "video-standard" +msgstr "Standard video" + +# Default: Visual effects +msgid "visual-effects" +msgstr "Effetti visivi" + +# Default: Votes +msgid "votes" +msgstr "Voti" + +# Default: Votes distribution +msgid "votes-distribution" +msgstr "Distribuzione dei voti" + +# Default: Weekend gross +msgid "weekend-gross" +msgstr "Lordo del primo fine settimana" + +# Default: Where now +msgid "where-now" +msgstr "Cosa sta facendo ora" + +# Default: With +msgid "with" +msgstr "Con" + +# Default: Writer +msgid "writer" +msgstr "Scrittore" + +# Default: Written by +msgid "written-by" +msgstr "Scritto da" + +# Default: Year +msgid "year" +msgstr "Anno" + +# Default: Zshops +msgid "zshops" +msgstr "" diff --git a/lib/imdb/locale/imdbpy-tr.po b/lib/imdb/locale/imdbpy-tr.po new file mode 100644 index 0000000000000000000000000000000000000000..a44452aedb9a47b17e7621bce36710cf5810d205 --- /dev/null +++ b/lib/imdb/locale/imdbpy-tr.po @@ -0,0 +1,1300 @@ +# Gettext message file for imdbpy +msgid "" +msgstr "" +"Project-Id-Version: imdbpy\n" +"POT-Creation-Date: 2010-03-18 14:35+0000\n" +"PO-Revision-Date: 2009-04-21 19:04+0200\n" +"Last-Translator: H. Turgut Uyar <uyar@itu.edu.tr>\n" +"Language-Team: IMDbPY Türkçe <uyar@itu.edu.tr>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language-Code: tr\n" +"Language-Name: Türkçe\n" +"Preferred-Encodings: utf-8\n" +"Domain: imdbpy\n" + +# Default: Actor +msgid "actor" +msgstr "Oyuncu" + +# Default: Actress +msgid "actress" +msgstr "Oyuncu" + +# Default: Adaption +msgid "adaption" +msgstr "" + +# Default: Additional information +msgid "additional-information" +msgstr "Ek bilgi" + +# Default: Admissions +msgid "admissions" +msgstr "" + +# Default: Agent address +msgid "agent-address" +msgstr "" + +# Default: Airing +msgid "airing" +msgstr "Yayımlanma" + +# Default: Akas +msgid "akas" +msgstr "DiÄŸer baÅŸlıklar" + +# Default: Akas from release info +msgid "akas-from-release-info" +msgstr "" + +# Default: All products +msgid "all-products" +msgstr "Bütün ürünler" + +# Default: Alternate language version of +msgid "alternate-language-version-of" +msgstr "" + +# Default: Alternate versions +msgid "alternate-versions" +msgstr "" + +# Default: Amazon reviews +msgid "amazon-reviews" +msgstr "Amazon eleÅŸtirileri" + +# Default: Analog left +msgid "analog-left" +msgstr "Analog sol" + +# Default: Analog right +msgid "analog-right" +msgstr "Analog saÄŸ" + +# Default: Animation department +msgid "animation-department" +msgstr "Animasyon departmanı" + +# Default: Archive footage +msgid "archive-footage" +msgstr "ArÅŸiv çekimleri" + +# Default: Arithmetic mean +msgid "arithmetic-mean" +msgstr "Aritmetik ortalama" + +# Default: Art department +msgid "art-department" +msgstr "Sanat departmanı" + +# Default: Art direction +msgid "art-direction" +msgstr "Sanat yönetmenliÄŸi" + +# Default: Art director +msgid "art-director" +msgstr "Sanat yönetmeni" + +# Default: Article +msgid "article" +msgstr "" + +# Default: Asin +msgid "asin" +msgstr "ASIN" + +# Default: Aspect ratio +msgid "aspect-ratio" +msgstr "En-boy oranı" + +# Default: Assigner +msgid "assigner" +msgstr "Veren" + +# Default: Assistant director +msgid "assistant-director" +msgstr "Yardımcı yönetmen" + +# Default: Auctions +msgid "auctions" +msgstr "Açık artırmalar" + +# Default: Audio noise +msgid "audio-noise" +msgstr "Ses gürültüsü" + +# Default: Audio quality +msgid "audio-quality" +msgstr "Ses kalitesi" + +# Default: Award +msgid "award" +msgstr "Ödül" + +# Default: Awards +msgid "awards" +msgstr "Ödüller" + +# Default: Biographical movies +msgid "biographical-movies" +msgstr "Biyografik filmler" + +# Default: Biography +msgid "biography" +msgstr "Biyografi" + +# Default: Biography print +msgid "biography-print" +msgstr "Basılı biyografi" + +# Default: Birth date +msgid "birth-date" +msgstr "DoÄŸum tarihi" + +# Default: Birth name +msgid "birth-name" +msgstr "Asıl ismi" + +# Default: Birth notes +msgid "birth-notes" +msgstr "DoÄŸum notları" + +# Default: Body +msgid "body" +msgstr "Metin" + +# Default: Book +msgid "book" +msgstr "Kitap" + +# Default: Books +msgid "books" +msgstr "Kitaplar" + +# Default: Bottom 100 rank +msgid "bottom-100-rank" +msgstr "En kötü 100 içindeki sırası" + +# Default: Budget +msgid "budget" +msgstr "Bütçe" + +# Default: Business +msgid "business" +msgstr "GiÅŸe" + +# Default: By arrangement with +msgid "by-arrangement-with" +msgstr "" + +# Default: Camera +msgid "camera" +msgstr "Kamera" + +# Default: Camera and electrical department +msgid "camera-and-electrical-department" +msgstr "Kamera ve elektrik departmanı" + +# Default: Canonical episode title +msgid "canonical-episode-title" +msgstr "" + +# Default: Canonical name +msgid "canonical-name" +msgstr "" + +# Default: Canonical series title +msgid "canonical-series-title" +msgstr "" + +# Default: Canonical title +msgid "canonical-title" +msgstr "" + +# Default: Cast +msgid "cast" +msgstr "Oynayanlar" + +# Default: Casting department +msgid "casting-department" +msgstr "Oyuncu seçme departmanı" + +# Default: Casting director +msgid "casting-director" +msgstr "Oyuncu seçme yönetmeni" + +# Default: Catalog number +msgid "catalog-number" +msgstr "Katalog numarası" + +# Default: Category +msgid "category" +msgstr "Kategori" + +# Default: Certificate +msgid "certificate" +msgstr "Sertifika" + +# Default: Certificates +msgid "certificates" +msgstr "Sertifikalar" + +# Default: Certification +msgid "certification" +msgstr "" + +# Default: Channel +msgid "channel" +msgstr "Kanal" + +# Default: Character +msgid "character" +msgstr "Karakter" + +# Default: Cinematographer +msgid "cinematographer" +msgstr "Kameraman" + +# Default: Cinematographic process +msgid "cinematographic-process" +msgstr "" + +# Default: Close captions teletext ld g +msgid "close-captions-teletext-ld-g" +msgstr "" + +# Default: Color info +msgid "color-info" +msgstr "Renk bilgisi" + +# Default: Color information +msgid "color-information" +msgstr "Renk bilgisi" + +# Default: Color rendition +msgid "color-rendition" +msgstr "" + +# Default: Company +msgid "company" +msgstr "Åžirket" + +# Default: Complete cast +msgid "complete-cast" +msgstr "Bütün oynayanlar" + +# Default: Complete crew +msgid "complete-crew" +msgstr "Bütün çalışanlar" + +# Default: Composer +msgid "composer" +msgstr "Besteci" + +# Default: Connections +msgid "connections" +msgstr "BaÄŸlantılar" + +# Default: Contrast +msgid "contrast" +msgstr "Kontrast" + +# Default: Copyright holder +msgid "copyright-holder" +msgstr "Telif sahibi" + +# Default: Costume department +msgid "costume-department" +msgstr "Kostüm departmanı" + +# Default: Costume designer +msgid "costume-designer" +msgstr "Kostüm tasarımcısı" + +# Default: Countries +msgid "countries" +msgstr "Ülkeler" + +# Default: Country +msgid "country" +msgstr "Ülke" + +# Default: Courtesy of +msgid "courtesy-of" +msgstr "" + +# Default: Cover +msgid "cover" +msgstr "Poster" + +# Default: Cover url +msgid "cover-url" +msgstr "Poster adresi" + +# Default: Crazy credits +msgid "crazy-credits" +msgstr "" + +# Default: Creator +msgid "creator" +msgstr "Yaratıcı" + +# Default: Current role +msgid "current-role" +msgstr "Åžimdiki rol" + +# Default: Database +msgid "database" +msgstr "Veritabanı" + +# Default: Date +msgid "date" +msgstr "Tarih" + +# Default: Death date +msgid "death-date" +msgstr "Ölüm tarihi" + +# Default: Death notes +msgid "death-notes" +msgstr "Ölüm notları" + +# Default: Demographic +msgid "demographic" +msgstr "Demografi" + +# Default: Description +msgid "description" +msgstr "Tarif" + +# Default: Dialogue intellegibility +msgid "dialogue-intellegibility" +msgstr "" + +# Default: Digital sound +msgid "digital-sound" +msgstr "Dijital ses" + +# Default: Director +msgid "director" +msgstr "Yönetmen" + +# Default: Disc format +msgid "disc-format" +msgstr "Disk formatı" + +# Default: Disc size +msgid "disc-size" +msgstr "Disk boyu" + +# Default: Distributors +msgid "distributors" +msgstr "Dağıtıcılar" + +# Default: Dvd +msgid "dvd" +msgstr "DVD" + +# Default: Dvd features +msgid "dvd-features" +msgstr "DVD özellikleri" + +# Default: Dvd format +msgid "dvd-format" +msgstr "DVD formatı" + +# Default: Dvds +msgid "dvds" +msgstr "DVD'ler" + +# Default: Dynamic range +msgid "dynamic-range" +msgstr "" + +# Default: Edited from +msgid "edited-from" +msgstr "" + +# Default: Edited into +msgid "edited-into" +msgstr "" + +# Default: Editor +msgid "editor" +msgstr "Montajcı" + +# Default: Editorial department +msgid "editorial-department" +msgstr "Montaj departmanı" + +# Default: Episode +msgid "episode" +msgstr "Bölüm" + +# Default: Episode of +msgid "episode-of" +msgstr "Dizi" + +# Default: Episode title +msgid "episode-title" +msgstr "Bölüm baÅŸlığı" + +# Default: Episodes +msgid "episodes" +msgstr "Bölümler" + +# Default: Episodes rating +msgid "episodes-rating" +msgstr "Bölüm puanı" + +# Default: Essays +msgid "essays" +msgstr "Denemeler" + +# Default: External reviews +msgid "external-reviews" +msgstr "Harici eleÅŸtiriler" + +# Default: Faqs +msgid "faqs" +msgstr "SSS" + +# Default: Feature +msgid "feature" +msgstr "" + +# Default: Featured in +msgid "featured-in" +msgstr "" + +# Default: Features +msgid "features" +msgstr "" + +# Default: Film negative format +msgid "film-negative-format" +msgstr "Film negatif formatı" + +# Default: Filming dates +msgid "filming-dates" +msgstr "Çekim tarihleri" + +# Default: Filmography +msgid "filmography" +msgstr "Filmografi" + +# Default: Followed by +msgid "followed-by" +msgstr "PeÅŸinden gelen film" + +# Default: Follows +msgid "follows" +msgstr "PeÅŸinden geldiÄŸi film" + +# Default: For +msgid "for" +msgstr "Film" + +# Default: Frequency response +msgid "frequency-response" +msgstr "" + +# Default: From +msgid "from" +msgstr "" + +# Default: Full article link +msgid "full-article-link" +msgstr "" + +# Default: Full size cover url +msgid "full-size-cover-url" +msgstr "" + +# Default: Full size headshot +msgid "full-size-headshot" +msgstr "" + +# Default: Genres +msgid "genres" +msgstr "Türler" + +# Default: Goofs +msgid "goofs" +msgstr "Hatalar" + +# Default: Gross +msgid "gross" +msgstr "Hasılat" + +# Default: Group genre +msgid "group-genre" +msgstr "" + +# Default: Headshot +msgid "headshot" +msgstr "Resim" + +# Default: Height +msgid "height" +msgstr "Boy" + +# Default: Imdbindex +msgid "imdbindex" +msgstr "" + +# Default: In development +msgid "in-development" +msgstr "" + +# Default: Interview +msgid "interview" +msgstr "SöyleÅŸi" + +# Default: Interviews +msgid "interviews" +msgstr "SöyleÅŸiler" + +# Default: Introduction +msgid "introduction" +msgstr "İlk filmi" + +# Default: Item +msgid "item" +msgstr "" + +# Default: Keywords +msgid "keywords" +msgstr "Anahtar sözcükler" + +# Default: Kind +msgid "kind" +msgstr "Tip" + +# Default: Label +msgid "label" +msgstr "" + +# Default: Laboratory +msgid "laboratory" +msgstr "Laboratuar" + +# Default: Language +msgid "language" +msgstr "Dil" + +# Default: Languages +msgid "languages" +msgstr "Diller" + +# Default: Laserdisc +msgid "laserdisc" +msgstr "Lazer Disk" + +# Default: Laserdisc title +msgid "laserdisc-title" +msgstr "" + +# Default: Length +msgid "length" +msgstr "Süre" + +# Default: Line +msgid "line" +msgstr "Replik" + +# Default: Link +msgid "link" +msgstr "BaÄŸlantı" + +# Default: Link text +msgid "link-text" +msgstr "BaÄŸlantı metni" + +# Default: Literature +msgid "literature" +msgstr "Edebiyat" + +# Default: Locations +msgid "locations" +msgstr "Çekim yerleri" + +# Default: Long imdb canonical name +msgid "long-imdb-canonical-name" +msgstr "" + +# Default: Long imdb canonical title +msgid "long-imdb-canonical-title" +msgstr "" + +# Default: Long imdb episode title +msgid "long-imdb-episode-title" +msgstr "IMDb uzun bölüm baÅŸlığı" + +# Default: Long imdb name +msgid "long-imdb-name" +msgstr "IMDb uzun ismi" + +# Default: Long imdb title +msgid "long-imdb-title" +msgstr "IMDb uzun baÅŸlığı" + +# Default: Magazine cover photo +msgid "magazine-cover-photo" +msgstr "Dergi kapağı resmi" + +# Default: Make up +msgid "make-up" +msgstr "Makyaj" + +# Default: Master format +msgid "master-format" +msgstr "Master format" + +# Default: Median +msgid "median" +msgstr "Orta deÄŸer" + +# Default: Merchandising links +msgid "merchandising-links" +msgstr "" + +# Default: Mini biography +msgid "mini-biography" +msgstr "Mini biyografi" + +# Default: Misc links +msgid "misc-links" +msgstr "" + +# Default: Miscellaneous companies +msgid "miscellaneous-companies" +msgstr "" + +# Default: Miscellaneous crew +msgid "miscellaneous-crew" +msgstr "" + +# Default: Movie +msgid "movie" +msgstr "Film" + +# Default: Mpaa +msgid "mpaa" +msgstr "MPAA" + +# Default: Music department +msgid "music-department" +msgstr "Müzik departmanı" + +# Default: Name +msgid "name" +msgstr "İsim" + +# Default: News +msgid "news" +msgstr "Haberler" + +# Default: Newsgroup reviews +msgid "newsgroup-reviews" +msgstr "Haber grubu eleÅŸtirileri" + +# Default: Nick names +msgid "nick-names" +msgstr "Takma isimler" + +# Default: Notes +msgid "notes" +msgstr "Notlar" + +# Default: Novel +msgid "novel" +msgstr "Roman" + +# Default: Number +msgid "number" +msgstr "Sayı" + +# Default: Number of chapter stops +msgid "number-of-chapter-stops" +msgstr "" + +# Default: Number of episodes +msgid "number-of-episodes" +msgstr "Bölüm sayısı" + +# Default: Number of seasons +msgid "number-of-seasons" +msgstr "Sezon sayısı" + +# Default: Number of sides +msgid "number-of-sides" +msgstr "" + +# Default: Number of votes +msgid "number-of-votes" +msgstr "Oy sayısı" + +# Default: Official retail price +msgid "official-retail-price" +msgstr "Resmi perakende satış fiyatı" + +# Default: Official sites +msgid "official-sites" +msgstr "Resmi siteler" + +# Default: Opening weekend +msgid "opening-weekend" +msgstr "Açılış haftasonu" + +# Default: Original air date +msgid "original-air-date" +msgstr "İlk yayımlanma tarihi" + +# Default: Original music +msgid "original-music" +msgstr "Orijinal müzik" + +# Default: Original title +msgid "original-title" +msgstr "" + +# Default: Other literature +msgid "other-literature" +msgstr "" + +# Default: Other works +msgid "other-works" +msgstr "DiÄŸer çalışmalar" + +# Default: Parents guide +msgid "parents-guide" +msgstr "Ana-baba kılavuzu" + +# Default: Performed by +msgid "performed-by" +msgstr "İcra eden" + +# Default: Person +msgid "person" +msgstr "KiÅŸi" + +# Default: Photo sites +msgid "photo-sites" +msgstr "FotoÄŸraf siteleri" + +# Default: Pictorial +msgid "pictorial" +msgstr "" + +# Default: Picture format +msgid "picture-format" +msgstr "Resim formatı" + +# Default: Plot +msgid "plot" +msgstr "Konu" + +# Default: Plot outline +msgid "plot-outline" +msgstr "Konu kısa özeti" + +# Default: Portrayed in +msgid "portrayed-in" +msgstr "" + +# Default: Pressing plant +msgid "pressing-plant" +msgstr "" + +# Default: Printed film format +msgid "printed-film-format" +msgstr "Basılı film formatı" + +# Default: Printed media reviews +msgid "printed-media-reviews" +msgstr "Basın eleÅŸtirileri" + +# Default: Producer +msgid "producer" +msgstr "Yapımcı" + +# Default: Production companies +msgid "production-companies" +msgstr "Yapım ÅŸirketleri" + +# Default: Production country +msgid "production-country" +msgstr "Yapımcı ülke" + +# Default: Production dates +msgid "production-dates" +msgstr "Yapım tarihleri" + +# Default: Production design +msgid "production-design" +msgstr "Yapım tasarımı" + +# Default: Production designer +msgid "production-designer" +msgstr "Yapım tasarımcısı" + +# Default: Production manager +msgid "production-manager" +msgstr "Yapım yöneticisi" + +# Default: Production process protocol +msgid "production-process-protocol" +msgstr "" + +# Default: Quality of source +msgid "quality-of-source" +msgstr "" + +# Default: Quality program +msgid "quality-program" +msgstr "" + +# Default: Quote +msgid "quote" +msgstr "Alıntı" + +# Default: Quotes +msgid "quotes" +msgstr "Alıntılar" + +# Default: Rating +msgid "rating" +msgstr "Puan" + +# Default: Recommendations +msgid "recommendations" +msgstr "Tavsiyeler" + +# Default: Referenced in +msgid "referenced-in" +msgstr "Gönderme yapılan filmler" + +# Default: References +msgid "references" +msgstr "Gönderme yaptığı filmler" + +# Default: Region +msgid "region" +msgstr "Bölge" + +# Default: Release country +msgid "release-country" +msgstr "" + +# Default: Release date +msgid "release-date" +msgstr "" + +# Default: Release dates +msgid "release-dates" +msgstr "" + +# Default: Remade as +msgid "remade-as" +msgstr "Yeniden çekiliÅŸi" + +# Default: Remake of +msgid "remake-of" +msgstr "Yeniden çekimi olduÄŸu film" + +# Default: Rentals +msgid "rentals" +msgstr "Kiralamalar" + +# Default: Result +msgid "result" +msgstr "Sonuç" + +# Default: Review +msgid "review" +msgstr "EleÅŸtiri" + +# Default: Review author +msgid "review-author" +msgstr "EleÅŸtiri yazarı" + +# Default: Review kind +msgid "review-kind" +msgstr "EleÅŸtiri tipi" + +# Default: Runtime +msgid "runtime" +msgstr "Süre" + +# Default: Runtimes +msgid "runtimes" +msgstr "Süreler" + +# Default: Salary history +msgid "salary-history" +msgstr "Üçret tarihçesi" + +# Default: Screenplay teleplay +msgid "screenplay-teleplay" +msgstr "Senaryo" + +# Default: Season +msgid "season" +msgstr "Sezon" + +# Default: Second unit director or assistant director +msgid "second-unit-director-or-assistant-director" +msgstr "İkinci birim yönetmeni ya da yardımcı yönetmen" + +# Default: Self +msgid "self" +msgstr "Kendisi" + +# Default: Series animation department +msgid "series-animation-department" +msgstr "Dizinin animasyon departmanı" + +# Default: Series art department +msgid "series-art-department" +msgstr "Dizinin sanat departmanı" + +# Default: Series assistant directors +msgid "series-assistant-directors" +msgstr "Dizinin yardımcı yönetmenleri" + +# Default: Series camera department +msgid "series-camera-department" +msgstr "Dizinin kamera departmanı" + +# Default: Series casting department +msgid "series-casting-department" +msgstr "Dizinin oyuncu seçimi departmanı" + +# Default: Series cinematographers +msgid "series-cinematographers" +msgstr "Dizinin kameramanları" + +# Default: Series costume department +msgid "series-costume-department" +msgstr "Dizinin kostüm departmanı" + +# Default: Series editorial department +msgid "series-editorial-department" +msgstr "Dizinin montaj departmanı" + +# Default: Series editors +msgid "series-editors" +msgstr "Dizinin montajcıları" + +# Default: Series make up department +msgid "series-make-up-department" +msgstr "Dizinin makyaj departmanı" + +# Default: Series miscellaneous +msgid "series-miscellaneous" +msgstr "" + +# Default: Series music department +msgid "series-music-department" +msgstr "Dizinin müzik departmanı" + +# Default: Series producers +msgid "series-producers" +msgstr "Dizinin yapımcıları" + +# Default: Series production designers +msgid "series-production-designers" +msgstr "Dizinin yapım tasarımcıları" + +# Default: Series production managers +msgid "series-production-managers" +msgstr "Dizinin yapım yöneticileri" + +# Default: Series sound department +msgid "series-sound-department" +msgstr "Dizinin ses departmanı" + +# Default: Series special effects department +msgid "series-special-effects-department" +msgstr "Dizinin özel efekt departmanı" + +# Default: Series stunts +msgid "series-stunts" +msgstr "Dizinin dublörleri" + +# Default: Series title +msgid "series-title" +msgstr "Dizinin baÅŸlığı" + +# Default: Series transportation department +msgid "series-transportation-department" +msgstr "Dizinin ulaşım departmanı" + +# Default: Series visual effects department +msgid "series-visual-effects-department" +msgstr "Dizinin görsel efekt departmanı" + +# Default: Series writers +msgid "series-writers" +msgstr "Dizinin yazarları" + +# Default: Series years +msgid "series-years" +msgstr "Dizinin yılları" + +# Default: Set decoration +msgid "set-decoration" +msgstr "Set dekorasyonu" + +# Default: Sharpness +msgid "sharpness" +msgstr "Keskinlik" + +# Default: Similar to +msgid "similar-to" +msgstr "Benzer" + +# Default: Smart canonical episode title +msgid "smart-canonical-episode-title" +msgstr "" + +# Default: Smart canonical series title +msgid "smart-canonical-series-title" +msgstr "" + +# Default: Smart canonical title +msgid "smart-canonical-title" +msgstr "" + +# Default: Smart long imdb canonical title +msgid "smart-long-imdb-canonical-title" +msgstr "" + +# Default: Sound clips +msgid "sound-clips" +msgstr "Ses klipleri" + +# Default: Sound crew +msgid "sound-crew" +msgstr "Ses ekibi" + +# Default: Sound encoding +msgid "sound-encoding" +msgstr "Ses kodlaması" + +# Default: Sound mix +msgid "sound-mix" +msgstr "" + +# Default: Soundtrack +msgid "soundtrack" +msgstr "Film müzikleri" + +# Default: Spaciality +msgid "spaciality" +msgstr "" + +# Default: Special effects +msgid "special-effects" +msgstr "Özel efektler" + +# Default: Special effects companies +msgid "special-effects-companies" +msgstr "Özel efekt ÅŸirketleri" + +# Default: Special effects department +msgid "special-effects-department" +msgstr "Özel efekt departmanı" + +# Default: Spin off +msgid "spin-off" +msgstr "" + +# Default: Spin off from +msgid "spin-off-from" +msgstr "" + +# Default: Spoofed in +msgid "spoofed-in" +msgstr "Dalga geçildiÄŸi filmler" + +# Default: Spoofs +msgid "spoofs" +msgstr "Dalga geçtiÄŸi filmler" + +# Default: Spouse +msgid "spouse" +msgstr "EÅŸi" + +# Default: Status of availablility +msgid "status-of-availablility" +msgstr "" + +# Default: Studio +msgid "studio" +msgstr "Stüdyo" + +# Default: Studios +msgid "studios" +msgstr "Stüdyolar" + +# Default: Stunt performer +msgid "stunt-performer" +msgstr "" + +# Default: Stunts +msgid "stunts" +msgstr "Dublörler" + +# Default: Subtitles +msgid "subtitles" +msgstr "Altyazılar" + +# Default: Supplement +msgid "supplement" +msgstr "" + +# Default: Supplements +msgid "supplements" +msgstr "" + +# Default: Synopsis +msgid "synopsis" +msgstr "Sinopsis" + +# Default: Taglines +msgid "taglines" +msgstr "Spotlar" + +# Default: Tech info +msgid "tech-info" +msgstr "Teknik bilgi" + +# Default: Thanks +msgid "thanks" +msgstr "TeÅŸekkürler" + +# Default: Time +msgid "time" +msgstr "Zaman" + +# Default: Title +msgid "title" +msgstr "BaÅŸlık" + +# Default: Titles in this product +msgid "titles-in-this-product" +msgstr "Bu üründeki baÅŸlıklar" + +# Default: To +msgid "to" +msgstr "Alan" + +# Default: Top 250 rank +msgid "top-250-rank" +msgstr "En iyi 250 içindeki sırası" + +# Default: Trade mark +msgid "trade-mark" +msgstr "Kendine has özelliÄŸi" + +# Default: Transportation department +msgid "transportation-department" +msgstr "Ulaşım departmanı" + +# Default: Trivia +msgid "trivia" +msgstr "İlginç notlar" + +# Default: Tv +msgid "tv" +msgstr "" + +# Default: Under license from +msgid "under-license-from" +msgstr "" + +# Default: Unknown link +msgid "unknown-link" +msgstr "" + +# Default: Upc +msgid "upc" +msgstr "" + +# Default: Version of +msgid "version-of" +msgstr "" + +# Default: Vhs +msgid "vhs" +msgstr "VHS" + +# Default: Video +msgid "video" +msgstr "" + +# Default: Video artifacts +msgid "video-artifacts" +msgstr "" + +# Default: Video clips +msgid "video-clips" +msgstr "Video klipleri" + +# Default: Video noise +msgid "video-noise" +msgstr "Video gürültüsü" + +# Default: Video quality +msgid "video-quality" +msgstr "Video kalitesi" + +# Default: Video standard +msgid "video-standard" +msgstr "Video standardı" + +# Default: Visual effects +msgid "visual-effects" +msgstr "Görsel efektler" + +# Default: Votes +msgid "votes" +msgstr "Oylar" + +# Default: Votes distribution +msgid "votes-distribution" +msgstr "Oyların dağılımı" + +# Default: Weekend gross +msgid "weekend-gross" +msgstr "Haftasonu hasılatı" + +# Default: Where now +msgid "where-now" +msgstr "Åžu anda nerede" + +# Default: With +msgid "with" +msgstr "" + +# Default: Writer +msgid "writer" +msgstr "Yazar" + +# Default: Written by +msgid "written-by" +msgstr "Yazan" + +# Default: Year +msgid "year" +msgstr "Yıl" + +# Default: Zshops +msgid "zshops" +msgstr "ZShops" diff --git a/lib/imdb/locale/imdbpy.pot b/lib/imdb/locale/imdbpy.pot new file mode 100644 index 0000000000000000000000000000000000000000..14ac1669ef75e926d576296f58f8cb5fe7b62a4b --- /dev/null +++ b/lib/imdb/locale/imdbpy.pot @@ -0,0 +1,1301 @@ +# Gettext message file for imdbpy +msgid "" +msgstr "" +"Project-Id-Version: imdbpy\n" +"POT-Creation-Date: 2010-03-18 14:35+0000\n" +"PO-Revision-Date: YYYY-MM-DD HH:MM+0000\n" +"Last-Translator: YOUR NAME <YOUR@EMAIL>\n" +"Language-Team: TEAM NAME <TEAM@EMAIL>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8\n" +"Domain: imdbpy\n" + +# Default: Actor +msgid "actor" +msgstr "" + +# Default: Actress +msgid "actress" +msgstr "" + +# Default: Adaption +msgid "adaption" +msgstr "" + +# Default: Additional information +msgid "additional-information" +msgstr "" + +# Default: Admissions +msgid "admissions" +msgstr "" + +# Default: Agent address +msgid "agent-address" +msgstr "" + +# Default: Airing +msgid "airing" +msgstr "" + +# Default: Akas +msgid "akas" +msgstr "" + +# Default: Akas from release info +msgid "akas-from-release-info" +msgstr "" + +# Default: All products +msgid "all-products" +msgstr "" + +# Default: Alternate language version of +msgid "alternate-language-version-of" +msgstr "" + +# Default: Alternate versions +msgid "alternate-versions" +msgstr "" + +# Default: Amazon reviews +msgid "amazon-reviews" +msgstr "" + +# Default: Analog left +msgid "analog-left" +msgstr "" + +# Default: Analog right +msgid "analog-right" +msgstr "" + +# Default: Animation department +msgid "animation-department" +msgstr "" + +# Default: Archive footage +msgid "archive-footage" +msgstr "" + +# Default: Arithmetic mean +msgid "arithmetic-mean" +msgstr "" + +# Default: Art department +msgid "art-department" +msgstr "" + +# Default: Art direction +msgid "art-direction" +msgstr "" + +# Default: Art director +msgid "art-director" +msgstr "" + +# Default: Article +msgid "article" +msgstr "" + +# Default: Asin +msgid "asin" +msgstr "" + +# Default: Aspect ratio +msgid "aspect-ratio" +msgstr "" + +# Default: Assigner +msgid "assigner" +msgstr "" + +# Default: Assistant director +msgid "assistant-director" +msgstr "" + +# Default: Auctions +msgid "auctions" +msgstr "" + +# Default: Audio noise +msgid "audio-noise" +msgstr "" + +# Default: Audio quality +msgid "audio-quality" +msgstr "" + +# Default: Award +msgid "award" +msgstr "" + +# Default: Awards +msgid "awards" +msgstr "" + +# Default: Biographical movies +msgid "biographical-movies" +msgstr "" + +# Default: Biography +msgid "biography" +msgstr "" + +# Default: Biography print +msgid "biography-print" +msgstr "" + +# Default: Birth date +msgid "birth-date" +msgstr "" + +# Default: Birth name +msgid "birth-name" +msgstr "" + +# Default: Birth notes +msgid "birth-notes" +msgstr "" + +# Default: Body +msgid "body" +msgstr "" + +# Default: Book +msgid "book" +msgstr "" + +# Default: Books +msgid "books" +msgstr "" + +# Default: Bottom 100 rank +msgid "bottom-100-rank" +msgstr "" + +# Default: Budget +msgid "budget" +msgstr "" + +# Default: Business +msgid "business" +msgstr "" + +# Default: By arrangement with +msgid "by-arrangement-with" +msgstr "" + +# Default: Camera +msgid "camera" +msgstr "" + +# Default: Camera and electrical department +msgid "camera-and-electrical-department" +msgstr "" + +# Default: Canonical episode title +msgid "canonical-episode-title" +msgstr "" + +# Default: Canonical name +msgid "canonical-name" +msgstr "" + +# Default: Canonical series title +msgid "canonical-series-title" +msgstr "" + +# Default: Canonical title +msgid "canonical-title" +msgstr "" + +# Default: Cast +msgid "cast" +msgstr "" + +# Default: Casting department +msgid "casting-department" +msgstr "" + +# Default: Casting director +msgid "casting-director" +msgstr "" + +# Default: Catalog number +msgid "catalog-number" +msgstr "" + +# Default: Category +msgid "category" +msgstr "" + +# Default: Certificate +msgid "certificate" +msgstr "" + +# Default: Certificates +msgid "certificates" +msgstr "" + +# Default: Certification +msgid "certification" +msgstr "" + +# Default: Channel +msgid "channel" +msgstr "" + +# Default: Character +msgid "character" +msgstr "" + +# Default: Cinematographer +msgid "cinematographer" +msgstr "" + +# Default: Cinematographic process +msgid "cinematographic-process" +msgstr "" + +# Default: Close captions teletext ld g +msgid "close-captions-teletext-ld-g" +msgstr "" + +# Default: Color info +msgid "color-info" +msgstr "" + +# Default: Color information +msgid "color-information" +msgstr "" + +# Default: Color rendition +msgid "color-rendition" +msgstr "" + +# Default: Company +msgid "company" +msgstr "" + +# Default: Complete cast +msgid "complete-cast" +msgstr "" + +# Default: Complete crew +msgid "complete-crew" +msgstr "" + +# Default: Composer +msgid "composer" +msgstr "" + +# Default: Connections +msgid "connections" +msgstr "" + +# Default: Contrast +msgid "contrast" +msgstr "" + +# Default: Copyright holder +msgid "copyright-holder" +msgstr "" + +# Default: Costume department +msgid "costume-department" +msgstr "" + +# Default: Costume designer +msgid "costume-designer" +msgstr "" + +# Default: Countries +msgid "countries" +msgstr "" + +# Default: Country +msgid "country" +msgstr "" + +# Default: Courtesy of +msgid "courtesy-of" +msgstr "" + +# Default: Cover +msgid "cover" +msgstr "" + +# Default: Cover url +msgid "cover-url" +msgstr "" + +# Default: Crazy credits +msgid "crazy-credits" +msgstr "" + +# Default: Creator +msgid "creator" +msgstr "" + +# Default: Current role +msgid "current-role" +msgstr "" + +# Default: Database +msgid "database" +msgstr "" + +# Default: Date +msgid "date" +msgstr "" + +# Default: Death date +msgid "death-date" +msgstr "" + +# Default: Death notes +msgid "death-notes" +msgstr "" + +# Default: Demographic +msgid "demographic" +msgstr "" + +# Default: Description +msgid "description" +msgstr "" + +# Default: Dialogue intellegibility +msgid "dialogue-intellegibility" +msgstr "" + +# Default: Digital sound +msgid "digital-sound" +msgstr "" + +# Default: Director +msgid "director" +msgstr "" + +# Default: Disc format +msgid "disc-format" +msgstr "" + +# Default: Disc size +msgid "disc-size" +msgstr "" + +# Default: Distributors +msgid "distributors" +msgstr "" + +# Default: Dvd +msgid "dvd" +msgstr "" + +# Default: Dvd features +msgid "dvd-features" +msgstr "" + +# Default: Dvd format +msgid "dvd-format" +msgstr "" + +# Default: Dvds +msgid "dvds" +msgstr "" + +# Default: Dynamic range +msgid "dynamic-range" +msgstr "" + +# Default: Edited from +msgid "edited-from" +msgstr "" + +# Default: Edited into +msgid "edited-into" +msgstr "" + +# Default: Editor +msgid "editor" +msgstr "" + +# Default: Editorial department +msgid "editorial-department" +msgstr "" + +# Default: Episode +msgid "episode" +msgstr "" + +# Default: Episode of +msgid "episode-of" +msgstr "" + +# Default: Episode title +msgid "episode-title" +msgstr "" + +# Default: Episodes +msgid "episodes" +msgstr "" + +# Default: Episodes rating +msgid "episodes-rating" +msgstr "" + +# Default: Essays +msgid "essays" +msgstr "" + +# Default: External reviews +msgid "external-reviews" +msgstr "" + +# Default: Faqs +msgid "faqs" +msgstr "" + +# Default: Feature +msgid "feature" +msgstr "" + +# Default: Featured in +msgid "featured-in" +msgstr "" + +# Default: Features +msgid "features" +msgstr "" + +# Default: Film negative format +msgid "film-negative-format" +msgstr "" + +# Default: Filming dates +msgid "filming-dates" +msgstr "" + +# Default: Filmography +msgid "filmography" +msgstr "" + +# Default: Followed by +msgid "followed-by" +msgstr "" + +# Default: Follows +msgid "follows" +msgstr "" + +# Default: For +msgid "for" +msgstr "" + +# Default: Frequency response +msgid "frequency-response" +msgstr "" + +# Default: From +msgid "from" +msgstr "" + +# Default: Full article link +msgid "full-article-link" +msgstr "" + +# Default: Full size cover url +msgid "full-size-cover-url" +msgstr "" + +# Default: Full size headshot +msgid "full-size-headshot" +msgstr "" + +# Default: Genres +msgid "genres" +msgstr "" + +# Default: Goofs +msgid "goofs" +msgstr "" + +# Default: Gross +msgid "gross" +msgstr "" + +# Default: Group genre +msgid "group-genre" +msgstr "" + +# Default: Headshot +msgid "headshot" +msgstr "" + +# Default: Height +msgid "height" +msgstr "" + +# Default: Imdbindex +msgid "imdbindex" +msgstr "" + +# Default: In development +msgid "in-development" +msgstr "" + +# Default: Interview +msgid "interview" +msgstr "" + +# Default: Interviews +msgid "interviews" +msgstr "" + +# Default: Introduction +msgid "introduction" +msgstr "" + +# Default: Item +msgid "item" +msgstr "" + +# Default: Keywords +msgid "keywords" +msgstr "" + +# Default: Kind +msgid "kind" +msgstr "" + +# Default: Label +msgid "label" +msgstr "" + +# Default: Laboratory +msgid "laboratory" +msgstr "" + +# Default: Language +msgid "language" +msgstr "" + +# Default: Languages +msgid "languages" +msgstr "" + +# Default: Laserdisc +msgid "laserdisc" +msgstr "" + +# Default: Laserdisc title +msgid "laserdisc-title" +msgstr "" + +# Default: Length +msgid "length" +msgstr "" + +# Default: Line +msgid "line" +msgstr "" + +# Default: Link +msgid "link" +msgstr "" + +# Default: Link text +msgid "link-text" +msgstr "" + +# Default: Literature +msgid "literature" +msgstr "" + +# Default: Locations +msgid "locations" +msgstr "" + +# Default: Long imdb canonical name +msgid "long-imdb-canonical-name" +msgstr "" + +# Default: Long imdb canonical title +msgid "long-imdb-canonical-title" +msgstr "" + +# Default: Long imdb episode title +msgid "long-imdb-episode-title" +msgstr "" + +# Default: Long imdb name +msgid "long-imdb-name" +msgstr "" + +# Default: Long imdb title +msgid "long-imdb-title" +msgstr "" + +# Default: Magazine cover photo +msgid "magazine-cover-photo" +msgstr "" + +# Default: Make up +msgid "make-up" +msgstr "" + +# Default: Master format +msgid "master-format" +msgstr "" + +# Default: Median +msgid "median" +msgstr "" + +# Default: Merchandising links +msgid "merchandising-links" +msgstr "" + +# Default: Mini biography +msgid "mini-biography" +msgstr "" + +# Default: Misc links +msgid "misc-links" +msgstr "" + +# Default: Miscellaneous companies +msgid "miscellaneous-companies" +msgstr "" + +# Default: Miscellaneous crew +msgid "miscellaneous-crew" +msgstr "" + +# Default: Movie +msgid "movie" +msgstr "" + +# Default: Mpaa +msgid "mpaa" +msgstr "" + +# Default: Music department +msgid "music-department" +msgstr "" + +# Default: Name +msgid "name" +msgstr "" + +# Default: News +msgid "news" +msgstr "" + +# Default: Newsgroup reviews +msgid "newsgroup-reviews" +msgstr "" + +# Default: Nick names +msgid "nick-names" +msgstr "" + +# Default: Notes +msgid "notes" +msgstr "" + +# Default: Novel +msgid "novel" +msgstr "" + +# Default: Number +msgid "number" +msgstr "" + +# Default: Number of chapter stops +msgid "number-of-chapter-stops" +msgstr "" + +# Default: Number of episodes +msgid "number-of-episodes" +msgstr "" + +# Default: Number of seasons +msgid "number-of-seasons" +msgstr "" + +# Default: Number of sides +msgid "number-of-sides" +msgstr "" + +# Default: Number of votes +msgid "number-of-votes" +msgstr "" + +# Default: Official retail price +msgid "official-retail-price" +msgstr "" + +# Default: Official sites +msgid "official-sites" +msgstr "" + +# Default: Opening weekend +msgid "opening-weekend" +msgstr "" + +# Default: Original air date +msgid "original-air-date" +msgstr "" + +# Default: Original music +msgid "original-music" +msgstr "" + +# Default: Original title +msgid "original-title" +msgstr "" + +# Default: Other literature +msgid "other-literature" +msgstr "" + +# Default: Other works +msgid "other-works" +msgstr "" + +# Default: Parents guide +msgid "parents-guide" +msgstr "" + +# Default: Performed by +msgid "performed-by" +msgstr "" + +# Default: Person +msgid "person" +msgstr "" + +# Default: Photo sites +msgid "photo-sites" +msgstr "" + +# Default: Pictorial +msgid "pictorial" +msgstr "" + +# Default: Picture format +msgid "picture-format" +msgstr "" + +# Default: Plot +msgid "plot" +msgstr "" + +# Default: Plot outline +msgid "plot-outline" +msgstr "" + +# Default: Portrayed in +msgid "portrayed-in" +msgstr "" + +# Default: Pressing plant +msgid "pressing-plant" +msgstr "" + +# Default: Printed film format +msgid "printed-film-format" +msgstr "" + +# Default: Printed media reviews +msgid "printed-media-reviews" +msgstr "" + +# Default: Producer +msgid "producer" +msgstr "" + +# Default: Production companies +msgid "production-companies" +msgstr "" + +# Default: Production country +msgid "production-country" +msgstr "" + +# Default: Production dates +msgid "production-dates" +msgstr "" + +# Default: Production design +msgid "production-design" +msgstr "" + +# Default: Production designer +msgid "production-designer" +msgstr "" + +# Default: Production manager +msgid "production-manager" +msgstr "" + +# Default: Production process protocol +msgid "production-process-protocol" +msgstr "" + +# Default: Quality of source +msgid "quality-of-source" +msgstr "" + +# Default: Quality program +msgid "quality-program" +msgstr "" + +# Default: Quote +msgid "quote" +msgstr "" + +# Default: Quotes +msgid "quotes" +msgstr "" + +# Default: Rating +msgid "rating" +msgstr "" + +# Default: Recommendations +msgid "recommendations" +msgstr "" + +# Default: Referenced in +msgid "referenced-in" +msgstr "" + +# Default: References +msgid "references" +msgstr "" + +# Default: Region +msgid "region" +msgstr "" + +# Default: Release country +msgid "release-country" +msgstr "" + +# Default: Release date +msgid "release-date" +msgstr "" + +# Default: Release dates +msgid "release-dates" +msgstr "" + +# Default: Remade as +msgid "remade-as" +msgstr "" + +# Default: Remake of +msgid "remake-of" +msgstr "" + +# Default: Rentals +msgid "rentals" +msgstr "" + +# Default: Result +msgid "result" +msgstr "" + +# Default: Review +msgid "review" +msgstr "" + +# Default: Review author +msgid "review-author" +msgstr "" + +# Default: Review kind +msgid "review-kind" +msgstr "" + +# Default: Runtime +msgid "runtime" +msgstr "" + +# Default: Runtimes +msgid "runtimes" +msgstr "" + +# Default: Salary history +msgid "salary-history" +msgstr "" + +# Default: Screenplay teleplay +msgid "screenplay-teleplay" +msgstr "" + +# Default: Season +msgid "season" +msgstr "" + +# Default: Second unit director or assistant director +msgid "second-unit-director-or-assistant-director" +msgstr "" + +# Default: Self +msgid "self" +msgstr "" + +# Default: Series animation department +msgid "series-animation-department" +msgstr "" + +# Default: Series art department +msgid "series-art-department" +msgstr "" + +# Default: Series assistant directors +msgid "series-assistant-directors" +msgstr "" + +# Default: Series camera department +msgid "series-camera-department" +msgstr "" + +# Default: Series casting department +msgid "series-casting-department" +msgstr "" + +# Default: Series cinematographers +msgid "series-cinematographers" +msgstr "" + +# Default: Series costume department +msgid "series-costume-department" +msgstr "" + +# Default: Series editorial department +msgid "series-editorial-department" +msgstr "" + +# Default: Series editors +msgid "series-editors" +msgstr "" + +# Default: Series make up department +msgid "series-make-up-department" +msgstr "" + +# Default: Series miscellaneous +msgid "series-miscellaneous" +msgstr "" + +# Default: Series music department +msgid "series-music-department" +msgstr "" + +# Default: Series producers +msgid "series-producers" +msgstr "" + +# Default: Series production designers +msgid "series-production-designers" +msgstr "" + +# Default: Series production managers +msgid "series-production-managers" +msgstr "" + +# Default: Series sound department +msgid "series-sound-department" +msgstr "" + +# Default: Series special effects department +msgid "series-special-effects-department" +msgstr "" + +# Default: Series stunts +msgid "series-stunts" +msgstr "" + +# Default: Series title +msgid "series-title" +msgstr "" + +# Default: Series transportation department +msgid "series-transportation-department" +msgstr "" + +# Default: Series visual effects department +msgid "series-visual-effects-department" +msgstr "" + +# Default: Series writers +msgid "series-writers" +msgstr "" + +# Default: Series years +msgid "series-years" +msgstr "" + +# Default: Set decoration +msgid "set-decoration" +msgstr "" + +# Default: Sharpness +msgid "sharpness" +msgstr "" + +# Default: Similar to +msgid "similar-to" +msgstr "" + +# Default: Smart canonical episode title +msgid "smart-canonical-episode-title" +msgstr "" + +# Default: Smart canonical series title +msgid "smart-canonical-series-title" +msgstr "" + +# Default: Smart canonical title +msgid "smart-canonical-title" +msgstr "" + +# Default: Smart long imdb canonical title +msgid "smart-long-imdb-canonical-title" +msgstr "" + +# Default: Sound clips +msgid "sound-clips" +msgstr "" + +# Default: Sound crew +msgid "sound-crew" +msgstr "" + +# Default: Sound encoding +msgid "sound-encoding" +msgstr "" + +# Default: Sound mix +msgid "sound-mix" +msgstr "" + +# Default: Soundtrack +msgid "soundtrack" +msgstr "" + +# Default: Spaciality +msgid "spaciality" +msgstr "" + +# Default: Special effects +msgid "special-effects" +msgstr "" + +# Default: Special effects companies +msgid "special-effects-companies" +msgstr "" + +# Default: Special effects department +msgid "special-effects-department" +msgstr "" + +# Default: Spin off +msgid "spin-off" +msgstr "" + +# Default: Spin off from +msgid "spin-off-from" +msgstr "" + +# Default: Spoofed in +msgid "spoofed-in" +msgstr "" + +# Default: Spoofs +msgid "spoofs" +msgstr "" + +# Default: Spouse +msgid "spouse" +msgstr "" + +# Default: Status of availablility +msgid "status-of-availablility" +msgstr "" + +# Default: Studio +msgid "studio" +msgstr "" + +# Default: Studios +msgid "studios" +msgstr "" + +# Default: Stunt performer +msgid "stunt-performer" +msgstr "" + +# Default: Stunts +msgid "stunts" +msgstr "" + +# Default: Subtitles +msgid "subtitles" +msgstr "" + +# Default: Supplement +msgid "supplement" +msgstr "" + +# Default: Supplements +msgid "supplements" +msgstr "" + +# Default: Synopsis +msgid "synopsis" +msgstr "" + +# Default: Taglines +msgid "taglines" +msgstr "" + +# Default: Tech info +msgid "tech-info" +msgstr "" + +# Default: Thanks +msgid "thanks" +msgstr "" + +# Default: Time +msgid "time" +msgstr "" + +# Default: Title +msgid "title" +msgstr "" + +# Default: Titles in this product +msgid "titles-in-this-product" +msgstr "" + +# Default: To +msgid "to" +msgstr "" + +# Default: Top 250 rank +msgid "top-250-rank" +msgstr "" + +# Default: Trade mark +msgid "trade-mark" +msgstr "" + +# Default: Transportation department +msgid "transportation-department" +msgstr "" + +# Default: Trivia +msgid "trivia" +msgstr "" + +# Default: Tv +msgid "tv" +msgstr "" + +# Default: Under license from +msgid "under-license-from" +msgstr "" + +# Default: Unknown link +msgid "unknown-link" +msgstr "" + +# Default: Upc +msgid "upc" +msgstr "" + +# Default: Version of +msgid "version-of" +msgstr "" + +# Default: Vhs +msgid "vhs" +msgstr "" + +# Default: Video +msgid "video" +msgstr "" + +# Default: Video artifacts +msgid "video-artifacts" +msgstr "" + +# Default: Video clips +msgid "video-clips" +msgstr "" + +# Default: Video noise +msgid "video-noise" +msgstr "" + +# Default: Video quality +msgid "video-quality" +msgstr "" + +# Default: Video standard +msgid "video-standard" +msgstr "" + +# Default: Visual effects +msgid "visual-effects" +msgstr "" + +# Default: Votes +msgid "votes" +msgstr "" + +# Default: Votes distribution +msgid "votes-distribution" +msgstr "" + +# Default: Weekend gross +msgid "weekend-gross" +msgstr "" + +# Default: Where now +msgid "where-now" +msgstr "" + +# Default: With +msgid "with" +msgstr "" + +# Default: Writer +msgid "writer" +msgstr "" + +# Default: Written by +msgid "written-by" +msgstr "" + +# Default: Year +msgid "year" +msgstr "" + +# Default: Zshops +msgid "zshops" +msgstr "" + diff --git a/lib/imdb/locale/msgfmt.py b/lib/imdb/locale/msgfmt.py new file mode 100644 index 0000000000000000000000000000000000000000..9e0ab747337eb01898aac9e107f015c2ddf9ae25 --- /dev/null +++ b/lib/imdb/locale/msgfmt.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- +"""Generate binary message catalog from textual translation description. + +This program converts a textual Uniforum-style message catalog (.po file) into +a binary GNU catalog (.mo file). This is essentially the same function as the +GNU msgfmt program, however, it is a simpler implementation. + +Usage: msgfmt.py [OPTIONS] filename.po + +Options: + -o file + --output-file=file + Specify the output file to write to. If omitted, output will go to a + file named filename.mo (based off the input file name). + + -h + --help + Print this message and exit. + + -V + --version + Display version information and exit. + +Written by Martin v. L�wis <loewis@informatik.hu-berlin.de>, +refactored / fixed by Thomas Waldmann <tw AT waldmann-edv DOT de>. +""" + +import sys, os +import getopt, struct, array + +__version__ = "1.3" + +class SyntaxErrorException(Exception): + """raised when having trouble parsing the po file content""" + pass + +class MsgFmt(object): + """transform .po -> .mo format""" + def __init__(self): + self.messages = {} + + def make_filenames(self, filename, outfile=None): + """Compute .mo name from .po name or language""" + if filename.endswith('.po'): + infile = filename + else: + infile = filename + '.po' + if outfile is None: + outfile = os.path.splitext(infile)[0] + '.mo' + return infile, outfile + + def add(self, id, str, fuzzy): + """Add a non-fuzzy translation to the dictionary.""" + if not fuzzy and str: + self.messages[id] = str + + def read_po(self, lines): + ID = 1 + STR = 2 + section = None + fuzzy = False + line_no = 0 + msgid = msgstr = '' + # Parse the catalog + for line in lines: + line_no += 1 + # If we get a comment line after a msgstr, this is a new entry + if line.startswith('#') and section == STR: + self.add(msgid, msgstr, fuzzy) + section = None + fuzzy = False + # Record a fuzzy mark + if line.startswith('#,') and 'fuzzy' in line: + fuzzy = True + # Skip comments + if line.startswith('#'): + continue + # Now we are in a msgid section, output previous section + if line.startswith('msgid'): + if section == STR: + self.add(msgid, msgstr, fuzzy) + fuzzy = False + section = ID + line = line[5:] + msgid = msgstr = '' + # Now we are in a msgstr section + elif line.startswith('msgstr'): + section = STR + line = line[6:] + # Skip empty lines + line = line.strip() + if not line: + continue + # XXX: Does this always follow Python escape semantics? + line = eval(line) + if section == ID: + msgid += line + elif section == STR: + msgstr += line + else: + raise SyntaxErrorException('Syntax error on line %d, before:\n%s' % (line_no, line)) + # Add last entry + if section == STR: + self.add(msgid, msgstr, fuzzy) + + def generate_mo(self): + """Return the generated output.""" + keys = self.messages.keys() + # the keys are sorted in the .mo file + keys.sort() + offsets = [] + ids = '' + strs = '' + for id in keys: + # For each string, we need size and file offset. Each string is NUL + # terminated; the NUL does not count into the size. + offsets.append((len(ids), len(id), len(strs), len(self.messages[id]))) + ids += id + '\0' + strs += self.messages[id] + '\0' + output = [] + # The header is 7 32-bit unsigned integers. We don't use hash tables, so + # the keys start right after the index tables. + # translated string. + keystart = 7*4 + 16*len(keys) + # and the values start after the keys + valuestart = keystart + len(ids) + koffsets = [] + voffsets = [] + # The string table first has the list of keys, then the list of values. + # Each entry has first the size of the string, then the file offset. + for o1, l1, o2, l2 in offsets: + koffsets += [l1, o1 + keystart] + voffsets += [l2, o2 + valuestart] + offsets = koffsets + voffsets + output.append(struct.pack("Iiiiiii", + 0x950412deL, # Magic + 0, # Version + len(keys), # # of entries + 7*4, # start of key index + 7*4 + len(keys)*8, # start of value index + 0, 0)) # size and offset of hash table + output.append(array.array("i", offsets).tostring()) + output.append(ids) + output.append(strs) + return ''.join(output) + + +def make(filename, outfile): + mf = MsgFmt() + infile, outfile = mf.make_filenames(filename, outfile) + try: + lines = file(infile).readlines() + except IOError, msg: + print >> sys.stderr, msg + sys.exit(1) + try: + mf.read_po(lines) + output = mf.generate_mo() + except SyntaxErrorException, msg: + print >> sys.stderr, msg + + try: + open(outfile, "wb").write(output) + except IOError, msg: + print >> sys.stderr, msg + + +def usage(code, msg=''): + print >> sys.stderr, __doc__ + if msg: + print >> sys.stderr, msg + sys.exit(code) + + +def main(): + try: + opts, args = getopt.getopt(sys.argv[1:], 'hVo:', ['help', 'version', 'output-file=']) + except getopt.error, msg: + usage(1, msg) + + outfile = None + # parse options + for opt, arg in opts: + if opt in ('-h', '--help'): + usage(0) + elif opt in ('-V', '--version'): + print >> sys.stderr, "msgfmt.py", __version__ + sys.exit(0) + elif opt in ('-o', '--output-file'): + outfile = arg + # do it + if not args: + print >> sys.stderr, 'No input file given' + print >> sys.stderr, "Try `msgfmt --help' for more information." + return + + for filename in args: + make(filename, outfile) + + +if __name__ == '__main__': + main() + diff --git a/lib/imdb/locale/rebuildmo.py b/lib/imdb/locale/rebuildmo.py new file mode 100644 index 0000000000000000000000000000000000000000..b72a74c3691fd3cb2a43f4ca2355a13921fa543b --- /dev/null +++ b/lib/imdb/locale/rebuildmo.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +rebuildmo.py script. + +This script builds the .mo files, from the .po files. + +Copyright 2009 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import glob +import msgfmt +import os + +#LOCALE_DIR = os.path.dirname(__file__) + +def rebuildmo(): + lang_glob = 'imdbpy-*.po' + created = [] + for input_file in glob.glob(lang_glob): + lang = input_file[7:-3] + if not os.path.exists(lang): + os.mkdir(lang) + mo_dir = os.path.join(lang, 'LC_MESSAGES') + if not os.path.exists(mo_dir): + os.mkdir(mo_dir) + output_file = os.path.join(mo_dir, 'imdbpy.mo') + msgfmt.make(input_file, output_file) + created.append(lang) + return created + + +if __name__ == '__main__': + languages = rebuildmo() + print 'Created locale for: %s.' % ' '.join(languages) + diff --git a/lib/imdb/parser/__init__.py b/lib/imdb/parser/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4c3c90a85187cbb573d4992f3e3315aa1568a31d --- /dev/null +++ b/lib/imdb/parser/__init__.py @@ -0,0 +1,28 @@ +""" +parser package (imdb package). + +This package provides various parsers to access IMDb data (e.g.: a +parser for the web/http interface, a parser for the SQL database +interface, etc.). +So far, the http/httpThin, mobile and sql parsers are implemented. + +Copyright 2004-2009 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +__all__ = ['http', 'mobile', 'sql'] + + diff --git a/lib/imdb/parser/http/__init__.py b/lib/imdb/parser/http/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e49e7bb6d7323dab2372e4ce068dd89c834a201a --- /dev/null +++ b/lib/imdb/parser/http/__init__.py @@ -0,0 +1,830 @@ +""" +parser.http package (imdb package). + +This package provides the IMDbHTTPAccessSystem class used to access +IMDb's data through the web interface. +the imdb.IMDb function will return an instance of this class when +called with the 'accessSystem' argument set to "http" or "web" +or "html" (this is the default). + +Copyright 2004-2012 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys +import socket +import logging +from urllib import FancyURLopener, quote_plus +from codecs import lookup + +from imdb import IMDbBase, imdbURL_movie_main, imdbURL_person_main, \ + imdbURL_character_main, imdbURL_company_main, \ + imdbURL_keyword_main, imdbURL_find, imdbURL_top250, \ + imdbURL_bottom100 +from imdb.utils import analyze_title +from imdb._exceptions import IMDbDataAccessError, IMDbParserError + +import searchMovieParser +import searchPersonParser +import searchCharacterParser +import searchCompanyParser +import searchKeywordParser +import movieParser +import personParser +import characterParser +import companyParser +import topBottomParser + +# Logger for miscellaneous functions. +_aux_logger = logging.getLogger('imdbpy.parser.http.aux') + +IN_GAE = False +try: + import google.appengine + IN_GAE = True + _aux_logger.info('IMDbPY is running in the Google App Engine environment') +except ImportError: + pass + + +class _ModuleProxy: + """A proxy to instantiate and access parsers.""" + def __init__(self, module, defaultKeys=None, oldParsers=False, + useModule=None, fallBackToNew=False): + """Initialize a proxy for the given module; defaultKeys, if set, + muste be a dictionary of values to set for instanced objects.""" + if oldParsers or fallBackToNew: + _aux_logger.warn('The old set of parsers was removed; falling ' \ + 'back to the new parsers.') + self.useModule = useModule + if defaultKeys is None: + defaultKeys = {} + self._defaultKeys = defaultKeys + self._module = module + + def __getattr__(self, name): + """Called only when no look-up is found.""" + _sm = self._module + # Read the _OBJECTS dictionary to build the asked parser. + if name in _sm._OBJECTS: + _entry = _sm._OBJECTS[name] + # Initialize the parser. + kwds = {} + if self.useModule: + kwds = {'useModule': self.useModule} + parserClass = _entry[0][0] + obj = parserClass(**kwds) + attrsToSet = self._defaultKeys.copy() + attrsToSet.update(_entry[1] or {}) + # Set attribute to the object. + for key in attrsToSet: + setattr(obj, key, attrsToSet[key]) + setattr(self, name, obj) + return obj + return getattr(_sm, name) + + +PY_VERSION = sys.version_info[:2] + + +# The cookies for the "adult" search. +# Please don't mess with these account. +# Old 'IMDbPY' account. +_old_cookie_id = 'boM2bYxz9MCsOnH9gZ0S9QHs12NWrNdApxsls1Vb5/NGrNdjcHx3dUas10UASoAjVEvhAbGagERgOpNkAPvxdbfKwaV2ikEj9SzXY1WPxABmDKQwdqzwRbM+12NSeJFGUEx3F8as10WwidLzVshDtxaPIbP13NdjVS9UZTYqgTVGrNcT9vyXU1' +_old_cookie_uu = '3M3AXsquTU5Gur/Svik+ewflPm5Rk2ieY3BIPlLjyK3C0Dp9F8UoPgbTyKiGtZp4x1X+uAUGKD7BM2g+dVd8eqEzDErCoYvdcvGLvVLAen1y08hNQtALjVKAe+1hM8g9QbNonlG1/t4S82ieUsBbrSIQbq1yhV6tZ6ArvSbA7rgHc8n5AdReyAmDaJ5Wm/ee3VDoCnGj/LlBs2ieUZNorhHDKK5Q==' +# New 'IMDbPYweb' account. +_cookie_id = 'rH1jNAkjTlNXvHolvBVBsgaPICNZbNdjVjzFwzas9JRmusdjVoqBs/Hs12NR+1WFxEoR9bGKEDUg6sNlADqXwkas12N131Rwdb+UQNGKN8PWrNdjcdqBQVLq8mbGDHP3hqzxhbD692NQi9D0JjpBtRaPIbP1zNdjUOqENQYv1ADWrNcT9vyXU1' +_cookie_uu = 'su4/m8cho4c6HP+W1qgq6wchOmhnF0w+lIWvHjRUPJ6nRA9sccEafjGADJ6hQGrMd4GKqLcz2X4z5+w+M4OIKnRn7FpENH7dxDQu3bQEHyx0ZEyeRFTPHfQEX03XF+yeN1dsPpcXaqjUZAw+lGRfXRQEfz3RIX9IgVEffdBAHw2wQXyf9xdMPrQELw0QNB8dsffsqcdQemjPB0w+moLcPh0JrKrHJ9hjBzdMPpcXTH7XRwwOk=' + +# imdbpy2010 account. +#_cookie_id = 'QrCdxVi+L+WgqOLrQJJgBgRRXGInphxiBPU/YXSFDyExMFzCp6YcYgSVXyEUhS/xMID8wqemHGID4DlntwZ49vemP5UXsAxiJ4D6goSmHGIgNT9hMXBaRSF2vMS3phxB0bVfQiQlP1RxdrzhB6YcRHFASyIhQVowwXCKtDSlD2YhgRvxBsCKtGemHBKH9mxSI=' +#_cookie_uu = 'oiEo2yoJFCA2Zbn/o7Z1LAPIwotAu6QdALv3foDb1x5F/tdrFY63XkSfty4kntS8Y8jkHSDLt3406+d+JThEilPI0mtTaOQdA/t2/iErp22jaLdeVU5ya4PIREpj7HFdpzhEHadcIAngSER50IoHDpD6Bz4Qy3b+UIhE/hBbhz5Q63ceA2hEvhPo5B0FnrL9Q8jkWjDIbA0Au3d+AOtnXoCIRL4Q28c+UOtnXpP4RL4T6OQdA+6ijUCI5B0AW2d+UOtnXpPYRL4T6OQdA8jkTUOYlC0A==' + + +class _FakeURLOpener(object): + """Fake URLOpener object, used to return empty strings instead of + errors. + """ + def __init__(self, url, headers): + self.url = url + self.headers = headers + def read(self, *args, **kwds): return '' + def close(self, *args, **kwds): pass + def info(self, *args, **kwds): return self.headers + + +class IMDbURLopener(FancyURLopener): + """Fetch web pages and handle errors.""" + _logger = logging.getLogger('imdbpy.parser.http.urlopener') + + def __init__(self, *args, **kwargs): + self._last_url = u'' + FancyURLopener.__init__(self, *args, **kwargs) + # Headers to add to every request. + # XXX: IMDb's web server doesn't like urllib-based programs, + # so lets fake to be Mozilla. + # Wow! I'm shocked by my total lack of ethic! <g> + for header in ('User-Agent', 'User-agent', 'user-agent'): + self.del_header(header) + self.set_header('User-Agent', 'Mozilla/5.0') + # XXX: This class is used also to perform "Exact Primary + # [Title|Name]" searches, and so by default the cookie is set. + c_header = 'id=%s; uu=%s' % (_cookie_id, _cookie_uu) + self.set_header('Cookie', c_header) + + def get_proxy(self): + """Return the used proxy, or an empty string.""" + return self.proxies.get('http', '') + + def set_proxy(self, proxy): + """Set the proxy.""" + if not proxy: + if self.proxies.has_key('http'): + del self.proxies['http'] + else: + if not proxy.lower().startswith('http://'): + proxy = 'http://%s' % proxy + self.proxies['http'] = proxy + + def set_header(self, header, value, _overwrite=True): + """Set a default header.""" + if _overwrite: + self.del_header(header) + self.addheaders.append((header, value)) + + def get_header(self, header): + """Return the first value of a header, or None + if not present.""" + for index in xrange(len(self.addheaders)): + if self.addheaders[index][0] == header: + return self.addheaders[index][1] + return None + + def del_header(self, header): + """Remove a default header.""" + for index in xrange(len(self.addheaders)): + if self.addheaders[index][0] == header: + del self.addheaders[index] + break + + def retrieve_unicode(self, url, size=-1): + """Retrieves the given URL, and returns a unicode string, + trying to guess the encoding of the data (assuming latin_1 + by default)""" + encode = None + try: + if size != -1: + self.set_header('Range', 'bytes=0-%d' % size) + uopener = self.open(url) + kwds = {} + if PY_VERSION > (2, 3) and not IN_GAE: + kwds['size'] = size + content = uopener.read(**kwds) + self._last_url = uopener.url + # Maybe the server is so nice to tell us the charset... + server_encode = uopener.info().getparam('charset') + # Otherwise, look at the content-type HTML meta tag. + if server_encode is None and content: + first_bytes = content[:512] + begin_h = first_bytes.find('text/html; charset=') + if begin_h != -1: + end_h = first_bytes[19+begin_h:].find('"') + if end_h != -1: + server_encode = first_bytes[19+begin_h:19+begin_h+end_h] + if server_encode: + try: + if lookup(server_encode): + encode = server_encode + except (LookupError, ValueError, TypeError): + pass + uopener.close() + if size != -1: + self.del_header('Range') + self.close() + except IOError, e: + if size != -1: + # Ensure that the Range header is removed. + self.del_header('Range') + raise IMDbDataAccessError({'errcode': e.errno, + 'errmsg': str(e.strerror), + 'url': url, + 'proxy': self.get_proxy(), + 'exception type': 'IOError', + 'original exception': e}) + if encode is None: + encode = 'latin_1' + # The detection of the encoding is error prone... + self._logger.warn('Unable to detect the encoding of the retrieved ' + 'page [%s]; falling back to default latin1.', encode) + ##print unicode(content, encode, 'replace').encode('utf8') + return unicode(content, encode, 'replace') + + def http_error_default(self, url, fp, errcode, errmsg, headers): + if errcode == 404: + self._logger.warn('404 code returned for %s: %s (headers: %s)', + url, errmsg, headers) + return _FakeURLOpener(url, headers) + raise IMDbDataAccessError({'url': 'http:%s' % url, + 'errcode': errcode, + 'errmsg': errmsg, + 'headers': headers, + 'error type': 'http_error_default', + 'proxy': self.get_proxy()}) + + def open_unknown(self, fullurl, data=None): + raise IMDbDataAccessError({'fullurl': fullurl, + 'data': str(data), + 'error type': 'open_unknown', + 'proxy': self.get_proxy()}) + + def open_unknown_proxy(self, proxy, fullurl, data=None): + raise IMDbDataAccessError({'proxy': str(proxy), + 'fullurl': fullurl, + 'error type': 'open_unknown_proxy', + 'data': str(data)}) + + +class IMDbHTTPAccessSystem(IMDbBase): + """The class used to access IMDb's data through the web.""" + + accessSystem = 'http' + _http_logger = logging.getLogger('imdbpy.parser.http') + + def __init__(self, isThin=0, adultSearch=1, proxy=-1, oldParsers=False, + fallBackToNew=False, useModule=None, cookie_id=-1, + timeout=30, cookie_uu=None, *arguments, **keywords): + """Initialize the access system.""" + IMDbBase.__init__(self, *arguments, **keywords) + self.urlOpener = IMDbURLopener() + # When isThin is set, we're parsing the "maindetails" page + # of a movie (instead of the "combined" page) and movie/person + # references are not collected if no defaultModFunct is provided. + # + # NOTE: httpThin was removed since IMDbPY 4.8. + self.isThin = isThin + self._getRefs = True + self._mdparse = False + if isThin: + self._http_logger.warn('"httpThin" access system no longer ' + + 'supported; "http" used automatically', exc_info=False) + self.isThin = 0 + if self.accessSystem in ('httpThin', 'webThin', 'htmlThin'): + self.accessSystem = 'http' + self.set_timeout(timeout) + self.do_adult_search(adultSearch) + if cookie_id != -1: + if cookie_id is None: + self.del_cookies() + elif cookie_uu is not None: + self.set_cookies(cookie_id, cookie_uu) + if proxy != -1: + self.set_proxy(proxy) + if useModule is not None: + if not isinstance(useModule, (list, tuple)) and ',' in useModule: + useModule = useModule.split(',') + _def = {'_modFunct': self._defModFunct, '_as': self.accessSystem} + # Proxy objects. + self.smProxy = _ModuleProxy(searchMovieParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.spProxy = _ModuleProxy(searchPersonParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.scProxy = _ModuleProxy(searchCharacterParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.scompProxy = _ModuleProxy(searchCompanyParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.skProxy = _ModuleProxy(searchKeywordParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.mProxy = _ModuleProxy(movieParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.pProxy = _ModuleProxy(personParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.cProxy = _ModuleProxy(characterParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.compProxy = _ModuleProxy(companyParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + self.topBottomProxy = _ModuleProxy(topBottomParser, defaultKeys=_def, + oldParsers=oldParsers, useModule=useModule, + fallBackToNew=fallBackToNew) + + def _normalize_movieID(self, movieID): + """Normalize the given movieID.""" + try: + return '%07d' % int(movieID) + except ValueError, e: + raise IMDbParserError('invalid movieID "%s": %s' % (movieID, e)) + + def _normalize_personID(self, personID): + """Normalize the given personID.""" + try: + return '%07d' % int(personID) + except ValueError, e: + raise IMDbParserError('invalid personID "%s": %s' % (personID, e)) + + def _normalize_characterID(self, characterID): + """Normalize the given characterID.""" + try: + return '%07d' % int(characterID) + except ValueError, e: + raise IMDbParserError('invalid characterID "%s": %s' % \ + (characterID, e)) + + def _normalize_companyID(self, companyID): + """Normalize the given companyID.""" + try: + return '%07d' % int(companyID) + except ValueError, e: + raise IMDbParserError('invalid companyID "%s": %s' % \ + (companyID, e)) + + def get_imdbMovieID(self, movieID): + """Translate a movieID in an imdbID; in this implementation + the movieID _is_ the imdbID. + """ + return movieID + + def get_imdbPersonID(self, personID): + """Translate a personID in an imdbID; in this implementation + the personID _is_ the imdbID. + """ + return personID + + def get_imdbCharacterID(self, characterID): + """Translate a characterID in an imdbID; in this implementation + the characterID _is_ the imdbID. + """ + return characterID + + def get_imdbCompanyID(self, companyID): + """Translate a companyID in an imdbID; in this implementation + the companyID _is_ the imdbID. + """ + return companyID + + def get_proxy(self): + """Return the used proxy or an empty string.""" + return self.urlOpener.get_proxy() + + def set_proxy(self, proxy): + """Set the web proxy to use. + + It should be a string like 'http://localhost:8080/'; if the + string is empty, no proxy will be used. + If set, the value of the environment variable HTTP_PROXY is + automatically used. + """ + self.urlOpener.set_proxy(proxy) + + def set_timeout(self, timeout): + """Set the default timeout, in seconds, of the connection.""" + try: + timeout = int(timeout) + except Exception: + timeout = 0 + if timeout <= 0: + timeout = None + socket.setdefaulttimeout(timeout) + + def set_cookies(self, cookie_id, cookie_uu): + """Set a cookie to access an IMDb's account.""" + c_header = 'id=%s; uu=%s' % (cookie_id, cookie_uu) + self.urlOpener.set_header('Cookie', c_header) + + def del_cookies(self): + """Remove the used cookie.""" + self.urlOpener.del_header('Cookie') + + def do_adult_search(self, doAdult, + cookie_id=_cookie_id, cookie_uu=_cookie_uu): + """If doAdult is true, 'adult' movies are included in the + search results; cookie_id and cookie_uu are optional + parameters to select a specific account (see your cookie + or cookies.txt file.""" + if doAdult: + self.set_cookies(cookie_id, cookie_uu) + #c_header = 'id=%s; uu=%s' % (cookie_id, cookie_uu) + #self.urlOpener.set_header('Cookie', c_header) + else: + self.urlOpener.del_header('Cookie') + + def _retrieve(self, url, size=-1, _noCookies=False): + """Retrieve the given URL.""" + ##print url + _cookies = None + # XXX: quite obscene, but in some very limited + # cases (/ttXXXXXXX/epdate) if the cookies + # are set, a 500 error is returned. + if _noCookies: + _cookies = self.urlOpener.get_header('Cookie') + self.del_cookies() + self._http_logger.debug('fetching url %s (size: %d)', url, size) + try: + ret = self.urlOpener.retrieve_unicode(url, size=size) + finally: + if _noCookies and _cookies: + self.urlOpener.set_header('Cookie', _cookies) + return ret + + def _get_search_content(self, kind, ton, results): + """Retrieve the web page for a given search. + kind can be 'tt' (for titles), 'nm' (for names), + 'char' (for characters) or 'co' (for companies). + ton is the title or the name to search. + results is the maximum number of results to be retrieved.""" + if isinstance(ton, unicode): + try: + ton = ton.encode('iso8859-1') + except Exception, e: + try: + ton = ton.encode('utf-8') + except Exception, e: + pass + ##params = 'q=%s&%s=on&mx=%s' % (quote_plus(ton), kind, str(results)) + params = 'q=%s;s=%s;mx=%s' % (quote_plus(ton), kind, str(results)) + if kind == 'ep': + params = params.replace('s=ep;', 's=tt;ttype=ep;', 1) + cont = self._retrieve(self.urls['find'] % params) + #print 'URL:', imdbURL_find % params + if cont.find('Your search returned more than') == -1 or \ + cont.find("displayed the exact matches") == -1: + return cont + # The retrieved page contains no results, because too many + # titles or names contain the string we're looking for. + params = 'q=%s;ls=%s;lm=0' % (quote_plus(ton), kind) + size = 131072 + results * 512 + return self._retrieve(self.urls['find'] % params, size=size) + + def _search_movie(self, title, results): + # The URL of the query. + # XXX: To retrieve the complete results list: + # params = urllib.urlencode({'more': 'tt', 'q': title}) + ##params = urllib.urlencode({'tt': 'on','mx': str(results),'q': title}) + ##params = 'q=%s&tt=on&mx=%s' % (quote_plus(title), str(results)) + ##cont = self._retrieve(imdbURL_find % params) + cont = self._get_search_content('tt', title, results) + return self.smProxy.search_movie_parser.parse(cont, results=results)['data'] + + def _search_episode(self, title, results): + t_dict = analyze_title(title) + if t_dict['kind'] == 'episode': + title = t_dict['title'] + cont = self._get_search_content('ep', title, results) + return self.smProxy.search_movie_parser.parse(cont, results=results)['data'] + + def get_movie_main(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'combined') + return self.mProxy.movie_parser.parse(cont, mdparse=self._mdparse) + + def get_movie_full_credits(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'fullcredits') + return self.mProxy.movie_parser.parse(cont) + + def get_movie_plot(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'plotsummary') + return self.mProxy.plot_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_awards(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'awards') + return self.mProxy.movie_awards_parser.parse(cont) + + def get_movie_taglines(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'taglines') + return self.mProxy.taglines_parser.parse(cont) + + def get_movie_keywords(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'keywords') + return self.mProxy.keywords_parser.parse(cont) + + def get_movie_alternate_versions(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'alternateversions') + return self.mProxy.alternateversions_parser.parse(cont, + getRefs=self._getRefs) + + def get_movie_crazy_credits(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'crazycredits') + return self.mProxy.crazycredits_parser.parse(cont, + getRefs=self._getRefs) + + def get_movie_goofs(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'goofs') + return self.mProxy.goofs_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_quotes(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'quotes') + return self.mProxy.quotes_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_release_dates(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'releaseinfo') + ret = self.mProxy.releasedates_parser.parse(cont) + ret['info sets'] = ('release dates', 'akas') + return ret + get_movie_akas = get_movie_release_dates + get_movie_release_info = get_movie_release_dates + + def get_movie_vote_details(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'ratings') + return self.mProxy.ratings_parser.parse(cont) + + def get_movie_official_sites(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'officialsites') + return self.mProxy.officialsites_parser.parse(cont) + + def get_movie_trivia(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'trivia') + return self.mProxy.trivia_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_connections(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'movieconnections') + return self.mProxy.connections_parser.parse(cont) + + def get_movie_technical(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'technical') + return self.mProxy.tech_parser.parse(cont) + + def get_movie_business(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'business') + return self.mProxy.business_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_literature(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'literature') + return self.mProxy.literature_parser.parse(cont) + + def get_movie_locations(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'locations') + return self.mProxy.locations_parser.parse(cont) + + def get_movie_soundtrack(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'soundtrack') + return self.mProxy.soundtrack_parser.parse(cont) + + def get_movie_dvd(self, movieID): + self._http_logger.warn('dvd information no longer available', exc_info=False) + return {} + + def get_movie_recommendations(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'recommendations') + return self.mProxy.rec_parser.parse(cont) + + def get_movie_external_reviews(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'externalreviews') + return self.mProxy.externalrev_parser.parse(cont) + + def get_movie_newsgroup_reviews(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'newsgroupreviews') + return self.mProxy.newsgrouprev_parser.parse(cont) + + def get_movie_misc_sites(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'miscsites') + return self.mProxy.misclinks_parser.parse(cont) + + def get_movie_sound_clips(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'soundsites') + return self.mProxy.soundclips_parser.parse(cont) + + def get_movie_video_clips(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'videosites') + return self.mProxy.videoclips_parser.parse(cont) + + def get_movie_photo_sites(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'photosites') + return self.mProxy.photosites_parser.parse(cont) + + def get_movie_news(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'news') + return self.mProxy.news_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_amazon_reviews(self, movieID): + self._http_logger.warn('amazon review no longer available', exc_info=False) + return {} + + def get_movie_guests(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'epcast') + return self.mProxy.episodes_cast_parser.parse(cont) + get_movie_episodes_cast = get_movie_guests + + def get_movie_merchandising_links(self, movieID): + self._http_logger.warn('merchandising links no longer available', + exc_info=False) + return {} + + def _purge_seasons_data(self, data_d): + if '_current_season' in data_d['data']: + del data_d['data']['_current_season'] + if '_seasons' in data_d['data']: + del data_d['data']['_seasons'] + return data_d + + def get_movie_episodes(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'episodes') + data_d = self.mProxy.season_episodes_parser.parse(cont) + if not data_d and 'data' in data_d: + return {} + _current_season = data_d['data'].get('_current_season', '') + _seasons = data_d['data'].get('_seasons') or [] + data_d = self._purge_seasons_data(data_d) + data_d['data'].setdefault('episodes', {}) + + nr_eps = len(data_d['data']['episodes'].get(_current_season) or []) + + for season in _seasons: + if season == _current_season: + continue + other_cont = self._retrieve(self.urls['movie_main'] % movieID + 'episodes?season=' + str(season)) + other_d = self.mProxy.season_episodes_parser.parse(other_cont) + other_d = self._purge_seasons_data(other_d) + other_d['data'].setdefault('episodes', {}) + if not (other_d and other_d['data'] and other_d['data']['episodes'][season]): + continue + nr_eps += len(other_d['data']['episodes'].get(season) or []) + data_d['data']['episodes'][season] = other_d['data']['episodes'][season] + data_d['data']['number of episodes'] = nr_eps + return data_d + + def get_movie_episodes_rating(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'epdate', _noCookies=True) + data_d = self.mProxy.eprating_parser.parse(cont) + # set movie['episode of'].movieID for every episode. + if data_d.get('data', {}).has_key('episodes rating'): + for item in data_d['data']['episodes rating']: + episode = item['episode'] + episode['episode of'].movieID = movieID + return data_d + + def get_movie_faqs(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'faq') + return self.mProxy.movie_faqs_parser.parse(cont, getRefs=self._getRefs) + + def get_movie_airing(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'tvschedule') + return self.mProxy.airing_parser.parse(cont) + + get_movie_tv_schedule = get_movie_airing + + def get_movie_synopsis(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'synopsis') + return self.mProxy.synopsis_parser.parse(cont) + + def get_movie_parents_guide(self, movieID): + cont = self._retrieve(self.urls['movie_main'] % movieID + 'parentalguide') + return self.mProxy.parentsguide_parser.parse(cont) + + def _search_person(self, name, results): + # The URL of the query. + # XXX: To retrieve the complete results list: + # params = urllib.urlencode({'more': 'nm', 'q': name}) + ##params = urllib.urlencode({'nm': 'on', 'mx': str(results), 'q': name}) + #params = 'q=%s&nm=on&mx=%s' % (quote_plus(name), str(results)) + #cont = self._retrieve(imdbURL_find % params) + cont = self._get_search_content('nm', name, results) + return self.spProxy.search_person_parser.parse(cont, results=results)['data'] + + def get_person_main(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'maindetails') + ret = self.pProxy.maindetails_parser.parse(cont) + ret['info sets'] = ('main', 'filmography') + return ret + + def get_person_filmography(self, personID): + return self.get_person_main(personID) + + def get_person_biography(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'bio') + return self.pProxy.bio_parser.parse(cont, getRefs=self._getRefs) + + def get_person_awards(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'awards') + return self.pProxy.person_awards_parser.parse(cont) + + def get_person_other_works(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'otherworks') + return self.pProxy.otherworks_parser.parse(cont, getRefs=self._getRefs) + + #def get_person_agent(self, personID): + # cont = self._retrieve(self.urls['person_main'] % personID + 'agent') + # return self.pProxy.agent_parser.parse(cont) + + def get_person_publicity(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'publicity') + return self.pProxy.publicity_parser.parse(cont) + + def get_person_official_sites(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'officialsites') + return self.pProxy.person_officialsites_parser.parse(cont) + + def get_person_news(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'news') + return self.pProxy.news_parser.parse(cont) + + def get_person_episodes(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'filmoseries') + return self.pProxy.person_series_parser.parse(cont) + + def get_person_merchandising_links(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'forsale') + return self.pProxy.sales_parser.parse(cont) + + def get_person_genres_links(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'filmogenre') + return self.pProxy.person_genres_parser.parse(cont) + + def get_person_keywords_links(self, personID): + cont = self._retrieve(self.urls['person_main'] % personID + 'filmokey') + return self.pProxy.person_keywords_parser.parse(cont) + + def _search_character(self, name, results): + cont = self._get_search_content('char', name, results) + return self.scProxy.search_character_parser.parse(cont, results=results)['data'] + + def get_character_main(self, characterID): + cont = self._retrieve(self.urls['character_main'] % characterID) + ret = self.cProxy.character_main_parser.parse(cont) + ret['info sets'] = ('main', 'filmography') + return ret + + get_character_filmography = get_character_main + + def get_character_biography(self, characterID): + cont = self._retrieve(self.urls['character_main'] % characterID + 'bio') + return self.cProxy.character_bio_parser.parse(cont, + getRefs=self._getRefs) + + def get_character_episodes(self, characterID): + cont = self._retrieve(self.urls['character_main'] % characterID + + 'filmoseries') + return self.cProxy.character_series_parser.parse(cont) + + def get_character_quotes(self, characterID): + cont = self._retrieve(self.urls['character_main'] % characterID + 'quotes') + return self.cProxy.character_quotes_parser.parse(cont, + getRefs=self._getRefs) + + def _search_company(self, name, results): + cont = self._get_search_content('co', name, results) + url = self.urlOpener._last_url + return self.scompProxy.search_company_parser.parse(cont, url=url, + results=results)['data'] + + def get_company_main(self, companyID): + cont = self._retrieve(self.urls['company_main'] % companyID) + ret = self.compProxy.company_main_parser.parse(cont) + return ret + + def _search_keyword(self, keyword, results): + # XXX: the IMDb web server seems to have some serious problem with + # non-ascii keyword. + # E.g.: http://akas.imdb.com/keyword/fianc%E9/ + # will return a 500 Internal Server Error: Redirect Recursion. + keyword = keyword.encode('utf8', 'ignore') + try: + cont = self._get_search_content('kw', keyword, results) + except IMDbDataAccessError: + self._http_logger.warn('unable to search for keyword %s', keyword, + exc_info=True) + return [] + return self.skProxy.search_keyword_parser.parse(cont, results=results)['data'] + + def _get_keyword(self, keyword, results): + keyword = keyword.encode('utf8', 'ignore') + try: + cont = self._retrieve(self.urls['keyword_main'] % keyword) + except IMDbDataAccessError: + self._http_logger.warn('unable to get keyword %s', keyword, + exc_info=True) + return [] + return self.skProxy.search_moviekeyword_parser.parse(cont, results=results)['data'] + + def _get_top_bottom_movies(self, kind): + if kind == 'top': + parser = self.topBottomProxy.top250_parser + url = self.urls['top250'] + elif kind == 'bottom': + parser = self.topBottomProxy.bottom100_parser + url = self.urls['bottom100'] + else: + return [] + cont = self._retrieve(url) + return parser.parse(cont)['data'] + + diff --git a/lib/imdb/parser/http/bsouplxml/__init__.py b/lib/imdb/parser/http/bsouplxml/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/imdb/parser/http/bsouplxml/_bsoup.py b/lib/imdb/parser/http/bsouplxml/_bsoup.py new file mode 100644 index 0000000000000000000000000000000000000000..afab5da988f52e36b71d97e4685cd65184af616a --- /dev/null +++ b/lib/imdb/parser/http/bsouplxml/_bsoup.py @@ -0,0 +1,1970 @@ +""" +imdb.parser.http._bsoup module (imdb.parser.http package). +This is the BeautifulSoup.py module, not modified; it's included here +so that it's not an external dependency. + +Beautiful Soup +Elixir and Tonic +"The Screen-Scraper's Friend" +http://www.crummy.com/software/BeautifulSoup/ + +Beautiful Soup parses a (possibly invalid) XML or HTML document into a +tree representation. It provides methods and Pythonic idioms that make +it easy to navigate, search, and modify the tree. + +A well-formed XML/HTML document yields a well-formed data +structure. An ill-formed XML/HTML document yields a correspondingly +ill-formed data structure. If your document is only locally +well-formed, you can use this library to find and process the +well-formed part of it. + +Beautiful Soup works with Python 2.2 and up. It has no external +dependencies, but you'll have more success at converting data to UTF-8 +if you also install these three packages: + +* chardet, for auto-detecting character encodings + http://chardet.feedparser.org/ +* cjkcodecs and iconv_codec, which add more encodings to the ones supported + by stock Python. + http://cjkpython.i18n.org/ + +Beautiful Soup defines classes for two main parsing strategies: + + * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific + language that kind of looks like XML. + + * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid + or invalid. This class has web browser-like heuristics for + obtaining a sensible parse tree in the face of common HTML errors. + +Beautiful Soup also defines a class (UnicodeDammit) for autodetecting +the encoding of an HTML or XML document, and converting it to +Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. + +For more than you ever wanted to know about Beautiful Soup, see the +documentation: +http://www.crummy.com/software/BeautifulSoup/documentation.html + +Here, have some legalese: + +Copyright (c) 2004-2008, Leonard Richardson + +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. + + * Neither the name of the the Beautiful Soup Consortium and All + Night Kosher Bakery nor the names of its contributors may 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 OWNER 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, DAMMIT. + +""" +from __future__ import generators + +__author__ = "Leonard Richardson (leonardr@segfault.org)" +__version__ = "3.0.7a" +__copyright__ = "Copyright (c) 2004-2008 Leonard Richardson" +__license__ = "New-style BSD" + +from sgmllib import SGMLParser, SGMLParseError +import codecs +import markupbase +import types +import re +import sgmllib +try: + from htmlentitydefs import name2codepoint +except ImportError: + name2codepoint = {} +try: + set +except NameError: + from sets import Set as set + +#These hacks make Beautiful Soup able to parse XML with namespaces +sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') +markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match + +DEFAULT_OUTPUT_ENCODING = "utf-8" + +# First, the classes that represent markup elements. + +class PageElement: + """Contains the navigational information for some part of the page + (either a tag or a piece of text)""" + + def setup(self, parent=None, previous=None): + """Sets up the initial relations between this element and + other elements.""" + self.parent = parent + self.previous = previous + self.next = None + self.previousSibling = None + self.nextSibling = None + if self.parent and self.parent.contents: + self.previousSibling = self.parent.contents[-1] + self.previousSibling.nextSibling = self + + def replaceWith(self, replaceWith): + oldParent = self.parent + myIndex = self.parent.contents.index(self) + if hasattr(replaceWith, 'parent') and replaceWith.parent == self.parent: + # We're replacing this element with one of its siblings. + index = self.parent.contents.index(replaceWith) + if index and index < myIndex: + # Furthermore, it comes before this element. That + # means that when we extract it, the index of this + # element will change. + myIndex = myIndex - 1 + self.extract() + oldParent.insert(myIndex, replaceWith) + + def extract(self): + """Destructively rips this element out of the tree.""" + if self.parent: + try: + self.parent.contents.remove(self) + except ValueError: + pass + + #Find the two elements that would be next to each other if + #this element (and any children) hadn't been parsed. Connect + #the two. + lastChild = self._lastRecursiveChild() + nextElement = lastChild.next + + if self.previous: + self.previous.next = nextElement + if nextElement: + nextElement.previous = self.previous + self.previous = None + lastChild.next = None + + self.parent = None + if self.previousSibling: + self.previousSibling.nextSibling = self.nextSibling + if self.nextSibling: + self.nextSibling.previousSibling = self.previousSibling + self.previousSibling = self.nextSibling = None + return self + + def _lastRecursiveChild(self): + "Finds the last element beneath this object to be parsed." + lastChild = self + while hasattr(lastChild, 'contents') and lastChild.contents: + lastChild = lastChild.contents[-1] + return lastChild + + def insert(self, position, newChild): + if (isinstance(newChild, basestring) + or isinstance(newChild, unicode)) \ + and not isinstance(newChild, NavigableString): + newChild = NavigableString(newChild) + + position = min(position, len(self.contents)) + if hasattr(newChild, 'parent') and newChild.parent != None: + # We're 'inserting' an element that's already one + # of this object's children. + if newChild.parent == self: + index = self.find(newChild) + if index and index < position: + # Furthermore we're moving it further down the + # list of this object's children. That means that + # when we extract this element, our target index + # will jump down one. + position = position - 1 + newChild.extract() + + newChild.parent = self + previousChild = None + if position == 0: + newChild.previousSibling = None + newChild.previous = self + else: + previousChild = self.contents[position-1] + newChild.previousSibling = previousChild + newChild.previousSibling.nextSibling = newChild + newChild.previous = previousChild._lastRecursiveChild() + if newChild.previous: + newChild.previous.next = newChild + + newChildsLastElement = newChild._lastRecursiveChild() + + if position >= len(self.contents): + newChild.nextSibling = None + + parent = self + parentsNextSibling = None + while not parentsNextSibling: + parentsNextSibling = parent.nextSibling + parent = parent.parent + if not parent: # This is the last element in the document. + break + if parentsNextSibling: + newChildsLastElement.next = parentsNextSibling + else: + newChildsLastElement.next = None + else: + nextChild = self.contents[position] + newChild.nextSibling = nextChild + if newChild.nextSibling: + newChild.nextSibling.previousSibling = newChild + newChildsLastElement.next = nextChild + + if newChildsLastElement.next: + newChildsLastElement.next.previous = newChildsLastElement + self.contents.insert(position, newChild) + + def append(self, tag): + """Appends the given tag to the contents of this tag.""" + self.insert(len(self.contents), tag) + + def findNext(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears after this Tag in the document.""" + return self._findOne(self.findAllNext, name, attrs, text, **kwargs) + + def findAllNext(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.nextGenerator, + **kwargs) + + def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears after this Tag in the document.""" + return self._findOne(self.findNextSiblings, name, attrs, text, + **kwargs) + + def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.nextSiblingGenerator, **kwargs) + fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x + + def findPrevious(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears before this Tag in the document.""" + return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) + + def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.previousGenerator, + **kwargs) + fetchPrevious = findAllPrevious # Compatibility with pre-3.x + + def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears before this Tag in the document.""" + return self._findOne(self.findPreviousSiblings, name, attrs, text, + **kwargs) + + def findPreviousSiblings(self, name=None, attrs={}, text=None, + limit=None, **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.previousSiblingGenerator, **kwargs) + fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x + + def findParent(self, name=None, attrs={}, **kwargs): + """Returns the closest parent of this Tag that matches the given + criteria.""" + # NOTE: We can't use _findOne because findParents takes a different + # set of arguments. + r = None + l = self.findParents(name, attrs, 1) + if l: + r = l[0] + return r + + def findParents(self, name=None, attrs={}, limit=None, **kwargs): + """Returns the parents of this Tag that match the given + criteria.""" + + return self._findAll(name, attrs, None, limit, self.parentGenerator, + **kwargs) + fetchParents = findParents # Compatibility with pre-3.x + + #These methods do the real heavy lifting. + + def _findOne(self, method, name, attrs, text, **kwargs): + r = None + l = method(name, attrs, text, 1, **kwargs) + if l: + r = l[0] + return r + + def _findAll(self, name, attrs, text, limit, generator, **kwargs): + "Iterates over a generator looking for things that match." + + if isinstance(name, SoupStrainer): + strainer = name + else: + # Build a SoupStrainer + strainer = SoupStrainer(name, attrs, text, **kwargs) + results = ResultSet(strainer) + g = generator() + while True: + try: + i = g.next() + except StopIteration: + break + if i: + found = strainer.search(i) + if found: + results.append(found) + if limit and len(results) >= limit: + break + return results + + #These Generators can be used to navigate starting from both + #NavigableStrings and Tags. + def nextGenerator(self): + i = self + while i: + i = i.next + yield i + + def nextSiblingGenerator(self): + i = self + while i: + i = i.nextSibling + yield i + + def previousGenerator(self): + i = self + while i: + i = i.previous + yield i + + def previousSiblingGenerator(self): + i = self + while i: + i = i.previousSibling + yield i + + def parentGenerator(self): + i = self + while i: + i = i.parent + yield i + + # Utility methods + def substituteEncoding(self, str, encoding=None): + encoding = encoding or "utf-8" + return str.replace("%SOUP-ENCODING%", encoding) + + def toEncoding(self, s, encoding=None): + """Encodes an object to a string in some encoding, or to Unicode. + .""" + if isinstance(s, unicode): + if encoding: + s = s.encode(encoding) + elif isinstance(s, str): + if encoding: + s = s.encode(encoding) + else: + s = unicode(s) + else: + if encoding: + s = self.toEncoding(str(s), encoding) + else: + s = unicode(s) + return s + +class NavigableString(unicode, PageElement): + + def __new__(cls, value): + """Create a new NavigableString. + + When unpickling a NavigableString, this method is called with + the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be + passed in to the superclass's __new__ or the superclass won't know + how to handle non-ASCII characters. + """ + if isinstance(value, unicode): + return unicode.__new__(cls, value) + return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) + + def __getnewargs__(self): + return (NavigableString.__str__(self),) + + def __getattr__(self, attr): + """text.string gives you text. This is for backwards + compatibility for Navigable*String, but for CData* it lets you + get the string without the CData wrapper.""" + if attr == 'string': + return self + else: + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + + def __unicode__(self): + return str(self).decode(DEFAULT_OUTPUT_ENCODING) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + if encoding: + return self.encode(encoding) + else: + return self + +class CData(NavigableString): + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding) + +class ProcessingInstruction(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + output = self + if "%SOUP-ENCODING%" in output: + output = self.substituteEncoding(output, encoding) + return "<?%s?>" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "<!--%s-->" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "<!%s>" % NavigableString.__str__(self, encoding) + +class Tag(PageElement): + + """Represents a found HTML tag with its attributes and contents.""" + + def _invert(h): + "Cheap function to invert a hash." + i = {} + for k,v in h.items(): + i[v] = k + return i + + XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", + "quot" : '"', + "amp" : "&", + "lt" : "<", + "gt" : ">" } + + XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) + + def _convertEntities(self, match): + """Used in a call to re.sub to replace HTML, XML, and numeric + entities with the appropriate Unicode characters. If HTML + entities are being converted, any unrecognized entities are + escaped.""" + x = match.group(1) + if self.convertHTMLEntities and x in name2codepoint: + return unichr(name2codepoint[x]) + elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: + if self.convertXMLEntities: + return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] + else: + return u'&%s;' % x + elif len(x) > 0 and x[0] == '#': + # Handle numeric entities + if len(x) > 1 and x[1] == 'x': + return unichr(int(x[2:], 16)) + else: + return unichr(int(x[1:])) + + elif self.escapeUnrecognizedEntities: + return u'&%s;' % x + else: + return u'&%s;' % x + + def __init__(self, parser, name, attrs=None, parent=None, + previous=None): + "Basic constructor." + + # We don't actually store the parser object: that lets extracted + # chunks be garbage-collected + self.parserClass = parser.__class__ + self.isSelfClosing = parser.isSelfClosingTag(name) + self.name = name + if attrs == None: + attrs = [] + self.attrs = attrs + self.contents = [] + self.setup(parent, previous) + self.hidden = False + self.containsSubstitutions = False + self.convertHTMLEntities = parser.convertHTMLEntities + self.convertXMLEntities = parser.convertXMLEntities + self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities + + # Convert any HTML, XML, or numeric entities in the attribute values. + convert = lambda(k, val): (k, + re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", + self._convertEntities, + val)) + self.attrs = map(convert, self.attrs) + + def get(self, key, default=None): + """Returns the value of the 'key' attribute for the tag, or + the value given for 'default' if it doesn't have that + attribute.""" + return self._getAttrMap().get(key, default) + + def has_key(self, key): + return self._getAttrMap().has_key(key) + + def __getitem__(self, key): + """tag[key] returns the value of the 'key' attribute for the tag, + and throws an exception if it's not there.""" + return self._getAttrMap()[key] + + def __iter__(self): + "Iterating over a tag iterates over its contents." + return iter(self.contents) + + def __len__(self): + "The length of a tag is the length of its list of contents." + return len(self.contents) + + def __contains__(self, x): + return x in self.contents + + def __nonzero__(self): + "A tag is non-None even if it has no contents." + return True + + def __setitem__(self, key, value): + """Setting tag[key] sets the value of the 'key' attribute for the + tag.""" + self._getAttrMap() + self.attrMap[key] = value + found = False + for i in range(0, len(self.attrs)): + if self.attrs[i][0] == key: + self.attrs[i] = (key, value) + found = True + if not found: + self.attrs.append((key, value)) + self._getAttrMap()[key] = value + + def __delitem__(self, key): + "Deleting tag[key] deletes all 'key' attributes for the tag." + for item in self.attrs: + if item[0] == key: + self.attrs.remove(item) + #We don't break because bad HTML can define the same + #attribute multiple times. + self._getAttrMap() + if self.attrMap.has_key(key): + del self.attrMap[key] + + def __call__(self, *args, **kwargs): + """Calling a tag like a function is the same as calling its + findAll() method. Eg. tag('a') returns a list of all the A tags + found within this tag.""" + return apply(self.findAll, args, kwargs) + + def __getattr__(self, tag): + #print "Getattr %s.%s" % (self.__class__, tag) + if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + return self.find(tag[:-3]) + elif tag.find('__') != 0: + return self.find(tag) + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) + + def __eq__(self, other): + """Returns true iff this tag has the same name, the same attributes, + and the same contents (recursively) as the given tag. + + NOTE: right now this will return false if two tags have the + same attributes in a different order. Should this be fixed?""" + if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): + return False + for i in range(0, len(self.contents)): + if self.contents[i] != other.contents[i]: + return False + return True + + def __ne__(self, other): + """Returns true iff this tag is not identical to the other tag, + as defined in __eq__.""" + return not self == other + + def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): + """Renders this tag as a string.""" + return self.__str__(encoding) + + def __unicode__(self): + return self.__str__(None) + + BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + + ")") + + def _sub_entity(self, x): + """Used with a regular expression to substitute the + appropriate XML entity for an XML special character.""" + return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Returns a string or Unicode representation of this tag and + its contents. To get Unicode, pass None for encoding. + + NOTE: since Python's HTML parser consumes whitespace, this + method is not certain to reproduce the whitespace present in + the original string.""" + + encodedName = self.toEncoding(self.name, encoding) + + attrs = [] + if self.attrs: + for key, val in self.attrs: + fmt = '%s="%s"' + if isString(val): + if self.containsSubstitutions and '%SOUP-ENCODING%' in val: + val = self.substituteEncoding(val, encoding) + + # The attribute value either: + # + # * Contains no embedded double quotes or single quotes. + # No problem: we enclose it in double quotes. + # * Contains embedded single quotes. No problem: + # double quotes work here too. + # * Contains embedded double quotes. No problem: + # we enclose it in single quotes. + # * Embeds both single _and_ double quotes. This + # can't happen naturally, but it can happen if + # you modify an attribute value after parsing + # the document. Now we have a bit of a + # problem. We solve it by enclosing the + # attribute in single quotes, and escaping any + # embedded single quotes to XML entities. + if '"' in val: + fmt = "%s='%s'" + if "'" in val: + # TODO: replace with apos when + # appropriate. + val = val.replace("'", "&squot;") + + # Now we're okay w/r/t quotes. But the attribute + # value might also contain angle brackets, or + # ampersands that aren't part of entities. We need + # to escape those to XML entities too. + val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) + + attrs.append(fmt % (self.toEncoding(key, encoding), + self.toEncoding(val, encoding))) + close = '' + closeTag = '' + if self.isSelfClosing: + close = ' /' + else: + closeTag = '</%s>' % encodedName + + indentTag, indentContents = 0, 0 + if prettyPrint: + indentTag = indentLevel + space = (' ' * (indentTag-1)) + indentContents = indentTag + 1 + contents = self.renderContents(encoding, prettyPrint, indentContents) + if self.hidden: + s = contents + else: + s = [] + attributeString = '' + if attrs: + attributeString = ' ' + ' '.join(attrs) + if prettyPrint: + s.append(space) + s.append('<%s%s%s>' % (encodedName, attributeString, close)) + if prettyPrint: + s.append("\n") + s.append(contents) + if prettyPrint and contents and contents[-1] != "\n": + s.append("\n") + if prettyPrint and closeTag: + s.append(space) + s.append(closeTag) + if prettyPrint and closeTag and self.nextSibling: + s.append("\n") + s = ''.join(s) + return s + + def decompose(self): + """Recursively destroys the contents of this tree.""" + contents = [i for i in self.contents] + for i in contents: + if isinstance(i, Tag): + i.decompose() + else: + i.extract() + self.extract() + + def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): + return self.__str__(encoding, True) + + def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Renders the contents of this tag as a string in the given + encoding. If encoding is None, returns a Unicode string..""" + s=[] + for c in self: + text = None + if isinstance(c, NavigableString): + text = c.__str__(encoding) + elif isinstance(c, Tag): + s.append(c.__str__(encoding, prettyPrint, indentLevel)) + if text and prettyPrint: + text = text.strip() + if text: + if prettyPrint: + s.append(" " * (indentLevel-1)) + s.append(text) + if prettyPrint: + s.append("\n") + return ''.join(s) + + #Soup methods + + def find(self, name=None, attrs={}, recursive=True, text=None, + **kwargs): + """Return only the first child of this Tag matching the given + criteria.""" + r = None + l = self.findAll(name, attrs, recursive, text, 1, **kwargs) + if l: + r = l[0] + return r + findChild = find + + def findAll(self, name=None, attrs={}, recursive=True, text=None, + limit=None, **kwargs): + """Extracts a list of Tag objects that match the given + criteria. You can specify the name of the Tag and any + attributes you want the Tag to have. + + The value of a key-value pair in the 'attrs' map can be a + string, a list of strings, a regular expression object, or a + callable that takes a string and returns whether or not the + string matches for some custom definition of 'matches'. The + same is true of the tag name.""" + generator = self.recursiveChildGenerator + if not recursive: + generator = self.childGenerator + return self._findAll(name, attrs, text, limit, generator, **kwargs) + findChildren = findAll + + # Pre-3.x compatibility methods + first = find + fetch = findAll + + def fetchText(self, text=None, recursive=True, limit=None): + return self.findAll(text=text, recursive=recursive, limit=limit) + + def firstText(self, text=None, recursive=True): + return self.find(text=text, recursive=recursive) + + #Private methods + + def _getAttrMap(self): + """Initializes a map representation of this tag's attributes, + if not already initialized.""" + if not getattr(self, 'attrMap'): + self.attrMap = {} + for (key, value) in self.attrs: + self.attrMap[key] = value + return self.attrMap + + #Generator methods + def childGenerator(self): + for i in range(0, len(self.contents)): + yield self.contents[i] + raise StopIteration + + def recursiveChildGenerator(self): + stack = [(self, 0)] + while stack: + tag, start = stack.pop() + if isinstance(tag, Tag): + for i in range(start, len(tag.contents)): + a = tag.contents[i] + yield a + if isinstance(a, Tag) and tag.contents: + if i < len(tag.contents) - 1: + stack.append((tag, i+1)) + stack.append((a, 0)) + break + raise StopIteration + +# Next, a couple classes to represent queries and their results. +class SoupStrainer: + """Encapsulates a number of ways of matching a markup element (tag or + text).""" + + def __init__(self, name=None, attrs={}, text=None, **kwargs): + self.name = name + if isString(attrs): + kwargs['class'] = attrs + attrs = None + if kwargs: + if attrs: + attrs = attrs.copy() + attrs.update(kwargs) + else: + attrs = kwargs + self.attrs = attrs + self.text = text + + def __str__(self): + if self.text: + return self.text + else: + return "%s|%s" % (self.name, self.attrs) + + def searchTag(self, markupName=None, markupAttrs={}): + found = None + markup = None + if isinstance(markupName, Tag): + markup = markupName + markupAttrs = markup + callFunctionWithTagData = callable(self.name) \ + and not isinstance(markupName, Tag) + + if (not self.name) \ + or callFunctionWithTagData \ + or (markup and self._matches(markup, self.name)) \ + or (not markup and self._matches(markupName, self.name)): + if callFunctionWithTagData: + match = self.name(markupName, markupAttrs) + else: + match = True + markupAttrMap = None + for attr, matchAgainst in self.attrs.items(): + if not markupAttrMap: + if hasattr(markupAttrs, 'get'): + markupAttrMap = markupAttrs + else: + markupAttrMap = {} + for k,v in markupAttrs: + markupAttrMap[k] = v + attrValue = markupAttrMap.get(attr) + if not self._matches(attrValue, matchAgainst): + match = False + break + if match: + if markup: + found = markup + else: + found = markupName + return found + + def search(self, markup): + #print 'looking for %s in %s' % (self, markup) + found = None + # If given a list of items, scan it for a text element that + # matches. + if isList(markup) and not isinstance(markup, Tag): + for element in markup: + if isinstance(element, NavigableString) \ + and self.search(element): + found = element + break + # If it's a Tag, make sure its name or attributes match. + # Don't bother with Tags if we're searching for text. + elif isinstance(markup, Tag): + if not self.text: + found = self.searchTag(markup) + # If it's text, make sure the text matches. + elif isinstance(markup, NavigableString) or \ + isString(markup): + if self._matches(markup, self.text): + found = markup + else: + raise Exception, "I don't know how to match against a %s" \ + % markup.__class__ + return found + + def _matches(self, markup, matchAgainst): + #print "Matching %s against %s" % (markup, matchAgainst) + result = False + if matchAgainst == True and type(matchAgainst) == types.BooleanType: + result = markup != None + elif callable(matchAgainst): + result = matchAgainst(markup) + else: + #Custom match methods take the tag as an argument, but all + #other ways of matching match the tag name as a string. + if isinstance(markup, Tag): + markup = markup.name + if markup and not isString(markup): + markup = unicode(markup) + #Now we know that chunk is either a string, or None. + if hasattr(matchAgainst, 'match'): + # It's a regexp object. + result = markup and matchAgainst.search(markup) + elif isList(matchAgainst): + result = markup in matchAgainst + elif hasattr(matchAgainst, 'items'): + result = markup.has_key(matchAgainst) + elif matchAgainst and isString(markup): + if isinstance(markup, unicode): + matchAgainst = unicode(matchAgainst) + else: + matchAgainst = str(matchAgainst) + + if not result: + result = matchAgainst == markup + return result + +class ResultSet(list): + """A ResultSet is just a list that keeps track of the SoupStrainer + that created it.""" + def __init__(self, source): + list.__init__([]) + self.source = source + +# Now, some helper functions. + +def isList(l): + """Convenience method that works with all 2.x versions of Python + to determine whether or not something is listlike.""" + return hasattr(l, '__iter__') \ + or (type(l) in (types.ListType, types.TupleType)) + +def isString(s): + """Convenience method that works with all 2.x versions of Python + to determine whether or not something is stringlike.""" + try: + return isinstance(s, unicode) or isinstance(s, basestring) + except NameError: + return isinstance(s, str) + +def buildTagMap(default, *args): + """Turns a list of maps, lists, or scalars into a single map. + Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and + NESTING_RESET_TAGS maps out of lists and partial maps.""" + built = {} + for portion in args: + if hasattr(portion, 'items'): + #It's a map. Merge it. + for k,v in portion.items(): + built[k] = v + elif isList(portion): + #It's a list. Map each item to the default. + for k in portion: + built[k] = default + else: + #It's a scalar. Map it to the default. + built[portion] = default + return built + +# Now, the parser classes. + +class BeautifulStoneSoup(Tag, SGMLParser): + + """This class contains the basic parser and search code. It defines + a parser that knows nothing about tag behavior except for the + following: + + You can't close a tag without closing all the tags it encloses. + That is, "<foo><bar></foo>" actually means + "<foo><bar></bar></foo>". + + [Another possible explanation is "<foo><bar /></foo>", but since + this class defines no SELF_CLOSING_TAGS, it will never use that + explanation.] + + This class is useful for parsing XML or made-up markup languages, + or when BeautifulSoup makes an assumption counter to what you were + expecting.""" + + SELF_CLOSING_TAGS = {} + NESTABLE_TAGS = {} + RESET_NESTING_TAGS = {} + QUOTE_TAGS = {} + PRESERVE_WHITESPACE_TAGS = [] + + MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), + lambda x: x.group(1) + ' />'), + (re.compile('<!\s+([^<>]*)>'), + lambda x: '<!' + x.group(1) + '>') + ] + + ROOT_TAG_NAME = u'[document]' + + HTML_ENTITIES = "html" + XML_ENTITIES = "xml" + XHTML_ENTITIES = "xhtml" + # TODO: This only exists for backwards-compatibility + ALL_ENTITIES = XHTML_ENTITIES + + # Used when determining whether a text node is all whitespace and + # can be replaced with a single space. A text node that contains + # fancy Unicode spaces (usually non-breaking) should be left + # alone. + STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } + + def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, + markupMassage=True, smartQuotesTo=XML_ENTITIES, + convertEntities=None, selfClosingTags=None, isHTML=False): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser. + + sgmllib will process most bad HTML, and the BeautifulSoup + class has some tricks for dealing with some HTML that kills + sgmllib, but Beautiful Soup can nonetheless choke or lose data + if your data uses self-closing tags or declarations + incorrectly. + + By default, Beautiful Soup uses regexes to sanitize input, + avoiding the vast majority of these problems. If the problems + don't apply to you, pass in False for markupMassage, and + you'll get better performance. + + The default parser massage techniques fix the two most common + instances of invalid HTML that choke sgmllib: + + <br/> (No space between name of closing tag and tag close) + <! --Comment--> (Extraneous whitespace in declaration) + + You can pass in a custom list of (RE object, replace method) + tuples to get Beautiful Soup to scrub your input the way you + want.""" + + self.parseOnlyThese = parseOnlyThese + self.fromEncoding = fromEncoding + self.smartQuotesTo = smartQuotesTo + self.convertEntities = convertEntities + # Set the rules for how we'll deal with the entities we + # encounter + if self.convertEntities: + # It doesn't make sense to convert encoded characters to + # entities even while you're converting entities to Unicode. + # Just convert it all to Unicode. + self.smartQuotesTo = None + if convertEntities == self.HTML_ENTITIES: + self.convertXMLEntities = False + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = True + elif convertEntities == self.XHTML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = False + elif convertEntities == self.XML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + else: + self.convertXMLEntities = False + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + + self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) + SGMLParser.__init__(self) + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + self.markup = markup + self.markupMassage = markupMassage + try: + self._feed(isHTML=isHTML) + except StopParsing: + pass + self.markup = None # The markup can now be GCed + + def convert_charref(self, name): + """This method fixes a bug in Python's SGMLParser.""" + try: + n = int(name) + except ValueError: + return + if not 0 <= n <= 127 : # ASCII ends at 127, not 255 + return + return self.convert_codepoint(n) + + def _feed(self, inDocumentEncoding=None, isHTML=False): + # Convert the document to Unicode. + markup = self.markup + if isinstance(markup, unicode): + if not hasattr(self, 'originalEncoding'): + self.originalEncoding = None + else: + dammit = UnicodeDammit\ + (markup, [self.fromEncoding, inDocumentEncoding], + smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) + markup = dammit.unicode + self.originalEncoding = dammit.originalEncoding + self.declaredHTMLEncoding = dammit.declaredHTMLEncoding + if markup: + if self.markupMassage: + if not isList(self.markupMassage): + self.markupMassage = self.MARKUP_MASSAGE + for fix, m in self.markupMassage: + markup = fix.sub(m, markup) + # TODO: We get rid of markupMassage so that the + # soup object can be deepcopied later on. Some + # Python installations can't copy regexes. If anyone + # was relying on the existence of markupMassage, this + # might cause problems. + del(self.markupMassage) + self.reset() + + SGMLParser.feed(self, markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def __getattr__(self, methodName): + """This method routes method call requests to either the SGMLParser + superclass or the Tag superclass, depending on the method name.""" + #print "__getattr__ called on %s.%s" % (self.__class__, methodName) + + if methodName.find('start_') == 0 or methodName.find('end_') == 0 \ + or methodName.find('do_') == 0: + return SGMLParser.__getattr__(self, methodName) + elif methodName.find('__') != 0: + return Tag.__getattr__(self, methodName) + else: + raise AttributeError + + def isSelfClosingTag(self, name): + """Returns true iff the given string is the name of a + self-closing tag according to this parser.""" + return self.SELF_CLOSING_TAGS.has_key(name) \ + or self.instanceSelfClosingTags.has_key(name) + + def reset(self): + Tag.__init__(self, self, self.ROOT_TAG_NAME) + self.hidden = 1 + SGMLParser.reset(self) + self.currentData = [] + self.currentTag = None + self.tagStack = [] + self.quoteStack = [] + self.pushTag(self) + + def popTag(self): + tag = self.tagStack.pop() + # Tags with just one string-owning child get the child as a + # 'string' property, so that soup.tag.string is shorthand for + # soup.tag.contents[0] + if len(self.currentTag.contents) == 1 and \ + isinstance(self.currentTag.contents[0], NavigableString): + self.currentTag.string = self.currentTag.contents[0] + + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.contents.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + + def endData(self, containerClass=NavigableString): + if self.currentData: + currentData = u''.join(self.currentData) + if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and + not set([tag.name for tag in self.tagStack]).intersection( + self.PRESERVE_WHITESPACE_TAGS)): + if '\n' in currentData: + currentData = '\n' + else: + currentData = ' ' + self.currentData = [] + if self.parseOnlyThese and len(self.tagStack) <= 1 and \ + (not self.parseOnlyThese.text or \ + not self.parseOnlyThese.search(currentData)): + return + o = containerClass(currentData) + o.setup(self.currentTag, self.previous) + if self.previous: + self.previous.next = o + self.previous = o + self.currentTag.contents.append(o) + + + def _popToTag(self, name, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + return + + numPops = 0 + mostRecentTag = None + for i in range(len(self.tagStack)-1, 0, -1): + if name == self.tagStack[i].name: + numPops = len(self.tagStack)-i + break + if not inclusivePop: + numPops = numPops - 1 + + for i in range(0, numPops): + mostRecentTag = self.popTag() + return mostRecentTag + + def _smartPop(self, name): + + """We need to pop up to the previous tag of this type, unless + one of this tag's nesting reset triggers comes between this + tag and the previous tag of this type, OR unless this tag is a + generic nesting trigger and another generic nesting trigger + comes between this tag and the previous tag of this type. + + Examples: + <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'. + <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'. + <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'. + + <li><ul><li> *<li>* should pop to 'ul', not the first 'li'. + <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr' + <td><tr><td> *<td>* should pop to 'tr', not the first 'td' + """ + + nestingResetTriggers = self.NESTABLE_TAGS.get(name) + isNestable = nestingResetTriggers != None + isResetNesting = self.RESET_NESTING_TAGS.has_key(name) + popTo = None + inclusive = True + for i in range(len(self.tagStack)-1, 0, -1): + p = self.tagStack[i] + if (not p or p.name == name) and not isNestable: + #Non-nestable tags get popped to the top or to their + #last occurance. + popTo = name + break + if (nestingResetTriggers != None + and p.name in nestingResetTriggers) \ + or (nestingResetTriggers == None and isResetNesting + and self.RESET_NESTING_TAGS.has_key(p.name)): + + #If we encounter one of the nesting reset triggers + #peculiar to this tag, or we encounter another tag + #that causes nesting to reset, pop up to but not + #including that tag. + popTo = p.name + inclusive = False + break + p = p.parent + if popTo: + self._popToTag(popTo, inclusive) + + def unknown_starttag(self, name, attrs, selfClosing=0): + #print "Start tag %s: %s" % (name, attrs) + if self.quoteStack: + #This is not a real tag. + #print "<%s> is not real!" % name + attrs = ''.join(map(lambda(x, y): ' %s="%s"' % (x, y), attrs)) + self.handle_data('<%s%s>' % (name, attrs)) + return + self.endData() + + if not self.isSelfClosingTag(name) and not selfClosing: + self._smartPop(name) + + if self.parseOnlyThese and len(self.tagStack) <= 1 \ + and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): + return + + tag = Tag(self, name, attrs, self.currentTag, self.previous) + if self.previous: + self.previous.next = tag + self.previous = tag + self.pushTag(tag) + if selfClosing or self.isSelfClosingTag(name): + self.popTag() + if name in self.QUOTE_TAGS: + #print "Beginning quote (%s)" % name + self.quoteStack.append(name) + self.literal = 1 + return tag + + def unknown_endtag(self, name): + #print "End tag %s" % name + if self.quoteStack and self.quoteStack[-1] != name: + #This is not a real end tag. + #print "</%s> is not real!" % name + self.handle_data('</%s>' % name) + return + self.endData() + self._popToTag(name) + if self.quoteStack and self.quoteStack[-1] == name: + self.quoteStack.pop() + self.literal = (len(self.quoteStack) > 0) + + def handle_data(self, data): + self.currentData.append(data) + + def _toStringSubclass(self, text, subclass): + """Adds a certain piece of text to the tree as a NavigableString + subclass.""" + self.endData() + self.handle_data(text) + self.endData(subclass) + + def handle_pi(self, text): + """Handle a processing instruction as a ProcessingInstruction + object, possibly one with a %SOUP-ENCODING% slot into which an + encoding will be plugged later.""" + if text[:3] == "xml": + text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" + self._toStringSubclass(text, ProcessingInstruction) + + def handle_comment(self, text): + "Handle comments as Comment objects." + self._toStringSubclass(text, Comment) + + def handle_charref(self, ref): + "Handle character references as data." + if self.convertEntities: + data = unichr(int(ref)) + else: + data = '&#%s;' % ref + self.handle_data(data) + + def handle_entityref(self, ref): + """Handle entity references as data, possibly converting known + HTML and/or XML entity references to the corresponding Unicode + characters.""" + data = None + if self.convertHTMLEntities: + try: + data = unichr(name2codepoint[ref]) + except KeyError: + pass + + if not data and self.convertXMLEntities: + data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) + + if not data and self.convertHTMLEntities and \ + not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): + # TODO: We've got a problem here. We're told this is + # an entity reference, but it's not an XML entity + # reference or an HTML entity reference. Nonetheless, + # the logical thing to do is to pass it through as an + # unrecognized entity reference. + # + # Except: when the input is "&carol;" this function + # will be called with input "carol". When the input is + # "AT&T", this function will be called with input + # "T". We have no way of knowing whether a semicolon + # was present originally, so we don't know whether + # this is an unknown entity or just a misplaced + # ampersand. + # + # The more common case is a misplaced ampersand, so I + # escape the ampersand and omit the trailing semicolon. + data = "&%s" % ref + if not data: + # This case is different from the one above, because we + # haven't already gone through a supposedly comprehensive + # mapping of entities to Unicode characters. We might not + # have gone through any mapping at all. So the chances are + # very high that this is a real entity, and not a + # misplaced ampersand. + data = "&%s;" % ref + self.handle_data(data) + + def handle_decl(self, data): + "Handle DOCTYPEs and the like as Declaration objects." + self._toStringSubclass(data, Declaration) + + def parse_declaration(self, i): + """Treat a bogus SGML declaration as raw data. Treat a CDATA + declaration as a CData object.""" + j = None + if self.rawdata[i:i+9] == '<![CDATA[': + k = self.rawdata.find(']]>', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self._toStringSubclass(data, CData) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.handle_data(toHandle) + j = i + len(toHandle) + return j + +class BeautifulSoup(BeautifulStoneSoup): + + """This parser knows the following facts about HTML: + + * Some tags have no closing tag and should be interpreted as being + closed as soon as they are encountered. + + * The text inside some tags (ie. 'script') may contain tags which + are not really part of the document and which should be parsed + as text, not tags. If you want to parse the text as tags, you can + always fetch it and parse it explicitly. + + * Tag nesting rules: + + Most tags can't be nested at all. For instance, the occurance of + a <p> tag should implicitly close the previous <p> tag. + + <p>Para1<p>Para2 + should be transformed into: + <p>Para1</p><p>Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a <blockquote> tag should _not_ implicitly close the previous + <blockquote> tag. + + Alice said: <blockquote>Bob said: <blockquote>Blah + should NOT be transformed into: + Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a <tr> tag should + implicitly close the previous <tr> tag within the same <table>, + but not close a <tr> tag in another table. + + <table><tr>Blah<tr>Blah + should be transformed into: + <table><tr>Blah</tr><tr>Blah + but, + <tr>Blah<table><tr>Blah + should NOT be transformed into + <tr>Blah<table></tr><tr>Blah + + Differing assumptions about tag nesting rules are a major source + of problems with the BeautifulSoup class. If BeautifulSoup is not + treating as nestable a tag your page author treats as nestable, + try ICantBelieveItsBeautifulSoup, MinimalSoup, or + BeautifulStoneSoup before writing your own subclass.""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('smartQuotesTo'): + kwargs['smartQuotesTo'] = self.HTML_ENTITIES + kwargs['isHTML'] = True + BeautifulStoneSoup.__init__(self, *args, **kwargs) + + SELF_CLOSING_TAGS = buildTagMap(None, + ['br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base']) + + PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) + + QUOTE_TAGS = {'script' : None, 'textarea' : None} + + #According to the HTML standard, each of these inline tags can + #contain another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_INLINE_TAGS = ['span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', + 'center'] + + #According to the HTML standard, these block tags can contain + #another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_BLOCK_TAGS = ['blockquote', 'div', 'fieldset', 'ins', 'del'] + + #Lists can contain other lists, but there are restrictions. + NESTABLE_LIST_TAGS = { 'ol' : [], + 'ul' : [], + 'li' : ['ul', 'ol'], + 'dl' : [], + 'dd' : ['dl'], + 'dt' : ['dl'] } + + #Tables can contain other tables, but there are restrictions. + NESTABLE_TABLE_TAGS = {'table' : [], + 'tr' : ['table', 'tbody', 'tfoot', 'thead'], + 'td' : ['tr'], + 'th' : ['tr'], + 'thead' : ['table'], + 'tbody' : ['table'], + 'tfoot' : ['table'], + } + + NON_NESTABLE_BLOCK_TAGS = ['address', 'form', 'p', 'pre'] + + #If one of these tags is encountered, all tags up to the next tag of + #this type are popped. + RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', + NON_NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, + NESTABLE_TABLE_TAGS) + + NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) + + # Used to detect the charset in a META tag; see start_meta + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + + def start_meta(self, attrs): + """Beautiful Soup can detect a charset included in a META tag, + try to convert the document to that charset, and re-parse the + document from the beginning.""" + httpEquiv = None + contentType = None + contentTypeIndex = None + tagNeedsEncodingSubstitution = False + + for i in range(0, len(attrs)): + key, value = attrs[i] + key = key.lower() + if key == 'http-equiv': + httpEquiv = value + elif key == 'content': + contentType = value + contentTypeIndex = i + + if httpEquiv and contentType: # It's an interesting meta tag. + match = self.CHARSET_RE.search(contentType) + if match: + if (self.declaredHTMLEncoding is not None or + self.originalEncoding == self.fromEncoding): + # An HTML encoding was sniffed while converting + # the document to Unicode, or an HTML encoding was + # sniffed during a previous pass through the + # document, or an encoding was specified + # explicitly and it worked. Rewrite the meta tag. + def rewrite(match): + return match.group(1) + "%SOUP-ENCODING%" + newAttr = self.CHARSET_RE.sub(rewrite, contentType) + attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], + newAttr) + tagNeedsEncodingSubstitution = True + else: + # This is our first pass through the document. + # Go through it again with the encoding information. + newCharset = match.group(3) + if newCharset and newCharset != self.originalEncoding: + self.declaredHTMLEncoding = newCharset + self._feed(self.declaredHTMLEncoding) + raise StopParsing + pass + tag = self.unknown_starttag("meta", attrs) + if tag and tagNeedsEncodingSubstitution: + tag.containsSubstitutions = True + +class StopParsing(Exception): + pass + +class ICantBelieveItsBeautifulSoup(BeautifulSoup): + + """The BeautifulSoup class is oriented towards skipping over + common HTML errors like unclosed tags. However, sometimes it makes + errors of its own. For instance, consider this fragment: + + <b>Foo<b>Bar</b></b> + + This is perfectly valid (if bizarre) HTML. However, the + BeautifulSoup class will implicitly close the first b tag when it + encounters the second 'b'. It will think the author wrote + "<b>Foo<b>Bar", and didn't close the first 'b' tag, because + there's no real-world reason to bold something that's already + bold. When it encounters '</b></b>' it will close two more 'b' + tags, for a grand total of three tags closed instead of two. This + can throw off the rest of your document structure. The same is + true of a number of other tags, listed below. + + It's much more common for someone to forget to close a 'b' tag + than to actually use nested 'b' tags, and the BeautifulSoup class + handles the common case. This class handles the not-co-common + case: where you can't believe someone wrote what they did, but + it's valid HTML and BeautifulSoup screwed up by assuming it + wouldn't be.""" + + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ + ['em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', + 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', + 'big'] + + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ['noscript'] + + NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) + +class MinimalSoup(BeautifulSoup): + """The MinimalSoup class is for parsing HTML that contains + pathologically bad markup. It makes no assumptions about tag + nesting, but it does know which tags are self-closing, that + <script> tags contain Javascript and should not be parsed, that + META tags may contain encoding information, and so on. + + This also makes it better for subclassing than BeautifulStoneSoup + or BeautifulSoup.""" + + RESET_NESTING_TAGS = buildTagMap('noscript') + NESTABLE_TAGS = {} + +class BeautifulSOAP(BeautifulStoneSoup): + """This class will push a tag with only a single string child into + the tag's parent as an attribute. The attribute's name is the tag + name, and the value is the string child. An example should give + the flavor of the change: + + <foo><bar>baz</bar></foo> + => + <foo bar="baz"><bar>baz</bar></foo> + + You can then access fooTag['bar'] instead of fooTag.barTag.string. + + This is, of course, useful for scraping structures that tend to + use subelements instead of attributes, such as SOAP messages. Note + that it modifies its input, so don't print the modified version + out. + + I'm not sure how many people really want to use this class; let me + know if you do. Mainly I like the name.""" + + def popTag(self): + if len(self.tagStack) > 1: + tag = self.tagStack[-1] + parent = self.tagStack[-2] + parent._getAttrMap() + if (isinstance(tag, Tag) and len(tag.contents) == 1 and + isinstance(tag.contents[0], NavigableString) and + not parent.attrMap.has_key(tag.name)): + parent[tag.name] = tag.contents[0] + BeautifulStoneSoup.popTag(self) + +#Enterprise class names! It has come to our attention that some people +#think the names of the Beautiful Soup parser classes are too silly +#and "unprofessional" for use in enterprise screen-scraping. We feel +#your pain! For such-minded folk, the Beautiful Soup Consortium And +#All-Night Kosher Bakery recommends renaming this file to +#"RobustParser.py" (or, in cases of extreme enterprisiness, +#"RobustParserBeanInterface.class") and using the following +#enterprise-friendly class aliases: +class RobustXMLParser(BeautifulStoneSoup): + pass +class RobustHTMLParser(BeautifulSoup): + pass +class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup): + pass +class RobustInsanelyWackAssHTMLParser(MinimalSoup): + pass +class SimplifyingSOAPParser(BeautifulSOAP): + pass + +###################################################### +# +# Bonus library: Unicode, Dammit +# +# This class forces XML data into a standard format (usually to UTF-8 +# or Unicode). It is heavily based on code from Mark Pilgrim's +# Universal Feed Parser. It does not rewrite the XML or HTML to +# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi +# (XML) and BeautifulSoup.start_meta (HTML). + +# Autodetects character encodings. +# Download from http://chardet.feedparser.org/ +try: + import chardet +# import chardet.constants +# chardet.constants._debug = 1 +except ImportError: + chardet = None + +# cjkcodecs and iconv_codec make Python know about more character encodings. +# Both are available from http://cjkpython.i18n.org/ +# They're built in if you use Python 2.4. +try: + import cjkcodecs.aliases +except ImportError: + pass +try: + import iconv_codec +except ImportError: + pass + +class UnicodeDammit: + """A class for detecting the encoding of a *ML document and + converting it to a Unicode string. If the source encoding is + windows-1252, can replace MS smart quotes with their HTML or XML + equivalents.""" + + # This dictionary maps commonly seen values for "charset" in HTML + # meta tags to the corresponding Python codec names. It only covers + # values that aren't in Python's aliases and can't be determined + # by the heuristics in find_codec. + CHARSET_ALIASES = { "macintosh" : "mac-roman", + "x-sjis" : "shift-jis" } + + def __init__(self, markup, overrideEncodings=[], + smartQuotesTo='xml', isHTML=False): + self.declaredHTMLEncoding = None + self.markup, documentEncoding, sniffedEncoding = \ + self._detectEncoding(markup, isHTML) + self.smartQuotesTo = smartQuotesTo + self.triedEncodings = [] + if markup == '' or isinstance(markup, unicode): + self.originalEncoding = None + self.unicode = unicode(markup) + return + + u = None + for proposedEncoding in overrideEncodings: + u = self._convertFrom(proposedEncoding) + if u: break + if not u: + for proposedEncoding in (documentEncoding, sniffedEncoding): + u = self._convertFrom(proposedEncoding) + if u: break + + # If no luck and we have auto-detection library, try that: + if not u and chardet and not isinstance(self.markup, unicode): + u = self._convertFrom(chardet.detect(self.markup)['encoding']) + + # As a last resort, try utf-8 and windows-1252: + if not u: + for proposed_encoding in ("utf-8", "windows-1252"): + u = self._convertFrom(proposed_encoding) + if u: break + + self.unicode = u + if not u: self.originalEncoding = None + + def _subMSChar(self, orig): + """Changes a MS smart quote character to an XML or HTML + entity.""" + sub = self.MS_CHARS.get(orig) + if type(sub) == types.TupleType: + if self.smartQuotesTo == 'xml': + sub = '&#x%s;' % sub[1] + else: + sub = '&%s;' % sub[0] + return sub + + def _convertFrom(self, proposed): + proposed = self.find_codec(proposed) + if not proposed or proposed in self.triedEncodings: + return None + self.triedEncodings.append(proposed) + markup = self.markup + + # Convert smart quotes to HTML if coming from an encoding + # that might have them. + if self.smartQuotesTo and proposed.lower() in("windows-1252", + "iso-8859-1", + "iso-8859-2"): + markup = re.compile("([\x80-\x9f])").sub \ + (lambda(x): self._subMSChar(x.group(1)), + markup) + + try: + # print "Trying to convert document to %s" % proposed + u = self._toUnicode(markup, proposed) + self.markup = u + self.originalEncoding = proposed + except Exception, e: + # print "That didn't work!" + # print e + return None + #print "Correct encoding: %s" % proposed + return self.markup + + def _toUnicode(self, data, encoding): + '''Given a string and its encoding, decodes the string into Unicode. + %encoding is a string recognized by encodings.aliases''' + + # strip Byte Order Mark (if present) + if (len(data) >= 4) and (data[:2] == '\xfe\xff') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16be' + data = data[2:] + elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16le' + data = data[2:] + elif data[:3] == '\xef\xbb\xbf': + encoding = 'utf-8' + data = data[3:] + elif data[:4] == '\x00\x00\xfe\xff': + encoding = 'utf-32be' + data = data[4:] + elif data[:4] == '\xff\xfe\x00\x00': + encoding = 'utf-32le' + data = data[4:] + newdata = unicode(data, encoding) + return newdata + + def _detectEncoding(self, xml_data, isHTML=False): + """Given a document, tries to detect its XML encoding.""" + xml_encoding = sniffed_xml_encoding = None + try: + if xml_data[:4] == '\x4c\x6f\xa7\x94': + # EBCDIC + xml_data = self._ebcdic_to_ascii(xml_data) + elif xml_data[:4] == '\x00\x3c\x00\x3f': + # UTF-16BE + sniffed_xml_encoding = 'utf-16be' + xml_data = unicode(xml_data, 'utf-16be').encode('utf-8') + elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \ + and (xml_data[2:4] != '\x00\x00'): + # UTF-16BE with BOM + sniffed_xml_encoding = 'utf-16be' + xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8') + elif xml_data[:4] == '\x3c\x00\x3f\x00': + # UTF-16LE + sniffed_xml_encoding = 'utf-16le' + xml_data = unicode(xml_data, 'utf-16le').encode('utf-8') + elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \ + (xml_data[2:4] != '\x00\x00'): + # UTF-16LE with BOM + sniffed_xml_encoding = 'utf-16le' + xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8') + elif xml_data[:4] == '\x00\x00\x00\x3c': + # UTF-32BE + sniffed_xml_encoding = 'utf-32be' + xml_data = unicode(xml_data, 'utf-32be').encode('utf-8') + elif xml_data[:4] == '\x3c\x00\x00\x00': + # UTF-32LE + sniffed_xml_encoding = 'utf-32le' + xml_data = unicode(xml_data, 'utf-32le').encode('utf-8') + elif xml_data[:4] == '\x00\x00\xfe\xff': + # UTF-32BE with BOM + sniffed_xml_encoding = 'utf-32be' + xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8') + elif xml_data[:4] == '\xff\xfe\x00\x00': + # UTF-32LE with BOM + sniffed_xml_encoding = 'utf-32le' + xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8') + elif xml_data[:3] == '\xef\xbb\xbf': + # UTF-8 with BOM + sniffed_xml_encoding = 'utf-8' + xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8') + else: + sniffed_xml_encoding = 'ascii' + pass + except: + xml_encoding_match = None + xml_encoding_match = re.compile( + '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data) + if not xml_encoding_match and isHTML: + regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I) + xml_encoding_match = regexp.search(xml_data) + if xml_encoding_match is not None: + xml_encoding = xml_encoding_match.groups()[0].lower() + if isHTML: + self.declaredHTMLEncoding = xml_encoding + if sniffed_xml_encoding and \ + (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', + 'iso-10646-ucs-4', 'ucs-4', 'csucs4', + 'utf-16', 'utf-32', 'utf_16', 'utf_32', + 'utf16', 'u16')): + xml_encoding = sniffed_xml_encoding + return xml_data, xml_encoding, sniffed_xml_encoding + + + def find_codec(self, charset): + return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \ + or (charset and self._codec(charset.replace("-", ""))) \ + or (charset and self._codec(charset.replace("-", "_"))) \ + or charset + + def _codec(self, charset): + if not charset: return charset + codec = None + try: + codecs.lookup(charset) + codec = charset + except (LookupError, ValueError): + pass + return codec + + EBCDIC_TO_ASCII_MAP = None + def _ebcdic_to_ascii(self, s): + c = self.__class__ + if not c.EBCDIC_TO_ASCII_MAP: + emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15, + 16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31, + 128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7, + 144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26, + 32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33, + 38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94, + 45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63, + 186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34, + 195,97,98,99,100,101,102,103,104,105,196,197,198,199,200, + 201,202,106,107,108,109,110,111,112,113,114,203,204,205, + 206,207,208,209,126,115,116,117,118,119,120,121,122,210, + 211,212,213,214,215,216,217,218,219,220,221,222,223,224, + 225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72, + 73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81, + 82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89, + 90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57, + 250,251,252,253,254,255) + import string + c.EBCDIC_TO_ASCII_MAP = string.maketrans( \ + ''.join(map(chr, range(256))), ''.join(map(chr, emap))) + return s.translate(c.EBCDIC_TO_ASCII_MAP) + + MS_CHARS = { '\x80' : ('euro', '20AC'), + '\x81' : ' ', + '\x82' : ('sbquo', '201A'), + '\x83' : ('fnof', '192'), + '\x84' : ('bdquo', '201E'), + '\x85' : ('hellip', '2026'), + '\x86' : ('dagger', '2020'), + '\x87' : ('Dagger', '2021'), + '\x88' : ('circ', '2C6'), + '\x89' : ('permil', '2030'), + '\x8A' : ('Scaron', '160'), + '\x8B' : ('lsaquo', '2039'), + '\x8C' : ('OElig', '152'), + '\x8D' : '?', + '\x8E' : ('#x17D', '17D'), + '\x8F' : '?', + '\x90' : '?', + '\x91' : ('lsquo', '2018'), + '\x92' : ('rsquo', '2019'), + '\x93' : ('ldquo', '201C'), + '\x94' : ('rdquo', '201D'), + '\x95' : ('bull', '2022'), + '\x96' : ('ndash', '2013'), + '\x97' : ('mdash', '2014'), + '\x98' : ('tilde', '2DC'), + '\x99' : ('trade', '2122'), + '\x9a' : ('scaron', '161'), + '\x9b' : ('rsaquo', '203A'), + '\x9c' : ('oelig', '153'), + '\x9d' : '?', + '\x9e' : ('#x17E', '17E'), + '\x9f' : ('Yuml', ''),} + +####################################################################### + + +#By default, act as an HTML pretty-printer. +if __name__ == '__main__': + import sys + soup = BeautifulSoup(sys.stdin) + print soup.prettify() diff --git a/lib/imdb/parser/http/bsouplxml/bsoupxpath.py b/lib/imdb/parser/http/bsouplxml/bsoupxpath.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c489db7fc5b46500a79ef1a985485a29d49223 --- /dev/null +++ b/lib/imdb/parser/http/bsouplxml/bsoupxpath.py @@ -0,0 +1,410 @@ +""" +parser.http.bsoupxpath module (imdb.parser.http package). + +This module provides XPath support for BeautifulSoup. + +Copyright 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +__author__ = 'H. Turgut Uyar <uyar@tekir.org>' +__docformat__ = 'restructuredtext' + + +import re +import string +import _bsoup as BeautifulSoup + + +# XPath related enumerations and constants + +AXIS_ANCESTOR = 'ancestor' +AXIS_ATTRIBUTE = 'attribute' +AXIS_CHILD = 'child' +AXIS_DESCENDANT = 'descendant' +AXIS_FOLLOWING = 'following' +AXIS_FOLLOWING_SIBLING = 'following-sibling' +AXIS_PRECEDING_SIBLING = 'preceding-sibling' + +AXES = (AXIS_ANCESTOR, AXIS_ATTRIBUTE, AXIS_CHILD, AXIS_DESCENDANT, + AXIS_FOLLOWING, AXIS_FOLLOWING_SIBLING, AXIS_PRECEDING_SIBLING) + +XPATH_FUNCTIONS = ('starts-with', 'string-length', 'contains') + + +def tokenize_path(path): + """Tokenize a location path into location steps. Return the list of steps. + + If two steps are separated by a double slash, the double slashes are part of + the second step. If they are separated by only one slash, the slash is not + included in any of the steps. + """ + # form a list of tuples that mark the start and end positions of steps + separators = [] + last_position = 0 + i = -1 + in_string = False + while i < len(path) - 1: + i = i + 1 + if path[i] == "'": + in_string = not in_string + if in_string: + # slashes within strings are not step separators + continue + if path[i] == '/': + if i > 0: + separators.append((last_position, i)) + if (path[i+1] == '/'): + last_position = i + i = i + 1 + else: + last_position = i + 1 + separators.append((last_position, len(path))) + + steps = [] + for start, end in separators: + steps.append(path[start:end]) + return steps + + +class Path: + """A location path. + """ + + def __init__(self, path, parse=True): + self.path = path + self.steps = [] + if parse: + if (path[0] == '/') and (path[1] != '/'): + # if not on the descendant axis, remove the leading slash + path = path[1:] + steps = tokenize_path(path) + for step in steps: + self.steps.append(PathStep(step)) + + def apply(self, node): + """Apply the path to a node. Return the resulting list of nodes. + + Apply the steps in the path sequentially by sending the output of each + step as input to the next step. + """ + # FIXME: this should return a node SET, not a node LIST + # or at least a list with no duplicates + if self.path[0] == '/': + # for an absolute path, start from the root + if not isinstance(node, BeautifulSoup.Tag) \ + or (node.name != '[document]'): + node = node.findParent('[document]') + nodes = [node] + for step in self.steps: + nodes = step.apply(nodes) + return nodes + + +class PathStep: + """A location step in a location path. + """ + + AXIS_PATTERN = r"""(%s)::|@""" % '|'.join(AXES) + NODE_TEST_PATTERN = r"""\w+(\(\))?""" + PREDICATE_PATTERN = r"""\[(.*?)\]""" + LOCATION_STEP_PATTERN = r"""(%s)?(%s)((%s)*)""" \ + % (AXIS_PATTERN, NODE_TEST_PATTERN, PREDICATE_PATTERN) + + _re_location_step = re.compile(LOCATION_STEP_PATTERN) + + PREDICATE_NOT_PATTERN = r"""not\((.*?)\)""" + PREDICATE_AXIS_PATTERN = r"""(%s)?(%s)(='(.*?)')?""" \ + % (AXIS_PATTERN, NODE_TEST_PATTERN) + PREDICATE_FUNCTION_PATTERN = r"""(%s)\(([^,]+(,\s*[^,]+)*)?\)(=(.*))?""" \ + % '|'.join(XPATH_FUNCTIONS) + + _re_predicate_not = re.compile(PREDICATE_NOT_PATTERN) + _re_predicate_axis = re.compile(PREDICATE_AXIS_PATTERN) + _re_predicate_function = re.compile(PREDICATE_FUNCTION_PATTERN) + + def __init__(self, step): + self.step = step + if (step == '.') or (step == '..'): + return + + if step[:2] == '//': + default_axis = AXIS_DESCENDANT + step = step[2:] + else: + default_axis = AXIS_CHILD + + step_match = self._re_location_step.match(step) + + # determine the axis + axis = step_match.group(1) + if axis is None: + self.axis = default_axis + elif axis == '@': + self.axis = AXIS_ATTRIBUTE + else: + self.axis = step_match.group(2) + + self.soup_args = {} + self.index = None + + self.node_test = step_match.group(3) + if self.node_test == 'text()': + self.soup_args['text'] = True + else: + self.soup_args['name'] = self.node_test + + self.checkers = [] + predicates = step_match.group(5) + if predicates is not None: + predicates = [p for p in predicates[1:-1].split('][') if p] + for predicate in predicates: + checker = self.__parse_predicate(predicate) + if checker is not None: + self.checkers.append(checker) + + def __parse_predicate(self, predicate): + """Parse the predicate. Return a callable that can be used to filter + nodes. Update `self.soup_args` to take advantage of BeautifulSoup search + features. + """ + try: + position = int(predicate) + if self.axis == AXIS_DESCENDANT: + return PredicateFilter('position', value=position) + else: + # use the search limit feature instead of a checker + self.soup_args['limit'] = position + self.index = position - 1 + return None + except ValueError: + pass + + if predicate == "last()": + self.index = -1 + return None + + negate = self._re_predicate_not.match(predicate) + if negate: + predicate = negate.group(1) + + function_match = self._re_predicate_function.match(predicate) + if function_match: + name = function_match.group(1) + arguments = function_match.group(2) + value = function_match.group(4) + if value is not None: + value = function_match.group(5) + return PredicateFilter(name, arguments, value) + + axis_match = self._re_predicate_axis.match(predicate) + if axis_match: + axis = axis_match.group(1) + if axis is None: + axis = AXIS_CHILD + elif axis == '@': + axis = AXIS_ATTRIBUTE + if axis == AXIS_ATTRIBUTE: + # use the attribute search feature instead of a checker + attribute_name = axis_match.group(3) + if axis_match.group(5) is not None: + attribute_value = axis_match.group(6) + elif not negate: + attribute_value = True + else: + attribute_value = None + if not self.soup_args.has_key('attrs'): + self.soup_args['attrs'] = {} + self.soup_args['attrs'][attribute_name] = attribute_value + return None + elif axis == AXIS_CHILD: + node_test = axis_match.group(3) + node_value = axis_match.group(6) + return PredicateFilter('axis', node_test, value=node_value, + negate=negate) + + raise NotImplementedError("This predicate is not implemented") + + def apply(self, nodes): + """Apply the step to a list of nodes. Return the list of nodes for the + next step. + """ + if self.step == '.': + return nodes + elif self.step == '..': + return [node.parent for node in nodes] + + result = [] + for node in nodes: + if self.axis == AXIS_CHILD: + found = node.findAll(recursive=False, **self.soup_args) + elif self.axis == AXIS_DESCENDANT: + found = node.findAll(recursive=True, **self.soup_args) + elif self.axis == AXIS_ATTRIBUTE: + try: + found = [node[self.node_test]] + except KeyError: + found = [] + elif self.axis == AXIS_FOLLOWING_SIBLING: + found = node.findNextSiblings(**self.soup_args) + elif self.axis == AXIS_PRECEDING_SIBLING: + # TODO: make sure that the result is reverse ordered + found = node.findPreviousSiblings(**self.soup_args) + elif self.axis == AXIS_FOLLOWING: + # find the last descendant of this node + last = node + while (not isinstance(last, BeautifulSoup.NavigableString)) \ + and (len(last.contents) > 0): + last = last.contents[-1] + found = last.findAllNext(**self.soup_args) + elif self.axis == AXIS_ANCESTOR: + found = node.findParents(**self.soup_args) + + # this should only be active if there is a position predicate + # and the axis is not 'descendant' + if self.index is not None: + if found: + if len(found) > self.index: + found = [found[self.index]] + else: + found = [] + + if found: + for checker in self.checkers: + found = filter(checker, found) + result.extend(found) + + return result + + +class PredicateFilter: + """A callable class for filtering nodes. + """ + + def __init__(self, name, arguments=None, value=None, negate=False): + self.name = name + self.arguments = arguments + self.negate = negate + + if name == 'position': + self.__filter = self.__position + self.value = value + elif name == 'axis': + self.__filter = self.__axis + self.node_test = arguments + self.value = value + elif name in ('starts-with', 'contains'): + if name == 'starts-with': + self.__filter = self.__starts_with + else: + self.__filter = self.__contains + args = map(string.strip, arguments.split(',')) + if args[0][0] == '@': + self.arguments = (True, args[0][1:], args[1][1:-1]) + else: + self.arguments = (False, args[0], args[1][1:-1]) + elif name == 'string-length': + self.__filter = self.__string_length + args = map(string.strip, arguments.split(',')) + if args[0][0] == '@': + self.arguments = (True, args[0][1:]) + else: + self.arguments = (False, args[0]) + self.value = int(value) + else: + raise NotImplementedError("This XPath function is not implemented") + + def __call__(self, node): + if self.negate: + return not self.__filter(node) + else: + return self.__filter(node) + + def __position(self, node): + if isinstance(node, BeautifulSoup.NavigableString): + actual_position = len(node.findPreviousSiblings(text=True)) + 1 + else: + actual_position = len(node.findPreviousSiblings(node.name)) + 1 + return actual_position == self.value + + def __axis(self, node): + if self.node_test == 'text()': + return node.string == self.value + else: + children = node.findAll(self.node_test, recursive=False) + if len(children) > 0 and self.value is None: + return True + for child in children: + if child.string == self.value: + return True + return False + + def __starts_with(self, node): + if self.arguments[0]: + # this is an attribute + attribute_name = self.arguments[1] + if node.has_key(attribute_name): + first = node[attribute_name] + return first.startswith(self.arguments[2]) + elif self.arguments[1] == 'text()': + first = node.contents and node.contents[0] + if isinstance(first, BeautifulSoup.NavigableString): + return first.startswith(self.arguments[2]) + return False + + def __contains(self, node): + if self.arguments[0]: + # this is an attribute + attribute_name = self.arguments[1] + if node.has_key(attribute_name): + first = node[attribute_name] + return self.arguments[2] in first + elif self.arguments[1] == 'text()': + first = node.contents and node.contents[0] + if isinstance(first, BeautifulSoup.NavigableString): + return self.arguments[2] in first + return False + + def __string_length(self, node): + if self.arguments[0]: + # this is an attribute + attribute_name = self.arguments[1] + if node.has_key(attribute_name): + value = node[attribute_name] + else: + value = None + elif self.arguments[1] == 'text()': + value = node.string + if value is not None: + return len(value) == self.value + return False + + +_paths = {} +_steps = {} + +def get_path(path): + """Utility for eliminating repeated parsings of the same paths and steps. + """ + if not _paths.has_key(path): + p = Path(path, parse=False) + steps = tokenize_path(path) + for step in steps: + if not _steps.has_key(step): + _steps[step] = PathStep(step) + p.steps.append(_steps[step]) + _paths[path] = p + return _paths[path] diff --git a/lib/imdb/parser/http/bsouplxml/etree.py b/lib/imdb/parser/http/bsouplxml/etree.py new file mode 100644 index 0000000000000000000000000000000000000000..28465f5c246a065895d79354ea1702002491cb67 --- /dev/null +++ b/lib/imdb/parser/http/bsouplxml/etree.py @@ -0,0 +1,75 @@ +""" +parser.http.bsouplxml.etree module (imdb.parser.http package). + +This module adapts the beautifulsoup interface to lxml.etree module. + +Copyright 2008 H. Turgut Uyar <uyar@tekir.org> + 2008 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import _bsoup as BeautifulSoup +from _bsoup import Tag as Element + +import bsoupxpath + +# Not directly used by IMDbPY, but do not remove: it's used by IMDbPYKit, +# for example. +def fromstring(xml_string): + """Return a DOM representation of the string.""" + # We try to not use BeautifulSoup.BeautifulStoneSoup.XML_ENTITIES, + # for convertEntities. + return BeautifulSoup.BeautifulStoneSoup(xml_string, + convertEntities=None).findChild(True) + + +def tostring(element, encoding=None, pretty_print=False): + """Return a string or unicode representation of an element.""" + if encoding is unicode: + encoding = None + # For BeautifulSoup 3.1 + #encArgs = {'prettyPrint': pretty_print} + #if encoding is not None: + # encArgs['encoding'] = encoding + #return element.encode(**encArgs) + return element.__str__(encoding, pretty_print) + +def setattribute(tag, name, value): + tag[name] = value + +def xpath(node, expr): + """Apply an xpath expression to a node. Return a list of nodes.""" + #path = bsoupxpath.Path(expr) + path = bsoupxpath.get_path(expr) + return path.apply(node) + + +# XXX: monkey patching the beautifulsoup tag class +class _EverythingIsNestable(dict): + """"Fake that every tag is nestable.""" + def get(self, key, *args, **kwds): + return [] + +BeautifulSoup.BeautifulStoneSoup.NESTABLE_TAGS = _EverythingIsNestable() +BeautifulSoup.Tag.tag = property(fget=lambda self: self.name) +BeautifulSoup.Tag.attrib = property(fget=lambda self: self) +BeautifulSoup.Tag.text = property(fget=lambda self: self.string) +BeautifulSoup.Tag.set = setattribute +BeautifulSoup.Tag.getparent = lambda self: self.parent +BeautifulSoup.Tag.drop_tree = BeautifulSoup.Tag.extract +BeautifulSoup.Tag.xpath = xpath + +# TODO: setting the text attribute for tags diff --git a/lib/imdb/parser/http/bsouplxml/html.py b/lib/imdb/parser/http/bsouplxml/html.py new file mode 100644 index 0000000000000000000000000000000000000000..bbf13bd642a34f3c2eeb3468c911921bac00163a --- /dev/null +++ b/lib/imdb/parser/http/bsouplxml/html.py @@ -0,0 +1,31 @@ +""" +parser.http.bsouplxml.html module (imdb.parser.http package). + +This module adapts the beautifulsoup interface to lxml.html module. + +Copyright 2008 H. Turgut Uyar <uyar@tekir.org> + 2008 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import _bsoup as BeautifulSoup + + +def fromstring(html_string): + """Return a DOM representation of the string.""" + return BeautifulSoup.BeautifulSoup(html_string, + convertEntities=BeautifulSoup.BeautifulSoup.HTML_ENTITIES + ).findChild(True) diff --git a/lib/imdb/parser/http/characterParser.py b/lib/imdb/parser/http/characterParser.py new file mode 100644 index 0000000000000000000000000000000000000000..ff5ea09bc2872b6aa227fe5324227d248f5aeefb --- /dev/null +++ b/lib/imdb/parser/http/characterParser.py @@ -0,0 +1,203 @@ +""" +parser.http.characterParser module (imdb package). + +This module provides the classes (and the instances), used to parse +the IMDb pages on the akas.imdb.com server about a character. +E.g., for "Jesse James" the referred pages would be: + main details: http://www.imdb.com/character/ch0000001/ + biography: http://www.imdb.com/character/ch0000001/bio + ...and so on... + +Copyright 2007-2009 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +from utils import Attribute, Extractor, DOMParserBase, build_movie, \ + analyze_imdbid +from personParser import DOMHTMLMaindetailsParser + +from imdb.Movie import Movie + +_personIDs = re.compile(r'/name/nm([0-9]{7})') +class DOMHTMLCharacterMaindetailsParser(DOMHTMLMaindetailsParser): + """Parser for the "filmography" page of a given character. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + bparser = DOMHTMLCharacterMaindetailsParser() + result = bparser.parse(character_biography_html_string) + """ + _containsObjects = True + + _film_attrs = [Attribute(key=None, + multi=True, + path={ + 'link': "./a[1]/@href", + 'title': ".//text()", + 'status': "./i/a//text()", + 'roleID': "./a/@href" + }, + postprocess=lambda x: + build_movie(x.get('title') or u'', + movieID=analyze_imdbid(x.get('link') or u''), + roleID=_personIDs.findall(x.get('roleID') or u''), + status=x.get('status') or None, + _parsingCharacter=True))] + + extractors = [ + Extractor(label='title', + path="//title", + attrs=Attribute(key='name', + path="./text()", + postprocess=lambda x: \ + x.replace(' (Character)', '').replace( + '- Filmography by type', '').strip())), + + Extractor(label='headshot', + path="//a[@name='headshot']", + attrs=Attribute(key='headshot', + path="./img/@src")), + + Extractor(label='akas', + path="//div[h5='Alternate Names:']", + attrs=Attribute(key='akas', + path="./div//text()", + postprocess=lambda x: x.strip().split(' / '))), + + Extractor(label='filmography', + path="//div[@class='filmo'][not(h5)]/ol/li", + attrs=_film_attrs), + + Extractor(label='filmography sections', + group="//div[@class='filmo'][h5]", + group_key="./h5/a/text()", + group_key_normalize=lambda x: x.lower()[:-1], + path="./ol/li", + attrs=_film_attrs), + ] + + preprocessors = [ + # Check that this doesn't cut "status"... + (re.compile(r'<br>(\.\.\.| ).+?</li>', re.I | re.M), '</li>')] + + +class DOMHTMLCharacterBioParser(DOMParserBase): + """Parser for the "biography" page of a given character. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + bparser = DOMHTMLCharacterBioParser() + result = bparser.parse(character_biography_html_string) + """ + _defGetRefs = True + + extractors = [ + Extractor(label='introduction', + path="//div[@id='_intro']", + attrs=Attribute(key='introduction', + path=".//text()", + postprocess=lambda x: x.strip())), + + Extractor(label='biography', + path="//span[@class='_biography']", + attrs=Attribute(key='biography', + multi=True, + path={ + 'info': "./preceding-sibling::h4[1]//text()", + 'text': ".//text()" + }, + postprocess=lambda x: u'%s: %s' % ( + x.get('info').strip(), + x.get('text').replace('\n', + ' ').replace('||', '\n\n').strip()))), + ] + + preprocessors = [ + (re.compile('(<div id="swiki.2.3.1">)', re.I), r'\1<div id="_intro">'), + (re.compile('(<a name="history">)\s*(<table .*?</table>)', + re.I | re.DOTALL), + r'</div>\2\1</a>'), + (re.compile('(<a name="[^"]+">)(<h4>)', re.I), r'</span>\1</a>\2'), + (re.compile('(</h4>)</a>', re.I), r'\1<span class="_biography">'), + (re.compile('<br/><br/>', re.I), r'||'), + (re.compile('\|\|\n', re.I), r'</span>'), + ] + + +class DOMHTMLCharacterQuotesParser(DOMParserBase): + """Parser for the "quotes" page of a given character. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + qparser = DOMHTMLCharacterQuotesParser() + result = qparser.parse(character_quotes_html_string) + """ + _defGetRefs = True + + extractors = [ + Extractor(label='charquotes', + group="//h5", + group_key="./a/text()", + path="./following-sibling::div[1]", + attrs=Attribute(key=None, + path={'txt': ".//text()", + 'movieID': ".//a[1]/@href"}, + postprocess=lambda x: (analyze_imdbid(x['movieID']), + x['txt'].strip().replace(': ', + ': ').replace(': ', ': ').split('||')))) + ] + + preprocessors = [ + (re.compile('(</h5>)', re.I), r'\1<div>'), + (re.compile('\s*<br/><br/>\s*', re.I), r'||'), + (re.compile('\|\|\s*(<hr/>)', re.I), r'</div>\1'), + (re.compile('\s*<br/>\s*', re.I), r'::') + ] + + def postprocess_data(self, data): + if not data: + return {} + newData = {} + for title in data: + movieID, quotes = data[title] + if movieID is None: + movie = title + else: + movie = Movie(title=title, movieID=movieID, + accessSystem=self._as, modFunct=self._modFunct) + newData[movie] = [quote.split('::') for quote in quotes] + return {'quotes': newData} + + +from personParser import DOMHTMLSeriesParser + +_OBJECTS = { + 'character_main_parser': ((DOMHTMLCharacterMaindetailsParser,), + {'kind': 'character'}), + 'character_series_parser': ((DOMHTMLSeriesParser,), None), + 'character_bio_parser': ((DOMHTMLCharacterBioParser,), None), + 'character_quotes_parser': ((DOMHTMLCharacterQuotesParser,), None) +} + + diff --git a/lib/imdb/parser/http/companyParser.py b/lib/imdb/parser/http/companyParser.py new file mode 100644 index 0000000000000000000000000000000000000000..843379169cc595c59f611a635500753a9ea827f2 --- /dev/null +++ b/lib/imdb/parser/http/companyParser.py @@ -0,0 +1,91 @@ +""" +parser.http.companyParser module (imdb package). + +This module provides the classes (and the instances), used to parse +the IMDb pages on the akas.imdb.com server about a company. +E.g., for "Columbia Pictures [us]" the referred page would be: + main details: http://akas.imdb.com/company/co0071509/ + +Copyright 2008-2009 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +from utils import build_movie, Attribute, Extractor, DOMParserBase, \ + analyze_imdbid + +from imdb.utils import analyze_company_name + + +class DOMCompanyParser(DOMParserBase): + """Parser for the main page of a given company. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + cparser = DOMCompanyParser() + result = cparser.parse(company_html_string) + """ + _containsObjects = True + + extractors = [ + Extractor(label='name', + path="//title", + attrs=Attribute(key='name', + path="./text()", + postprocess=lambda x: \ + analyze_company_name(x, stripNotes=True))), + + Extractor(label='filmography', + group="//b/a[@name]", + group_key="./text()", + group_key_normalize=lambda x: x.lower(), + path="../following-sibling::ol[1]/li", + attrs=Attribute(key=None, + multi=True, + path={ + 'link': "./a[1]/@href", + 'title': "./a[1]/text()", + 'year': "./text()[1]" + }, + postprocess=lambda x: + build_movie(u'%s %s' % \ + (x.get('title'), x.get('year').strip()), + movieID=analyze_imdbid(x.get('link') or u''), + _parsingCompany=True))), + ] + + preprocessors = [ + (re.compile('(<b><a name=)', re.I), r'</p>\1') + ] + + def postprocess_data(self, data): + for key in data.keys(): + new_key = key.replace('company', 'companies') + new_key = new_key.replace('other', 'miscellaneous') + new_key = new_key.replace('distributor', 'distributors') + if new_key != key: + data[new_key] = data[key] + del data[key] + return data + + +_OBJECTS = { + 'company_main_parser': ((DOMCompanyParser,), None) +} + diff --git a/lib/imdb/parser/http/movieParser.py b/lib/imdb/parser/http/movieParser.py new file mode 100644 index 0000000000000000000000000000000000000000..d05b9e01130e059bc1a46762df38e37196186125 --- /dev/null +++ b/lib/imdb/parser/http/movieParser.py @@ -0,0 +1,1892 @@ +""" +parser.http.movieParser module (imdb package). + +This module provides the classes (and the instances), used to parse the +IMDb pages on the akas.imdb.com server about a movie. +E.g., for Brian De Palma's "The Untouchables", the referred +pages would be: + combined details: http://akas.imdb.com/title/tt0094226/combined + plot summary: http://akas.imdb.com/title/tt0094226/plotsummary + ...and so on... + +Copyright 2004-2012 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +import urllib + +from imdb import imdbURL_base +from imdb.Person import Person +from imdb.Movie import Movie +from imdb.Company import Company +from imdb.utils import analyze_title, split_company_name_notes, _Container +from utils import build_person, DOMParserBase, Attribute, Extractor, \ + analyze_imdbid + + +# Dictionary used to convert some section's names. +_SECT_CONV = { + 'directed': 'director', + 'directed by': 'director', + 'directors': 'director', + 'editors': 'editor', + 'writing credits': 'writer', + 'writers': 'writer', + 'produced': 'producer', + 'cinematography': 'cinematographer', + 'film editing': 'editor', + 'casting': 'casting director', + 'costume design': 'costume designer', + 'makeup department': 'make up', + 'production management': 'production manager', + 'second unit director or assistant director': 'assistant director', + 'costume and wardrobe department': 'costume department', + 'sound department': 'sound crew', + 'stunts': 'stunt performer', + 'other crew': 'miscellaneous crew', + 'also known as': 'akas', + 'country': 'countries', + 'runtime': 'runtimes', + 'language': 'languages', + 'certification': 'certificates', + 'genre': 'genres', + 'created': 'creator', + 'creators': 'creator', + 'color': 'color info', + 'plot': 'plot outline', + 'seasons': 'number of seasons', + 'art directors': 'art direction', + 'assistant directors': 'assistant director', + 'set decorators': 'set decoration', + 'visual effects department': 'visual effects', + 'production managers': 'production manager', + 'miscellaneous': 'miscellaneous crew', + 'make up department': 'make up', + 'plot summary': 'plot outline', + 'cinematographers': 'cinematographer', + 'camera department': 'camera and electrical department', + 'costume designers': 'costume designer', + 'production designers': 'production design', + 'production managers': 'production manager', + 'music original': 'original music', + 'casting directors': 'casting director', + 'other companies': 'miscellaneous companies', + 'producers': 'producer', + 'special effects by': 'special effects department', + 'special effects': 'special effects companies' + } + + +def _manageRoles(mo): + """Perform some transformation on the html, so that roleIDs can + be easily retrieved.""" + firstHalf = mo.group(1) + secondHalf = mo.group(2) + newRoles = [] + roles = secondHalf.split(' / ') + for role in roles: + role = role.strip() + if not role: + continue + roleID = analyze_imdbid(role) + if roleID is None: + roleID = u'/' + else: + roleID += u'/' + newRoles.append(u'<div class="_imdbpyrole" roleid="%s">%s</div>' % \ + (roleID, role.strip())) + return firstHalf + u' / '.join(newRoles) + mo.group(3) + + +_reRolesMovie = re.compile(r'(<td class="char">)(.*?)(</td>)', + re.I | re.M | re.S) + +def _replaceBR(mo): + """Replaces <br> tags with '::' (useful for some akas)""" + txt = mo.group(0) + return txt.replace('<br>', '::') + +_reAkas = re.compile(r'<h5>also known as:</h5>.*?</div>', re.I | re.M | re.S) + +def makeSplitter(lstrip=None, sep='|', comments=True, + origNotesSep=' (', newNotesSep='::(', strip=None): + """Return a splitter function suitable for a given set of data.""" + def splitter(x): + if not x: return x + x = x.strip() + if not x: return x + if lstrip is not None: + x = x.lstrip(lstrip).lstrip() + lx = x.split(sep) + lx[:] = filter(None, [j.strip() for j in lx]) + if comments: + lx[:] = [j.replace(origNotesSep, newNotesSep, 1) for j in lx] + if strip: + lx[:] = [j.strip(strip) for j in lx] + return lx + return splitter + + +def _toInt(val, replace=()): + """Return the value, converted to integer, or None; if present, 'replace' + must be a list of tuples of values to replace.""" + for before, after in replace: + val = val.replace(before, after) + try: + return int(val) + except (TypeError, ValueError): + return None + + +class DOMHTMLMovieParser(DOMParserBase): + """Parser for the "combined details" (and if instance.mdparse is + True also for the "main details") page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + mparser = DOMHTMLMovieParser() + result = mparser.parse(combined_details_html_string) + """ + _containsObjects = True + + extractors = [Extractor(label='title', + path="//h1", + attrs=Attribute(key='title', + path=".//text()", + postprocess=analyze_title)), + + Extractor(label='glossarysections', + group="//a[@class='glossary']", + group_key="./@name", + group_key_normalize=lambda x: x.replace('_', ' '), + path="../../../..//tr", + attrs=Attribute(key=None, + multi=True, + path={'person': ".//text()", + 'link': "./td[1]/a[@href]/@href"}, + postprocess=lambda x: \ + build_person(x.get('person') or u'', + personID=analyze_imdbid(x.get('link'))) + )), + + Extractor(label='cast', + path="//table[@class='cast']//tr", + attrs=Attribute(key="cast", + multi=True, + path={'person': ".//text()", + 'link': "td[2]/a/@href", + 'roleID': \ + "td[4]/div[@class='_imdbpyrole']/@roleid"}, + postprocess=lambda x: \ + build_person(x.get('person') or u'', + personID=analyze_imdbid(x.get('link')), + roleID=(x.get('roleID') or u'').split('/')) + )), + + Extractor(label='genres', + path="//div[@class='info']//a[starts-with(@href," \ + " '/Sections/Genres')]", + attrs=Attribute(key="genres", + multi=True, + path="./text()")), + + Extractor(label='h5sections', + path="//div[@class='info']/h5/..", + attrs=[ + Attribute(key="plot summary", + path="./h5[starts-with(text(), " \ + "'Plot:')]/../div/text()", + postprocess=lambda x: \ + x.strip().rstrip('|').rstrip()), + Attribute(key="aspect ratio", + path="./h5[starts-with(text()," \ + " 'Aspect')]/../div/text()", + postprocess=lambda x: x.strip()), + Attribute(key="mpaa", + path="./h5/a[starts-with(text()," \ + " 'MPAA')]/../../div/text()", + postprocess=lambda x: x.strip()), + Attribute(key="countries", + path="./h5[starts-with(text(), " \ + "'Countr')]/../div[@class='info-content']//text()", + postprocess=makeSplitter('|')), + Attribute(key="language", + path="./h5[starts-with(text(), " \ + "'Language')]/..//text()", + postprocess=makeSplitter('Language:')), + Attribute(key='color info', + path="./h5[starts-with(text(), " \ + "'Color')]/..//text()", + postprocess=makeSplitter('Color:')), + Attribute(key='sound mix', + path="./h5[starts-with(text(), " \ + "'Sound Mix')]/..//text()", + postprocess=makeSplitter('Sound Mix:')), + # Collects akas not encosed in <i> tags. + Attribute(key='other akas', + path="./h5[starts-with(text(), " \ + "'Also Known As')]/../div//text()", + postprocess=makeSplitter(sep='::', + origNotesSep='" - ', + newNotesSep='::', + strip='"')), + Attribute(key='runtimes', + path="./h5[starts-with(text(), " \ + "'Runtime')]/../div/text()", + postprocess=makeSplitter()), + Attribute(key='certificates', + path="./h5[starts-with(text(), " \ + "'Certificat')]/..//text()", + postprocess=makeSplitter('Certification:')), + Attribute(key='number of seasons', + path="./h5[starts-with(text(), " \ + "'Seasons')]/..//text()", + postprocess=lambda x: x.count('|') + 1), + Attribute(key='original air date', + path="./h5[starts-with(text(), " \ + "'Original Air Date')]/../div/text()"), + Attribute(key='tv series link', + path="./h5[starts-with(text(), " \ + "'TV Series')]/..//a/@href"), + Attribute(key='tv series title', + path="./h5[starts-with(text(), " \ + "'TV Series')]/..//a/text()") + ]), + + Extractor(label='language codes', + path="//h5[starts-with(text(), 'Language')]/..//a[starts-with(@href, '/language/')]", + attrs=Attribute(key='language codes', multi=True, + path="./@href", + postprocess=lambda x: x.split('/')[2].strip() + )), + + Extractor(label='country codes', + path="//h5[starts-with(text(), 'Country')]/..//a[starts-with(@href, '/country/')]", + attrs=Attribute(key='country codes', multi=True, + path="./@href", + postprocess=lambda x: x.split('/')[2].strip() + )), + + Extractor(label='creator', + path="//h5[starts-with(text(), 'Creator')]/..//a", + attrs=Attribute(key='creator', multi=True, + path={'name': "./text()", + 'link': "./@href"}, + postprocess=lambda x: \ + build_person(x.get('name') or u'', + personID=analyze_imdbid(x.get('link'))) + )), + + Extractor(label='thin writer', + path="//h5[starts-with(text(), 'Writer')]/..//a", + attrs=Attribute(key='thin writer', multi=True, + path={'name': "./text()", + 'link': "./@href"}, + postprocess=lambda x: \ + build_person(x.get('name') or u'', + personID=analyze_imdbid(x.get('link'))) + )), + + Extractor(label='thin director', + path="//h5[starts-with(text(), 'Director')]/..//a", + attrs=Attribute(key='thin director', multi=True, + path={'name': "./text()", + 'link': "@href"}, + postprocess=lambda x: \ + build_person(x.get('name') or u'', + personID=analyze_imdbid(x.get('link'))) + )), + + Extractor(label='top 250/bottom 100', + path="//div[@class='starbar-special']/" \ + "a[starts-with(@href, '/chart/')]", + attrs=Attribute(key='top/bottom rank', + path="./text()")), + + Extractor(label='series years', + path="//div[@id='tn15title']//span" \ + "[starts-with(text(), 'TV series')]", + attrs=Attribute(key='series years', + path="./text()", + postprocess=lambda x: \ + x.replace('TV series','').strip())), + + Extractor(label='number of episodes', + path="//a[@title='Full Episode List']", + attrs=Attribute(key='number of episodes', + path="./text()", + postprocess=lambda x: \ + _toInt(x, [(' Episodes', '')]))), + + Extractor(label='akas', + path="//i[@class='transl']", + attrs=Attribute(key='akas', multi=True, path='text()', + postprocess=lambda x: + x.replace(' ', ' ').rstrip('-').replace('" - ', + '"::', 1).strip('"').replace(' ', ' '))), + + Extractor(label='production notes/status', + path="//h5[starts-with(text(), 'Status:')]/..//div[@class='info-content']", + attrs=Attribute(key='production status', + path=".//text()", + postprocess=lambda x: x.strip().split('|')[0].strip().lower())), + + Extractor(label='production notes/status updated', + path="//h5[starts-with(text(), 'Status Updated:')]/..//div[@class='info-content']", + attrs=Attribute(key='production status updated', + path=".//text()", + postprocess=lambda x: x.strip())), + + Extractor(label='production notes/comments', + path="//h5[starts-with(text(), 'Comments:')]/..//div[@class='info-content']", + attrs=Attribute(key='production comments', + path=".//text()", + postprocess=lambda x: x.strip())), + + Extractor(label='production notes/note', + path="//h5[starts-with(text(), 'Note:')]/..//div[@class='info-content']", + attrs=Attribute(key='production note', + path=".//text()", + postprocess=lambda x: x.strip())), + + Extractor(label='blackcatheader', + group="//b[@class='blackcatheader']", + group_key="./text()", + group_key_normalize=lambda x: x.lower(), + path="../ul/li", + attrs=Attribute(key=None, + multi=True, + path={'name': "./a//text()", + 'comp-link': "./a/@href", + 'notes': "./text()"}, + postprocess=lambda x: \ + Company(name=x.get('name') or u'', + companyID=analyze_imdbid(x.get('comp-link')), + notes=(x.get('notes') or u'').strip()) + )), + + Extractor(label='rating', + path="//div[@class='starbar-meta']/b", + attrs=Attribute(key='rating', + path=".//text()")), + + Extractor(label='votes', + path="//div[@class='starbar-meta']/a[@href]", + attrs=Attribute(key='votes', + path=".//text()")), + + Extractor(label='cover url', + path="//a[@name='poster']", + attrs=Attribute(key='cover url', + path="./img/@src")) + ] + + preprocessors = [ + (re.compile(r'(<b class="blackcatheader">.+?</b>)', re.I), + r'</div><div>\1'), + ('<small>Full cast and crew for<br>', ''), + ('<td> </td>', '<td>...</td>'), + ('<span class="tv-extra">TV mini-series</span>', + '<span class="tv-extra">(mini)</span>'), + (_reRolesMovie, _manageRoles), + (_reAkas, _replaceBR)] + + def preprocess_dom(self, dom): + # Handle series information. + xpath = self.xpath(dom, "//b[text()='Series Crew']") + if xpath: + b = xpath[-1] # In doubt, take the last one. + for a in self.xpath(b, "./following::h5/a[@class='glossary']"): + name = a.get('name') + if name: + a.set('name', 'series %s' % name) + # Remove links to IMDbPro. + for proLink in self.xpath(dom, "//span[@class='pro-link']"): + proLink.drop_tree() + # Remove some 'more' links (keep others, like the one around + # the number of votes). + for tn15more in self.xpath(dom, + "//a[@class='tn15more'][starts-with(@href, '/title/')]"): + tn15more.drop_tree() + return dom + + re_space = re.compile(r'\s+') + re_airdate = re.compile(r'(.*)\s*\(season (\d+), episode (\d+)\)', re.I) + def postprocess_data(self, data): + # Convert section names. + for sect in data.keys(): + if sect in _SECT_CONV: + data[_SECT_CONV[sect]] = data[sect] + del data[sect] + sect = _SECT_CONV[sect] + # Filter out fake values. + for key in data: + value = data[key] + if isinstance(value, list) and value: + if isinstance(value[0], Person): + data[key] = filter(lambda x: x.personID is not None, value) + if isinstance(value[0], _Container): + for obj in data[key]: + obj.accessSystem = self._as + obj.modFunct = self._modFunct + if 'akas' in data or 'other akas' in data: + akas = data.get('akas') or [] + other_akas = data.get('other akas') or [] + akas += other_akas + nakas = [] + for aka in akas: + aka = aka.strip() + if aka.endswith('" -'): + aka = aka[:-3].rstrip() + nakas.append(aka) + if 'akas' in data: + del data['akas'] + if 'other akas' in data: + del data['other akas'] + if nakas: + data['akas'] = nakas + if 'runtimes' in data: + data['runtimes'] = [x.replace(' min', u'') + for x in data['runtimes']] + if 'original air date' in data: + oid = self.re_space.sub(' ', data['original air date']).strip() + data['original air date'] = oid + aid = self.re_airdate.findall(oid) + if aid and len(aid[0]) == 3: + date, season, episode = aid[0] + date = date.strip() + try: season = int(season) + except: pass + try: episode = int(episode) + except: pass + if date and date != '????': + data['original air date'] = date + else: + del data['original air date'] + # Handle also "episode 0". + if season or type(season) is type(0): + data['season'] = season + if episode or type(season) is type(0): + data['episode'] = episode + for k in ('writer', 'director'): + t_k = 'thin %s' % k + if t_k not in data: + continue + if k not in data: + data[k] = data[t_k] + del data[t_k] + if 'top/bottom rank' in data: + tbVal = data['top/bottom rank'].lower() + if tbVal.startswith('top'): + tbKey = 'top 250 rank' + tbVal = _toInt(tbVal, [('top 250: #', '')]) + else: + tbKey = 'bottom 100 rank' + tbVal = _toInt(tbVal, [('bottom 100: #', '')]) + if tbVal: + data[tbKey] = tbVal + del data['top/bottom rank'] + if 'year' in data and data['year'] == '????': + del data['year'] + if 'tv series link' in data: + if 'tv series title' in data: + data['episode of'] = Movie(title=data['tv series title'], + movieID=analyze_imdbid( + data['tv series link']), + accessSystem=self._as, + modFunct=self._modFunct) + del data['tv series title'] + del data['tv series link'] + if 'rating' in data: + try: + data['rating'] = float(data['rating'].replace('/10', '')) + except (TypeError, ValueError): + pass + if 'votes' in data: + try: + votes = data['votes'].replace(',', '').replace('votes', '') + data['votes'] = int(votes) + except (TypeError, ValueError): + pass + return data + + +def _process_plotsummary(x): + """Process a plot (contributed by Rdian06).""" + xauthor = x.get('author') + if xauthor: + xauthor = xauthor.replace('{', '<').replace('}', '>').replace('(', + '<').replace(')', '>').strip() + xplot = x.get('plot', u'').strip() + if xauthor: + xplot += u'::%s' % xauthor + return xplot + +class DOMHTMLPlotParser(DOMParserBase): + """Parser for the "plot summary" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a 'plot' key, containing a list + of string with the structure: 'summary::summary_author <author@email>'. + + Example: + pparser = HTMLPlotParser() + result = pparser.parse(plot_summary_html_string) + """ + _defGetRefs = True + + # Notice that recently IMDb started to put the email of the + # author only in the link, that we're not collecting, here. + extractors = [Extractor(label='plot', + path="//p[@class='plotpar']", + attrs=Attribute(key='plot', + multi=True, + path={'plot': './text()', + 'author': './i/a/text()'}, + postprocess=_process_plotsummary))] + + +def _process_award(x): + award = {} + award['award'] = x.get('award').strip() + if not award['award']: + return {} + award['year'] = x.get('year').strip() + if award['year'] and award['year'].isdigit(): + award['year'] = int(award['year']) + award['result'] = x.get('result').strip() + category = x.get('category').strip() + if category: + award['category'] = category + received_with = x.get('with') + if received_with is not None: + award['with'] = received_with.strip() + notes = x.get('notes') + if notes is not None: + notes = notes.strip() + if notes: + award['notes'] = notes + award['anchor'] = x.get('anchor') + return award + + + +class DOMHTMLAwardsParser(DOMParserBase): + """Parser for the "awards" page of a given person or movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + awparser = HTMLAwardsParser() + result = awparser.parse(awards_html_string) + """ + subject = 'title' + _containsObjects = True + + extractors = [ + Extractor(label='awards', + group="//table//big", + group_key="./a", + path="./ancestor::tr[1]/following-sibling::tr/" \ + "td[last()][not(@colspan)]", + attrs=Attribute(key=None, + multi=True, + path={ + 'year': "../td[1]/a/text()", + 'result': "../td[2]/b/text()", + 'award': "../td[3]/text()", + 'category': "./text()[1]", + # FIXME: takes only the first co-recipient + 'with': "./small[starts-with(text()," \ + " 'Shared with:')]/following-sibling::a[1]/text()", + 'notes': "./small[last()]//text()", + 'anchor': ".//text()" + }, + postprocess=_process_award + )), + Extractor(label='recipients', + group="//table//big", + group_key="./a", + path="./ancestor::tr[1]/following-sibling::tr/" \ + "td[last()]/small[1]/preceding-sibling::a", + attrs=Attribute(key=None, + multi=True, + path={ + 'name': "./text()", + 'link': "./@href", + 'anchor': "..//text()" + } + )) + ] + + preprocessors = [ + (re.compile('(<tr><td[^>]*>.*?</td></tr>\n\n</table>)', re.I), + r'\1</table>'), + (re.compile('(<tr><td[^>]*>\n\n<big>.*?</big></td></tr>)', re.I), + r'</table><table class="_imdbpy">\1'), + (re.compile('(<table[^>]*>\n\n)</table>(<table)', re.I), r'\1\2'), + (re.compile('(<small>.*?)<br>(.*?</small)', re.I), r'\1 \2'), + (re.compile('(</tr>\n\n)(<td)', re.I), r'\1<tr>\2') + ] + + def preprocess_dom(self, dom): + """Repeat td elements according to their rowspan attributes + in subsequent tr elements. + """ + cols = self.xpath(dom, "//td[@rowspan]") + for col in cols: + span = int(col.get('rowspan')) + del col.attrib['rowspan'] + position = len(self.xpath(col, "./preceding-sibling::td")) + row = col.getparent() + for tr in self.xpath(row, "./following-sibling::tr")[:span-1]: + # if not cloned, child will be moved to new parent + clone = self.clone(col) + # XXX: beware that here we don't use an "adapted" function, + # because both BeautifulSoup and lxml uses the same + # "insert" method. + tr.insert(position, clone) + return dom + + def postprocess_data(self, data): + if len(data) == 0: + return {} + nd = [] + for key in data.keys(): + dom = self.get_dom(key) + assigner = self.xpath(dom, "//a/text()")[0] + for entry in data[key]: + if not entry.has_key('name'): + if not entry: + continue + # this is an award, not a recipient + entry['assigner'] = assigner.strip() + # find the recipients + matches = [p for p in data[key] + if p.has_key('name') and (entry['anchor'] == + p['anchor'])] + if self.subject == 'title': + recipients = [Person(name=recipient['name'], + personID=analyze_imdbid(recipient['link'])) + for recipient in matches] + entry['to'] = recipients + elif self.subject == 'name': + recipients = [Movie(title=recipient['name'], + movieID=analyze_imdbid(recipient['link'])) + for recipient in matches] + entry['for'] = recipients + nd.append(entry) + del entry['anchor'] + return {'awards': nd} + + +class DOMHTMLTaglinesParser(DOMParserBase): + """Parser for the "taglines" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + tparser = DOMHTMLTaglinesParser() + result = tparser.parse(taglines_html_string) + """ + extractors = [Extractor(label='taglines', + path="//div[@id='tn15content']/p", + attrs=Attribute(key='taglines', multi=True, + path="./text()"))] + + +class DOMHTMLKeywordsParser(DOMParserBase): + """Parser for the "keywords" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + kwparser = DOMHTMLKeywordsParser() + result = kwparser.parse(keywords_html_string) + """ + extractors = [Extractor(label='keywords', + path="//a[starts-with(@href, '/keyword/')]", + attrs=Attribute(key='keywords', + path="./text()", multi=True, + postprocess=lambda x: \ + x.lower().replace(' ', '-')))] + + +class DOMHTMLAlternateVersionsParser(DOMParserBase): + """Parser for the "alternate versions" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + avparser = HTMLAlternateVersionsParser() + result = avparser.parse(alternateversions_html_string) + """ + _defGetRefs = True + extractors = [Extractor(label='alternate versions', + path="//ul[@class='trivia']/li", + attrs=Attribute(key='alternate versions', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip()))] + + +class DOMHTMLTriviaParser(DOMParserBase): + """Parser for the "trivia" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + avparser = HTMLAlternateVersionsParser() + result = avparser.parse(alternateversions_html_string) + """ + _defGetRefs = True + extractors = [Extractor(label='alternate versions', + path="//div[@class='sodatext']", + attrs=Attribute(key='trivia', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip()))] + + def preprocess_dom(self, dom): + # Remove "link this quote" links. + for qLink in self.xpath(dom, "//span[@class='linksoda']"): + qLink.drop_tree() + return dom + + + +class DOMHTMLSoundtrackParser(DOMHTMLAlternateVersionsParser): + kind = 'soundtrack' + + preprocessors = [ + ('<br>', '\n') + ] + + def postprocess_data(self, data): + if 'soundtrack' in data: + nd = [] + for x in data['soundtrack']: + ds = x.split('\n') + title = ds[0] + if title[0] == '"' and title[-1] == '"': + title = title[1:-1] + nds = [] + newData = {} + for l in ds[1:]: + if ' with ' in l or ' by ' in l or ' from ' in l \ + or ' of ' in l or l.startswith('From '): + nds.append(l) + else: + if nds: + nds[-1] += l + else: + nds.append(l) + newData[title] = {} + for l in nds: + skip = False + for sep in ('From ',): + if l.startswith(sep): + fdix = len(sep) + kind = l[:fdix].rstrip().lower() + info = l[fdix:].lstrip() + newData[title][kind] = info + skip = True + if not skip: + for sep in ' with ', ' by ', ' from ', ' of ': + fdix = l.find(sep) + if fdix != -1: + fdix = fdix+len(sep) + kind = l[:fdix].rstrip().lower() + info = l[fdix:].lstrip() + newData[title][kind] = info + break + nd.append(newData) + data['soundtrack'] = nd + return data + + +class DOMHTMLCrazyCreditsParser(DOMParserBase): + """Parser for the "crazy credits" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + ccparser = DOMHTMLCrazyCreditsParser() + result = ccparser.parse(crazycredits_html_string) + """ + _defGetRefs = True + + extractors = [Extractor(label='crazy credits', path="//ul/li/tt", + attrs=Attribute(key='crazy credits', multi=True, + path=".//text()", + postprocess=lambda x: \ + x.replace('\n', ' ').replace(' ', ' ')))] + + +class DOMHTMLGoofsParser(DOMParserBase): + """Parser for the "goofs" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + gparser = DOMHTMLGoofsParser() + result = gparser.parse(goofs_html_string) + """ + _defGetRefs = True + + extractors = [Extractor(label='goofs', path="//ul[@class='trivia']/li", + attrs=Attribute(key='goofs', multi=True, path=".//text()", + postprocess=lambda x: (x or u'').strip()))] + + +class DOMHTMLQuotesParser(DOMParserBase): + """Parser for the "memorable quotes" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + qparser = DOMHTMLQuotesParser() + result = qparser.parse(quotes_html_string) + """ + _defGetRefs = True + + extractors = [ + Extractor(label='quotes', + path="//div[@class='_imdbpy']", + attrs=Attribute(key='quotes', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip().replace(' \n', + '::').replace('::\n', '::').replace('\n', ' '))) + ] + + preprocessors = [ + (re.compile('(<a name="?qt[0-9]{7}"?></a>)', re.I), + r'\1<div class="_imdbpy">'), + (re.compile('<hr width="30%">', re.I), '</div>'), + (re.compile('<hr/>', re.I), '</div>'), + (re.compile('<script.*?</script>', re.I|re.S), ''), + # For BeautifulSoup. + (re.compile('<!-- sid: t-channel : MIDDLE_CENTER -->', re.I), '</div>') + ] + + def preprocess_dom(self, dom): + # Remove "link this quote" links. + for qLink in self.xpath(dom, "//p[@class='linksoda']"): + qLink.drop_tree() + return dom + + def postprocess_data(self, data): + if 'quotes' not in data: + return {} + for idx, quote in enumerate(data['quotes']): + data['quotes'][idx] = quote.split('::') + return data + + +class DOMHTMLReleaseinfoParser(DOMParserBase): + """Parser for the "release dates" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + rdparser = DOMHTMLReleaseinfoParser() + result = rdparser.parse(releaseinfo_html_string) + """ + extractors = [Extractor(label='release dates', + path="//th[@class='xxxx']/../../tr", + attrs=Attribute(key='release dates', multi=True, + path={'country': ".//td[1]//text()", + 'date': ".//td[2]//text()", + 'notes': ".//td[3]//text()"})), + Extractor(label='akas', + path="//div[@class='_imdbpy_akas']/table/tr", + attrs=Attribute(key='akas', multi=True, + path={'title': "./td[1]/text()", + 'countries': "./td[2]/text()"}))] + + preprocessors = [ + (re.compile('(<h5><a name="?akas"?.*</table>)', re.I | re.M | re.S), + r'<div class="_imdbpy_akas">\1</div>')] + + def postprocess_data(self, data): + if not ('release dates' in data or 'akas' in data): return data + releases = data.get('release dates') or [] + rl = [] + for i in releases: + country = i.get('country') + date = i.get('date') + if not (country and date): continue + country = country.strip() + date = date.strip() + if not (country and date): continue + notes = i['notes'] + info = u'%s::%s' % (country, date) + if notes: + info += notes + rl.append(info) + if releases: + del data['release dates'] + if rl: + data['release dates'] = rl + akas = data.get('akas') or [] + nakas = [] + for aka in akas: + title = (aka.get('title') or '').strip() + if not title: + continue + countries = (aka.get('countries') or '').split('/') + if not countries: + nakas.append(title) + else: + for country in countries: + nakas.append('%s::%s' % (title, country.strip())) + if akas: + del data['akas'] + if nakas: + data['akas from release info'] = nakas + return data + + +class DOMHTMLRatingsParser(DOMParserBase): + """Parser for the "user ratings" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + rparser = DOMHTMLRatingsParser() + result = rparser.parse(userratings_html_string) + """ + re_means = re.compile('mean\s*=\s*([0-9]\.[0-9])\.\s*median\s*=\s*([0-9])', + re.I) + extractors = [ + Extractor(label='number of votes', + path="//td[b='Percentage']/../../tr", + attrs=[Attribute(key='votes', + multi=True, + path={ + 'votes': "td[1]//text()", + 'ordinal': "td[3]//text()" + })]), + Extractor(label='mean and median', + path="//p[starts-with(text(), 'Arithmetic mean')]", + attrs=Attribute(key='mean and median', + path="text()")), + Extractor(label='rating', + path="//a[starts-with(@href, '/search/title?user_rating=')]", + attrs=Attribute(key='rating', + path="text()")), + Extractor(label='demographic voters', + path="//td[b='Average']/../../tr", + attrs=Attribute(key='demographic voters', + multi=True, + path={ + 'voters': "td[1]//text()", + 'votes': "td[2]//text()", + 'average': "td[3]//text()" + })), + Extractor(label='top 250', + path="//a[text()='top 250']", + attrs=Attribute(key='top 250', + path="./preceding-sibling::text()[1]")) + ] + + def postprocess_data(self, data): + nd = {} + votes = data.get('votes', []) + if votes: + nd['number of votes'] = {} + for i in xrange(1, 11): + _ordinal = int(votes[i]['ordinal']) + _strvts = votes[i]['votes'] or '0' + nd['number of votes'][_ordinal] = \ + int(_strvts.replace(',', '')) + mean = data.get('mean and median', '') + if mean: + means = self.re_means.findall(mean) + if means and len(means[0]) == 2: + am, med = means[0] + try: am = float(am) + except (ValueError, OverflowError): pass + if type(am) is type(1.0): + nd['arithmetic mean'] = am + try: med = int(med) + except (ValueError, OverflowError): pass + if type(med) is type(0): + nd['median'] = med + if 'rating' in data: + nd['rating'] = float(data['rating']) + dem_voters = data.get('demographic voters') + if dem_voters: + nd['demographic'] = {} + for i in xrange(1, len(dem_voters)): + if (dem_voters[i]['votes'] is not None) \ + and (dem_voters[i]['votes'].strip()): + nd['demographic'][dem_voters[i]['voters'].strip().lower()] \ + = (int(dem_voters[i]['votes'].replace(',', '')), + float(dem_voters[i]['average'])) + if 'imdb users' in nd.get('demographic', {}): + nd['votes'] = nd['demographic']['imdb users'][0] + nd['demographic']['all votes'] = nd['demographic']['imdb users'] + del nd['demographic']['imdb users'] + top250 = data.get('top 250') + if top250: + sd = top250[9:] + i = sd.find(' ') + if i != -1: + sd = sd[:i] + try: sd = int(sd) + except (ValueError, OverflowError): pass + if type(sd) is type(0): + nd['top 250 rank'] = sd + return nd + + +class DOMHTMLEpisodesRatings(DOMParserBase): + """Parser for the "episode ratings ... by date" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + erparser = DOMHTMLEpisodesRatings() + result = erparser.parse(eprating_html_string) + """ + _containsObjects = True + + extractors = [Extractor(label='title', path="//title", + attrs=Attribute(key='title', path="./text()")), + Extractor(label='ep ratings', + path="//th/../..//tr", + attrs=Attribute(key='episodes', multi=True, + path={'nr': ".//td[1]/text()", + 'ep title': ".//td[2]//text()", + 'movieID': ".//td[2]/a/@href", + 'rating': ".//td[3]/text()", + 'votes': ".//td[4]/text()"}))] + + def postprocess_data(self, data): + if 'title' not in data or 'episodes' not in data: return {} + nd = [] + title = data['title'] + for i in data['episodes']: + ept = i['ep title'] + movieID = analyze_imdbid(i['movieID']) + votes = i['votes'] + rating = i['rating'] + if not (ept and movieID and votes and rating): continue + try: + votes = int(votes.replace(',', '').replace('.', '')) + except: + pass + try: + rating = float(rating) + except: + pass + ept = ept.strip() + ept = u'%s {%s' % (title, ept) + nr = i['nr'] + if nr: + ept += u' (#%s)' % nr.strip() + ept += '}' + if movieID is not None: + movieID = str(movieID) + m = Movie(title=ept, movieID=movieID, accessSystem=self._as, + modFunct=self._modFunct) + epofdict = m.get('episode of') + if epofdict is not None: + m['episode of'] = Movie(data=epofdict, accessSystem=self._as, + modFunct=self._modFunct) + nd.append({'episode': m, 'votes': votes, 'rating': rating}) + return {'episodes rating': nd} + + +def _normalize_href(href): + if (href is not None) and (not href.lower().startswith('http://')): + if href.startswith('/'): href = href[1:] + # TODO: imdbURL_base may be set by the user! + href = '%s%s' % (imdbURL_base, href) + return href + + +class DOMHTMLOfficialsitesParser(DOMParserBase): + """Parser for the "official sites", "external reviews", "newsgroup + reviews", "miscellaneous links", "sound clips", "video clips" and + "photographs" pages of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + osparser = DOMHTMLOfficialsitesParser() + result = osparser.parse(officialsites_html_string) + """ + kind = 'official sites' + + extractors = [ + Extractor(label='site', + path="//ol/li/a", + attrs=Attribute(key='self.kind', + multi=True, + path={ + 'link': "./@href", + 'info': "./text()" + }, + postprocess=lambda x: (x.get('info').strip(), + urllib.unquote(_normalize_href(x.get('link')))))) + ] + + +class DOMHTMLConnectionParser(DOMParserBase): + """Parser for the "connections" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + connparser = DOMHTMLConnectionParser() + result = connparser.parse(connections_html_string) + """ + _containsObjects = True + + extractors = [Extractor(label='connection', + group="//div[@class='_imdbpy']", + group_key="./h5/text()", + group_key_normalize=lambda x: x.lower(), + path="./a", + attrs=Attribute(key=None, + path={'title': "./text()", + 'movieID': "./@href"}, + multi=True))] + + preprocessors = [ + ('<h5>', '</div><div class="_imdbpy"><h5>'), + # To get the movie's year. + ('</a> (', ' ('), + ('\n<br/>', '</a>'), + ('<br/> - ', '::') + ] + + def postprocess_data(self, data): + for key in data.keys(): + nl = [] + for v in data[key]: + title = v['title'] + ts = title.split('::', 1) + title = ts[0].strip() + notes = u'' + if len(ts) == 2: + notes = ts[1].strip() + m = Movie(title=title, + movieID=analyze_imdbid(v['movieID']), + accessSystem=self._as, notes=notes, + modFunct=self._modFunct) + nl.append(m) + data[key] = nl + if not data: return {} + return {'connections': data} + + +class DOMHTMLLocationsParser(DOMParserBase): + """Parser for the "locations" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + lparser = DOMHTMLLocationsParser() + result = lparser.parse(locations_html_string) + """ + extractors = [Extractor(label='locations', path="//dt", + attrs=Attribute(key='locations', multi=True, + path={'place': ".//text()", + 'note': "./following-sibling::dd[1]" \ + "//text()"}, + postprocess=lambda x: (u'%s::%s' % ( + x['place'].strip(), + (x['note'] or u'').strip())).strip(':')))] + + +class DOMHTMLTechParser(DOMParserBase): + """Parser for the "technical", "business", "literature", + "publicity" (for people) and "contacts (for people) pages of + a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + tparser = HTMLTechParser() + result = tparser.parse(technical_html_string) + """ + kind = 'tech' + + extractors = [Extractor(label='tech', + group="//h5", + group_key="./text()", + group_key_normalize=lambda x: x.lower(), + path="./following-sibling::div[1]", + attrs=Attribute(key=None, + path=".//text()", + postprocess=lambda x: [t.strip() + for t in x.split('\n') if t.strip()]))] + + preprocessors = [ + (re.compile('(<h5>.*?</h5>)', re.I), r'</div>\1<div class="_imdbpy">'), + (re.compile('((<br/>|</p>|</table>))\n?<br/>(?!<a)', re.I), + r'\1</div>'), + # the ones below are for the publicity parser + (re.compile('<p>(.*?)</p>', re.I), r'\1<br/>'), + (re.compile('(</td><td valign="top">)', re.I), r'\1::'), + (re.compile('(</tr><tr>)', re.I), r'\n\1'), + # this is for splitting individual entries + (re.compile('<br/>', re.I), r'\n'), + ] + + def postprocess_data(self, data): + for key in data: + data[key] = filter(None, data[key]) + if self.kind in ('literature', 'business', 'contacts') and data: + if 'screenplay/teleplay' in data: + data['screenplay-teleplay'] = data['screenplay/teleplay'] + del data['screenplay/teleplay'] + data = {self.kind: data} + else: + if self.kind == 'publicity': + if 'biography (print)' in data: + data['biography-print'] = data['biography (print)'] + del data['biography (print)'] + # Tech info. + for key in data.keys(): + if key.startswith('film negative format'): + data['film negative format'] = data[key] + del data[key] + elif key.startswith('film length'): + data['film length'] = data[key] + del data[key] + return data + + +class DOMHTMLRecParser(DOMParserBase): + """Parser for the "recommendations" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + rparser = HTMLRecParser() + result = rparser.parse(recommendations_html_string) + """ + _containsObjects = True + + extractors = [Extractor(label='recommendations', + path="//td[@valign='middle'][1]", + attrs=Attribute(key='../../tr/td[1]//text()', + multi=True, + path={'title': ".//text()", + 'movieID': ".//a/@href"}))] + def postprocess_data(self, data): + for key in data.keys(): + n_key = key + n_keyl = n_key.lower() + if n_keyl == 'suggested by the database': + n_key = 'database' + elif n_keyl == 'imdb users recommend': + n_key = 'users' + data[n_key] = [Movie(title=x['title'], + movieID=analyze_imdbid(x['movieID']), + accessSystem=self._as, modFunct=self._modFunct) + for x in data[key]] + del data[key] + if data: return {'recommendations': data} + return data + + +class DOMHTMLNewsParser(DOMParserBase): + """Parser for the "news" page of a given movie or person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + nwparser = DOMHTMLNewsParser() + result = nwparser.parse(news_html_string) + """ + _defGetRefs = True + + extractors = [ + Extractor(label='news', + path="//h2", + attrs=Attribute(key='news', + multi=True, + path={ + 'title': "./text()", + 'fromdate': "../following-sibling::p[1]/small//text()", + # FIXME: sometimes (see The Matrix (1999)) <p> is found + # inside news text. + 'body': "../following-sibling::p[2]//text()", + 'link': "../..//a[text()='Permalink']/@href", + 'fulllink': "../..//a[starts-with(text(), " \ + "'See full article at')]/@href" + }, + postprocess=lambda x: { + 'title': x.get('title').strip(), + 'date': x.get('fromdate').split('|')[0].strip(), + 'from': x.get('fromdate').split('|')[1].replace('From ', + '').strip(), + 'body': (x.get('body') or u'').strip(), + 'link': _normalize_href(x.get('link')), + 'full article link': _normalize_href(x.get('fulllink')) + })) + ] + + preprocessors = [ + (re.compile('(<a name=[^>]+><h2>)', re.I), r'<div class="_imdbpy">\1'), + (re.compile('(<hr/>)', re.I), r'</div>\1'), + (re.compile('<p></p>', re.I), r'') + ] + + def postprocess_data(self, data): + if not data.has_key('news'): + return {} + for news in data['news']: + if news.has_key('full article link'): + if news['full article link'] is None: + del news['full article link'] + return data + + +def _parse_review(x): + result = {} + title = x.get('title').strip() + if title[-1] == ':': title = title[:-1] + result['title'] = title + result['link'] = _normalize_href(x.get('link')) + kind = x.get('kind').strip() + if kind[-1] == ':': kind = kind[:-1] + result['review kind'] = kind + text = x.get('review').replace('\n\n', '||').replace('\n', ' ').split('||') + review = '\n'.join(text) + if x.get('author') is not None: + author = x.get('author').strip() + review = review.split(author)[0].strip() + result['review author'] = author[2:] + if x.get('item') is not None: + item = x.get('item').strip() + review = review[len(item):].strip() + review = "%s: %s" % (item, review) + result['review'] = review + return result + + +class DOMHTMLSeasonEpisodesParser(DOMParserBase): + """Parser for the "episode list" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + sparser = DOMHTMLSeasonEpisodesParser() + result = sparser.parse(episodes_html_string) + """ + extractors = [ + Extractor(label='series link', + path="//div[@class='parent']", + attrs=[Attribute(key='series link', + path=".//a/@href")] + ), + + Extractor(label='series title', + path="//head/meta[@property='og:title']", + attrs=[Attribute(key='series title', + path="./@content")] + ), + + Extractor(label='seasons list', + path="//select[@id='bySeason']//option", + attrs=[Attribute(key='_seasons', + multi=True, + path="./@value")]), + + Extractor(label='selected season', + path="//select[@id='bySeason']//option[@selected]", + attrs=[Attribute(key='_current_season', + path='./@value')]), + + Extractor(label='episodes', + path=".", + group="//div[@class='info']", + group_key=".//meta/@content", + group_key_normalize=lambda x: 'episode %s' % x, + attrs=[Attribute(key=None, + multi=True, + path={ + "link": ".//strong//a[@href][1]/@href", + "original air date": ".//div[@class='airdate']/text()", + "title": ".//strong//text()", + "plot": ".//div[@class='item_description']//text()" + } + )] + ) + ] + + def postprocess_data(self, data): + series_id = analyze_imdbid(data.get('series link')) + series_title = data.get('series title', '').strip() + selected_season = data.get('_current_season', + 'unknown season').strip() + if not (series_id and series_title): + return {} + series = Movie(title=series_title, movieID=str(series_id), + accessSystem=self._as, modFunct=self._modFunct) + if series.get('kind') == 'movie': + series['kind'] = u'tv series' + try: selected_season = int(selected_season) + except: pass + nd = {selected_season: {}} + for episode_nr, episode in data.iteritems(): + if not (episode and episode[0] and + episode_nr.startswith('episode ')): + continue + episode = episode[0] + episode_nr = episode_nr[8:].rstrip() + try: episode_nr = int(episode_nr) + except: pass + episode_id = analyze_imdbid(episode.get('link' '')) + episode_air_date = episode.get('original air date', + '').strip() + episode_title = episode.get('title', '').strip() + episode_plot = episode.get('plot', '') + if not (episode_nr and episode_id and episode_title): + continue + ep_obj = Movie(movieID=episode_id, title=episode_title, + accessSystem=self._as, modFunct=self._modFunct) + ep_obj['kind'] = u'episode' + ep_obj['episode of'] = series + ep_obj['season'] = selected_season + ep_obj['episode'] = episode_nr + if episode_air_date: + ep_obj['original air date'] = episode_air_date + if episode_air_date[-4:].isdigit(): + ep_obj['year'] = episode_air_date[-4:] + if episode_plot: + ep_obj['plot'] = episode_plot + nd[selected_season][episode_nr] = ep_obj + _seasons = data.get('_seasons') or [] + for idx, season in enumerate(_seasons): + try: _seasons[idx] = int(season) + except: pass + return {'episodes': nd, '_seasons': _seasons, + '_current_season': selected_season} + + +def _build_episode(x): + """Create a Movie object for a given series' episode.""" + episode_id = analyze_imdbid(x.get('link')) + episode_title = x.get('title') + e = Movie(movieID=episode_id, title=episode_title) + e['kind'] = u'episode' + oad = x.get('oad') + if oad: + e['original air date'] = oad.strip() + year = x.get('year') + if year is not None: + year = year[5:] + if year == 'unknown': year = u'????' + if year and year.isdigit(): + year = int(year) + e['year'] = year + else: + if oad and oad[-4:].isdigit(): + e['year'] = int(oad[-4:]) + epinfo = x.get('episode') + if epinfo is not None: + season, episode = epinfo.split(':')[0].split(',') + e['season'] = int(season[7:]) + e['episode'] = int(episode[8:]) + else: + e['season'] = 'unknown' + e['episode'] = 'unknown' + plot = x.get('plot') + if plot: + e['plot'] = plot.strip() + return e + + +class DOMHTMLEpisodesParser(DOMParserBase): + """Parser for the "episode list" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + eparser = DOMHTMLEpisodesParser() + result = eparser.parse(episodes_html_string) + """ + # XXX: no more used for the list of episodes parser, + # but only for the episodes cast parser (see below). + _containsObjects = True + + kind = 'episodes list' + _episodes_path = "..//h4" + _oad_path = "./following-sibling::span/strong[1]/text()" + + def _init(self): + self.extractors = [ + Extractor(label='series', + path="//html", + attrs=[Attribute(key='series title', + path=".//title/text()"), + Attribute(key='series movieID', + path=".//h1/a[@class='main']/@href", + postprocess=analyze_imdbid) + ]), + Extractor(label='episodes', + group="//div[@class='_imdbpy']/h3", + group_key="./a/@name", + path=self._episodes_path, + attrs=Attribute(key=None, + multi=True, + path={ + 'link': "./a/@href", + 'title': "./a/text()", + 'year': "./preceding-sibling::a[1]/@name", + 'episode': "./text()[1]", + 'oad': self._oad_path, + 'plot': "./following-sibling::text()[1]" + }, + postprocess=_build_episode))] + if self.kind == 'episodes cast': + self.extractors += [ + Extractor(label='cast', + group="//h4", + group_key="./text()[1]", + group_key_normalize=lambda x: x.strip(), + path="./following-sibling::table[1]//td[@class='nm']", + attrs=Attribute(key=None, + multi=True, + path={'person': "..//text()", + 'link': "./a/@href", + 'roleID': \ + "../td[4]/div[@class='_imdbpyrole']/@roleid"}, + postprocess=lambda x: \ + build_person(x.get('person') or u'', + personID=analyze_imdbid(x.get('link')), + roleID=(x.get('roleID') or u'').split('/'), + accessSystem=self._as, + modFunct=self._modFunct))) + ] + + preprocessors = [ + (re.compile('(<hr/>\n)(<h3>)', re.I), + r'</div>\1<div class="_imdbpy">\2'), + (re.compile('(</p>\n\n)</div>', re.I), r'\1'), + (re.compile('<h3>(.*?)</h3>', re.I), r'<h4>\1</h4>'), + (_reRolesMovie, _manageRoles), + (re.compile('(<br/> <br/>\n)(<hr/>)', re.I), r'\1</div>\2') + ] + + def postprocess_data(self, data): + # A bit extreme? + if not 'series title' in data: return {} + if not 'series movieID' in data: return {} + stitle = data['series title'].replace('- Episode list', '') + stitle = stitle.replace('- Episodes list', '') + stitle = stitle.replace('- Episode cast', '') + stitle = stitle.replace('- Episodes cast', '') + stitle = stitle.strip() + if not stitle: return {} + seriesID = data['series movieID'] + if seriesID is None: return {} + series = Movie(title=stitle, movieID=str(seriesID), + accessSystem=self._as, modFunct=self._modFunct) + nd = {} + for key in data.keys(): + if key.startswith('filter-season-') or key.startswith('season-'): + season_key = key.replace('filter-season-', '').replace('season-', '') + try: season_key = int(season_key) + except: pass + nd[season_key] = {} + ep_counter = 1 + for episode in data[key]: + if not episode: continue + episode_key = episode.get('episode') + if episode_key is None: continue + if not isinstance(episode_key, int): + episode_key = ep_counter + ep_counter += 1 + cast_key = 'Season %s, Episode %s:' % (season_key, + episode_key) + if data.has_key(cast_key): + cast = data[cast_key] + for i in xrange(len(cast)): + cast[i].billingPos = i + 1 + episode['cast'] = cast + episode['episode of'] = series + nd[season_key][episode_key] = episode + if len(nd) == 0: + return {} + return {'episodes': nd} + + +class DOMHTMLEpisodesCastParser(DOMHTMLEpisodesParser): + """Parser for the "episodes cast" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + eparser = DOMHTMLEpisodesParser() + result = eparser.parse(episodes_html_string) + """ + kind = 'episodes cast' + _episodes_path = "..//h4" + _oad_path = "./following-sibling::b[1]/text()" + + +class DOMHTMLFaqsParser(DOMParserBase): + """Parser for the "FAQ" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + fparser = DOMHTMLFaqsParser() + result = fparser.parse(faqs_html_string) + """ + _defGetRefs = True + + # XXX: bsoup and lxml don't match (looks like a minor issue, anyway). + + extractors = [ + Extractor(label='faqs', + path="//div[@class='section']", + attrs=Attribute(key='faqs', + multi=True, + path={ + 'question': "./h3/a/span/text()", + 'answer': "../following-sibling::div[1]//text()" + }, + postprocess=lambda x: u'%s::%s' % (x.get('question').strip(), + '\n\n'.join(x.get('answer').replace( + '\n\n', '\n').strip().split('||'))))) + ] + + preprocessors = [ + (re.compile('<br/><br/>', re.I), r'||'), + (re.compile('<h4>(.*?)</h4>\n', re.I), r'||\1--'), + (re.compile('<span class="spoiler"><span>(.*?)</span></span>', re.I), + r'[spoiler]\1[/spoiler]') + ] + + +class DOMHTMLAiringParser(DOMParserBase): + """Parser for the "airing" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + aparser = DOMHTMLAiringParser() + result = aparser.parse(airing_html_string) + """ + _containsObjects = True + + extractors = [ + Extractor(label='series title', + path="//title", + attrs=Attribute(key='series title', path="./text()", + postprocess=lambda x: \ + x.replace(' - TV schedule', u''))), + Extractor(label='series id', + path="//h1/a[@href]", + attrs=Attribute(key='series id', path="./@href")), + + Extractor(label='tv airings', + path="//tr[@class]", + attrs=Attribute(key='airing', + multi=True, + path={ + 'date': "./td[1]//text()", + 'time': "./td[2]//text()", + 'channel': "./td[3]//text()", + 'link': "./td[4]/a[1]/@href", + 'title': "./td[4]//text()", + 'season': "./td[5]//text()", + }, + postprocess=lambda x: { + 'date': x.get('date'), + 'time': x.get('time'), + 'channel': x.get('channel').strip(), + 'link': x.get('link'), + 'title': x.get('title'), + 'season': (x.get('season') or '').strip() + } + )) + ] + + def postprocess_data(self, data): + if len(data) == 0: + return {} + seriesTitle = data['series title'] + seriesID = analyze_imdbid(data['series id']) + if data.has_key('airing'): + for airing in data['airing']: + title = airing.get('title', '').strip() + if not title: + epsTitle = seriesTitle + if seriesID is None: + continue + epsID = seriesID + else: + epsTitle = '%s {%s}' % (data['series title'], + airing['title']) + epsID = analyze_imdbid(airing['link']) + e = Movie(title=epsTitle, movieID=epsID) + airing['episode'] = e + del airing['link'] + del airing['title'] + if not airing['season']: + del airing['season'] + if 'series title' in data: + del data['series title'] + if 'series id' in data: + del data['series id'] + if 'airing' in data: + data['airing'] = filter(None, data['airing']) + if 'airing' not in data or not data['airing']: + return {} + return data + + +class DOMHTMLSynopsisParser(DOMParserBase): + """Parser for the "synopsis" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + sparser = HTMLSynopsisParser() + result = sparser.parse(synopsis_html_string) + """ + extractors = [ + Extractor(label='synopsis', + path="//div[@class='display'][not(@style)]", + attrs=Attribute(key='synopsis', + path=".//text()", + postprocess=lambda x: '\n\n'.join(x.strip().split('||')))) + ] + + preprocessors = [ + (re.compile('<br/><br/>', re.I), r'||') + ] + + +class DOMHTMLParentsGuideParser(DOMParserBase): + """Parser for the "parents guide" page of a given movie. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + pgparser = HTMLParentsGuideParser() + result = pgparser.parse(parentsguide_html_string) + """ + extractors = [ + Extractor(label='parents guide', + group="//div[@class='section']", + group_key="./h3/a/span/text()", + group_key_normalize=lambda x: x.lower(), + path="../following-sibling::div[1]/p", + attrs=Attribute(key=None, + path=".//text()", + postprocess=lambda x: [t.strip().replace('\n', ' ') + for t in x.split('||') if t.strip()])) + ] + + preprocessors = [ + (re.compile('<br/><br/>', re.I), r'||') + ] + + def postprocess_data(self, data): + data2 = {} + for key in data: + if data[key]: + data2[key] = data[key] + if not data2: + return {} + return {'parents guide': data2} + + +_OBJECTS = { + 'movie_parser': ((DOMHTMLMovieParser,), None), + 'plot_parser': ((DOMHTMLPlotParser,), None), + 'movie_awards_parser': ((DOMHTMLAwardsParser,), None), + 'taglines_parser': ((DOMHTMLTaglinesParser,), None), + 'keywords_parser': ((DOMHTMLKeywordsParser,), None), + 'crazycredits_parser': ((DOMHTMLCrazyCreditsParser,), None), + 'goofs_parser': ((DOMHTMLGoofsParser,), None), + 'alternateversions_parser': ((DOMHTMLAlternateVersionsParser,), None), + 'trivia_parser': ((DOMHTMLTriviaParser,), None), + 'soundtrack_parser': ((DOMHTMLSoundtrackParser,), {'kind': 'soundtrack'}), + 'quotes_parser': ((DOMHTMLQuotesParser,), None), + 'releasedates_parser': ((DOMHTMLReleaseinfoParser,), None), + 'ratings_parser': ((DOMHTMLRatingsParser,), None), + 'officialsites_parser': ((DOMHTMLOfficialsitesParser,), None), + 'externalrev_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'external reviews'}), + 'newsgrouprev_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'newsgroup reviews'}), + 'misclinks_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'misc links'}), + 'soundclips_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'sound clips'}), + 'videoclips_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'video clips'}), + 'photosites_parser': ((DOMHTMLOfficialsitesParser,), + {'kind': 'photo sites'}), + 'connections_parser': ((DOMHTMLConnectionParser,), None), + 'tech_parser': ((DOMHTMLTechParser,), None), + 'business_parser': ((DOMHTMLTechParser,), + {'kind': 'business', '_defGetRefs': 1}), + 'literature_parser': ((DOMHTMLTechParser,), {'kind': 'literature'}), + 'locations_parser': ((DOMHTMLLocationsParser,), None), + 'rec_parser': ((DOMHTMLRecParser,), None), + 'news_parser': ((DOMHTMLNewsParser,), None), + 'episodes_parser': ((DOMHTMLEpisodesParser,), None), + 'season_episodes_parser': ((DOMHTMLSeasonEpisodesParser,), None), + 'episodes_cast_parser': ((DOMHTMLEpisodesCastParser,), None), + 'eprating_parser': ((DOMHTMLEpisodesRatings,), None), + 'movie_faqs_parser': ((DOMHTMLFaqsParser,), None), + 'airing_parser': ((DOMHTMLAiringParser,), None), + 'synopsis_parser': ((DOMHTMLSynopsisParser,), None), + 'parentsguide_parser': ((DOMHTMLParentsGuideParser,), None) +} + diff --git a/lib/imdb/parser/http/personParser.py b/lib/imdb/parser/http/personParser.py new file mode 100644 index 0000000000000000000000000000000000000000..af9672103ee73b2684cb74479ce0bfd1e17c6937 --- /dev/null +++ b/lib/imdb/parser/http/personParser.py @@ -0,0 +1,507 @@ +""" +parser.http.personParser module (imdb package). + +This module provides the classes (and the instances), used to parse +the IMDb pages on the akas.imdb.com server about a person. +E.g., for "Mel Gibson" the referred pages would be: + categorized: http://akas.imdb.com/name/nm0000154/maindetails + biography: http://akas.imdb.com/name/nm0000154/bio + ...and so on... + +Copyright 2004-20101 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +from imdb.Movie import Movie +from imdb.utils import analyze_name, canonicalName, normalizeName, \ + analyze_title, date_and_notes +from utils import build_movie, DOMParserBase, Attribute, Extractor, \ + analyze_imdbid + + +from movieParser import _manageRoles +_reRoles = re.compile(r'(<li>.*? \.\.\.\. )(.*?)(</li>|<br>)', + re.I | re.M | re.S) + +def build_date(date): + day = date.get('day') + year = date.get('year') + if day and year: + return "%s %s" % (day, year) + if day: + return day + if year: + return year + return "" + +class DOMHTMLMaindetailsParser(DOMParserBase): + """Parser for the "categorized" (maindetails) page of a given person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + cparser = DOMHTMLMaindetailsParser() + result = cparser.parse(categorized_html_string) + """ + _containsObjects = True + + _birth_attrs = [Attribute(key='birth date', + path='.//time[@itemprop="birthDate"]/@datetime'), + Attribute(key='birth place', + path=".//a[starts-with(@href, " \ + "'/search/name?birth_place=')]/text()")] + _death_attrs = [Attribute(key='death date', + path='.//time[@itemprop="deathDate"]/@datetime'), + Attribute(key='death place', + path=".//a[starts-with(@href, " \ + "'/search/name?death_place=')]/text()")] + _film_attrs = [Attribute(key=None, + multi=True, + path={ + 'link': "./b/a[1]/@href", + 'title': "./b/a[1]/text()", + 'notes': "./b/following-sibling::text()", + 'year': "./span[@class='year_column']/text()", + 'status': "./a[@class='in_production']/text()", + 'rolesNoChar': './/br/following-sibling::text()', + 'chrRoles': "./a[@imdbpyname]/@imdbpyname", + 'roleID': "./a[starts-with(@href, '/character/')]/@href" + }, + postprocess=lambda x: + build_movie(x.get('title') or u'', + year=x.get('year'), + movieID=analyze_imdbid(x.get('link') or u''), + rolesNoChar=(x.get('rolesNoChar') or u'').strip(), + chrRoles=(x.get('chrRoles') or u'').strip(), + additionalNotes=x.get('notes'), + roleID=(x.get('roleID') or u''), + status=x.get('status') or None))] + + extractors = [ + Extractor(label='name', + path="//h1[@class='header']", + attrs=Attribute(key='name', + path=".//text()", + postprocess=lambda x: analyze_name(x, + canonical=1))), + + Extractor(label='birth info', + path="//div[h4='Born:']", + attrs=_birth_attrs), + + Extractor(label='death info', + path="//div[h4='Died:']", + attrs=_death_attrs), + + Extractor(label='headshot', + path="//td[@id='img_primary']/a", + attrs=Attribute(key='headshot', + path="./img/@src")), + + Extractor(label='akas', + path="//div[h4='Alternate Names:']", + attrs=Attribute(key='akas', + path="./text()", + postprocess=lambda x: x.strip().split(' '))), + + Extractor(label='filmography', + group="//div[starts-with(@id, 'filmo-head-')]", + group_key="./a[@name]/text()", + group_key_normalize=lambda x: x.lower().replace(': ', ' '), + path="./following-sibling::div[1]" \ + "/div[starts-with(@class, 'filmo-row')]", + attrs=_film_attrs), + + Extractor(label='indevelopment', + path="//div[starts-with(@class,'devitem')]", + attrs=Attribute(key='in development', + multi=True, + path={ + 'link': './a/@href', + 'title': './a/text()' + }, + postprocess=lambda x: + build_movie(x.get('title') or u'', + movieID=analyze_imdbid(x.get('link') or u''), + roleID=(x.get('roleID') or u'').split('/'), + status=x.get('status') or None))) + ] + + preprocessors = [('<div class="clear"/> </div>', ''), + ('<br/>', '<br />'), + (re.compile(r'(<a href="/character/ch[0-9]{7}")>(.*?)</a>'), + r'\1 imdbpyname="\2@@">\2</a>')] + + def postprocess_data(self, data): + for what in 'birth date', 'death date': + if what in data and not data[what]: + del data[what] + # XXX: the code below is for backwards compatibility + # probably could be removed + for key in data.keys(): + if key.startswith('actor '): + if not data.has_key('actor'): + data['actor'] = [] + data['actor'].extend(data[key]) + del data[key] + if key.startswith('actress '): + if not data.has_key('actress'): + data['actress'] = [] + data['actress'].extend(data[key]) + del data[key] + if key.startswith('self '): + if not data.has_key('self'): + data['self'] = [] + data['self'].extend(data[key]) + del data[key] + if key == 'birth place': + data['birth notes'] = data[key] + del data[key] + if key == 'death place': + data['death notes'] = data[key] + del data[key] + return data + + +class DOMHTMLBioParser(DOMParserBase): + """Parser for the "biography" page of a given person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + bioparser = DOMHTMLBioParser() + result = bioparser.parse(biography_html_string) + """ + _defGetRefs = True + + _birth_attrs = [Attribute(key='birth date', + path={ + 'day': "./a[starts-with(@href, " \ + "'/date/')]/text()", + 'year': "./a[starts-with(@href, " \ + "'/search/name?birth_year=')]/text()" + }, + postprocess=build_date), + Attribute(key='birth notes', + path="./a[starts-with(@href, " \ + "'/search/name?birth_place=')]/text()")] + _death_attrs = [Attribute(key='death date', + path={ + 'day': "./a[starts-with(@href, " \ + "'/date/')]/text()", + 'year': "./a[starts-with(@href, " \ + "'/search/name?death_date=')]/text()" + }, + postprocess=build_date), + Attribute(key='death notes', + path="./text()", + # TODO: check if this slicing is always correct + postprocess=lambda x: u''.join(x).strip()[2:])] + extractors = [ + Extractor(label='headshot', + path="//a[@name='headshot']", + attrs=Attribute(key='headshot', + path="./img/@src")), + Extractor(label='birth info', + path="//div[h5='Date of Birth']", + attrs=_birth_attrs), + Extractor(label='death info', + path="//div[h5='Date of Death']", + attrs=_death_attrs), + Extractor(label='nick names', + path="//div[h5='Nickname']", + attrs=Attribute(key='nick names', + path="./text()", + joiner='|', + postprocess=lambda x: [n.strip().replace(' (', + '::(', 1) for n in x.split('|') + if n.strip()])), + Extractor(label='birth name', + path="//div[h5='Birth Name']", + attrs=Attribute(key='birth name', + path="./text()", + postprocess=lambda x: canonicalName(x.strip()))), + Extractor(label='height', + path="//div[h5='Height']", + attrs=Attribute(key='height', + path="./text()", + postprocess=lambda x: x.strip())), + Extractor(label='mini biography', + path="//div[h5='Mini Biography']", + attrs=Attribute(key='mini biography', + multi=True, + path={ + 'bio': "./p//text()", + 'by': "./b/following-sibling::a/text()" + }, + postprocess=lambda x: "%s::%s" % \ + (x.get('bio').strip(), + (x.get('by') or u'').strip() or u'Anonymous'))), + Extractor(label='spouse', + path="//div[h5='Spouse']/table/tr", + attrs=Attribute(key='spouse', + multi=True, + path={ + 'name': "./td[1]//text()", + 'info': "./td[2]//text()" + }, + postprocess=lambda x: ("%s::%s" % \ + (x.get('name').strip(), + (x.get('info') or u'').strip())).strip(':'))), + Extractor(label='trade mark', + path="//div[h5='Trade Mark']/p", + attrs=Attribute(key='trade mark', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip())), + Extractor(label='trivia', + path="//div[h5='Trivia']/p", + attrs=Attribute(key='trivia', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip())), + Extractor(label='quotes', + path="//div[h5='Personal Quotes']/p", + attrs=Attribute(key='quotes', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip())), + Extractor(label='salary', + path="//div[h5='Salary']/table/tr", + attrs=Attribute(key='salary history', + multi=True, + path={ + 'title': "./td[1]//text()", + 'info': "./td[2]/text()", + }, + postprocess=lambda x: "%s::%s" % \ + (x.get('title').strip(), + x.get('info').strip()))), + Extractor(label='where now', + path="//div[h5='Where Are They Now']/p", + attrs=Attribute(key='where now', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip())), + ] + + preprocessors = [ + (re.compile('(<h5>)', re.I), r'</div><div class="_imdbpy">\1'), + (re.compile('(</table>\n</div>\s+)</div>', re.I + re.DOTALL), r'\1'), + (re.compile('(<div id="tn15bot">)'), r'</div>\1'), + (re.compile('\.<br><br>([^\s])', re.I), r'. \1') + ] + + def postprocess_data(self, data): + for what in 'birth date', 'death date': + if what in data and not data[what]: + del data[what] + return data + + +class DOMHTMLOtherWorksParser(DOMParserBase): + """Parser for the "other works" and "agent" pages of a given person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + owparser = DOMHTMLOtherWorksParser() + result = owparser.parse(otherworks_html_string) + """ + _defGetRefs = True + kind = 'other works' + + # XXX: looks like the 'agent' page is no more public. + extractors = [ + Extractor(label='other works', + path="//h5[text()='Other works']/" \ + "following-sibling::div[1]", + attrs=Attribute(key='self.kind', + path=".//text()", + postprocess=lambda x: x.strip().split('\n\n'))) + ] + + preprocessors = [ + (re.compile('(<h5>[^<]+</h5>)', re.I), + r'</div>\1<div class="_imdbpy">'), + (re.compile('(</table>\n</div>\s+)</div>', re.I), r'\1'), + (re.compile('(<div id="tn15bot">)'), r'</div>\1'), + (re.compile('<br/><br/>', re.I), r'\n\n') + ] + + +def _build_episode(link, title, minfo, role, roleA, roleAID): + """Build an Movie object for a given episode of a series.""" + episode_id = analyze_imdbid(link) + notes = u'' + minidx = minfo.find(' -') + # Sometimes, for some unknown reason, the role is left in minfo. + if minidx != -1: + slfRole = minfo[minidx+3:].lstrip() + minfo = minfo[:minidx].rstrip() + if slfRole.endswith(')'): + commidx = slfRole.rfind('(') + if commidx != -1: + notes = slfRole[commidx:] + slfRole = slfRole[:commidx] + if slfRole and role is None and roleA is None: + role = slfRole + eps_data = analyze_title(title) + eps_data['kind'] = u'episode' + # FIXME: it's wrong for multiple characters (very rare on tv series?). + if role is None: + role = roleA # At worse, it's None. + if role is None: + roleAID = None + if roleAID is not None: + roleAID = analyze_imdbid(roleAID) + e = Movie(movieID=episode_id, data=eps_data, currentRole=role, + roleID=roleAID, notes=notes) + # XXX: are we missing some notes? + # XXX: does it parse things as "Episode dated 12 May 2005 (12 May 2005)"? + if minfo.startswith('('): + pe = minfo.find(')') + if pe != -1: + date = minfo[1:pe] + if date != '????': + e['original air date'] = date + if eps_data.get('year', '????') == '????': + syear = date.split()[-1] + if syear.isdigit(): + e['year'] = int(syear) + return e + + +class DOMHTMLSeriesParser(DOMParserBase): + """Parser for the "by TV series" page of a given person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + sparser = DOMHTMLSeriesParser() + result = sparser.parse(filmoseries_html_string) + """ + _containsObjects = True + + extractors = [ + Extractor(label='series', + group="//div[@class='filmo']/span[1]", + group_key="./a[1]", + path="./following-sibling::ol[1]/li/a[1]", + attrs=Attribute(key=None, + multi=True, + path={ + 'link': "./@href", + 'title': "./text()", + 'info': "./following-sibling::text()", + 'role': "./following-sibling::i[1]/text()", + 'roleA': "./following-sibling::a[1]/text()", + 'roleAID': "./following-sibling::a[1]/@href" + }, + postprocess=lambda x: _build_episode(x.get('link'), + x.get('title'), + (x.get('info') or u'').strip(), + x.get('role'), + x.get('roleA'), + x.get('roleAID')))) + ] + + def postprocess_data(self, data): + if len(data) == 0: + return {} + nd = {} + for key in data.keys(): + dom = self.get_dom(key) + link = self.xpath(dom, "//a/@href")[0] + title = self.xpath(dom, "//a/text()")[0][1:-1] + series = Movie(movieID=analyze_imdbid(link), + data=analyze_title(title), + accessSystem=self._as, modFunct=self._modFunct) + nd[series] = [] + for episode in data[key]: + # XXX: should we create a copy of 'series', to avoid + # circular references? + episode['episode of'] = series + nd[series].append(episode) + return {'episodes': nd} + + +class DOMHTMLPersonGenresParser(DOMParserBase): + """Parser for the "by genre" and "by keywords" pages of a given person. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + gparser = DOMHTMLPersonGenresParser() + result = gparser.parse(bygenre_html_string) + """ + kind = 'genres' + _containsObjects = True + + extractors = [ + Extractor(label='genres', + group="//b/a[@name]/following-sibling::a[1]", + group_key="./text()", + group_key_normalize=lambda x: x.lower(), + path="../../following-sibling::ol[1]/li//a[1]", + attrs=Attribute(key=None, + multi=True, + path={ + 'link': "./@href", + 'title': "./text()", + 'info': "./following-sibling::text()" + }, + postprocess=lambda x: \ + build_movie(x.get('title') + \ + x.get('info').split('[')[0], + analyze_imdbid(x.get('link'))))) + ] + + def postprocess_data(self, data): + if len(data) == 0: + return {} + return {self.kind: data} + + +from movieParser import DOMHTMLTechParser +from movieParser import DOMHTMLOfficialsitesParser +from movieParser import DOMHTMLAwardsParser +from movieParser import DOMHTMLNewsParser + + +_OBJECTS = { + 'maindetails_parser': ((DOMHTMLMaindetailsParser,), None), + 'bio_parser': ((DOMHTMLBioParser,), None), + 'otherworks_parser': ((DOMHTMLOtherWorksParser,), None), + #'agent_parser': ((DOMHTMLOtherWorksParser,), {'kind': 'agent'}), + 'person_officialsites_parser': ((DOMHTMLOfficialsitesParser,), None), + 'person_awards_parser': ((DOMHTMLAwardsParser,), {'subject': 'name'}), + 'publicity_parser': ((DOMHTMLTechParser,), {'kind': 'publicity'}), + 'person_series_parser': ((DOMHTMLSeriesParser,), None), + 'person_contacts_parser': ((DOMHTMLTechParser,), {'kind': 'contacts'}), + 'person_genres_parser': ((DOMHTMLPersonGenresParser,), None), + 'person_keywords_parser': ((DOMHTMLPersonGenresParser,), + {'kind': 'keywords'}), + 'news_parser': ((DOMHTMLNewsParser,), None), +} + diff --git a/lib/imdb/parser/http/searchCharacterParser.py b/lib/imdb/parser/http/searchCharacterParser.py new file mode 100644 index 0000000000000000000000000000000000000000..c81ca7e4a8fa274bcf2a75010bf0ff84f1bdf40a --- /dev/null +++ b/lib/imdb/parser/http/searchCharacterParser.py @@ -0,0 +1,69 @@ +""" +parser.http.searchCharacterParser module (imdb package). + +This module provides the HTMLSearchCharacterParser class (and the +search_character_parser instance), used to parse the results of a search +for a given character. +E.g., when searching for the name "Jesse James", the parsed page would be: + http://akas.imdb.com/find?s=Characters;mx=20;q=Jesse+James + +Copyright 2007-2009 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from imdb.utils import analyze_name, build_name +from utils import Extractor, Attribute, analyze_imdbid + +from searchMovieParser import DOMHTMLSearchMovieParser, DOMBasicMovieParser + + +class DOMBasicCharacterParser(DOMBasicMovieParser): + """Simply get the name of a character and the imdbID. + + It's used by the DOMHTMLSearchCharacterParser class to return a result + for a direct match (when a search on IMDb results in a single + character, the web server sends directly the movie page.""" + _titleFunct = lambda self, x: analyze_name(x or u'', canonical=False) + + +class DOMHTMLSearchCharacterParser(DOMHTMLSearchMovieParser): + _BaseParser = DOMBasicCharacterParser + _notDirectHitTitle = '<title>imdb search' + _titleBuilder = lambda self, x: build_name(x, canonical=False) + _linkPrefix = '/character/ch' + + _attrs = [Attribute(key='data', + multi=True, + path={ + 'link': "./a[1]/@href", + 'name': "./a[1]/text()" + }, + postprocess=lambda x: ( + analyze_imdbid(x.get('link') or u''), + {'name': x.get('name')} + ))] + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, " \ + "'/character/ch')]/..", + attrs=_attrs)] + + +_OBJECTS = { + 'search_character_parser': ((DOMHTMLSearchCharacterParser,), + {'kind': 'character', '_basic_parser': DOMBasicCharacterParser}) +} + diff --git a/lib/imdb/parser/http/searchCompanyParser.py b/lib/imdb/parser/http/searchCompanyParser.py new file mode 100644 index 0000000000000000000000000000000000000000..ab666fbc30106e29fa5f29721e0a0220e2d4cd53 --- /dev/null +++ b/lib/imdb/parser/http/searchCompanyParser.py @@ -0,0 +1,71 @@ +""" +parser.http.searchCompanyParser module (imdb package). + +This module provides the HTMLSearchCompanyParser class (and the +search_company_parser instance), used to parse the results of a search +for a given company. +E.g., when searching for the name "Columbia Pictures", the parsed page would be: + http://akas.imdb.com/find?s=co;mx=20;q=Columbia+Pictures + +Copyright 2008-2009 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from imdb.utils import analyze_company_name, build_company_name +from utils import Extractor, Attribute, analyze_imdbid + +from searchMovieParser import DOMHTMLSearchMovieParser, DOMBasicMovieParser + +class DOMBasicCompanyParser(DOMBasicMovieParser): + """Simply get the name of a company and the imdbID. + + It's used by the DOMHTMLSearchCompanyParser class to return a result + for a direct match (when a search on IMDb results in a single + company, the web server sends directly the company page. + """ + _titleFunct = lambda self, x: analyze_company_name(x or u'') + + +class DOMHTMLSearchCompanyParser(DOMHTMLSearchMovieParser): + _BaseParser = DOMBasicCompanyParser + _notDirectHitTitle = '<title>imdb company' + _titleBuilder = lambda self, x: build_company_name(x) + _linkPrefix = '/company/co' + + _attrs = [Attribute(key='data', + multi=True, + path={ + 'link': "./a[1]/@href", + 'name': "./a[1]/text()", + 'notes': "./text()[1]" + }, + postprocess=lambda x: ( + analyze_imdbid(x.get('link')), + analyze_company_name(x.get('name')+(x.get('notes') + or u''), stripNotes=True) + ))] + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, " \ + "'/company/co')]/..", + attrs=_attrs)] + + +_OBJECTS = { + 'search_company_parser': ((DOMHTMLSearchCompanyParser,), + {'kind': 'company', '_basic_parser': DOMBasicCompanyParser}) +} + diff --git a/lib/imdb/parser/http/searchKeywordParser.py b/lib/imdb/parser/http/searchKeywordParser.py new file mode 100644 index 0000000000000000000000000000000000000000..ed72906cef5bc4f46a358fe825e2c283ba6e72cc --- /dev/null +++ b/lib/imdb/parser/http/searchKeywordParser.py @@ -0,0 +1,111 @@ +""" +parser.http.searchKeywordParser module (imdb package). + +This module provides the HTMLSearchKeywordParser class (and the +search_company_parser instance), used to parse the results of a search +for a given keyword. +E.g., when searching for the keyword "alabama", the parsed page would be: + http://akas.imdb.com/find?s=kw;mx=20;q=alabama + +Copyright 2009 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from utils import Extractor, Attribute, analyze_imdbid +from imdb.utils import analyze_title, analyze_company_name + +from searchMovieParser import DOMHTMLSearchMovieParser, DOMBasicMovieParser + +class DOMBasicKeywordParser(DOMBasicMovieParser): + """Simply get the name of a keyword. + + It's used by the DOMHTMLSearchKeywordParser class to return a result + for a direct match (when a search on IMDb results in a single + keyword, the web server sends directly the keyword page. + """ + # XXX: it's still to be tested! + # I'm not even sure there can be a direct hit, searching for keywords. + _titleFunct = lambda self, x: analyze_company_name(x or u'') + + +class DOMHTMLSearchKeywordParser(DOMHTMLSearchMovieParser): + """Parse the html page that the IMDb web server shows when the + "new search system" is used, searching for keywords similar to + the one given.""" + + _BaseParser = DOMBasicKeywordParser + _notDirectHitTitle = '<title>imdb keyword' + _titleBuilder = lambda self, x: x + _linkPrefix = '/keyword/' + + _attrs = [Attribute(key='data', + multi=True, + path="./a[1]/text()" + )] + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, " \ + "'/keyword/')]/..", + attrs=_attrs)] + + +def custom_analyze_title4kwd(title, yearNote, outline): + """Return a dictionary with the needed info.""" + title = title.strip() + if not title: + return {} + if yearNote: + yearNote = '%s)' % yearNote.split(' ')[0] + title = title + ' ' + yearNote + retDict = analyze_title(title) + if outline: + retDict['plot outline'] = outline + return retDict + + +class DOMHTMLSearchMovieKeywordParser(DOMHTMLSearchMovieParser): + """Parse the html page that the IMDb web server shows when the + "new search system" is used, searching for movies with the given + keyword.""" + + _notDirectHitTitle = '<title>best' + + _attrs = [Attribute(key='data', + multi=True, + path={ + 'link': "./a[1]/@href", + 'info': "./a[1]//text()", + 'ynote': "./span[@class='desc']/text()", + 'outline': "./span[@class='outline']//text()" + }, + postprocess=lambda x: ( + analyze_imdbid(x.get('link') or u''), + custom_analyze_title4kwd(x.get('info') or u'', + x.get('ynote') or u'', + x.get('outline') or u'') + ))] + + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, " \ + "'/title/tt')]/..", + attrs=_attrs)] + + +_OBJECTS = { + 'search_keyword_parser': ((DOMHTMLSearchKeywordParser,), + {'kind': 'keyword', '_basic_parser': DOMBasicKeywordParser}), + 'search_moviekeyword_parser': ((DOMHTMLSearchMovieKeywordParser,), None) +} + diff --git a/lib/imdb/parser/http/searchMovieParser.py b/lib/imdb/parser/http/searchMovieParser.py new file mode 100644 index 0000000000000000000000000000000000000000..44c78d0cc5049b7cd23ebb2ac6c192d3635f8dc4 --- /dev/null +++ b/lib/imdb/parser/http/searchMovieParser.py @@ -0,0 +1,182 @@ +""" +parser.http.searchMovieParser module (imdb package). + +This module provides the HTMLSearchMovieParser class (and the +search_movie_parser instance), used to parse the results of a search +for a given title. +E.g., for when searching for the title "the passion", the parsed +page would be: + http://akas.imdb.com/find?q=the+passion&tt=on&mx=20 + +Copyright 2004-2010 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +from imdb.utils import analyze_title, build_title +from utils import DOMParserBase, Attribute, Extractor, analyze_imdbid + + +class DOMBasicMovieParser(DOMParserBase): + """Simply get the title of a movie and the imdbID. + + It's used by the DOMHTMLSearchMovieParser class to return a result + for a direct match (when a search on IMDb results in a single + movie, the web server sends directly the movie page.""" + # Stay generic enough to be used also for other DOMBasic*Parser classes. + _titleAttrPath = ".//text()" + _linkPath = "//link[@rel='canonical']" + _titleFunct = lambda self, x: analyze_title(x or u'') + + def _init(self): + self.preprocessors += [('<span class="tv-extra">TV mini-series</span>', + '<span class="tv-extra">(mini)</span>')] + self.extractors = [Extractor(label='title', + path="//h1", + attrs=Attribute(key='title', + path=self._titleAttrPath, + postprocess=self._titleFunct)), + Extractor(label='link', + path=self._linkPath, + attrs=Attribute(key='link', path="./@href", + postprocess=lambda x: \ + analyze_imdbid((x or u'').replace( + 'http://pro.imdb.com', '')) + ))] + + # Remove 'More at IMDb Pro' links. + preprocessors = [(re.compile(r'<span class="pro-link".*?</span>'), ''), + (re.compile(r'<a href="http://ad.doubleclick.net.*?;id=(co[0-9]{7});'), r'<a href="http://pro.imdb.com/company/\1"></a>< a href="')] + + def postprocess_data(self, data): + if not 'link' in data: + data = [] + else: + link = data.pop('link') + if (link and data): + data = [(link, data)] + else: + data = [] + return data + + +def custom_analyze_title(title): + """Remove garbage notes after the (year), (year/imdbIndex) or (year) (TV)""" + # XXX: very crappy. :-( + nt = title.split(' ')[0] + if nt: + title = nt + if not title: + return {} + return analyze_title(title) + +# Manage AKAs. +_reAKAStitles = re.compile(r'(?:aka) <em>"(.*?)(<br>|<\/td>)', re.I | re.M) + +class DOMHTMLSearchMovieParser(DOMParserBase): + """Parse the html page that the IMDb web server shows when the + "new search system" is used, for movies.""" + + _BaseParser = DOMBasicMovieParser + _notDirectHitTitle = '<title>imdb title' + _titleBuilder = lambda self, x: build_title(x) + _linkPrefix = '/title/tt' + + _attrs = [Attribute(key='data', + multi=True, + path={ + 'link': "./a[1]/@href", + 'info': ".//text()", + #'akas': ".//div[@class='_imdbpyAKA']//text()" + 'akas': ".//p[@class='find-aka']//text()" + }, + postprocess=lambda x: ( + analyze_imdbid(x.get('link') or u''), + custom_analyze_title(x.get('info') or u''), + x.get('akas') + ))] + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, '/title/tt')]/..", + attrs=_attrs)] + def _init(self): + self.url = u'' + + def _reset(self): + self.url = u'' + + def preprocess_string(self, html_string): + if self._notDirectHitTitle in html_string[:1024].lower(): + if self._linkPrefix == '/title/tt': + # Only for movies. + html_string = html_string.replace('(TV mini-series)', '(mini)') + html_string = html_string.replace('<p class="find-aka">', + '<p class="find-aka">::') + #html_string = _reAKAStitles.sub( + # r'<div class="_imdbpyAKA">\1::</div>\2', html_string) + return html_string + # Direct hit! + dbme = self._BaseParser(useModule=self._useModule) + res = dbme.parse(html_string, url=self.url) + if not res: return u'' + res = res['data'] + if not (res and res[0]): return u'' + link = '%s%s' % (self._linkPrefix, res[0][0]) + # # Tries to cope with companies for which links to pro.imdb.com + # # are missing. + # link = self.url.replace(imdbURL_base[:-1], '') + title = self._titleBuilder(res[0][1]) + if not (link and title): return u'' + link = link.replace('http://pro.imdb.com', '') + new_html = '<td></td><td></td><td><a href="%s">%s</a></td>' % (link, + title) + return new_html + + def postprocess_data(self, data): + if not data.has_key('data'): + data['data'] = [] + results = getattr(self, 'results', None) + if results is not None: + data['data'][:] = data['data'][:results] + # Horrible hack to support AKAs. + if data and data['data'] and len(data['data'][0]) == 3 and \ + isinstance(data['data'][0], tuple): + data['data'] = [x for x in data['data'] if x[0] and x[1]] + for idx, datum in enumerate(data['data']): + if not isinstance(datum, tuple): + continue + if not datum[0] and datum[1]: + continue + if datum[2] is not None: + akas = filter(None, datum[2].split('::')) + if self._linkPrefix == '/title/tt': + akas = [a.replace('" - ', '::').rstrip() for a in akas] + akas = [a.replace('aka "', '', 1).replace('aka "', + '', 1).lstrip() for a in akas] + datum[1]['akas'] = akas + data['data'][idx] = (datum[0], datum[1]) + else: + data['data'][idx] = (datum[0], datum[1]) + return data + + def add_refs(self, data): + return data + + +_OBJECTS = { + 'search_movie_parser': ((DOMHTMLSearchMovieParser,), None) +} + diff --git a/lib/imdb/parser/http/searchPersonParser.py b/lib/imdb/parser/http/searchPersonParser.py new file mode 100644 index 0000000000000000000000000000000000000000..1756efc5eaf6c14ca2e8dcc47c783ffc171eb4a0 --- /dev/null +++ b/lib/imdb/parser/http/searchPersonParser.py @@ -0,0 +1,92 @@ +""" +parser.http.searchPersonParser module (imdb package). + +This module provides the HTMLSearchPersonParser class (and the +search_person_parser instance), used to parse the results of a search +for a given person. +E.g., when searching for the name "Mel Gibson", the parsed page would be: + http://akas.imdb.com/find?q=Mel+Gibson&nm=on&mx=20 + +Copyright 2004-2010 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +from imdb.utils import analyze_name, build_name +from utils import Extractor, Attribute, analyze_imdbid + +from searchMovieParser import DOMHTMLSearchMovieParser, DOMBasicMovieParser + + +def _cleanName(n): + """Clean the name in a title tag.""" + if not n: + return u'' + n = n.replace('Filmography by type for', '') # FIXME: temporary. + return n + +class DOMBasicPersonParser(DOMBasicMovieParser): + """Simply get the name of a person and the imdbID. + + It's used by the DOMHTMLSearchPersonParser class to return a result + for a direct match (when a search on IMDb results in a single + person, the web server sends directly the movie page.""" + _titleFunct = lambda self, x: analyze_name(_cleanName(x), canonical=1) + + +_reAKASp = re.compile(r'(?:aka|birth name) (<em>")(.*?)"(<br>|<\/em>|<\/td>)', + re.I | re.M) + +class DOMHTMLSearchPersonParser(DOMHTMLSearchMovieParser): + """Parse the html page that the IMDb web server shows when the + "new search system" is used, for persons.""" + _BaseParser = DOMBasicPersonParser + _notDirectHitTitle = '<title>imdb name' + _titleBuilder = lambda self, x: build_name(x, canonical=True) + _linkPrefix = '/name/nm' + + _attrs = [Attribute(key='data', + multi=True, + path={ + 'link': "./a[1]/@href", + 'name': "./a[1]/text()", + 'index': "./text()[1]", + 'akas': ".//div[@class='_imdbpyAKA']/text()" + }, + postprocess=lambda x: ( + analyze_imdbid(x.get('link') or u''), + analyze_name((x.get('name') or u'') + \ + (x.get('index') or u''), + canonical=1), x.get('akas') + ))] + extractors = [Extractor(label='search', + path="//td[3]/a[starts-with(@href, '/name/nm')]/..", + attrs=_attrs)] + + def preprocess_string(self, html_string): + if self._notDirectHitTitle in html_string[:1024].lower(): + html_string = _reAKASp.sub( + r'\1<div class="_imdbpyAKA">\2::</div>\3', + html_string) + return DOMHTMLSearchMovieParser.preprocess_string(self, html_string) + + +_OBJECTS = { + 'search_person_parser': ((DOMHTMLSearchPersonParser,), + {'kind': 'person', '_basic_parser': DOMBasicPersonParser}) +} + diff --git a/lib/imdb/parser/http/topBottomParser.py b/lib/imdb/parser/http/topBottomParser.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f29509fd1d50d493d3f60e54f68423947fd1aa --- /dev/null +++ b/lib/imdb/parser/http/topBottomParser.py @@ -0,0 +1,106 @@ +""" +parser.http.topBottomParser module (imdb package). + +This module provides the classes (and the instances), used to parse the +lists of top 250 and bottom 100 movies. +E.g.: + http://akas.imdb.com/chart/top + http://akas.imdb.com/chart/bottom + +Copyright 2009 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from imdb.utils import analyze_title +from utils import DOMParserBase, Attribute, Extractor, analyze_imdbid + + +class DOMHTMLTop250Parser(DOMParserBase): + """Parser for the "top 250" page. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + tparser = DOMHTMLTop250Parser() + result = tparser.parse(top250_html_string) + """ + label = 'top 250' + ranktext = 'top 250 rank' + + def _init(self): + self.extractors = [Extractor(label=self.label, + path="//div[@id='main']//table//tr", + attrs=Attribute(key=None, + multi=True, + path={self.ranktext: "./td[1]//text()", + 'rating': "./td[2]//text()", + 'title': "./td[3]//text()", + 'movieID': "./td[3]//a/@href", + 'votes': "./td[4]//text()" + }))] + + def postprocess_data(self, data): + if not data or self.label not in data: + return [] + mlist = [] + data = data[self.label] + # Avoid duplicates. A real fix, using XPath, is auspicabile. + # XXX: probably this is no more needed. + seenIDs = [] + for d in data: + if 'movieID' not in d: continue + if self.ranktext not in d: continue + if 'title' not in d: continue + theID = analyze_imdbid(d['movieID']) + if theID is None: + continue + theID = str(theID) + if theID in seenIDs: + continue + seenIDs.append(theID) + minfo = analyze_title(d['title']) + try: minfo[self.ranktext] = int(d[self.ranktext].replace('.', '')) + except: pass + if 'votes' in d: + try: minfo['votes'] = int(d['votes'].replace(',', '')) + except: pass + if 'rating' in d: + try: minfo['rating'] = float(d['rating']) + except: pass + mlist.append((theID, minfo)) + return mlist + + +class DOMHTMLBottom100Parser(DOMHTMLTop250Parser): + """Parser for the "bottom 100" page. + The page should be provided as a string, as taken from + the akas.imdb.com server. The final result will be a + dictionary, with a key for every relevant section. + + Example: + tparser = DOMHTMLBottom100Parser() + result = tparser.parse(bottom100_html_string) + """ + label = 'bottom 100' + ranktext = 'bottom 100 rank' + + +_OBJECTS = { + 'top250_parser': ((DOMHTMLTop250Parser,), None), + 'bottom100_parser': ((DOMHTMLBottom100Parser,), None) +} + diff --git a/lib/imdb/parser/http/utils.py b/lib/imdb/parser/http/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f8dbc050b0e41ed3f99b63831121994768a32fef --- /dev/null +++ b/lib/imdb/parser/http/utils.py @@ -0,0 +1,876 @@ +""" +parser.http.utils module (imdb package). + +This module provides miscellaneous utilities used by +the imdb.parser.http classes. + +Copyright 2004-2012 Davide Alberani <da@erlug.linux.it> + 2008 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +import logging +import warnings + +from imdb._exceptions import IMDbError + +from imdb.utils import flatten, _Container +from imdb.Movie import Movie +from imdb.Person import Person +from imdb.Character import Character + + +# Year, imdbIndex and kind. +re_yearKind_index = re.compile(r'(\([0-9\?]{4}(?:/[IVXLCDM]+)?\)(?: \(mini\)| \(TV\)| \(V\)| \(VG\))?)') + +# Match imdb ids in href tags +re_imdbid = re.compile(r'(title/tt|name/nm|character/ch|company/co)([0-9]+)') + +def analyze_imdbid(href): + """Return an imdbID from an URL.""" + if not href: + return None + match = re_imdbid.search(href) + if not match: + return None + return str(match.group(2)) + + +_modify_keys = list(Movie.keys_tomodify_list) + list(Person.keys_tomodify_list) +def _putRefs(d, re_titles, re_names, re_characters, lastKey=None): + """Iterate over the strings inside list items or dictionary values, + substitutes movie titles and person names with the (qv) references.""" + if isinstance(d, list): + for i in xrange(len(d)): + if isinstance(d[i], (unicode, str)): + if lastKey in _modify_keys: + if re_names: + d[i] = re_names.sub(ur"'\1' (qv)", d[i]) + if re_titles: + d[i] = re_titles.sub(ur'_\1_ (qv)', d[i]) + if re_characters: + d[i] = re_characters.sub(ur'#\1# (qv)', d[i]) + elif isinstance(d[i], (list, dict)): + _putRefs(d[i], re_titles, re_names, re_characters, + lastKey=lastKey) + elif isinstance(d, dict): + for k, v in d.items(): + lastKey = k + if isinstance(v, (unicode, str)): + if lastKey in _modify_keys: + if re_names: + d[k] = re_names.sub(ur"'\1' (qv)", v) + if re_titles: + d[k] = re_titles.sub(ur'_\1_ (qv)', v) + if re_characters: + d[k] = re_characters.sub(ur'#\1# (qv)', v) + elif isinstance(v, (list, dict)): + _putRefs(d[k], re_titles, re_names, re_characters, + lastKey=lastKey) + + +# Handle HTML/XML/SGML entities. +from htmlentitydefs import entitydefs +entitydefs = entitydefs.copy() +entitydefsget = entitydefs.get +entitydefs['nbsp'] = ' ' + +sgmlentity = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\'', 'ndash': '-'} +sgmlentityget = sgmlentity.get +_sgmlentkeys = sgmlentity.keys() + +entcharrefs = {} +entcharrefsget = entcharrefs.get +for _k, _v in entitydefs.items(): + if _k in _sgmlentkeys: continue + if _v[0:2] == '&#': + dec_code = _v[1:-1] + _v = unichr(int(_v[2:-1])) + entcharrefs[dec_code] = _v + else: + dec_code = '#' + str(ord(_v)) + _v = unicode(_v, 'latin_1', 'replace') + entcharrefs[dec_code] = _v + entcharrefs[_k] = _v +del _sgmlentkeys, _k, _v +entcharrefs['#160'] = u' ' +entcharrefs['#xA0'] = u' ' +entcharrefs['#xa0'] = u' ' +entcharrefs['#XA0'] = u' ' +entcharrefs['#x22'] = u'"' +entcharrefs['#X22'] = u'"' +# convert &x26; to &, to make BeautifulSoup happy; beware that this +# leaves lone '&' in the html broken, but I assume this is better than +# the contrary... +entcharrefs['#38'] = u'&' +entcharrefs['#x26'] = u'&' +entcharrefs['#x26'] = u'&' + +re_entcharrefs = re.compile('&(%s|\#160|\#\d{1,5}|\#x[0-9a-f]{1,4});' % + '|'.join(map(re.escape, entcharrefs)), re.I) +re_entcharrefssub = re_entcharrefs.sub + +sgmlentity.update(dict([('#34', u'"'), ('#38', u'&'), + ('#60', u'<'), ('#62', u'>'), ('#39', u"'")])) +re_sgmlref = re.compile('&(%s);' % '|'.join(map(re.escape, sgmlentity))) +re_sgmlrefsub = re_sgmlref.sub + +# Matches XML-only single tags, like <br/> ; they are invalid in HTML, +# but widely used by IMDb web site. :-/ +re_xmltags = re.compile('<([a-zA-Z]+)/>') + + +def _replXMLRef(match): + """Replace the matched XML/HTML entities and references; + replace everything except sgml entities like <, >, ...""" + ref = match.group(1) + value = entcharrefsget(ref) + if value is None: + if ref[0] == '#': + ref_code = ref[1:] + if ref_code in ('34', '38', '60', '62', '39'): + return match.group(0) + elif ref_code[0].lower() == 'x': + #if ref[2:] == '26': + # # Don't convert &x26; to &, to make BeautifulSoup happy. + # return '&' + return unichr(int(ref[2:], 16)) + else: + return unichr(int(ref[1:])) + else: + return ref + return value + +def subXMLRefs(s): + """Return the given html string with entity and char references + replaced.""" + return re_entcharrefssub(_replXMLRef, s) + +# XXX: no more used here; move it to mobile (they are imported by helpers, too)? +def _replSGMLRefs(match): + """Replace the matched SGML entity.""" + ref = match.group(1) + return sgmlentityget(ref, ref) + +def subSGMLRefs(s): + """Return the given html string with sgml entity and char references + replaced.""" + return re_sgmlrefsub(_replSGMLRefs, s) + + +_b_p_logger = logging.getLogger('imdbpy.parser.http.build_person') +def build_person(txt, personID=None, billingPos=None, + roleID=None, accessSystem='http', modFunct=None): + """Return a Person instance from the tipical <tr>...</tr> strings + found in the IMDb's web site.""" + #if personID is None + # _b_p_logger.debug('empty name or personID for "%s"', txt) + notes = u'' + role = u'' + # Search the (optional) separator between name and role/notes. + if txt.find('....') != -1: + sep = '....' + elif txt.find('...') != -1: + sep = '...' + else: + sep = '...' + # Replace the first parenthesis, assuming there are only + # notes, after. + # Rationale: no imdbIndex is (ever?) showed on the web site. + txt = txt.replace('(', '...(', 1) + txt_split = txt.split(sep, 1) + name = txt_split[0].strip() + if len(txt_split) == 2: + role_comment = txt_split[1].strip() + # Strip common endings. + if role_comment[-4:] == ' and': + role_comment = role_comment[:-4].rstrip() + elif role_comment[-2:] == ' &': + role_comment = role_comment[:-2].rstrip() + elif role_comment[-6:] == '& ....': + role_comment = role_comment[:-6].rstrip() + # Get the notes. + if roleID is not None: + if not isinstance(roleID, list): + cmt_idx = role_comment.find('(') + if cmt_idx != -1: + role = role_comment[:cmt_idx].rstrip() + notes = role_comment[cmt_idx:] + else: + # Just a role, without notes. + role = role_comment + else: + role = role_comment + else: + # We're managing something that doesn't have a 'role', so + # everything are notes. + notes = role_comment + if role == '....': role = u'' + roleNotes = [] + # Manages multiple roleIDs. + if isinstance(roleID, list): + rolesplit = role.split('/') + role = [] + for r in rolesplit: + nidx = r.find('(') + if nidx != -1: + role.append(r[:nidx].rstrip()) + roleNotes.append(r[nidx:]) + else: + role.append(r) + roleNotes.append(None) + lr = len(role) + lrid = len(roleID) + if lr > lrid: + roleID += [None] * (lrid - lr) + elif lr < lrid: + roleID = roleID[:lr] + for i, rid in enumerate(roleID): + if rid is not None: + roleID[i] = str(rid) + if lr == 1: + role = role[0] + roleID = roleID[0] + notes = roleNotes[0] or u'' + elif roleID is not None: + roleID = str(roleID) + if personID is not None: + personID = str(personID) + if (not name) or (personID is None): + # Set to 'debug', since build_person is expected to receive some crap. + _b_p_logger.debug('empty name or personID for "%s"', txt) + # XXX: return None if something strange is detected? + person = Person(name=name, personID=personID, currentRole=role, + roleID=roleID, notes=notes, billingPos=billingPos, + modFunct=modFunct, accessSystem=accessSystem) + if roleNotes and len(roleNotes) == len(roleID): + for idx, role in enumerate(person.currentRole): + if roleNotes[idx]: + role.notes = roleNotes[idx] + return person + + +_re_chrIDs = re.compile('[0-9]{7}') + +_b_m_logger = logging.getLogger('imdbpy.parser.http.build_movie') +# To shrink spaces. +re_spaces = re.compile(r'\s+') +def build_movie(txt, movieID=None, roleID=None, status=None, + accessSystem='http', modFunct=None, _parsingCharacter=False, + _parsingCompany=False, year=None, chrRoles=None, + rolesNoChar=None, additionalNotes=None): + """Given a string as normally seen on the "categorized" page of + a person on the IMDb's web site, returns a Movie instance.""" + # FIXME: Oook, lets face it: build_movie and build_person are now + # two horrible sets of patches to support the new IMDb design. They + # must be rewritten from scratch. + if _parsingCharacter: + _defSep = ' Played by ' + elif _parsingCompany: + _defSep = ' ... ' + else: + _defSep = ' .... ' + title = re_spaces.sub(' ', txt).strip() + # Split the role/notes from the movie title. + tsplit = title.split(_defSep, 1) + role = u'' + notes = u'' + roleNotes = [] + if len(tsplit) == 2: + title = tsplit[0].rstrip() + role = tsplit[1].lstrip() + if title[-9:] == 'TV Series': + title = title[:-9].rstrip() + #elif title[-7:] == '(short)': + # title = title[:-7].rstrip() + #elif title[-11:] == '(TV series)': + # title = title[:-11].rstrip() + #elif title[-10:] == '(TV movie)': + # title = title[:-10].rstrip() + elif title[-14:] == 'TV mini-series': + title = title[:-14] + ' (mini)' + if title and title.endswith(_defSep.rstrip()): + title = title[:-len(_defSep)+1] + # Try to understand where the movie title ends. + while True: + if year: + break + if title[-1:] != ')': + # Ignore the silly "TV Series" notice. + if title[-9:] == 'TV Series': + title = title[:-9].rstrip() + continue + else: + # Just a title: stop here. + break + # Try to match paired parentheses; yes: sometimes there are + # parentheses inside comments... + nidx = title.rfind('(') + while (nidx != -1 and \ + title[nidx:].count('(') != title[nidx:].count(')')): + nidx = title[:nidx].rfind('(') + # Unbalanced parentheses: stop here. + if nidx == -1: break + # The last item in parentheses seems to be a year: stop here. + first4 = title[nidx+1:nidx+5] + if (first4.isdigit() or first4 == '????') and \ + title[nidx+5:nidx+6] in (')', '/'): break + # The last item in parentheses is a known kind: stop here. + if title[nidx+1:-1] in ('TV', 'V', 'mini', 'VG', 'TV movie', + 'TV series', 'short'): break + # Else, in parentheses there are some notes. + # XXX: should the notes in the role half be kept separated + # from the notes in the movie title half? + if notes: notes = '%s %s' % (title[nidx:], notes) + else: notes = title[nidx:] + title = title[:nidx].rstrip() + if year: + year = year.strip() + if title[-1] == ')': + fpIdx = title.rfind('(') + if fpIdx != -1: + if notes: notes = '%s %s' % (title[fpIdx:], notes) + else: notes = title[fpIdx:] + title = title[:fpIdx].rstrip() + title = u'%s (%s)' % (title, year) + if _parsingCharacter and roleID and not role: + roleID = None + if not roleID: + roleID = None + elif len(roleID) == 1: + roleID = roleID[0] + if not role and chrRoles and isinstance(roleID, (str, unicode)): + roleID = _re_chrIDs.findall(roleID) + role = ' / '.join(filter(None, chrRoles.split('@@'))) + # Manages multiple roleIDs. + if isinstance(roleID, list): + tmprole = role.split('/') + role = [] + for r in tmprole: + nidx = r.find('(') + if nidx != -1: + role.append(r[:nidx].rstrip()) + roleNotes.append(r[nidx:]) + else: + role.append(r) + roleNotes.append(None) + lr = len(role) + lrid = len(roleID) + if lr > lrid: + roleID += [None] * (lrid - lr) + elif lr < lrid: + roleID = roleID[:lr] + for i, rid in enumerate(roleID): + if rid is not None: + roleID[i] = str(rid) + if lr == 1: + role = role[0] + roleID = roleID[0] + elif roleID is not None: + roleID = str(roleID) + if movieID is not None: + movieID = str(movieID) + if (not title) or (movieID is None): + _b_m_logger.error('empty title or movieID for "%s"', txt) + if rolesNoChar: + rolesNoChar = filter(None, [x.strip() for x in rolesNoChar.split('/')]) + if not role: + role = [] + elif not isinstance(role, list): + role = [role] + role += rolesNoChar + notes = notes.strip() + if additionalNotes: + additionalNotes = re_spaces.sub(' ', additionalNotes).strip() + if notes: + notes += u' ' + notes += additionalNotes + if role and isinstance(role, list) and notes.endswith(role[-1].replace('\n', ' ')): + role = role[:-1] + m = Movie(title=title, movieID=movieID, notes=notes, currentRole=role, + roleID=roleID, roleIsPerson=_parsingCharacter, + modFunct=modFunct, accessSystem=accessSystem) + if roleNotes and len(roleNotes) == len(roleID): + for idx, role in enumerate(m.currentRole): + try: + if roleNotes[idx]: + role.notes = roleNotes[idx] + except IndexError: + break + # Status can't be checked here, and must be detected by the parser. + if status: + m['status'] = status + return m + + +class DOMParserBase(object): + """Base parser to handle HTML data from the IMDb's web server.""" + _defGetRefs = False + _containsObjects = False + + preprocessors = [] + extractors = [] + usingModule = None + + _logger = logging.getLogger('imdbpy.parser.http.domparser') + + def __init__(self, useModule=None): + """Initialize the parser. useModule can be used to force it + to use 'BeautifulSoup' or 'lxml'; by default, it's auto-detected, + using 'lxml' if available and falling back to 'BeautifulSoup' + otherwise.""" + # Module to use. + if useModule is None: + useModule = ('lxml', 'BeautifulSoup') + if not isinstance(useModule, (tuple, list)): + useModule = [useModule] + self._useModule = useModule + nrMods = len(useModule) + _gotError = False + for idx, mod in enumerate(useModule): + mod = mod.strip().lower() + try: + if mod == 'lxml': + from lxml.html import fromstring + from lxml.etree import tostring + self._is_xml_unicode = False + self.usingModule = 'lxml' + elif mod == 'beautifulsoup': + from bsouplxml.html import fromstring + from bsouplxml.etree import tostring + self._is_xml_unicode = True + self.usingModule = 'beautifulsoup' + else: + self._logger.warn('unknown module "%s"' % mod) + continue + self.fromstring = fromstring + self._tostring = tostring + if _gotError: + warnings.warn('falling back to "%s"' % mod) + break + except ImportError, e: + if idx+1 >= nrMods: + # Raise the exception, if we don't have any more + # options to try. + raise IMDbError('unable to use any parser in %s: %s' % \ + (str(useModule), str(e))) + else: + warnings.warn('unable to use "%s": %s' % (mod, str(e))) + _gotError = True + continue + else: + raise IMDbError('unable to use parsers in %s' % str(useModule)) + # Fall-back defaults. + self._modFunct = None + self._as = 'http' + self._cname = self.__class__.__name__ + self._init() + self.reset() + + def reset(self): + """Reset the parser.""" + # Names and titles references. + self._namesRefs = {} + self._titlesRefs = {} + self._charactersRefs = {} + self._reset() + + def _init(self): + """Subclasses can override this method, if needed.""" + pass + + def _reset(self): + """Subclasses can override this method, if needed.""" + pass + + def parse(self, html_string, getRefs=None, **kwds): + """Return the dictionary generated from the given html string; + getRefs can be used to force the gathering of movies/persons/characters + references.""" + self.reset() + if getRefs is not None: + self.getRefs = getRefs + else: + self.getRefs = self._defGetRefs + # Useful only for the testsuite. + if not isinstance(html_string, unicode): + html_string = unicode(html_string, 'latin_1', 'replace') + html_string = subXMLRefs(html_string) + # Temporary fix: self.parse_dom must work even for empty strings. + html_string = self.preprocess_string(html_string) + html_string = html_string.strip() + if self.usingModule == 'beautifulsoup': + # tag attributes like title=""Family Guy"" will be + # converted to title=""Family Guy"" and this confuses BeautifulSoup. + html_string = html_string.replace('""', '"') + # Browser-specific escapes create problems to BeautifulSoup. + html_string = html_string.replace('<!--[if IE]>', '"') + html_string = html_string.replace('<![endif]-->', '"') + #print html_string.encode('utf8') + if html_string: + dom = self.get_dom(html_string) + #print self.tostring(dom).encode('utf8') + try: + dom = self.preprocess_dom(dom) + except Exception, e: + self._logger.error('%s: caught exception preprocessing DOM', + self._cname, exc_info=True) + if self.getRefs: + try: + self.gather_refs(dom) + except Exception, e: + self._logger.warn('%s: unable to gather refs: %s', + self._cname, exc_info=True) + data = self.parse_dom(dom) + else: + data = {} + try: + data = self.postprocess_data(data) + except Exception, e: + self._logger.error('%s: caught exception postprocessing data', + self._cname, exc_info=True) + if self._containsObjects: + self.set_objects_params(data) + data = self.add_refs(data) + return data + + def _build_empty_dom(self): + from bsouplxml import _bsoup + return _bsoup.BeautifulSoup('') + + def get_dom(self, html_string): + """Return a dom object, from the given string.""" + try: + dom = self.fromstring(html_string) + if dom is None: + dom = self._build_empty_dom() + self._logger.error('%s: using a fake empty DOM', self._cname) + return dom + except Exception, e: + self._logger.error('%s: caught exception parsing DOM', + self._cname, exc_info=True) + return self._build_empty_dom() + + def xpath(self, element, path): + """Return elements matching the given XPath.""" + try: + xpath_result = element.xpath(path) + if self._is_xml_unicode: + return xpath_result + result = [] + for item in xpath_result: + if isinstance(item, str): + item = unicode(item) + result.append(item) + return result + except Exception, e: + self._logger.error('%s: caught exception extracting XPath "%s"', + self._cname, path, exc_info=True) + return [] + + def tostring(self, element): + """Convert the element to a string.""" + if isinstance(element, (unicode, str)): + return unicode(element) + else: + try: + return self._tostring(element, encoding=unicode) + except Exception, e: + self._logger.error('%s: unable to convert to string', + self._cname, exc_info=True) + return u'' + + def clone(self, element): + """Clone an element.""" + return self.fromstring(self.tostring(element)) + + def preprocess_string(self, html_string): + """Here we can modify the text, before it's parsed.""" + if not html_string: + return html_string + # Remove silly » and – chars. + html_string = html_string.replace(u' \xbb', u'') + html_string = html_string.replace(u'–', u'-') + try: + preprocessors = self.preprocessors + except AttributeError: + return html_string + for src, sub in preprocessors: + # re._pattern_type is present only since Python 2.5. + if callable(getattr(src, 'sub', None)): + html_string = src.sub(sub, html_string) + elif isinstance(src, str): + html_string = html_string.replace(src, sub) + elif callable(src): + try: + html_string = src(html_string) + except Exception, e: + _msg = '%s: caught exception preprocessing html' + self._logger.error(_msg, self._cname, exc_info=True) + continue + ##print html_string.encode('utf8') + return html_string + + def gather_refs(self, dom): + """Collect references.""" + grParser = GatherRefs(useModule=self._useModule) + grParser._as = self._as + grParser._modFunct = self._modFunct + refs = grParser.parse_dom(dom) + refs = grParser.postprocess_data(refs) + self._namesRefs = refs['names refs'] + self._titlesRefs = refs['titles refs'] + self._charactersRefs = refs['characters refs'] + + def preprocess_dom(self, dom): + """Last chance to modify the dom, before the rules in self.extractors + are applied by the parse_dom method.""" + return dom + + def parse_dom(self, dom): + """Parse the given dom according to the rules specified + in self.extractors.""" + result = {} + for extractor in self.extractors: + ##print extractor.label + if extractor.group is None: + elements = [(extractor.label, element) + for element in self.xpath(dom, extractor.path)] + else: + groups = self.xpath(dom, extractor.group) + elements = [] + for group in groups: + group_key = self.xpath(group, extractor.group_key) + if not group_key: continue + group_key = group_key[0] + # XXX: always tries the conversion to unicode: + # BeautifulSoup.NavigableString is a subclass + # of unicode, and so it's never converted. + group_key = self.tostring(group_key) + normalizer = extractor.group_key_normalize + if normalizer is not None: + if callable(normalizer): + try: + group_key = normalizer(group_key) + except Exception, e: + _m = '%s: unable to apply group_key normalizer' + self._logger.error(_m, self._cname, + exc_info=True) + group_elements = self.xpath(group, extractor.path) + elements.extend([(group_key, element) + for element in group_elements]) + for group_key, element in elements: + for attr in extractor.attrs: + if isinstance(attr.path, dict): + data = {} + for field in attr.path.keys(): + path = attr.path[field] + value = self.xpath(element, path) + if not value: + data[field] = None + else: + # XXX: use u'' , to join? + data[field] = ''.join(value) + else: + data = self.xpath(element, attr.path) + if not data: + data = None + else: + data = attr.joiner.join(data) + if not data: + continue + attr_postprocess = attr.postprocess + if callable(attr_postprocess): + try: + data = attr_postprocess(data) + except Exception, e: + _m = '%s: unable to apply attr postprocess' + self._logger.error(_m, self._cname, exc_info=True) + key = attr.key + if key is None: + key = group_key + elif key.startswith('.'): + # assuming this is an xpath + try: + key = self.xpath(element, key)[0] + except IndexError: + self._logger.error('%s: XPath returned no items', + self._cname, exc_info=True) + elif key.startswith('self.'): + key = getattr(self, key[5:]) + if attr.multi: + if key not in result: + result[key] = [] + result[key].append(data) + else: + if isinstance(data, dict): + result.update(data) + else: + result[key] = data + return result + + def postprocess_data(self, data): + """Here we can modify the data.""" + return data + + def set_objects_params(self, data): + """Set parameters of Movie/Person/... instances, since they are + not always set in the parser's code.""" + for obj in flatten(data, yieldDictKeys=True, scalar=_Container): + obj.accessSystem = self._as + obj.modFunct = self._modFunct + + def add_refs(self, data): + """Modify data according to the expected output.""" + if self.getRefs: + titl_re = ur'(%s)' % '|'.join([re.escape(x) for x + in self._titlesRefs.keys()]) + if titl_re != ur'()': re_titles = re.compile(titl_re, re.U) + else: re_titles = None + nam_re = ur'(%s)' % '|'.join([re.escape(x) for x + in self._namesRefs.keys()]) + if nam_re != ur'()': re_names = re.compile(nam_re, re.U) + else: re_names = None + chr_re = ur'(%s)' % '|'.join([re.escape(x) for x + in self._charactersRefs.keys()]) + if chr_re != ur'()': re_characters = re.compile(chr_re, re.U) + else: re_characters = None + _putRefs(data, re_titles, re_names, re_characters) + return {'data': data, 'titlesRefs': self._titlesRefs, + 'namesRefs': self._namesRefs, + 'charactersRefs': self._charactersRefs} + + +class Extractor(object): + """Instruct the DOM parser about how to parse a document.""" + def __init__(self, label, path, attrs, group=None, group_key=None, + group_key_normalize=None): + """Initialize an Extractor object, used to instruct the DOM parser + about how to parse a document.""" + # rarely (never?) used, mostly for debugging purposes. + self.label = label + self.group = group + if group_key is None: + self.group_key = ".//text()" + else: + self.group_key = group_key + self.group_key_normalize = group_key_normalize + self.path = path + # A list of attributes to fetch. + if isinstance(attrs, Attribute): + attrs = [attrs] + self.attrs = attrs + + def __repr__(self): + """String representation of an Extractor object.""" + r = '<Extractor id:%s (label=%s, path=%s, attrs=%s, group=%s, ' \ + 'group_key=%s group_key_normalize=%s)>' % (id(self), + self.label, self.path, repr(self.attrs), self.group, + self.group_key, self.group_key_normalize) + return r + + +class Attribute(object): + """The attribute to consider, for a given node.""" + def __init__(self, key, multi=False, path=None, joiner=None, + postprocess=None): + """Initialize an Attribute object, used to specify the + attribute to consider, for a given node.""" + # The key under which information will be saved; can be a string or an + # XPath. If None, the label of the containing extractor will be used. + self.key = key + self.multi = multi + self.path = path + if joiner is None: + joiner = '' + self.joiner = joiner + # Post-process this set of information. + self.postprocess = postprocess + + def __repr__(self): + """String representation of an Attribute object.""" + r = '<Attribute id:%s (key=%s, multi=%s, path=%s, joiner=%s, ' \ + 'postprocess=%s)>' % (id(self), self.key, + self.multi, repr(self.path), + self.joiner, repr(self.postprocess)) + return r + + +def _parse_ref(text, link, info): + """Manage links to references.""" + if link.find('/title/tt') != -1: + yearK = re_yearKind_index.match(info) + if yearK and yearK.start() == 0: + text += ' %s' % info[:yearK.end()] + return (text.replace('\n', ' '), link) + + +class GatherRefs(DOMParserBase): + """Parser used to gather references to movies, persons and characters.""" + _attrs = [Attribute(key=None, multi=True, + path={ + 'text': './text()', + 'link': './@href', + 'info': './following::text()[1]' + }, + postprocess=lambda x: _parse_ref(x.get('text') or u'', x.get('link') or '', + (x.get('info') or u'').strip()))] + extractors = [ + Extractor(label='names refs', + path="//a[starts-with(@href, '/name/nm')][string-length(@href)=16]", + attrs=_attrs), + + Extractor(label='titles refs', + path="//a[starts-with(@href, '/title/tt')]" \ + "[string-length(@href)=17]", + attrs=_attrs), + + Extractor(label='characters refs', + path="//a[starts-with(@href, '/character/ch')]" \ + "[string-length(@href)=21]", + attrs=_attrs), + ] + + def postprocess_data(self, data): + result = {} + for item in ('names refs', 'titles refs', 'characters refs'): + result[item] = {} + for k, v in data.get(item, []): + k = k.strip() + v = v.strip() + if not (k and v): + continue + if not v.endswith('/'): continue + imdbID = analyze_imdbid(v) + if item == 'names refs': + obj = Person(personID=imdbID, name=k, + accessSystem=self._as, modFunct=self._modFunct) + elif item == 'titles refs': + obj = Movie(movieID=imdbID, title=k, + accessSystem=self._as, modFunct=self._modFunct) + else: + obj = Character(characterID=imdbID, name=k, + accessSystem=self._as, modFunct=self._modFunct) + # XXX: companies aren't handled: are they ever found in text, + # as links to their page? + result[item][k] = obj + return result + + def add_refs(self, data): + return data + + diff --git a/lib/imdb/parser/mobile/__init__.py b/lib/imdb/parser/mobile/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c391386b32da892f8b0132824eff7fc487ffc300 --- /dev/null +++ b/lib/imdb/parser/mobile/__init__.py @@ -0,0 +1,844 @@ +""" +parser.mobile package (imdb package). + +This package provides the IMDbMobileAccessSystem class used to access +IMDb's data for mobile systems. +the imdb.IMDb function will return an instance of this class when +called with the 'accessSystem' argument set to "mobile". + +Copyright 2005-2011 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +import logging +from urllib import unquote + +from imdb.Movie import Movie +from imdb.utils import analyze_title, analyze_name, canonicalName, \ + date_and_notes +from imdb._exceptions import IMDbDataAccessError +from imdb.parser.http import IMDbHTTPAccessSystem +from imdb.parser.http.utils import subXMLRefs, subSGMLRefs, build_person, \ + build_movie, re_spaces + +# XXX NOTE: the first version of this module was heavily based on +# regular expressions. This new version replace regexps with +# find() strings' method calls; despite being less flexible, it +# seems to be at least as fast and, hopefully, much more +# lightweight. Yes: the regexp-based version was too heavyweight +# for systems with very limited CPU power and memory footprint. +re_spacessub = re_spaces.sub +# Strip html. +re_unhtml = re.compile(r'<.+?>') +re_unhtmlsub = re_unhtml.sub +# imdb person or movie ids. +re_imdbID = re.compile(r'(?<=nm|tt|ch)([0-9]{7})\b') + +# movie AKAs. +re_makas = re.compile('(<p class="find-aka">.*?</p>)') + +# Remove episode numbers. +re_filmo_episodes = re.compile('<div class="filmo-episodes">.*?</div>', + re.M | re.I) + + +def _unHtml(s): + """Return a string without tags and no multiple spaces.""" + return subSGMLRefs(re_spacessub(' ', re_unhtmlsub('', s)).strip()) + + +_inttype = type(0) + +def _getTagsWith(s, cont, toClosure=False, maxRes=None): + """Return the html tags in the 's' string containing the 'cont' + string; if toClosure is True, everything between the opening + tag and the closing tag is returned.""" + lres = [] + bi = s.find(cont) + if bi != -1: + btag = s[:bi].rfind('<') + if btag != -1: + if not toClosure: + etag = s[bi+1:].find('>') + if etag != -1: + endidx = bi+2+etag + lres.append(s[btag:endidx]) + if maxRes is not None and len(lres) >= maxRes: return lres + lres += _getTagsWith(s[endidx:], cont, + toClosure=toClosure) + else: + spaceidx = s[btag:].find(' ') + if spaceidx != -1: + ctag = '</%s>' % s[btag+1:btag+spaceidx] + closeidx = s[bi:].find(ctag) + if closeidx != -1: + endidx = bi+closeidx+len(ctag) + lres.append(s[btag:endidx]) + if maxRes is not None and len(lres) >= maxRes: + return lres + lres += _getTagsWith(s[endidx:], cont, + toClosure=toClosure) + return lres + + +def _findBetween(s, begins, ends, beginindx=0, maxRes=None, lres=None): + """Return the list of strings from the 's' string which are included + between the 'begins' and 'ends' strings.""" + if lres is None: + lres = [] + bi = s.find(begins, beginindx) + if bi != -1: + lbegins = len(begins) + if isinstance(ends, (list, tuple)): + eset = [s.find(end, bi+lbegins) for end in ends] + eset[:] = [x for x in eset if x != -1] + if not eset: ei = -1 + else: ei = min(eset) + else: + ei = s.find(ends, bi+lbegins) + if ei != -1: + match = s[bi+lbegins:ei] + lres.append(match) + if maxRes is not None and len(lres) >= maxRes: return lres + _findBetween(s, begins, ends, beginindx=ei, maxRes=maxRes, + lres=lres) + return lres + + +class IMDbMobileAccessSystem(IMDbHTTPAccessSystem): + """The class used to access IMDb's data through the web for + mobile terminals.""" + + accessSystem = 'mobile' + _mobile_logger = logging.getLogger('imdbpy.parser.mobile') + + def __init__(self, isThin=0, *arguments, **keywords): + self.accessSystem = 'mobile' + IMDbHTTPAccessSystem.__init__(self, isThin, *arguments, **keywords) + + def _clean_html(self, html): + """Normalize the retrieve html.""" + html = re_spaces.sub(' ', html) + # Remove silly » chars. + html = html.replace(' »', '') + return subXMLRefs(html) + + def _mretrieve(self, url, size=-1): + """Retrieve an html page and normalize it.""" + cont = self._retrieve(url, size=size) + return self._clean_html(cont) + + def _getPersons(self, s, sep='<br/>'): + """Return a list of Person objects, from the string s; items + are assumed to be separated by the sep string.""" + names = s.split(sep) + pl = [] + plappend = pl.append + counter = 1 + for name in names: + pid = re_imdbID.findall(name) + if not pid: continue + characters = _getTagsWith(name, 'class="char"', + toClosure=True, maxRes=1) + chpids = [] + if characters: + for ch in characters[0].split(' / '): + chid = re_imdbID.findall(ch) + if not chid: + chpids.append(None) + else: + chpids.append(chid[-1]) + if not chpids: + chpids = None + elif len(chpids) == 1: + chpids = chpids[0] + name = _unHtml(name) + # Catch unclosed tags. + gt_indx = name.find('>') + if gt_indx != -1: + name = name[gt_indx+1:].lstrip() + if not name: continue + if name.endswith('...'): + name = name[:-3] + p = build_person(name, personID=str(pid[0]), billingPos=counter, + modFunct=self._defModFunct, roleID=chpids, + accessSystem=self.accessSystem) + plappend(p) + counter += 1 + return pl + + def _search_movie(self, title, results): + ##params = urllib.urlencode({'tt': 'on','mx': str(results),'q': title}) + ##params = 'q=%s&tt=on&mx=%s' % (urllib.quote_plus(title), str(results)) + ##cont = self._mretrieve(imdbURL_search % params) + cont = subXMLRefs(self._get_search_content('tt', title, results)) + title = _findBetween(cont, '<title>', '</title>', maxRes=1) + res = [] + if not title: + self._mobile_logger.error('no title tag searching for movie %s', + title) + return res + tl = title[0].lower() + if not tl.startswith('imdb title'): + # a direct hit! + title = _unHtml(title[0]) + mid = None + midtag = _getTagsWith(cont, 'rel="canonical"', maxRes=1) + if midtag: + mid = _findBetween(midtag[0], '/title/tt', '/', maxRes=1) + if not (mid and title): + self._mobile_logger.error('no direct hit title/movieID for' \ + ' title %s', title) + return res + if cont.find('<span class="tv-extra">TV mini-series</span>') != -1: + title += ' (mini)' + res[:] = [(str(mid[0]), analyze_title(title))] + else: + # XXX: this results*3 prevents some recursion errors, but... + # it's not exactly understandable (i.e.: why 'results' is + # not enough to get all the results?) + lis = _findBetween(cont, 'td valign="top">', '</td>', + maxRes=results*3) + for li in lis: + akas = re_makas.findall(li) + for idx, aka in enumerate(akas): + aka = aka.replace('" - ', '::', 1) + aka = _unHtml(aka) + if aka.startswith('aka "'): + aka = aka[5:].strip() + if aka[-1] == '"': + aka = aka[:-1] + akas[idx] = aka + imdbid = re_imdbID.findall(li) + li = re_makas.sub('', li) + mtitle = _unHtml(li) + if not (imdbid and mtitle): + self._mobile_logger.debug('no title/movieID parsing' \ + ' %s searching for title %s', li, + title) + continue + mtitle = mtitle.replace('(TV mini-series)', '(mini)') + resd = analyze_title(mtitle) + if akas: + resd['akas'] = akas + res.append((str(imdbid[0]), resd)) + return res + + def get_movie_main(self, movieID): + cont = self._mretrieve(self.urls['movie_main'] % movieID + 'maindetails') + title = _findBetween(cont, '<title>', '</title>', maxRes=1) + if not title: + raise IMDbDataAccessError('unable to get movieID "%s"' % movieID) + title = _unHtml(title[0]) + if title.endswith(' - IMDb'): + title = title[:-7] + if cont.find('<span class="tv-extra">TV mini-series</span>') != -1: + title += ' (mini)' + d = analyze_title(title) + kind = d.get('kind') + tv_series = _findBetween(cont, 'TV Series:</h5>', '</a>', maxRes=1) + if tv_series: mid = re_imdbID.findall(tv_series[0]) + else: mid = None + if tv_series and mid: + s_title = _unHtml(tv_series[0]) + s_data = analyze_title(s_title) + m = Movie(movieID=str(mid[0]), data=s_data, + accessSystem=self.accessSystem, + modFunct=self._defModFunct) + d['kind'] = kind = u'episode' + d['episode of'] = m + if kind in ('tv series', 'tv mini series'): + years = _findBetween(cont, '<h1>', '</h1>', maxRes=1) + if years: + years[:] = _findBetween(years[0], 'TV series', '</span>', + maxRes=1) + if years: + d['series years'] = years[0].strip() + air_date = _findBetween(cont, 'Original Air Date:</h5>', '</div>', + maxRes=1) + if air_date: + air_date = air_date[0] + vi = air_date.find('(') + if vi != -1: + date = _unHtml(air_date[:vi]).strip() + if date != '????': + d['original air date'] = date + air_date = air_date[vi:] + season = _findBetween(air_date, 'Season', ',', maxRes=1) + if season: + season = season[0].strip() + try: season = int(season) + except: pass + if season or type(season) is _inttype: + d['season'] = season + episode = _findBetween(air_date, 'Episode', ')', maxRes=1) + if episode: + episode = episode[0].strip() + try: episode = int(episode) + except: pass + if episode or type(season) is _inttype: + d['episode'] = episode + direct = _findBetween(cont, '<h5>Director', ('</div>', '<br/> <br/>'), + maxRes=1) + if direct: + direct = direct[0] + h5idx = direct.find('/h5>') + if h5idx != -1: + direct = direct[h5idx+4:] + direct = self._getPersons(direct) + if direct: d['director'] = direct + if kind in ('tv series', 'tv mini series', 'episode'): + if kind != 'episode': + seasons = _findBetween(cont, 'Seasons:</h5>', '</div>', + maxRes=1) + if seasons: + d['number of seasons'] = seasons[0].count('|') + 1 + creator = _findBetween(cont, 'Created by</h5>', ('class="tn15more"', + '</div>', + '<br/> <br/>'), + maxRes=1) + if not creator: + # They change 'Created by' to 'Creator' and viceversa + # from time to time... + # XXX: is 'Creators' also used? + creator = _findBetween(cont, 'Creator:</h5>', + ('class="tn15more"', '</div>', + '<br/> <br/>'), maxRes=1) + if creator: + creator = creator[0] + if creator.find('tn15more'): creator = '%s>' % creator + creator = self._getPersons(creator) + if creator: d['creator'] = creator + writers = _findBetween(cont, '<h5>Writer', ('</div>', '<br/> <br/>'), + maxRes=1) + if writers: + writers = writers[0] + h5idx = writers.find('/h5>') + if h5idx != -1: + writers = writers[h5idx+4:] + writers = self._getPersons(writers) + if writers: d['writer'] = writers + cvurl = _getTagsWith(cont, 'name="poster"', toClosure=True, maxRes=1) + if cvurl: + cvurl = _findBetween(cvurl[0], 'src="', '"', maxRes=1) + if cvurl: d['cover url'] = cvurl[0] + genres = _findBetween(cont, 'href="/genre/', '"') + if genres: + d['genres'] = list(set(genres)) + ur = _findBetween(cont, 'id="star-bar-user-rate">', '</div>', + maxRes=1) + if ur: + rat = _findBetween(ur[0], '<b>', '</b>', maxRes=1) + if rat: + if rat: + d['rating'] = rat[0].strip() + else: + self._mobile_logger.warn('wrong rating: %s', rat) + vi = ur[0].rfind('href="ratings"') + if vi != -1 and ur[0][vi+10:].find('await') == -1: + try: + votes = _findBetween(ur[0][vi:], "title='", + " IMDb", maxRes=1) + votes = int(votes[0].replace(',', '')) + d['votes'] = votes + except (ValueError, IndexError): + self._mobile_logger.warn('wrong votes: %s', ur) + top250 = _findBetween(cont, 'href="/chart/top?', '</a>', maxRes=1) + if top250: + fn = top250[0].rfind('#') + if fn != -1: + try: + td = int(top250[0][fn+1:]) + d['top 250 rank'] = td + except ValueError: + self._mobile_logger.warn('wrong top250: %s', top250) + castdata = _findBetween(cont, 'Cast overview', '</table>', maxRes=1) + if not castdata: + castdata = _findBetween(cont, 'Credited cast', '</table>', maxRes=1) + if not castdata: + castdata = _findBetween(cont, 'Complete credited cast', '</table>', + maxRes=1) + if not castdata: + castdata = _findBetween(cont, 'Series Cast Summary', '</table>', + maxRes=1) + if not castdata: + castdata = _findBetween(cont, 'Episode Credited cast', '</table>', + maxRes=1) + if castdata: + castdata = castdata[0] + # Reintegrate the fist tag. + fl = castdata.find('href=') + if fl != -1: castdata = '<a ' + castdata[fl:] + # Exclude the 'rest of cast listed alphabetically' row. + smib = castdata.find('<tr><td align="center" colspan="4"><small>') + if smib != -1: + smie = castdata.rfind('</small></td></tr>') + if smie != -1: + castdata = castdata[:smib].strip() + \ + castdata[smie+18:].strip() + castdata = castdata.replace('/tr> <tr', '/tr><tr') + cast = self._getPersons(castdata, sep='</tr><tr') + if cast: d['cast'] = cast + akas = _findBetween(cont, 'Also Known As:</h5>', '</div>', maxRes=1) + if akas: + # For some reason, here <br> is still used in place of <br/>. + akas[:] = [x for x in akas[0].split('<br>') if x.strip()] + akas = [_unHtml(x).replace('" - ','::', 1).lstrip('"').strip() + for x in akas] + if 'See more' in akas: akas.remove('See more') + akas[:] = [x for x in akas if x] + if akas: + d['akas'] = akas + mpaa = _findBetween(cont, 'MPAA</a>:', '</div>', maxRes=1) + if mpaa: d['mpaa'] = _unHtml(mpaa[0]) + runtimes = _findBetween(cont, 'Runtime:</h5>', '</div>', maxRes=1) + if runtimes: + runtimes = runtimes[0] + runtimes = [x.strip().replace(' min', '').replace(' (', '::(', 1) + for x in runtimes.split('|')] + d['runtimes'] = [_unHtml(x).strip() for x in runtimes] + if kind == 'episode': + # number of episodes. + epsn = _findBetween(cont, 'title="Full Episode List">', '</a>', + maxRes=1) + if epsn: + epsn = epsn[0].replace(' Episodes', '').strip() + if epsn: + try: + epsn = int(epsn) + except: + self._mobile_logger.warn('wrong episodes #: %s', epsn) + d['number of episodes'] = epsn + country = _findBetween(cont, 'Country:</h5>', '</div>', maxRes=1) + if country: + country[:] = country[0].split(' | ') + country[:] = ['<a %s' % x for x in country if x] + country[:] = [_unHtml(x.replace(' <i>', '::')) for x in country] + if country: d['countries'] = country + lang = _findBetween(cont, 'Language:</h5>', '</div>', maxRes=1) + if lang: + lang[:] = lang[0].split(' | ') + lang[:] = ['<a %s' % x for x in lang if x] + lang[:] = [_unHtml(x.replace(' <i>', '::')) for x in lang] + if lang: d['languages'] = lang + col = _findBetween(cont, '"/search/title?colors=', '</div>') + if col: + col[:] = col[0].split(' | ') + col[:] = ['<a %s' % x for x in col if x] + col[:] = [_unHtml(x.replace(' <i>', '::')) for x in col] + if col: d['color info'] = col + sm = _findBetween(cont, '/search/title?sound_mixes=', '</div>', + maxRes=1) + if sm: + sm[:] = sm[0].split(' | ') + sm[:] = ['<a %s' % x for x in sm if x] + sm[:] = [_unHtml(x.replace(' <i>', '::')) for x in sm] + if sm: d['sound mix'] = sm + cert = _findBetween(cont, 'Certification:</h5>', '</div>', maxRes=1) + if cert: + cert[:] = cert[0].split(' | ') + cert[:] = [_unHtml(x.replace(' <i>', '::')) for x in cert] + if cert: d['certificates'] = cert + plotoutline = _findBetween(cont, 'Plot:</h5>', ['<a ', '</div>'], + maxRes=1) + if plotoutline: + plotoutline = plotoutline[0].strip() + plotoutline = plotoutline.rstrip('|').rstrip() + if plotoutline: d['plot outline'] = _unHtml(plotoutline) + aratio = _findBetween(cont, 'Aspect Ratio:</h5>', ['<a ', '</div>'], + maxRes=1) + if aratio: + aratio = aratio[0].strip().replace(' (', '::(', 1) + if aratio: + d['aspect ratio'] = _unHtml(aratio) + return {'data': d} + + def get_movie_plot(self, movieID): + cont = self._mretrieve(self.urls['movie_main'] % movieID + 'plotsummary') + plot = _findBetween(cont, '<p class="plotpar">', '</p>') + plot[:] = [_unHtml(x) for x in plot] + for i in xrange(len(plot)): + p = plot[i] + wbyidx = p.rfind(' Written by ') + if wbyidx != -1: + plot[i] = '%s::%s' % \ + (p[:wbyidx].rstrip(), + p[wbyidx+12:].rstrip().replace('{','<').replace('}','>')) + if plot: return {'data': {'plot': plot}} + return {'data': {}} + + def _search_person(self, name, results): + ##params = urllib.urlencode({'nm': 'on', 'mx': str(results), 'q': name}) + ##params = 'q=%s&nm=on&mx=%s' % (urllib.quote_plus(name), str(results)) + ##cont = self._mretrieve(imdbURL_search % params) + cont = subXMLRefs(self._get_search_content('nm', name, results)) + name = _findBetween(cont, '<title>', '</title>', maxRes=1) + res = [] + if not name: + self._mobile_logger.warn('no title tag searching for name %s', name) + return res + nl = name[0].lower() + if not nl.startswith('imdb name'): + # a direct hit! + name = _unHtml(name[0]) + name = name.replace('- Filmography by type' , '').strip() + pid = None + pidtag = _getTagsWith(cont, 'rel="canonical"', maxRes=1) + if pidtag: + pid = _findBetween(pidtag[0], '/name/nm', '/', maxRes=1) + if not (pid and name): + self._mobile_logger.error('no direct hit name/personID for' \ + ' name %s', name) + return res + res[:] = [(str(pid[0]), analyze_name(name, canonical=1))] + else: + lis = _findBetween(cont, 'td valign="top">', '</td>', + maxRes=results*3) + for li in lis: + akas = _findBetween(li, '<em>"', '"</em>') + for sep in ['<small', '<br> aka', '<br> birth name']: + sepIdx = li.find(sep) + if sepIdx != -1: + li = li[:sepIdx] + pid = re_imdbID.findall(li) + pname = _unHtml(li) + if not (pid and pname): + self._mobile_logger.debug('no name/personID parsing' \ + ' %s searching for name %s', li, + name) + continue + resd = analyze_name(pname, canonical=1) + if akas: + resd['akas'] = akas + res.append((str(pid[0]), resd)) + return res + + def get_person_main(self, personID, _parseChr=False): + if not _parseChr: + url = self.urls['person_main'] % personID + 'maindetails' + else: + url = self.urls['character_main'] % personID + s = self._mretrieve(url) + r = {} + name = _findBetween(s, '<title>', '</title>', maxRes=1) + if not name: + if _parseChr: w = 'characterID' + else: w = 'personID' + raise IMDbDataAccessError('unable to get %s "%s"' % (w, personID)) + name = _unHtml(name[0].replace(' - IMDb', '')) + if _parseChr: + name = name.replace('(Character)', '').strip() + name = name.replace('- Filmography by type', '').strip() + else: + name = name.replace('- Filmography by', '').strip() + r = analyze_name(name, canonical=not _parseChr) + for dKind in ('Born', 'Died'): + date = _findBetween(s, '%s:</h4>' % dKind.capitalize(), + ('<div class', '</div>', '<br/><br/>'), maxRes=1) + if date: + date = _unHtml(date[0]) + if date: + #date, notes = date_and_notes(date) + # TODO: fix to handle real names. + date_notes = date.split(' in ', 1) + notes = u'' + date = date_notes[0] + if len(date_notes) == 2: + notes = date_notes[1] + dtitle = 'birth' + if dKind == 'Died': + dtitle = 'death' + if date: + r['%s date' % dtitle] = date + if notes: + r['%s notes' % dtitle] = notes + akas = _findBetween(s, 'Alternate Names:</h4>', ('</div>', + '<br/><br/>'), maxRes=1) + if akas: + akas = akas[0] + if akas: + akas = _unHtml(akas) + if akas.find(' | ') != -1: + akas = akas.split(' | ') + else: + akas = akas.split(' / ') + if akas: r['akas'] = filter(None, [x.strip() for x in akas]) + hs = _findBetween(s, "rel='image_src'", '>', maxRes=1) + if not hs: + hs = _findBetween(s, 'rel="image_src"', '>', maxRes=1) + if not hs: + hs = _findBetween(s, '<a name="headshot"', '</a>', maxRes=1) + if hs: + hsl = _findBetween(hs[0], "href='", "'", maxRes=1) + if not hsl: + hsl = _findBetween(hs[0], 'href="', '"', maxRes=1) + if hsl and 'imdb-share-logo' not in hsl[0]: + r['headshot'] = hsl[0] + # Build a list of tuples such [('hrefLink', 'section name')] + workkind = _findBetween(s, 'id="jumpto_', '</a>') + ws = [] + for work in workkind: + sep = '" >' + if '">' in work: + sep = '">' + wsplit = work.split(sep, 1) + if len(wsplit) == 2: + sect = wsplit[0] + if '"' in sect: + sect = sect[:sect.find('"')] + ws.append((sect, wsplit[1].lower())) + # XXX: I think "guest appearances" are gone. + if s.find('<a href="#guest-appearances"') != -1: + ws.append(('guest-appearances', 'notable tv guest appearances')) + #if _parseChr: + # ws.append(('filmography', 'filmography')) + for sect, sectName in ws: + raws = u'' + if sectName == 'self': + sect = 'Self' + # Everything between the current section link and the end + # of the <ol> tag. + if _parseChr and sect == 'filmography': + inisect = s.find('<div class="filmo">') + else: + inisect = s.find('<a name="%s' % sect) + if inisect != -1: + endsect = s[inisect:].find('<div id="filmo-head-') + if endsect == -1: + endsect = s[inisect:].find('<div class="article"') + if endsect != -1: raws = s[inisect:inisect+endsect] + #if not raws: continue + mlist = _findBetween(raws, '<div class="filmo-row', + ('<div class="clear"/>',)) + for m in mlist: + fCB = m.find('>') + if fCB != -1: + m = m[fCB+1:].lstrip() + m = re_filmo_episodes.sub('', m) + # For every movie in the current section. + movieID = re_imdbID.findall(m) + if not movieID: + self._mobile_logger.debug('no movieID in %s', m) + continue + m = m.replace('<br/>', ' .... ', 1) + if not _parseChr: + chrIndx = m.find(' .... ') + else: + chrIndx = m.find(' Played by ') + chids = [] + if chrIndx != -1: + chrtxt = m[chrIndx+6:] + if _parseChr: + chrtxt = chrtxt[5:] + for ch in chrtxt.split(' / '): + chid = re_imdbID.findall(ch) + if not chid: + chids.append(None) + else: + chids.append(chid[-1]) + if not chids: + chids = None + elif len(chids) == 1: + chids = chids[0] + movieID = str(movieID[0]) + # Search the status. + stidx = m.find('<i>') + status = u'' + if stidx != -1: + stendidx = m.rfind('</i>') + if stendidx != -1: + status = _unHtml(m[stidx+3:stendidx]) + m = m.replace(m[stidx+3:stendidx], '') + year = _findBetween(m, 'year_column">', '</span>', maxRes=1) + if year: + year = year[0] + m = m.replace('<span class="year_column">%s</span>' % year, + '') + else: + year = None + m = _unHtml(m) + if not m: + self._mobile_logger.warn('no title for movieID %s', movieID) + continue + movie = build_movie(m, movieID=movieID, status=status, + roleID=chids, modFunct=self._defModFunct, + accessSystem=self.accessSystem, + _parsingCharacter=_parseChr, year=year) + sectName = sectName.split(':')[0] + r.setdefault(sectName, []).append(movie) + # If available, take the always correct name from a form. + itag = _getTagsWith(s, 'NAME="primary"', maxRes=1) + if not itag: + itag = _getTagsWith(s, 'name="primary"', maxRes=1) + if itag: + vtag = _findBetween(itag[0], 'VALUE="', ('"', '>'), maxRes=1) + if not vtag: + vtag = _findBetween(itag[0], 'value="', ('"', '>'), maxRes=1) + if vtag: + try: + vtag = unquote(str(vtag[0])) + vtag = unicode(vtag, 'latin_1') + r.update(analyze_name(vtag)) + except UnicodeEncodeError: + pass + return {'data': r, 'info sets': ('main', 'filmography')} + + def get_person_biography(self, personID): + cont = self._mretrieve(self.urls['person_main'] % personID + 'bio') + d = {} + spouses = _findBetween(cont, 'Spouse</h5>', ('</table>', '</dd>'), + maxRes=1) + if spouses: + sl = [] + for spouse in spouses[0].split('</tr>'): + if spouse.count('</td>') > 1: + spouse = spouse.replace('</td>', '::</td>', 1) + spouse = _unHtml(spouse) + spouse = spouse.replace(':: ', '::').strip() + if spouse: sl.append(spouse) + if sl: d['spouse'] = sl + nnames = _findBetween(cont, '<h5>Nickname</h5>', ('<br/> <br/>','<h5>'), + maxRes=1) + if nnames: + nnames = nnames[0] + if nnames: + nnames = [x.strip().replace(' (', '::(', 1) + for x in nnames.split('<br/>')] + if nnames: + d['nick names'] = nnames + misc_sects = _findBetween(cont, '<h5>', '<br/>') + misc_sects[:] = [x.split('</h5>') for x in misc_sects] + misc_sects[:] = [x for x in misc_sects if len(x) == 2] + for sect, data in misc_sects: + sect = sect.lower().replace(':', '').strip() + if d.has_key(sect) and sect != 'mini biography': continue + elif sect in ('spouse', 'nickname'): continue + if sect == 'salary': sect = 'salary history' + elif sect == 'where are they now': sect = 'where now' + elif sect == 'personal quotes': sect = 'quotes' + data = data.replace('</p><p>', '::') + data = data.replace('<br><br>', ' ') # for multi-paragraphs 'bio' + data = data.replace('</td> <td valign="top">', '@@@@') + data = data.replace('</td> </tr>', '::') + data = _unHtml(data) + data = [x.strip() for x in data.split('::')] + data[:] = [x.replace('@@@@', '::') for x in data if x] + if sect == 'height' and data: data = data[0] + elif sect == 'birth name': data = canonicalName(data[0]) + elif sect == 'date of birth': + date, notes = date_and_notes(data[0]) + if date: + d['birth date'] = date + if notes: + d['birth notes'] = notes + continue + elif sect == 'date of death': + date, notes = date_and_notes(data[0]) + if date: + d['death date'] = date + if notes: + d['death notes'] = notes + continue + elif sect == 'mini biography': + ndata = [] + for bio in data: + byidx = bio.rfind('IMDb Mini Biography By') + if byidx != -1: + bioAuth = bio[:byidx].rstrip() + else: + bioAuth = 'Anonymous' + bio = u'%s::%s' % (bioAuth, bio[byidx+23:].lstrip()) + ndata.append(bio) + data[:] = ndata + if 'mini biography' in d: + d['mini biography'].append(ndata[0]) + continue + d[sect] = data + return {'data': d} + + def _search_character(self, name, results): + cont = subXMLRefs(self._get_search_content('char', name, results)) + name = _findBetween(cont, '<title>', '</title>', maxRes=1) + res = [] + if not name: + self._mobile_logger.error('no title tag searching character %s', + name) + return res + nl = name[0].lower() + if not (nl.startswith('imdb search') or nl.startswith('imdb search') \ + or nl.startswith('imdb character')): + # a direct hit! + name = _unHtml(name[0]).replace('(Character)', '').strip() + pid = None + pidtag = _getTagsWith(cont, 'rel="canonical"', maxRes=1) + if pidtag: + pid = _findBetween(pidtag[0], '/character/ch', '/', maxRes=1) + if not (pid and name): + self._mobile_logger.error('no direct hit name/characterID for' \ + ' character %s', name) + return res + res[:] = [(str(pid[0]), analyze_name(name))] + else: + sects = _findBetween(cont, '<b>Popular Characters</b>', '</table>', + maxRes=results*3) + sects += _findBetween(cont, '<b>Characters', '</table>', + maxRes=results*3) + for sect in sects: + lis = _findBetween(sect, '<a href="/character/', + ['<small', '</td>', '<br']) + for li in lis: + li = '<%s' % li + pid = re_imdbID.findall(li) + pname = _unHtml(li) + if not (pid and pname): + self._mobile_logger.debug('no name/characterID' \ + ' parsing %s searching for' \ + ' character %s', li, name) + continue + res.append((str(pid[0]), analyze_name(pname))) + return res + + def get_character_main(self, characterID): + return self.get_person_main(characterID, _parseChr=True) + + def get_character_biography(self, characterID): + cont = self._mretrieve(self.urls['character_main'] % characterID + 'bio') + d = {} + intro = _findBetween(cont, '<div class="display">', + ('<span>', '<h4>'), maxRes=1) + if intro: + intro = _unHtml(intro[0]).strip() + if intro: + d['introduction'] = intro + tocidx = cont.find('<table id="toc..') + if tocidx != -1: + cont = cont[tocidx:] + bios = _findBetween(cont, '<h4>', ('<h4>', '</div>')) + if bios: + for bio in bios: + bio = bio.replace('</h4>', '::') + bio = bio.replace('\n', ' ') + bio = bio.replace('<br>', '\n') + bio = bio.replace('<br/>', '\n') + bio = subSGMLRefs(re_unhtmlsub('', bio).strip()) + bio = bio.replace(' ::', '::').replace(':: ', '::') + bio = bio.replace('::', ': ', 1) + if bio: + d.setdefault('biography', []).append(bio) + return {'data': d} + + diff --git a/lib/imdb/parser/sql/__init__.py b/lib/imdb/parser/sql/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4ab5adc4ca0095e3b102fd415f84dfbb6066750e --- /dev/null +++ b/lib/imdb/parser/sql/__init__.py @@ -0,0 +1,1589 @@ +""" +parser.sql package (imdb package). + +This package provides the IMDbSqlAccessSystem class used to access +IMDb's data through a SQL database. Every database supported by +the SQLObject _AND_ SQLAlchemy Object Relational Managers is available. +the imdb.IMDb function will return an instance of this class when +called with the 'accessSystem' argument set to "sql", "database" or "db". + +Copyright 2005-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +# FIXME: this whole module was written in a veeery short amount of time. +# The code should be commented, rewritten and cleaned. :-) + +import re +import logging +from difflib import SequenceMatcher +from codecs import lookup + +from imdb import IMDbBase +from imdb.utils import normalizeName, normalizeTitle, build_title, \ + build_name, analyze_name, analyze_title, \ + canonicalTitle, canonicalName, re_titleRef, \ + build_company_name, re_episodes, _unicodeArticles, \ + analyze_company_name, re_year_index, re_nameRef +from imdb.Person import Person +from imdb.Movie import Movie +from imdb.Company import Company +from imdb._exceptions import IMDbDataAccessError, IMDbError + + +# Logger for miscellaneous functions. +_aux_logger = logging.getLogger('imdbpy.parser.sql.aux') + +# ============================= +# Things that once upon a time were in imdb.parser.common.locsql. + +def titleVariations(title, fromPtdf=0): + """Build title variations useful for searches; if fromPtdf is true, + the input is assumed to be in the plain text data files format.""" + if fromPtdf: title1 = u'' + else: title1 = title + title2 = title3 = u'' + if fromPtdf or re_year_index.search(title): + # If it appears to have a (year[/imdbIndex]) indication, + # assume that a long imdb canonical name was provided. + titldict = analyze_title(title, canonical=1) + # title1: the canonical name. + title1 = titldict['title'] + if titldict['kind'] != 'episode': + # title3: the long imdb canonical name. + if fromPtdf: title3 = title + else: title3 = build_title(titldict, canonical=1, ptdf=1) + else: + title1 = normalizeTitle(title1) + title3 = build_title(titldict, canonical=1, ptdf=1) + else: + # Just a title. + # title1: the canonical title. + title1 = canonicalTitle(title) + title3 = u'' + # title2 is title1 without the article, or title1 unchanged. + if title1: + title2 = title1 + t2s = title2.split(u', ') + if t2s[-1].lower() in _unicodeArticles: + title2 = u', '.join(t2s[:-1]) + _aux_logger.debug('title variations: 1:[%s] 2:[%s] 3:[%s]', + title1, title2, title3) + return title1, title2, title3 + + +re_nameIndex = re.compile(r'\(([IVXLCDM]+)\)') + +def nameVariations(name, fromPtdf=0): + """Build name variations useful for searches; if fromPtdf is true, + the input is assumed to be in the plain text data files format.""" + name1 = name2 = name3 = u'' + if fromPtdf or re_nameIndex.search(name): + # We've a name with an (imdbIndex) + namedict = analyze_name(name, canonical=1) + # name1 is the name in the canonical format. + name1 = namedict['name'] + # name3 is the canonical name with the imdbIndex. + if fromPtdf: + if namedict.has_key('imdbIndex'): + name3 = name + else: + name3 = build_name(namedict, canonical=1) + else: + # name1 is the name in the canonical format. + name1 = canonicalName(name) + name3 = u'' + # name2 is the name in the normal format, if it differs from name1. + name2 = normalizeName(name1) + if name1 == name2: name2 = u'' + _aux_logger.debug('name variations: 1:[%s] 2:[%s] 3:[%s]', + name1, name2, name3) + return name1, name2, name3 + + +try: + from cutils import ratcliff as _ratcliff + def ratcliff(s1, s2, sm): + """Return the Ratcliff-Obershelp value between the two strings, + using the C implementation.""" + return _ratcliff(s1.encode('latin_1', 'replace'), + s2.encode('latin_1', 'replace')) +except ImportError: + _aux_logger.warn('Unable to import the cutils.ratcliff function.' + ' Searching names and titles using the "sql"' + ' data access system will be slower.') + + def ratcliff(s1, s2, sm): + """Ratcliff-Obershelp similarity.""" + STRING_MAXLENDIFFER = 0.7 + s1len = len(s1) + s2len = len(s2) + if s1len < s2len: + threshold = float(s1len) / s2len + else: + threshold = float(s2len) / s1len + if threshold < STRING_MAXLENDIFFER: + return 0.0 + sm.set_seq2(s2.lower()) + return sm.ratio() + + +def merge_roles(mop): + """Merge multiple roles.""" + new_list = [] + for m in mop: + if m in new_list: + keep_this = new_list[new_list.index(m)] + if not isinstance(keep_this.currentRole, list): + keep_this.currentRole = [keep_this.currentRole] + keep_this.currentRole.append(m.currentRole) + else: + new_list.append(m) + return new_list + + +def scan_names(name_list, name1, name2, name3, results=0, ro_thresold=None, + _scan_character=False): + """Scan a list of names, searching for best matches against + the given variations.""" + if ro_thresold is not None: RO_THRESHOLD = ro_thresold + else: RO_THRESHOLD = 0.6 + sm1 = SequenceMatcher() + sm2 = SequenceMatcher() + sm3 = SequenceMatcher() + sm1.set_seq1(name1.lower()) + if name2: sm2.set_seq1(name2.lower()) + if name3: sm3.set_seq1(name3.lower()) + resd = {} + for i, n_data in name_list: + nil = n_data['name'] + # XXX: on Symbian, here we get a str; not sure this is the + # right place to fix it. + if isinstance(nil, str): + nil = unicode(nil, 'latin1', 'ignore') + # Distance with the canonical name. + ratios = [ratcliff(name1, nil, sm1) + 0.05] + namesurname = u'' + if not _scan_character: + nils = nil.split(', ', 1) + surname = nils[0] + if len(nils) == 2: namesurname = '%s %s' % (nils[1], surname) + else: + nils = nil.split(' ', 1) + surname = nils[-1] + namesurname = nil + if surname != nil: + # Distance with the "Surname" in the database. + ratios.append(ratcliff(name1, surname, sm1)) + if not _scan_character: + ratios.append(ratcliff(name1, namesurname, sm1)) + if name2: + ratios.append(ratcliff(name2, surname, sm2)) + # Distance with the "Name Surname" in the database. + if namesurname: + ratios.append(ratcliff(name2, namesurname, sm2)) + if name3: + # Distance with the long imdb canonical name. + ratios.append(ratcliff(name3, + build_name(n_data, canonical=1), sm3) + 0.1) + ratio = max(ratios) + if ratio >= RO_THRESHOLD: + if resd.has_key(i): + if ratio > resd[i][0]: resd[i] = (ratio, (i, n_data)) + else: resd[i] = (ratio, (i, n_data)) + res = resd.values() + res.sort() + res.reverse() + if results > 0: res[:] = res[:results] + return res + + +def scan_titles(titles_list, title1, title2, title3, results=0, + searchingEpisode=0, onlyEpisodes=0, ro_thresold=None): + """Scan a list of titles, searching for best matches against + the given variations.""" + if ro_thresold is not None: RO_THRESHOLD = ro_thresold + else: RO_THRESHOLD = 0.6 + sm1 = SequenceMatcher() + sm2 = SequenceMatcher() + sm3 = SequenceMatcher() + sm1.set_seq1(title1.lower()) + sm2.set_seq2(title2.lower()) + if title3: + sm3.set_seq1(title3.lower()) + if title3[-1] == '}': searchingEpisode = 1 + hasArt = 0 + if title2 != title1: hasArt = 1 + resd = {} + for i, t_data in titles_list: + if onlyEpisodes: + if t_data.get('kind') != 'episode': + continue + til = t_data['title'] + if til[-1] == ')': + dateIdx = til.rfind('(') + if dateIdx != -1: + til = til[:dateIdx].rstrip() + if not til: + continue + ratio = ratcliff(title1, til, sm1) + if ratio >= RO_THRESHOLD: + resd[i] = (ratio, (i, t_data)) + continue + if searchingEpisode: + if t_data.get('kind') != 'episode': continue + elif t_data.get('kind') == 'episode': continue + til = t_data['title'] + # XXX: on Symbian, here we get a str; not sure this is the + # right place to fix it. + if isinstance(til, str): + til = unicode(til, 'latin1', 'ignore') + # Distance with the canonical title (with or without article). + # titleS -> titleR + # titleS, the -> titleR, the + if not searchingEpisode: + til = canonicalTitle(til) + ratios = [ratcliff(title1, til, sm1) + 0.05] + # til2 is til without the article, if present. + til2 = til + tils = til2.split(', ') + matchHasArt = 0 + if tils[-1].lower() in _unicodeArticles: + til2 = ', '.join(tils[:-1]) + matchHasArt = 1 + if hasArt and not matchHasArt: + # titleS[, the] -> titleR + ratios.append(ratcliff(title2, til, sm2)) + elif matchHasArt and not hasArt: + # titleS -> titleR[, the] + ratios.append(ratcliff(title1, til2, sm1)) + else: + ratios = [0.0] + if title3: + # Distance with the long imdb canonical title. + ratios.append(ratcliff(title3, + build_title(t_data, canonical=1, ptdf=1), sm3) + 0.1) + ratio = max(ratios) + if ratio >= RO_THRESHOLD: + if resd.has_key(i): + if ratio > resd[i][0]: + resd[i] = (ratio, (i, t_data)) + else: resd[i] = (ratio, (i, t_data)) + res = resd.values() + res.sort() + res.reverse() + if results > 0: res[:] = res[:results] + return res + + +def scan_company_names(name_list, name1, results=0, ro_thresold=None): + """Scan a list of company names, searching for best matches against + the given name. Notice that this function takes a list of + strings, and not a list of dictionaries.""" + if ro_thresold is not None: RO_THRESHOLD = ro_thresold + else: RO_THRESHOLD = 0.6 + sm1 = SequenceMatcher() + sm1.set_seq1(name1.lower()) + resd = {} + withoutCountry = not name1.endswith(']') + for i, n in name_list: + # XXX: on Symbian, here we get a str; not sure this is the + # right place to fix it. + if isinstance(n, str): + n = unicode(n, 'latin1', 'ignore') + o_name = n + var = 0.0 + if withoutCountry and n.endswith(']'): + cidx = n.rfind('[') + if cidx != -1: + n = n[:cidx].rstrip() + var = -0.05 + # Distance with the company name. + ratio = ratcliff(name1, n, sm1) + var + if ratio >= RO_THRESHOLD: + if resd.has_key(i): + if ratio > resd[i][0]: resd[i] = (ratio, + (i, analyze_company_name(o_name))) + else: + resd[i] = (ratio, (i, analyze_company_name(o_name))) + res = resd.values() + res.sort() + res.reverse() + if results > 0: res[:] = res[:results] + return res + + +try: + from cutils import soundex +except ImportError: + _aux_logger.warn('Unable to import the cutils.soundex function.' + ' Searches of movie titles and person names will be' + ' a bit slower.') + + _translate = dict(B='1', C='2', D='3', F='1', G='2', J='2', K='2', L='4', + M='5', N='5', P='1', Q='2', R='6', S='2', T='3', V='1', + X='2', Z='2') + _translateget = _translate.get + _re_non_ascii = re.compile(r'^[^a-z]*', re.I) + SOUNDEX_LEN = 5 + + def soundex(s): + """Return the soundex code for the given string.""" + # Maximum length of the soundex code. + s = _re_non_ascii.sub('', s) + if not s: return None + s = s.upper() + soundCode = s[0] + for c in s[1:]: + cw = _translateget(c, '0') + if cw != '0' and soundCode[-1] != cw: + soundCode += cw + return soundCode[:SOUNDEX_LEN] or None + + +def _sortKeywords(keyword, kwds): + """Sort a list of keywords, based on the searched one.""" + sm = SequenceMatcher() + sm.set_seq1(keyword.lower()) + ratios = [(ratcliff(keyword, k, sm), k) for k in kwds] + checkContained = False + if len(keyword) > 4: + checkContained = True + for idx, data in enumerate(ratios): + ratio, key = data + if key.startswith(keyword): + ratios[idx] = (ratio+0.5, key) + elif checkContained and keyword in key: + ratios[idx] = (ratio+0.3, key) + ratios.sort() + ratios.reverse() + return [r[1] for r in ratios] + + +def filterSimilarKeywords(keyword, kwdsIterator): + """Return a sorted list of keywords similar to the one given.""" + seenDict = {} + kwdSndx = soundex(keyword.encode('ascii', 'ignore')) + matches = [] + matchesappend = matches.append + checkContained = False + if len(keyword) > 4: + checkContained = True + for movieID, key in kwdsIterator: + if key in seenDict: + continue + seenDict[key] = None + if checkContained and keyword in key: + matchesappend(key) + continue + if kwdSndx == soundex(key.encode('ascii', 'ignore')): + matchesappend(key) + return _sortKeywords(keyword, matches) + + + +# ============================= + +_litlist = ['screenplay/teleplay', 'novel', 'adaption', 'book', + 'production process protocol', 'interviews', + 'printed media reviews', 'essays', 'other literature'] +_litd = dict([(x, ('literature', x)) for x in _litlist]) + +_buslist = ['budget', 'weekend gross', 'gross', 'opening weekend', 'rentals', + 'admissions', 'filming dates', 'production dates', 'studios', + 'copyright holder'] +_busd = dict([(x, ('business', x)) for x in _buslist]) + + +def _reGroupDict(d, newgr): + """Regroup keys in the d dictionary in subdictionaries, based on + the scheme in the newgr dictionary. + E.g.: in the newgr, an entry 'LD label': ('laserdisc', 'label') + tells the _reGroupDict() function to take the entry with + label 'LD label' (as received from the sql database) + and put it in the subsection (another dictionary) named + 'laserdisc', using the key 'label'.""" + r = {} + newgrks = newgr.keys() + for k, v in d.items(): + if k in newgrks: + r.setdefault(newgr[k][0], {})[newgr[k][1]] = v + # A not-so-clearer version: + ##r.setdefault(newgr[k][0], {}) + ##r[newgr[k][0]][newgr[k][1]] = v + else: r[k] = v + return r + + +def _groupListBy(l, index): + """Regroup items in a list in a list of lists, grouped by + the value at the given index.""" + tmpd = {} + for item in l: + tmpd.setdefault(item[index], []).append(item) + res = tmpd.values() + return res + + +def sub_dict(d, keys): + """Return the subdictionary of 'd', with just the keys listed in 'keys'.""" + return dict([(k, d[k]) for k in keys if k in d]) + + +def get_movie_data(movieID, kindDict, fromAka=0, _table=None): + """Return a dictionary containing data about the given movieID; + if fromAka is true, the AkaTitle table is searched; _table is + reserved for the imdbpy2sql.py script.""" + if _table is not None: + Table = _table + else: + if not fromAka: Table = Title + else: Table = AkaTitle + m = Table.get(movieID) + mdict = {'title': m.title, 'kind': kindDict[m.kindID], + 'year': m.productionYear, 'imdbIndex': m.imdbIndex, + 'season': m.seasonNr, 'episode': m.episodeNr} + if not fromAka: + if m.seriesYears is not None: + mdict['series years'] = unicode(m.seriesYears) + if mdict['imdbIndex'] is None: del mdict['imdbIndex'] + if mdict['year'] is None: del mdict['year'] + else: + try: + mdict['year'] = int(mdict['year']) + except (TypeError, ValueError): + del mdict['year'] + if mdict['season'] is None: del mdict['season'] + else: + try: mdict['season'] = int(mdict['season']) + except: pass + if mdict['episode'] is None: del mdict['episode'] + else: + try: mdict['episode'] = int(mdict['episode']) + except: pass + episodeOfID = m.episodeOfID + if episodeOfID is not None: + ser_dict = get_movie_data(episodeOfID, kindDict, fromAka) + mdict['episode of'] = Movie(data=ser_dict, movieID=episodeOfID, + accessSystem='sql') + if fromAka: + ser_note = AkaTitle.get(episodeOfID).note + if ser_note: + mdict['episode of'].notes = ser_note + return mdict + + +def _iterKeywords(results): + """Iterate over (key.id, key.keyword) columns of a selection of + the Keyword table.""" + for key in results: + yield key.id, key.keyword + + +def getSingleInfo(table, movieID, infoType, notAList=False): + """Return a dictionary in the form {infoType: infoListOrString}, + retrieving a single set of information about a given movie, from + the specified table.""" + infoTypeID = InfoType.select(InfoType.q.info == infoType) + if infoTypeID.count() == 0: + return {} + res = table.select(AND(table.q.movieID == movieID, + table.q.infoTypeID == infoTypeID[0].id)) + retList = [] + for r in res: + info = r.info + note = r.note + if note: + info += u'::%s' % note + retList.append(info) + if not retList: + return {} + if not notAList: return {infoType: retList} + else: return {infoType: retList[0]} + + +def _cmpTop(a, b, what='top 250 rank'): + """Compare function used to sort top 250/bottom 10 rank.""" + av = int(a[1].get(what)) + bv = int(b[1].get(what)) + if av == bv: + return 0 + return (-1, 1)[av > bv] + +def _cmpBottom(a, b): + """Compare function used to sort top 250/bottom 10 rank.""" + return _cmpTop(a, b, what='bottom 10 rank') + + +class IMDbSqlAccessSystem(IMDbBase): + """The class used to access IMDb's data through a SQL database.""" + + accessSystem = 'sql' + _sql_logger = logging.getLogger('imdbpy.parser.sql') + + def __init__(self, uri, adultSearch=1, useORM=None, *arguments, **keywords): + """Initialize the access system.""" + IMDbBase.__init__(self, *arguments, **keywords) + if useORM is None: + useORM = ('sqlobject', 'sqlalchemy') + if not isinstance(useORM, (tuple, list)): + if ',' in useORM: + useORM = useORM.split(',') + else: + useORM = [useORM] + self.useORM = useORM + nrMods = len(useORM) + _gotError = False + DB_TABLES = [] + for idx, mod in enumerate(useORM): + mod = mod.strip().lower() + try: + if mod == 'sqlalchemy': + from alchemyadapter import getDBTables, NotFoundError, \ + setConnection, AND, OR, IN, \ + ISNULL, CONTAINSSTRING, toUTF8 + elif mod == 'sqlobject': + from objectadapter import getDBTables, NotFoundError, \ + setConnection, AND, OR, IN, \ + ISNULL, CONTAINSSTRING, toUTF8 + else: + self._sql_logger.warn('unknown module "%s"' % mod) + continue + self._sql_logger.info('using %s ORM', mod) + # XXX: look ma'... black magic! It's used to make + # TableClasses and some functions accessible + # through the whole module. + for k, v in [('NotFoundError', NotFoundError), + ('AND', AND), ('OR', OR), ('IN', IN), + ('ISNULL', ISNULL), + ('CONTAINSSTRING', CONTAINSSTRING)]: + globals()[k] = v + self.toUTF8 = toUTF8 + DB_TABLES = getDBTables(uri) + for t in DB_TABLES: + globals()[t._imdbpyName] = t + if _gotError: + self._sql_logger.warn('falling back to "%s"' % mod) + break + except ImportError, e: + if idx+1 >= nrMods: + raise IMDbError('unable to use any ORM in %s: %s' % ( + str(useORM), str(e))) + else: + self._sql_logger.warn('unable to use "%s": %s' % (mod, + str(e))) + _gotError = True + continue + else: + raise IMDbError('unable to use any ORM in %s' % str(useORM)) + # Set the connection to the database. + self._sql_logger.debug('connecting to %s', uri) + try: + self._connection = setConnection(uri, DB_TABLES) + except AssertionError, e: + raise IMDbDataAccessError( \ + 'unable to connect to the database server; ' + \ + 'complete message: "%s"' % str(e)) + self.Error = self._connection.module.Error + # Maps some IDs to the corresponding strings. + self._kind = {} + self._kindRev = {} + self._sql_logger.debug('reading constants from the database') + try: + for kt in KindType.select(): + self._kind[kt.id] = kt.kind + self._kindRev[str(kt.kind)] = kt.id + except self.Error: + # NOTE: you can also get the error, but - at least with + # MySQL - it also contains the password, and I don't + # like the idea to print it out. + raise IMDbDataAccessError( \ + 'unable to connect to the database server') + self._role = {} + for rl in RoleType.select(): + self._role[rl.id] = str(rl.role) + self._info = {} + self._infoRev = {} + for inf in InfoType.select(): + self._info[inf.id] = str(inf.info) + self._infoRev[str(inf.info)] = inf.id + self._compType = {} + for cType in CompanyType.select(): + self._compType[cType.id] = cType.kind + info = [(it.id, it.info) for it in InfoType.select()] + self._compcast = {} + for cc in CompCastType.select(): + self._compcast[cc.id] = str(cc.kind) + self._link = {} + for lt in LinkType.select(): + self._link[lt.id] = str(lt.link) + self._moviesubs = {} + # Build self._moviesubs, a dictionary used to rearrange + # the data structure for a movie object. + for vid, vinfo in info: + if not vinfo.startswith('LD '): continue + self._moviesubs[vinfo] = ('laserdisc', vinfo[3:]) + self._moviesubs.update(_litd) + self._moviesubs.update(_busd) + self.do_adult_search(adultSearch) + + def _findRefs(self, o, trefs, nrefs): + """Find titles or names references in strings.""" + if isinstance(o, (unicode, str)): + for title in re_titleRef.findall(o): + a_title = analyze_title(title, canonical=0) + rtitle = build_title(a_title, ptdf=1) + if trefs.has_key(rtitle): continue + movieID = self._getTitleID(rtitle) + if movieID is None: + movieID = self._getTitleID(title) + if movieID is None: + continue + m = Movie(title=rtitle, movieID=movieID, + accessSystem=self.accessSystem) + trefs[rtitle] = m + rtitle2 = canonicalTitle(a_title.get('title', u'')) + if rtitle2 and rtitle2 != rtitle and rtitle2 != title: + trefs[rtitle2] = m + if title != rtitle: + trefs[title] = m + for name in re_nameRef.findall(o): + a_name = analyze_name(name, canonical=1) + rname = build_name(a_name, canonical=1) + if nrefs.has_key(rname): continue + personID = self._getNameID(rname) + if personID is None: + personID = self._getNameID(name) + if personID is None: continue + p = Person(name=rname, personID=personID, + accessSystem=self.accessSystem) + nrefs[rname] = p + rname2 = normalizeName(a_name.get('name', u'')) + if rname2 and rname2 != rname: + nrefs[rname2] = p + if name != rname and name != rname2: + nrefs[name] = p + elif isinstance(o, (list, tuple)): + for item in o: + self._findRefs(item, trefs, nrefs) + elif isinstance(o, dict): + for value in o.values(): + self._findRefs(value, trefs, nrefs) + return (trefs, nrefs) + + def _extractRefs(self, o): + """Scan for titles or names references in strings.""" + trefs = {} + nrefs = {} + try: + return self._findRefs(o, trefs, nrefs) + except RuntimeError, e: + # Symbian/python 2.2 has a poor regexp implementation. + import warnings + warnings.warn('RuntimeError in ' + "imdb.parser.sql.IMDbSqlAccessSystem; " + "if it's not a recursion limit exceeded and we're not " + "running in a Symbian environment, it's a bug:\n%s" % e) + return (trefs, nrefs) + + def _changeAKAencoding(self, akanotes, akatitle): + """Return akatitle in the correct charset, as specified in + the akanotes field; if akatitle doesn't need to be modified, + return None.""" + oti = akanotes.find('(original ') + if oti == -1: return None + ote = akanotes[oti+10:].find(' title)') + if ote != -1: + cs_info = akanotes[oti+10:oti+10+ote].lower().split() + for e in cs_info: + # excludes some strings that clearly are not encoding. + if e in ('script', '', 'cyrillic', 'greek'): continue + if e.startswith('iso-') and e.find('latin') != -1: + e = e[4:].replace('-', '') + try: + lookup(e) + lat1 = akatitle.encode('latin_1', 'replace') + return unicode(lat1, e, 'replace') + except (LookupError, ValueError, TypeError): + continue + return None + + def _buildNULLCondition(self, col, val): + """Build a comparison for columns where values can be NULL.""" + if val is None: + return ISNULL(col) + else: + if isinstance(val, (int, long)): + return col == val + else: + return col == self.toUTF8(val) + + def _getTitleID(self, title): + """Given a long imdb canonical title, returns a movieID or + None if not found.""" + td = analyze_title(title) + condition = None + if td['kind'] == 'episode': + epof = td['episode of'] + seriesID = [s.id for s in Title.select( + AND(Title.q.title == self.toUTF8(epof['title']), + self._buildNULLCondition(Title.q.imdbIndex, + epof.get('imdbIndex')), + Title.q.kindID == self._kindRev[epof['kind']], + self._buildNULLCondition(Title.q.productionYear, + epof.get('year'))))] + if seriesID: + condition = AND(IN(Title.q.episodeOfID, seriesID), + Title.q.title == self.toUTF8(td['title']), + self._buildNULLCondition(Title.q.imdbIndex, + td.get('imdbIndex')), + Title.q.kindID == self._kindRev[td['kind']], + self._buildNULLCondition(Title.q.productionYear, + td.get('year'))) + if condition is None: + condition = AND(Title.q.title == self.toUTF8(td['title']), + self._buildNULLCondition(Title.q.imdbIndex, + td.get('imdbIndex')), + Title.q.kindID == self._kindRev[td['kind']], + self._buildNULLCondition(Title.q.productionYear, + td.get('year'))) + res = Title.select(condition) + try: + if res.count() != 1: + return None + except (UnicodeDecodeError, TypeError): + return None + return res[0].id + + def _getNameID(self, name): + """Given a long imdb canonical name, returns a personID or + None if not found.""" + nd = analyze_name(name) + res = Name.select(AND(Name.q.name == self.toUTF8(nd['name']), + self._buildNULLCondition(Name.q.imdbIndex, + nd.get('imdbIndex')))) + try: + c = res.count() + if res.count() != 1: + return None + except (UnicodeDecodeError, TypeError): + return None + return res[0].id + + def _normalize_movieID(self, movieID): + """Normalize the given movieID.""" + try: + return int(movieID) + except (ValueError, OverflowError): + raise IMDbError('movieID "%s" can\'t be converted to integer' % \ + movieID) + + def _normalize_personID(self, personID): + """Normalize the given personID.""" + try: + return int(personID) + except (ValueError, OverflowError): + raise IMDbError('personID "%s" can\'t be converted to integer' % \ + personID) + + def _normalize_characterID(self, characterID): + """Normalize the given characterID.""" + try: + return int(characterID) + except (ValueError, OverflowError): + raise IMDbError('characterID "%s" can\'t be converted to integer' \ + % characterID) + + def _normalize_companyID(self, companyID): + """Normalize the given companyID.""" + try: + return int(companyID) + except (ValueError, OverflowError): + raise IMDbError('companyID "%s" can\'t be converted to integer' \ + % companyID) + + def get_imdbMovieID(self, movieID): + """Translate a movieID in an imdbID. + If not in the database, try an Exact Primary Title search on IMDb; + return None if it's unable to get the imdbID. + """ + try: movie = Title.get(movieID) + except NotFoundError: return None + imdbID = movie.imdbID + if imdbID is not None: return '%07d' % imdbID + m_dict = get_movie_data(movie.id, self._kind) + titline = build_title(m_dict, ptdf=1) + imdbID = self.title2imdbID(titline) + # If the imdbID was retrieved from the web and was not in the + # database, update the database (ignoring errors, because it's + # possibile that the current user has not update privileges). + # There're times when I think I'm a genius; this one of + # those times... <g> + if imdbID is not None: + try: movie.imdbID = int(imdbID) + except: pass + return imdbID + + def get_imdbPersonID(self, personID): + """Translate a personID in an imdbID. + If not in the database, try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID. + """ + try: person = Name.get(personID) + except NotFoundError: return None + imdbID = person.imdbID + if imdbID is not None: return '%07d' % imdbID + n_dict = {'name': person.name, 'imdbIndex': person.imdbIndex} + namline = build_name(n_dict, canonical=1) + imdbID = self.name2imdbID(namline) + if imdbID is not None: + try: person.imdbID = int(imdbID) + except: pass + return imdbID + + def get_imdbCharacterID(self, characterID): + """Translate a characterID in an imdbID. + If not in the database, try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID. + """ + try: character = CharName.get(characterID) + except NotFoundError: return None + imdbID = character.imdbID + if imdbID is not None: return '%07d' % imdbID + n_dict = {'name': character.name, 'imdbIndex': character.imdbIndex} + namline = build_name(n_dict, canonical=1) + imdbID = self.character2imdbID(namline) + if imdbID is not None: + try: character.imdbID = int(imdbID) + except: pass + return imdbID + + def get_imdbCompanyID(self, companyID): + """Translate a companyID in an imdbID. + If not in the database, try an Exact Primary Name search on IMDb; + return None if it's unable to get the imdbID. + """ + try: company = CompanyName.get(companyID) + except NotFoundError: return None + imdbID = company.imdbID + if imdbID is not None: return '%07d' % imdbID + n_dict = {'name': company.name, 'country': company.countryCode} + namline = build_company_name(n_dict) + imdbID = self.company2imdbID(namline) + if imdbID is not None: + try: company.imdbID = int(imdbID) + except: pass + return imdbID + + def do_adult_search(self, doAdult): + """If set to 0 or False, movies in the Adult category are not + episodeOf = title_dict.get('episode of') + shown in the results of a search.""" + self.doAdult = doAdult + + def _search_movie(self, title, results, _episodes=False): + title = title.strip() + if not title: return [] + title_dict = analyze_title(title, canonical=1) + s_title = title_dict['title'] + if not s_title: return [] + episodeOf = title_dict.get('episode of') + if episodeOf: + _episodes = False + s_title_split = s_title.split(', ') + if len(s_title_split) > 1 and \ + s_title_split[-1].lower() in _unicodeArticles: + s_title_rebuilt = ', '.join(s_title_split[:-1]) + if s_title_rebuilt: + s_title = s_title_rebuilt + #if not episodeOf: + # if not _episodes: + # s_title_split = s_title.split(', ') + # if len(s_title_split) > 1 and \ + # s_title_split[-1].lower() in _articles: + # s_title_rebuilt = ', '.join(s_title_split[:-1]) + # if s_title_rebuilt: + # s_title = s_title_rebuilt + #else: + # _episodes = False + if isinstance(s_title, unicode): + s_title = s_title.encode('ascii', 'ignore') + + soundexCode = soundex(s_title) + + # XXX: improve the search restricting the kindID if the + # "kind" of the input differs from "movie"? + condition = conditionAka = None + if _episodes: + condition = AND(Title.q.phoneticCode == soundexCode, + Title.q.kindID == self._kindRev['episode']) + conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, + AkaTitle.q.kindID == self._kindRev['episode']) + elif title_dict['kind'] == 'episode' and episodeOf is not None: + # set canonical=0 ? Should not make much difference. + series_title = build_title(episodeOf, canonical=1) + # XXX: is it safe to get "results" results? + # Too many? Too few? + serRes = results + if serRes < 3 or serRes > 10: + serRes = 10 + searchSeries = self._search_movie(series_title, serRes) + seriesIDs = [result[0] for result in searchSeries] + if seriesIDs: + condition = AND(Title.q.phoneticCode == soundexCode, + IN(Title.q.episodeOfID, seriesIDs), + Title.q.kindID == self._kindRev['episode']) + conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, + IN(AkaTitle.q.episodeOfID, seriesIDs), + AkaTitle.q.kindID == self._kindRev['episode']) + else: + # XXX: bad situation: we have found no matching series; + # try searching everything (both episodes and + # non-episodes) for the title. + condition = AND(Title.q.phoneticCode == soundexCode, + IN(Title.q.episodeOfID, seriesIDs)) + conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, + IN(AkaTitle.q.episodeOfID, seriesIDs)) + if condition is None: + # XXX: excludes episodes? + condition = AND(Title.q.kindID != self._kindRev['episode'], + Title.q.phoneticCode == soundexCode) + conditionAka = AND(AkaTitle.q.kindID != self._kindRev['episode'], + AkaTitle.q.phoneticCode == soundexCode) + + # Up to 3 variations of the title are searched, plus the + # long imdb canonical title, if provided. + if not _episodes: + title1, title2, title3 = titleVariations(title) + else: + title1 = title + title2 = '' + title3 = '' + try: + qr = [(q.id, get_movie_data(q.id, self._kind)) + for q in Title.select(condition)] + q2 = [(q.movieID, get_movie_data(q.id, self._kind, fromAka=1)) + for q in AkaTitle.select(conditionAka)] + qr += q2 + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to search the database: "%s"' % str(e)) + + resultsST = results * 3 + res = scan_titles(qr, title1, title2, title3, resultsST, + searchingEpisode=episodeOf is not None, + onlyEpisodes=_episodes, + ro_thresold=0.0) + res[:] = [x[1] for x in res] + + if res and not self.doAdult: + mids = [x[0] for x in res] + genreID = self._infoRev['genres'] + adultlist = [al.movieID for al + in MovieInfo.select( + AND(MovieInfo.q.infoTypeID == genreID, + MovieInfo.q.info == 'Adult', + IN(MovieInfo.q.movieID, mids)))] + res[:] = [x for x in res if x[0] not in adultlist] + + new_res = [] + # XXX: can there be duplicates? + for r in res: + if r not in q2: + new_res.append(r) + continue + mdict = r[1] + aka_title = build_title(mdict, ptdf=1) + orig_dict = get_movie_data(r[0], self._kind) + orig_title = build_title(orig_dict, ptdf=1) + if aka_title == orig_title: + new_res.append(r) + continue + orig_dict['akas'] = [aka_title] + new_res.append((r[0], orig_dict)) + if results > 0: new_res[:] = new_res[:results] + return new_res + + def _search_episode(self, title, results): + return self._search_movie(title, results, _episodes=True) + + def get_movie_main(self, movieID): + # Every movie information is retrieved from here. + infosets = self.get_movie_infoset() + try: + res = get_movie_data(movieID, self._kind) + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to get movieID "%s": "%s"' % (movieID, str(e))) + if not res: + raise IMDbDataAccessError('unable to get movieID "%s"' % movieID) + # Collect cast information. + castdata = [[cd.personID, cd.personRoleID, cd.note, cd.nrOrder, + self._role[cd.roleID]] + for cd in CastInfo.select(CastInfo.q.movieID == movieID)] + for p in castdata: + person = Name.get(p[0]) + p += [person.name, person.imdbIndex] + if p[4] in ('actor', 'actress'): + p[4] = 'cast' + # Regroup by role/duty (cast, writer, director, ...) + castdata[:] = _groupListBy(castdata, 4) + for group in castdata: + duty = group[0][4] + for pdata in group: + curRole = pdata[1] + curRoleID = None + if curRole is not None: + robj = CharName.get(curRole) + curRole = robj.name + curRoleID = robj.id + p = Person(personID=pdata[0], name=pdata[5], + currentRole=curRole or u'', + roleID=curRoleID, + notes=pdata[2] or u'', + accessSystem='sql') + if pdata[6]: p['imdbIndex'] = pdata[6] + p.billingPos = pdata[3] + res.setdefault(duty, []).append(p) + if duty == 'cast': + res[duty] = merge_roles(res[duty]) + res[duty].sort() + # Info about the movie. + minfo = [(self._info[m.infoTypeID], m.info, m.note) + for m in MovieInfo.select(MovieInfo.q.movieID == movieID)] + minfo += [(self._info[m.infoTypeID], m.info, m.note) + for m in MovieInfoIdx.select(MovieInfoIdx.q.movieID == movieID)] + minfo += [('keywords', Keyword.get(m.keywordID).keyword, None) + for m in MovieKeyword.select(MovieKeyword.q.movieID == movieID)] + minfo = _groupListBy(minfo, 0) + for group in minfo: + sect = group[0][0] + for mdata in group: + data = mdata[1] + if mdata[2]: data += '::%s' % mdata[2] + res.setdefault(sect, []).append(data) + # Companies info about a movie. + cinfo = [(self._compType[m.companyTypeID], m.companyID, m.note) for m + in MovieCompanies.select(MovieCompanies.q.movieID == movieID)] + cinfo = _groupListBy(cinfo, 0) + for group in cinfo: + sect = group[0][0] + for mdata in group: + cDb = CompanyName.get(mdata[1]) + cDbTxt = cDb.name + if cDb.countryCode: + cDbTxt += ' %s' % cDb.countryCode + company = Company(name=cDbTxt, + companyID=mdata[1], + notes=mdata[2] or u'', + accessSystem=self.accessSystem) + res.setdefault(sect, []).append(company) + # AKA titles. + akat = [(get_movie_data(at.id, self._kind, fromAka=1), at.note) + for at in AkaTitle.select(AkaTitle.q.movieID == movieID)] + if akat: + res['akas'] = [] + for td, note in akat: + nt = build_title(td, ptdf=1) + if note: + net = self._changeAKAencoding(note, nt) + if net is not None: nt = net + nt += '::%s' % note + if nt not in res['akas']: res['akas'].append(nt) + # Complete cast/crew. + compcast = [(self._compcast[cc.subjectID], self._compcast[cc.statusID]) + for cc in CompleteCast.select(CompleteCast.q.movieID == movieID)] + if compcast: + for entry in compcast: + val = unicode(entry[1]) + res[u'complete %s' % entry[0]] = val + # Movie connections. + mlinks = [[ml.linkedMovieID, self._link[ml.linkTypeID]] + for ml in MovieLink.select(MovieLink.q.movieID == movieID)] + if mlinks: + for ml in mlinks: + lmovieData = get_movie_data(ml[0], self._kind) + m = Movie(movieID=ml[0], data=lmovieData, accessSystem='sql') + ml[0] = m + res['connections'] = {} + mlinks[:] = _groupListBy(mlinks, 1) + for group in mlinks: + lt = group[0][1] + res['connections'][lt] = [i[0] for i in group] + # Episodes. + episodes = {} + eps_list = list(Title.select(Title.q.episodeOfID == movieID)) + eps_list.sort() + if eps_list: + ps_data = {'title': res['title'], 'kind': res['kind'], + 'year': res.get('year'), + 'imdbIndex': res.get('imdbIndex')} + parentSeries = Movie(movieID=movieID, data=ps_data, + accessSystem='sql') + for episode in eps_list: + episodeID = episode.id + episode_data = get_movie_data(episodeID, self._kind) + m = Movie(movieID=episodeID, data=episode_data, + accessSystem='sql') + m['episode of'] = parentSeries + season = episode_data.get('season', 'UNKNOWN') + if season not in episodes: episodes[season] = {} + ep_number = episode_data.get('episode') + if ep_number is None: + ep_number = max((episodes[season].keys() or [0])) + 1 + episodes[season][ep_number] = m + res['episodes'] = episodes + res['number of episodes'] = sum([len(x) for x in episodes.values()]) + res['number of seasons'] = len(episodes.keys()) + # Regroup laserdisc information. + res = _reGroupDict(res, self._moviesubs) + # Do some transformation to preserve consistency with other + # data access systems. + if 'quotes' in res: + for idx, quote in enumerate(res['quotes']): + res['quotes'][idx] = quote.split('::') + if 'runtimes' in res and len(res['runtimes']) > 0: + rt = res['runtimes'][0] + episodes = re_episodes.findall(rt) + if episodes: + res['runtimes'][0] = re_episodes.sub('', rt) + if res['runtimes'][0][-2:] == '::': + res['runtimes'][0] = res['runtimes'][0][:-2] + if 'votes' in res: + res['votes'] = int(res['votes'][0]) + if 'rating' in res: + res['rating'] = float(res['rating'][0]) + if 'votes distribution' in res: + res['votes distribution'] = res['votes distribution'][0] + if 'mpaa' in res: + res['mpaa'] = res['mpaa'][0] + if 'top 250 rank' in res: + try: res['top 250 rank'] = int(res['top 250 rank']) + except: pass + if 'bottom 10 rank' in res: + try: res['bottom 100 rank'] = int(res['bottom 10 rank']) + except: pass + del res['bottom 10 rank'] + for old, new in [('guest', 'guests'), ('trademarks', 'trade-mark'), + ('articles', 'article'), ('pictorials', 'pictorial'), + ('magazine-covers', 'magazine-cover-photo')]: + if old in res: + res[new] = res[old] + del res[old] + trefs,nrefs = {}, {} + trefs,nrefs = self._extractRefs(sub_dict(res,Movie.keys_tomodify_list)) + return {'data': res, 'titlesRefs': trefs, 'namesRefs': nrefs, + 'info sets': infosets} + + # Just to know what kind of information are available. + get_movie_alternate_versions = get_movie_main + get_movie_business = get_movie_main + get_movie_connections = get_movie_main + get_movie_crazy_credits = get_movie_main + get_movie_goofs = get_movie_main + get_movie_keywords = get_movie_main + get_movie_literature = get_movie_main + get_movie_locations = get_movie_main + get_movie_plot = get_movie_main + get_movie_quotes = get_movie_main + get_movie_release_dates = get_movie_main + get_movie_soundtrack = get_movie_main + get_movie_taglines = get_movie_main + get_movie_technical = get_movie_main + get_movie_trivia = get_movie_main + get_movie_vote_details = get_movie_main + get_movie_episodes = get_movie_main + + def _search_person(self, name, results): + name = name.strip() + if not name: return [] + s_name = analyze_name(name)['name'] + if not s_name: return [] + if isinstance(s_name, unicode): + s_name = s_name.encode('ascii', 'ignore') + soundexCode = soundex(s_name) + name1, name2, name3 = nameVariations(name) + + # If the soundex is None, compare only with the first + # phoneticCode column. + if soundexCode is not None: + condition = IN(soundexCode, [Name.q.namePcodeCf, + Name.q.namePcodeNf, + Name.q.surnamePcode]) + conditionAka = IN(soundexCode, [AkaName.q.namePcodeCf, + AkaName.q.namePcodeNf, + AkaName.q.surnamePcode]) + else: + condition = ISNULL(Name.q.namePcodeCf) + conditionAka = ISNULL(AkaName.q.namePcodeCf) + + try: + qr = [(q.id, {'name': q.name, 'imdbIndex': q.imdbIndex}) + for q in Name.select(condition)] + + q2 = [(q.personID, {'name': q.name, 'imdbIndex': q.imdbIndex}) + for q in AkaName.select(conditionAka)] + qr += q2 + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to search the database: "%s"' % str(e)) + + res = scan_names(qr, name1, name2, name3, results) + res[:] = [x[1] for x in res] + # Purge empty imdbIndex. + returnl = [] + for x in res: + tmpd = x[1] + if tmpd['imdbIndex'] is None: + del tmpd['imdbIndex'] + returnl.append((x[0], tmpd)) + + new_res = [] + # XXX: can there be duplicates? + for r in returnl: + if r not in q2: + new_res.append(r) + continue + pdict = r[1] + aka_name = build_name(pdict, canonical=1) + p = Name.get(r[0]) + orig_dict = {'name': p.name, 'imdbIndex': p.imdbIndex} + if orig_dict['imdbIndex'] is None: + del orig_dict['imdbIndex'] + orig_name = build_name(orig_dict, canonical=1) + if aka_name == orig_name: + new_res.append(r) + continue + orig_dict['akas'] = [aka_name] + new_res.append((r[0], orig_dict)) + if results > 0: new_res[:] = new_res[:results] + + return new_res + + def get_person_main(self, personID): + # Every person information is retrieved from here. + infosets = self.get_person_infoset() + try: + p = Name.get(personID) + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to get personID "%s": "%s"' % (personID, str(e))) + res = {'name': p.name, 'imdbIndex': p.imdbIndex} + if res['imdbIndex'] is None: del res['imdbIndex'] + if not res: + raise IMDbDataAccessError('unable to get personID "%s"' % personID) + # Collect cast information. + castdata = [(cd.movieID, cd.personRoleID, cd.note, + self._role[cd.roleID], + get_movie_data(cd.movieID, self._kind)) + for cd in CastInfo.select(CastInfo.q.personID == personID)] + # Regroup by role/duty (cast, writer, director, ...) + castdata[:] = _groupListBy(castdata, 3) + episodes = {} + seenDuties = [] + for group in castdata: + for mdata in group: + duty = orig_duty = group[0][3] + if duty not in seenDuties: seenDuties.append(orig_duty) + note = mdata[2] or u'' + if 'episode of' in mdata[4]: + duty = 'episodes' + if orig_duty not in ('actor', 'actress'): + if note: note = ' %s' % note + note = '[%s]%s' % (orig_duty, note) + curRole = mdata[1] + curRoleID = None + if curRole is not None: + robj = CharName.get(curRole) + curRole = robj.name + curRoleID = robj.id + m = Movie(movieID=mdata[0], data=mdata[4], + currentRole=curRole or u'', + roleID=curRoleID, + notes=note, accessSystem='sql') + if duty != 'episodes': + res.setdefault(duty, []).append(m) + else: + episodes.setdefault(m['episode of'], []).append(m) + if episodes: + for k in episodes: + episodes[k].sort() + episodes[k].reverse() + res['episodes'] = episodes + for duty in seenDuties: + if duty in res: + if duty in ('actor', 'actress', 'himself', 'herself', + 'themselves'): + res[duty] = merge_roles(res[duty]) + res[duty].sort() + # Info about the person. + pinfo = [(self._info[pi.infoTypeID], pi.info, pi.note) + for pi in PersonInfo.select(PersonInfo.q.personID == personID)] + # Regroup by duty. + pinfo = _groupListBy(pinfo, 0) + for group in pinfo: + sect = group[0][0] + for pdata in group: + data = pdata[1] + if pdata[2]: data += '::%s' % pdata[2] + res.setdefault(sect, []).append(data) + # AKA names. + akan = [(an.name, an.imdbIndex) + for an in AkaName.select(AkaName.q.personID == personID)] + if akan: + res['akas'] = [] + for n in akan: + nd = {'name': n[0]} + if n[1]: nd['imdbIndex'] = n[1] + nt = build_name(nd, canonical=1) + res['akas'].append(nt) + # Do some transformation to preserve consistency with other + # data access systems. + for key in ('birth date', 'birth notes', 'death date', 'death notes', + 'birth name', 'height'): + if key in res: + res[key] = res[key][0] + if 'guest' in res: + res['notable tv guest appearances'] = res['guest'] + del res['guest'] + miscnames = res.get('nick names', []) + if 'birth name' in res: miscnames.append(res['birth name']) + if 'akas' in res: + for mname in miscnames: + if mname in res['akas']: res['akas'].remove(mname) + if not res['akas']: del res['akas'] + trefs,nrefs = self._extractRefs(sub_dict(res,Person.keys_tomodify_list)) + return {'data': res, 'titlesRefs': trefs, 'namesRefs': nrefs, + 'info sets': infosets} + + # Just to know what kind of information are available. + get_person_filmography = get_person_main + get_person_biography = get_person_main + get_person_other_works = get_person_main + get_person_episodes = get_person_main + + def _search_character(self, name, results): + name = name.strip() + if not name: return [] + s_name = analyze_name(name)['name'] + if not s_name: return [] + if isinstance(s_name, unicode): + s_name = s_name.encode('ascii', 'ignore') + s_name = normalizeName(s_name) + soundexCode = soundex(s_name) + surname = s_name.split(' ')[-1] + surnameSoundex = soundex(surname) + name2 = '' + soundexName2 = None + nsplit = s_name.split() + if len(nsplit) > 1: + name2 = '%s %s' % (nsplit[-1], ' '.join(nsplit[:-1])) + if s_name == name2: + name2 = '' + else: + soundexName2 = soundex(name2) + # If the soundex is None, compare only with the first + # phoneticCode column. + if soundexCode is not None: + if soundexName2 is not None: + condition = OR(surnameSoundex == CharName.q.surnamePcode, + IN(CharName.q.namePcodeNf, [soundexCode, + soundexName2]), + IN(CharName.q.surnamePcode, [soundexCode, + soundexName2])) + else: + condition = OR(surnameSoundex == CharName.q.surnamePcode, + IN(soundexCode, [CharName.q.namePcodeNf, + CharName.q.surnamePcode])) + else: + condition = ISNULL(Name.q.namePcodeNf) + try: + qr = [(q.id, {'name': q.name, 'imdbIndex': q.imdbIndex}) + for q in CharName.select(condition)] + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to search the database: "%s"' % str(e)) + res = scan_names(qr, s_name, name2, '', results, + _scan_character=True) + res[:] = [x[1] for x in res] + # Purge empty imdbIndex. + returnl = [] + for x in res: + tmpd = x[1] + if tmpd['imdbIndex'] is None: + del tmpd['imdbIndex'] + returnl.append((x[0], tmpd)) + return returnl + + def get_character_main(self, characterID, results=1000): + # Every character information is retrieved from here. + infosets = self.get_character_infoset() + try: + c = CharName.get(characterID) + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to get characterID "%s": "%s"' % (characterID, e)) + res = {'name': c.name, 'imdbIndex': c.imdbIndex} + if res['imdbIndex'] is None: del res['imdbIndex'] + if not res: + raise IMDbDataAccessError('unable to get characterID "%s"' % \ + characterID) + # Collect filmography information. + items = CastInfo.select(CastInfo.q.personRoleID == characterID) + if results > 0: + items = items[:results] + filmodata = [(cd.movieID, cd.personID, cd.note, + get_movie_data(cd.movieID, self._kind)) for cd in items + if self._role[cd.roleID] in ('actor', 'actress')] + fdata = [] + for f in filmodata: + curRole = None + curRoleID = f[1] + note = f[2] or u'' + if curRoleID is not None: + robj = Name.get(curRoleID) + curRole = robj.name + m = Movie(movieID=f[0], data=f[3], + currentRole=curRole or u'', + roleID=curRoleID, roleIsPerson=True, + notes=note, accessSystem='sql') + fdata.append(m) + fdata = merge_roles(fdata) + fdata.sort() + if fdata: + res['filmography'] = fdata + return {'data': res, 'info sets': infosets} + + get_character_filmography = get_character_main + get_character_biography = get_character_main + + def _search_company(self, name, results): + name = name.strip() + if not name: return [] + if isinstance(name, unicode): + name = name.encode('ascii', 'ignore') + soundexCode = soundex(name) + # If the soundex is None, compare only with the first + # phoneticCode column. + if soundexCode is None: + condition = ISNULL(CompanyName.q.namePcodeNf) + else: + if name.endswith(']'): + condition = CompanyName.q.namePcodeSf == soundexCode + else: + condition = CompanyName.q.namePcodeNf == soundexCode + try: + qr = [(q.id, {'name': q.name, 'country': q.countryCode}) + for q in CompanyName.select(condition)] + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to search the database: "%s"' % str(e)) + qr[:] = [(x[0], build_company_name(x[1])) for x in qr] + res = scan_company_names(qr, name, results) + res[:] = [x[1] for x in res] + # Purge empty country keys. + returnl = [] + for x in res: + tmpd = x[1] + country = tmpd.get('country') + if country is None and 'country' in tmpd: + del tmpd['country'] + returnl.append((x[0], tmpd)) + return returnl + + def get_company_main(self, companyID, results=0): + # Every company information is retrieved from here. + infosets = self.get_company_infoset() + try: + c = CompanyName.get(companyID) + except NotFoundError, e: + raise IMDbDataAccessError( \ + 'unable to get companyID "%s": "%s"' % (companyID, e)) + res = {'name': c.name, 'country': c.countryCode} + if res['country'] is None: del res['country'] + if not res: + raise IMDbDataAccessError('unable to get companyID "%s"' % \ + companyID) + # Collect filmography information. + items = MovieCompanies.select(MovieCompanies.q.companyID == companyID) + if results > 0: + items = items[:results] + filmodata = [(cd.movieID, cd.companyID, + self._compType[cd.companyTypeID], cd.note, + get_movie_data(cd.movieID, self._kind)) for cd in items] + filmodata = _groupListBy(filmodata, 2) + for group in filmodata: + ctype = group[0][2] + for movieID, companyID, ctype, note, movieData in group: + movie = Movie(data=movieData, movieID=movieID, + notes=note or u'', accessSystem=self.accessSystem) + res.setdefault(ctype, []).append(movie) + res.get(ctype, []).sort() + return {'data': res, 'info sets': infosets} + + def _search_keyword(self, keyword, results): + constr = OR(Keyword.q.phoneticCode == + soundex(keyword.encode('ascii', 'ignore')), + CONTAINSSTRING(Keyword.q.keyword, self.toUTF8(keyword))) + return filterSimilarKeywords(keyword, + _iterKeywords(Keyword.select(constr)))[:results] + + def _get_keyword(self, keyword, results): + keyID = Keyword.select(Keyword.q.keyword == keyword) + if keyID.count() == 0: + return [] + keyID = keyID[0].id + movies = MovieKeyword.select(MovieKeyword.q.keywordID == + keyID)[:results] + return [(m.movieID, get_movie_data(m.movieID, self._kind)) + for m in movies] + + def _get_top_bottom_movies(self, kind): + if kind == 'top': + kind = 'top 250 rank' + elif kind == 'bottom': + # Not a refuse: the plain text data files contains only + # the bottom 10 movies. + kind = 'bottom 10 rank' + else: + return [] + infoID = InfoType.select(InfoType.q.info == kind) + if infoID.count() == 0: + return [] + infoID = infoID[0].id + movies = MovieInfoIdx.select(MovieInfoIdx.q.infoTypeID == infoID) + ml = [] + for m in movies: + minfo = get_movie_data(m.movieID, self._kind) + for k in kind, 'votes', 'rating', 'votes distribution': + valueDict = getSingleInfo(MovieInfoIdx, m.movieID, + k, notAList=True) + if k in (kind, 'votes') and k in valueDict: + valueDict[k] = int(valueDict[k]) + elif k == 'rating' and k in valueDict: + valueDict[k] = float(valueDict[k]) + minfo.update(valueDict) + ml.append((m.movieID, minfo)) + sorter = (_cmpBottom, _cmpTop)[kind == 'top 250 rank'] + ml.sort(sorter) + return ml + + def __del__(self): + """Ensure that the connection is closed.""" + if not hasattr(self, '_connection'): return + self._sql_logger.debug('closing connection to the database') + self._connection.close() + diff --git a/lib/imdb/parser/sql/alchemyadapter.py b/lib/imdb/parser/sql/alchemyadapter.py new file mode 100644 index 0000000000000000000000000000000000000000..9b5c79e49b2f59ef810c310273caa1835252a517 --- /dev/null +++ b/lib/imdb/parser/sql/alchemyadapter.py @@ -0,0 +1,508 @@ +""" +parser.sql.alchemyadapter module (imdb.parser.sql package). + +This module adapts the SQLAlchemy ORM to the internal mechanism. + +Copyright 2008-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import re +import sys +import logging +from sqlalchemy import * +from sqlalchemy import schema +try: from sqlalchemy import exc # 0.5 +except ImportError: from sqlalchemy import exceptions as exc # 0.4 + +_alchemy_logger = logging.getLogger('imdbpy.parser.sql.alchemy') + +try: + import migrate.changeset + HAS_MC = True +except ImportError: + HAS_MC = False + _alchemy_logger.warn('Unable to import migrate.changeset: Foreign ' \ + 'Keys will not be created.') + +from imdb._exceptions import IMDbDataAccessError +from dbschema import * + +# Used to convert table and column names. +re_upper = re.compile(r'([A-Z])') + +# XXX: I'm not sure at all that this is the best method to connect +# to the database and bind that connection to every table. +metadata = MetaData() + +# Maps our placeholders to SQLAlchemy's column types. +MAP_COLS = { + INTCOL: Integer, + UNICODECOL: UnicodeText, + STRINGCOL: String +} + + +class NotFoundError(IMDbDataAccessError): + """Exception raised when Table.get(id) returns no value.""" + pass + + +def _renameTable(tname): + """Build the name of a table, as done by SQLObject.""" + tname = re_upper.sub(r'_\1', tname) + if tname.startswith('_'): + tname = tname[1:] + return tname.lower() + +def _renameColumn(cname): + """Build the name of a column, as done by SQLObject.""" + cname = cname.replace('ID', 'Id') + return _renameTable(cname) + + +class DNNameObj(object): + """Used to access table.sqlmeta.columns[column].dbName (a string).""" + def __init__(self, dbName): + self.dbName = dbName + + def __repr__(self): + return '<DNNameObj(dbName=%s) [id=%s]>' % (self.dbName, id(self)) + + +class DNNameDict(object): + """Used to access table.sqlmeta.columns (a dictionary).""" + def __init__(self, colMap): + self.colMap = colMap + + def __getitem__(self, key): + return DNNameObj(self.colMap[key]) + + def __repr__(self): + return '<DNNameDict(colMap=%s) [id=%s]>' % (self.colMap, id(self)) + + +class SQLMetaAdapter(object): + """Used to access table.sqlmeta (an object with .table, .columns and + .idName attributes).""" + def __init__(self, table, colMap=None): + self.table = table + if colMap is None: + colMap = {} + self.colMap = colMap + + def __getattr__(self, name): + if name == 'table': + return getattr(self.table, name) + if name == 'columns': + return DNNameDict(self.colMap) + if name == 'idName': + return self.colMap.get('id', 'id') + return None + + def __repr__(self): + return '<SQLMetaAdapter(table=%s, colMap=%s) [id=%s]>' % \ + (repr(self.table), repr(self.colMap), id(self)) + + +class QAdapter(object): + """Used to access table.q attribute (remapped to SQLAlchemy table.c).""" + def __init__(self, table, colMap=None): + self.table = table + if colMap is None: + colMap = {} + self.colMap = colMap + + def __getattr__(self, name): + try: return getattr(self.table.c, self.colMap[name]) + except KeyError, e: raise AttributeError("unable to get '%s'" % name) + + def __repr__(self): + return '<QAdapter(table=%s, colMap=%s) [id=%s]>' % \ + (repr(self.table), repr(self.colMap), id(self)) + + +class RowAdapter(object): + """Adapter for a SQLAlchemy RowProxy object.""" + def __init__(self, row, table, colMap=None): + self.row = row + # FIXME: it's OBSCENE that 'table' should be passed from + # TableAdapter through ResultAdapter only to land here, + # where it's used to directly update a row item. + self.table = table + if colMap is None: + colMap = {} + self.colMap = colMap + self.colMapKeys = colMap.keys() + + def __getattr__(self, name): + try: return getattr(self.row, self.colMap[name]) + except KeyError, e: raise AttributeError("unable to get '%s'" % name) + + def __setattr__(self, name, value): + # FIXME: I can't even think about how much performances suffer, + # for this horrible hack (and it's used so rarely...) + # For sure something like a "property" to map column names + # to getter/setter functions would be much better, but it's + # not possible (or at least not easy) to build them for a + # single instance. + if name in self.__dict__.get('colMapKeys', ()): + # Trying to update a value in the database. + row = self.__dict__['row'] + table = self.__dict__['table'] + colMap = self.__dict__['colMap'] + params = {colMap[name]: value} + table.update(table.c.id==row.id).execute(**params) + # XXX: minor bug: after a value is assigned with the + # 'rowAdapterInstance.colName = value' syntax, for some + # reason rowAdapterInstance.colName still returns the + # previous value (even if the database is updated). + # Fix it? I'm not even sure it's ever used. + return + # For every other attribute. + object.__setattr__(self, name, value) + + def __repr__(self): + return '<RowAdapter(row=%s, table=%s, colMap=%s) [id=%s]>' % \ + (repr(self.row), repr(self.table), repr(self.colMap), id(self)) + + +class ResultAdapter(object): + """Adapter for a SQLAlchemy ResultProxy object.""" + def __init__(self, result, table, colMap=None): + self.result = result + self.table = table + if colMap is None: + colMap = {} + self.colMap = colMap + + def count(self): + return len(self) + + def __len__(self): + # FIXME: why sqlite returns -1? (that's wrooong!) + if self.result.rowcount == -1: + return 0 + return self.result.rowcount + + def __getitem__(self, key): + res = list(self.result)[key] + if not isinstance(key, slice): + # A single item. + return RowAdapter(res, self.table, colMap=self.colMap) + else: + # A (possible empty) list of items. + return [RowAdapter(x, self.table, colMap=self.colMap) + for x in res] + + def __iter__(self): + for item in self.result: + yield RowAdapter(item, self.table, colMap=self.colMap) + + def __repr__(self): + return '<ResultAdapter(result=%s, table=%s, colMap=%s) [id=%s]>' % \ + (repr(self.result), repr(self.table), + repr(self.colMap), id(self)) + + +class TableAdapter(object): + """Adapter for a SQLAlchemy Table object, to mimic a SQLObject class.""" + def __init__(self, table, uri=None): + """Initialize a TableAdapter object.""" + self._imdbpySchema = table + self._imdbpyName = table.name + self.connectionURI = uri + self.colMap = {} + columns = [] + for col in table.cols: + # Column's paramters. + params = {'nullable': True} + params.update(col.params) + if col.name == 'id': + params['primary_key'] = True + if 'notNone' in params: + params['nullable'] = not params['notNone'] + del params['notNone'] + cname = _renameColumn(col.name) + self.colMap[col.name] = cname + colClass = MAP_COLS[col.kind] + colKindParams = {} + if 'length' in params: + colKindParams['length'] = params['length'] + del params['length'] + elif colClass is UnicodeText and col.index: + # XXX: limit length for UNICODECOLs that will have an index. + # this can result in name.name and title.title truncations! + colClass = Unicode + # Should work for most of the database servers. + length = 511 + if self.connectionURI: + if self.connectionURI.startswith('mysql'): + # To stay compatible with MySQL 4.x. + length = 255 + colKindParams['length'] = length + elif self._imdbpyName == 'PersonInfo' and col.name == 'info': + if self.connectionURI: + if self.connectionURI.startswith('ibm'): + # There are some entries longer than 32KB. + colClass = CLOB + # I really do hope that this space isn't wasted + # for each other shorter entry... <g> + colKindParams['length'] = 68*1024 + colKind = colClass(**colKindParams) + if 'alternateID' in params: + # There's no need to handle them here. + del params['alternateID'] + # Create a column. + colObj = Column(cname, colKind, **params) + columns.append(colObj) + self.tableName = _renameTable(table.name) + # Create the table. + self.table = Table(self.tableName, metadata, *columns) + self._ta_insert = self.table.insert() + self._ta_select = self.table.select + # Adapters for special attributes. + self.q = QAdapter(self.table, colMap=self.colMap) + self.sqlmeta = SQLMetaAdapter(self.table, colMap=self.colMap) + + def select(self, conditions=None): + """Return a list of results.""" + result = self._ta_select(conditions).execute() + return ResultAdapter(result, self.table, colMap=self.colMap) + + def get(self, theID): + """Get an object given its ID.""" + result = self.select(self.table.c.id == theID) + #if not result: + # raise NotFoundError, 'no data for ID %s' % theID + # FIXME: isn't this a bit risky? We can't check len(result), + # because sqlite returns -1... + # What about converting it to a list and getting the first item? + try: + return result[0] + except KeyError: + raise NotFoundError('no data for ID %s' % theID) + + def dropTable(self, checkfirst=True): + """Drop the table.""" + dropParams = {'checkfirst': checkfirst} + # Guess what? Another work-around for a ibm_db bug. + if self.table.bind.engine.url.drivername.startswith('ibm_db'): + del dropParams['checkfirst'] + try: + self.table.drop(**dropParams) + except exc.ProgrammingError: + # As above: re-raise the exception, but only if it's not ibm_db. + if not self.table.bind.engine.url.drivername.startswith('ibm_db'): + raise + + def createTable(self, checkfirst=True): + """Create the table.""" + self.table.create(checkfirst=checkfirst) + # Create indexes for alternateID columns (other indexes will be + # created later, at explicit request for performances reasons). + for col in self._imdbpySchema.cols: + if col.name == 'id': + continue + if col.params.get('alternateID', False): + self._createIndex(col, checkfirst=checkfirst) + + def _createIndex(self, col, checkfirst=True): + """Create an index for a given (schema) column.""" + # XXX: indexLen is ignored in SQLAlchemy, and that means that + # indexes will be over the whole 255 chars strings... + # NOTE: don't use a dot as a separator, or DB2 will do + # nasty things. + idx_name = '%s_%s' % (self.table.name, col.index or col.name) + if checkfirst: + for index in self.table.indexes: + if index.name == idx_name: + return + idx = Index(idx_name, getattr(self.table.c, self.colMap[col.name])) + # XXX: beware that exc.OperationalError can be raised, is some + # strange circumstances; that's why the index name doesn't + # follow the SQLObject convention, but includes the table name: + # sqlite, for example, expects index names to be unique at + # db-level. + try: + idx.create() + except exc.OperationalError, e: + _alchemy_logger.warn('Skipping creation of the %s.%s index: %s' % + (self.sqlmeta.table, col.name, e)) + + def addIndexes(self, ifNotExists=True): + """Create all required indexes.""" + for col in self._imdbpySchema.cols: + if col.index: + self._createIndex(col, checkfirst=ifNotExists) + + def addForeignKeys(self, mapTables, ifNotExists=True): + """Create all required foreign keys.""" + if not HAS_MC: + return + # It seems that there's no reason to prevent the creation of + # indexes for columns with FK constrains: if there's already + # an index, the FK index is not created. + countCols = 0 + for col in self._imdbpySchema.cols: + countCols += 1 + if not col.foreignKey: + continue + fks = col.foreignKey.split('.', 1) + foreignTableName = fks[0] + if len(fks) == 2: + foreignColName = fks[1] + else: + foreignColName = 'id' + foreignColName = mapTables[foreignTableName].colMap.get( + foreignColName, foreignColName) + thisColName = self.colMap.get(col.name, col.name) + thisCol = self.table.columns[thisColName] + foreignTable = mapTables[foreignTableName].table + foreignCol = getattr(foreignTable.c, foreignColName) + # Need to explicitly set an unique name, otherwise it will + # explode, if two cols points to the same table. + fkName = 'fk_%s_%s_%d' % (foreignTable.name, foreignColName, + countCols) + constrain = migrate.changeset.ForeignKeyConstraint([thisCol], + [foreignCol], + name=fkName) + try: + constrain.create() + except exc.OperationalError: + continue + + def __call__(self, *args, **kwds): + """To insert a new row with the syntax: TableClass(key=value, ...)""" + taArgs = {} + for key, value in kwds.items(): + taArgs[self.colMap.get(key, key)] = value + self._ta_insert.execute(*args, **taArgs) + + def __repr__(self): + return '<TableAdapter(table=%s) [id=%s]>' % (repr(self.table), id(self)) + + +# Module-level "cache" for SQLObject classes, to prevent +# "Table 'tableName' is already defined for this MetaData instance" errors, +# when two or more connections to the database are made. +# XXX: is this the best way to act? +TABLES_REPOSITORY = {} + +def getDBTables(uri=None): + """Return a list of TableAdapter objects to be used to access the + database through the SQLAlchemy ORM. The connection uri is optional, and + can be used to tailor the db schema to specific needs.""" + DB_TABLES = [] + for table in DB_SCHEMA: + if table.name in TABLES_REPOSITORY: + DB_TABLES.append(TABLES_REPOSITORY[table.name]) + continue + tableAdapter = TableAdapter(table, uri) + DB_TABLES.append(tableAdapter) + TABLES_REPOSITORY[table.name] = tableAdapter + return DB_TABLES + + +# Functions used to emulate SQLObject's logical operators. +def AND(*params): + """Emulate SQLObject's AND.""" + return and_(*params) + +def OR(*params): + """Emulate SQLObject's OR.""" + return or_(*params) + +def IN(item, inList): + """Emulate SQLObject's IN.""" + if not isinstance(item, schema.Column): + return OR(*[x == item for x in inList]) + else: + return item.in_(inList) + +def ISNULL(x): + """Emulate SQLObject's ISNULL.""" + # XXX: Should we use null()? Can null() be a global instance? + # XXX: Is it safe to test None with the == operator, in this case? + return x == None + +def ISNOTNULL(x): + """Emulate SQLObject's ISNOTNULL.""" + return x != None + +def CONTAINSSTRING(expr, pattern): + """Emulate SQLObject's CONTAINSSTRING.""" + return expr.like('%%%s%%' % pattern) + + +def toUTF8(s): + """For some strange reason, sometimes SQLObject wants utf8 strings + instead of unicode; with SQLAlchemy we just return the unicode text.""" + return s + + +class _AlchemyConnection(object): + """A proxy for the connection object, required since _ConnectionFairy + uses __slots__.""" + def __init__(self, conn): + self.conn = conn + + def __getattr__(self, name): + return getattr(self.conn, name) + + +def setConnection(uri, tables, encoding='utf8', debug=False): + """Set connection for every table.""" + # FIXME: why on earth MySQL requires an additional parameter, + # is well beyond my understanding... + if uri.startswith('mysql'): + if '?' in uri: + uri += '&' + else: + uri += '?' + uri += 'charset=%s' % encoding + params = {'encoding': encoding} + if debug: + params['echo'] = True + if uri.startswith('ibm_db'): + # Try to work-around a possible bug of the ibm_db DB2 driver. + params['convert_unicode'] = True + # XXX: is this the best way to connect? + engine = create_engine(uri, **params) + metadata.bind = engine + eng_conn = engine.connect() + if uri.startswith('sqlite'): + major = sys.version_info[0] + minor = sys.version_info[1] + if major > 2 or (major == 2 and minor > 5): + eng_conn.connection.connection.text_factory = str + # XXX: OH MY, THAT'S A MESS! + # We need to return a "connection" object, with the .dbName + # attribute set to the db engine name (e.g. "mysql"), .paramstyle + # set to the style of the paramters for query() calls, and the + # .module attribute set to a module (?) with .OperationalError and + # .IntegrityError attributes. + # Another attribute of "connection" is the getConnection() function, + # used to return an object with a .cursor() method. + connection = _AlchemyConnection(eng_conn.connection) + paramstyle = eng_conn.dialect.paramstyle + connection.module = eng_conn.dialect.dbapi + connection.paramstyle = paramstyle + connection.getConnection = lambda: connection.connection + connection.dbName = engine.url.drivername + return connection + + diff --git a/lib/imdb/parser/sql/cutils.c b/lib/imdb/parser/sql/cutils.c new file mode 100644 index 0000000000000000000000000000000000000000..677c1b1e0a039bfa38c989b612236abd880b99a1 --- /dev/null +++ b/lib/imdb/parser/sql/cutils.c @@ -0,0 +1,269 @@ +/* + * cutils.c module. + * + * Miscellaneous functions to speed up the IMDbPY package. + * + * Contents: + * - pyratcliff(): + * Function that implements the Ratcliff-Obershelp comparison + * amongst Python strings. + * + * - pysoundex(): + * Return a soundex code string, for the given string. + * + * Copyright 2004-2009 Davide Alberani <da@erlug.linux.it> + * Released under the GPL license. + * + * NOTE: The Ratcliff-Obershelp part was heavily based on code from the + * "simil" Python module. + * The "simil" module is copyright of Luca Montecchiani <cbm64 _at_ inwind.it> + * and can be found here: http://spazioinwind.libero.it/montecchiani/ + * It was released under the GPL license; original comments are leaved + * below. + * + */ + + +/*========== Ratcliff-Obershelp ==========*/ +/***************************************************************************** + * + * Stolen code from : + * + * [Python-Dev] Why is soundex marked obsolete? + * by Eric S. Raymond [4]esr@thyrsus.com + * on Sun, 14 Jan 2001 14:09:01 -0500 + * + *****************************************************************************/ + +/***************************************************************************** + * + * Ratcliff-Obershelp common-subpattern similarity. + * + * This code first appeared in a letter to the editor in Doctor + * Dobbs's Journal, 11/1988. The original article on the algorithm, + * "Pattern Matching by Gestalt" by John Ratcliff, had appeared in the + * July 1988 issue (#181) but the algorithm was presented in assembly. + * The main drawback of the Ratcliff-Obershelp algorithm is the cost + * of the pairwise comparisons. It is significantly more expensive + * than stemming, Hamming distance, soundex, and the like. + * + * Running time quadratic in the data size, memory usage constant. + * + *****************************************************************************/ + +#include <Python.h> + +#define DONTCOMPARE_NULL 0.0 +#define DONTCOMPARE_SAME 1.0 +#define COMPARE 2.0 +#define STRING_MAXLENDIFFER 0.7 + +/* As of 05 Mar 2008, the longest title is ~600 chars. */ +#define MXLINELEN 1023 + +#define MAX(a,b) ((a) > (b) ? (a) : (b)) + + +//***************************************** +// preliminary check.... +//***************************************** +static float +strings_check(char const *s, char const *t) +{ + float threshold; // lenght difference + int s_len = strlen(s); // length of s + int t_len = strlen(t); // length of t + + // NULL strings ? + if ((t_len * s_len) == 0) + return (DONTCOMPARE_NULL); + + // the same ? + if (strcmp(s, t) == 0) + return (DONTCOMPARE_SAME); + + // string lenght difference threshold + // we don't want to compare too different lenght strings ;) + if (s_len < t_len) + threshold = (float) s_len / (float) t_len; + else + threshold = (float) t_len / (float) s_len; + if (threshold < STRING_MAXLENDIFFER) + return (DONTCOMPARE_NULL); + + // proceed + return (COMPARE); +} + + +static int +RatcliffObershelp(char *st1, char *end1, char *st2, char *end2) +{ + register char *a1, *a2; + char *b1, *b2; + char *s1 = st1, *s2 = st2; /* initializations are just to pacify GCC */ + short max, i; + + if (end1 <= st1 || end2 <= st2) + return (0); + if (end1 == st1 + 1 && end2 == st2 + 1) + return (0); + + max = 0; + b1 = end1; + b2 = end2; + + for (a1 = st1; a1 < b1; a1++) { + for (a2 = st2; a2 < b2; a2++) { + if (*a1 == *a2) { + /* determine length of common substring */ + for (i = 1; a1[i] && (a1[i] == a2[i]); i++) + continue; + if (i > max) { + max = i; + s1 = a1; + s2 = a2; + b1 = end1 - max; + b2 = end2 - max; + } + } + } + } + if (!max) + return (0); + max += RatcliffObershelp(s1 + max, end1, s2 + max, end2); /* rhs */ + max += RatcliffObershelp(st1, s1, st2, s2); /* lhs */ + return max; +} + + +static float +ratcliff(char *s1, char *s2) +/* compute Ratcliff-Obershelp similarity of two strings */ +{ + int l1, l2; + float res; + + // preliminary tests + res = strings_check(s1, s2); + if (res != COMPARE) + return(res); + + l1 = strlen(s1); + l2 = strlen(s2); + + return 2.0 * RatcliffObershelp(s1, s1 + l1, s2, s2 + l2) / (l1 + l2); +} + + +/* Change a string to lowercase. */ +static void +strtolower(char *s1) +{ + int i; + for (i=0; i < strlen(s1); i++) s1[i] = tolower(s1[i]); +} + + +/* Ratcliff-Obershelp for two python strings; returns a python float. */ +static PyObject* +pyratcliff(PyObject *self, PyObject *pArgs) +{ + char *s1 = NULL; + char *s2 = NULL; + PyObject *discard = NULL; + char s1copy[MXLINELEN+1]; + char s2copy[MXLINELEN+1]; + + /* The optional PyObject parameter is here to be compatible + * with the pure python implementation, which uses a + * difflib.SequenceMatcher object. */ + if (!PyArg_ParseTuple(pArgs, "ss|O", &s1, &s2, &discard)) + return NULL; + + strncpy(s1copy, s1, MXLINELEN); + strncpy(s2copy, s2, MXLINELEN); + /* Work on copies. */ + strtolower(s1copy); + strtolower(s2copy); + + return Py_BuildValue("f", ratcliff(s1copy, s2copy)); +} + + +/*========== soundex ==========*/ +/* Max length of the soundex code to output (an uppercase char and + * _at most_ 4 digits). */ +#define SOUNDEX_LEN 5 + +/* Group Number Lookup Table */ +static char soundTable[26] = +{ 0 /* A */, '1' /* B */, '2' /* C */, '3' /* D */, 0 /* E */, '1' /* F */, + '2' /* G */, 0 /* H */, 0 /* I */, '2' /* J */, '2' /* K */, '4' /* L */, + '5' /* M */, '5' /* N */, 0 /* O */, '1' /* P */, '2' /* Q */, '6' /* R */, + '2' /* S */, '3' /* T */, 0 /* U */, '1' /* V */, 0 /* W */, '2' /* X */, + 0 /* Y */, '2' /* Z */}; + +static PyObject* +pysoundex(PyObject *self, PyObject *pArgs) +{ + int i, j, n; + char *s = NULL; + char word[MXLINELEN+1]; + char soundCode[SOUNDEX_LEN+1]; + char c; + + if (!PyArg_ParseTuple(pArgs, "s", &s)) + return NULL; + + j = 0; + n = strlen(s); + + /* Convert to uppercase and exclude non-ascii chars. */ + for (i = 0; i < n; i++) { + c = toupper(s[i]); + if (c < 91 && c > 64) { + word[j] = c; + j++; + } + } + word[j] = '\0'; + + n = strlen(word); + if (n == 0) { + /* If the string is empty, returns None. */ + return Py_BuildValue(""); + } + soundCode[0] = word[0]; + + /* Build the soundCode string. */ + j = 1; + for (i = 1; j < SOUNDEX_LEN && i < n; i++) { + c = soundTable[(word[i]-65)]; + /* Compact zeroes and equal consecutive digits ("12234112"->"123412") */ + if (c != 0 && c != soundCode[j-1]) { + soundCode[j++] = c; + } + } + soundCode[j] = '\0'; + + return Py_BuildValue("s", soundCode); +} + + +static PyMethodDef cutils_methods[] = { + {"ratcliff", pyratcliff, + METH_VARARGS, "Ratcliff-Obershelp similarity."}, + {"soundex", pysoundex, + METH_VARARGS, "Soundex code for strings."}, + {NULL} +}; + + +void +initcutils(void) +{ + Py_InitModule("cutils", cutils_methods); +} + + diff --git a/lib/imdb/parser/sql/cutils.so b/lib/imdb/parser/sql/cutils.so new file mode 100644 index 0000000000000000000000000000000000000000..80246ffd19f5f796323f034f03f263b390710a1f Binary files /dev/null and b/lib/imdb/parser/sql/cutils.so differ diff --git a/lib/imdb/parser/sql/dbschema.py b/lib/imdb/parser/sql/dbschema.py new file mode 100644 index 0000000000000000000000000000000000000000..2f359fba72474cb6713cfb0a1a10ffabd02b8f59 --- /dev/null +++ b/lib/imdb/parser/sql/dbschema.py @@ -0,0 +1,476 @@ +#-*- encoding: utf-8 -*- +""" +parser.sql.dbschema module (imdb.parser.sql package). + +This module provides the schema used to describe the layout of the +database used by the imdb.parser.sql package; functions to create/drop +tables and indexes are also provided. + +Copyright 2005-2012 Davide Alberani <da@erlug.linux.it> + 2006 Giuseppe "Cowo" Corbelli <cowo --> lugbs.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import logging + +_dbschema_logger = logging.getLogger('imdbpy.parser.sql.dbschema') + + +# Placeholders for column types. +INTCOL = 1 +UNICODECOL = 2 +STRINGCOL = 3 +_strMap = {1: 'INTCOL', 2: 'UNICODECOL', 3: 'STRINGCOL'} + +class DBCol(object): + """Define column objects.""" + def __init__(self, name, kind, **params): + self.name = name + self.kind = kind + self.index = None + self.indexLen = None + # If not None, two notations are accepted: 'TableName' + # and 'TableName.ColName'; in the first case, 'id' is assumed + # as the name of the pointed column. + self.foreignKey = None + if 'index' in params: + self.index = params['index'] + del params['index'] + if 'indexLen' in params: + self.indexLen = params['indexLen'] + del params['indexLen'] + if 'foreignKey' in params: + self.foreignKey = params['foreignKey'] + del params['foreignKey'] + self.params = params + + def __str__(self): + """Class representation.""" + s = '<DBCol %s %s' % (self.name, _strMap[self.kind]) + if self.index: + s += ' INDEX' + if self.indexLen: + s += '[:%d]' % self.indexLen + if self.foreignKey: + s += ' FOREIGN' + if 'default' in self.params: + val = self.params['default'] + if val is not None: + val = '"%s"' % val + s += ' DEFAULT=%s' % val + for param in self.params: + if param == 'default': continue + s += ' %s' % param.upper() + s += '>' + return s + + def __repr__(self): + """Class representation.""" + s = '<DBCol(name="%s", %s' % (self.name, _strMap[self.kind]) + if self.index: + s += ', index="%s"' % self.index + if self.indexLen: + s += ', indexLen=%d' % self.indexLen + if self.foreignKey: + s += ', foreignKey="%s"' % self.foreignKey + for param in self.params: + val = self.params[param] + if isinstance(val, (unicode, str)): + val = u'"%s"' % val + s += ', %s=%s' % (param, val) + s += ')>' + return s + + +class DBTable(object): + """Define table objects.""" + def __init__(self, name, *cols, **kwds): + self.name = name + self.cols = cols + # Default values. + self.values = kwds.get('values', {}) + + def __str__(self): + """Class representation.""" + return '<DBTable %s (%d cols, %d values)>' % (self.name, + len(self.cols), sum([len(v) for v in self.values.values()])) + + def __repr__(self): + """Class representation.""" + s = '<DBTable(name="%s"' % self.name + col_s = ', '.join([repr(col).rstrip('>').lstrip('<') + for col in self.cols]) + if col_s: + s += ', %s' % col_s + if self.values: + s += ', values=%s' % self.values + s += ')>' + return s + + +# Default values to insert in some tables: {'column': (list, of, values, ...)} +kindTypeDefs = {'kind': ('movie', 'tv series', 'tv movie', 'video movie', + 'tv mini series', 'video game', 'episode')} +companyTypeDefs = {'kind': ('distributors', 'production companies', + 'special effects companies', 'miscellaneous companies')} +infoTypeDefs = {'info': ('runtimes', 'color info', 'genres', 'languages', + 'certificates', 'sound mix', 'tech info', 'countries', 'taglines', + 'keywords', 'alternate versions', 'crazy credits', 'goofs', + 'soundtrack', 'quotes', 'release dates', 'trivia', 'locations', + 'mini biography', 'birth notes', 'birth date', 'height', + 'death date', 'spouse', 'other works', 'birth name', + 'salary history', 'nick names', 'books', 'agent address', + 'biographical movies', 'portrayed in', 'where now', 'trade mark', + 'interviews', 'article', 'magazine cover photo', 'pictorial', + 'death notes', 'LD disc format', 'LD year', 'LD digital sound', + 'LD official retail price', 'LD frequency response', 'LD pressing plant', + 'LD length', 'LD language', 'LD review', 'LD spaciality', 'LD release date', + 'LD production country', 'LD contrast', 'LD color rendition', + 'LD picture format', 'LD video noise', 'LD video artifacts', + 'LD release country', 'LD sharpness', 'LD dynamic range', + 'LD audio noise', 'LD color information', 'LD group genre', + 'LD quality program', 'LD close captions-teletext-ld-g', + 'LD category', 'LD analog left', 'LD certification', + 'LD audio quality', 'LD video quality', 'LD aspect ratio', + 'LD analog right', 'LD additional information', + 'LD number of chapter stops', 'LD dialogue intellegibility', + 'LD disc size', 'LD master format', 'LD subtitles', + 'LD status of availablility', 'LD quality of source', + 'LD number of sides', 'LD video standard', 'LD supplement', + 'LD original title', 'LD sound encoding', 'LD number', 'LD label', + 'LD catalog number', 'LD laserdisc title', 'screenplay-teleplay', + 'novel', 'adaption', 'book', 'production process protocol', + 'printed media reviews', 'essays', 'other literature', 'mpaa', + 'plot', 'votes distribution', 'votes', 'rating', + 'production dates', 'copyright holder', 'filming dates', 'budget', + 'weekend gross', 'gross', 'opening weekend', 'rentals', + 'admissions', 'studios', 'top 250 rank', 'bottom 10 rank')} +compCastTypeDefs = {'kind': ('cast', 'crew', 'complete', 'complete+verified')} +linkTypeDefs = {'link': ('follows', 'followed by', 'remake of', 'remade as', + 'references', 'referenced in', 'spoofs', 'spoofed in', + 'features', 'featured in', 'spin off from', 'spin off', + 'version of', 'similar to', 'edited into', + 'edited from', 'alternate language version of', + 'unknown link')} +roleTypeDefs = {'role': ('actor', 'actress', 'producer', 'writer', + 'cinematographer', 'composer', 'costume designer', + 'director', 'editor', 'miscellaneous crew', + 'production designer', 'guest')} + +# Schema of tables in our database. +# XXX: Foreign keys can be used to create constrains between tables, +# but they create indexes in the database, and this +# means poor performances at insert-time. +DB_SCHEMA = [ + DBTable('Name', + # namePcodeCf is the soundex of the name in the canonical format. + # namePcodeNf is the soundex of the name in the normal format, if + # different from namePcodeCf. + # surnamePcode is the soundex of the surname, if different from the + # other two values. + + # The 'id' column is simply skipped by SQLObject (it's a default); + # the alternateID attribute here will be ignored by SQLAlchemy. + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), + DBCol('imdbIndex', UNICODECOL, length=12, default=None), + DBCol('imdbID', INTCOL, default=None, index='idx_imdb_id'), + DBCol('gender', STRINGCOL, length=1, default=None), + DBCol('namePcodeCf', STRINGCOL, length=5, default=None, + index='idx_pcodecf'), + DBCol('namePcodeNf', STRINGCOL, length=5, default=None, + index='idx_pcodenf'), + DBCol('surnamePcode', STRINGCOL, length=5, default=None, + index='idx_pcode'), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('CharName', + # namePcodeNf is the soundex of the name in the normal format. + # surnamePcode is the soundex of the surname, if different + # from namePcodeNf. + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), + DBCol('imdbIndex', UNICODECOL, length=12, default=None), + DBCol('imdbID', INTCOL, default=None), + DBCol('namePcodeNf', STRINGCOL, length=5, default=None, + index='idx_pcodenf'), + DBCol('surnamePcode', STRINGCOL, length=5, default=None, + index='idx_pcode'), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('CompanyName', + # namePcodeNf is the soundex of the name in the normal format. + # namePcodeSf is the soundex of the name plus the country code. + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), + DBCol('countryCode', UNICODECOL, length=255, default=None), + DBCol('imdbID', INTCOL, default=None), + DBCol('namePcodeNf', STRINGCOL, length=5, default=None, + index='idx_pcodenf'), + DBCol('namePcodeSf', STRINGCOL, length=5, default=None, + index='idx_pcodesf'), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('KindType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('kind', STRINGCOL, length=15, default=None, alternateID=True), + values=kindTypeDefs + ), + + DBTable('Title', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('title', UNICODECOL, notNone=True, + index='idx_title', indexLen=10), + DBCol('imdbIndex', UNICODECOL, length=12, default=None), + DBCol('kindID', INTCOL, notNone=True, foreignKey='KindType'), + DBCol('productionYear', INTCOL, default=None), + DBCol('imdbID', INTCOL, default=None, index="idx_imdb_id"), + DBCol('phoneticCode', STRINGCOL, length=5, default=None, + index='idx_pcode'), + DBCol('episodeOfID', INTCOL, default=None, index='idx_epof', + foreignKey='Title'), + DBCol('seasonNr', INTCOL, default=None, index="idx_season_nr"), + DBCol('episodeNr', INTCOL, default=None, index="idx_episode_nr"), + # Maximum observed length is 44; 49 can store 5 comma-separated + # year-year pairs. + DBCol('seriesYears', STRINGCOL, length=49, default=None), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('CompanyType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('kind', STRINGCOL, length=32, default=None, alternateID=True), + values=companyTypeDefs + ), + + DBTable('AkaName', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('personID', INTCOL, notNone=True, index='idx_person', + foreignKey='Name'), + DBCol('name', UNICODECOL, notNone=True), + DBCol('imdbIndex', UNICODECOL, length=12, default=None), + DBCol('namePcodeCf', STRINGCOL, length=5, default=None, + index='idx_pcodecf'), + DBCol('namePcodeNf', STRINGCOL, length=5, default=None, + index='idx_pcodenf'), + DBCol('surnamePcode', STRINGCOL, length=5, default=None, + index='idx_pcode'), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('AkaTitle', + # XXX: It's safer to set notNone to False, here. + # alias for akas are stored completely in the AkaTitle table; + # this means that episodes will set also a "tv series" alias name. + # Reading the aka-title.list file it looks like there are + # episode titles with aliases to different titles for both + # the episode and the series title, while for just the series + # there are no aliases. + # E.g.: + # aka title original title + # "Series, The" (2005) {The Episode} "Other Title" (2005) {Other Title} + # But there is no: + # "Series, The" (2005) "Other Title" (2005) + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_movieid', + foreignKey='Title'), + DBCol('title', UNICODECOL, notNone=True), + DBCol('imdbIndex', UNICODECOL, length=12, default=None), + DBCol('kindID', INTCOL, notNone=True, foreignKey='KindType'), + DBCol('productionYear', INTCOL, default=None), + DBCol('phoneticCode', STRINGCOL, length=5, default=None, + index='idx_pcode'), + DBCol('episodeOfID', INTCOL, default=None, index='idx_epof', + foreignKey='AkaTitle'), + DBCol('seasonNr', INTCOL, default=None), + DBCol('episodeNr', INTCOL, default=None), + DBCol('note', UNICODECOL, default=None), + DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') + ), + + DBTable('RoleType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('role', STRINGCOL, length=32, notNone=True, alternateID=True), + values=roleTypeDefs + ), + + DBTable('CastInfo', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('personID', INTCOL, notNone=True, index='idx_pid', + foreignKey='Name'), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('personRoleID', INTCOL, default=None, index='idx_cid', + foreignKey='CharName'), + DBCol('note', UNICODECOL, default=None), + DBCol('nrOrder', INTCOL, default=None), + DBCol('roleID', INTCOL, notNone=True, foreignKey='RoleType') + ), + + DBTable('CompCastType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('kind', STRINGCOL, length=32, notNone=True, alternateID=True), + values=compCastTypeDefs + ), + + DBTable('CompleteCast', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, index='idx_mid', foreignKey='Title'), + DBCol('subjectID', INTCOL, notNone=True, foreignKey='CompCastType'), + DBCol('statusID', INTCOL, notNone=True, foreignKey='CompCastType') + ), + + DBTable('InfoType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('info', STRINGCOL, length=32, notNone=True, alternateID=True), + values=infoTypeDefs + ), + + DBTable('LinkType', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('link', STRINGCOL, length=32, notNone=True, alternateID=True), + values=linkTypeDefs + ), + + DBTable('Keyword', + DBCol('id', INTCOL, notNone=True, alternateID=True), + # XXX: can't use alternateID=True, because it would create + # a UNIQUE index; unfortunately (at least with a common + # collation like utf8_unicode_ci) MySQL will consider + # some different keywords identical - like + # "fiancée" and "fiancee". + DBCol('keyword', UNICODECOL, notNone=True, + index='idx_keyword', indexLen=5), + DBCol('phoneticCode', STRINGCOL, length=5, default=None, + index='idx_pcode') + ), + + DBTable('MovieKeyword', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('keywordID', INTCOL, notNone=True, index='idx_keywordid', + foreignKey='Keyword') + ), + + DBTable('MovieLink', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('linkedMovieID', INTCOL, notNone=True, foreignKey='Title'), + DBCol('linkTypeID', INTCOL, notNone=True, foreignKey='LinkType') + ), + + DBTable('MovieInfo', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('infoTypeID', INTCOL, notNone=True, foreignKey='InfoType'), + DBCol('info', UNICODECOL, notNone=True), + DBCol('note', UNICODECOL, default=None) + ), + + # This table is identical to MovieInfo, except that both 'infoTypeID' + # and 'info' are indexed. + DBTable('MovieInfoIdx', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('infoTypeID', INTCOL, notNone=True, index='idx_infotypeid', + foreignKey='InfoType'), + DBCol('info', UNICODECOL, notNone=True, index='idx_info', indexLen=10), + DBCol('note', UNICODECOL, default=None) + ), + + DBTable('MovieCompanies', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('movieID', INTCOL, notNone=True, index='idx_mid', + foreignKey='Title'), + DBCol('companyID', INTCOL, notNone=True, index='idx_cid', + foreignKey='CompanyName'), + DBCol('companyTypeID', INTCOL, notNone=True, foreignKey='CompanyType'), + DBCol('note', UNICODECOL, default=None) + ), + + DBTable('PersonInfo', + DBCol('id', INTCOL, notNone=True, alternateID=True), + DBCol('personID', INTCOL, notNone=True, index='idx_pid', + foreignKey='Name'), + DBCol('infoTypeID', INTCOL, notNone=True, foreignKey='InfoType'), + DBCol('info', UNICODECOL, notNone=True), + DBCol('note', UNICODECOL, default=None) + ) +] + + +# Functions to manage tables. +def dropTables(tables, ifExists=True): + """Drop the tables.""" + # In reverse order (useful to avoid errors about foreign keys). + DB_TABLES_DROP = list(tables) + DB_TABLES_DROP.reverse() + for table in DB_TABLES_DROP: + _dbschema_logger.info('dropping table %s', table._imdbpyName) + table.dropTable(ifExists) + +def createTables(tables, ifNotExists=True): + """Create the tables and insert default values.""" + for table in tables: + # Create the table. + _dbschema_logger.info('creating table %s', table._imdbpyName) + table.createTable(ifNotExists) + # Insert default values, if any. + if table._imdbpySchema.values: + _dbschema_logger.info('inserting values into table %s', + table._imdbpyName) + for key in table._imdbpySchema.values: + for value in table._imdbpySchema.values[key]: + table(**{key: unicode(value)}) + +def createIndexes(tables, ifNotExists=True): + """Create the indexes in the database. + Return a list of errors, if any.""" + errors = [] + for table in tables: + _dbschema_logger.info('creating indexes for table %s', + table._imdbpyName) + try: + table.addIndexes(ifNotExists) + except Exception, e: + errors.append(e) + continue + return errors + +def createForeignKeys(tables, ifNotExists=True): + """Create Foreign Keys. + Return a list of errors, if any.""" + errors = [] + mapTables = {} + for table in tables: + mapTables[table._imdbpyName] = table + for table in tables: + _dbschema_logger.info('creating foreign keys for table %s', + table._imdbpyName) + try: + table.addForeignKeys(mapTables, ifNotExists) + except Exception, e: + errors.append(e) + continue + return errors + diff --git a/lib/imdb/parser/sql/objectadapter.py b/lib/imdb/parser/sql/objectadapter.py new file mode 100644 index 0000000000000000000000000000000000000000..9797104267275a3484cf0e527c19e1b66956963c --- /dev/null +++ b/lib/imdb/parser/sql/objectadapter.py @@ -0,0 +1,207 @@ +""" +parser.sql.objectadapter module (imdb.parser.sql package). + +This module adapts the SQLObject ORM to the internal mechanism. + +Copyright 2008-2010 Davide Alberani <da@erlug.linux.it> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys +import logging + +from sqlobject import * +from sqlobject.sqlbuilder import ISNULL, ISNOTNULL, AND, OR, IN, CONTAINSSTRING + +from dbschema import * + +_object_logger = logging.getLogger('imdbpy.parser.sql.object') + + +# Maps our placeholders to SQLAlchemy's column types. +MAP_COLS = { + INTCOL: IntCol, + UNICODECOL: UnicodeCol, + STRINGCOL: StringCol +} + + +# Exception raised when Table.get(id) returns no value. +NotFoundError = SQLObjectNotFound + + +# class method to be added to the SQLObject class. +def addIndexes(cls, ifNotExists=True): + """Create all required indexes.""" + for col in cls._imdbpySchema.cols: + if col.index: + idxName = col.index + colToIdx = col.name + if col.indexLen: + colToIdx = {'column': col.name, 'length': col.indexLen} + if idxName in [i.name for i in cls.sqlmeta.indexes]: + # Check if the index is already present. + continue + idx = DatabaseIndex(colToIdx, name=idxName) + cls.sqlmeta.addIndex(idx) + try: + cls.createIndexes(ifNotExists) + except dberrors.OperationalError, e: + _object_logger.warn('Skipping creation of the %s.%s index: %s' % + (cls.sqlmeta.table, col.name, e)) +addIndexes = classmethod(addIndexes) + + +# Global repository for "fake" tables with Foreign Keys - need to +# prevent troubles if addForeignKeys is called more than one time. +FAKE_TABLES_REPOSITORY = {} + +def _buildFakeFKTable(cls, fakeTableName): + """Return a "fake" table, with foreign keys where needed.""" + countCols = 0 + attrs = {} + for col in cls._imdbpySchema.cols: + countCols += 1 + if col.name == 'id': + continue + if not col.foreignKey: + # A non-foreign key column - add it as usual. + attrs[col.name] = MAP_COLS[col.kind](**col.params) + continue + # XXX: Foreign Keys pointing to TableName.ColName not yet supported. + thisColName = col.name + if thisColName.endswith('ID'): + thisColName = thisColName[:-2] + + fks = col.foreignKey.split('.', 1) + foreignTableName = fks[0] + if len(fks) == 2: + foreignColName = fks[1] + else: + foreignColName = 'id' + # Unused... + #fkName = 'fk_%s_%s_%d' % (foreignTableName, foreignColName, + # countCols) + # Create a Foreign Key column, with the correct references. + fk = ForeignKey(foreignTableName, name=thisColName, default=None) + attrs[thisColName] = fk + # Build a _NEW_ SQLObject subclass, with foreign keys, if needed. + newcls = type(fakeTableName, (SQLObject,), attrs) + return newcls + +def addForeignKeys(cls, mapTables, ifNotExists=True): + """Create all required foreign keys.""" + # Do not even try, if there are no FK, in this table. + if not filter(None, [col.foreignKey for col in cls._imdbpySchema.cols]): + return + fakeTableName = 'myfaketable%s' % cls.sqlmeta.table + if fakeTableName in FAKE_TABLES_REPOSITORY: + newcls = FAKE_TABLES_REPOSITORY[fakeTableName] + else: + newcls = _buildFakeFKTable(cls, fakeTableName) + FAKE_TABLES_REPOSITORY[fakeTableName] = newcls + # Connect the class with foreign keys. + newcls.setConnection(cls._connection) + for col in cls._imdbpySchema.cols: + if col.name == 'id': + continue + if not col.foreignKey: + continue + # Get the SQL that _WOULD BE_ run, if we had to create + # this "fake" table. + fkQuery = newcls._connection.createReferenceConstraint(newcls, + newcls.sqlmeta.columns[col.name]) + if not fkQuery: + # Probably the db doesn't support foreign keys (SQLite). + continue + # Remove "myfaketable" to get references to _real_ tables. + fkQuery = fkQuery.replace('myfaketable', '') + # Execute the query. + newcls._connection.query(fkQuery) + # Disconnect it. + newcls._connection.close() +addForeignKeys = classmethod(addForeignKeys) + + +# Module-level "cache" for SQLObject classes, to prevent +# "class TheClass is already in the registry" errors, when +# two or more connections to the database are made. +# XXX: is this the best way to act? +TABLES_REPOSITORY = {} + +def getDBTables(uri=None): + """Return a list of classes to be used to access the database + through the SQLObject ORM. The connection uri is optional, and + can be used to tailor the db schema to specific needs.""" + DB_TABLES = [] + for table in DB_SCHEMA: + if table.name in TABLES_REPOSITORY: + DB_TABLES.append(TABLES_REPOSITORY[table.name]) + continue + attrs = {'_imdbpyName': table.name, '_imdbpySchema': table, + 'addIndexes': addIndexes, 'addForeignKeys': addForeignKeys} + for col in table.cols: + if col.name == 'id': + continue + attrs[col.name] = MAP_COLS[col.kind](**col.params) + # Create a subclass of SQLObject. + # XXX: use a metaclass? I can't see any advantage. + cls = type(table.name, (SQLObject,), attrs) + DB_TABLES.append(cls) + TABLES_REPOSITORY[table.name] = cls + return DB_TABLES + + +def toUTF8(s): + """For some strange reason, sometimes SQLObject wants utf8 strings + instead of unicode.""" + return s.encode('utf_8') + + +def setConnection(uri, tables, encoding='utf8', debug=False): + """Set connection for every table.""" + kw = {} + # FIXME: it's absolutely unclear what we should do to correctly + # support unicode in MySQL; with some versions of SQLObject, + # it seems that setting use_unicode=1 is the _wrong_ thing to do. + _uriLower = uri.lower() + if _uriLower.startswith('mysql'): + kw['use_unicode'] = 1 + #kw['sqlobject_encoding'] = encoding + kw['charset'] = encoding + conn = connectionForURI(uri, **kw) + conn.debug = debug + # XXX: doesn't work and a work-around was put in imdbpy2sql.py; + # is there any way to modify the text_factory parameter of + # a SQLite connection? + #if uri.startswith('sqlite'): + # major = sys.version_info[0] + # minor = sys.version_info[1] + # if major > 2 or (major == 2 and minor > 5): + # sqliteConn = conn.getConnection() + # sqliteConn.text_factory = str + for table in tables: + table.setConnection(conn) + #table.sqlmeta.cacheValues = False + # FIXME: is it safe to set table._cacheValue to False? Looks like + # we can't retrieve correct values after an update (I think + # it's never needed, but...) Anyway, these are set to False + # for performance reason at insert time (see imdbpy2sql.py). + table._cacheValue = False + # Required by imdbpy2sql.py. + conn.paramstyle = conn.module.paramstyle + return conn + diff --git a/lib/imdb/utils.py b/lib/imdb/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9c300b0852c6d33784b25b3216299745e0bfd90b --- /dev/null +++ b/lib/imdb/utils.py @@ -0,0 +1,1572 @@ +""" +utils module (imdb package). + +This module provides basic utilities for the imdb package. + +Copyright 2004-2012 Davide Alberani <da@erlug.linux.it> + 2009 H. Turgut Uyar <uyar@tekir.org> + +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 2 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, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from __future__ import generators +import re +import string +import logging +from copy import copy, deepcopy +from time import strptime, strftime + +from imdb import VERSION +from imdb import linguistics +from imdb._exceptions import IMDbParserError + + +# Logger for imdb.utils module. +_utils_logger = logging.getLogger('imdbpy.utils') + +# The regular expression for the "long" year format of IMDb, like +# "(1998)" and "(1986/II)", where the optional roman number (that I call +# "imdbIndex" after the slash is used for movies with the same title +# and year of release. +# XXX: probably L, C, D and M are far too much! ;-) +re_year_index = re.compile(r'\(([0-9\?]{4}(/[IVXLCDM]+)?)\)') +re_extended_year_index = re.compile(r'\((TV episode|TV Series|TV mini-series|TV|Video|Video Game)? ?((?:[0-9\?]{4})(?:-[0-9\?]{4})?)(?:/([IVXLCDM]+)?)?\)') +re_remove_kind = re.compile(r'\((TV episode|TV Series|TV mini-series|TV|Video|Video Game)? ?') + +# Match only the imdbIndex (for name strings). +re_index = re.compile(r'^\(([IVXLCDM]+)\)$') + +# Match things inside parentheses. +re_parentheses = re.compile(r'(\(.*\))') + +# Match the number of episodes. +re_episodes = re.compile('\s?\((\d+) episodes\)', re.I) +re_episode_info = re.compile(r'{\s*(.+?)?\s?(\([0-9\?]{4}-[0-9\?]{1,2}-[0-9\?]{1,2}\))?\s?(\(#[0-9]+\.[0-9]+\))?}') + +# Common suffixes in surnames. +_sname_suffixes = ('de', 'la', 'der', 'den', 'del', 'y', 'da', 'van', + 'e', 'von', 'the', 'di', 'du', 'el', 'al') + +def canonicalName(name): + """Return the given name in canonical "Surname, Name" format. + It assumes that name is in the 'Name Surname' format.""" + # XXX: some statistics (as of 17 Apr 2008, over 2288622 names): + # - just a surname: 69476 + # - single surname, single name: 2209656 + # - composed surname, composed name: 9490 + # - composed surname, single name: 67606 + # (2: 59764, 3: 6862, 4: 728) + # - single surname, composed name: 242310 + # (2: 229467, 3: 9901, 4: 2041, 5: 630) + # - Jr.: 8025 + # Don't convert names already in the canonical format. + if name.find(', ') != -1: return name + if isinstance(name, unicode): + joiner = u'%s, %s' + sur_joiner = u'%s %s' + sur_space = u' %s' + space = u' ' + else: + joiner = '%s, %s' + sur_joiner = '%s %s' + sur_space = ' %s' + space = ' ' + sname = name.split(' ') + snl = len(sname) + if snl == 2: + # Just a name and a surname: how boring... + name = joiner % (sname[1], sname[0]) + elif snl > 2: + lsname = [x.lower() for x in sname] + if snl == 3: _indexes = (0, snl-2) + else: _indexes = (0, snl-2, snl-3) + # Check for common surname prefixes at the beginning and near the end. + for index in _indexes: + if lsname[index] not in _sname_suffixes: continue + try: + # Build the surname. + surn = sur_joiner % (sname[index], sname[index+1]) + del sname[index] + del sname[index] + try: + # Handle the "Jr." after the name. + if lsname[index+2].startswith('jr'): + surn += sur_space % sname[index] + del sname[index] + except (IndexError, ValueError): + pass + name = joiner % (surn, space.join(sname)) + break + except ValueError: + continue + else: + name = joiner % (sname[-1], space.join(sname[:-1])) + return name + +def normalizeName(name): + """Return a name in the normal "Name Surname" format.""" + if isinstance(name, unicode): + joiner = u'%s %s' + else: + joiner = '%s %s' + sname = name.split(', ') + if len(sname) == 2: + name = joiner % (sname[1], sname[0]) + return name + +def analyze_name(name, canonical=None): + """Return a dictionary with the name and the optional imdbIndex + keys, from the given string. + + If canonical is None (default), the name is stored in its own style. + If canonical is True, the name is converted to canonical style. + If canonical is False, the name is converted to normal format. + + raise an IMDbParserError exception if the name is not valid. + """ + original_n = name + name = name.strip() + res = {} + imdbIndex = '' + opi = name.rfind('(') + cpi = name.rfind(')') + # Strip notes (but not if the name starts with a parenthesis). + if opi not in (-1, 0) and cpi > opi: + if re_index.match(name[opi:cpi+1]): + imdbIndex = name[opi+1:cpi] + name = name[:opi].rstrip() + else: + # XXX: for the birth and death dates case like " (1926-2004)" + name = re_parentheses.sub('', name).strip() + if not name: + raise IMDbParserError('invalid name: "%s"' % original_n) + if canonical is not None: + if canonical: + name = canonicalName(name) + else: + name = normalizeName(name) + res['name'] = name + if imdbIndex: + res['imdbIndex'] = imdbIndex + return res + + +def build_name(name_dict, canonical=None): + """Given a dictionary that represents a "long" IMDb name, + return a string. + If canonical is None (default), the name is returned in the stored style. + If canonical is True, the name is converted to canonical style. + If canonical is False, the name is converted to normal format. + """ + name = name_dict.get('canonical name') or name_dict.get('name', '') + if not name: return '' + if canonical is not None: + if canonical: + name = canonicalName(name) + else: + name = normalizeName(name) + imdbIndex = name_dict.get('imdbIndex') + if imdbIndex: + name += ' (%s)' % imdbIndex + return name + + +# XXX: here only for backward compatibility. Find and remove any dependency. +_articles = linguistics.GENERIC_ARTICLES +_unicodeArticles = linguistics.toUnicode(_articles) +articlesDicts = linguistics.articlesDictsForLang(None) +spArticles = linguistics.spArticlesForLang(None) + +def canonicalTitle(title, lang=None): + """Return the title in the canonic format 'Movie Title, The'; + beware that it doesn't handle long imdb titles, but only the + title portion, without year[/imdbIndex] or special markup. + The 'lang' argument can be used to specify the language of the title. + """ + isUnicode = isinstance(title, unicode) + articlesDicts = linguistics.articlesDictsForLang(lang) + try: + if title.split(', ')[-1].lower() in articlesDicts[isUnicode]: + return title + except IndexError: + pass + if isUnicode: + _format = u'%s, %s' + else: + _format = '%s, %s' + ltitle = title.lower() + spArticles = linguistics.spArticlesForLang(lang) + for article in spArticles[isUnicode]: + if ltitle.startswith(article): + lart = len(article) + title = _format % (title[lart:], title[:lart]) + if article[-1] == ' ': + title = title[:-1] + break + ## XXX: an attempt using a dictionary lookup. + ##for artSeparator in (' ', "'", '-'): + ## article = _articlesDict.get(ltitle.split(artSeparator)[0]) + ## if article is not None: + ## lart = len(article) + ## # check titles like "una", "I'm Mad" and "L'abbacchio". + ## if title[lart:] == '' or (artSeparator != ' ' and + ## title[lart:][1] != artSeparator): continue + ## title = '%s, %s' % (title[lart:], title[:lart]) + ## if artSeparator == ' ': title = title[1:] + ## break + return title + +def normalizeTitle(title, lang=None): + """Return the title in the normal "The Title" format; + beware that it doesn't handle long imdb titles, but only the + title portion, without year[/imdbIndex] or special markup. + The 'lang' argument can be used to specify the language of the title. + """ + isUnicode = isinstance(title, unicode) + stitle = title.split(', ') + articlesDicts = linguistics.articlesDictsForLang(lang) + if len(stitle) > 1 and stitle[-1].lower() in articlesDicts[isUnicode]: + sep = ' ' + if stitle[-1][-1] in ("'", '-'): + sep = '' + if isUnicode: + _format = u'%s%s%s' + _joiner = u', ' + else: + _format = '%s%s%s' + _joiner = ', ' + title = _format % (stitle[-1], sep, _joiner.join(stitle[:-1])) + return title + + +def _split_series_episode(title): + """Return the series and the episode titles; if this is not a + series' episode, the returned series title is empty. + This function recognize two different styles: + "The Series" An Episode (2005) + "The Series" (2004) {An Episode (2005) (#season.episode)}""" + series_title = '' + episode_or_year = '' + if title[-1:] == '}': + # Title of the episode, as in the plain text data files. + begin_eps = title.rfind('{') + if begin_eps == -1: return '', '' + series_title = title[:begin_eps].rstrip() + # episode_or_year is returned with the {...} + episode_or_year = title[begin_eps:].strip() + if episode_or_year[:12] == '{SUSPENDED}}': return '', '' + # XXX: works only with tv series; it's still unclear whether + # IMDb will support episodes for tv mini series and tv movies... + elif title[0:1] == '"': + second_quot = title[1:].find('"') + 2 + if second_quot != 1: # a second " was found. + episode_or_year = title[second_quot:].lstrip() + first_char = episode_or_year[0:1] + if not first_char: return '', '' + if first_char != '(': + # There is not a (year) but the title of the episode; + # that means this is an episode title, as returned by + # the web server. + series_title = title[:second_quot] + ##elif episode_or_year[-1:] == '}': + ## # Title of the episode, as in the plain text data files. + ## begin_eps = episode_or_year.find('{') + ## if begin_eps == -1: return series_title, episode_or_year + ## series_title = title[:second_quot+begin_eps].rstrip() + ## # episode_or_year is returned with the {...} + ## episode_or_year = episode_or_year[begin_eps:] + return series_title, episode_or_year + + +def is_series_episode(title): + """Return True if 'title' is an series episode.""" + title = title.strip() + if _split_series_episode(title)[0]: return 1 + return 0 + + +def analyze_title(title, canonical=None, canonicalSeries=None, + canonicalEpisode=None, _emptyString=u''): + """Analyze the given title and return a dictionary with the + "stripped" title, the kind of the show ("movie", "tv series", etc.), + the year of production and the optional imdbIndex (a roman number + used to distinguish between movies with the same title and year). + + If canonical is None (default), the title is stored in its own style. + If canonical is True, the title is converted to canonical style. + If canonical is False, the title is converted to normal format. + + raise an IMDbParserError exception if the title is not valid. + """ + # XXX: introduce the 'lang' argument? + if canonical is not None: + canonicalSeries = canonicalEpisode = canonical + original_t = title + result = {} + title = title.strip() + year = _emptyString + kind = _emptyString + imdbIndex = _emptyString + series_title, episode_or_year = _split_series_episode(title) + if series_title: + # It's an episode of a series. + series_d = analyze_title(series_title, canonical=canonicalSeries) + oad = sen = ep_year = _emptyString + # Plain text data files format. + if episode_or_year[0:1] == '{' and episode_or_year[-1:] == '}': + match = re_episode_info.findall(episode_or_year) + if match: + # Episode title, original air date and #season.episode + episode_or_year, oad, sen = match[0] + episode_or_year = episode_or_year.strip() + if not oad: + # No year, but the title is something like (2005-04-12) + if episode_or_year and episode_or_year[0] == '(' and \ + episode_or_year[-1:] == ')' and \ + episode_or_year[1:2] != '#': + oad = episode_or_year + if oad[1:5] and oad[5:6] == '-': + try: + ep_year = int(oad[1:5]) + except (TypeError, ValueError): + pass + if not oad and not sen and episode_or_year.startswith('(#'): + sen = episode_or_year + elif episode_or_year.startswith('Episode dated'): + oad = episode_or_year[14:] + if oad[-4:].isdigit(): + try: + ep_year = int(oad[-4:]) + except (TypeError, ValueError): + pass + episode_d = analyze_title(episode_or_year, canonical=canonicalEpisode) + episode_d['kind'] = u'episode' + episode_d['episode of'] = series_d + if oad: + episode_d['original air date'] = oad[1:-1] + if ep_year and episode_d.get('year') is None: + episode_d['year'] = ep_year + if sen and sen[2:-1].find('.') != -1: + seas, epn = sen[2:-1].split('.') + if seas: + # Set season and episode. + try: seas = int(seas) + except: pass + try: epn = int(epn) + except: pass + episode_d['season'] = seas + if epn: + episode_d['episode'] = epn + return episode_d + # First of all, search for the kind of show. + # XXX: Number of entries at 17 Apr 2008: + # movie: 379,871 + # episode: 483,832 + # tv movie: 61,119 + # tv series: 44,795 + # video movie: 57,915 + # tv mini series: 5,497 + # video game: 5,490 + # More up-to-date statistics: http://us.imdb.com/database_statistics + if title.endswith('(TV)'): + kind = u'tv movie' + title = title[:-4].rstrip() + elif title.endswith('(V)'): + kind = u'video movie' + title = title[:-3].rstrip() + elif title.endswith('(video)'): + kind = u'video movie' + title = title[:-7].rstrip() + elif title.endswith('(mini)'): + kind = u'tv mini series' + title = title[:-6].rstrip() + elif title.endswith('(VG)'): + kind = u'video game' + title = title[:-4].rstrip() + # Search for the year and the optional imdbIndex (a roman number). + yi = re_year_index.findall(title) + if not yi: + yi = re_extended_year_index.findall(title) + if yi: + yk, yiy, yii = yi[-1] + yi = [(yiy, yii)] + if yk == 'TV episode': + kind = u'episode' + elif yk == 'TV': + kind = u'tv movie' + elif yk == 'TV Series': + kind = u'tv series' + elif yk == 'Video': + kind = u'video movie' + elif yk == 'TV mini-series': + kind = u'tv mini series' + elif yk == 'Video Game': + kind = u'video game' + title = re_remove_kind.sub('(', title) + if yi: + last_yi = yi[-1] + year = last_yi[0] + if last_yi[1]: + imdbIndex = last_yi[1][1:] + year = year[:-len(imdbIndex)-1] + i = title.rfind('(%s)' % last_yi[0]) + if i != -1: + title = title[:i-1].rstrip() + # This is a tv (mini) series: strip the '"' at the begin and at the end. + # XXX: strip('"') is not used for compatibility with Python 2.0. + if title and title[0] == title[-1] == '"': + if not kind: + kind = u'tv series' + title = title[1:-1].strip() + elif title.endswith('(TV series)'): + kind = u'tv series' + title = title[:-11].rstrip() + if not title: + raise IMDbParserError('invalid title: "%s"' % original_t) + if canonical is not None: + if canonical: + title = canonicalTitle(title) + else: + title = normalizeTitle(title) + # 'kind' is one in ('movie', 'episode', 'tv series', 'tv mini series', + # 'tv movie', 'video movie', 'video game') + result['title'] = title + result['kind'] = kind or u'movie' + if year and year != '????': + if '-' in year: + result['series years'] = year + year = year[:4] + try: + result['year'] = int(year) + except (TypeError, ValueError): + pass + if imdbIndex: + result['imdbIndex'] = imdbIndex + if isinstance(_emptyString, str): + result['kind'] = str(kind or 'movie') + return result + + +_web_format = '%d %B %Y' +_ptdf_format = '(%Y-%m-%d)' +def _convertTime(title, fromPTDFtoWEB=1, _emptyString=u''): + """Convert a time expressed in the pain text data files, to + the 'Episode dated ...' format used on the web site; if + fromPTDFtoWEB is false, the inverted conversion is applied.""" + try: + if fromPTDFtoWEB: + from_format = _ptdf_format + to_format = _web_format + else: + from_format = u'Episode dated %s' % _web_format + to_format = _ptdf_format + t = strptime(title, from_format) + title = strftime(to_format, t) + if fromPTDFtoWEB: + if title[0] == '0': title = title[1:] + title = u'Episode dated %s' % title + except ValueError: + pass + if isinstance(_emptyString, str): + try: + title = str(title) + except UnicodeDecodeError: + pass + return title + + +def build_title(title_dict, canonical=None, canonicalSeries=None, + canonicalEpisode=None, ptdf=0, lang=None, _doYear=1, + _emptyString=u''): + """Given a dictionary that represents a "long" IMDb title, + return a string. + + If canonical is None (default), the title is returned in the stored style. + If canonical is True, the title is converted to canonical style. + If canonical is False, the title is converted to normal format. + + lang can be used to specify the language of the title. + + If ptdf is true, the plain text data files format is used. + """ + if canonical is not None: + canonicalSeries = canonical + pre_title = _emptyString + kind = title_dict.get('kind') + episode_of = title_dict.get('episode of') + if kind == 'episode' and episode_of is not None: + # Works with both Movie instances and plain dictionaries. + doYear = 0 + if ptdf: + doYear = 1 + pre_title = build_title(episode_of, canonical=canonicalSeries, + ptdf=0, _doYear=doYear, + _emptyString=_emptyString) + ep_dict = {'title': title_dict.get('title', ''), + 'imdbIndex': title_dict.get('imdbIndex')} + ep_title = ep_dict['title'] + if not ptdf: + doYear = 1 + ep_dict['year'] = title_dict.get('year', '????') + if ep_title[0:1] == '(' and ep_title[-1:] == ')' and \ + ep_title[1:5].isdigit(): + ep_dict['title'] = _convertTime(ep_title, fromPTDFtoWEB=1, + _emptyString=_emptyString) + else: + doYear = 0 + if ep_title.startswith('Episode dated'): + ep_dict['title'] = _convertTime(ep_title, fromPTDFtoWEB=0, + _emptyString=_emptyString) + episode_title = build_title(ep_dict, + canonical=canonicalEpisode, ptdf=ptdf, + _doYear=doYear, _emptyString=_emptyString) + if ptdf: + oad = title_dict.get('original air date', _emptyString) + if len(oad) == 10 and oad[4] == '-' and oad[7] == '-' and \ + episode_title.find(oad) == -1: + episode_title += ' (%s)' % oad + seas = title_dict.get('season') + if seas is not None: + episode_title += ' (#%s' % seas + episode = title_dict.get('episode') + if episode is not None: + episode_title += '.%s' % episode + episode_title += ')' + episode_title = '{%s}' % episode_title + return '%s %s' % (pre_title, episode_title) + title = title_dict.get('title', '') + if not title: return _emptyString + if canonical is not None: + if canonical: + title = canonicalTitle(title, lang=lang) + else: + title = normalizeTitle(title, lang=lang) + if pre_title: + title = '%s %s' % (pre_title, title) + if kind in (u'tv series', u'tv mini series'): + title = '"%s"' % title + if _doYear: + imdbIndex = title_dict.get('imdbIndex') + year = title_dict.get('year') or u'????' + if isinstance(_emptyString, str): + year = str(year) + title += ' (%s' % year + if imdbIndex: + title += '/%s' % imdbIndex + title += ')' + if kind: + if kind == 'tv movie': + title += ' (TV)' + elif kind == 'video movie': + title += ' (V)' + elif kind == 'tv mini series': + title += ' (mini)' + elif kind == 'video game': + title += ' (VG)' + return title + + +def split_company_name_notes(name): + """Return two strings, the first representing the company name, + and the other representing the (optional) notes.""" + name = name.strip() + notes = u'' + if name.endswith(')'): + fpidx = name.find('(') + if fpidx != -1: + notes = name[fpidx:] + name = name[:fpidx].rstrip() + return name, notes + + +def analyze_company_name(name, stripNotes=False): + """Return a dictionary with the name and the optional 'country' + keys, from the given string. + If stripNotes is true, tries to not consider optional notes. + + raise an IMDbParserError exception if the name is not valid. + """ + if stripNotes: + name = split_company_name_notes(name)[0] + o_name = name + name = name.strip() + country = None + if name.endswith(']'): + idx = name.rfind('[') + if idx != -1: + country = name[idx:] + name = name[:idx].rstrip() + if not name: + raise IMDbParserError('invalid name: "%s"' % o_name) + result = {'name': name} + if country: + result['country'] = country + return result + + +def build_company_name(name_dict, _emptyString=u''): + """Given a dictionary that represents a "long" IMDb company name, + return a string. + """ + name = name_dict.get('name') + if not name: + return _emptyString + country = name_dict.get('country') + if country is not None: + name += ' %s' % country + return name + + +class _LastC: + """Size matters.""" + def __cmp__(self, other): + if isinstance(other, self.__class__): return 0 + return 1 + +_last = _LastC() + +def cmpMovies(m1, m2): + """Compare two movies by year, in reverse order; the imdbIndex is checked + for movies with the same year of production and title.""" + # Sort tv series' episodes. + m1e = m1.get('episode of') + m2e = m2.get('episode of') + if m1e is not None and m2e is not None: + cmp_series = cmpMovies(m1e, m2e) + if cmp_series != 0: + return cmp_series + m1s = m1.get('season') + m2s = m2.get('season') + if m1s is not None and m2s is not None: + if m1s < m2s: + return 1 + elif m1s > m2s: + return -1 + m1p = m1.get('episode') + m2p = m2.get('episode') + if m1p < m2p: + return 1 + elif m1p > m2p: + return -1 + try: + if m1e is None: m1y = int(m1.get('year', 0)) + else: m1y = int(m1e.get('year', 0)) + except ValueError: + m1y = 0 + try: + if m2e is None: m2y = int(m2.get('year', 0)) + else: m2y = int(m2e.get('year', 0)) + except ValueError: + m2y = 0 + if m1y > m2y: return -1 + if m1y < m2y: return 1 + # Ok, these movies have the same production year... + #m1t = m1.get('canonical title', _last) + #m2t = m2.get('canonical title', _last) + # It should works also with normal dictionaries (returned from searches). + #if m1t is _last and m2t is _last: + m1t = m1.get('title', _last) + m2t = m2.get('title', _last) + if m1t < m2t: return -1 + if m1t > m2t: return 1 + # Ok, these movies have the same title... + m1i = m1.get('imdbIndex', _last) + m2i = m2.get('imdbIndex', _last) + if m1i > m2i: return -1 + if m1i < m2i: return 1 + m1id = getattr(m1, 'movieID', None) + # Introduce this check even for other comparisons functions? + # XXX: is it safe to check without knowning the data access system? + # probably not a great idea. Check for 'kind', instead? + if m1id is not None: + m2id = getattr(m2, 'movieID', None) + if m1id > m2id: return -1 + elif m1id < m2id: return 1 + return 0 + + +def cmpPeople(p1, p2): + """Compare two people by billingPos, name and imdbIndex.""" + p1b = getattr(p1, 'billingPos', None) or _last + p2b = getattr(p2, 'billingPos', None) or _last + if p1b > p2b: return 1 + if p1b < p2b: return -1 + p1n = p1.get('canonical name', _last) + p2n = p2.get('canonical name', _last) + if p1n is _last and p2n is _last: + p1n = p1.get('name', _last) + p2n = p2.get('name', _last) + if p1n > p2n: return 1 + if p1n < p2n: return -1 + p1i = p1.get('imdbIndex', _last) + p2i = p2.get('imdbIndex', _last) + if p1i > p2i: return 1 + if p1i < p2i: return -1 + return 0 + + +def cmpCompanies(p1, p2): + """Compare two companies.""" + p1n = p1.get('long imdb name', _last) + p2n = p2.get('long imdb name', _last) + if p1n is _last and p2n is _last: + p1n = p1.get('name', _last) + p2n = p2.get('name', _last) + if p1n > p2n: return 1 + if p1n < p2n: return -1 + p1i = p1.get('country', _last) + p2i = p2.get('country', _last) + if p1i > p2i: return 1 + if p1i < p2i: return -1 + return 0 + + +# References to titles, names and characters. +# XXX: find better regexp! +re_titleRef = re.compile(r'_(.+?(?: \([0-9\?]{4}(?:/[IVXLCDM]+)?\))?(?: \(mini\)| \(TV\)| \(V\)| \(VG\))?)_ \(qv\)') +# FIXME: doesn't match persons with ' in the name. +re_nameRef = re.compile(r"'([^']+?)' \(qv\)") +# XXX: good choice? Are there characters with # in the name? +re_characterRef = re.compile(r"#([^']+?)# \(qv\)") + +# Functions used to filter the text strings. +def modNull(s, titlesRefs, namesRefs, charactersRefs): + """Do nothing.""" + return s + +def modClearTitleRefs(s, titlesRefs, namesRefs, charactersRefs): + """Remove titles references.""" + return re_titleRef.sub(r'\1', s) + +def modClearNameRefs(s, titlesRefs, namesRefs, charactersRefs): + """Remove names references.""" + return re_nameRef.sub(r'\1', s) + +def modClearCharacterRefs(s, titlesRefs, namesRefs, charactersRefs): + """Remove characters references""" + return re_characterRef.sub(r'\1', s) + +def modClearRefs(s, titlesRefs, namesRefs, charactersRefs): + """Remove titles, names and characters references.""" + s = modClearTitleRefs(s, {}, {}, {}) + s = modClearCharacterRefs(s, {}, {}, {}) + return modClearNameRefs(s, {}, {}, {}) + + +def modifyStrings(o, modFunct, titlesRefs, namesRefs, charactersRefs): + """Modify a string (or string values in a dictionary or strings + in a list), using the provided modFunct function and titlesRefs + namesRefs and charactersRefs references dictionaries.""" + # Notice that it doesn't go any deeper than the first two levels in a list. + if isinstance(o, (unicode, str)): + return modFunct(o, titlesRefs, namesRefs, charactersRefs) + elif isinstance(o, (list, tuple, dict)): + _stillorig = 1 + if isinstance(o, (list, tuple)): keys = xrange(len(o)) + else: keys = o.keys() + for i in keys: + v = o[i] + if isinstance(v, (unicode, str)): + if _stillorig: + o = copy(o) + _stillorig = 0 + o[i] = modFunct(v, titlesRefs, namesRefs, charactersRefs) + elif isinstance(v, (list, tuple)): + modifyStrings(o[i], modFunct, titlesRefs, namesRefs, + charactersRefs) + return o + + +def date_and_notes(s): + """Parse (birth|death) date and notes; returns a tuple in the + form (date, notes).""" + s = s.strip() + if not s: return (u'', u'') + notes = u'' + if s[0].isdigit() or s.split()[0].lower() in ('c.', 'january', 'february', + 'march', 'april', 'may', 'june', + 'july', 'august', 'september', + 'october', 'november', + 'december', 'ca.', 'circa', + '????,'): + i = s.find(',') + if i != -1: + notes = s[i+1:].strip() + s = s[:i] + else: + notes = s + s = u'' + if s == '????': s = u'' + return s, notes + + +class RolesList(list): + """A list of Person or Character instances, used for the currentRole + property.""" + def __unicode__(self): + return u' / '.join([unicode(x) for x in self]) + + def __str__(self): + # FIXME: does it make sense at all? Return a unicode doesn't + # seem right, in __str__. + return u' / '.join([unicode(x).encode('utf8') for x in self]) + + +# Replace & with &, but only if it's not already part of a charref. +#_re_amp = re.compile(r'(&)(?!\w+;)', re.I) +#_re_amp = re.compile(r'(?<=\W)&(?=[^a-zA-Z0-9_#])') +_re_amp = re.compile(r'&(?![^a-zA-Z0-9_#]{1,5};)') + +def escape4xml(value): + """Escape some chars that can't be present in a XML value.""" + if isinstance(value, int): + value = str(value) + value = _re_amp.sub('&', value) + value = value.replace('"', '"').replace("'", ''') + value = value.replace('<', '<').replace('>', '>') + if isinstance(value, unicode): + value = value.encode('ascii', 'xmlcharrefreplace') + return value + + +def _refsToReplace(value, modFunct, titlesRefs, namesRefs, charactersRefs): + """Return three lists - for movie titles, persons and characters names - + with two items tuples: the first item is the reference once escaped + by the user-provided modFunct function, the second is the same + reference un-escaped.""" + mRefs = [] + for refRe, refTemplate in [(re_titleRef, u'_%s_ (qv)'), + (re_nameRef, u"'%s' (qv)"), + (re_characterRef, u'#%s# (qv)')]: + theseRefs = [] + for theRef in refRe.findall(value): + # refTemplate % theRef values don't change for a single + # _Container instance, so this is a good candidate for a + # cache or something - even if it's so rarely used that... + # Moreover, it can grow - ia.update(...) - and change if + # modFunct is modified. + goodValue = modFunct(refTemplate % theRef, titlesRefs, namesRefs, + charactersRefs) + # Prevents problems with crap in plain text data files. + # We should probably exclude invalid chars and string that + # are too long in the re_*Ref expressions. + if '_' in goodValue or len(goodValue) > 128: + continue + toReplace = escape4xml(goodValue) + # Only the 'value' portion is replaced. + replaceWith = goodValue.replace(theRef, escape4xml(theRef)) + theseRefs.append((toReplace, replaceWith)) + mRefs.append(theseRefs) + return mRefs + + +def _handleTextNotes(s): + """Split text::notes strings.""" + ssplit = s.split('::', 1) + if len(ssplit) == 1: + return s + return u'%s<notes>%s</notes>' % (ssplit[0], ssplit[1]) + + +def _normalizeValue(value, withRefs=False, modFunct=None, titlesRefs=None, + namesRefs=None, charactersRefs=None): + """Replace some chars that can't be present in a XML text.""" + # XXX: use s.encode(encoding, 'xmlcharrefreplace') ? Probably not + # a great idea: after all, returning a unicode is safe. + if isinstance(value, (unicode, str)): + if not withRefs: + value = _handleTextNotes(escape4xml(value)) + else: + # Replace references that were accidentally escaped. + replaceLists = _refsToReplace(value, modFunct, titlesRefs, + namesRefs, charactersRefs) + value = modFunct(value, titlesRefs or {}, namesRefs or {}, + charactersRefs or {}) + value = _handleTextNotes(escape4xml(value)) + for replaceList in replaceLists: + for toReplace, replaceWith in replaceList: + value = value.replace(toReplace, replaceWith) + else: + value = unicode(value) + return value + + +def _tag4TON(ton, addAccessSystem=False, _containerOnly=False): + """Build a tag for the given _Container instance; + both open and close tags are returned.""" + tag = ton.__class__.__name__.lower() + what = 'name' + if tag == 'movie': + value = ton.get('long imdb title') or ton.get('title', '') + what = 'title' + else: + value = ton.get('long imdb name') or ton.get('name', '') + value = _normalizeValue(value) + extras = u'' + crl = ton.currentRole + if crl: + if not isinstance(crl, list): + crl = [crl] + for cr in crl: + crTag = cr.__class__.__name__.lower() + crValue = cr['long imdb name'] + crValue = _normalizeValue(crValue) + crID = cr.getID() + if crID is not None: + extras += u'<current-role><%s id="%s">' \ + u'<name>%s</name></%s>' % (crTag, crID, + crValue, crTag) + else: + extras += u'<current-role><%s><name>%s</name></%s>' % \ + (crTag, crValue, crTag) + if cr.notes: + extras += u'<notes>%s</notes>' % _normalizeValue(cr.notes) + extras += u'</current-role>' + theID = ton.getID() + if theID is not None: + beginTag = u'<%s id="%s"' % (tag, theID) + if addAccessSystem and ton.accessSystem: + beginTag += ' access-system="%s"' % ton.accessSystem + if not _containerOnly: + beginTag += u'><%s>%s</%s>' % (what, value, what) + else: + beginTag += u'>' + else: + if not _containerOnly: + beginTag = u'<%s><%s>%s</%s>' % (tag, what, value, what) + else: + beginTag = u'<%s>' % tag + beginTag += extras + if ton.notes: + beginTag += u'<notes>%s</notes>' % _normalizeValue(ton.notes) + return (beginTag, u'</%s>' % tag) + + +TAGS_TO_MODIFY = { + 'movie.parents-guide': ('item', True), + 'movie.number-of-votes': ('item', True), + 'movie.soundtrack.item': ('item', True), + 'movie.quotes': ('quote', False), + 'movie.quotes.quote': ('line', False), + 'movie.demographic': ('item', True), + 'movie.episodes': ('season', True), + 'movie.episodes.season': ('episode', True), + 'person.merchandising-links': ('item', True), + 'person.genres': ('item', True), + 'person.quotes': ('quote', False), + 'person.keywords': ('item', True), + 'character.quotes': ('item', True), + 'character.quotes.item': ('quote', False), + 'character.quotes.item.quote': ('line', False) + } + +_allchars = string.maketrans('', '') +_keepchars = _allchars.translate(_allchars, string.ascii_lowercase + '-' + + string.digits) + +def _tagAttr(key, fullpath): + """Return a tuple with a tag name and a (possibly empty) attribute, + applying the conversions specified in TAGS_TO_MODIFY and checking + that the tag is safe for a XML document.""" + attrs = {} + _escapedKey = escape4xml(key) + if fullpath in TAGS_TO_MODIFY: + tagName, useTitle = TAGS_TO_MODIFY[fullpath] + if useTitle: + attrs['key'] = _escapedKey + elif not isinstance(key, unicode): + if isinstance(key, str): + tagName = unicode(key, 'ascii', 'ignore') + else: + strType = str(type(key)).replace("<type '", "").replace("'>", "") + attrs['keytype'] = strType + tagName = unicode(key) + else: + tagName = key + if isinstance(key, int): + attrs['keytype'] = 'int' + origTagName = tagName + tagName = tagName.lower().replace(' ', '-') + tagName = str(tagName).translate(_allchars, _keepchars) + if origTagName != tagName: + if 'key' not in attrs: + attrs['key'] = _escapedKey + if (not tagName) or tagName[0].isdigit() or tagName[0] == '-': + # This is a fail-safe: we should never be here, since unpredictable + # keys must be listed in TAGS_TO_MODIFY. + # This will proably break the DTD/schema, but at least it will + # produce a valid XML. + tagName = 'item' + _utils_logger.error('invalid tag: %s [%s]' % (_escapedKey, fullpath)) + attrs['key'] = _escapedKey + return tagName, u' '.join([u'%s="%s"' % i for i in attrs.items()]) + + +def _seq2xml(seq, _l=None, withRefs=False, modFunct=None, + titlesRefs=None, namesRefs=None, charactersRefs=None, + _topLevel=True, key2infoset=None, fullpath=''): + """Convert a sequence or a dictionary to a list of XML + unicode strings.""" + if _l is None: + _l = [] + if isinstance(seq, dict): + for key in seq: + value = seq[key] + if isinstance(key, _Container): + # Here we're assuming that a _Container is never a top-level + # key (otherwise we should handle key2infoset). + openTag, closeTag = _tag4TON(key) + # So that fullpath will contains something meaningful. + tagName = key.__class__.__name__.lower() + else: + tagName, attrs = _tagAttr(key, fullpath) + openTag = u'<%s' % tagName + if attrs: + openTag += ' %s' % attrs + if _topLevel and key2infoset and key in key2infoset: + openTag += u' infoset="%s"' % key2infoset[key] + if isinstance(value, int): + openTag += ' type="int"' + elif isinstance(value, float): + openTag += ' type="float"' + openTag += u'>' + closeTag = u'</%s>' % tagName + _l.append(openTag) + _seq2xml(value, _l, withRefs, modFunct, titlesRefs, + namesRefs, charactersRefs, _topLevel=False, + fullpath='%s.%s' % (fullpath, tagName)) + _l.append(closeTag) + elif isinstance(seq, (list, tuple)): + tagName, attrs = _tagAttr('item', fullpath) + beginTag = u'<%s' % tagName + if attrs: + beginTag += u' %s' % attrs + #beginTag += u'>' + closeTag = u'</%s>' % tagName + for item in seq: + if isinstance(item, _Container): + _seq2xml(item, _l, withRefs, modFunct, titlesRefs, + namesRefs, charactersRefs, _topLevel=False, + fullpath='%s.%s' % (fullpath, + item.__class__.__name__.lower())) + else: + openTag = beginTag + if isinstance(item, int): + openTag += ' type="int"' + elif isinstance(item, float): + openTag += ' type="float"' + openTag += u'>' + _l.append(openTag) + _seq2xml(item, _l, withRefs, modFunct, titlesRefs, + namesRefs, charactersRefs, _topLevel=False, + fullpath='%s.%s' % (fullpath, tagName)) + _l.append(closeTag) + else: + if isinstance(seq, _Container): + _l.extend(_tag4TON(seq)) + else: + # Text, ints, floats and the like. + _l.append(_normalizeValue(seq, withRefs=withRefs, + modFunct=modFunct, + titlesRefs=titlesRefs, + namesRefs=namesRefs, + charactersRefs=charactersRefs)) + return _l + + +_xmlHead = u"""<?xml version="1.0"?> +<!DOCTYPE %s SYSTEM "http://imdbpy.sf.net/dtd/imdbpy{VERSION}.dtd"> + +""" +_xmlHead = _xmlHead.replace('{VERSION}', + VERSION.replace('.', '').split('dev')[0][:2]) + + +class _Container(object): + """Base class for Movie, Person, Character and Company classes.""" + # The default sets of information retrieved. + default_info = () + + # Aliases for some not-so-intuitive keys. + keys_alias = {} + + # List of keys to modify. + keys_tomodify_list = () + + # Function used to compare two instances of this class. + cmpFunct = None + + # Regular expression used to build the 'full-size (headshot|cover url)'. + _re_fullsizeURL = re.compile(r'\._V1\._SX(\d+)_SY(\d+)_') + + def __init__(self, myID=None, data=None, notes=u'', + currentRole=u'', roleID=None, roleIsPerson=False, + accessSystem=None, titlesRefs=None, namesRefs=None, + charactersRefs=None, modFunct=None, *args, **kwds): + """Initialize a Movie, Person, Character or Company object. + *myID* -- your personal identifier for this object. + *data* -- a dictionary used to initialize the object. + *notes* -- notes for the person referred in the currentRole + attribute; e.g.: '(voice)' or the alias used in the + movie credits. + *accessSystem* -- a string representing the data access system used. + *currentRole* -- a Character instance representing the current role + or duty of a person in this movie, or a Person + object representing the actor/actress who played + a given character in a Movie. If a string is + passed, an object is automatically build. + *roleID* -- if available, the characterID/personID of the currentRole + object. + *roleIsPerson* -- when False (default) the currentRole is assumed + to be a Character object, otherwise a Person. + *titlesRefs* -- a dictionary with references to movies. + *namesRefs* -- a dictionary with references to persons. + *charactersRefs* -- a dictionary with references to characters. + *modFunct* -- function called returning text fields. + """ + self.reset() + self.accessSystem = accessSystem + self.myID = myID + if data is None: data = {} + self.set_data(data, override=1) + self.notes = notes + if titlesRefs is None: titlesRefs = {} + self.update_titlesRefs(titlesRefs) + if namesRefs is None: namesRefs = {} + self.update_namesRefs(namesRefs) + if charactersRefs is None: charactersRefs = {} + self.update_charactersRefs(charactersRefs) + self.set_mod_funct(modFunct) + self.keys_tomodify = {} + for item in self.keys_tomodify_list: + self.keys_tomodify[item] = None + self._roleIsPerson = roleIsPerson + if not roleIsPerson: + from imdb.Character import Character + self._roleClass = Character + else: + from imdb.Person import Person + self._roleClass = Person + self.currentRole = currentRole + if roleID: + self.roleID = roleID + self._init(*args, **kwds) + + def _get_roleID(self): + """Return the characterID or personID of the currentRole object.""" + if not self.__role: + return None + if isinstance(self.__role, list): + return [x.getID() for x in self.__role] + return self.currentRole.getID() + + def _set_roleID(self, roleID): + """Set the characterID or personID of the currentRole object.""" + if not self.__role: + # XXX: needed? Just ignore it? It's probably safer to + # ignore it, to prevent some bugs in the parsers. + #raise IMDbError,"Can't set ID of an empty Character/Person object." + pass + if not self._roleIsPerson: + if not isinstance(roleID, (list, tuple)): + self.currentRole.characterID = roleID + else: + for index, item in enumerate(roleID): + self.__role[index].characterID = item + else: + if not isinstance(roleID, (list, tuple)): + self.currentRole.personID = roleID + else: + for index, item in enumerate(roleID): + self.__role[index].personID = item + + roleID = property(_get_roleID, _set_roleID, + doc="the characterID or personID of the currentRole object.") + + def _get_currentRole(self): + """Return a Character or Person instance.""" + if self.__role: + return self.__role + return self._roleClass(name=u'', accessSystem=self.accessSystem, + modFunct=self.modFunct) + + def _set_currentRole(self, role): + """Set self.currentRole to a Character or Person instance.""" + if isinstance(role, (unicode, str)): + if not role: + self.__role = None + else: + self.__role = self._roleClass(name=role, modFunct=self.modFunct, + accessSystem=self.accessSystem) + elif isinstance(role, (list, tuple)): + self.__role = RolesList() + for item in role: + if isinstance(item, (unicode, str)): + self.__role.append(self._roleClass(name=item, + accessSystem=self.accessSystem, + modFunct=self.modFunct)) + else: + self.__role.append(item) + if not self.__role: + self.__role = None + else: + self.__role = role + + currentRole = property(_get_currentRole, _set_currentRole, + doc="The role of a Person in a Movie" + \ + " or the interpreter of a Character in a Movie.") + + def _init(self, **kwds): pass + + def reset(self): + """Reset the object.""" + self.data = {} + self.myID = None + self.notes = u'' + self.titlesRefs = {} + self.namesRefs = {} + self.charactersRefs = {} + self.modFunct = modClearRefs + self.current_info = [] + self.infoset2keys = {} + self.key2infoset = {} + self.__role = None + self._reset() + + def _reset(self): pass + + def clear(self): + """Reset the dictionary.""" + self.data.clear() + self.notes = u'' + self.titlesRefs = {} + self.namesRefs = {} + self.charactersRefs = {} + self.current_info = [] + self.infoset2keys = {} + self.key2infoset = {} + self.__role = None + self._clear() + + def _clear(self): pass + + def get_current_info(self): + """Return the current set of information retrieved.""" + return self.current_info + + def update_infoset_map(self, infoset, keys, mainInfoset): + """Update the mappings between infoset and keys.""" + if keys is None: + keys = [] + if mainInfoset is not None: + theIS = mainInfoset + else: + theIS = infoset + self.infoset2keys[theIS] = keys + for key in keys: + self.key2infoset[key] = theIS + + def set_current_info(self, ci): + """Set the current set of information retrieved.""" + # XXX:Remove? It's never used and there's no way to update infoset2keys. + self.current_info = ci + + def add_to_current_info(self, val, keys=None, mainInfoset=None): + """Add a set of information to the current list.""" + if val not in self.current_info: + self.current_info.append(val) + self.update_infoset_map(val, keys, mainInfoset) + + def has_current_info(self, val): + """Return true if the given set of information is in the list.""" + return val in self.current_info + + def set_mod_funct(self, modFunct): + """Set the fuction used to modify the strings.""" + if modFunct is None: modFunct = modClearRefs + self.modFunct = modFunct + + def update_titlesRefs(self, titlesRefs): + """Update the dictionary with the references to movies.""" + self.titlesRefs.update(titlesRefs) + + def get_titlesRefs(self): + """Return the dictionary with the references to movies.""" + return self.titlesRefs + + def update_namesRefs(self, namesRefs): + """Update the dictionary with the references to names.""" + self.namesRefs.update(namesRefs) + + def get_namesRefs(self): + """Return the dictionary with the references to names.""" + return self.namesRefs + + def update_charactersRefs(self, charactersRefs): + """Update the dictionary with the references to characters.""" + self.charactersRefs.update(charactersRefs) + + def get_charactersRefs(self): + """Return the dictionary with the references to characters.""" + return self.charactersRefs + + def set_data(self, data, override=0): + """Set the movie data to the given dictionary; if 'override' is + set, the previous data is removed, otherwise the two dictionary + are merged. + """ + if not override: + self.data.update(data) + else: + self.data = data + + def getID(self): + """Return movieID, personID, characterID or companyID.""" + raise NotImplementedError('override this method') + + def __cmp__(self, other): + """Compare two Movie, Person, Character or Company objects.""" + # XXX: raise an exception? + if self.cmpFunct is None: return -1 + if not isinstance(other, self.__class__): return -1 + return self.cmpFunct(other) + + def __hash__(self): + """Hash for this object.""" + # XXX: does it always work correctly? + theID = self.getID() + if theID is not None and self.accessSystem not in ('UNKNOWN', None): + # Handle 'http' and 'mobile' as they are the same access system. + acs = self.accessSystem + if acs in ('mobile', 'httpThin'): + acs = 'http' + # There must be some indication of the kind of the object, too. + s4h = '%s:%s[%s]' % (self.__class__.__name__, theID, acs) + else: + s4h = repr(self) + return hash(s4h) + + def isSame(self, other): + """Return True if the two represent the same object.""" + if not isinstance(other, self.__class__): return 0 + if hash(self) == hash(other): return 1 + return 0 + + def __len__(self): + """Number of items in the data dictionary.""" + return len(self.data) + + def getAsXML(self, key, _with_add_keys=True): + """Return a XML representation of the specified key, or None + if empty. If _with_add_keys is False, dinamically generated + keys are excluded.""" + # Prevent modifyStrings in __getitem__ to be called; if needed, + # it will be called by the _normalizeValue function. + origModFunct = self.modFunct + self.modFunct = modNull + # XXX: not totally sure it's a good idea, but could prevent + # problems (i.e.: the returned string always contains + # a DTD valid tag, and not something that can be only in + # the keys_alias map). + key = self.keys_alias.get(key, key) + if (not _with_add_keys) and (key in self._additional_keys()): + self.modFunct = origModFunct + return None + try: + withRefs = False + if key in self.keys_tomodify and \ + origModFunct not in (None, modNull): + withRefs = True + value = self.get(key) + if value is None: + return None + tag = self.__class__.__name__.lower() + return u''.join(_seq2xml({key: value}, withRefs=withRefs, + modFunct=origModFunct, + titlesRefs=self.titlesRefs, + namesRefs=self.namesRefs, + charactersRefs=self.charactersRefs, + key2infoset=self.key2infoset, + fullpath=tag)) + finally: + self.modFunct = origModFunct + + def asXML(self, _with_add_keys=True): + """Return a XML representation of the whole object. + If _with_add_keys is False, dinamically generated keys are excluded.""" + beginTag, endTag = _tag4TON(self, addAccessSystem=True, + _containerOnly=True) + resList = [beginTag] + for key in self.keys(): + value = self.getAsXML(key, _with_add_keys=_with_add_keys) + if not value: + continue + resList.append(value) + resList.append(endTag) + head = _xmlHead % self.__class__.__name__.lower() + return head + u''.join(resList) + + def _getitem(self, key): + """Handle special keys.""" + return None + + def __getitem__(self, key): + """Return the value for a given key, checking key aliases; + a KeyError exception is raised if the key is not found. + """ + value = self._getitem(key) + if value is not None: return value + # Handle key aliases. + key = self.keys_alias.get(key, key) + rawData = self.data[key] + if key in self.keys_tomodify and \ + self.modFunct not in (None, modNull): + try: + return modifyStrings(rawData, self.modFunct, self.titlesRefs, + self.namesRefs, self.charactersRefs) + except RuntimeError, e: + # Symbian/python 2.2 has a poor regexp implementation. + import warnings + warnings.warn('RuntimeError in ' + "imdb.utils._Container.__getitem__; if it's not " + "a recursion limit exceeded and we're not running " + "in a Symbian environment, it's a bug:\n%s" % e) + return rawData + + def __setitem__(self, key, item): + """Directly store the item with the given key.""" + self.data[key] = item + + def __delitem__(self, key): + """Remove the given section or key.""" + # XXX: how to remove an item of a section? + del self.data[key] + + def _additional_keys(self): + """Valid keys to append to the data.keys() list.""" + return [] + + def keys(self): + """Return a list of valid keys.""" + return self.data.keys() + self._additional_keys() + + def items(self): + """Return the items in the dictionary.""" + return [(k, self.get(k)) for k in self.keys()] + + # XXX: is this enough? + def iteritems(self): return self.data.iteritems() + def iterkeys(self): return self.data.iterkeys() + def itervalues(self): return self.data.itervalues() + + def values(self): + """Return the values in the dictionary.""" + return [self.get(k) for k in self.keys()] + + def has_key(self, key): + """Return true if a given section is defined.""" + try: + self.__getitem__(key) + except KeyError: + return 0 + return 1 + + # XXX: really useful??? + # consider also that this will confuse people who meant to + # call ia.update(movieObject, 'data set') instead. + def update(self, dict): + self.data.update(dict) + + def get(self, key, failobj=None): + """Return the given section, or default if it's not found.""" + try: + return self.__getitem__(key) + except KeyError: + return failobj + + def setdefault(self, key, failobj=None): + if not self.has_key(key): + self[key] = failobj + return self[key] + + def pop(self, key, *args): + return self.data.pop(key, *args) + + def popitem(self): + return self.data.popitem() + + def __repr__(self): + """String representation of an object.""" + raise NotImplementedError('override this method') + + def __str__(self): + """Movie title or person name.""" + raise NotImplementedError('override this method') + + def __contains__(self, key): + raise NotImplementedError('override this method') + + def append_item(self, key, item): + """The item is appended to the list identified by the given key.""" + self.data.setdefault(key, []).append(item) + + def set_item(self, key, item): + """Directly store the item with the given key.""" + self.data[key] = item + + def __nonzero__(self): + """Return true if self.data contains something.""" + if self.data: return 1 + return 0 + + def __deepcopy__(self, memo): + raise NotImplementedError('override this method') + + def copy(self): + """Return a deep copy of the object itself.""" + return deepcopy(self) + + +def flatten(seq, toDescend=(list, dict, tuple), yieldDictKeys=0, + onlyKeysType=(_Container,), scalar=None): + """Iterate over nested lists and dictionaries; toDescend is a list + or a tuple of types to be considered non-scalar; if yieldDictKeys is + true, also dictionaries' keys are yielded; if scalar is not None, only + items of the given type(s) are yielded.""" + if scalar is None or isinstance(seq, scalar): + yield seq + if isinstance(seq, toDescend): + if isinstance(seq, (dict, _Container)): + if yieldDictKeys: + # Yield also the keys of the dictionary. + for key in seq.iterkeys(): + for k in flatten(key, toDescend=toDescend, + yieldDictKeys=yieldDictKeys, + onlyKeysType=onlyKeysType, scalar=scalar): + if onlyKeysType and isinstance(k, onlyKeysType): + yield k + for value in seq.itervalues(): + for v in flatten(value, toDescend=toDescend, + yieldDictKeys=yieldDictKeys, + onlyKeysType=onlyKeysType, scalar=scalar): + yield v + elif not isinstance(seq, (str, unicode, int, float)): + for item in seq: + for i in flatten(item, toDescend=toDescend, + yieldDictKeys=yieldDictKeys, + onlyKeysType=onlyKeysType, scalar=scalar): + yield i + + diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index d8ea48fd13a58e51bebf0078ec2de9e8f171dcb9..689684643762b576b78b795d6686b4c1d4bc2ae5 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -1,1543 +1,1605 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import cherrypy -import webbrowser -import sqlite3 -import datetime -import socket -import os, sys, subprocess, re -import urllib - -from threading import Lock - -# apparently py2exe won't build these unless they're imported somewhere -from sickbeard import providers, metadata -from providers import ezrss, tvtorrents, torrentleech, btn, nzbsrus, newznab, womble, nzbx, omgwtfnzbs, binnewz, t411, cpasbien, piratebay, gks, kat -from sickbeard.config import CheckSection, check_setting_int, check_setting_str, ConfigMigrator - -from sickbeard import searchCurrent, searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, subtitles, traktWatchListChecker -from sickbeard import helpers, db, exceptions, show_queue, search_queue, scheduler -from sickbeard import logger -from sickbeard import naming - -from common import SD, SKIPPED, NAMING_REPEAT - -from sickbeard.databases import mainDB, cache_db - -from lib.configobj import ConfigObj - -invoked_command = None - -SOCKET_TIMEOUT = 30 - -PID = None - -CFG = None -CONFIG_FILE = None - -# this is the version of the config we EXPECT to find -CONFIG_VERSION = 1 - -PROG_DIR = '.' -MY_FULLNAME = None -MY_NAME = None -MY_ARGS = [] -SYS_ENCODING = '' -DATA_DIR = '' -CREATEPID = False -PIDFILE = '' - -DAEMON = None -NO_RESIZE = False - -backlogSearchScheduler = None -currentSearchScheduler = None -showUpdateScheduler = None -versionCheckScheduler = None -showQueueScheduler = None -searchQueueScheduler = None -properFinderScheduler = None -autoPostProcesserScheduler = None -autoTorrentPostProcesserScheduler = None -subtitlesFinderScheduler = None -traktWatchListCheckerSchedular = None - -showList = None -loadingShowList = None - -providerList = [] -newznabProviderList = [] -metadata_provider_dict = {} - -NEWEST_VERSION = None -NEWEST_VERSION_STRING = None -VERSION_NOTIFY = None - -INIT_LOCK = Lock() -__INITIALIZED__ = False -started = False - -LOG_DIR = None - -WEB_PORT = None -WEB_LOG = None -WEB_ROOT = None -WEB_USERNAME = None -WEB_PASSWORD = None -WEB_HOST = None -WEB_IPV6 = None - -USE_API = False -API_KEY = None - -ENABLE_HTTPS = False -HTTPS_CERT = None -HTTPS_KEY = None - -LAUNCH_BROWSER = None -CACHE_DIR = None -ACTUAL_CACHE_DIR = None -ROOT_DIRS = None - -USE_BANNER = None -USE_LISTVIEW = None -METADATA_XBMC = None -METADATA_XBMCFRODO = None -METADATA_MEDIABROWSER = None -METADATA_PS3 = None -METADATA_WDTV = None -METADATA_TIVO = None -METADATA_SYNOLOGY = None - -QUALITY_DEFAULT = None -STATUS_DEFAULT = None -FLATTEN_FOLDERS_DEFAULT = None -AUDIO_SHOW_DEFAULT = None -SUBTITLES_DEFAULT = None -PROVIDER_ORDER = [] - -NAMING_MULTI_EP = None -NAMING_PATTERN = None -NAMING_ABD_PATTERN = None -NAMING_CUSTOM_ABD = None -NAMING_FORCE_FOLDERS = False - -TVDB_API_KEY = '9DAF49C96CBF8DAC' -TVDB_BASE_URL = None -TVDB_API_PARMS = {} - -USE_NZBS = None -USE_TORRENTS = None - -NZB_METHOD = None -NZB_DIR = None -USENET_RETENTION = None -TORRENT_METHOD = None -TORRENT_DIR = None -DOWNLOAD_PROPERS = None -PREFERED_METHOD = None -SEARCH_FREQUENCY = None -BACKLOG_SEARCH_FREQUENCY = 1 - -MIN_SEARCH_FREQUENCY = 10 - -DEFAULT_SEARCH_FREQUENCY = 60 - -EZRSS = False -TVTORRENTS = False -TVTORRENTS_DIGEST = None -TVTORRENTS_HASH = None - -TORRENTLEECH = False -TORRENTLEECH_KEY = None - -BTN = False -BTN_API_KEY = None - -TORRENT_DIR = None - -ADD_SHOWS_WO_DIR = None -CREATE_MISSING_SHOW_DIRS = None -RENAME_EPISODES = False -PROCESS_AUTOMATICALLY = False -PROCESS_AUTOMATICALLY_TORRENT = False -KEEP_PROCESSED_DIR = False -MOVE_ASSOCIATED_FILES = False -TV_DOWNLOAD_DIR = None -TORRENT_DOWNLOAD_DIR = None - -NZBS = False -NZBS_UID = None -NZBS_HASH = None - -WOMBLE = False - -NZBX = False -NZBX_COMPLETION = 100 - -OMGWTFNZBS = False -OMGWTFNZBS_UID = None -OMGWTFNZBS_KEY = None - -NZBSRUS = False -NZBSRUS_UID = None -NZBSRUS_HASH = None - -BINNEWZ = False - -T411 = False -T411_USERNAME = None -T411_PASSWORD = None - -THEPIRATEBAY = False -THEPIRATEBAY_TRUSTED = True -THEPIRATEBAY_PROXY = False -THEPIRATEBAY_PROXY_URL = None - -Cpasbien = False -kat = False - -NZBMATRIX = False -NZBMATRIX_USERNAME = None -NZBMATRIX_APIKEY = None - -NEWZBIN = False -NEWZBIN_USERNAME = None -NEWZBIN_PASSWORD = None - -SAB_USERNAME = None -SAB_PASSWORD = None -SAB_APIKEY = None -SAB_CATEGORY = None -SAB_HOST = '' - -NZBGET_PASSWORD = None -NZBGET_CATEGORY = None -NZBGET_HOST = None - -GKS = False -GKS_KEY = None - -TORRENT_USERNAME = None -TORRENT_PASSWORD = None -TORRENT_HOST = '' -TORRENT_PATH = '' -TORRENT_RATIO = '' -TORRENT_PAUSED = False -TORRENT_LABEL = '' - -USE_XBMC = False -XBMC_NOTIFY_ONSNATCH = False -XBMC_NOTIFY_ONDOWNLOAD = False -XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = False -XBMC_UPDATE_LIBRARY = False -XBMC_UPDATE_FULL = False -XBMC_UPDATE_ONLYFIRST = False -XBMC_HOST = '' -XBMC_USERNAME = None -XBMC_PASSWORD = None - -USE_PLEX = False -PLEX_NOTIFY_ONSNATCH = False -PLEX_NOTIFY_ONDOWNLOAD = False -PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = False -PLEX_UPDATE_LIBRARY = False -PLEX_SERVER_HOST = None -PLEX_HOST = None -PLEX_USERNAME = None -PLEX_PASSWORD = None - -USE_GROWL = False -GROWL_NOTIFY_ONSNATCH = False -GROWL_NOTIFY_ONDOWNLOAD = False -GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = False -GROWL_HOST = '' -GROWL_PASSWORD = None - -USE_PROWL = False -PROWL_NOTIFY_ONSNATCH = False -PROWL_NOTIFY_ONDOWNLOAD = False -PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = False -PROWL_API = None -PROWL_PRIORITY = 0 - -USE_TWITTER = False -TWITTER_NOTIFY_ONSNATCH = False -TWITTER_NOTIFY_ONDOWNLOAD = False -TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = False -TWITTER_USERNAME = None -TWITTER_PASSWORD = None -TWITTER_PREFIX = None - -USE_NOTIFO = False -NOTIFO_NOTIFY_ONSNATCH = False -NOTIFO_NOTIFY_ONDOWNLOAD = False -NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = False -NOTIFO_USERNAME = None -NOTIFO_APISECRET = None -NOTIFO_PREFIX = None - -USE_BOXCAR = False -BOXCAR_NOTIFY_ONSNATCH = False -BOXCAR_NOTIFY_ONDOWNLOAD = False -BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = False -BOXCAR_USERNAME = None -BOXCAR_PASSWORD = None -BOXCAR_PREFIX = None - -USE_PUSHOVER = False -PUSHOVER_NOTIFY_ONSNATCH = False -PUSHOVER_NOTIFY_ONDOWNLOAD = False -PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHOVER_USERKEY = None - -USE_LIBNOTIFY = False -LIBNOTIFY_NOTIFY_ONSNATCH = False -LIBNOTIFY_NOTIFY_ONDOWNLOAD = False -LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = False - -USE_NMJ = False -NMJ_HOST = None -NMJ_DATABASE = None -NMJ_MOUNT = None - -USE_SYNOINDEX = False - -USE_NMJv2 = False -NMJv2_HOST = None -NMJv2_DATABASE = None -NMJv2_DBLOC = None - -USE_TRAKT = False -TRAKT_USERNAME = None -TRAKT_PASSWORD = None -TRAKT_API = '' -TRAKT_REMOVE_WATCHLIST = False -TRAKT_USE_WATCHLIST = False -TRAKT_METHOD_ADD = 0 -TRAKT_START_PAUSED = False - -USE_PYTIVO = False -PYTIVO_NOTIFY_ONSNATCH = False -PYTIVO_NOTIFY_ONDOWNLOAD = False -PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = False -PYTIVO_UPDATE_LIBRARY = False -PYTIVO_HOST = '' -PYTIVO_SHARE_NAME = '' -PYTIVO_TIVO_NAME = '' - -USE_NMA = False -NMA_NOTIFY_ONSNATCH = False -NMA_NOTIFY_ONDOWNLOAD = False -NMA_NOTIFY_ONSUBTITLEDOWNLOAD = False -NMA_API = None -NMA_PRIORITY = 0 - -USE_MAIL = False -MAIL_USERNAME = None -MAIL_PASSWORD = None -MAIL_SERVER = None -MAIL_SSL = False -MAIL_FROM = None -MAIL_TO = None -MAIL_NOTIFY_ONSNATCH = False - -COMING_EPS_LAYOUT = None -COMING_EPS_DISPLAY_PAUSED = None -COMING_EPS_SORT = None -COMING_EPS_MISSED_RANGE = None - -USE_SUBTITLES = False -SUBTITLES_LANGUAGES = [] -SUBTITLES_DIR = '' -SUBTITLES_DIR_SUB = False -SUBSNOLANG = False -SUBTITLES_SERVICES_LIST = [] -SUBTITLES_SERVICES_ENABLED = [] -SUBTITLES_HISTORY = False - -DISPLAY_POSTERS = None - -EXTRA_SCRIPTS = [] - -GIT_PATH = None - -IGNORE_WORDS = "german,spanish,core2hd,dutch,swedish" - -__INITIALIZED__ = False - - -def get_backlog_cycle_time(): - cycletime = SEARCH_FREQUENCY * 2 + 7 - return max([cycletime, 120]) - - -def initialize(consoleLogging=True): - - with INIT_LOCK: - - global LOG_DIR, WEB_PORT, WEB_LOG, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, USE_API, API_KEY, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ - USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, TORRENT_METHOD, PREFERED_METHOD, \ - SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_HOST, \ - NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, currentSearchScheduler, backlogSearchScheduler, \ - TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_RATIO, TORRENT_PAUSED, TORRENT_LABEL, \ - USE_XBMC, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \ - XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, \ - USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API,TRAKT_REMOVE_WATCHLIST,TRAKT_USE_WATCHLIST,TRAKT_METHOD_ADD,TRAKT_START_PAUSED,traktWatchListCheckerSchedular, \ - USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \ - PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, \ - showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, DISPLAY_POSTERS, showList, loadingShowList, \ - NZBS, NZBS_UID, NZBS_HASH, EZRSS, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, BTN, BTN_API_KEY, TORRENTLEECH, TORRENTLEECH_KEY, TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \ - BINNEWZ, \ - T411, T411_USERNAME, T411_PASSWORD, \ - THEPIRATEBAY, THEPIRATEBAY_PROXY, THEPIRATEBAY_PROXY_URL, THEPIRATEBAY_TRUSTED, \ - Cpasbien, \ - kat, \ - SEARCH_FREQUENCY, DEFAULT_SEARCH_FREQUENCY, BACKLOG_SEARCH_FREQUENCY, \ - QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, AUDIO_SHOW_DEFAULT, \ - GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, \ - USE_GROWL, GROWL_HOST, GROWL_PASSWORD, USE_PROWL, PROWL_NOTIFY_ONSNATCH, PROWL_NOTIFY_ONDOWNLOAD, PROWL_NOTIFY_ONSUBTITLEDOWNLOAD, PROWL_API, PROWL_PRIORITY, PROG_DIR, NZBMATRIX, NZBMATRIX_USERNAME, \ - USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ - USE_NMA, NMA_NOTIFY_ONSNATCH, NMA_NOTIFY_ONDOWNLOAD, NMA_NOTIFY_ONSUBTITLEDOWNLOAD, NMA_API, NMA_PRIORITY, \ - USE_MAIL, MAIL_USERNAME, MAIL_PASSWORD, MAIL_SERVER, MAIL_SSL, MAIL_FROM, MAIL_TO, MAIL_NOTIFY_ONSNATCH, \ - NZBMATRIX_APIKEY, versionCheckScheduler, VERSION_NOTIFY, PROCESS_AUTOMATICALLY, PROCESS_AUTOMATICALLY_TORRENT, \ - KEEP_PROCESSED_DIR, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ - showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TVDB_API_PARMS, \ - NAMING_PATTERN, NAMING_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, \ - RENAME_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, \ - NZBSRUS, NZBSRUS_UID, NZBSRUS_HASH, WOMBLE, NZBX, NZBX_COMPLETION, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_KEY, providerList, newznabProviderList, \ - EXTRA_SCRIPTS, USE_TWITTER, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \ - USE_NOTIFO, NOTIFO_USERNAME, NOTIFO_APISECRET, NOTIFO_NOTIFY_ONDOWNLOAD, NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD, NOTIFO_NOTIFY_ONSNATCH, \ - USE_BOXCAR, BOXCAR_USERNAME, BOXCAR_PASSWORD, BOXCAR_NOTIFY_ONDOWNLOAD, BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR_NOTIFY_ONSNATCH, \ - USE_PUSHOVER, PUSHOVER_USERKEY, PUSHOVER_NOTIFY_ONDOWNLOAD, PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHOVER_NOTIFY_ONSNATCH, \ - USE_LIBNOTIFY, LIBNOTIFY_NOTIFY_ONSNATCH, LIBNOTIFY_NOTIFY_ONDOWNLOAD, LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD, USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, USE_NMJv2, NMJv2_HOST, NMJv2_DATABASE, NMJv2_DBLOC, USE_SYNOINDEX, \ - USE_BANNER, USE_LISTVIEW, METADATA_XBMC, METADATA_XBMCFRODO, METADATA_MEDIABROWSER, METADATA_PS3, METADATA_SYNOLOGY, metadata_provider_dict, \ - NEWZBIN, NEWZBIN_USERNAME, NEWZBIN_PASSWORD, GIT_PATH, MOVE_ASSOCIATED_FILES, \ - GKS, GKS_KEY, \ - COMING_EPS_LAYOUT, COMING_EPS_SORT, COMING_EPS_DISPLAY_PAUSED, COMING_EPS_MISSED_RANGE, METADATA_WDTV, METADATA_TIVO, IGNORE_WORDS, CREATE_MISSING_SHOW_DIRS, \ - ADD_SHOWS_WO_DIR, USE_SUBTITLES, SUBTITLES_LANGUAGES, SUBTITLES_DIR, SUBTITLES_DIR_SUB, SUBSNOLANG, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_HISTORY, subtitlesFinderScheduler - - - if __INITIALIZED__: - return False - - socket.setdefaulttimeout(SOCKET_TIMEOUT) - - CheckSection(CFG, 'General') - LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', 'Logs') - if not helpers.makeDir(LOG_DIR): - logger.log(u"!!! No log folder, logging to screen only!", logger.ERROR) - - try: - WEB_PORT = check_setting_int(CFG, 'General', 'web_port', 8081) - except: - WEB_PORT = 8081 - - if WEB_PORT < 21 or WEB_PORT > 65535: - WEB_PORT = 8081 - - WEB_HOST = check_setting_str(CFG, 'General', 'web_host', '0.0.0.0') - WEB_IPV6 = bool(check_setting_int(CFG, 'General', 'web_ipv6', 0)) - WEB_ROOT = check_setting_str(CFG, 'General', 'web_root', '').rstrip("/") - WEB_LOG = bool(check_setting_int(CFG, 'General', 'web_log', 0)) - WEB_USERNAME = check_setting_str(CFG, 'General', 'web_username', '') - WEB_PASSWORD = check_setting_str(CFG, 'General', 'web_password', '') - LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) - DISPLAY_POSTERS = bool(check_setting_int(CFG, 'General', 'display_posters', 1)) - - USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0)) - API_KEY = check_setting_str(CFG, 'General', 'api_key', '') - - ENABLE_HTTPS = bool(check_setting_int(CFG, 'General', 'enable_https', 0)) - HTTPS_CERT = check_setting_str(CFG, 'General', 'https_cert', 'server.crt') - HTTPS_KEY = check_setting_str(CFG, 'General', 'https_key', 'server.key') - - ACTUAL_CACHE_DIR = check_setting_str(CFG, 'General', 'cache_dir', 'cache') - # fix bad configs due to buggy code - if ACTUAL_CACHE_DIR == 'None': - ACTUAL_CACHE_DIR = 'cache' - - # unless they specify, put the cache dir inside the data dir - if not os.path.isabs(ACTUAL_CACHE_DIR): - CACHE_DIR = os.path.join(DATA_DIR, ACTUAL_CACHE_DIR) - else: - CACHE_DIR = ACTUAL_CACHE_DIR - - if not helpers.makeDir(CACHE_DIR): - logger.log(u"!!! Creating local cache dir failed, using system default", logger.ERROR) - CACHE_DIR = None - - ROOT_DIRS = check_setting_str(CFG, 'General', 'root_dirs', '') - if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', ROOT_DIRS): - ROOT_DIRS = '' - - proxies = urllib.getproxies() - proxy_url = None # @UnusedVariable - if 'http' in proxies: - proxy_url = proxies['http'] # @UnusedVariable - elif 'ftp' in proxies: - proxy_url = proxies['ftp'] # @UnusedVariable - - # Set our common tvdb_api options here - TVDB_API_PARMS = {'apikey': TVDB_API_KEY, - 'language': 'en', - 'useZip': True} - - if CACHE_DIR: - TVDB_API_PARMS['cache'] = os.path.join(CACHE_DIR, 'tvdb') - - TVDB_BASE_URL = 'http://thetvdb.com/api/' + TVDB_API_KEY - - QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) - STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) - AUDIO_SHOW_DEFAULT = check_setting_str(CFG, 'General', 'audio_show_default', 'fr' ) - VERSION_NOTIFY = check_setting_int(CFG, 'General', 'version_notify', 1) - FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0)) - - PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split() - - NAMING_PATTERN = check_setting_str(CFG, 'General', 'naming_pattern', '') - NAMING_ABD_PATTERN = check_setting_str(CFG, 'General', 'naming_abd_pattern', '') - NAMING_CUSTOM_ABD = check_setting_int(CFG, 'General', 'naming_custom_abd', 0) - NAMING_MULTI_EP = check_setting_int(CFG, 'General', 'naming_multi_ep', 1) - NAMING_FORCE_FOLDERS = naming.check_force_season_folders() - - USE_NZBS = bool(check_setting_int(CFG, 'General', 'use_nzbs', 1)) - USE_TORRENTS = bool(check_setting_int(CFG, 'General', 'use_torrents', 0)) - - NZB_METHOD = check_setting_str(CFG, 'General', 'nzb_method', 'blackhole') - if NZB_METHOD not in ('blackhole', 'sabnzbd', 'nzbget'): - NZB_METHOD = 'blackhole' - - TORRENT_METHOD = check_setting_str(CFG, 'General', 'torrent_method', 'blackhole') - if TORRENT_METHOD not in ('blackhole', 'utorrent', 'transmission', 'deluge', 'download_station'): - TORRENT_METHOD = 'blackhole' - - PREFERED_METHOD = check_setting_str(CFG, 'General', 'prefered_method', 'nzb') - if PREFERED_METHOD not in ('torrent', 'nzb'): - PREFERED_METHOD = 'nzb' - - DOWNLOAD_PROPERS = bool(check_setting_int(CFG, 'General', 'download_propers', 1)) - USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', 500) - SEARCH_FREQUENCY = check_setting_int(CFG, 'General', 'search_frequency', DEFAULT_SEARCH_FREQUENCY) - if SEARCH_FREQUENCY < MIN_SEARCH_FREQUENCY: - SEARCH_FREQUENCY = MIN_SEARCH_FREQUENCY - - TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_dir', '') - - TV_DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'tv_download_dir', '') - TORRENT_DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'torrent_download_dir', '') - PROCESS_AUTOMATICALLY = check_setting_int(CFG, 'General', 'process_automatically', 0) - PROCESS_AUTOMATICALLY_TORRENT = check_setting_int(CFG, 'General', 'process_automatically_torrent', 0) - RENAME_EPISODES = check_setting_int(CFG, 'General', 'rename_episodes', 1) - KEEP_PROCESSED_DIR = check_setting_int(CFG, 'General', 'keep_processed_dir', 1) - MOVE_ASSOCIATED_FILES = check_setting_int(CFG, 'General', 'move_associated_files', 0) - CREATE_MISSING_SHOW_DIRS = check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0) - ADD_SHOWS_WO_DIR = check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0) - - EZRSS = bool(check_setting_int(CFG, 'General', 'use_torrent', 0)) - if not EZRSS: - CheckSection(CFG, 'EZRSS') - EZRSS = bool(check_setting_int(CFG, 'EZRSS', 'ezrss', 0)) - - GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') - IGNORE_WORDS = check_setting_str(CFG, 'General', 'ignore_words', '') - EXTRA_SCRIPTS = [x for x in check_setting_str(CFG, 'General', 'extra_scripts', '').split('|') if x] - - USE_BANNER = bool(check_setting_int(CFG, 'General', 'use_banner', 0)) - USE_LISTVIEW = bool(check_setting_int(CFG, 'General', 'use_listview', 0)) - METADATA_TYPE = check_setting_str(CFG, 'General', 'metadata_type', '') - - metadata_provider_dict = metadata.get_metadata_generator_dict() - - # if this exists it's legacy, use the info to upgrade metadata to the new settings - if METADATA_TYPE: - - old_metadata_class = None - - if METADATA_TYPE == 'xbmc': - old_metadata_class = metadata.xbmc.metadata_class - elif METADATA_TYPE == 'xbmcfrodo': - old_metadata_class = metadata.xbmcfrodo.metadata_class - elif METADATA_TYPE == 'mediabrowser': - old_metadata_class = metadata.mediabrowser.metadata_class - elif METADATA_TYPE == 'ps3': - old_metadata_class = metadata.ps3.metadata_class - - if old_metadata_class: - - METADATA_SHOW = bool(check_setting_int(CFG, 'General', 'metadata_show', 1)) - METADATA_EPISODE = bool(check_setting_int(CFG, 'General', 'metadata_episode', 1)) - - ART_POSTER = bool(check_setting_int(CFG, 'General', 'art_poster', 1)) - ART_FANART = bool(check_setting_int(CFG, 'General', 'art_fanart', 1)) - ART_THUMBNAILS = bool(check_setting_int(CFG, 'General', 'art_thumbnails', 1)) - ART_SEASON_THUMBNAILS = bool(check_setting_int(CFG, 'General', 'art_season_thumbnails', 1)) - - new_metadata_class = old_metadata_class(METADATA_SHOW, - METADATA_EPISODE, - ART_POSTER, - ART_FANART, - ART_THUMBNAILS, - ART_SEASON_THUMBNAILS) - - metadata_provider_dict[new_metadata_class.name] = new_metadata_class - - # this is the normal codepath for metadata config - else: - METADATA_XBMC = check_setting_str(CFG, 'General', 'metadata_xbmc', '0|0|0|0|0|0') - METADATA_XBMCFRODO = check_setting_str(CFG, 'General', 'metadata_xbmcfrodo', '0|0|0|0|0|0') - METADATA_MEDIABROWSER = check_setting_str(CFG, 'General', 'metadata_mediabrowser', '0|0|0|0|0|0') - METADATA_PS3 = check_setting_str(CFG, 'General', 'metadata_ps3', '0|0|0|0|0|0') - METADATA_WDTV = check_setting_str(CFG, 'General', 'metadata_wdtv', '0|0|0|0|0|0') - METADATA_TIVO = check_setting_str(CFG, 'General', 'metadata_tivo', '0|0|0|0|0|0') - METADATA_SYNOLOGY = check_setting_str(CFG, 'General', 'metadata_synology', '0|0|0|0|0|0') - - for cur_metadata_tuple in [(METADATA_XBMC, metadata.xbmc), - (METADATA_XBMCFRODO, metadata.xbmcfrodo), - (METADATA_MEDIABROWSER, metadata.mediabrowser), - (METADATA_PS3, metadata.ps3), - (METADATA_WDTV, metadata.wdtv), - (METADATA_TIVO, metadata.tivo), - (METADATA_SYNOLOGY, metadata.synology), - ]: - - (cur_metadata_config, cur_metadata_class) = cur_metadata_tuple - tmp_provider = cur_metadata_class.metadata_class() - tmp_provider.set_config(cur_metadata_config) - metadata_provider_dict[tmp_provider.name] = tmp_provider - - CheckSection(CFG, 'GUI') - COMING_EPS_LAYOUT = check_setting_str(CFG, 'GUI', 'coming_eps_layout', 'banner') - COMING_EPS_DISPLAY_PAUSED = bool(check_setting_int(CFG, 'GUI', 'coming_eps_display_paused', 0)) - COMING_EPS_SORT = check_setting_str(CFG, 'GUI', 'coming_eps_sort', 'date') - - CheckSection(CFG, 'Newznab') - newznabData = check_setting_str(CFG, 'Newznab', 'newznab_data', '') - newznabProviderList = providers.getNewznabProviderList(newznabData) - providerList = providers.makeProviderList() - - CheckSection(CFG, 'Blackhole') - NZB_DIR = check_setting_str(CFG, 'Blackhole', 'nzb_dir', '') - TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_dir', '') - - CheckSection(CFG, 'TVTORRENTS') - TVTORRENTS = bool(check_setting_int(CFG, 'TVTORRENTS', 'tvtorrents', 0)) - TVTORRENTS_DIGEST = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_digest', '') - TVTORRENTS_HASH = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_hash', '') - - CheckSection(CFG, 'BTN') - BTN = bool(check_setting_int(CFG, 'BTN', 'btn', 0)) - BTN_API_KEY = check_setting_str(CFG, 'BTN', 'btn_api_key', '') - - CheckSection(CFG, 'TorrentLeech') - TORRENTLEECH = bool(check_setting_int(CFG, 'TorrentLeech', 'torrentleech', 0)) - TORRENTLEECH_KEY = check_setting_str(CFG, 'TorrentLeech', 'torrentleech_key', '') - - CheckSection(CFG, 'NZBs') - NZBS = bool(check_setting_int(CFG, 'NZBs', 'nzbs', 0)) - NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '') - NZBS_HASH = check_setting_str(CFG, 'NZBs', 'nzbs_hash', '') - - CheckSection(CFG, 'NZBsRUS') - NZBSRUS = bool(check_setting_int(CFG, 'NZBsRUS', 'nzbsrus', 0)) - NZBSRUS_UID = check_setting_str(CFG, 'NZBsRUS', 'nzbsrus_uid', '') - NZBSRUS_HASH = check_setting_str(CFG, 'NZBsRUS', 'nzbsrus_hash', '') - - CheckSection(CFG, 'NZBMatrix') - NZBMATRIX = bool(check_setting_int(CFG, 'NZBMatrix', 'nzbmatrix', 0)) - NZBMATRIX_USERNAME = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_username', '') - NZBMATRIX_APIKEY = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_apikey', '') - - CheckSection(CFG, 'BinNewz') - BINNEWZ = bool(check_setting_int(CFG, 'BinNewz', 'binnewz', 0)) - - CheckSection(CFG, 'T411') - T411 = bool(check_setting_int(CFG, 'T411', 't411', 0)) - T411_USERNAME = check_setting_str(CFG, 'T411', 'username', '') - T411_PASSWORD = check_setting_str(CFG, 'T411', 'password', '') - - CheckSection(CFG, 'PirateBay') - THEPIRATEBAY = bool(check_setting_int(CFG, 'PirateBay', 'piratebay', 0)) - THEPIRATEBAY_PROXY = bool(check_setting_int(CFG, 'PirateBay', 'piratebay_proxy', 0)) - THEPIRATEBAY_PROXY_URL = check_setting_str(CFG, 'PirateBay', 'piratebay_proxy_url', '') - THEPIRATEBAY_TRUSTED = bool(check_setting_int(CFG, 'PirateBay', 'piratebay_trusted', 0)) - - CheckSection(CFG, 'Cpasbien') - Cpasbien = bool(check_setting_int(CFG, 'Cpasbien', 'cpasbien', 0)) - - CheckSection(CFG, 'kat') - kat = bool(check_setting_int(CFG, 'kat', 'kat', 0)) - - CheckSection(CFG, 'Newzbin') - NEWZBIN = bool(check_setting_int(CFG, 'Newzbin', 'newzbin', 0)) - NEWZBIN_USERNAME = check_setting_str(CFG, 'Newzbin', 'newzbin_username', '') - NEWZBIN_PASSWORD = check_setting_str(CFG, 'Newzbin', 'newzbin_password', '') - - CheckSection(CFG, 'Womble') - WOMBLE = bool(check_setting_int(CFG, 'Womble', 'womble', 1)) - - CheckSection(CFG, 'nzbX') - NZBX = bool(check_setting_int(CFG, 'nzbX', 'nzbx', 0)) - NZBX_COMPLETION = check_setting_int(CFG, 'nzbX', 'nzbx_completion', 100) - - CheckSection(CFG, 'omgwtfnzbs') - OMGWTFNZBS = bool(check_setting_int(CFG, 'omgwtfnzbs', 'omgwtfnzbs', 0)) - OMGWTFNZBS_UID = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_uid', '') - OMGWTFNZBS_KEY = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_key', '') - - CheckSection(CFG, 'GKS') - GKS = bool(check_setting_int(CFG, 'GKS', 'gks', 0)) - GKS_KEY = check_setting_str(CFG, 'GKS', 'gks_key', '') - - CheckSection(CFG, 'SABnzbd') - SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') - SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '') - SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '') - SAB_CATEGORY = check_setting_str(CFG, 'SABnzbd', 'sab_category', 'tv') - SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') - - CheckSection(CFG, 'NZBget') - NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', 'tegbzn6789') - NZBGET_CATEGORY = check_setting_str(CFG, 'NZBget', 'nzbget_category', 'tv') - NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '') - - CheckSection(CFG, 'XBMC') - TORRENT_USERNAME = check_setting_str(CFG, 'TORRENT', 'torrent_username', '') - TORRENT_PASSWORD = check_setting_str(CFG, 'TORRENT', 'torrent_password', '') - TORRENT_HOST = check_setting_str(CFG, 'TORRENT', 'torrent_host', '') - TORRENT_PATH = check_setting_str(CFG, 'TORRENT', 'torrent_path', '') - TORRENT_RATIO = check_setting_str(CFG, 'TORRENT', 'torrent_ratio', '') - TORRENT_PAUSED = bool(check_setting_int(CFG, 'TORRENT', 'torrent_paused', 0)) - TORRENT_LABEL = check_setting_str(CFG, 'TORRENT', 'torrent_label', '') - - USE_XBMC = bool(check_setting_int(CFG, 'XBMC', 'use_xbmc', 0)) - XBMC_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_onsnatch', 0)) - XBMC_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_ondownload', 0)) - XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_onsubtitledownload', 0)) - XBMC_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_library', 0)) - XBMC_UPDATE_FULL = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_full', 0)) - XBMC_UPDATE_ONLYFIRST = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_onlyfirst', 0)) - XBMC_HOST = check_setting_str(CFG, 'XBMC', 'xbmc_host', '') - XBMC_USERNAME = check_setting_str(CFG, 'XBMC', 'xbmc_username', '') - XBMC_PASSWORD = check_setting_str(CFG, 'XBMC', 'xbmc_password', '') - - CheckSection(CFG, 'Plex') - USE_PLEX = bool(check_setting_int(CFG, 'Plex', 'use_plex', 0)) - PLEX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsnatch', 0)) - PLEX_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_ondownload', 0)) - PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsubtitledownload', 0)) - PLEX_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Plex', 'plex_update_library', 0)) - PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '') - PLEX_HOST = check_setting_str(CFG, 'Plex', 'plex_host', '') - PLEX_USERNAME = check_setting_str(CFG, 'Plex', 'plex_username', '') - PLEX_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_password', '') - - CheckSection(CFG, 'Growl') - USE_GROWL = bool(check_setting_int(CFG, 'Growl', 'use_growl', 0)) - GROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsnatch', 0)) - GROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_ondownload', 0)) - GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsubtitledownload', 0)) - GROWL_HOST = check_setting_str(CFG, 'Growl', 'growl_host', '') - GROWL_PASSWORD = check_setting_str(CFG, 'Growl', 'growl_password', '') - - CheckSection(CFG, 'Prowl') - USE_PROWL = bool(check_setting_int(CFG, 'Prowl', 'use_prowl', 0)) - PROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsnatch', 0)) - PROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_ondownload', 0)) - PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsubtitledownload', 0)) - PROWL_API = check_setting_str(CFG, 'Prowl', 'prowl_api', '') - PROWL_PRIORITY = check_setting_str(CFG, 'Prowl', 'prowl_priority', "0") - - CheckSection(CFG, 'Twitter') - USE_TWITTER = bool(check_setting_int(CFG, 'Twitter', 'use_twitter', 0)) - TWITTER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_onsnatch', 0)) - TWITTER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_ondownload', 0)) - TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_onsubtitledownload', 0)) - TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '') - TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '') - TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Sick Beard') - - CheckSection(CFG, 'Notifo') - USE_NOTIFO = bool(check_setting_int(CFG, 'Notifo', 'use_notifo', 0)) - NOTIFO_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_onsnatch', 0)) - NOTIFO_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_ondownload', 0)) - NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_onsubtitledownload', 0)) - NOTIFO_USERNAME = check_setting_str(CFG, 'Notifo', 'notifo_username', '') - NOTIFO_APISECRET = check_setting_str(CFG, 'Notifo', 'notifo_apisecret', '') - - CheckSection(CFG, 'Boxcar') - USE_BOXCAR = bool(check_setting_int(CFG, 'Boxcar', 'use_boxcar', 0)) - BOXCAR_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_onsnatch', 0)) - BOXCAR_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_ondownload', 0)) - BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_onsubtitledownload', 0)) - BOXCAR_USERNAME = check_setting_str(CFG, 'Boxcar', 'boxcar_username', '') - - CheckSection(CFG, 'Pushover') - USE_PUSHOVER = bool(check_setting_int(CFG, 'Pushover', 'use_pushover', 0)) - PUSHOVER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsnatch', 0)) - PUSHOVER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_ondownload', 0)) - PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0)) - PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '') - - CheckSection(CFG, 'Libnotify') - USE_LIBNOTIFY = bool(check_setting_int(CFG, 'Libnotify', 'use_libnotify', 0)) - LIBNOTIFY_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsnatch', 0)) - LIBNOTIFY_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_ondownload', 0)) - LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsubtitledownload', 0)) - - CheckSection(CFG, 'NMJ') - USE_NMJ = bool(check_setting_int(CFG, 'NMJ', 'use_nmj', 0)) - NMJ_HOST = check_setting_str(CFG, 'NMJ', 'nmj_host', '') - NMJ_DATABASE = check_setting_str(CFG, 'NMJ', 'nmj_database', '') - NMJ_MOUNT = check_setting_str(CFG, 'NMJ', 'nmj_mount', '') - - CheckSection(CFG, 'NMJv2') - USE_NMJv2 = bool(check_setting_int(CFG, 'NMJv2', 'use_nmjv2', 0)) - NMJv2_HOST = check_setting_str(CFG, 'NMJv2', 'nmjv2_host', '') - NMJv2_DATABASE = check_setting_str(CFG, 'NMJv2', 'nmjv2_database', '') - NMJ_DBLOC = check_setting_str(CFG, 'NMJv2', 'nmjv2_dbloc', '') - - CheckSection(CFG, 'Synology') - USE_SYNOINDEX = bool(check_setting_int(CFG, 'Synology', 'use_synoindex', 0)) - - CheckSection(CFG, 'Trakt') - USE_TRAKT = bool(check_setting_int(CFG, 'Trakt', 'use_trakt', 0)) - TRAKT_USERNAME = check_setting_str(CFG, 'Trakt', 'trakt_username', '') - TRAKT_PASSWORD = check_setting_str(CFG, 'Trakt', 'trakt_password', '') - TRAKT_API = check_setting_str(CFG, 'Trakt', 'trakt_api', '') - TRAKT_REMOVE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_watchlist', 0)) - TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0)) - TRAKT_METHOD_ADD = check_setting_str(CFG, 'Trakt', 'trakt_method_add', "0") - TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0)) - - CheckSection(CFG, 'pyTivo') - USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0)) - PYTIVO_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsnatch', 0)) - PYTIVO_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_ondownload', 0)) - PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsubtitledownload', 0)) - PYTIVO_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'pyTivo', 'pyTivo_update_library', 0)) - PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '') - PYTIVO_SHARE_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_share_name', '') - PYTIVO_TIVO_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_tivo_name', '') - - CheckSection(CFG, 'NMA') - USE_NMA = bool(check_setting_int(CFG, 'NMA', 'use_nma', 0)) - NMA_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsnatch', 0)) - NMA_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_ondownload', 0)) - NMA_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsubtitledownload', 0)) - NMA_API = check_setting_str(CFG, 'NMA', 'nma_api', '') - NMA_PRIORITY = check_setting_str(CFG, 'NMA', 'nma_priority', "0") - - CheckSection(CFG, 'Mail') - USE_MAIL = bool(check_setting_int(CFG, 'Mail', 'use_mail', 0)) - MAIL_USERNAME = check_setting_str(CFG, 'Mail', 'mail_username', '') - MAIL_PASSWORD = check_setting_str(CFG, 'Mail', 'mail_password', '') - MAIL_SERVER = check_setting_str(CFG, 'Mail', 'mail_server', '') - MAIL_SSL = bool(check_setting_int(CFG, 'Mail', 'mail_ssl', 0)) - MAIL_FROM = check_setting_str(CFG, 'Mail', 'mail_from', '') - MAIL_TO = check_setting_str(CFG, 'Mail', 'mail_to', '') - MAIL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Mail', 'mail_notify_onsnatch', 0)) - - - USE_SUBTITLES = bool(check_setting_int(CFG, 'Subtitles', 'use_subtitles', 0)) - SUBTITLES_LANGUAGES = check_setting_str(CFG, 'Subtitles', 'subtitles_languages', '').split(',') - if SUBTITLES_LANGUAGES[0] == '': - SUBTITLES_LANGUAGES = [] - SUBTITLES_DIR = check_setting_str(CFG, 'Subtitles', 'subtitles_dir', '') - SUBTITLES_DIR_SUB = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_dir_sub', 0)) - SUBSNOLANG = bool(check_setting_int(CFG, 'Subtitles', 'subsnolang', 0)) - SUBTITLES_SERVICES_LIST = check_setting_str(CFG, 'Subtitles', 'SUBTITLES_SERVICES_LIST', '').split(',') - SUBTITLES_SERVICES_ENABLED = [int(x) for x in check_setting_str(CFG, 'Subtitles', 'SUBTITLES_SERVICES_ENABLED', '').split('|') if x] - SUBTITLES_DEFAULT = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_default', 0)) - SUBTITLES_HISTORY = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_history', 0)) - # start up all the threads - logger.sb_log_instance.initLogging(consoleLogging=consoleLogging) - - # initialize the main SB database - db.upgradeDatabase(db.DBConnection(), mainDB.InitialSchema) - - # initialize the cache database - db.upgradeDatabase(db.DBConnection("cache.db"), cache_db.InitialSchema) - - # fix up any db problems - db.sanityCheckDatabase(db.DBConnection(), mainDB.MainSanityCheck) - - # migrate the config if it needs it - migrator = ConfigMigrator(CFG) - migrator.migrate_config() - - currentSearchScheduler = scheduler.Scheduler(searchCurrent.CurrentSearcher(), - cycleTime=datetime.timedelta(minutes=SEARCH_FREQUENCY), - threadName="SEARCH", - runImmediately=True) - - # the interval for this is stored inside the ShowUpdater class - showUpdaterInstance = showUpdater.ShowUpdater() - showUpdateScheduler = scheduler.Scheduler(showUpdaterInstance, - cycleTime=showUpdaterInstance.updateInterval, - threadName="SHOWUPDATER", - runImmediately=False) - - versionCheckScheduler = scheduler.Scheduler(versionChecker.CheckVersion(), - cycleTime=datetime.timedelta(hours=12), - threadName="CHECKVERSION", - runImmediately=True) - - showQueueScheduler = scheduler.Scheduler(show_queue.ShowQueue(), - cycleTime=datetime.timedelta(seconds=3), - threadName="SHOWQUEUE", - silent=True) - - searchQueueScheduler = scheduler.Scheduler(search_queue.SearchQueue(), - cycleTime=datetime.timedelta(seconds=3), - threadName="SEARCHQUEUE", - silent=True) - - properFinderInstance = properFinder.ProperFinder() - properFinderScheduler = scheduler.Scheduler(properFinderInstance, - cycleTime=properFinderInstance.updateInterval, - threadName="FINDPROPERS", - runImmediately=False) - - if PROCESS_AUTOMATICALLY: - autoPostProcesserScheduler = scheduler.Scheduler(autoPostProcesser.PostProcesser( TV_DOWNLOAD_DIR ), - cycleTime=datetime.timedelta(minutes=10), - threadName="NZB_POSTPROCESSER", - runImmediately=True) - - if PROCESS_AUTOMATICALLY_TORRENT: - autoTorrentPostProcesserScheduler = scheduler.Scheduler(autoPostProcesser.PostProcesser( TORRENT_DOWNLOAD_DIR ), - cycleTime=datetime.timedelta(minutes=10), - threadName="TORRENT_POSTPROCESSER", - runImmediately=True) - - traktWatchListCheckerSchedular = scheduler.Scheduler(traktWatchListChecker.TraktChecker(), - cycleTime=datetime.timedelta(minutes=10), - threadName="TRAKTWATCHLIST", - runImmediately=True) - - backlogSearchScheduler = searchBacklog.BacklogSearchScheduler(searchBacklog.BacklogSearcher(), - cycleTime=datetime.timedelta(minutes=get_backlog_cycle_time()), - threadName="BACKLOG", - runImmediately=True) - backlogSearchScheduler.action.cycleTime = BACKLOG_SEARCH_FREQUENCY - - - subtitlesFinderScheduler = scheduler.Scheduler(subtitles.SubtitlesFinder(), - cycleTime=datetime.timedelta(hours=1), - threadName="FINDSUBTITLES", - runImmediately=True) - - showList = [] - loadingShowList = {} - - __INITIALIZED__ = True - return True - - -def start(): - - global __INITIALIZED__, currentSearchScheduler, backlogSearchScheduler, \ - showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \ - properFinderScheduler, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, searchQueueScheduler, \ - subtitlesFinderScheduler, started, USE_SUBTITLES, \ - traktWatchListCheckerSchedular, started - - with INIT_LOCK: - - if __INITIALIZED__: - - # start the search scheduler - currentSearchScheduler.thread.start() - - # start the backlog scheduler - backlogSearchScheduler.thread.start() - - # start the show updater - showUpdateScheduler.thread.start() - - # start the version checker - versionCheckScheduler.thread.start() - - # start the queue checker - showQueueScheduler.thread.start() - - # start the search queue checker - searchQueueScheduler.thread.start() - - # start the queue checker - properFinderScheduler.thread.start() - - if autoPostProcesserScheduler: - autoPostProcesserScheduler.thread.start() - - if autoTorrentPostProcesserScheduler: - autoTorrentPostProcesserScheduler.thread.start() - - # start the subtitles finder - if USE_SUBTITLES: - subtitlesFinderScheduler.thread.start() - - # start the trakt watchlist - traktWatchListCheckerSchedular.thread.start() - - started = True - -def halt(): - - global __INITIALIZED__, currentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \ - showQueueScheduler, properFinderScheduler, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, searchQueueScheduler, \ - subtitlesFinderScheduler, started, \ - traktWatchListCheckerSchedular - - with INIT_LOCK: - - if __INITIALIZED__: - - logger.log(u"Aborting all threads") - - # abort all the threads - - currentSearchScheduler.abort = True - logger.log(u"Waiting for the SEARCH thread to exit") - try: - currentSearchScheduler.thread.join(10) - except: - pass - - backlogSearchScheduler.abort = True - logger.log(u"Waiting for the BACKLOG thread to exit") - try: - backlogSearchScheduler.thread.join(10) - except: - pass - - showUpdateScheduler.abort = True - logger.log(u"Waiting for the SHOWUPDATER thread to exit") - try: - showUpdateScheduler.thread.join(10) - except: - pass - - versionCheckScheduler.abort = True - logger.log(u"Waiting for the VERSIONCHECKER thread to exit") - try: - versionCheckScheduler.thread.join(10) - except: - pass - - showQueueScheduler.abort = True - logger.log(u"Waiting for the SHOWQUEUE thread to exit") - try: - showQueueScheduler.thread.join(10) - except: - pass - - searchQueueScheduler.abort = True - logger.log(u"Waiting for the SEARCHQUEUE thread to exit") - try: - searchQueueScheduler.thread.join(10) - except: - pass - - if autoPostProcesserScheduler: - autoPostProcesserScheduler.abort = True - logger.log(u"Waiting for the NZB_POSTPROCESSER thread to exit") - try: - autoPostProcesserScheduler.thread.join(10) - except: - pass - - if autoTorrentPostProcesserScheduler: - autoTorrentPostProcesserScheduler.abort = True - logger.log(u"Waiting for the TORRENT_POSTPROCESSER thread to exit") - try: - autoTorrentPostProcesserScheduler.thread.join(10) - except: - pass - traktWatchListCheckerSchedular.abort = True - logger.log(u"Waiting for the TRAKTWATCHLIST thread to exit") - try: - traktWatchListCheckerSchedular.thread.join(10) - except: - pass - - properFinderScheduler.abort = True - logger.log(u"Waiting for the PROPERFINDER thread to exit") - try: - properFinderScheduler.thread.join(10) - except: - pass - - subtitlesFinderScheduler.abort = True - logger.log(u"Waiting for the SUBTITLESFINDER thread to exit") - try: - subtitlesFinderScheduler.thread.join(10) - except: - pass - - - __INITIALIZED__ = False - - -def sig_handler(signum=None, frame=None): - if type(signum) != type(None): - logger.log(u"Signal %i caught, saving and exiting..." % int(signum)) - saveAndShutdown() - - -def saveAll(): - - global showList - - # write all shows - logger.log(u"Saving all shows to the database") - for show in showList: - show.saveToDB() - - # save config - logger.log(u"Saving config file to disk") - save_config() - - -def saveAndShutdown(restart=False): - - halt() - - saveAll() - - logger.log(u"Killing cherrypy") - cherrypy.engine.exit() - - if CREATEPID: - logger.log(u"Removing pidfile " + str(PIDFILE)) - os.remove(PIDFILE) - - if restart: - install_type = versionCheckScheduler.action.install_type - - popen_list = [] - - if install_type in ('git', 'source'): - popen_list = [sys.executable, MY_FULLNAME] - elif install_type == 'win': - if hasattr(sys, 'frozen'): - # c:\dir\to\updater.exe 12345 c:\dir\to\sickbeard.exe - popen_list = [os.path.join(PROG_DIR, 'updater.exe'), str(PID), sys.executable] - else: - logger.log(u"Unknown SB launch method, please file a bug report about this", logger.ERROR) - popen_list = [sys.executable, os.path.join(PROG_DIR, 'updater.py'), str(PID), sys.executable, MY_FULLNAME ] - - if popen_list: - popen_list += MY_ARGS - if '--nolaunch' not in popen_list: - popen_list += ['--nolaunch'] - logger.log(u"Restarting Sick Beard with " + str(popen_list)) - subprocess.Popen(popen_list, cwd=os.getcwd()) - - os._exit(0) - - -def invoke_command(to_call, *args, **kwargs): - global invoked_command - - def delegate(): - to_call(*args, **kwargs) - invoked_command = delegate - logger.log(u"Placed invoked command: " + repr(invoked_command) + " for " + repr(to_call) + " with " + repr(args) + " and " + repr(kwargs), logger.DEBUG) - - -def invoke_restart(soft=True): - invoke_command(restart, soft=soft) - - -def invoke_shutdown(): - invoke_command(saveAndShutdown) - - -def restart(soft=True): - if soft: - halt() - saveAll() - #logger.log(u"Restarting cherrypy") - #cherrypy.engine.restart() - logger.log(u"Re-initializing all data") - initialize() - - else: - saveAndShutdown(restart=True) - - -def save_config(): - - new_config = ConfigObj() - new_config.filename = CONFIG_FILE - - new_config['General'] = {} - new_config['General']['log_dir'] = LOG_DIR - new_config['General']['web_port'] = WEB_PORT - new_config['General']['web_host'] = WEB_HOST - new_config['General']['web_ipv6'] = int(WEB_IPV6) - new_config['General']['web_log'] = int(WEB_LOG) - new_config['General']['web_root'] = WEB_ROOT - new_config['General']['web_username'] = WEB_USERNAME - new_config['General']['web_password'] = WEB_PASSWORD - new_config['General']['use_api'] = int(USE_API) - new_config['General']['api_key'] = API_KEY - new_config['General']['enable_https'] = int(ENABLE_HTTPS) - new_config['General']['https_cert'] = HTTPS_CERT - new_config['General']['https_key'] = HTTPS_KEY - new_config['General']['use_nzbs'] = int(USE_NZBS) - new_config['General']['use_torrents'] = int(USE_TORRENTS) - new_config['General']['nzb_method'] = NZB_METHOD - new_config['General']['torrent_method'] = TORRENT_METHOD - new_config['General']['prefered_method'] = PREFERED_METHOD - new_config['General']['usenet_retention'] = int(USENET_RETENTION) - new_config['General']['search_frequency'] = int(SEARCH_FREQUENCY) - new_config['General']['download_propers'] = int(DOWNLOAD_PROPERS) - new_config['General']['quality_default'] = int(QUALITY_DEFAULT) - new_config['General']['status_default'] = int(STATUS_DEFAULT) - new_config['General']['audio_show_default'] = AUDIO_SHOW_DEFAULT - new_config['General']['flatten_folders_default'] = int(FLATTEN_FOLDERS_DEFAULT) - new_config['General']['provider_order'] = ' '.join([x.getID() for x in providers.sortedProviderList()]) - new_config['General']['version_notify'] = int(VERSION_NOTIFY) - new_config['General']['naming_pattern'] = NAMING_PATTERN - new_config['General']['naming_custom_abd'] = int(NAMING_CUSTOM_ABD) - new_config['General']['naming_abd_pattern'] = NAMING_ABD_PATTERN - new_config['General']['naming_multi_ep'] = int(NAMING_MULTI_EP) - new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) - new_config['General']['display_posters'] = int(DISPLAY_POSTERS) - - new_config['General']['use_banner'] = int(USE_BANNER) - new_config['General']['use_listview'] = int(USE_LISTVIEW) - new_config['General']['metadata_xbmc'] = metadata_provider_dict['XBMC'].get_config() - new_config['General']['metadata_xbmcfrodo'] = metadata_provider_dict['XBMC (Frodo)'].get_config() - new_config['General']['metadata_mediabrowser'] = metadata_provider_dict['MediaBrowser'].get_config() - new_config['General']['metadata_ps3'] = metadata_provider_dict['Sony PS3'].get_config() - new_config['General']['metadata_wdtv'] = metadata_provider_dict['WDTV'].get_config() - new_config['General']['metadata_tivo'] = metadata_provider_dict['TIVO'].get_config() - new_config['General']['metadata_synology'] = metadata_provider_dict['Synology'].get_config() - - new_config['General']['cache_dir'] = ACTUAL_CACHE_DIR if ACTUAL_CACHE_DIR else 'cache' - new_config['General']['root_dirs'] = ROOT_DIRS if ROOT_DIRS else '' - new_config['General']['tv_download_dir'] = TV_DOWNLOAD_DIR - new_config['General']['torrent_download_dir'] = TORRENT_DOWNLOAD_DIR - new_config['General']['keep_processed_dir'] = int(KEEP_PROCESSED_DIR) - new_config['General']['move_associated_files'] = int(MOVE_ASSOCIATED_FILES) - new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) - new_config['General']['process_automatically_torrent'] = int(PROCESS_AUTOMATICALLY_TORRENT) - new_config['General']['rename_episodes'] = int(RENAME_EPISODES) - new_config['General']['create_missing_show_dirs'] = CREATE_MISSING_SHOW_DIRS - new_config['General']['add_shows_wo_dir'] = ADD_SHOWS_WO_DIR - - new_config['General']['extra_scripts'] = '|'.join(EXTRA_SCRIPTS) - new_config['General']['git_path'] = GIT_PATH - new_config['General']['ignore_words'] = IGNORE_WORDS - - new_config['Blackhole'] = {} - new_config['Blackhole']['nzb_dir'] = NZB_DIR - new_config['Blackhole']['torrent_dir'] = TORRENT_DIR - - new_config['EZRSS'] = {} - new_config['EZRSS']['ezrss'] = int(EZRSS) - - new_config['TVTORRENTS'] = {} - new_config['TVTORRENTS']['tvtorrents'] = int(TVTORRENTS) - new_config['TVTORRENTS']['tvtorrents_digest'] = TVTORRENTS_DIGEST - new_config['TVTORRENTS']['tvtorrents_hash'] = TVTORRENTS_HASH - - new_config['BTN'] = {} - new_config['BTN']['btn'] = int(BTN) - new_config['BTN']['btn_api_key'] = BTN_API_KEY - - new_config['TorrentLeech'] = {} - new_config['TorrentLeech']['torrentleech'] = int(TORRENTLEECH) - new_config['TorrentLeech']['torrentleech_key'] = TORRENTLEECH_KEY - - new_config['NZBs'] = {} - new_config['NZBs']['nzbs'] = int(NZBS) - new_config['NZBs']['nzbs_uid'] = NZBS_UID - new_config['NZBs']['nzbs_hash'] = NZBS_HASH - - new_config['NZBsRUS'] = {} - new_config['NZBsRUS']['nzbsrus'] = int(NZBSRUS) - new_config['NZBsRUS']['nzbsrus_uid'] = NZBSRUS_UID - new_config['NZBsRUS']['nzbsrus_hash'] = NZBSRUS_HASH - - new_config['NZBMatrix'] = {} - new_config['NZBMatrix']['nzbmatrix'] = int(NZBMATRIX) - new_config['NZBMatrix']['nzbmatrix_username'] = NZBMATRIX_USERNAME - new_config['NZBMatrix']['nzbmatrix_apikey'] = NZBMATRIX_APIKEY - - new_config['Newzbin'] = {} - new_config['Newzbin']['newzbin'] = int(NEWZBIN) - new_config['Newzbin']['newzbin_username'] = NEWZBIN_USERNAME - new_config['Newzbin']['newzbin_password'] = NEWZBIN_PASSWORD - - new_config['BinNewz'] = {} - new_config['BinNewz']['binnewz'] = int(BINNEWZ) - - new_config['T411'] = {} - new_config['T411']['t411'] = int(T411) - new_config['T411']['username'] = T411_USERNAME - new_config['T411']['password'] = T411_PASSWORD - - new_config['Cpasbien'] = {} - new_config['Cpasbien']['cpasbien'] = int(Cpasbien) - - new_config['kat'] = {} - new_config['kat']['kat'] = int(kat) - - new_config['PirateBay'] = {} - new_config['PirateBay']['piratebay'] = int(THEPIRATEBAY) - new_config['PirateBay']['piratebay_proxy'] = THEPIRATEBAY_PROXY - new_config['PirateBay']['piratebay_proxy_url'] = THEPIRATEBAY_PROXY_URL - new_config['PirateBay']['piratebay_trusted'] = THEPIRATEBAY_TRUSTED - - new_config['Womble'] = {} - new_config['Womble']['womble'] = int(WOMBLE) - - new_config['nzbX'] = {} - new_config['nzbX']['nzbx'] = int(NZBX) - new_config['nzbX']['nzbx_completion'] = int(NZBX_COMPLETION) - - new_config['omgwtfnzbs'] = {} - new_config['omgwtfnzbs']['omgwtfnzbs'] = int(OMGWTFNZBS) - new_config['omgwtfnzbs']['omgwtfnzbs_uid'] = OMGWTFNZBS_UID - new_config['omgwtfnzbs']['omgwtfnzbs_key'] = OMGWTFNZBS_KEY - - new_config['GKS'] = {} - new_config['GKS']['gks'] = int(GKS) - new_config['GKS']['gks_key'] = GKS_KEY - - new_config['SABnzbd'] = {} - new_config['SABnzbd']['sab_username'] = SAB_USERNAME - new_config['SABnzbd']['sab_password'] = SAB_PASSWORD - new_config['SABnzbd']['sab_apikey'] = SAB_APIKEY - new_config['SABnzbd']['sab_category'] = SAB_CATEGORY - new_config['SABnzbd']['sab_host'] = SAB_HOST - - new_config['NZBget'] = {} - new_config['NZBget']['nzbget_password'] = NZBGET_PASSWORD - new_config['NZBget']['nzbget_category'] = NZBGET_CATEGORY - new_config['NZBget']['nzbget_host'] = NZBGET_HOST - - new_config['TORRENT'] = {} - new_config['TORRENT']['torrent_username'] = TORRENT_USERNAME - new_config['TORRENT']['torrent_password'] = TORRENT_PASSWORD - new_config['TORRENT']['torrent_host'] = TORRENT_HOST - new_config['TORRENT']['torrent_path'] = TORRENT_PATH - new_config['TORRENT']['torrent_ratio'] = TORRENT_RATIO - new_config['TORRENT']['torrent_paused'] = int(TORRENT_PAUSED) - new_config['TORRENT']['torrent_label'] = TORRENT_LABEL - - new_config['XBMC'] = {} - new_config['XBMC']['use_xbmc'] = int(USE_XBMC) - new_config['XBMC']['xbmc_notify_onsnatch'] = int(XBMC_NOTIFY_ONSNATCH) - new_config['XBMC']['xbmc_notify_ondownload'] = int(XBMC_NOTIFY_ONDOWNLOAD) - new_config['XBMC']['xbmc_notify_onsubtitledownload'] = int(XBMC_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['XBMC']['xbmc_update_library'] = int(XBMC_UPDATE_LIBRARY) - new_config['XBMC']['xbmc_update_full'] = int(XBMC_UPDATE_FULL) - new_config['XBMC']['xbmc_update_onlyfirst'] = int(XBMC_UPDATE_ONLYFIRST) - new_config['XBMC']['xbmc_host'] = XBMC_HOST - new_config['XBMC']['xbmc_username'] = XBMC_USERNAME - new_config['XBMC']['xbmc_password'] = XBMC_PASSWORD - - new_config['Plex'] = {} - new_config['Plex']['use_plex'] = int(USE_PLEX) - new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH) - new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD) - new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Plex']['plex_update_library'] = int(PLEX_UPDATE_LIBRARY) - new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST - new_config['Plex']['plex_host'] = PLEX_HOST - new_config['Plex']['plex_username'] = PLEX_USERNAME - new_config['Plex']['plex_password'] = PLEX_PASSWORD - - new_config['Growl'] = {} - new_config['Growl']['use_growl'] = int(USE_GROWL) - new_config['Growl']['growl_notify_onsnatch'] = int(GROWL_NOTIFY_ONSNATCH) - new_config['Growl']['growl_notify_ondownload'] = int(GROWL_NOTIFY_ONDOWNLOAD) - new_config['Growl']['growl_notify_onsubtitledownload'] = int(GROWL_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Growl']['growl_host'] = GROWL_HOST - new_config['Growl']['growl_password'] = GROWL_PASSWORD - - new_config['Prowl'] = {} - new_config['Prowl']['use_prowl'] = int(USE_PROWL) - new_config['Prowl']['prowl_notify_onsnatch'] = int(PROWL_NOTIFY_ONSNATCH) - new_config['Prowl']['prowl_notify_ondownload'] = int(PROWL_NOTIFY_ONDOWNLOAD) - new_config['Prowl']['prowl_notify_onsubtitledownload'] = int(PROWL_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Prowl']['prowl_api'] = PROWL_API - new_config['Prowl']['prowl_priority'] = PROWL_PRIORITY - - new_config['Twitter'] = {} - new_config['Twitter']['use_twitter'] = int(USE_TWITTER) - new_config['Twitter']['twitter_notify_onsnatch'] = int(TWITTER_NOTIFY_ONSNATCH) - new_config['Twitter']['twitter_notify_ondownload'] = int(TWITTER_NOTIFY_ONDOWNLOAD) - new_config['Twitter']['twitter_notify_onsubtitledownload'] = int(TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Twitter']['twitter_username'] = TWITTER_USERNAME - new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD - new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX - - new_config['Notifo'] = {} - new_config['Notifo']['use_notifo'] = int(USE_NOTIFO) - new_config['Notifo']['notifo_notify_onsnatch'] = int(NOTIFO_NOTIFY_ONSNATCH) - new_config['Notifo']['notifo_notify_ondownload'] = int(NOTIFO_NOTIFY_ONDOWNLOAD) - new_config['Notifo']['notifo_notify_onsubtitledownload'] = int(NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Notifo']['notifo_username'] = NOTIFO_USERNAME - new_config['Notifo']['notifo_apisecret'] = NOTIFO_APISECRET - - new_config['Boxcar'] = {} - new_config['Boxcar']['use_boxcar'] = int(USE_BOXCAR) - new_config['Boxcar']['boxcar_notify_onsnatch'] = int(BOXCAR_NOTIFY_ONSNATCH) - new_config['Boxcar']['boxcar_notify_ondownload'] = int(BOXCAR_NOTIFY_ONDOWNLOAD) - new_config['Boxcar']['boxcar_notify_onsubtitledownload'] = int(BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Boxcar']['boxcar_username'] = BOXCAR_USERNAME - - new_config['Pushover'] = {} - new_config['Pushover']['use_pushover'] = int(USE_PUSHOVER) - new_config['Pushover']['pushover_notify_onsnatch'] = int(PUSHOVER_NOTIFY_ONSNATCH) - new_config['Pushover']['pushover_notify_ondownload'] = int(PUSHOVER_NOTIFY_ONDOWNLOAD) - new_config['Pushover']['pushover_notify_onsubtitledownload'] = int(PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushover']['pushover_userkey'] = PUSHOVER_USERKEY - - new_config['Libnotify'] = {} - new_config['Libnotify']['use_libnotify'] = int(USE_LIBNOTIFY) - new_config['Libnotify']['libnotify_notify_onsnatch'] = int(LIBNOTIFY_NOTIFY_ONSNATCH) - new_config['Libnotify']['libnotify_notify_ondownload'] = int(LIBNOTIFY_NOTIFY_ONDOWNLOAD) - new_config['Libnotify']['libnotify_notify_onsubtitledownload'] = int(LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD) - - new_config['NMJ'] = {} - new_config['NMJ']['use_nmj'] = int(USE_NMJ) - new_config['NMJ']['nmj_host'] = NMJ_HOST - new_config['NMJ']['nmj_database'] = NMJ_DATABASE - new_config['NMJ']['nmj_mount'] = NMJ_MOUNT - - new_config['Synology'] = {} - new_config['Synology']['use_synoindex'] = int(USE_SYNOINDEX) - - new_config['NMJv2'] = {} - new_config['NMJv2']['use_nmjv2'] = int(USE_NMJv2) - new_config['NMJv2']['nmjv2_host'] = NMJv2_HOST - new_config['NMJv2']['nmjv2_database'] = NMJv2_DATABASE - new_config['NMJv2']['nmjv2_dbloc'] = NMJv2_DBLOC - - new_config['Trakt'] = {} - new_config['Trakt']['use_trakt'] = int(USE_TRAKT) - new_config['Trakt']['trakt_username'] = TRAKT_USERNAME - new_config['Trakt']['trakt_password'] = TRAKT_PASSWORD - new_config['Trakt']['trakt_api'] = TRAKT_API - new_config['Trakt']['trakt_remove_watchlist'] = int(TRAKT_REMOVE_WATCHLIST) - new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST) - new_config['Trakt']['trakt_method_add'] = TRAKT_METHOD_ADD - new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED) - - new_config['pyTivo'] = {} - new_config['pyTivo']['use_pytivo'] = int(USE_PYTIVO) - new_config['pyTivo']['pytivo_notify_onsnatch'] = int(PYTIVO_NOTIFY_ONSNATCH) - new_config['pyTivo']['pytivo_notify_ondownload'] = int(PYTIVO_NOTIFY_ONDOWNLOAD) - new_config['pyTivo']['pytivo_notify_onsubtitledownload'] = int(PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['pyTivo']['pyTivo_update_library'] = int(PYTIVO_UPDATE_LIBRARY) - new_config['pyTivo']['pytivo_host'] = PYTIVO_HOST - new_config['pyTivo']['pytivo_share_name'] = PYTIVO_SHARE_NAME - new_config['pyTivo']['pytivo_tivo_name'] = PYTIVO_TIVO_NAME - - new_config['NMA'] = {} - new_config['NMA']['use_nma'] = int(USE_NMA) - new_config['NMA']['nma_notify_onsnatch'] = int(NMA_NOTIFY_ONSNATCH) - new_config['NMA']['nma_notify_ondownload'] = int(NMA_NOTIFY_ONDOWNLOAD) - new_config['NMA']['nma_notify_onsubtitledownload'] = int(NMA_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['NMA']['nma_api'] = NMA_API - new_config['NMA']['nma_priority'] = NMA_PRIORITY - - new_config['Mail'] = {} - new_config['Mail']['use_mail'] = int(USE_MAIL) - new_config['Mail']['mail_username'] = MAIL_USERNAME - new_config['Mail']['mail_password'] = MAIL_PASSWORD - new_config['Mail']['mail_server'] = MAIL_SERVER - new_config['Mail']['mail_ssl'] = int(MAIL_SSL) - new_config['Mail']['mail_from'] = MAIL_FROM - new_config['Mail']['mail_to'] = MAIL_TO - new_config['Mail']['mail_notify_onsnatch'] = int(MAIL_NOTIFY_ONSNATCH) - - new_config['Newznab'] = {} - new_config['Newznab']['newznab_data'] = '!!!'.join([x.configStr() for x in newznabProviderList]) - - new_config['GUI'] = {} - new_config['GUI']['coming_eps_layout'] = COMING_EPS_LAYOUT - new_config['GUI']['coming_eps_display_paused'] = int(COMING_EPS_DISPLAY_PAUSED) - new_config['GUI']['coming_eps_sort'] = COMING_EPS_SORT - - new_config['Subtitles'] = {} - new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES) - new_config['Subtitles']['subtitles_languages'] = ','.join(SUBTITLES_LANGUAGES) - new_config['Subtitles']['SUBTITLES_SERVICES_LIST'] = ','.join(SUBTITLES_SERVICES_LIST) - new_config['Subtitles']['SUBTITLES_SERVICES_ENABLED'] = '|'.join([str(x) for x in SUBTITLES_SERVICES_ENABLED]) - new_config['Subtitles']['subtitles_dir'] = SUBTITLES_DIR - new_config['Subtitles']['subtitles_dir_sub'] = int(SUBTITLES_DIR_SUB) - new_config['Subtitles']['subsnolang'] = int(SUBSNOLANG) - new_config['Subtitles']['subtitles_default'] = int(SUBTITLES_DEFAULT) - new_config['Subtitles']['subtitles_history'] = int(SUBTITLES_HISTORY) - - new_config['General']['config_version'] = CONFIG_VERSION - - new_config.write() - - -def launchBrowser(startPort=None): - if not startPort: - startPort = WEB_PORT - if ENABLE_HTTPS: - browserURL = 'https://localhost:%d%s' % (startPort, WEB_ROOT) - else: - browserURL = 'http://localhost:%d%s' % (startPort, WEB_ROOT) - try: - webbrowser.open(browserURL, 2, 1) - except: - try: - webbrowser.open(browserURL, 1, 1) - except: - logger.log(u"Unable to launch a browser", logger.ERROR) - - -def getEpList(epIDs, showid=None): - if epIDs == None or len(epIDs) == 0: - return [] - - query = "SELECT * FROM tv_episodes WHERE tvdbid in (%s)" % (",".join(['?'] * len(epIDs)),) - params = epIDs - - if showid != None: - query += " AND showid = ?" - params.append(showid) - - myDB = db.DBConnection() - sqlResults = myDB.select(query, params) - - epList = [] - - for curEp in sqlResults: - curShowObj = helpers.findCertainShow(showList, int(curEp["showid"])) - curEpObj = curShowObj.getEpisode(int(curEp["season"]), int(curEp["episode"])) - epList.append(curEpObj) - - return epList +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import cherrypy +import webbrowser +import sqlite3 +import datetime +import socket +import os, sys, subprocess, re +import urllib + +from threading import Lock + +# apparently py2exe won't build these unless they're imported somewhere +from sickbeard import providers, metadata +from providers import ezrss, tvtorrents, torrentleech, btn, nzbsrus, newznab, womble, nzbx, omgwtfnzbs, binnewz, t411, cpasbien, piratebay, gks, kat +from sickbeard.config import CheckSection, check_setting_int, check_setting_str, ConfigMigrator + +from sickbeard import searchCurrent, searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, subtitles, traktWatchListChecker +from sickbeard import helpers, db, exceptions, show_queue, search_queue, scheduler +from sickbeard import logger +from sickbeard import naming + +from common import SD, SKIPPED, NAMING_REPEAT + +from sickbeard.databases import mainDB, cache_db + +from lib.configobj import ConfigObj + +invoked_command = None + +SOCKET_TIMEOUT = 30 + +PID = None + +CFG = None +CONFIG_FILE = None + +# this is the version of the config we EXPECT to find +CONFIG_VERSION = 1 + +PROG_DIR = '.' +MY_FULLNAME = None +MY_NAME = None +MY_ARGS = [] +SYS_ENCODING = '' +DATA_DIR = '' +CREATEPID = False +PIDFILE = '' + +DAEMON = None +NO_RESIZE = False + +backlogSearchScheduler = None +currentSearchScheduler = None +showUpdateScheduler = None +versionCheckScheduler = None +showQueueScheduler = None +searchQueueScheduler = None +properFinderScheduler = None +autoPostProcesserScheduler = None +autoTorrentPostProcesserScheduler = None +subtitlesFinderScheduler = None +traktWatchListCheckerSchedular = None + +showList = None +loadingShowList = None + +providerList = [] +newznabProviderList = [] +metadata_provider_dict = {} + +NEWEST_VERSION = None +NEWEST_VERSION_STRING = None +VERSION_NOTIFY = None + +INIT_LOCK = Lock() +__INITIALIZED__ = False +started = False + +LOG_DIR = None + +WEB_PORT = None +WEB_LOG = None +WEB_ROOT = None +WEB_USERNAME = None +WEB_PASSWORD = None +WEB_HOST = None +WEB_IPV6 = None + +USE_API = False +API_KEY = None + +ENABLE_HTTPS = False +HTTPS_CERT = None +HTTPS_KEY = None + +LAUNCH_BROWSER = None +CACHE_DIR = None +ACTUAL_CACHE_DIR = None +ROOT_DIRS = None +UPDATE_SHOWS_ON_START = None +SORT_ARTICLE = None + +USE_BANNER = None +USE_LISTVIEW = None +METADATA_XBMC = None +METADATA_XBMCFRODO = None +METADATA_MEDIABROWSER = None +METADATA_PS3 = None +METADATA_WDTV = None +METADATA_TIVO = None +METADATA_SYNOLOGY = None + +QUALITY_DEFAULT = None +STATUS_DEFAULT = None +FLATTEN_FOLDERS_DEFAULT = None +AUDIO_SHOW_DEFAULT = None +SUBTITLES_DEFAULT = None +PROVIDER_ORDER = [] + +NAMING_MULTI_EP = None +NAMING_PATTERN = None +NAMING_ABD_PATTERN = None +NAMING_CUSTOM_ABD = None +NAMING_FORCE_FOLDERS = False + +TVDB_API_KEY = '9DAF49C96CBF8DAC' +TVDB_BASE_URL = None +TVDB_API_PARMS = {} + +USE_NZBS = None +USE_TORRENTS = None + +NZB_METHOD = None +NZB_DIR = None +USENET_RETENTION = None +TORRENT_METHOD = None +TORRENT_DIR = None +DOWNLOAD_PROPERS = None +PREFERED_METHOD = None +SEARCH_FREQUENCY = None +BACKLOG_SEARCH_FREQUENCY = 1 + +MIN_SEARCH_FREQUENCY = 10 + +DEFAULT_SEARCH_FREQUENCY = 60 + +EZRSS = False +TVTORRENTS = False +TVTORRENTS_DIGEST = None +TVTORRENTS_HASH = None + +TORRENTLEECH = False +TORRENTLEECH_KEY = None + +BTN = False +BTN_API_KEY = None + +TORRENT_DIR = None + +ADD_SHOWS_WO_DIR = None +CREATE_MISSING_SHOW_DIRS = None +RENAME_EPISODES = False +PROCESS_AUTOMATICALLY = False +PROCESS_AUTOMATICALLY_TORRENT = False +KEEP_PROCESSED_DIR = False +MOVE_ASSOCIATED_FILES = False +TV_DOWNLOAD_DIR = None +TORRENT_DOWNLOAD_DIR = None + +NZBS = False +NZBS_UID = None +NZBS_HASH = None + +WOMBLE = False + +NZBX = False +NZBX_COMPLETION = 100 + +OMGWTFNZBS = False +OMGWTFNZBS_UID = None +OMGWTFNZBS_KEY = None + +NZBSRUS = False +NZBSRUS_UID = None +NZBSRUS_HASH = None + +BINNEWZ = False + +T411 = False +T411_USERNAME = None +T411_PASSWORD = None + +THEPIRATEBAY = False +THEPIRATEBAY_TRUSTED = True +THEPIRATEBAY_PROXY = False +THEPIRATEBAY_PROXY_URL = None + +Cpasbien = False +kat = False + +NZBMATRIX = False +NZBMATRIX_USERNAME = None +NZBMATRIX_APIKEY = None + +NEWZBIN = False +NEWZBIN_USERNAME = None +NEWZBIN_PASSWORD = None + +SAB_USERNAME = None +SAB_PASSWORD = None +SAB_APIKEY = None +SAB_CATEGORY = None +SAB_HOST = '' + +NZBGET_PASSWORD = None +NZBGET_CATEGORY = None +NZBGET_HOST = None + +GKS = False +GKS_KEY = None + +TORRENT_USERNAME = None +TORRENT_PASSWORD = None +TORRENT_HOST = '' +TORRENT_PATH = '' +TORRENT_RATIO = '' +TORRENT_PAUSED = False +TORRENT_LABEL = '' + +USE_XBMC = False +XBMC_NOTIFY_ONSNATCH = False +XBMC_NOTIFY_ONDOWNLOAD = False +XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = False +XBMC_UPDATE_LIBRARY = False +XBMC_UPDATE_FULL = False +XBMC_UPDATE_ONLYFIRST = False +XBMC_HOST = '' +XBMC_USERNAME = None +XBMC_PASSWORD = None + +USE_PLEX = False +PLEX_NOTIFY_ONSNATCH = False +PLEX_NOTIFY_ONDOWNLOAD = False +PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = False +PLEX_UPDATE_LIBRARY = False +PLEX_SERVER_HOST = None +PLEX_HOST = None +PLEX_USERNAME = None +PLEX_PASSWORD = None + +USE_GROWL = False +GROWL_NOTIFY_ONSNATCH = False +GROWL_NOTIFY_ONDOWNLOAD = False +GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = False +GROWL_HOST = '' +GROWL_PASSWORD = None + +USE_PROWL = False +PROWL_NOTIFY_ONSNATCH = False +PROWL_NOTIFY_ONDOWNLOAD = False +PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = False +PROWL_API = None +PROWL_PRIORITY = 0 + +USE_TWITTER = False +TWITTER_NOTIFY_ONSNATCH = False +TWITTER_NOTIFY_ONDOWNLOAD = False +TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = False +TWITTER_USERNAME = None +TWITTER_PASSWORD = None +TWITTER_PREFIX = None + +USE_NOTIFO = False +NOTIFO_NOTIFY_ONSNATCH = False +NOTIFO_NOTIFY_ONDOWNLOAD = False +NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = False +NOTIFO_USERNAME = None +NOTIFO_APISECRET = None +NOTIFO_PREFIX = None + +USE_BOXCAR = False +BOXCAR_NOTIFY_ONSNATCH = False +BOXCAR_NOTIFY_ONDOWNLOAD = False +BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = False +BOXCAR_USERNAME = None +BOXCAR_PASSWORD = None +BOXCAR_PREFIX = None + +USE_PUSHOVER = False +PUSHOVER_NOTIFY_ONSNATCH = False +PUSHOVER_NOTIFY_ONDOWNLOAD = False +PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = False +PUSHOVER_USERKEY = None + +USE_LIBNOTIFY = False +LIBNOTIFY_NOTIFY_ONSNATCH = False +LIBNOTIFY_NOTIFY_ONDOWNLOAD = False +LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = False + +USE_NMJ = False +NMJ_HOST = None +NMJ_DATABASE = None +NMJ_MOUNT = None + +USE_SYNOINDEX = False + +USE_NMJv2 = False +NMJv2_HOST = None +NMJv2_DATABASE = None +NMJv2_DBLOC = None + +USE_SYNOLOGYNOTIFIER = False +SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = False +SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = False +SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = False + +USE_TRAKT = False +TRAKT_USERNAME = None +TRAKT_PASSWORD = None +TRAKT_API = '' +TRAKT_REMOVE_WATCHLIST = False +TRAKT_USE_WATCHLIST = False +TRAKT_METHOD_ADD = 0 +TRAKT_START_PAUSED = False + +USE_PYTIVO = False +PYTIVO_NOTIFY_ONSNATCH = False +PYTIVO_NOTIFY_ONDOWNLOAD = False +PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = False +PYTIVO_UPDATE_LIBRARY = False +PYTIVO_HOST = '' +PYTIVO_SHARE_NAME = '' +PYTIVO_TIVO_NAME = '' + +USE_NMA = False +NMA_NOTIFY_ONSNATCH = False +NMA_NOTIFY_ONDOWNLOAD = False +NMA_NOTIFY_ONSUBTITLEDOWNLOAD = False +NMA_API = None +NMA_PRIORITY = 0 + +USE_PUSHALOT = False +PUSHALOT_NOTIFY_ONSNATCH = False +PUSHALOT_NOTIFY_ONDOWNLOAD = False +PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = False +PUSHALOT_AUTHORIZATIONTOKEN = None + +USE_MAIL = False +MAIL_USERNAME = None +MAIL_PASSWORD = None +MAIL_SERVER = None +MAIL_SSL = False +MAIL_FROM = None +MAIL_TO = None +MAIL_NOTIFY_ONSNATCH = False + +HOME_LAYOUT = None +DISPLAY_SHOW_SPECIALS = None +COMING_EPS_LAYOUT = None +COMING_EPS_DISPLAY_PAUSED = None +COMING_EPS_SORT = None +COMING_EPS_MISSED_RANGE = None + +USE_SUBTITLES = False +SUBTITLES_LANGUAGES = [] +SUBTITLES_DIR = '' +SUBTITLES_DIR_SUB = False +SUBSNOLANG = False +SUBTITLES_SERVICES_LIST = [] +SUBTITLES_SERVICES_ENABLED = [] +SUBTITLES_HISTORY = False + +DISPLAY_POSTERS = None + +EXTRA_SCRIPTS = [] + +GIT_PATH = None + +IGNORE_WORDS = "german,spanish,core2hd,dutch,swedish" + +__INITIALIZED__ = False + + +def get_backlog_cycle_time(): + cycletime = SEARCH_FREQUENCY * 2 + 7 + return max([cycletime, 120]) + + +def initialize(consoleLogging=True): + + with INIT_LOCK: + + global LOG_DIR, WEB_PORT, WEB_LOG, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, USE_API, API_KEY, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ + USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, TORRENT_METHOD, PREFERED_METHOD, \ + SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_HOST, \ + NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, currentSearchScheduler, backlogSearchScheduler, \ + TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_RATIO, TORRENT_PAUSED, TORRENT_LABEL, \ + USE_XBMC, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \ + XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, \ + USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API,TRAKT_REMOVE_WATCHLIST,TRAKT_USE_WATCHLIST,TRAKT_METHOD_ADD,TRAKT_START_PAUSED,traktWatchListCheckerSchedular, \ + USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \ + PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, \ + showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SORT_ARTICLE, showList, loadingShowList, \ + NZBS, NZBS_UID, NZBS_HASH, EZRSS, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, BTN, BTN_API_KEY, TORRENTLEECH, TORRENTLEECH_KEY, TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \ + BINNEWZ, \ + T411, T411_USERNAME, T411_PASSWORD, \ + THEPIRATEBAY, THEPIRATEBAY_PROXY, THEPIRATEBAY_PROXY_URL, THEPIRATEBAY_TRUSTED, \ + Cpasbien, \ + kat, \ + SEARCH_FREQUENCY, DEFAULT_SEARCH_FREQUENCY, BACKLOG_SEARCH_FREQUENCY, \ + QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, AUDIO_SHOW_DEFAULT, \ + GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, \ + USE_GROWL, GROWL_HOST, GROWL_PASSWORD, USE_PROWL, PROWL_NOTIFY_ONSNATCH, PROWL_NOTIFY_ONDOWNLOAD, PROWL_NOTIFY_ONSUBTITLEDOWNLOAD, PROWL_API, PROWL_PRIORITY, PROG_DIR, NZBMATRIX, NZBMATRIX_USERNAME, \ + USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ + USE_NMA, NMA_NOTIFY_ONSNATCH, NMA_NOTIFY_ONDOWNLOAD, NMA_NOTIFY_ONSUBTITLEDOWNLOAD, NMA_API, NMA_PRIORITY, \ + USE_PUSHALOT, PUSHALOT_NOTIFY_ONSNATCH, PUSHALOT_NOTIFY_ONDOWNLOAD, PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHALOT_AUTHORIZATIONTOKEN, \ + USE_SYNOLOGYNOTIFIER, SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH, SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD, SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD, \ + USE_MAIL, MAIL_USERNAME, MAIL_PASSWORD, MAIL_SERVER, MAIL_SSL, MAIL_FROM, MAIL_TO, MAIL_NOTIFY_ONSNATCH, \ + NZBMATRIX_APIKEY, versionCheckScheduler, VERSION_NOTIFY, PROCESS_AUTOMATICALLY, PROCESS_AUTOMATICALLY_TORRENT, \ + KEEP_PROCESSED_DIR, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ + showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TVDB_API_PARMS, \ + NAMING_PATTERN, NAMING_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, \ + RENAME_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, \ + NZBSRUS, NZBSRUS_UID, NZBSRUS_HASH, WOMBLE, NZBX, NZBX_COMPLETION, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_KEY, providerList, newznabProviderList, \ + EXTRA_SCRIPTS, USE_TWITTER, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \ + USE_NOTIFO, NOTIFO_USERNAME, NOTIFO_APISECRET, NOTIFO_NOTIFY_ONDOWNLOAD, NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD, NOTIFO_NOTIFY_ONSNATCH, \ + USE_BOXCAR, BOXCAR_USERNAME, BOXCAR_PASSWORD, BOXCAR_NOTIFY_ONDOWNLOAD, BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR_NOTIFY_ONSNATCH, \ + USE_PUSHOVER, PUSHOVER_USERKEY, PUSHOVER_NOTIFY_ONDOWNLOAD, PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHOVER_NOTIFY_ONSNATCH, \ + USE_LIBNOTIFY, LIBNOTIFY_NOTIFY_ONSNATCH, LIBNOTIFY_NOTIFY_ONDOWNLOAD, LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD, USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, USE_NMJv2, NMJv2_HOST, NMJv2_DATABASE, NMJv2_DBLOC, USE_SYNOINDEX, \ + USE_BANNER, USE_LISTVIEW, METADATA_XBMC, METADATA_XBMCFRODO, METADATA_MEDIABROWSER, METADATA_PS3, METADATA_SYNOLOGY, metadata_provider_dict, \ + NEWZBIN, NEWZBIN_USERNAME, NEWZBIN_PASSWORD, GIT_PATH, MOVE_ASSOCIATED_FILES, \ + GKS, GKS_KEY, \ + HOME_LAYOUT, DISPLAY_SHOW_SPECIALS, COMING_EPS_LAYOUT, COMING_EPS_SORT, COMING_EPS_DISPLAY_PAUSED, COMING_EPS_MISSED_RANGE, METADATA_WDTV, METADATA_TIVO, IGNORE_WORDS, CREATE_MISSING_SHOW_DIRS, \ + ADD_SHOWS_WO_DIR, USE_SUBTITLES, SUBTITLES_LANGUAGES, SUBTITLES_DIR, SUBTITLES_DIR_SUB, SUBSNOLANG, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_HISTORY, subtitlesFinderScheduler + + + if __INITIALIZED__: + return False + + socket.setdefaulttimeout(SOCKET_TIMEOUT) + + CheckSection(CFG, 'General') + LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', 'Logs') + if not helpers.makeDir(LOG_DIR): + logger.log(u"!!! No log folder, logging to screen only!", logger.ERROR) + + try: + WEB_PORT = check_setting_int(CFG, 'General', 'web_port', 8081) + except: + WEB_PORT = 8081 + + if WEB_PORT < 21 or WEB_PORT > 65535: + WEB_PORT = 8081 + + WEB_HOST = check_setting_str(CFG, 'General', 'web_host', '0.0.0.0') + WEB_IPV6 = bool(check_setting_int(CFG, 'General', 'web_ipv6', 0)) + WEB_ROOT = check_setting_str(CFG, 'General', 'web_root', '').rstrip("/") + WEB_LOG = bool(check_setting_int(CFG, 'General', 'web_log', 0)) + WEB_USERNAME = check_setting_str(CFG, 'General', 'web_username', '') + WEB_PASSWORD = check_setting_str(CFG, 'General', 'web_password', '') + LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) + + UPDATE_SHOWS_ON_START = bool(check_setting_int(CFG, 'General', 'update_shows_on_start', 1)) + SORT_ARTICLE = bool(check_setting_int(CFG, 'General', 'sort_article', 0)) + + USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0)) + API_KEY = check_setting_str(CFG, 'General', 'api_key', '') + + ENABLE_HTTPS = bool(check_setting_int(CFG, 'General', 'enable_https', 0)) + HTTPS_CERT = check_setting_str(CFG, 'General', 'https_cert', 'server.crt') + HTTPS_KEY = check_setting_str(CFG, 'General', 'https_key', 'server.key') + + ACTUAL_CACHE_DIR = check_setting_str(CFG, 'General', 'cache_dir', 'cache') + # fix bad configs due to buggy code + if ACTUAL_CACHE_DIR == 'None': + ACTUAL_CACHE_DIR = 'cache' + + # unless they specify, put the cache dir inside the data dir + if not os.path.isabs(ACTUAL_CACHE_DIR): + CACHE_DIR = os.path.join(DATA_DIR, ACTUAL_CACHE_DIR) + else: + CACHE_DIR = ACTUAL_CACHE_DIR + + if not helpers.makeDir(CACHE_DIR): + logger.log(u"!!! Creating local cache dir failed, using system default", logger.ERROR) + CACHE_DIR = None + + ROOT_DIRS = check_setting_str(CFG, 'General', 'root_dirs', '') + if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', ROOT_DIRS): + ROOT_DIRS = '' + + proxies = urllib.getproxies() + proxy_url = None # @UnusedVariable + if 'http' in proxies: + proxy_url = proxies['http'] # @UnusedVariable + elif 'ftp' in proxies: + proxy_url = proxies['ftp'] # @UnusedVariable + + # Set our common tvdb_api options here + TVDB_API_PARMS = {'apikey': TVDB_API_KEY, + 'language': 'en', + 'useZip': True} + + if CACHE_DIR: + TVDB_API_PARMS['cache'] = os.path.join(CACHE_DIR, 'tvdb') + + TVDB_BASE_URL = 'http://thetvdb.com/api/' + TVDB_API_KEY + + QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) + STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) + AUDIO_SHOW_DEFAULT = check_setting_str(CFG, 'General', 'audio_show_default', 'fr' ) + VERSION_NOTIFY = check_setting_int(CFG, 'General', 'version_notify', 1) + FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0)) + + PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split() + + NAMING_PATTERN = check_setting_str(CFG, 'General', 'naming_pattern', '') + NAMING_ABD_PATTERN = check_setting_str(CFG, 'General', 'naming_abd_pattern', '') + NAMING_CUSTOM_ABD = check_setting_int(CFG, 'General', 'naming_custom_abd', 0) + NAMING_MULTI_EP = check_setting_int(CFG, 'General', 'naming_multi_ep', 1) + NAMING_FORCE_FOLDERS = naming.check_force_season_folders() + + USE_NZBS = bool(check_setting_int(CFG, 'General', 'use_nzbs', 1)) + USE_TORRENTS = bool(check_setting_int(CFG, 'General', 'use_torrents', 0)) + + NZB_METHOD = check_setting_str(CFG, 'General', 'nzb_method', 'blackhole') + if NZB_METHOD not in ('blackhole', 'sabnzbd', 'nzbget'): + NZB_METHOD = 'blackhole' + + TORRENT_METHOD = check_setting_str(CFG, 'General', 'torrent_method', 'blackhole') + if TORRENT_METHOD not in ('blackhole', 'utorrent', 'transmission', 'deluge', 'download_station'): + TORRENT_METHOD = 'blackhole' + + PREFERED_METHOD = check_setting_str(CFG, 'General', 'prefered_method', 'nzb') + if PREFERED_METHOD not in ('torrent', 'nzb'): + PREFERED_METHOD = 'nzb' + + DOWNLOAD_PROPERS = bool(check_setting_int(CFG, 'General', 'download_propers', 1)) + USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', 500) + SEARCH_FREQUENCY = check_setting_int(CFG, 'General', 'search_frequency', DEFAULT_SEARCH_FREQUENCY) + if SEARCH_FREQUENCY < MIN_SEARCH_FREQUENCY: + SEARCH_FREQUENCY = MIN_SEARCH_FREQUENCY + + TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_dir', '') + + TV_DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'tv_download_dir', '') + TORRENT_DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'torrent_download_dir', '') + PROCESS_AUTOMATICALLY = check_setting_int(CFG, 'General', 'process_automatically', 0) + PROCESS_AUTOMATICALLY_TORRENT = check_setting_int(CFG, 'General', 'process_automatically_torrent', 0) + RENAME_EPISODES = check_setting_int(CFG, 'General', 'rename_episodes', 1) + KEEP_PROCESSED_DIR = check_setting_int(CFG, 'General', 'keep_processed_dir', 1) + MOVE_ASSOCIATED_FILES = check_setting_int(CFG, 'General', 'move_associated_files', 0) + CREATE_MISSING_SHOW_DIRS = check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0) + ADD_SHOWS_WO_DIR = check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0) + + EZRSS = bool(check_setting_int(CFG, 'General', 'use_torrent', 0)) + if not EZRSS: + CheckSection(CFG, 'EZRSS') + EZRSS = bool(check_setting_int(CFG, 'EZRSS', 'ezrss', 0)) + + GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') + IGNORE_WORDS = check_setting_str(CFG, 'General', 'ignore_words', '') + EXTRA_SCRIPTS = [x for x in check_setting_str(CFG, 'General', 'extra_scripts', '').split('|') if x] + + USE_BANNER = bool(check_setting_int(CFG, 'General', 'use_banner', 0)) + USE_LISTVIEW = bool(check_setting_int(CFG, 'General', 'use_listview', 0)) + METADATA_TYPE = check_setting_str(CFG, 'General', 'metadata_type', '') + + metadata_provider_dict = metadata.get_metadata_generator_dict() + + # if this exists it's legacy, use the info to upgrade metadata to the new settings + if METADATA_TYPE: + + old_metadata_class = None + + if METADATA_TYPE == 'xbmc': + old_metadata_class = metadata.xbmc.metadata_class + elif METADATA_TYPE == 'xbmcfrodo': + old_metadata_class = metadata.xbmcfrodo.metadata_class + elif METADATA_TYPE == 'mediabrowser': + old_metadata_class = metadata.mediabrowser.metadata_class + elif METADATA_TYPE == 'ps3': + old_metadata_class = metadata.ps3.metadata_class + + if old_metadata_class: + + METADATA_SHOW = bool(check_setting_int(CFG, 'General', 'metadata_show', 1)) + METADATA_EPISODE = bool(check_setting_int(CFG, 'General', 'metadata_episode', 1)) + + ART_POSTER = bool(check_setting_int(CFG, 'General', 'art_poster', 1)) + ART_FANART = bool(check_setting_int(CFG, 'General', 'art_fanart', 1)) + ART_THUMBNAILS = bool(check_setting_int(CFG, 'General', 'art_thumbnails', 1)) + ART_SEASON_THUMBNAILS = bool(check_setting_int(CFG, 'General', 'art_season_thumbnails', 1)) + + new_metadata_class = old_metadata_class(METADATA_SHOW, + METADATA_EPISODE, + ART_POSTER, + ART_FANART, + ART_THUMBNAILS, + ART_SEASON_THUMBNAILS) + + metadata_provider_dict[new_metadata_class.name] = new_metadata_class + + # this is the normal codepath for metadata config + else: + METADATA_XBMC = check_setting_str(CFG, 'General', 'metadata_xbmc', '0|0|0|0|0|0') + METADATA_XBMCFRODO = check_setting_str(CFG, 'General', 'metadata_xbmcfrodo', '0|0|0|0|0|0') + METADATA_MEDIABROWSER = check_setting_str(CFG, 'General', 'metadata_mediabrowser', '0|0|0|0|0|0') + METADATA_PS3 = check_setting_str(CFG, 'General', 'metadata_ps3', '0|0|0|0|0|0') + METADATA_WDTV = check_setting_str(CFG, 'General', 'metadata_wdtv', '0|0|0|0|0|0') + METADATA_TIVO = check_setting_str(CFG, 'General', 'metadata_tivo', '0|0|0|0|0|0') + METADATA_SYNOLOGY = check_setting_str(CFG, 'General', 'metadata_synology', '0|0|0|0|0|0') + + for cur_metadata_tuple in [(METADATA_XBMC, metadata.xbmc), + (METADATA_XBMCFRODO, metadata.xbmcfrodo), + (METADATA_MEDIABROWSER, metadata.mediabrowser), + (METADATA_PS3, metadata.ps3), + (METADATA_WDTV, metadata.wdtv), + (METADATA_TIVO, metadata.tivo), + (METADATA_SYNOLOGY, metadata.synology), + ]: + + (cur_metadata_config, cur_metadata_class) = cur_metadata_tuple + tmp_provider = cur_metadata_class.metadata_class() + tmp_provider.set_config(cur_metadata_config) + metadata_provider_dict[tmp_provider.name] = tmp_provider + + CheckSection(CFG, 'GUI') + HOME_LAYOUT = check_setting_str(CFG, 'GUI', 'home_layout', 'poster') + DISPLAY_SHOW_SPECIALS = bool(check_setting_int(CFG, 'GUI', 'display_show_specials', 1)) + COMING_EPS_LAYOUT = check_setting_str(CFG, 'GUI', 'coming_eps_layout', 'banner') + COMING_EPS_DISPLAY_PAUSED = bool(check_setting_int(CFG, 'GUI', 'coming_eps_display_paused', 0)) + COMING_EPS_SORT = check_setting_str(CFG, 'GUI', 'coming_eps_sort', 'date') + COMING_EPS_MISSED_RANGE = check_setting_int(CFG, 'GUI', 'coming_eps_missed_range', 7) + + CheckSection(CFG, 'Newznab') + newznabData = check_setting_str(CFG, 'Newznab', 'newznab_data', '') + newznabProviderList = providers.getNewznabProviderList(newznabData) + providerList = providers.makeProviderList() + + CheckSection(CFG, 'Blackhole') + NZB_DIR = check_setting_str(CFG, 'Blackhole', 'nzb_dir', '') + TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_dir', '') + + CheckSection(CFG, 'TVTORRENTS') + TVTORRENTS = bool(check_setting_int(CFG, 'TVTORRENTS', 'tvtorrents', 0)) + TVTORRENTS_DIGEST = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_digest', '') + TVTORRENTS_HASH = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_hash', '') + + CheckSection(CFG, 'BTN') + BTN = bool(check_setting_int(CFG, 'BTN', 'btn', 0)) + BTN_API_KEY = check_setting_str(CFG, 'BTN', 'btn_api_key', '') + + CheckSection(CFG, 'TorrentLeech') + TORRENTLEECH = bool(check_setting_int(CFG, 'TorrentLeech', 'torrentleech', 0)) + TORRENTLEECH_KEY = check_setting_str(CFG, 'TorrentLeech', 'torrentleech_key', '') + + CheckSection(CFG, 'NZBs') + NZBS = bool(check_setting_int(CFG, 'NZBs', 'nzbs', 0)) + NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '') + NZBS_HASH = check_setting_str(CFG, 'NZBs', 'nzbs_hash', '') + + CheckSection(CFG, 'NZBsRUS') + NZBSRUS = bool(check_setting_int(CFG, 'NZBsRUS', 'nzbsrus', 0)) + NZBSRUS_UID = check_setting_str(CFG, 'NZBsRUS', 'nzbsrus_uid', '') + NZBSRUS_HASH = check_setting_str(CFG, 'NZBsRUS', 'nzbsrus_hash', '') + + CheckSection(CFG, 'NZBMatrix') + NZBMATRIX = bool(check_setting_int(CFG, 'NZBMatrix', 'nzbmatrix', 0)) + NZBMATRIX_USERNAME = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_username', '') + NZBMATRIX_APIKEY = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_apikey', '') + + CheckSection(CFG, 'BinNewz') + BINNEWZ = bool(check_setting_int(CFG, 'BinNewz', 'binnewz', 0)) + + CheckSection(CFG, 'T411') + T411 = bool(check_setting_int(CFG, 'T411', 't411', 0)) + T411_USERNAME = check_setting_str(CFG, 'T411', 'username', '') + T411_PASSWORD = check_setting_str(CFG, 'T411', 'password', '') + + CheckSection(CFG, 'PirateBay') + THEPIRATEBAY = bool(check_setting_int(CFG, 'PirateBay', 'piratebay', 0)) + THEPIRATEBAY_PROXY = bool(check_setting_int(CFG, 'PirateBay', 'piratebay_proxy', 0)) + THEPIRATEBAY_PROXY_URL = check_setting_str(CFG, 'PirateBay', 'piratebay_proxy_url', '') + THEPIRATEBAY_TRUSTED = bool(check_setting_int(CFG, 'PirateBay', 'piratebay_trusted', 0)) + + CheckSection(CFG, 'Cpasbien') + Cpasbien = bool(check_setting_int(CFG, 'Cpasbien', 'cpasbien', 0)) + + CheckSection(CFG, 'kat') + kat = bool(check_setting_int(CFG, 'kat', 'kat', 0)) + + CheckSection(CFG, 'Newzbin') + NEWZBIN = bool(check_setting_int(CFG, 'Newzbin', 'newzbin', 0)) + NEWZBIN_USERNAME = check_setting_str(CFG, 'Newzbin', 'newzbin_username', '') + NEWZBIN_PASSWORD = check_setting_str(CFG, 'Newzbin', 'newzbin_password', '') + + CheckSection(CFG, 'Womble') + WOMBLE = bool(check_setting_int(CFG, 'Womble', 'womble', 1)) + + CheckSection(CFG, 'nzbX') + NZBX = bool(check_setting_int(CFG, 'nzbX', 'nzbx', 0)) + NZBX_COMPLETION = check_setting_int(CFG, 'nzbX', 'nzbx_completion', 100) + + CheckSection(CFG, 'omgwtfnzbs') + OMGWTFNZBS = bool(check_setting_int(CFG, 'omgwtfnzbs', 'omgwtfnzbs', 0)) + OMGWTFNZBS_UID = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_uid', '') + OMGWTFNZBS_KEY = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_key', '') + + CheckSection(CFG, 'GKS') + GKS = bool(check_setting_int(CFG, 'GKS', 'gks', 0)) + GKS_KEY = check_setting_str(CFG, 'GKS', 'gks_key', '') + + CheckSection(CFG, 'SABnzbd') + SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') + SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '') + SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '') + SAB_CATEGORY = check_setting_str(CFG, 'SABnzbd', 'sab_category', 'tv') + SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') + + CheckSection(CFG, 'NZBget') + NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', 'tegbzn6789') + NZBGET_CATEGORY = check_setting_str(CFG, 'NZBget', 'nzbget_category', 'tv') + NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '') + + CheckSection(CFG, 'XBMC') + TORRENT_USERNAME = check_setting_str(CFG, 'TORRENT', 'torrent_username', '') + TORRENT_PASSWORD = check_setting_str(CFG, 'TORRENT', 'torrent_password', '') + TORRENT_HOST = check_setting_str(CFG, 'TORRENT', 'torrent_host', '') + TORRENT_PATH = check_setting_str(CFG, 'TORRENT', 'torrent_path', '') + TORRENT_RATIO = check_setting_str(CFG, 'TORRENT', 'torrent_ratio', '') + TORRENT_PAUSED = bool(check_setting_int(CFG, 'TORRENT', 'torrent_paused', 0)) + TORRENT_LABEL = check_setting_str(CFG, 'TORRENT', 'torrent_label', '') + + USE_XBMC = bool(check_setting_int(CFG, 'XBMC', 'use_xbmc', 0)) + XBMC_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_onsnatch', 0)) + XBMC_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_ondownload', 0)) + XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify_onsubtitledownload', 0)) + XBMC_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_library', 0)) + XBMC_UPDATE_FULL = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_full', 0)) + XBMC_UPDATE_ONLYFIRST = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update_onlyfirst', 0)) + XBMC_HOST = check_setting_str(CFG, 'XBMC', 'xbmc_host', '') + XBMC_USERNAME = check_setting_str(CFG, 'XBMC', 'xbmc_username', '') + XBMC_PASSWORD = check_setting_str(CFG, 'XBMC', 'xbmc_password', '') + + CheckSection(CFG, 'Plex') + USE_PLEX = bool(check_setting_int(CFG, 'Plex', 'use_plex', 0)) + PLEX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsnatch', 0)) + PLEX_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_ondownload', 0)) + PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsubtitledownload', 0)) + PLEX_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Plex', 'plex_update_library', 0)) + PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '') + PLEX_HOST = check_setting_str(CFG, 'Plex', 'plex_host', '') + PLEX_USERNAME = check_setting_str(CFG, 'Plex', 'plex_username', '') + PLEX_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_password', '') + + CheckSection(CFG, 'Growl') + USE_GROWL = bool(check_setting_int(CFG, 'Growl', 'use_growl', 0)) + GROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsnatch', 0)) + GROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_ondownload', 0)) + GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsubtitledownload', 0)) + GROWL_HOST = check_setting_str(CFG, 'Growl', 'growl_host', '') + GROWL_PASSWORD = check_setting_str(CFG, 'Growl', 'growl_password', '') + + CheckSection(CFG, 'Prowl') + USE_PROWL = bool(check_setting_int(CFG, 'Prowl', 'use_prowl', 0)) + PROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsnatch', 0)) + PROWL_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_ondownload', 0)) + PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Prowl', 'prowl_notify_onsubtitledownload', 0)) + PROWL_API = check_setting_str(CFG, 'Prowl', 'prowl_api', '') + PROWL_PRIORITY = check_setting_str(CFG, 'Prowl', 'prowl_priority', "0") + + CheckSection(CFG, 'Twitter') + USE_TWITTER = bool(check_setting_int(CFG, 'Twitter', 'use_twitter', 0)) + TWITTER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_onsnatch', 0)) + TWITTER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_ondownload', 0)) + TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Twitter', 'twitter_notify_onsubtitledownload', 0)) + TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '') + TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '') + TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Sick Beard') + + CheckSection(CFG, 'Notifo') + USE_NOTIFO = bool(check_setting_int(CFG, 'Notifo', 'use_notifo', 0)) + NOTIFO_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_onsnatch', 0)) + NOTIFO_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_ondownload', 0)) + NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Notifo', 'notifo_notify_onsubtitledownload', 0)) + NOTIFO_USERNAME = check_setting_str(CFG, 'Notifo', 'notifo_username', '') + NOTIFO_APISECRET = check_setting_str(CFG, 'Notifo', 'notifo_apisecret', '') + + CheckSection(CFG, 'Boxcar') + USE_BOXCAR = bool(check_setting_int(CFG, 'Boxcar', 'use_boxcar', 0)) + BOXCAR_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_onsnatch', 0)) + BOXCAR_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_ondownload', 0)) + BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_notify_onsubtitledownload', 0)) + BOXCAR_USERNAME = check_setting_str(CFG, 'Boxcar', 'boxcar_username', '') + + CheckSection(CFG, 'Pushover') + USE_PUSHOVER = bool(check_setting_int(CFG, 'Pushover', 'use_pushover', 0)) + PUSHOVER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsnatch', 0)) + PUSHOVER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_ondownload', 0)) + PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0)) + PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '') + + CheckSection(CFG, 'Libnotify') + USE_LIBNOTIFY = bool(check_setting_int(CFG, 'Libnotify', 'use_libnotify', 0)) + LIBNOTIFY_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsnatch', 0)) + LIBNOTIFY_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_ondownload', 0)) + LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsubtitledownload', 0)) + + CheckSection(CFG, 'NMJ') + USE_NMJ = bool(check_setting_int(CFG, 'NMJ', 'use_nmj', 0)) + NMJ_HOST = check_setting_str(CFG, 'NMJ', 'nmj_host', '') + NMJ_DATABASE = check_setting_str(CFG, 'NMJ', 'nmj_database', '') + NMJ_MOUNT = check_setting_str(CFG, 'NMJ', 'nmj_mount', '') + + CheckSection(CFG, 'NMJv2') + USE_NMJv2 = bool(check_setting_int(CFG, 'NMJv2', 'use_nmjv2', 0)) + NMJv2_HOST = check_setting_str(CFG, 'NMJv2', 'nmjv2_host', '') + NMJv2_DATABASE = check_setting_str(CFG, 'NMJv2', 'nmjv2_database', '') + NMJv2_DBLOC = check_setting_str(CFG, 'NMJv2', 'nmjv2_dbloc', '') + + CheckSection(CFG, 'Synology') + USE_SYNOINDEX = bool(check_setting_int(CFG, 'Synology', 'use_synoindex', 0)) + + CheckSection(CFG, 'SynologyNotifier') + USE_SYNOLOGYNOTIFIER = bool(check_setting_int(CFG, 'SynologyNotifier', 'use_synologynotifier', 0)) + SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'SynologyNotifier', 'synologynotifier_notify_onsnatch', 0)) + SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'SynologyNotifier', 'synologynotifier_notify_ondownload', 0)) + SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'SynologyNotifier', 'synologynotifier_notify_onsubtitledownload', 0)) + + + CheckSection(CFG, 'Trakt') + USE_TRAKT = bool(check_setting_int(CFG, 'Trakt', 'use_trakt', 0)) + TRAKT_USERNAME = check_setting_str(CFG, 'Trakt', 'trakt_username', '') + TRAKT_PASSWORD = check_setting_str(CFG, 'Trakt', 'trakt_password', '') + TRAKT_API = check_setting_str(CFG, 'Trakt', 'trakt_api', '') + TRAKT_REMOVE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_remove_watchlist', 0)) + TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0)) + TRAKT_METHOD_ADD = check_setting_str(CFG, 'Trakt', 'trakt_method_add', "0") + TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0)) + + CheckSection(CFG, 'pyTivo') + USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0)) + PYTIVO_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsnatch', 0)) + PYTIVO_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_ondownload', 0)) + PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'pyTivo', 'pytivo_notify_onsubtitledownload', 0)) + PYTIVO_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'pyTivo', 'pyTivo_update_library', 0)) + PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '') + PYTIVO_SHARE_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_share_name', '') + PYTIVO_TIVO_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_tivo_name', '') + + CheckSection(CFG, 'NMA') + USE_NMA = bool(check_setting_int(CFG, 'NMA', 'use_nma', 0)) + NMA_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsnatch', 0)) + NMA_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_ondownload', 0)) + NMA_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'NMA', 'nma_notify_onsubtitledownload', 0)) + NMA_API = check_setting_str(CFG, 'NMA', 'nma_api', '') + NMA_PRIORITY = check_setting_str(CFG, 'NMA', 'nma_priority', "0") + + CheckSection(CFG, 'Pushalot') + USE_PUSHALOT = bool(check_setting_int(CFG, 'Pushalot', 'use_pushalot', 0)) + PUSHALOT_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_notify_onsnatch', 0)) + PUSHALOT_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_notify_ondownload', 0)) + PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_notify_onsubtitledownload', 0)) + PUSHALOT_AUTHORIZATIONTOKEN = check_setting_str(CFG, 'Pushalot', 'pushalot_authorizationtoken', '') + + CheckSection(CFG, 'Mail') + USE_MAIL = bool(check_setting_int(CFG, 'Mail', 'use_mail', 0)) + MAIL_USERNAME = check_setting_str(CFG, 'Mail', 'mail_username', '') + MAIL_PASSWORD = check_setting_str(CFG, 'Mail', 'mail_password', '') + MAIL_SERVER = check_setting_str(CFG, 'Mail', 'mail_server', '') + MAIL_SSL = bool(check_setting_int(CFG, 'Mail', 'mail_ssl', 0)) + MAIL_FROM = check_setting_str(CFG, 'Mail', 'mail_from', '') + MAIL_TO = check_setting_str(CFG, 'Mail', 'mail_to', '') + MAIL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Mail', 'mail_notify_onsnatch', 0)) + + + USE_SUBTITLES = bool(check_setting_int(CFG, 'Subtitles', 'use_subtitles', 0)) + SUBTITLES_LANGUAGES = check_setting_str(CFG, 'Subtitles', 'subtitles_languages', '').split(',') + if SUBTITLES_LANGUAGES[0] == '': + SUBTITLES_LANGUAGES = [] + SUBTITLES_DIR = check_setting_str(CFG, 'Subtitles', 'subtitles_dir', '') + SUBTITLES_DIR_SUB = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_dir_sub', 0)) + SUBSNOLANG = bool(check_setting_int(CFG, 'Subtitles', 'subsnolang', 0)) + SUBTITLES_SERVICES_LIST = check_setting_str(CFG, 'Subtitles', 'SUBTITLES_SERVICES_LIST', '').split(',') + SUBTITLES_SERVICES_ENABLED = [int(x) for x in check_setting_str(CFG, 'Subtitles', 'SUBTITLES_SERVICES_ENABLED', '').split('|') if x] + SUBTITLES_DEFAULT = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_default', 0)) + SUBTITLES_HISTORY = bool(check_setting_int(CFG, 'Subtitles', 'subtitles_history', 0)) + # start up all the threads + logger.sb_log_instance.initLogging(consoleLogging=consoleLogging) + + # initialize the main SB database + db.upgradeDatabase(db.DBConnection(), mainDB.InitialSchema) + + # initialize the cache database + db.upgradeDatabase(db.DBConnection("cache.db"), cache_db.InitialSchema) + + # fix up any db problems + db.sanityCheckDatabase(db.DBConnection(), mainDB.MainSanityCheck) + + # migrate the config if it needs it + migrator = ConfigMigrator(CFG) + migrator.migrate_config() + + currentSearchScheduler = scheduler.Scheduler(searchCurrent.CurrentSearcher(), + cycleTime=datetime.timedelta(minutes=SEARCH_FREQUENCY), + threadName="SEARCH", + runImmediately=True) + + # the interval for this is stored inside the ShowUpdater class + showUpdaterInstance = showUpdater.ShowUpdater() + if UPDATE_SHOWS_ON_START == True: + showUpdateScheduler = scheduler.Scheduler(showUpdaterInstance, + cycleTime=showUpdaterInstance.updateInterval, + threadName="SHOWUPDATER", + runImmediately=True) + + else: + showUpdateScheduler = scheduler.Scheduler(showUpdaterInstance, + cycleTime=showUpdaterInstance.updateInterval, + threadName="SHOWUPDATER", + runImmediately=False) + + + versionCheckScheduler = scheduler.Scheduler(versionChecker.CheckVersion(), + cycleTime=datetime.timedelta(hours=12), + threadName="CHECKVERSION", + runImmediately=True) + + showQueueScheduler = scheduler.Scheduler(show_queue.ShowQueue(), + cycleTime=datetime.timedelta(seconds=3), + threadName="SHOWQUEUE", + silent=True) + + searchQueueScheduler = scheduler.Scheduler(search_queue.SearchQueue(), + cycleTime=datetime.timedelta(seconds=3), + threadName="SEARCHQUEUE", + silent=True) + + properFinderInstance = properFinder.ProperFinder() + properFinderScheduler = scheduler.Scheduler(properFinderInstance, + cycleTime=properFinderInstance.updateInterval, + threadName="FINDPROPERS", + runImmediately=False) + + if PROCESS_AUTOMATICALLY: + autoPostProcesserScheduler = scheduler.Scheduler(autoPostProcesser.PostProcesser( TV_DOWNLOAD_DIR ), + cycleTime=datetime.timedelta(minutes=10), + threadName="NZB_POSTPROCESSER", + runImmediately=True) + + if PROCESS_AUTOMATICALLY_TORRENT: + autoTorrentPostProcesserScheduler = scheduler.Scheduler(autoPostProcesser.PostProcesser( TORRENT_DOWNLOAD_DIR ), + cycleTime=datetime.timedelta(minutes=10), + threadName="TORRENT_POSTPROCESSER", + runImmediately=True) + + traktWatchListCheckerSchedular = scheduler.Scheduler(traktWatchListChecker.TraktChecker(), + cycleTime=datetime.timedelta(minutes=10), + threadName="TRAKTWATCHLIST", + runImmediately=True) + + backlogSearchScheduler = searchBacklog.BacklogSearchScheduler(searchBacklog.BacklogSearcher(), + cycleTime=datetime.timedelta(minutes=get_backlog_cycle_time()), + threadName="BACKLOG", + runImmediately=True) + backlogSearchScheduler.action.cycleTime = BACKLOG_SEARCH_FREQUENCY + + + subtitlesFinderScheduler = scheduler.Scheduler(subtitles.SubtitlesFinder(), + cycleTime=datetime.timedelta(hours=1), + threadName="FINDSUBTITLES", + runImmediately=True) + + showList = [] + loadingShowList = {} + + __INITIALIZED__ = True + return True + + +def start(): + + global __INITIALIZED__, currentSearchScheduler, backlogSearchScheduler, \ + showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \ + properFinderScheduler, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, searchQueueScheduler, \ + subtitlesFinderScheduler, started, USE_SUBTITLES, \ + traktWatchListCheckerSchedular, started + + with INIT_LOCK: + + if __INITIALIZED__: + + # start the search scheduler + currentSearchScheduler.thread.start() + + # start the backlog scheduler + backlogSearchScheduler.thread.start() + + # start the show updater + showUpdateScheduler.thread.start() + + # start the version checker + versionCheckScheduler.thread.start() + + # start the queue checker + showQueueScheduler.thread.start() + + # start the search queue checker + searchQueueScheduler.thread.start() + + # start the queue checker + properFinderScheduler.thread.start() + + if autoPostProcesserScheduler: + autoPostProcesserScheduler.thread.start() + + if autoTorrentPostProcesserScheduler: + autoTorrentPostProcesserScheduler.thread.start() + + # start the subtitles finder + if USE_SUBTITLES: + subtitlesFinderScheduler.thread.start() + + # start the trakt watchlist + traktWatchListCheckerSchedular.thread.start() + + started = True + +def halt(): + + global __INITIALIZED__, currentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \ + showQueueScheduler, properFinderScheduler, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, searchQueueScheduler, \ + subtitlesFinderScheduler, started, \ + traktWatchListCheckerSchedular + + with INIT_LOCK: + + if __INITIALIZED__: + + logger.log(u"Aborting all threads") + + # abort all the threads + + currentSearchScheduler.abort = True + logger.log(u"Waiting for the SEARCH thread to exit") + try: + currentSearchScheduler.thread.join(10) + except: + pass + + backlogSearchScheduler.abort = True + logger.log(u"Waiting for the BACKLOG thread to exit") + try: + backlogSearchScheduler.thread.join(10) + except: + pass + + showUpdateScheduler.abort = True + logger.log(u"Waiting for the SHOWUPDATER thread to exit") + try: + showUpdateScheduler.thread.join(10) + except: + pass + + versionCheckScheduler.abort = True + logger.log(u"Waiting for the VERSIONCHECKER thread to exit") + try: + versionCheckScheduler.thread.join(10) + except: + pass + + showQueueScheduler.abort = True + logger.log(u"Waiting for the SHOWQUEUE thread to exit") + try: + showQueueScheduler.thread.join(10) + except: + pass + + searchQueueScheduler.abort = True + logger.log(u"Waiting for the SEARCHQUEUE thread to exit") + try: + searchQueueScheduler.thread.join(10) + except: + pass + + if autoPostProcesserScheduler: + autoPostProcesserScheduler.abort = True + logger.log(u"Waiting for the NZB_POSTPROCESSER thread to exit") + try: + autoPostProcesserScheduler.thread.join(10) + except: + pass + + if autoTorrentPostProcesserScheduler: + autoTorrentPostProcesserScheduler.abort = True + logger.log(u"Waiting for the TORRENT_POSTPROCESSER thread to exit") + try: + autoTorrentPostProcesserScheduler.thread.join(10) + except: + pass + traktWatchListCheckerSchedular.abort = True + logger.log(u"Waiting for the TRAKTWATCHLIST thread to exit") + try: + traktWatchListCheckerSchedular.thread.join(10) + except: + pass + + properFinderScheduler.abort = True + logger.log(u"Waiting for the PROPERFINDER thread to exit") + try: + properFinderScheduler.thread.join(10) + except: + pass + + subtitlesFinderScheduler.abort = True + logger.log(u"Waiting for the SUBTITLESFINDER thread to exit") + try: + subtitlesFinderScheduler.thread.join(10) + except: + pass + + + __INITIALIZED__ = False + + +def sig_handler(signum=None, frame=None): + if type(signum) != type(None): + logger.log(u"Signal %i caught, saving and exiting..." % int(signum)) + saveAndShutdown() + + +def saveAll(): + + global showList + + # write all shows + logger.log(u"Saving all shows to the database") + for show in showList: + show.saveToDB() + + # save config + logger.log(u"Saving config file to disk") + save_config() + + +def saveAndShutdown(restart=False): + + halt() + + saveAll() + + logger.log(u"Killing cherrypy") + cherrypy.engine.exit() + + if CREATEPID: + logger.log(u"Removing pidfile " + str(PIDFILE)) + os.remove(PIDFILE) + + if restart: + install_type = versionCheckScheduler.action.install_type + + popen_list = [] + + if install_type in ('git', 'source'): + popen_list = [sys.executable, MY_FULLNAME] + elif install_type == 'win': + if hasattr(sys, 'frozen'): + # c:\dir\to\updater.exe 12345 c:\dir\to\sickbeard.exe + popen_list = [os.path.join(PROG_DIR, 'updater.exe'), str(PID), sys.executable] + else: + logger.log(u"Unknown SB launch method, please file a bug report about this", logger.ERROR) + popen_list = [sys.executable, os.path.join(PROG_DIR, 'updater.py'), str(PID), sys.executable, MY_FULLNAME ] + + if popen_list: + popen_list += MY_ARGS + if '--nolaunch' not in popen_list: + popen_list += ['--nolaunch'] + logger.log(u"Restarting Sick Beard with " + str(popen_list)) + subprocess.Popen(popen_list, cwd=os.getcwd()) + + os._exit(0) + + +def invoke_command(to_call, *args, **kwargs): + global invoked_command + + def delegate(): + to_call(*args, **kwargs) + invoked_command = delegate + logger.log(u"Placed invoked command: " + repr(invoked_command) + " for " + repr(to_call) + " with " + repr(args) + " and " + repr(kwargs), logger.DEBUG) + + +def invoke_restart(soft=True): + invoke_command(restart, soft=soft) + + +def invoke_shutdown(): + invoke_command(saveAndShutdown) + + +def restart(soft=True): + if soft: + halt() + saveAll() + #logger.log(u"Restarting cherrypy") + #cherrypy.engine.restart() + logger.log(u"Re-initializing all data") + initialize() + + else: + saveAndShutdown(restart=True) + + +def save_config(): + + new_config = ConfigObj() + new_config.filename = CONFIG_FILE + + new_config['General'] = {} + new_config['General']['log_dir'] = LOG_DIR + new_config['General']['web_port'] = WEB_PORT + new_config['General']['web_host'] = WEB_HOST + new_config['General']['web_ipv6'] = int(WEB_IPV6) + new_config['General']['web_log'] = int(WEB_LOG) + new_config['General']['web_root'] = WEB_ROOT + new_config['General']['web_username'] = WEB_USERNAME + new_config['General']['web_password'] = WEB_PASSWORD + new_config['General']['use_api'] = int(USE_API) + new_config['General']['api_key'] = API_KEY + new_config['General']['enable_https'] = int(ENABLE_HTTPS) + new_config['General']['https_cert'] = HTTPS_CERT + new_config['General']['https_key'] = HTTPS_KEY + new_config['General']['use_nzbs'] = int(USE_NZBS) + new_config['General']['use_torrents'] = int(USE_TORRENTS) + new_config['General']['nzb_method'] = NZB_METHOD + new_config['General']['torrent_method'] = TORRENT_METHOD + new_config['General']['prefered_method'] = PREFERED_METHOD + new_config['General']['usenet_retention'] = int(USENET_RETENTION) + new_config['General']['search_frequency'] = int(SEARCH_FREQUENCY) + new_config['General']['download_propers'] = int(DOWNLOAD_PROPERS) + new_config['General']['quality_default'] = int(QUALITY_DEFAULT) + new_config['General']['status_default'] = int(STATUS_DEFAULT) + new_config['General']['audio_show_default'] = AUDIO_SHOW_DEFAULT + new_config['General']['flatten_folders_default'] = int(FLATTEN_FOLDERS_DEFAULT) + new_config['General']['provider_order'] = ' '.join([x.getID() for x in providers.sortedProviderList()]) + new_config['General']['version_notify'] = int(VERSION_NOTIFY) + new_config['General']['naming_pattern'] = NAMING_PATTERN + new_config['General']['naming_custom_abd'] = int(NAMING_CUSTOM_ABD) + new_config['General']['naming_abd_pattern'] = NAMING_ABD_PATTERN + new_config['General']['naming_multi_ep'] = int(NAMING_MULTI_EP) + new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) + new_config['General']['update_shows_on_start'] = int(UPDATE_SHOWS_ON_START) + new_config['General']['sort_article'] = int(SORT_ARTICLE) + + new_config['General']['use_banner'] = int(USE_BANNER) + new_config['General']['use_listview'] = int(USE_LISTVIEW) + new_config['General']['metadata_xbmc'] = metadata_provider_dict['XBMC'].get_config() + new_config['General']['metadata_xbmcfrodo'] = metadata_provider_dict['XBMC (Frodo)'].get_config() + new_config['General']['metadata_mediabrowser'] = metadata_provider_dict['MediaBrowser'].get_config() + new_config['General']['metadata_ps3'] = metadata_provider_dict['Sony PS3'].get_config() + new_config['General']['metadata_wdtv'] = metadata_provider_dict['WDTV'].get_config() + new_config['General']['metadata_tivo'] = metadata_provider_dict['TIVO'].get_config() + new_config['General']['metadata_synology'] = metadata_provider_dict['Synology'].get_config() + + new_config['General']['cache_dir'] = ACTUAL_CACHE_DIR if ACTUAL_CACHE_DIR else 'cache' + new_config['General']['root_dirs'] = ROOT_DIRS if ROOT_DIRS else '' + new_config['General']['tv_download_dir'] = TV_DOWNLOAD_DIR + new_config['General']['torrent_download_dir'] = TORRENT_DOWNLOAD_DIR + new_config['General']['keep_processed_dir'] = int(KEEP_PROCESSED_DIR) + new_config['General']['move_associated_files'] = int(MOVE_ASSOCIATED_FILES) + new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) + new_config['General']['process_automatically_torrent'] = int(PROCESS_AUTOMATICALLY_TORRENT) + new_config['General']['rename_episodes'] = int(RENAME_EPISODES) + new_config['General']['create_missing_show_dirs'] = CREATE_MISSING_SHOW_DIRS + new_config['General']['add_shows_wo_dir'] = ADD_SHOWS_WO_DIR + + new_config['General']['extra_scripts'] = '|'.join(EXTRA_SCRIPTS) + new_config['General']['git_path'] = GIT_PATH + new_config['General']['ignore_words'] = IGNORE_WORDS + + new_config['Blackhole'] = {} + new_config['Blackhole']['nzb_dir'] = NZB_DIR + new_config['Blackhole']['torrent_dir'] = TORRENT_DIR + + new_config['EZRSS'] = {} + new_config['EZRSS']['ezrss'] = int(EZRSS) + + new_config['TVTORRENTS'] = {} + new_config['TVTORRENTS']['tvtorrents'] = int(TVTORRENTS) + new_config['TVTORRENTS']['tvtorrents_digest'] = TVTORRENTS_DIGEST + new_config['TVTORRENTS']['tvtorrents_hash'] = TVTORRENTS_HASH + + new_config['BTN'] = {} + new_config['BTN']['btn'] = int(BTN) + new_config['BTN']['btn_api_key'] = BTN_API_KEY + + new_config['TorrentLeech'] = {} + new_config['TorrentLeech']['torrentleech'] = int(TORRENTLEECH) + new_config['TorrentLeech']['torrentleech_key'] = TORRENTLEECH_KEY + + new_config['NZBs'] = {} + new_config['NZBs']['nzbs'] = int(NZBS) + new_config['NZBs']['nzbs_uid'] = NZBS_UID + new_config['NZBs']['nzbs_hash'] = NZBS_HASH + + new_config['NZBsRUS'] = {} + new_config['NZBsRUS']['nzbsrus'] = int(NZBSRUS) + new_config['NZBsRUS']['nzbsrus_uid'] = NZBSRUS_UID + new_config['NZBsRUS']['nzbsrus_hash'] = NZBSRUS_HASH + + new_config['NZBMatrix'] = {} + new_config['NZBMatrix']['nzbmatrix'] = int(NZBMATRIX) + new_config['NZBMatrix']['nzbmatrix_username'] = NZBMATRIX_USERNAME + new_config['NZBMatrix']['nzbmatrix_apikey'] = NZBMATRIX_APIKEY + + new_config['Newzbin'] = {} + new_config['Newzbin']['newzbin'] = int(NEWZBIN) + new_config['Newzbin']['newzbin_username'] = NEWZBIN_USERNAME + new_config['Newzbin']['newzbin_password'] = NEWZBIN_PASSWORD + + new_config['BinNewz'] = {} + new_config['BinNewz']['binnewz'] = int(BINNEWZ) + + new_config['T411'] = {} + new_config['T411']['t411'] = int(T411) + new_config['T411']['username'] = T411_USERNAME + new_config['T411']['password'] = T411_PASSWORD + + new_config['Cpasbien'] = {} + new_config['Cpasbien']['cpasbien'] = int(Cpasbien) + + new_config['kat'] = {} + new_config['kat']['kat'] = int(kat) + + new_config['PirateBay'] = {} + new_config['PirateBay']['piratebay'] = int(THEPIRATEBAY) + new_config['PirateBay']['piratebay_proxy'] = THEPIRATEBAY_PROXY + new_config['PirateBay']['piratebay_proxy_url'] = THEPIRATEBAY_PROXY_URL + new_config['PirateBay']['piratebay_trusted'] = THEPIRATEBAY_TRUSTED + + new_config['Womble'] = {} + new_config['Womble']['womble'] = int(WOMBLE) + + new_config['nzbX'] = {} + new_config['nzbX']['nzbx'] = int(NZBX) + new_config['nzbX']['nzbx_completion'] = int(NZBX_COMPLETION) + + new_config['omgwtfnzbs'] = {} + new_config['omgwtfnzbs']['omgwtfnzbs'] = int(OMGWTFNZBS) + new_config['omgwtfnzbs']['omgwtfnzbs_uid'] = OMGWTFNZBS_UID + new_config['omgwtfnzbs']['omgwtfnzbs_key'] = OMGWTFNZBS_KEY + + new_config['GKS'] = {} + new_config['GKS']['gks'] = int(GKS) + new_config['GKS']['gks_key'] = GKS_KEY + + new_config['SABnzbd'] = {} + new_config['SABnzbd']['sab_username'] = SAB_USERNAME + new_config['SABnzbd']['sab_password'] = SAB_PASSWORD + new_config['SABnzbd']['sab_apikey'] = SAB_APIKEY + new_config['SABnzbd']['sab_category'] = SAB_CATEGORY + new_config['SABnzbd']['sab_host'] = SAB_HOST + + new_config['NZBget'] = {} + new_config['NZBget']['nzbget_password'] = NZBGET_PASSWORD + new_config['NZBget']['nzbget_category'] = NZBGET_CATEGORY + new_config['NZBget']['nzbget_host'] = NZBGET_HOST + + new_config['TORRENT'] = {} + new_config['TORRENT']['torrent_username'] = TORRENT_USERNAME + new_config['TORRENT']['torrent_password'] = TORRENT_PASSWORD + new_config['TORRENT']['torrent_host'] = TORRENT_HOST + new_config['TORRENT']['torrent_path'] = TORRENT_PATH + new_config['TORRENT']['torrent_ratio'] = TORRENT_RATIO + new_config['TORRENT']['torrent_paused'] = int(TORRENT_PAUSED) + new_config['TORRENT']['torrent_label'] = TORRENT_LABEL + + new_config['XBMC'] = {} + new_config['XBMC']['use_xbmc'] = int(USE_XBMC) + new_config['XBMC']['xbmc_notify_onsnatch'] = int(XBMC_NOTIFY_ONSNATCH) + new_config['XBMC']['xbmc_notify_ondownload'] = int(XBMC_NOTIFY_ONDOWNLOAD) + new_config['XBMC']['xbmc_notify_onsubtitledownload'] = int(XBMC_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['XBMC']['xbmc_update_library'] = int(XBMC_UPDATE_LIBRARY) + new_config['XBMC']['xbmc_update_full'] = int(XBMC_UPDATE_FULL) + new_config['XBMC']['xbmc_update_onlyfirst'] = int(XBMC_UPDATE_ONLYFIRST) + new_config['XBMC']['xbmc_host'] = XBMC_HOST + new_config['XBMC']['xbmc_username'] = XBMC_USERNAME + new_config['XBMC']['xbmc_password'] = XBMC_PASSWORD + + new_config['Plex'] = {} + new_config['Plex']['use_plex'] = int(USE_PLEX) + new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH) + new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD) + new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Plex']['plex_update_library'] = int(PLEX_UPDATE_LIBRARY) + new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST + new_config['Plex']['plex_host'] = PLEX_HOST + new_config['Plex']['plex_username'] = PLEX_USERNAME + new_config['Plex']['plex_password'] = PLEX_PASSWORD + + new_config['Growl'] = {} + new_config['Growl']['use_growl'] = int(USE_GROWL) + new_config['Growl']['growl_notify_onsnatch'] = int(GROWL_NOTIFY_ONSNATCH) + new_config['Growl']['growl_notify_ondownload'] = int(GROWL_NOTIFY_ONDOWNLOAD) + new_config['Growl']['growl_notify_onsubtitledownload'] = int(GROWL_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Growl']['growl_host'] = GROWL_HOST + new_config['Growl']['growl_password'] = GROWL_PASSWORD + + new_config['Prowl'] = {} + new_config['Prowl']['use_prowl'] = int(USE_PROWL) + new_config['Prowl']['prowl_notify_onsnatch'] = int(PROWL_NOTIFY_ONSNATCH) + new_config['Prowl']['prowl_notify_ondownload'] = int(PROWL_NOTIFY_ONDOWNLOAD) + new_config['Prowl']['prowl_notify_onsubtitledownload'] = int(PROWL_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Prowl']['prowl_api'] = PROWL_API + new_config['Prowl']['prowl_priority'] = PROWL_PRIORITY + + new_config['Twitter'] = {} + new_config['Twitter']['use_twitter'] = int(USE_TWITTER) + new_config['Twitter']['twitter_notify_onsnatch'] = int(TWITTER_NOTIFY_ONSNATCH) + new_config['Twitter']['twitter_notify_ondownload'] = int(TWITTER_NOTIFY_ONDOWNLOAD) + new_config['Twitter']['twitter_notify_onsubtitledownload'] = int(TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Twitter']['twitter_username'] = TWITTER_USERNAME + new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD + new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX + + new_config['Notifo'] = {} + new_config['Notifo']['use_notifo'] = int(USE_NOTIFO) + new_config['Notifo']['notifo_notify_onsnatch'] = int(NOTIFO_NOTIFY_ONSNATCH) + new_config['Notifo']['notifo_notify_ondownload'] = int(NOTIFO_NOTIFY_ONDOWNLOAD) + new_config['Notifo']['notifo_notify_onsubtitledownload'] = int(NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Notifo']['notifo_username'] = NOTIFO_USERNAME + new_config['Notifo']['notifo_apisecret'] = NOTIFO_APISECRET + + new_config['Boxcar'] = {} + new_config['Boxcar']['use_boxcar'] = int(USE_BOXCAR) + new_config['Boxcar']['boxcar_notify_onsnatch'] = int(BOXCAR_NOTIFY_ONSNATCH) + new_config['Boxcar']['boxcar_notify_ondownload'] = int(BOXCAR_NOTIFY_ONDOWNLOAD) + new_config['Boxcar']['boxcar_notify_onsubtitledownload'] = int(BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Boxcar']['boxcar_username'] = BOXCAR_USERNAME + + new_config['Pushover'] = {} + new_config['Pushover']['use_pushover'] = int(USE_PUSHOVER) + new_config['Pushover']['pushover_notify_onsnatch'] = int(PUSHOVER_NOTIFY_ONSNATCH) + new_config['Pushover']['pushover_notify_ondownload'] = int(PUSHOVER_NOTIFY_ONDOWNLOAD) + new_config['Pushover']['pushover_notify_onsubtitledownload'] = int(PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Pushover']['pushover_userkey'] = PUSHOVER_USERKEY + + new_config['Libnotify'] = {} + new_config['Libnotify']['use_libnotify'] = int(USE_LIBNOTIFY) + new_config['Libnotify']['libnotify_notify_onsnatch'] = int(LIBNOTIFY_NOTIFY_ONSNATCH) + new_config['Libnotify']['libnotify_notify_ondownload'] = int(LIBNOTIFY_NOTIFY_ONDOWNLOAD) + new_config['Libnotify']['libnotify_notify_onsubtitledownload'] = int(LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD) + + new_config['NMJ'] = {} + new_config['NMJ']['use_nmj'] = int(USE_NMJ) + new_config['NMJ']['nmj_host'] = NMJ_HOST + new_config['NMJ']['nmj_database'] = NMJ_DATABASE + new_config['NMJ']['nmj_mount'] = NMJ_MOUNT + + new_config['Synology'] = {} + new_config['Synology']['use_synoindex'] = int(USE_SYNOINDEX) + + new_config['SynologyNotifier'] = {} + new_config['SynologyNotifier']['use_synologynotifier'] = int(USE_SYNOLOGYNOTIFIER) + new_config['SynologyNotifier']['synologynotifier_notify_onsnatch'] = int(SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH) + new_config['SynologyNotifier']['synologynotifier_notify_ondownload'] = int(SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD) + new_config['SynologyNotifier']['synologynotifier_notify_onsubtitledownload'] = int(SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD) + + + new_config['NMJv2'] = {} + new_config['NMJv2']['use_nmjv2'] = int(USE_NMJv2) + new_config['NMJv2']['nmjv2_host'] = NMJv2_HOST + new_config['NMJv2']['nmjv2_database'] = NMJv2_DATABASE + new_config['NMJv2']['nmjv2_dbloc'] = NMJv2_DBLOC + + new_config['Trakt'] = {} + new_config['Trakt']['use_trakt'] = int(USE_TRAKT) + new_config['Trakt']['trakt_username'] = TRAKT_USERNAME + new_config['Trakt']['trakt_password'] = TRAKT_PASSWORD + new_config['Trakt']['trakt_api'] = TRAKT_API + new_config['Trakt']['trakt_remove_watchlist'] = int(TRAKT_REMOVE_WATCHLIST) + new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST) + new_config['Trakt']['trakt_method_add'] = TRAKT_METHOD_ADD + new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED) + + new_config['pyTivo'] = {} + new_config['pyTivo']['use_pytivo'] = int(USE_PYTIVO) + new_config['pyTivo']['pytivo_notify_onsnatch'] = int(PYTIVO_NOTIFY_ONSNATCH) + new_config['pyTivo']['pytivo_notify_ondownload'] = int(PYTIVO_NOTIFY_ONDOWNLOAD) + new_config['pyTivo']['pytivo_notify_onsubtitledownload'] = int(PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['pyTivo']['pyTivo_update_library'] = int(PYTIVO_UPDATE_LIBRARY) + new_config['pyTivo']['pytivo_host'] = PYTIVO_HOST + new_config['pyTivo']['pytivo_share_name'] = PYTIVO_SHARE_NAME + new_config['pyTivo']['pytivo_tivo_name'] = PYTIVO_TIVO_NAME + + new_config['NMA'] = {} + new_config['NMA']['use_nma'] = int(USE_NMA) + new_config['NMA']['nma_notify_onsnatch'] = int(NMA_NOTIFY_ONSNATCH) + new_config['NMA']['nma_notify_ondownload'] = int(NMA_NOTIFY_ONDOWNLOAD) + new_config['NMA']['nma_notify_onsubtitledownload'] = int(NMA_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['NMA']['nma_api'] = NMA_API + new_config['NMA']['nma_priority'] = NMA_PRIORITY + + new_config['Pushalot'] = {} + new_config['Pushalot']['use_pushalot'] = int(USE_PUSHALOT) + new_config['Pushalot']['pushalot_notify_onsnatch'] = int(PUSHALOT_NOTIFY_ONSNATCH) + new_config['Pushalot']['pushalot_notify_ondownload'] = int(PUSHALOT_NOTIFY_ONDOWNLOAD) + new_config['Pushalot']['pushalot_notify_onsubtitledownload'] = int(PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD) + new_config['Pushalot']['pushalot_authorizationtoken'] = PUSHALOT_AUTHORIZATIONTOKEN + + new_config['Mail'] = {} + new_config['Mail']['use_mail'] = int(USE_MAIL) + new_config['Mail']['mail_username'] = MAIL_USERNAME + new_config['Mail']['mail_password'] = MAIL_PASSWORD + new_config['Mail']['mail_server'] = MAIL_SERVER + new_config['Mail']['mail_ssl'] = int(MAIL_SSL) + new_config['Mail']['mail_from'] = MAIL_FROM + new_config['Mail']['mail_to'] = MAIL_TO + new_config['Mail']['mail_notify_onsnatch'] = int(MAIL_NOTIFY_ONSNATCH) + + new_config['Newznab'] = {} + new_config['Newznab']['newznab_data'] = '!!!'.join([x.configStr() for x in newznabProviderList]) + + new_config['GUI'] = {} + new_config['GUI']['home_layout'] = HOME_LAYOUT + new_config['GUI']['display_show_specials'] = int(DISPLAY_SHOW_SPECIALS) + new_config['GUI']['coming_eps_layout'] = COMING_EPS_LAYOUT + new_config['GUI']['coming_eps_display_paused'] = int(COMING_EPS_DISPLAY_PAUSED) + new_config['GUI']['coming_eps_sort'] = COMING_EPS_SORT + new_config['GUI']['coming_eps_missed_range'] = int(COMING_EPS_MISSED_RANGE) + + new_config['Subtitles'] = {} + new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES) + new_config['Subtitles']['subtitles_languages'] = ','.join(SUBTITLES_LANGUAGES) + new_config['Subtitles']['SUBTITLES_SERVICES_LIST'] = ','.join(SUBTITLES_SERVICES_LIST) + new_config['Subtitles']['SUBTITLES_SERVICES_ENABLED'] = '|'.join([str(x) for x in SUBTITLES_SERVICES_ENABLED]) + new_config['Subtitles']['subtitles_dir'] = SUBTITLES_DIR + new_config['Subtitles']['subtitles_dir_sub'] = int(SUBTITLES_DIR_SUB) + new_config['Subtitles']['subsnolang'] = int(SUBSNOLANG) + new_config['Subtitles']['subtitles_default'] = int(SUBTITLES_DEFAULT) + new_config['Subtitles']['subtitles_history'] = int(SUBTITLES_HISTORY) + + new_config['General']['config_version'] = CONFIG_VERSION + + new_config.write() + + +def launchBrowser(startPort=None): + if not startPort: + startPort = WEB_PORT + if ENABLE_HTTPS: + browserURL = 'https://localhost:%d%s' % (startPort, WEB_ROOT) + else: + browserURL = 'http://localhost:%d%s' % (startPort, WEB_ROOT) + try: + webbrowser.open(browserURL, 2, 1) + except: + try: + webbrowser.open(browserURL, 1, 1) + except: + logger.log(u"Unable to launch a browser", logger.ERROR) + + +def getEpList(epIDs, showid=None): + if epIDs == None or len(epIDs) == 0: + return [] + + query = "SELECT * FROM tv_episodes WHERE tvdbid in (%s)" % (",".join(['?'] * len(epIDs)),) + params = epIDs + + if showid != None: + query += " AND showid = ?" + params.append(showid) + + myDB = db.DBConnection() + sqlResults = myDB.select(query, params) + + epList = [] + + for curEp in sqlResults: + curShowObj = helpers.findCertainShow(showList, int(curEp["showid"])) + curEpObj = curShowObj.getEpisode(int(curEp["season"]), int(curEp["episode"])) + epList.append(curEpObj) + + return epList diff --git a/sickbeard/databases/cache_db.py b/sickbeard/databases/cache_db.py index 635e8a0db95114bda926083fa3b83b500219a998..5675ab8b745db0f04689d8cd8f9c22cd03e96948 100644 --- a/sickbeard/databases/cache_db.py +++ b/sickbeard/databases/cache_db.py @@ -48,4 +48,11 @@ class AddSceneNameCache(AddSceneExceptions): return self.hasTable("scene_names") def execute(self): - self.connection.action("CREATE TABLE scene_names (tvdb_id INTEGER, name TEXT)") \ No newline at end of file + self.connection.action("CREATE TABLE scene_names (tvdb_id INTEGER, name TEXT)") + +class AddNetworkTimezones(AddSceneNameCache): + def test(self): + return self.hasTable("network_timezones") + + def execute(self): + self.connection.action("CREATE TABLE network_timezones (network_name TEXT PRIMARY KEY, timezone TEXT)") \ No newline at end of file diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 56225c7d70fa92d4fb185fb66ce809ba44fe8be9..fbed18b5d1a0e5583cf54917a63e8d9a247706f8 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -25,7 +25,7 @@ from sickbeard.providers.generic import GenericProvider from sickbeard import encodingKludge as ek from sickbeard.name_parser.parser import NameParser, InvalidNameException -MAX_DB_VERSION = 14 +MAX_DB_VERSION = 15 class MainSanityCheck(db.DBSanityCheck): @@ -101,7 +101,9 @@ class InitialSchema (db.SchemaUpgrade): "CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, tvdbid NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC, hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT);", "CREATE TABLE info (last_backlog NUMERIC, last_tvdb NUMERIC);", "CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC, quality NUMERIC, resource TEXT, provider NUMERIC);", - "CREATE TABLE episode_links (episode_id INTEGER, link TEXT);" + "CREATE TABLE episode_links (episode_id INTEGER, link TEXT);", + "CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC);" + ] for query in queries: self.connection.action(query) @@ -122,6 +124,12 @@ class AddTvrName (AddTvrId): def execute(self): self.addColumn("tv_shows", "tvr_name", "TEXT", "") +class AddImdbId (InitialSchema): + def test(self): + return self.hasColumn("tv_shows", "imdb_id") + + def execute(self): + self.addColumn("tv_shows", "imdb_id", "TEXT", "") class AddAirdateIndex (AddTvrName): def test(self): @@ -142,7 +150,9 @@ class NumericProviders (AddAirdateIndex): 4: 'eztv', 5: 'nzbmatrix', 6: 'tvnzb', - 7: 'ezrss'} + 7: 'ezrss', + 8: 'thepiratebay', + 9: 'kat'}, def execute(self): self.connection.action("ALTER TABLE history RENAME TO history_old") @@ -707,3 +717,11 @@ class AddEpisodeLinkTable(AddSubtitlesSupport): if self.hasTable("episode_links") != True: self.connection.action("CREATE TABLE episode_links (episode_id INTEGER, link TEXT)") self.incDBVersion() +class AddIMDbInfo(AddEpisodeLinkTable): + def test(self): + return self.checkDBVersion() >= 15 + + def execute(self): + + self.connection.action("CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)") + self.incDBVersion() \ No newline at end of file diff --git a/sickbeard/db.py b/sickbeard/db.py index 34a9b4111bcfdf3aa4fec72ee8af64b3931fa014..aace0fb826bdbf8264f9cb07fd71155ee4d1de2c 100644 --- a/sickbeard/db.py +++ b/sickbeard/db.py @@ -67,15 +67,15 @@ class DBConnection: return 0 def mass_action(self, querylist, logTransaction=False): - + with db_lock: - + if querylist == None: return - + sqlResult = [] attempt = 0 - + while attempt < 5: try: for qu in querylist: @@ -107,9 +107,9 @@ class DBConnection: self.connection.rollback() logger.log(u"Fatal error executing query: " + ex(e), logger.ERROR) raise - + return sqlResult - + def action(self, query, args=None): with db_lock: @@ -123,10 +123,10 @@ class DBConnection: while attempt < 5: try: if args == None: - logger.log(self.filename+": "+query, logger.DEBUG) + logger.log(self.filename+": "+query, logger.DB) sqlResult = self.connection.execute(query) else: - logger.log(self.filename+": "+query+" with args "+str(args), logger.DEBUG) + logger.log(self.filename+": "+query+" with args "+str(args), logger.DB) sqlResult = self.connection.execute(query, args) self.connection.commit() # get out of the connection attempt loop since we were successful diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 9621874e0a672ce7d58683dc1925ebf489f6578e..93208c7eb6e291fb93833ead9629fc5094855e4b 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -1,751 +1,817 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import StringIO, zlib, gzip -import os -import stat -import urllib, urllib2 -import re, socket -import shutil -import traceback -import time, sys - -from httplib import BadStatusLine - -from xml.dom.minidom import Node - -import sickbeard - -from sickbeard.exceptions import MultipleShowObjectsException, ex -from sickbeard import logger, classes, common -from sickbeard.common import USER_AGENT, mediaExtensions, XML_NSMAP, subtitleExtensions - -from sickbeard import db -from sickbeard import encodingKludge as ek -from sickbeard import notifiers - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - -import xml.etree.cElementTree as etree - -from lib import subliminal -#from sickbeard.subtitles import EXTENSIONS - -urllib._urlopener = classes.SickBeardURLopener() - -def indentXML(elem, level=0): - ''' - Does our pretty printing, makes Matt very happy - ''' - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indentXML(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - # Strip out the newlines from text - if elem.text: - elem.text = elem.text.replace('\n', ' ') - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - -def replaceExtension(file, newExt): - ''' - >>> replaceExtension('foo.avi', 'mkv') - 'foo.mkv' - >>> replaceExtension('.vimrc', 'arglebargle') - '.vimrc' - >>> replaceExtension('a.b.c', 'd') - 'a.b.d' - >>> replaceExtension('', 'a') - '' - >>> replaceExtension('foo.bar', '') - 'foo.' - ''' - sepFile = file.rpartition(".") - if sepFile[0] == "": - return file - else: - return sepFile[0] + "." + newExt - -def isMediaFile (file): - # ignore samples - if re.search('(^|[\W_])sample\d*[\W_]', file): - return False - - # ignore MAC OS's retarded "resource fork" files - if file.startswith('._'): - return False - - sepFile = file.rpartition(".") - if sepFile[2].lower() in mediaExtensions: - return True - else: - return False - -def sanitizeFileName (name): - ''' - >>> sanitizeFileName('a/b/c') - 'a-b-c' - >>> sanitizeFileName('abc') - 'abc' - >>> sanitizeFileName('a"b') - 'ab' - >>> sanitizeFileName('.a.b..') - 'a.b' - ''' - - # remove bad chars from the filename - name = re.sub(r'[\\/\*]', '-', name) - name = re.sub(r'[:"<>|?]', '', name) - - # remove leading/trailing periods and spaces - name = name.strip(' .') - - return name - - -def getURL (url, headers=[]): - """ - Returns a byte-string retrieved from the url provider. - """ - - opener = urllib2.build_opener() - opener.addheaders = [('User-Agent', USER_AGENT), ('Accept-Encoding', 'gzip,deflate')] - for cur_header in headers: - opener.addheaders.append(cur_header) - - try: - usock = opener.open(url) - url = usock.geturl() - encoding = usock.info().get("Content-Encoding") - - if encoding in ('gzip', 'x-gzip', 'deflate'): - content = usock.read() - if encoding == 'deflate': - data = StringIO.StringIO(zlib.decompress(content)) - else: - data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) - result = data.read() - - else: - result = usock.read() - - usock.close() - - except urllib2.HTTPError, e: - logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.WARNING) - return None - except urllib2.URLError, e: - logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.WARNING) - return None - except BadStatusLine: - logger.log(u"BadStatusLine error while loading URL " + url, logger.WARNING) - return None - except socket.timeout: - logger.log(u"Timed out while loading URL " + url, logger.WARNING) - return None - except ValueError: - logger.log(u"Unknown error while loading URL " + url, logger.WARNING) - return None - except Exception: - logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) - return None - - return result - -def findCertainShow (showList, tvdbid): - results = filter(lambda x: x.tvdbid == tvdbid, showList) - if len(results) == 0: - return None - elif len(results) > 1: - raise MultipleShowObjectsException() - else: - return results[0] - -def findCertainTVRageShow (showList, tvrid): - - if tvrid == 0: - return None - - results = filter(lambda x: x.tvrid == tvrid, showList) - - if len(results) == 0: - return None - elif len(results) > 1: - raise MultipleShowObjectsException() - else: - return results[0] - - -def makeDir (dir): - if not ek.ek(os.path.isdir, dir): - try: - ek.ek(os.makedirs, dir) - # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(dir) - except OSError: - return False - return True - -def makeShowNFO(showID, showDir): - - logger.log(u"Making NFO for show "+str(showID)+" in dir "+showDir, logger.DEBUG) - - if not makeDir(showDir): - logger.log(u"Unable to create show dir, can't make NFO", logger.ERROR) - return False - - showObj = findCertainShow(sickbeard.showList, showID) - if not showObj: - logger.log(u"This should never have happened, post a bug about this!", logger.ERROR) - raise Exception("BAD STUFF HAPPENED") - - tvdb_lang = showObj.lang - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) - - try: - myShow = t[int(showID)] - except tvdb_exceptions.tvdb_shownotfound: - logger.log(u"Unable to find show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - raise - - except tvdb_exceptions.tvdb_error: - logger.log(u"TVDB is down, can't use its data to add this show", logger.ERROR) - raise - - # check for title and id - try: - if myShow["seriesname"] == None or myShow["seriesname"] == "" or myShow["id"] == None or myShow["id"] == "": - logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - - return False - except tvdb_exceptions.tvdb_attributenotfound: - logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) - - return False - - tvNode = buildNFOXML(myShow) - # Make it purdy - indentXML( tvNode ) - nfo = etree.ElementTree( tvNode ) - - logger.log(u"Writing NFO to "+os.path.join(showDir, "tvshow.nfo"), logger.DEBUG) - nfo_filename = os.path.join(showDir, "tvshow.nfo").encode('utf-8') - nfo_fh = open(nfo_filename, 'w') - nfo.write( nfo_fh, encoding="utf-8" ) - - return True - -def buildNFOXML(myShow): - ''' - Build an etree.Element of the root node of an NFO file with the - data from `myShow`, a TVDB show object. - - >>> from collections import defaultdict - >>> from xml.etree.cElementTree import tostring - >>> show = defaultdict(lambda: None, _actors=[]) - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title /><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' - >>> show['seriesname'] = 'Peaches' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' - >>> show['contentrating'] = 'PG' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre /><premiered /><studio /></tvshow>' - >>> show['genre'] = 'Fruit|Edibles' - >>> tostring(buildNFOXML(show)) - '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre>Fruit / Edibles</genre><premiered /><studio /></tvshow>' - ''' - tvNode = etree.Element( "tvshow" ) - for ns in XML_NSMAP.keys(): - tvNode.set(ns, XML_NSMAP[ns]) - - title = etree.SubElement( tvNode, "title" ) - if myShow["seriesname"] != None: - title.text = myShow["seriesname"] - - rating = etree.SubElement( tvNode, "rating" ) - if myShow["rating"] != None: - rating.text = myShow["rating"] - - plot = etree.SubElement( tvNode, "plot" ) - if myShow["overview"] != None: - plot.text = myShow["overview"] - - episodeguide = etree.SubElement( tvNode, "episodeguide" ) - episodeguideurl = etree.SubElement( episodeguide, "url" ) - if myShow["id"] != None: - showurl = sickbeard.TVDB_BASE_URL + '/series/' + myShow["id"] + '/all/en.zip' - episodeguideurl.text = showurl - - mpaa = etree.SubElement( tvNode, "mpaa" ) - if myShow["contentrating"] != None: - mpaa.text = myShow["contentrating"] - - tvdbid = etree.SubElement( tvNode, "id" ) - if myShow["id"] != None: - tvdbid.text = myShow["id"] - - genre = etree.SubElement( tvNode, "genre" ) - if myShow["genre"] != None: - genre.text = " / ".join([x for x in myShow["genre"].split('|') if x != '']) - - premiered = etree.SubElement( tvNode, "premiered" ) - if myShow["firstaired"] != None: - premiered.text = myShow["firstaired"] - - studio = etree.SubElement( tvNode, "studio" ) - if myShow["network"] != None: - studio.text = myShow["network"] - - for actor in myShow['_actors']: - - cur_actor = etree.SubElement( tvNode, "actor" ) - - cur_actor_name = etree.SubElement( cur_actor, "name" ) - cur_actor_name.text = actor['name'] - cur_actor_role = etree.SubElement( cur_actor, "role" ) - cur_actor_role_text = actor['role'] - - if cur_actor_role_text != None: - cur_actor_role.text = cur_actor_role_text - - cur_actor_thumb = etree.SubElement( cur_actor, "thumb" ) - cur_actor_thumb_text = actor['image'] - - if cur_actor_thumb_text != None: - cur_actor_thumb.text = cur_actor_thumb_text - - return tvNode - - -def searchDBForShow(regShowName): - - showNames = [re.sub('[. -]', ' ', regShowName)] - - myDB = db.DBConnection() - - yearRegex = "([^()]+?)\s*(\()?(\d{4})(?(2)\))$" - - for showName in showNames: - - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE show_name LIKE ? OR tvr_name LIKE ?", [showName, showName]) - - if len(sqlResults) == 1: - return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) - - else: - - # if we didn't get exactly one result then try again with the year stripped off if possible - match = re.match(yearRegex, showName) - if match and match.group(1): - logger.log(u"Unable to match original name but trying to manually strip and specify show year", logger.DEBUG) - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE (show_name LIKE ? OR tvr_name LIKE ?) AND startyear = ?", [match.group(1)+'%', match.group(1)+'%', match.group(3)]) - - if len(sqlResults) == 0: - logger.log(u"Unable to match a record in the DB for "+showName, logger.DEBUG) - continue - elif len(sqlResults) > 1: - logger.log(u"Multiple results for "+showName+" in the DB, unable to match show name", logger.DEBUG) - continue - else: - return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) - - - return None - -def sizeof_fmt(num): - ''' - >>> sizeof_fmt(2) - '2.0 bytes' - >>> sizeof_fmt(1024) - '1.0 KB' - >>> sizeof_fmt(2048) - '2.0 KB' - >>> sizeof_fmt(2**20) - '1.0 MB' - >>> sizeof_fmt(1234567) - '1.2 MB' - ''' - for x in ['bytes','KB','MB','GB','TB']: - if num < 1024.0: - return "%3.1f %s" % (num, x) - num /= 1024.0 - -def listMediaFiles(dir): - - if not dir or not ek.ek(os.path.isdir, dir): - return [] - - files = [] - for curFile in ek.ek(os.listdir, dir): - fullCurFile = ek.ek(os.path.join, dir, curFile) - - # if it's a dir do it recursively - if ek.ek(os.path.isdir, fullCurFile) and not curFile.startswith('.') and not curFile == 'Extras': - files += listMediaFiles(fullCurFile) - - elif isMediaFile(curFile): - files.append(fullCurFile) - - return files - -def copyFile(srcFile, destFile): - ek.ek(shutil.copyfile, srcFile, destFile) - try: - ek.ek(shutil.copymode, srcFile, destFile) - except OSError: - pass - -def moveFile(srcFile, destFile): - try: - ek.ek(os.rename, srcFile, destFile) - fixSetGroupID(destFile) - except OSError: - copyFile(srcFile, destFile) - ek.ek(os.unlink, srcFile) - -def del_empty_dirs(s_dir): - b_empty = True - - for s_target in os.listdir(s_dir): - s_path = os.path.join(s_dir, s_target) - if os.path.isdir(s_path): - if not del_empty_dirs(s_path): - b_empty = False - else: - b_empty = False - - if b_empty: - logger.log(u"Deleting " + s_dir.decode('utf-8')+ ' because it is empty') - os.rmdir(s_dir) - -def make_dirs(path): - """ - Creates any folders that are missing and assigns them the permissions of their - parents - """ - - logger.log(u"Checking if the path " + path + " already exists", logger.DEBUG) - - if not ek.ek(os.path.isdir, path): - # Windows, create all missing folders - if os.name == 'nt' or os.name == 'ce': - try: - logger.log(u"Folder " + path + " didn't exist, creating it", logger.DEBUG) - ek.ek(os.makedirs, path) - except (OSError, IOError), e: - logger.log(u"Failed creating " + path + " : " + ex(e), logger.ERROR) - return False - - # not Windows, create all missing folders and set permissions - else: - sofar = '' - folder_list = path.split(os.path.sep) - - # look through each subfolder and make sure they all exist - for cur_folder in folder_list: - sofar += cur_folder + os.path.sep; - - # if it exists then just keep walking down the line - if ek.ek(os.path.isdir, sofar): - continue - - try: - logger.log(u"Folder " + sofar + " didn't exist, creating it", logger.DEBUG) - ek.ek(os.mkdir, sofar) - # use normpath to remove end separator, otherwise checks permissions against itself - chmodAsParent(ek.ek(os.path.normpath, sofar)) - # do the library update for synoindex - notifiers.synoindex_notifier.addFolder(sofar) - except (OSError, IOError), e: - logger.log(u"Failed creating " + sofar + " : " + ex(e), logger.ERROR) - return False - - return True - - -def rename_ep_file(cur_path, new_path): - """ - Creates all folders needed to move a file to its new location, renames it, then cleans up any folders - left that are now empty. - - cur_path: The absolute path to the file you want to move/rename - new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION - """ - - new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable - cur_file_name, cur_file_ext = os.path.splitext(cur_path) #@UnusedVariable - - if cur_file_ext[1:] in subtitleExtensions: - #Extract subtitle language from filename - sublang = os.path.splitext(cur_file_name)[1][1:] - - #Check if the language extracted from filename is a valid language - try: - language = subliminal.language.Language(sublang, strict=True) - cur_file_ext = '.'+sublang+cur_file_ext - except ValueError: - pass - - # put the extension on the incoming file - new_path += cur_file_ext - - make_dirs(os.path.dirname(new_path)) - - # move the file - try: - logger.log(u"Renaming file from " + cur_path + " to " + new_path) - ek.ek(os.rename, cur_path, new_path) - except (OSError, IOError), e: - logger.log(u"Failed renaming " + cur_path + " to " + new_path + ": " + ex(e), logger.ERROR) - return False - - # clean up any old folders that are empty - delete_empty_folders(ek.ek(os.path.dirname, cur_path)) - - return True - - -def delete_empty_folders(check_empty_dir, keep_dir=None): - """ - Walks backwards up the path and deletes any empty folders found. - - check_empty_dir: The path to clean (absolute path to a folder) - keep_dir: Clean until this path is reached - """ - - # treat check_empty_dir as empty when it only contains these items - ignore_items = [] - - logger.log(u"Trying to clean any empty folders under " + check_empty_dir) - - # as long as the folder exists and doesn't contain any files, delete it - while ek.ek(os.path.isdir, check_empty_dir) and check_empty_dir != keep_dir: - - check_files = ek.ek(os.listdir, check_empty_dir) - - if not check_files or (len(check_files) <= len(ignore_items) and all([check_file in ignore_items for check_file in check_files])): - # directory is empty or contains only ignore_items - try: - logger.log(u"Deleting empty folder: " + check_empty_dir) - # need shutil.rmtree when ignore_items is really implemented - ek.ek(os.rmdir, check_empty_dir) - # do the library update for synoindex - notifiers.synoindex_notifier.deleteFolder(check_empty_dir) - except (WindowsError, OSError), e: - logger.log(u"Unable to delete " + check_empty_dir + ": " + repr(e) + " / " + str(e), logger.WARNING) - break - check_empty_dir = ek.ek(os.path.dirname, check_empty_dir) - else: - break - - -def chmodAsParent(childPath): - if os.name == 'nt' or os.name == 'ce': - return - - parentPath = ek.ek(os.path.dirname, childPath) - - if not parentPath: - logger.log(u"No parent path provided in "+childPath+", unable to get permissions from it", logger.DEBUG) - return - - parentMode = stat.S_IMODE(os.stat(parentPath)[stat.ST_MODE]) - - childPathStat = ek.ek(os.stat, childPath) - childPath_mode = stat.S_IMODE(childPathStat[stat.ST_MODE]) - - if ek.ek(os.path.isfile, childPath): - childMode = fileBitFilter(parentMode) - else: - childMode = parentMode - - if childPath_mode == childMode: - return - - childPath_owner = childPathStat.st_uid - user_id = os.geteuid() - - if user_id !=0 and user_id != childPath_owner: - logger.log(u"Not running as root or owner of "+childPath+", not trying to set permissions", logger.DEBUG) - return - - try: - ek.ek(os.chmod, childPath, childMode) - logger.log(u"Setting permissions for %s to %o as parent directory has %o" % (childPath, childMode, parentMode), logger.DEBUG) - except OSError: - logger.log(u"Failed to set permission for %s to %o" % (childPath, childMode), logger.ERROR) - -def fileBitFilter(mode): - for bit in [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH, stat.S_ISUID, stat.S_ISGID]: - if mode & bit: - mode -= bit - - return mode - -def fixSetGroupID(childPath): - if os.name == 'nt' or os.name == 'ce': - return - - parentPath = ek.ek(os.path.dirname, childPath) - parentStat = os.stat(parentPath) - parentMode = stat.S_IMODE(parentStat[stat.ST_MODE]) - - if parentMode & stat.S_ISGID: - parentGID = parentStat[stat.ST_GID] - childStat = ek.ek(os.stat, childPath) - childGID = childStat[stat.ST_GID] - - if childGID == parentGID: - return - - childPath_owner = childStat.st_uid - user_id = os.geteuid() - - if user_id !=0 and user_id != childPath_owner: - logger.log(u"Not running as root or owner of "+childPath+", not trying to set the set-group-ID", logger.DEBUG) - return - - try: - ek.ek(os.chown, childPath, -1, parentGID) #@UndefinedVariable - only available on UNIX - logger.log(u"Respecting the set-group-ID bit on the parent directory for %s" % (childPath), logger.DEBUG) - except OSError: - logger.log(u"Failed to respect the set-group-ID bit on the parent directory for %s (setting group ID %i)" % (childPath, parentGID), logger.ERROR) - -def sanitizeSceneName (name, ezrss=False): - """ - Takes a show name and returns the "scenified" version of it. - - ezrss: If true the scenified version will follow EZRSS's cracksmoker rules as best as possible - - Returns: A string containing the scene version of the show name given. - """ - - if not ezrss: - bad_chars = u",:()'!?\u2019" - # ezrss leaves : and ! in their show names as far as I can tell - else: - bad_chars = u",()'?\u2019" - - # strip out any bad chars - for x in bad_chars: - name = name.replace(x, "") - - # tidy up stuff that doesn't belong in scene names - name = name.replace("- ", ".").replace(" ", ".").replace("&", "and").replace('/','.') - name = re.sub("\.\.*", ".", name) - - if name.endswith('.'): - name = name[:-1] - - return name - -def create_https_certificates(ssl_cert, ssl_key): - """ - Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' - """ - try: - from OpenSSL import crypto #@UnresolvedImport - from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial #@UnresolvedImport - except: - logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) - return False - - # Create the CA Certificate - cakey = createKeyPair(TYPE_RSA, 1024) - careq = createCertRequest(cakey, CN='Certificate Authority') - cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years - - cname = 'SickBeard' - pkey = createKeyPair(TYPE_RSA, 1024) - req = createCertRequest(pkey, CN=cname) - cert = createCertificate(req, (cacert, cakey), serial, (0, 60*60*24*365*10)) # ten years - - # Save the key and certificate to disk - try: - open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - except: - logger.log(u"Error creating SSL key and certificate", logger.ERROR) - return False - - return True - -if __name__ == '__main__': - import doctest - doctest.testmod() - -def getAllLanguages (): - """ - Returns all show languages where an episode is wanted or unaired - - Returns: A list of all language codes - """ - myDB = db.DBConnection() - - sqlLanguages = myDB.select("SELECT DISTINCT(t.audio_lang) FROM tv_shows t, tv_episodes e WHERE t.tvdb_id = e.showid AND (e.status = ? OR e.status = ?)", [common.UNAIRED,common.WANTED]) - - languages = map(lambda x: str(x["audio_lang"]), sqlLanguages) - - return languages - - -def get_xml_text(node): - text = "" - for child_node in node.childNodes: - if child_node.nodeType in (Node.CDATA_SECTION_NODE, Node.TEXT_NODE): - text += child_node.data - return text.strip() - -def backupVersionedFile(oldFile, version): - numTries = 0 - - newFile = oldFile + '.' + 'v'+str(version) - - while not ek.ek(os.path.isfile, newFile): - if not ek.ek(os.path.isfile, oldFile): - break - - try: - logger.log(u"Attempting to back up "+oldFile+" before migration...") - shutil.copy(oldFile, newFile) - logger.log(u"Done backup, proceeding with migration.") - break - except Exception, e: - logger.log(u"Error while trying to back up "+oldFile+": "+ex(e)) - numTries += 1 - time.sleep(1) - logger.log(u"Trying again.") - - if numTries >= 10: - logger.log(u"Unable to back up "+oldFile+", please do it manually.") - sys.exit(1) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import StringIO, zlib, gzip +import os +import stat +import urllib, urllib2 +import re, socket +import shutil +import traceback +import time, sys + +import hashlib + +from httplib import BadStatusLine + +from xml.dom.minidom import Node + +import sickbeard + +from sickbeard.exceptions import MultipleShowObjectsException, ex +from sickbeard import logger, classes, common +from sickbeard.common import USER_AGENT, mediaExtensions, XML_NSMAP, subtitleExtensions + +from sickbeard import db +from sickbeard import encodingKludge as ek +from sickbeard import notifiers + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + +import xml.etree.cElementTree as etree + +from lib import subliminal +#from sickbeard.subtitles import EXTENSIONS + +urllib._urlopener = classes.SickBeardURLopener() + +def indentXML(elem, level=0): + ''' + Does our pretty printing, makes Matt very happy + ''' + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indentXML(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + # Strip out the newlines from text + if elem.text: + elem.text = elem.text.replace('\n', ' ') + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +def replaceExtension(file, newExt): + ''' + >>> replaceExtension('foo.avi', 'mkv') + 'foo.mkv' + >>> replaceExtension('.vimrc', 'arglebargle') + '.vimrc' + >>> replaceExtension('a.b.c', 'd') + 'a.b.d' + >>> replaceExtension('', 'a') + '' + >>> replaceExtension('foo.bar', '') + 'foo.' + ''' + sepFile = file.rpartition(".") + if sepFile[0] == "": + return file + else: + return sepFile[0] + "." + newExt + +def isMediaFile (file): + # ignore samples + if re.search('(^|[\W_])sample\d*[\W_]', file): + return False + + # ignore MAC OS's retarded "resource fork" files + if file.startswith('._'): + return False + + sepFile = file.rpartition(".") + if sepFile[2].lower() in mediaExtensions: + return True + else: + return False + +def sanitizeFileName (name): + ''' + >>> sanitizeFileName('a/b/c') + 'a-b-c' + >>> sanitizeFileName('abc') + 'abc' + >>> sanitizeFileName('a"b') + 'ab' + >>> sanitizeFileName('.a.b..') + 'a.b' + ''' + + # remove bad chars from the filename + name = re.sub(r'[\\/\*]', '-', name) + name = re.sub(r'[:"<>|?]', '', name) + + # remove leading/trailing periods and spaces + name = name.strip(' .') + + return name + + +def getURL (url, headers=[]): + """ + Returns a byte-string retrieved from the url provider. + """ + + opener = urllib2.build_opener() + opener.addheaders = [('User-Agent', USER_AGENT), ('Accept-Encoding', 'gzip,deflate')] + for cur_header in headers: + opener.addheaders.append(cur_header) + + try: + usock = opener.open(url) + url = usock.geturl() + encoding = usock.info().get("Content-Encoding") + + if encoding in ('gzip', 'x-gzip', 'deflate'): + content = usock.read() + if encoding == 'deflate': + data = StringIO.StringIO(zlib.decompress(content)) + else: + data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) + result = data.read() + + else: + result = usock.read() + + usock.close() + + except urllib2.HTTPError, e: + logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.WARNING) + return None + except urllib2.URLError, e: + logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.WARNING) + return None + except BadStatusLine: + logger.log(u"BadStatusLine error while loading URL " + url, logger.WARNING) + return None + except socket.timeout: + logger.log(u"Timed out while loading URL " + url, logger.WARNING) + return None + except ValueError: + logger.log(u"Unknown error while loading URL " + url, logger.WARNING) + return None + except Exception: + logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) + return None + + return result + +def _remove_file_failed(file): + try: + os.remove(file) + except: + pass + +def download_file(url, filename): + try: + req = urllib2.urlopen(url) + CHUNK = 16 * 1024 + with open(filename, 'wb') as fp: + while True: + chunk = req.read(CHUNK) + if not chunk: break + fp.write(chunk) + fp.close() + req.close() + + except urllib2.HTTPError, e: + _remove_file_failed(filename) + logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.WARNING) + return False + except urllib2.URLError, e: + _remove_file_failed(filename) + logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.WARNING) + return False + except BadStatusLine: + _remove_file_failed(filename) + logger.log(u"BadStatusLine error while loading URL " + url, logger.WARNING) + return False + except socket.timeout: + _remove_file_failed(filename) + logger.log(u"Timed out while loading URL " + url, logger.WARNING) + return False + except ValueError: + _remove_file_failed(filename) + logger.log(u"Unknown error while loading URL " + url, logger.WARNING) + return False + except Exception: + _remove_file_failed(filename) + logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING) + return False + + return True + +def findCertainShow (showList, tvdbid): + results = filter(lambda x: x.tvdbid == tvdbid, showList) + if len(results) == 0: + return None + elif len(results) > 1: + raise MultipleShowObjectsException() + else: + return results[0] + +def findCertainTVRageShow (showList, tvrid): + + if tvrid == 0: + return None + + results = filter(lambda x: x.tvrid == tvrid, showList) + + if len(results) == 0: + return None + elif len(results) > 1: + raise MultipleShowObjectsException() + else: + return results[0] + + +def makeDir (dir): + if not ek.ek(os.path.isdir, dir): + try: + ek.ek(os.makedirs, dir) + # do the library update for synoindex + notifiers.synoindex_notifier.addFolder(dir) + except OSError: + return False + return True + +def makeShowNFO(showID, showDir): + + logger.log(u"Making NFO for show "+str(showID)+" in dir "+showDir, logger.DEBUG) + + if not makeDir(showDir): + logger.log(u"Unable to create show dir, can't make NFO", logger.ERROR) + return False + + showObj = findCertainShow(sickbeard.showList, showID) + if not showObj: + logger.log(u"This should never have happened, post a bug about this!", logger.ERROR) + raise Exception("BAD STUFF HAPPENED") + + tvdb_lang = showObj.lang + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) + + try: + myShow = t[int(showID)] + except tvdb_exceptions.tvdb_shownotfound: + logger.log(u"Unable to find show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + raise + + except tvdb_exceptions.tvdb_error: + logger.log(u"TVDB is down, can't use its data to add this show", logger.ERROR) + raise + + # check for title and id + try: + if myShow["seriesname"] == None or myShow["seriesname"] == "" or myShow["id"] == None or myShow["id"] == "": + logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + + return False + except tvdb_exceptions.tvdb_attributenotfound: + logger.log(u"Incomplete info for show with id " + str(showID) + " on tvdb, skipping it", logger.ERROR) + + return False + + tvNode = buildNFOXML(myShow) + # Make it purdy + indentXML( tvNode ) + nfo = etree.ElementTree( tvNode ) + + logger.log(u"Writing NFO to "+os.path.join(showDir, "tvshow.nfo"), logger.DEBUG) + nfo_filename = os.path.join(showDir, "tvshow.nfo").encode('utf-8') + nfo_fh = open(nfo_filename, 'w') + nfo.write( nfo_fh, encoding="utf-8" ) + + return True + +def buildNFOXML(myShow): + ''' + Build an etree.Element of the root node of an NFO file with the + data from `myShow`, a TVDB show object. + + >>> from collections import defaultdict + >>> from xml.etree.cElementTree import tostring + >>> show = defaultdict(lambda: None, _actors=[]) + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title /><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' + >>> show['seriesname'] = 'Peaches' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa /><id /><genre /><premiered /><studio /></tvshow>' + >>> show['contentrating'] = 'PG' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre /><premiered /><studio /></tvshow>' + >>> show['genre'] = 'Fruit|Edibles' + >>> tostring(buildNFOXML(show)) + '<tvshow xsd="http://www.w3.org/2001/XMLSchema" xsi="http://www.w3.org/2001/XMLSchema-instance"><title>Peaches</title><rating /><plot /><episodeguide><url /></episodeguide><mpaa>PG</mpaa><id /><genre>Fruit / Edibles</genre><premiered /><studio /></tvshow>' + ''' + tvNode = etree.Element( "tvshow" ) + for ns in XML_NSMAP.keys(): + tvNode.set(ns, XML_NSMAP[ns]) + + title = etree.SubElement( tvNode, "title" ) + if myShow["seriesname"] != None: + title.text = myShow["seriesname"] + + rating = etree.SubElement( tvNode, "rating" ) + if myShow["rating"] != None: + rating.text = myShow["rating"] + + plot = etree.SubElement( tvNode, "plot" ) + if myShow["overview"] != None: + plot.text = myShow["overview"] + + episodeguide = etree.SubElement( tvNode, "episodeguide" ) + episodeguideurl = etree.SubElement( episodeguide, "url" ) + if myShow["id"] != None: + showurl = sickbeard.TVDB_BASE_URL + '/series/' + myShow["id"] + '/all/en.zip' + episodeguideurl.text = showurl + + mpaa = etree.SubElement( tvNode, "mpaa" ) + if myShow["contentrating"] != None: + mpaa.text = myShow["contentrating"] + + tvdbid = etree.SubElement( tvNode, "id" ) + if myShow["id"] != None: + tvdbid.text = myShow["id"] + + genre = etree.SubElement( tvNode, "genre" ) + if myShow["genre"] != None: + genre.text = " / ".join([x for x in myShow["genre"].split('|') if x != '']) + + premiered = etree.SubElement( tvNode, "premiered" ) + if myShow["firstaired"] != None: + premiered.text = myShow["firstaired"] + + studio = etree.SubElement( tvNode, "studio" ) + if myShow["network"] != None: + studio.text = myShow["network"] + + for actor in myShow['_actors']: + + cur_actor = etree.SubElement( tvNode, "actor" ) + + cur_actor_name = etree.SubElement( cur_actor, "name" ) + cur_actor_name.text = actor['name'] + cur_actor_role = etree.SubElement( cur_actor, "role" ) + cur_actor_role_text = actor['role'] + + if cur_actor_role_text != None: + cur_actor_role.text = cur_actor_role_text + + cur_actor_thumb = etree.SubElement( cur_actor, "thumb" ) + cur_actor_thumb_text = actor['image'] + + if cur_actor_thumb_text != None: + cur_actor_thumb.text = cur_actor_thumb_text + + return tvNode + + +def searchDBForShow(regShowName): + + showNames = [re.sub('[. -]', ' ', regShowName)] + + myDB = db.DBConnection() + + yearRegex = "([^()]+?)\s*(\()?(\d{4})(?(2)\))$" + + for showName in showNames: + + sqlResults = myDB.select("SELECT * FROM tv_shows WHERE show_name LIKE ? OR tvr_name LIKE ?", [showName, showName]) + + if len(sqlResults) == 1: + return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) + + else: + + # if we didn't get exactly one result then try again with the year stripped off if possible + match = re.match(yearRegex, showName) + if match and match.group(1): + logger.log(u"Unable to match original name but trying to manually strip and specify show year", logger.DEBUG) + sqlResults = myDB.select("SELECT * FROM tv_shows WHERE (show_name LIKE ? OR tvr_name LIKE ?) AND startyear = ?", [match.group(1)+'%', match.group(1)+'%', match.group(3)]) + + if len(sqlResults) == 0: + logger.log(u"Unable to match a record in the DB for "+showName, logger.DEBUG) + continue + elif len(sqlResults) > 1: + logger.log(u"Multiple results for "+showName+" in the DB, unable to match show name", logger.DEBUG) + continue + else: + return (int(sqlResults[0]["tvdb_id"]), sqlResults[0]["show_name"]) + + + return None + +def sizeof_fmt(num): + ''' + >>> sizeof_fmt(2) + '2.0 bytes' + >>> sizeof_fmt(1024) + '1.0 KB' + >>> sizeof_fmt(2048) + '2.0 KB' + >>> sizeof_fmt(2**20) + '1.0 MB' + >>> sizeof_fmt(1234567) + '1.2 MB' + ''' + for x in ['bytes','KB','MB','GB','TB']: + if num < 1024.0: + return "%3.1f %s" % (num, x) + num /= 1024.0 + +def listMediaFiles(dir): + + if not dir or not ek.ek(os.path.isdir, dir): + return [] + + files = [] + for curFile in ek.ek(os.listdir, dir): + fullCurFile = ek.ek(os.path.join, dir, curFile) + + # if it's a dir do it recursively + if ek.ek(os.path.isdir, fullCurFile) and not curFile.startswith('.') and not curFile == 'Extras': + files += listMediaFiles(fullCurFile) + + elif isMediaFile(curFile): + files.append(fullCurFile) + + return files + +def copyFile(srcFile, destFile): + ek.ek(shutil.copyfile, srcFile, destFile) + try: + ek.ek(shutil.copymode, srcFile, destFile) + except OSError: + pass + +def moveFile(srcFile, destFile): + try: + ek.ek(os.rename, srcFile, destFile) + fixSetGroupID(destFile) + except OSError: + copyFile(srcFile, destFile) + ek.ek(os.unlink, srcFile) + +def del_empty_dirs(s_dir): + b_empty = True + + for s_target in os.listdir(s_dir): + s_path = os.path.join(s_dir, s_target) + if os.path.isdir(s_path): + if not del_empty_dirs(s_path): + b_empty = False + else: + b_empty = False + + if b_empty: + logger.log(u"Deleting " + s_dir.decode('utf-8')+ ' because it is empty') + os.rmdir(s_dir) + +def make_dirs(path): + """ + Creates any folders that are missing and assigns them the permissions of their + parents + """ + + logger.log(u"Checking if the path " + path + " already exists", logger.DEBUG) + + if not ek.ek(os.path.isdir, path): + # Windows, create all missing folders + if os.name == 'nt' or os.name == 'ce': + try: + logger.log(u"Folder " + path + " didn't exist, creating it", logger.DEBUG) + ek.ek(os.makedirs, path) + except (OSError, IOError), e: + logger.log(u"Failed creating " + path + " : " + ex(e), logger.ERROR) + return False + + # not Windows, create all missing folders and set permissions + else: + sofar = '' + folder_list = path.split(os.path.sep) + + # look through each subfolder and make sure they all exist + for cur_folder in folder_list: + sofar += cur_folder + os.path.sep; + + # if it exists then just keep walking down the line + if ek.ek(os.path.isdir, sofar): + continue + + try: + logger.log(u"Folder " + sofar + " didn't exist, creating it", logger.DEBUG) + ek.ek(os.mkdir, sofar) + # use normpath to remove end separator, otherwise checks permissions against itself + chmodAsParent(ek.ek(os.path.normpath, sofar)) + # do the library update for synoindex + notifiers.synoindex_notifier.addFolder(sofar) + except (OSError, IOError), e: + logger.log(u"Failed creating " + sofar + " : " + ex(e), logger.ERROR) + return False + + return True + + +def rename_ep_file(cur_path, new_path): + """ + Creates all folders needed to move a file to its new location, renames it, then cleans up any folders + left that are now empty. + + cur_path: The absolute path to the file you want to move/rename + new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION + """ + + new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable + cur_file_name, cur_file_ext = os.path.splitext(cur_path) #@UnusedVariable + + if cur_file_ext[1:] in subtitleExtensions: + #Extract subtitle language from filename + sublang = os.path.splitext(cur_file_name)[1][1:] + + #Check if the language extracted from filename is a valid language + try: + language = subliminal.language.Language(sublang, strict=True) + cur_file_ext = '.'+sublang+cur_file_ext + except ValueError: + pass + + # put the extension on the incoming file + new_path += cur_file_ext + + make_dirs(os.path.dirname(new_path)) + + # move the file + try: + logger.log(u"Renaming file from " + cur_path + " to " + new_path) + ek.ek(os.rename, cur_path, new_path) + except (OSError, IOError), e: + logger.log(u"Failed renaming " + cur_path + " to " + new_path + ": " + ex(e), logger.ERROR) + return False + + # clean up any old folders that are empty + delete_empty_folders(ek.ek(os.path.dirname, cur_path)) + + return True + + +def delete_empty_folders(check_empty_dir, keep_dir=None): + """ + Walks backwards up the path and deletes any empty folders found. + + check_empty_dir: The path to clean (absolute path to a folder) + keep_dir: Clean until this path is reached + """ + + # treat check_empty_dir as empty when it only contains these items + ignore_items = [] + + logger.log(u"Trying to clean any empty folders under " + check_empty_dir) + + # as long as the folder exists and doesn't contain any files, delete it + while ek.ek(os.path.isdir, check_empty_dir) and check_empty_dir != keep_dir: + + check_files = ek.ek(os.listdir, check_empty_dir) + + if not check_files or (len(check_files) <= len(ignore_items) and all([check_file in ignore_items for check_file in check_files])): + # directory is empty or contains only ignore_items + try: + logger.log(u"Deleting empty folder: " + check_empty_dir) + # need shutil.rmtree when ignore_items is really implemented + ek.ek(os.rmdir, check_empty_dir) + # do the library update for synoindex + notifiers.synoindex_notifier.deleteFolder(check_empty_dir) + except (WindowsError, OSError), e: + logger.log(u"Unable to delete " + check_empty_dir + ": " + repr(e) + " / " + str(e), logger.WARNING) + break + check_empty_dir = ek.ek(os.path.dirname, check_empty_dir) + else: + break + +def chmodAsParent(childPath): + if os.name == 'nt' or os.name == 'ce': + return + + parentPath = ek.ek(os.path.dirname, childPath) + + if not parentPath: + logger.log(u"No parent path provided in "+childPath+", unable to get permissions from it", logger.DEBUG) + return + + parentMode = stat.S_IMODE(os.stat(parentPath)[stat.ST_MODE]) + + childPathStat = ek.ek(os.stat, childPath) + childPath_mode = stat.S_IMODE(childPathStat[stat.ST_MODE]) + + if ek.ek(os.path.isfile, childPath): + childMode = fileBitFilter(parentMode) + else: + childMode = parentMode + + if childPath_mode == childMode: + return + + childPath_owner = childPathStat.st_uid + user_id = os.geteuid() + + if user_id !=0 and user_id != childPath_owner: + logger.log(u"Not running as root or owner of "+childPath+", not trying to set permissions", logger.DEBUG) + return + + try: + ek.ek(os.chmod, childPath, childMode) + logger.log(u"Setting permissions for %s to %o as parent directory has %o" % (childPath, childMode, parentMode), logger.DEBUG) + except OSError: + logger.log(u"Failed to set permission for %s to %o" % (childPath, childMode), logger.ERROR) + +def fileBitFilter(mode): + for bit in [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH, stat.S_ISUID, stat.S_ISGID]: + if mode & bit: + mode -= bit + + return mode + +def fixSetGroupID(childPath): + if os.name == 'nt' or os.name == 'ce': + return + + parentPath = ek.ek(os.path.dirname, childPath) + parentStat = os.stat(parentPath) + parentMode = stat.S_IMODE(parentStat[stat.ST_MODE]) + + if parentMode & stat.S_ISGID: + parentGID = parentStat[stat.ST_GID] + childStat = ek.ek(os.stat, childPath) + childGID = childStat[stat.ST_GID] + + if childGID == parentGID: + return + + childPath_owner = childStat.st_uid + user_id = os.geteuid() + + if user_id !=0 and user_id != childPath_owner: + logger.log(u"Not running as root or owner of "+childPath+", not trying to set the set-group-ID", logger.DEBUG) + return + + try: + ek.ek(os.chown, childPath, -1, parentGID) #@UndefinedVariable - only available on UNIX + logger.log(u"Respecting the set-group-ID bit on the parent directory for %s" % (childPath), logger.DEBUG) + except OSError: + logger.log(u"Failed to respect the set-group-ID bit on the parent directory for %s (setting group ID %i)" % (childPath, parentGID), logger.ERROR) + +def sanitizeSceneName (name, ezrss=False): + """ + Takes a show name and returns the "scenified" version of it. + + ezrss: If true the scenified version will follow EZRSS's cracksmoker rules as best as possible + + Returns: A string containing the scene version of the show name given. + """ + + if not ezrss: + bad_chars = u",:()'!?\u2019" + # ezrss leaves : and ! in their show names as far as I can tell + else: + bad_chars = u",()'?\u2019" + + # strip out any bad chars + for x in bad_chars: + name = name.replace(x, "") + + # tidy up stuff that doesn't belong in scene names + name = name.replace("- ", ".").replace(" ", ".").replace("&", "and").replace('/','.') + name = re.sub("\.\.*", ".", name) + + if name.endswith('.'): + name = name[:-1] + + return name + +def create_https_certificates(ssl_cert, ssl_key): + """ + Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' + """ + try: + from OpenSSL import crypto #@UnresolvedImport + from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial #@UnresolvedImport + except: + logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) + return False + + # Create the CA Certificate + cakey = createKeyPair(TYPE_RSA, 1024) + careq = createCertRequest(cakey, CN='Certificate Authority') + cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years + + cname = 'SickBeard' + pkey = createKeyPair(TYPE_RSA, 1024) + req = createCertRequest(pkey, CN=cname) + cert = createCertificate(req, (cacert, cakey), serial, (0, 60*60*24*365*10)) # ten years + + # Save the key and certificate to disk + try: + open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + except: + logger.log(u"Error creating SSL key and certificate", logger.ERROR) + return False + + return True + +if __name__ == '__main__': + import doctest + doctest.testmod() + +def getAllLanguages (): + """ + Returns all show languages where an episode is wanted or unaired + + Returns: A list of all language codes + """ + myDB = db.DBConnection() + + sqlLanguages = myDB.select("SELECT DISTINCT(t.audio_lang) FROM tv_shows t, tv_episodes e WHERE t.tvdb_id = e.showid AND (e.status = ? OR e.status = ?)", [common.UNAIRED,common.WANTED]) + + languages = map(lambda x: str(x["audio_lang"]), sqlLanguages) + + return languages + + +def get_xml_text(node): + text = "" + for child_node in node.childNodes: + if child_node.nodeType in (Node.CDATA_SECTION_NODE, Node.TEXT_NODE): + text += child_node.data + return text.strip() + +def backupVersionedFile(oldFile, version): + numTries = 0 + + newFile = oldFile + '.' + 'v'+str(version) + + while not ek.ek(os.path.isfile, newFile): + if not ek.ek(os.path.isfile, oldFile): + break + + try: + logger.log(u"Attempting to back up "+oldFile+" before migration...") + shutil.copy(oldFile, newFile) + logger.log(u"Done backup, proceeding with migration.") + break + except Exception, e: + logger.log(u"Error while trying to back up "+oldFile+": "+ex(e)) + numTries += 1 + time.sleep(1) + logger.log(u"Trying again.") + + if numTries >= 10: + logger.log(u"Unable to back up "+oldFile+", please do it manually.") + sys.exit(1) +# try to convert to int, if it fails the default will be returned +def tryInt(s, s_default = 0): + try: return int(s) + except: return s_default + +# generates a md5 hash of a file +def md5_for_file(filename, block_size=2**16): + try: + with open(filename,'rb') as f: + md5 = hashlib.md5() + while True: + data = f.read(block_size) + if not data: + break + md5.update(data) + f.close() + return md5.hexdigest() + except Exception: + return None + diff --git a/sickbeard/image_cache.py b/sickbeard/image_cache.py index fecc41de4f3e87ac52be84632fe44db43605b167..c413b905440c1fce1a082d48487350dc93c04679 100644 --- a/sickbeard/image_cache.py +++ b/sickbeard/image_cache.py @@ -39,6 +39,12 @@ class ImageCache: """ return ek.ek(os.path.abspath, ek.ek(os.path.join, sickbeard.CACHE_DIR, 'images')) + def _thumbnails_dir(self): + """ + Builds up the full path to the thumbnails image cache directory + """ + return ek.ek(os.path.abspath, ek.ek(os.path.join, self._cache_dir(), 'thumbnails')) + def poster_path(self, tvdb_id): """ Builds up the path to a poster cache for a given tvdb id @@ -49,7 +55,7 @@ class ImageCache: """ poster_file_name = str(tvdb_id) + '.poster.jpg' return ek.ek(os.path.join, self._cache_dir(), poster_file_name) - + def banner_path(self, tvdb_id): """ Builds up the path to a banner cache for a given tvdb id @@ -61,6 +67,28 @@ class ImageCache: banner_file_name = str(tvdb_id) + '.banner.jpg' return ek.ek(os.path.join, self._cache_dir(), banner_file_name) + def poster_thumb_path(self, tvdb_id): + """ + Builds up the path to a poster cache for a given tvdb id + + returns: a full path to the cached poster file for the given tvdb id + + tvdb_id: ID of the show to use in the file name + """ + posterthumb_file_name = str(tvdb_id) + '.poster.jpg' + return ek.ek(os.path.join, self._thumbnails_dir(), posterthumb_file_name) + + def banner_thumb_path(self, tvdb_id): + """ + Builds up the path to a poster cache for a given tvdb id + + returns: a full path to the cached poster file for the given tvdb id + + tvdb_id: ID of the show to use in the file name + """ + bannerthumb_file_name = str(tvdb_id) + '.banner.jpg' + return ek.ek(os.path.join, self._thumbnails_dir(), bannerthumb_file_name) + def has_poster(self, tvdb_id): """ Returns true if a cached poster exists for the given tvdb id @@ -77,8 +105,27 @@ class ImageCache: logger.log(u"Checking if file "+str(banner_path)+" exists", logger.DEBUG) return ek.ek(os.path.isfile, banner_path) + def has_poster_thumbnail(self, tvdb_id): + """ + Returns true if a cached poster thumbnail exists for the given tvdb id + """ + poster_thumb_path = self.poster_thumb_path(tvdb_id) + logger.log(u"Checking if file "+str(poster_thumb_path)+" exists", logger.DEBUG) + return ek.ek(os.path.isfile, poster_thumb_path) + + def has_banner_thumbnail(self, tvdb_id): + """ + Returns true if a cached banner exists for the given tvdb id + """ + banner_thumb_path = self.banner_thumb_path(tvdb_id) + logger.log(u"Checking if file "+str(banner_thumb_path)+" exists", logger.DEBUG) + return ek.ek(os.path.isfile, banner_thumb_path) + + BANNER = 1 POSTER = 2 + BANNER_THUMB = 3 + POSTER_THUMB = 4 def which_type(self, path): """ @@ -141,6 +188,10 @@ class ImageCache: logger.log(u"Image cache dir didn't exist, creating it at "+str(self._cache_dir())) ek.ek(os.makedirs, self._cache_dir()) + if not ek.ek(os.path.isdir, self._thumbnails_dir()): + logger.log(u"Thumbnails cache dir didn't exist, creating it at "+str(self._thumbnails_dir())) + ek.ek(os.makedirs, self._thumbnails_dir()) + logger.log(u"Copying from "+image_path+" to "+dest_path) helpers.copyFile(image_path, dest_path) @@ -163,6 +214,12 @@ class ImageCache: elif img_type == self.BANNER: img_type_name = 'banner' dest_path = self.banner_path(show_obj.tvdbid) + elif img_type == self.POSTER_THUMB: + img_type_name = 'poster_thumb' + dest_path = self.poster_thumb_path(show_obj.tvdbid) + elif img_type == self.BANNER_THUMB: + img_type_name = 'banner_thumb' + dest_path = self.banner_thumb_path(show_obj.tvdbid) else: logger.log(u"Invalid cache image type: "+str(img_type), logger.ERROR) return False @@ -188,35 +245,37 @@ class ImageCache: # check if the images are already cached or not need_images = {self.POSTER: not self.has_poster(show_obj.tvdbid), self.BANNER: not self.has_banner(show_obj.tvdbid), - } + self.POSTER_THUMB: not self.has_poster_thumbnail(show_obj.tvdbid), + self.BANNER_THUMB: not self.has_banner_thumbnail(show_obj.tvdbid)} - if not need_images[self.POSTER] and not need_images[self.BANNER]: + if not need_images[self.POSTER] and not need_images[self.BANNER] and not need_images[self.POSTER_THUMB] and not need_images[self.BANNER_THUMB]: logger.log(u"No new cache images needed, not retrieving new ones") return - # check the show dir for images and use them - try: - for cur_provider in sickbeard.metadata_provider_dict.values(): - logger.log(u"Checking if we can use the show image from the "+cur_provider.name+" metadata", logger.DEBUG) - if ek.ek(os.path.isfile, cur_provider.get_poster_path(show_obj)): - cur_file_name = os.path.abspath(cur_provider.get_poster_path(show_obj)) - cur_file_type = self.which_type(cur_file_name) - - if cur_file_type == None: - logger.log(u"Unable to retrieve image type, not using the image from "+str(cur_file_name), logger.WARNING) - continue - - logger.log(u"Checking if image "+cur_file_name+" (type "+str(cur_file_type)+" needs metadata: "+str(need_images[cur_file_type]), logger.DEBUG) - - if cur_file_type in need_images and need_images[cur_file_type]: - logger.log(u"Found an image in the show dir that doesn't exist in the cache, caching it: "+cur_file_name+", type "+str(cur_file_type), logger.DEBUG) - self._cache_image_from_file(cur_file_name, cur_file_type, show_obj.tvdbid) - need_images[cur_file_type] = False - except exceptions.ShowDirNotFoundException: - logger.log(u"Unable to search for images in show dir because it doesn't exist", logger.WARNING) + # check the show dir for poster or banner images and use them + if need_images[self.POSTER] or need_images[self.BANNER]: + try: + for cur_provider in sickbeard.metadata_provider_dict.values(): + logger.log(u"Checking if we can use the show image from the "+cur_provider.name+" metadata", logger.DEBUG) + if ek.ek(os.path.isfile, cur_provider.get_poster_path(show_obj)): + cur_file_name = os.path.abspath(cur_provider.get_poster_path(show_obj)) + cur_file_type = self.which_type(cur_file_name) + + if cur_file_type == None: + logger.log(u"Unable to retrieve image type, not using the image from "+str(cur_file_name), logger.WARNING) + continue + + logger.log(u"Checking if image "+cur_file_name+" (type "+str(cur_file_type)+" needs metadata: "+str(need_images[cur_file_type]), logger.DEBUG) + + if cur_file_type in need_images and need_images[cur_file_type]: + logger.log(u"Found an image in the show dir that doesn't exist in the cache, caching it: "+cur_file_name+", type "+str(cur_file_type), logger.DEBUG) + self._cache_image_from_file(cur_file_name, cur_file_type, show_obj.tvdbid) + need_images[cur_file_type] = False + except exceptions.ShowDirNotFoundException: + logger.log(u"Unable to search for images in show dir because it doesn't exist", logger.WARNING) # download from TVDB for missing ones - for cur_image_type in [self.POSTER, self.BANNER]: + for cur_image_type in [self.POSTER, self.BANNER, self.POSTER_THUMB, self.BANNER_THUMB]: logger.log(u"Seeing if we still need an image of type "+str(cur_image_type)+": "+str(need_images[cur_image_type]), logger.DEBUG) if cur_image_type in need_images and need_images[cur_image_type]: self._cache_image_from_tvdb(show_obj, cur_image_type) diff --git a/sickbeard/logger.py b/sickbeard/logger.py index cb776d2bad56c8d7835fb63566cac07b2fbbc7de..473b69a750026a3258738c11d9f791d166c4f77a 100644 --- a/sickbeard/logger.py +++ b/sickbeard/logger.py @@ -38,11 +38,13 @@ ERROR = logging.ERROR WARNING = logging.WARNING MESSAGE = logging.INFO DEBUG = logging.DEBUG +DB = 5 reverseNames = {u'ERROR': ERROR, u'WARNING': WARNING, u'INFO': MESSAGE, - u'DEBUG': DEBUG} + u'DEBUG': DEBUG, + u'DB' : DB} class SBRotatingLogHandler(object): @@ -62,10 +64,13 @@ class SBRotatingLogHandler(object): self.log_file = os.path.join(sickbeard.LOG_DIR, self.log_file) self.cur_handler = self._config_handler() - + + logging.addLevelName(5,'DB') + logging.getLogger('sickbeard').addHandler(self.cur_handler) logging.getLogger('subliminal').addHandler(self.cur_handler) - + logging.getLogger('imdbpy').addHandler(self.cur_handler) + # define a Handler which writes INFO messages or higher to the sys.stderr if consoleLogging: console = logging.StreamHandler() @@ -73,23 +78,34 @@ class SBRotatingLogHandler(object): console.setLevel(logging.INFO) # set a format which is simpler for console use - console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S')) + console.setFormatter(DispatchingFormatter({'sickbeard' : logging.Formatter('%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S'), + 'subliminal' : logging.Formatter('%(asctime)s %(levelname)s::SUBLIMINAL :: %(message)s', '%H:%M:%S'), + 'imdbpy' : logging.Formatter('%(asctime)s %(levelname)s::IMDBPY :: %(message)s', '%H:%M:%S') + }, + logging.Formatter('%(message)s'),)) # add the handler to the root logger logging.getLogger('sickbeard').addHandler(console) logging.getLogger('subliminal').addHandler(console) - - logging.getLogger('sickbeard').setLevel(logging.DEBUG) - logging.getLogger('subliminal').setLevel(logging.ERROR) - + logging.getLogger('imdbpy').addHandler(console) + + logging.getLogger('sickbeard').setLevel(DB) + logging.getLogger('subliminal').setLevel(logging.WARNING) + logging.getLogger('imdbpy').setLevel(logging.WARNING) + def _config_handler(self): """ Configure a file handler to log at file_name and return it. """ file_handler = logging.FileHandler(self.log_file) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%b-%d %H:%M:%S')) + file_handler.setLevel(DB) + file_handler.setFormatter(DispatchingFormatter({'sickbeard' : logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%b-%d %H:%M:%S'), + 'subliminal' : logging.Formatter('%(asctime)s %(levelname)-8s SUBLIMINAL :: %(message)s', '%b-%d %H:%M:%S'), + 'imdbpy' : logging.Formatter('%(asctime)s %(levelname)-8s IMDBPY :: %(message)s', '%b-%d %H:%M:%S') + }, + logging.Formatter('%(message)s'),)) + return file_handler def _log_file_name(self, i): @@ -161,6 +177,7 @@ class SBRotatingLogHandler(object): out_line = message.encode('utf-8') sb_logger = logging.getLogger('sickbeard') + setattr(sb_logger, 'db', lambda *args: sb_logger.log(DB, *args)) try: if logLevel == DEBUG: @@ -171,14 +188,29 @@ class SBRotatingLogHandler(object): sb_logger.warning(out_line) elif logLevel == ERROR: sb_logger.error(out_line) - + # add errors to the UI logger classes.ErrorViewer.add(classes.UIError(message)) + elif logLevel == DB: + sb_logger.db(out_line) + else: sb_logger.log(logLevel, out_line) except ValueError: pass + +class DispatchingFormatter: + + def __init__(self, formatters, default_formatter): + self._formatters = formatters + self._default_formatter = default_formatter + + def format(self, record): + formatter = self._formatters.get(record.name, self._default_formatter) + return formatter.format(record) + + sb_log_instance = SBRotatingLogHandler('sickbeard.log', NUM_LOGS, LOG_SIZE) def log(toLog, logLevel=MESSAGE): diff --git a/sickbeard/metadata/generic.py b/sickbeard/metadata/generic.py index b303aca8de098ef3ba71618d5a7d7c5bbb806fb0..5b476e55a896727887ea4ce02ea3f32ffa6ebf94 100644 --- a/sickbeard/metadata/generic.py +++ b/sickbeard/metadata/generic.py @@ -1,639 +1,648 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import os.path - -import xml.etree.cElementTree as etree - -import re - -import sickbeard - -from sickbeard import exceptions, helpers -from sickbeard.metadata import helpers as metadata_helpers -from sickbeard import logger -from sickbeard import encodingKludge as ek -from sickbeard.exceptions import ex - -from lib.tvdb_api import tvdb_api, tvdb_exceptions - - -class GenericMetadata(): - """ - Base class for all metadata providers. Default behavior is meant to mostly - follow XBMC metadata standards. Has support for: - - - show poster - - show fanart - - show metadata file - - episode thumbnail - - episode metadata file - - season thumbnails - """ - - def __init__(self, - show_metadata=False, - episode_metadata=False, - poster=False, - fanart=False, - episode_thumbnails=False, - season_thumbnails=False): - - self._show_file_name = "tvshow.nfo" - self._ep_nfo_extension = "nfo" - - self.poster_name = "folder.jpg" - self.fanart_name = "fanart.jpg" - - self.generate_show_metadata = True - self.generate_ep_metadata = True - - self.name = 'Generic' - - self.show_metadata = show_metadata - self.episode_metadata = episode_metadata - self.poster = poster - self.fanart = fanart - self.episode_thumbnails = episode_thumbnails - self.season_thumbnails = season_thumbnails - - def get_config(self): - config_list = [self.show_metadata, self.episode_metadata, self.poster, self.fanart, self.episode_thumbnails, self.season_thumbnails] - return '|'.join([str(int(x)) for x in config_list]) - - def get_id(self): - return GenericMetadata.makeID(self.name) - - @staticmethod - def makeID(name): - return re.sub("[^\w\d_]", "_", name).lower() - - def set_config(self, string): - config_list = [bool(int(x)) for x in string.split('|')] - self.show_metadata = config_list[0] - self.episode_metadata = config_list[1] - self.poster = config_list[2] - self.fanart = config_list[3] - self.episode_thumbnails = config_list[4] - self.season_thumbnails = config_list[5] - - def _has_show_metadata(self, show_obj): - result = ek.ek(os.path.isfile, self.get_show_file_path(show_obj)) - logger.log("Checking if "+self.get_show_file_path(show_obj)+" exists: "+str(result), logger.DEBUG) - return result - - def _has_episode_metadata(self, ep_obj): - result = ek.ek(os.path.isfile, self.get_episode_file_path(ep_obj)) - logger.log("Checking if "+self.get_episode_file_path(ep_obj)+" exists: "+str(result), logger.DEBUG) - return result - - def _has_poster(self, show_obj): - result = ek.ek(os.path.isfile, self.get_poster_path(show_obj)) - logger.log("Checking if "+self.get_poster_path(show_obj)+" exists: "+str(result), logger.DEBUG) - return result - - def _has_fanart(self, show_obj): - result = ek.ek(os.path.isfile, self.get_fanart_path(show_obj)) - logger.log("Checking if "+self.get_fanart_path(show_obj)+" exists: "+str(result), logger.DEBUG) - return result - - def _has_episode_thumb(self, ep_obj): - location = self.get_episode_thumb_path(ep_obj) - result = location != None and ek.ek(os.path.isfile, location) - if location: - logger.log("Checking if "+location+" exists: "+str(result), logger.DEBUG) - return result - - def _has_season_thumb(self, show_obj, season): - location = self.get_season_thumb_path(show_obj, season) - result = location != None and ek.ek(os.path.isfile, location) - if location: - logger.log("Checking if "+location+" exists: "+str(result), logger.DEBUG) - return result - - def get_show_file_path(self, show_obj): - return ek.ek(os.path.join, show_obj.location, self._show_file_name) - - def get_episode_file_path(self, ep_obj): - return helpers.replaceExtension(ep_obj.location, self._ep_nfo_extension) - - def get_poster_path(self, show_obj): - return ek.ek(os.path.join, show_obj.location, self.poster_name) - - def get_fanart_path(self, show_obj): - return ek.ek(os.path.join, show_obj.location, self.fanart_name) - - def get_episode_thumb_path(self, ep_obj): - """ - Returns the path where the episode thumbnail should be stored. Defaults to - the same path as the episode file but with a .tbn extension. - - ep_obj: a TVEpisode instance for which to create the thumbnail - """ - if ek.ek(os.path.isfile, ep_obj.location): - tbn_filename = helpers.replaceExtension(ep_obj.location, 'tbn') - else: - return None - - return tbn_filename - - def get_season_thumb_path(self, show_obj, season): - """ - Returns the full path to the file for a given season thumb. - - show_obj: a TVShow instance for which to generate the path - season: a season number to be used for the path. Note that sesaon 0 - means specials. - """ - - # Our specials thumbnail is, well, special - if season == 0: - season_thumb_file_path = 'season-specials' - else: - season_thumb_file_path = 'season' + str(season).zfill(2) - - return ek.ek(os.path.join, show_obj.location, season_thumb_file_path+'.tbn') - - def _show_data(self, show_obj): - """ - This should be overridden by the implementing class. It should - provide the content of the show metadata file. - """ - return None - - def _ep_data(self, ep_obj): - """ - This should be overridden by the implementing class. It should - provide the content of the episode metadata file. - """ - return None - - def create_show_metadata(self, show_obj): - if self.show_metadata and show_obj and not self._has_show_metadata(show_obj): - logger.log("Metadata provider "+self.name+" creating show metadata for "+show_obj.name, logger.DEBUG) - return self.write_show_file(show_obj) - return False - - def create_episode_metadata(self, ep_obj): - if self.episode_metadata and ep_obj and not self._has_episode_metadata(ep_obj): - logger.log("Metadata provider "+self.name+" creating episode metadata for "+ep_obj.prettyName(), logger.DEBUG) - return self.write_ep_file(ep_obj) - return False - - def create_poster(self, show_obj): - if self.poster and show_obj and not self._has_poster(show_obj): - logger.log("Metadata provider "+self.name+" creating poster for "+show_obj.name, logger.DEBUG) - return self.save_poster(show_obj) - return False - - def create_fanart(self, show_obj): - if self.fanart and show_obj and not self._has_fanart(show_obj): - logger.log("Metadata provider "+self.name+" creating fanart for "+show_obj.name, logger.DEBUG) - return self.save_fanart(show_obj) - return False - - def create_episode_thumb(self, ep_obj): - if self.episode_thumbnails and ep_obj and not self._has_episode_thumb(ep_obj): - logger.log("Metadata provider "+self.name+" creating show metadata for "+ep_obj.prettyName(), logger.DEBUG) - return self.save_thumbnail(ep_obj) - return False - - def create_season_thumbs(self, show_obj): - if self.season_thumbnails and show_obj: - logger.log("Metadata provider "+self.name+" creating season thumbnails for "+show_obj.name, logger.DEBUG) - return self.save_season_thumbs(show_obj) - return False - - def _get_episode_thumb_url(self, ep_obj): - """ - Returns the URL to use for downloading an episode's thumbnail. Uses - theTVDB.com data. - - ep_obj: a TVEpisode object for which to grab the thumb URL - """ - all_eps = [ep_obj] + ep_obj.relatedEps - - tvdb_lang = ep_obj.show.lang - - # get a TVDB object - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) - tvdb_show_obj = t[ep_obj.show.tvdbid] - except tvdb_exceptions.tvdb_shownotfound, e: - raise exceptions.ShowNotFoundException(e.message) - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+ex(e), logger.ERROR) - return None - - # try all included episodes in case some have thumbs and others don't - for cur_ep in all_eps: - try: - myEp = tvdb_show_obj[cur_ep.season][cur_ep.episode] - except (tvdb_exceptions.tvdb_episodenotfound, tvdb_exceptions.tvdb_seasonnotfound): - logger.log(u"Unable to find episode " + str(cur_ep.season) + "x" + str(cur_ep.episode) + " on tvdb... has it been removed? Should I delete from db?") - continue - - thumb_url = myEp["filename"] - - if thumb_url: - return thumb_url - - return None - - def write_show_file(self, show_obj): - """ - Generates and writes show_obj's metadata under the given path to the - filename given by get_show_file_path() - - show_obj: TVShow object for which to create the metadata - - path: An absolute or relative path where we should put the file. Note that - the file name will be the default show_file_name. - - Note that this method expects that _show_data will return an ElementTree - object. If your _show_data returns data in another format you'll need to - override this method. - """ - - data = self._show_data(show_obj) - - if not data: - return False - - nfo_file_path = self.get_show_file_path(show_obj) - nfo_file_dir = ek.ek(os.path.dirname, nfo_file_path) - - try: - if not ek.ek(os.path.isdir, nfo_file_dir): - logger.log("Metadata dir didn't exist, creating it at "+nfo_file_dir, logger.DEBUG) - ek.ek(os.makedirs, nfo_file_dir) - helpers.chmodAsParent(nfo_file_dir) - - logger.log(u"Writing show nfo file to "+nfo_file_path) - - nfo_file = ek.ek(open, nfo_file_path, 'w') - - data.write(nfo_file, encoding="utf-8") - nfo_file.close() - helpers.chmodAsParent(nfo_file_path) - except IOError, e: - logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) - return False - - return True - - def write_ep_file(self, ep_obj): - """ - Generates and writes ep_obj's metadata under the given path with the - given filename root. Uses the episode's name with the extension in - _ep_nfo_extension. - - ep_obj: TVEpisode object for which to create the metadata - - file_name_path: The file name to use for this metadata. Note that the extension - will be automatically added based on _ep_nfo_extension. This should - include an absolute path. - - Note that this method expects that _ep_data will return an ElementTree - object. If your _ep_data returns data in another format you'll need to - override this method. - """ - - data = self._ep_data(ep_obj) - - if not data: - return False - - nfo_file_path = self.get_episode_file_path(ep_obj) - nfo_file_dir = ek.ek(os.path.dirname, nfo_file_path) - - try: - if not ek.ek(os.path.isdir, nfo_file_dir): - logger.log("Metadata dir didn't exist, creating it at "+nfo_file_dir, logger.DEBUG) - ek.ek(os.makedirs, nfo_file_dir) - helpers.chmodAsParent(nfo_file_dir) - - logger.log(u"Writing episode nfo file to "+nfo_file_path) - - nfo_file = ek.ek(open, nfo_file_path, 'w') - - data.write(nfo_file, encoding="utf-8") - nfo_file.close() - helpers.chmodAsParent(nfo_file_path) - except IOError, e: - logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) - return False - - return True - - def save_thumbnail(self, ep_obj): - """ - Retrieves a thumbnail and saves it to the correct spot. This method should not need to - be overridden by implementing classes, changing get_episode_thumb_path and - _get_episode_thumb_url should suffice. - - ep_obj: a TVEpisode object for which to generate a thumbnail - """ - - file_path = self.get_episode_thumb_path(ep_obj) - - if not file_path: - logger.log(u"Unable to find a file path to use for this thumbnail, not generating it", logger.DEBUG) - return False - - thumb_url = self._get_episode_thumb_url(ep_obj) - - # if we can't find one then give up - if not thumb_url: - logger.log("No thumb is available for this episode, not creating a thumb", logger.DEBUG) - return False - - thumb_data = metadata_helpers.getShowImage(thumb_url) - - result = self._write_image(thumb_data, file_path) - - if not result: - return False - - for cur_ep in [ep_obj] + ep_obj.relatedEps: - cur_ep.hastbn = True - - return True - - def save_fanart(self, show_obj, which=None): - """ - Downloads a fanart image and saves it to the filename specified by fanart_name - inside the show's root folder. - - show_obj: a TVShow object for which to download fanart - """ - - # use the default fanart name - fanart_path = self.get_fanart_path(show_obj) - - fanart_data = self._retrieve_show_image('fanart', show_obj, which) - - if not fanart_data: - logger.log(u"No fanart image was retrieved, unable to write fanart", logger.DEBUG) - return False - - return self._write_image(fanart_data, fanart_path) - - - def save_poster(self, show_obj, which=None): - """ - Downloads a poster image and saves it to the filename specified by poster_name - inside the show's root folder. - - show_obj: a TVShow object for which to download a poster - """ - - # use the default poster name - poster_path = self.get_poster_path(show_obj) - - if sickbeard.USE_BANNER: - img_type = 'banner' - else: - img_type = 'poster' - - poster_data = self._retrieve_show_image(img_type, show_obj, which) - - if not poster_data: - logger.log(u"No show folder image was retrieved, unable to write poster", logger.DEBUG) - return False - - return self._write_image(poster_data, poster_path) - - - def save_season_thumbs(self, show_obj): - """ - Saves all season thumbnails to disk for the given show. - - show_obj: a TVShow object for which to save the season thumbs - - Cycles through all seasons and saves the season thumbs if possible. This - method should not need to be overridden by implementing classes, changing - _season_thumb_dict and get_season_thumb_path should be good enough. - """ - - season_dict = self._season_thumb_dict(show_obj) - - # Returns a nested dictionary of season art with the season - # number as primary key. It's really overkill but gives the option - # to present to user via ui to pick down the road. - for cur_season in season_dict: - - cur_season_art = season_dict[cur_season] - - if len(cur_season_art) == 0: - continue - - # Just grab whatever's there for now - art_id, season_url = cur_season_art.popitem() #@UnusedVariable - - season_thumb_file_path = self.get_season_thumb_path(show_obj, cur_season) - - if not season_thumb_file_path: - logger.log(u"Path for season "+str(cur_season)+" came back blank, skipping this season", logger.DEBUG) - continue - - seasonData = metadata_helpers.getShowImage(season_url) - - if not seasonData: - logger.log(u"No season thumb data available, skipping this season", logger.DEBUG) - continue - - self._write_image(seasonData, season_thumb_file_path) - - return True - - def _write_image(self, image_data, image_path): - """ - Saves the data in image_data to the location image_path. Returns True/False - to represent success or failure. - - image_data: binary image data to write to file - image_path: file location to save the image to - """ - - # don't bother overwriting it - if ek.ek(os.path.isfile, image_path): - logger.log(u"Image already exists, not downloading", logger.DEBUG) - return False - - if not image_data: - logger.log(u"Unable to retrieve image, skipping", logger.WARNING) - return False - - image_dir = ek.ek(os.path.dirname, image_path) - - try: - if not ek.ek(os.path.isdir, image_dir): - logger.log("Metadata dir didn't exist, creating it at "+image_dir, logger.DEBUG) - ek.ek(os.makedirs, image_dir) - helpers.chmodAsParent(image_dir) - - outFile = ek.ek(open, image_path, 'wb') - outFile.write(image_data) - outFile.close() - helpers.chmodAsParent(image_path) - except IOError, e: - logger.log(u"Unable to write image to "+image_path+" - are you sure the show folder is writable? "+ex(e), logger.ERROR) - return False - - return True - - def _retrieve_show_image(self, image_type, show_obj, which=None): - """ - Gets an image URL from theTVDB.com, downloads it and returns the data. - - image_type: type of image to retrieve (currently supported: poster, fanart) - show_obj: a TVShow object to use when searching for the image - which: optional, a specific numbered poster to look for - - Returns: the binary image data if available, or else None - """ - - tvdb_lang = show_obj.lang - - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) - tvdb_show_obj = t[show_obj.tvdbid] - except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) - return None - - if image_type not in ('fanart', 'poster', 'banner'): - logger.log(u"Invalid image type "+str(image_type)+", couldn't find it in the TVDB object", logger.ERROR) - return None - - image_url = tvdb_show_obj[image_type] - - image_data = metadata_helpers.getShowImage(image_url, which) - - return image_data - - def _season_thumb_dict(self, show_obj): - """ - Should return a dict like: - - result = {<season number>: - {1: '<url 1>', 2: <url 2>, ...},} - """ - - # This holds our resulting dictionary of season art - result = {} - - tvdb_lang = show_obj.lang - - try: - # There's gotta be a better way of doing this but we don't wanna - # change the language value elsewhere - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - - if tvdb_lang and not tvdb_lang == 'en': - ltvdb_api_parms['language'] = tvdb_lang - - t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) - tvdb_show_obj = t[show_obj.tvdbid] - except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) - return result - - # How many seasons? - num_seasons = len(tvdb_show_obj) - - # if we have no season banners then just finish - if 'season' not in tvdb_show_obj['_banners'] or 'season' not in tvdb_show_obj['_banners']['season']: - return result - - # Give us just the normal poster-style season graphics - seasonsArtObj = tvdb_show_obj['_banners']['season']['season'] - - # Returns a nested dictionary of season art with the season - # number as primary key. It's really overkill but gives the option - # to present to user via ui to pick down the road. - for cur_season in range(num_seasons): - - result[cur_season] = {} - - # find the correct season in the tvdb object and just copy the dict into our result dict - for seasonArtID in seasonsArtObj.keys(): - if int(seasonsArtObj[seasonArtID]['season']) == cur_season and seasonsArtObj[seasonArtID]['language'] == 'en': - result[cur_season][seasonArtID] = seasonsArtObj[seasonArtID]['_bannerpath'] - - if len(result[cur_season]) == 0: - continue - - return result - - def retrieveShowMetadata(self, dir): - - empty_return = (None, None) - - metadata_path = ek.ek(os.path.join, dir, self._show_file_name) - - if not ek.ek(os.path.isdir, dir) or not ek.ek(os.path.isfile, metadata_path): - logger.log(u"Can't load the metadata file from "+repr(metadata_path)+", it doesn't exist", logger.DEBUG) - return empty_return - - logger.log(u"Loading show info from metadata file in "+dir, logger.DEBUG) - - try: - xmlFileObj = ek.ek(open, metadata_path, 'r') - showXML = etree.ElementTree(file = xmlFileObj) - - if showXML.findtext('title') == None or (showXML.findtext('tvdbid') == None and showXML.findtext('id') == None): - logger.log(u"Invalid info in tvshow.nfo (missing name or id):" \ - + str(showXML.findtext('title')) + " " \ - + str(showXML.findtext('tvdbid')) + " " \ - + str(showXML.findtext('id'))) - return empty_return - - name = showXML.findtext('title') - if showXML.findtext('tvdbid') != None: - tvdb_id = int(showXML.findtext('tvdbid')) - elif showXML.findtext('id'): - tvdb_id = int(showXML.findtext('id')) - else: - logger.log(u"Empty <id> or <tvdbid> field in NFO, unable to find an ID", logger.WARNING) - return empty_return - - if not tvdb_id: - logger.log(u"Invalid tvdb id ("+str(tvdb_id)+"), not using metadata file", logger.WARNING) - return empty_return - - except (exceptions.NoNFOException, SyntaxError, ValueError), e: - logger.log(u"There was an error parsing your existing metadata file: " + ex(e), logger.WARNING) - return empty_return - - return (tvdb_id, name) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import os.path + +import xml.etree.cElementTree as etree + +import re + +import sickbeard + +from sickbeard import exceptions, helpers +from sickbeard.metadata import helpers as metadata_helpers +from sickbeard import logger +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + +from lib.tvdb_api import tvdb_api, tvdb_exceptions + + +class GenericMetadata(): + """ + Base class for all metadata providers. Default behavior is meant to mostly + follow XBMC metadata standards. Has support for: + + - show poster + - show fanart + - show metadata file + - episode thumbnail + - episode metadata file + - season thumbnails + """ + + def __init__(self, + show_metadata=False, + episode_metadata=False, + poster=False, + fanart=False, + episode_thumbnails=False, + season_thumbnails=False): + + self._show_file_name = "tvshow.nfo" + self._ep_nfo_extension = "nfo" + + self.poster_name = "folder.jpg" + self.fanart_name = "fanart.jpg" + + self.generate_show_metadata = True + self.generate_ep_metadata = True + + self.name = 'Generic' + + self.show_metadata = show_metadata + self.episode_metadata = episode_metadata + self.poster = poster + self.fanart = fanart + self.episode_thumbnails = episode_thumbnails + self.season_thumbnails = season_thumbnails + + def get_config(self): + config_list = [self.show_metadata, self.episode_metadata, self.poster, self.fanart, self.episode_thumbnails, self.season_thumbnails] + return '|'.join([str(int(x)) for x in config_list]) + + def get_id(self): + return GenericMetadata.makeID(self.name) + + @staticmethod + def makeID(name): + return re.sub("[^\w\d_]", "_", name).lower() + + def set_config(self, string): + config_list = [bool(int(x)) for x in string.split('|')] + self.show_metadata = config_list[0] + self.episode_metadata = config_list[1] + self.poster = config_list[2] + self.fanart = config_list[3] + self.episode_thumbnails = config_list[4] + self.season_thumbnails = config_list[5] + + def _has_show_metadata(self, show_obj): + result = ek.ek(os.path.isfile, self.get_show_file_path(show_obj)) + logger.log("Checking if "+self.get_show_file_path(show_obj)+" exists: "+str(result), logger.DEBUG) + return result + + def _has_episode_metadata(self, ep_obj): + result = ek.ek(os.path.isfile, self.get_episode_file_path(ep_obj)) + logger.log("Checking if "+self.get_episode_file_path(ep_obj)+" exists: "+str(result), logger.DEBUG) + return result + + def _has_poster(self, show_obj): + result = ek.ek(os.path.isfile, self.get_poster_path(show_obj)) + logger.log("Checking if "+self.get_poster_path(show_obj)+" exists: "+str(result), logger.DEBUG) + return result + + def _has_fanart(self, show_obj): + result = ek.ek(os.path.isfile, self.get_fanart_path(show_obj)) + logger.log("Checking if "+self.get_fanart_path(show_obj)+" exists: "+str(result), logger.DEBUG) + return result + + def _has_episode_thumb(self, ep_obj): + location = self.get_episode_thumb_path(ep_obj) + result = location != None and ek.ek(os.path.isfile, location) + if location: + logger.log("Checking if "+location+" exists: "+str(result), logger.DEBUG) + return result + + def _has_season_thumb(self, show_obj, season): + location = self.get_season_thumb_path(show_obj, season) + result = location != None and ek.ek(os.path.isfile, location) + if location: + logger.log("Checking if "+location+" exists: "+str(result), logger.DEBUG) + return result + + def get_show_file_path(self, show_obj): + return ek.ek(os.path.join, show_obj.location, self._show_file_name) + + def get_episode_file_path(self, ep_obj): + return helpers.replaceExtension(ep_obj.location, self._ep_nfo_extension) + + def get_poster_path(self, show_obj): + return ek.ek(os.path.join, show_obj.location, self.poster_name) + + def get_fanart_path(self, show_obj): + return ek.ek(os.path.join, show_obj.location, self.fanart_name) + + def get_episode_thumb_path(self, ep_obj): + """ + Returns the path where the episode thumbnail should be stored. Defaults to + the same path as the episode file but with a .tbn extension. + + ep_obj: a TVEpisode instance for which to create the thumbnail + """ + if ek.ek(os.path.isfile, ep_obj.location): + tbn_filename = helpers.replaceExtension(ep_obj.location, 'tbn') + else: + return None + + return tbn_filename + + def get_season_thumb_path(self, show_obj, season): + """ + Returns the full path to the file for a given season thumb. + + show_obj: a TVShow instance for which to generate the path + season: a season number to be used for the path. Note that sesaon 0 + means specials. + """ + + # Our specials thumbnail is, well, special + if season == 0: + season_thumb_file_path = 'season-specials' + else: + season_thumb_file_path = 'season' + str(season).zfill(2) + + return ek.ek(os.path.join, show_obj.location, season_thumb_file_path+'.tbn') + + def _show_data(self, show_obj): + """ + This should be overridden by the implementing class. It should + provide the content of the show metadata file. + """ + return None + + def _ep_data(self, ep_obj): + """ + This should be overridden by the implementing class. It should + provide the content of the episode metadata file. + """ + return None + + def create_show_metadata(self, show_obj): + if self.show_metadata and show_obj and not self._has_show_metadata(show_obj): + logger.log("Metadata provider "+self.name+" creating show metadata for "+show_obj.name, logger.DEBUG) + return self.write_show_file(show_obj) + return False + + def create_episode_metadata(self, ep_obj): + if self.episode_metadata and ep_obj and not self._has_episode_metadata(ep_obj): + logger.log("Metadata provider "+self.name+" creating episode metadata for "+ep_obj.prettyName(), logger.DEBUG) + return self.write_ep_file(ep_obj) + return False + + def create_poster(self, show_obj): + if self.poster and show_obj and not self._has_poster(show_obj): + logger.log("Metadata provider "+self.name+" creating poster for "+show_obj.name, logger.DEBUG) + return self.save_poster(show_obj) + return False + + def create_fanart(self, show_obj): + if self.fanart and show_obj and not self._has_fanart(show_obj): + logger.log("Metadata provider "+self.name+" creating fanart for "+show_obj.name, logger.DEBUG) + return self.save_fanart(show_obj) + return False + + def create_episode_thumb(self, ep_obj): + if self.episode_thumbnails and ep_obj and not self._has_episode_thumb(ep_obj): + logger.log("Metadata provider "+self.name+" creating show metadata for "+ep_obj.prettyName(), logger.DEBUG) + return self.save_thumbnail(ep_obj) + return False + + def create_season_thumbs(self, show_obj): + if self.season_thumbnails and show_obj: + logger.log("Metadata provider "+self.name+" creating season thumbnails for "+show_obj.name, logger.DEBUG) + return self.save_season_thumbs(show_obj) + return False + + def _get_episode_thumb_url(self, ep_obj): + """ + Returns the URL to use for downloading an episode's thumbnail. Uses + theTVDB.com data. + + ep_obj: a TVEpisode object for which to grab the thumb URL + """ + all_eps = [ep_obj] + ep_obj.relatedEps + + tvdb_lang = ep_obj.show.lang + + # get a TVDB object + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) + tvdb_show_obj = t[ep_obj.show.tvdbid] + except tvdb_exceptions.tvdb_shownotfound, e: + raise exceptions.ShowNotFoundException(e.message) + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+ex(e), logger.ERROR) + return None + + # try all included episodes in case some have thumbs and others don't + for cur_ep in all_eps: + try: + myEp = tvdb_show_obj[cur_ep.season][cur_ep.episode] + except (tvdb_exceptions.tvdb_episodenotfound, tvdb_exceptions.tvdb_seasonnotfound): + logger.log(u"Unable to find episode " + str(cur_ep.season) + "x" + str(cur_ep.episode) + " on tvdb... has it been removed? Should I delete from db?") + continue + + thumb_url = myEp["filename"] + + if thumb_url: + return thumb_url + + return None + + def write_show_file(self, show_obj): + """ + Generates and writes show_obj's metadata under the given path to the + filename given by get_show_file_path() + + show_obj: TVShow object for which to create the metadata + + path: An absolute or relative path where we should put the file. Note that + the file name will be the default show_file_name. + + Note that this method expects that _show_data will return an ElementTree + object. If your _show_data returns data in another format you'll need to + override this method. + """ + + data = self._show_data(show_obj) + + if not data: + return False + + nfo_file_path = self.get_show_file_path(show_obj) + nfo_file_dir = ek.ek(os.path.dirname, nfo_file_path) + + try: + if not ek.ek(os.path.isdir, nfo_file_dir): + logger.log("Metadata dir didn't exist, creating it at "+nfo_file_dir, logger.DEBUG) + ek.ek(os.makedirs, nfo_file_dir) + helpers.chmodAsParent(nfo_file_dir) + + logger.log(u"Writing show nfo file to "+nfo_file_path) + + nfo_file = ek.ek(open, nfo_file_path, 'w') + + data.write(nfo_file, encoding="utf-8") + nfo_file.close() + helpers.chmodAsParent(nfo_file_path) + except IOError, e: + logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) + return False + + return True + + def write_ep_file(self, ep_obj): + """ + Generates and writes ep_obj's metadata under the given path with the + given filename root. Uses the episode's name with the extension in + _ep_nfo_extension. + + ep_obj: TVEpisode object for which to create the metadata + + file_name_path: The file name to use for this metadata. Note that the extension + will be automatically added based on _ep_nfo_extension. This should + include an absolute path. + + Note that this method expects that _ep_data will return an ElementTree + object. If your _ep_data returns data in another format you'll need to + override this method. + """ + + data = self._ep_data(ep_obj) + + if not data: + return False + + nfo_file_path = self.get_episode_file_path(ep_obj) + nfo_file_dir = ek.ek(os.path.dirname, nfo_file_path) + + try: + if not ek.ek(os.path.isdir, nfo_file_dir): + logger.log("Metadata dir didn't exist, creating it at "+nfo_file_dir, logger.DEBUG) + ek.ek(os.makedirs, nfo_file_dir) + helpers.chmodAsParent(nfo_file_dir) + + logger.log(u"Writing episode nfo file to "+nfo_file_path) + + nfo_file = ek.ek(open, nfo_file_path, 'w') + + data.write(nfo_file, encoding="utf-8") + nfo_file.close() + helpers.chmodAsParent(nfo_file_path) + except IOError, e: + logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) + return False + + return True + + def save_thumbnail(self, ep_obj): + """ + Retrieves a thumbnail and saves it to the correct spot. This method should not need to + be overridden by implementing classes, changing get_episode_thumb_path and + _get_episode_thumb_url should suffice. + + ep_obj: a TVEpisode object for which to generate a thumbnail + """ + + file_path = self.get_episode_thumb_path(ep_obj) + + if not file_path: + logger.log(u"Unable to find a file path to use for this thumbnail, not generating it", logger.DEBUG) + return False + + thumb_url = self._get_episode_thumb_url(ep_obj) + + # if we can't find one then give up + if not thumb_url: + logger.log("No thumb is available for this episode, not creating a thumb", logger.DEBUG) + return False + + thumb_data = metadata_helpers.getShowImage(thumb_url) + + result = self._write_image(thumb_data, file_path) + + if not result: + return False + + for cur_ep in [ep_obj] + ep_obj.relatedEps: + cur_ep.hastbn = True + + return True + + def save_fanart(self, show_obj, which=None): + """ + Downloads a fanart image and saves it to the filename specified by fanart_name + inside the show's root folder. + + show_obj: a TVShow object for which to download fanart + """ + + # use the default fanart name + fanart_path = self.get_fanart_path(show_obj) + + fanart_data = self._retrieve_show_image('fanart', show_obj, which) + + if not fanart_data: + logger.log(u"No fanart image was retrieved, unable to write fanart", logger.DEBUG) + return False + + return self._write_image(fanart_data, fanart_path) + + + def save_poster(self, show_obj, which=None): + """ + Downloads a poster image and saves it to the filename specified by poster_name + inside the show's root folder. + + show_obj: a TVShow object for which to download a poster + """ + + # use the default poster name + poster_path = self.get_poster_path(show_obj) + + if sickbeard.USE_BANNER: + img_type = 'banner' + else: + img_type = 'poster' + + poster_data = self._retrieve_show_image(img_type, show_obj, which) + + if not poster_data: + logger.log(u"No show folder image was retrieved, unable to write poster", logger.DEBUG) + return False + + return self._write_image(poster_data, poster_path) + + + def save_season_thumbs(self, show_obj): + """ + Saves all season thumbnails to disk for the given show. + + show_obj: a TVShow object for which to save the season thumbs + + Cycles through all seasons and saves the season thumbs if possible. This + method should not need to be overridden by implementing classes, changing + _season_thumb_dict and get_season_thumb_path should be good enough. + """ + + season_dict = self._season_thumb_dict(show_obj) + + # Returns a nested dictionary of season art with the season + # number as primary key. It's really overkill but gives the option + # to present to user via ui to pick down the road. + for cur_season in season_dict: + + cur_season_art = season_dict[cur_season] + + if len(cur_season_art) == 0: + continue + + # Just grab whatever's there for now + art_id, season_url = cur_season_art.popitem() #@UnusedVariable + + season_thumb_file_path = self.get_season_thumb_path(show_obj, cur_season) + + if not season_thumb_file_path: + logger.log(u"Path for season "+str(cur_season)+" came back blank, skipping this season", logger.DEBUG) + continue + + seasonData = metadata_helpers.getShowImage(season_url) + + if not seasonData: + logger.log(u"No season thumb data available, skipping this season", logger.DEBUG) + continue + + self._write_image(seasonData, season_thumb_file_path) + + return True + + def _write_image(self, image_data, image_path): + """ + Saves the data in image_data to the location image_path. Returns True/False + to represent success or failure. + + image_data: binary image data to write to file + image_path: file location to save the image to + """ + + # don't bother overwriting it + if ek.ek(os.path.isfile, image_path): + logger.log(u"Image already exists, not downloading", logger.DEBUG) + return False + + if not image_data: + logger.log(u"Unable to retrieve image, skipping", logger.WARNING) + return False + + image_dir = ek.ek(os.path.dirname, image_path) + + try: + if not ek.ek(os.path.isdir, image_dir): + logger.log("Metadata dir didn't exist, creating it at "+image_dir, logger.DEBUG) + ek.ek(os.makedirs, image_dir) + helpers.chmodAsParent(image_dir) + + outFile = ek.ek(open, image_path, 'wb') + outFile.write(image_data) + outFile.close() + helpers.chmodAsParent(image_path) + except IOError, e: + logger.log(u"Unable to write image to "+image_path+" - are you sure the show folder is writable? "+ex(e), logger.ERROR) + return False + + return True + + def _retrieve_show_image(self, image_type, show_obj, which=None): + """ + Gets an image URL from theTVDB.com, downloads it and returns the data. + + image_type: type of image to retrieve (currently supported: poster, fanart) + show_obj: a TVShow object to use when searching for the image + which: optional, a specific numbered poster to look for + + Returns: the binary image data if available, or else None + """ + + tvdb_lang = show_obj.lang + + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) + tvdb_show_obj = t[show_obj.tvdbid] + except (tvdb_exceptions.tvdb_error, IOError), e: + logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) + return None + + if image_type not in ('fanart', 'poster', 'banner', 'poster_thumb', 'banner_thumb'): + logger.log(u"Invalid image type "+str(image_type)+", couldn't find it in the TVDB object", logger.ERROR) + return None + + try: + if image_type == 'poster_thumb': + image_url = re.sub('posters', '_cache/posters', tvdb_show_obj['poster']) + elif image_type == 'banner_thumb': + image_url = re.sub('graphical', '_cache/graphical', tvdb_show_obj['banner']) + else: + image_url = tvdb_show_obj[image_type] + except: + return None + + image_data = metadata_helpers.getShowImage(image_url, which) + + return image_data + + def _season_thumb_dict(self, show_obj): + """ + Should return a dict like: + + result = {<season number>: + {1: '<url 1>', 2: <url 2>, ...},} + """ + + # This holds our resulting dictionary of season art + result = {} + + tvdb_lang = show_obj.lang + + try: + # There's gotta be a better way of doing this but we don't wanna + # change the language value elsewhere + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + + if tvdb_lang and not tvdb_lang == 'en': + ltvdb_api_parms['language'] = tvdb_lang + + t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) + tvdb_show_obj = t[show_obj.tvdbid] + except (tvdb_exceptions.tvdb_error, IOError), e: + logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) + return result + + # How many seasons? + num_seasons = len(tvdb_show_obj) + + # if we have no season banners then just finish + if 'season' not in tvdb_show_obj['_banners'] or 'season' not in tvdb_show_obj['_banners']['season']: + return result + + # Give us just the normal poster-style season graphics + seasonsArtObj = tvdb_show_obj['_banners']['season']['season'] + + # Returns a nested dictionary of season art with the season + # number as primary key. It's really overkill but gives the option + # to present to user via ui to pick down the road. + for cur_season in range(num_seasons+1): + + result[cur_season] = {} + + # find the correct season in the tvdb object and just copy the dict into our result dict + for seasonArtID in seasonsArtObj.keys(): + if int(seasonsArtObj[seasonArtID]['season']) == cur_season and seasonsArtObj[seasonArtID]['language'] == 'en': + result[cur_season][seasonArtID] = seasonsArtObj[seasonArtID]['_bannerpath'] + + if len(result[cur_season]) == 0: +# continue + del result[cur_season] + + return result + + def retrieveShowMetadata(self, dir): + + empty_return = (None, None) + + metadata_path = ek.ek(os.path.join, dir, self._show_file_name) + + if not ek.ek(os.path.isdir, dir) or not ek.ek(os.path.isfile, metadata_path): + logger.log(u"Can't load the metadata file from "+repr(metadata_path)+", it doesn't exist", logger.DEBUG) + return empty_return + + logger.log(u"Loading show info from metadata file in "+dir, logger.DEBUG) + + try: + xmlFileObj = ek.ek(open, metadata_path, 'r') + showXML = etree.ElementTree(file = xmlFileObj) + + if showXML.findtext('title') == None or (showXML.findtext('tvdbid') == None and showXML.findtext('id') == None): + logger.log(u"Invalid info in tvshow.nfo (missing name or id):" \ + + str(showXML.findtext('title')) + " " \ + + str(showXML.findtext('tvdbid')) + " " \ + + str(showXML.findtext('id'))) + return empty_return + + name = showXML.findtext('title') + if showXML.findtext('tvdbid') != None: + tvdb_id = int(showXML.findtext('tvdbid')) + elif showXML.findtext('id'): + tvdb_id = int(showXML.findtext('id')) + else: + logger.log(u"Empty <id> or <tvdbid> field in NFO, unable to find an ID", logger.WARNING) + return empty_return + + if not tvdb_id: + logger.log(u"Invalid tvdb id ("+str(tvdb_id)+"), not using metadata file", logger.WARNING) + return empty_return + + except (exceptions.NoNFOException, SyntaxError, ValueError), e: + logger.log(u"There was an error parsing your existing metadata file: " + ex(e), logger.WARNING) + return empty_return + + return (tvdb_id, name) diff --git a/sickbeard/network_timezones.py b/sickbeard/network_timezones.py new file mode 100644 index 0000000000000000000000000000000000000000..9b87cff1735896a95aa965dc6052547b7782a0cd --- /dev/null +++ b/sickbeard/network_timezones.py @@ -0,0 +1,166 @@ +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from lib.dateutil import tz +import lib.dateutil.zoneinfo +from sickbeard import db +from sickbeard import helpers +from sickbeard import logger +from sickbeard import encodingKludge as ek +from os.path import basename, realpath +import os +import re + +# helper to remove failed temp download +def _remove_zoneinfo_failed(filename): + try: + os.remove(filename) + except: + pass + +# update the dateutil zoneinfo +def _update_zoneinfo(): + + # now check if the zoneinfo needs update + url_zv = 'http://github.com/Prinz23/sb_network_timezones/raw/master/zoneinfo.txt' + + url_data = helpers.getURL(url_zv) + + if url_data is None: + # When urlData is None, trouble connecting to github + logger.log(u"Loading zoneinfo.txt failed. Unable to get URL: " + url_zv, logger.ERROR) + return + + if (lib.dateutil.zoneinfo.ZONEINFOFILE != None): + cur_zoneinfo = ek.ek(basename, lib.dateutil.zoneinfo.ZONEINFOFILE) + else: + cur_zoneinfo = None + (new_zoneinfo, zoneinfo_md5) = url_data.decode('utf-8').strip().rsplit(u' ') + + if ((cur_zoneinfo != None) and (new_zoneinfo == cur_zoneinfo)): + return + + # now load the new zoneinfo + url_tar = u'http://github.com/Prinz23/sb_network_timezones/raw/master/' + new_zoneinfo + zonefile = ek.ek(realpath, u'lib/dateutil/zoneinfo/' + new_zoneinfo) + zonefile_tmp = re.sub(r"\.tar\.gz$",'.tmp', zonefile) + + if (os.path.exists(zonefile_tmp)): + try: + os.remove(zonefile_tmp) + except: + logger.log(u"Unable to delete: " + zonefile_tmp,logger.ERROR) + return + + if not helpers.download_file(url_tar, zonefile_tmp): + return + + new_hash = str(helpers.md5_for_file(zonefile_tmp)) + + if (zoneinfo_md5.upper() == new_hash.upper()): + logger.log(u"Updating timezone info with new one: " + new_zoneinfo,logger.MESSAGE) + try: + # remove the old zoneinfo file + if (cur_zoneinfo != None): + old_file = ek.ek(realpath, u'lib/dateutil/zoneinfo/' + cur_zoneinfo) + if (os.path.exists(old_file)): + os.remove(old_file) + # rename downloaded file + os.rename(zonefile_tmp,zonefile) + # load the new zoneinfo + reload(lib.dateutil.zoneinfo) + except: + _remove_zoneinfo_failed(zonefile_tmp) + return + else: + _remove_zoneinfo_failed(zonefile_tmp) + logger.log(u"MD5 HASH doesn't match: " + zoneinfo_md5.upper() + ' File: ' + new_hash.upper(),logger.ERROR) + return + +# update the network timezone table +def update_network_dict(): + + _update_zoneinfo() + + d = {} + + # network timezones are stored on github pages + url = 'http://github.com/Prinz23/sb_network_timezones/raw/master/network_timezones.txt' + + url_data = helpers.getURL(url) + + if url_data is None: + # When urlData is None, trouble connecting to github + logger.log(u"Loading Network Timezones update failed. Unable to get URL: " + url, logger.ERROR) + return + + try: + for line in url_data.splitlines(): + (key, val) = line.decode('utf-8').strip().rsplit(u':',1) + if key == None or val == None: + continue + d[key] = val + except (IOError, OSError): + pass + + myDB = db.DBConnection("cache.db") + # load current network timezones + old_d = dict(myDB.select("SELECT * FROM network_timezones")) + + # list of sql commands to update the network_timezones table + ql = [] + for cur_d, cur_t in d.iteritems(): + h_k = old_d.has_key(cur_d) + if h_k and cur_t != old_d[cur_d]: + # update old record + ql.append(["UPDATE network_timezones SET network_name=?, timezone=? WHERE network_name=?", [cur_d, cur_t, cur_d]]) + elif not h_k: + # add new record + ql.append(["INSERT INTO network_timezones (network_name, timezone) VALUES (?,?)", [cur_d, cur_t]]) + if h_k: + del old_d[cur_d] + # remove deleted records + if len(old_d) > 0: + L = list(va for va in old_d) + ql.append(["DELETE FROM network_timezones WHERE network_name IN ("+','.join(['?'] * len(L))+")", L]) + # change all network timezone infos at once (much faster) + myDB.mass_action(ql) + +# load network timezones from db into dict +def load_network_dict(): + d = {} + try: + myDB = db.DBConnection("cache.db") + cur_network_list = myDB.select("SELECT * FROM network_timezones") + if cur_network_list == None or len(cur_network_list) < 1: + update_network_dict() + cur_network_list = myDB.select("SELECT * FROM network_timezones") + d = dict(cur_network_list) + except: + d = {} + return d + +# get timezone of a network or return default timezone +def get_network_timezone(network, network_dict, sb_timezone): + if network == None: + return sb_timezone + + try: + return tz.gettz(network_dict[network]) + except: + return sb_timezone \ No newline at end of file diff --git a/sickbeard/notifiers/__init__.py b/sickbeard/notifiers/__init__.py index fe0cd654f076a108377a922514f768a04ac1e0ad..130d0acc92ed08faf163bd56896fcb45e011d9ea 100755 --- a/sickbeard/notifiers/__init__.py +++ b/sickbeard/notifiers/__init__.py @@ -1,92 +1,98 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import sickbeard - -import xbmc -import plex -import nmj -import nmjv2 -import synoindex -import pytivo - -import growl -import prowl -import notifo -from . import libnotify -import pushover -import boxcar -import nma -import mail - -import tweet -import trakt - -from sickbeard.common import * - -# home theater -xbmc_notifier = xbmc.XBMCNotifier() -plex_notifier = plex.PLEXNotifier() -nmj_notifier = nmj.NMJNotifier() -synoindex_notifier = synoindex.synoIndexNotifier() -nmjv2_notifier = nmjv2.NMJv2Notifier() -pytivo_notifier = pytivo.pyTivoNotifier() -# devices -growl_notifier = growl.GrowlNotifier() -prowl_notifier = prowl.ProwlNotifier() -notifo_notifier = notifo.NotifoNotifier() -libnotify_notifier = libnotify.LibnotifyNotifier() -pushover_notifier = pushover.PushoverNotifier() -boxcar_notifier = boxcar.BoxcarNotifier() -nma_notifier = nma.NMA_Notifier() -# online -twitter_notifier = tweet.TwitterNotifier() -trakt_notifier = trakt.TraktNotifier() -mail_notifier = mail.MailNotifier() - -notifiers = [ - libnotify_notifier, # Libnotify notifier goes first because it doesn't involve blocking on network activity. - xbmc_notifier, - plex_notifier, - nmj_notifier, - nmjv2_notifier, - synoindex_notifier, - pytivo_notifier, - growl_notifier, - prowl_notifier, - notifo_notifier, - pushover_notifier, - boxcar_notifier, - nma_notifier, - twitter_notifier, - trakt_notifier, - mail_notifier, -] - - -def notify_download(ep_name): - for n in notifiers: - n.notify_download(ep_name) - -def notify_subtitle_download(ep_name, lang): - for n in notifiers: - n.notify_subtitle_download(ep_name, lang) - -def notify_snatch(ep_name): - for n in notifiers: - n.notify_snatch(ep_name) +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import sickbeard + +import xbmc +import plex +import nmj +import nmjv2 +import synoindex +import synologynotifier +import pytivo + +import growl +import prowl +import notifo +from . import libnotify +import pushover +import boxcar +import nma +import mail +import pushalot + +import tweet +import trakt + +from sickbeard.common import * + +# home theater +xbmc_notifier = xbmc.XBMCNotifier() +plex_notifier = plex.PLEXNotifier() +nmj_notifier = nmj.NMJNotifier() +synoindex_notifier = synoindex.synoIndexNotifier() +nmjv2_notifier = nmjv2.NMJv2Notifier() +synology_notifier = synologynotifier.synologyNotifier() +pytivo_notifier = pytivo.pyTivoNotifier() +# devices +growl_notifier = growl.GrowlNotifier() +prowl_notifier = prowl.ProwlNotifier() +notifo_notifier = notifo.NotifoNotifier() +libnotify_notifier = libnotify.LibnotifyNotifier() +pushover_notifier = pushover.PushoverNotifier() +boxcar_notifier = boxcar.BoxcarNotifier() +nma_notifier = nma.NMA_Notifier() +pushalot_notifier = pushalot.PushalotNotifier() +# online +twitter_notifier = tweet.TwitterNotifier() +trakt_notifier = trakt.TraktNotifier() +mail_notifier = mail.MailNotifier() + +notifiers = [ + libnotify_notifier, # Libnotify notifier goes first because it doesn't involve blocking on network activity. + xbmc_notifier, + plex_notifier, + nmj_notifier, + nmjv2_notifier, + synoindex_notifier, + synology_notifier, + pytivo_notifier, + growl_notifier, + prowl_notifier, + notifo_notifier, + pushover_notifier, + boxcar_notifier, + nma_notifier, + pushalot_notifier, + twitter_notifier, + trakt_notifier, + mail_notifier, +] + + +def notify_download(ep_name): + for n in notifiers: + n.notify_download(ep_name) + +def notify_subtitle_download(ep_name, lang): + for n in notifiers: + n.notify_subtitle_download(ep_name, lang) + +def notify_snatch(ep_name): + for n in notifiers: + n.notify_snatch(ep_name) diff --git a/sickbeard/notifiers/pushalot.py b/sickbeard/notifiers/pushalot.py new file mode 100644 index 0000000000000000000000000000000000000000..3d3d634d16ed24ff4463f151d7c3af284049bfff --- /dev/null +++ b/sickbeard/notifiers/pushalot.py @@ -0,0 +1,83 @@ +# Author: Maciej Olesinski (https://github.com/molesinski/) +# Based on prowl.py by Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from httplib import HTTPSConnection, HTTPException +from urllib import urlencode +from ssl import SSLError + +import sickbeard +from sickbeard import logger, common + +class PushalotNotifier: + + def test_notify(self, pushalot_authorizationtoken): + return self._sendPushalot(pushalot_authorizationtoken, event="Test", message="Testing Pushalot settings from Sick Beard", force=True) + + def notify_snatch(self, ep_name): + if sickbeard.PUSHALOT_NOTIFY_ONSNATCH: + self._sendPushalot(pushalot_authorizationtoken=None, event=common.notifyStrings[common.NOTIFY_SNATCH], message=ep_name) + + def notify_download(self, ep_name): + if sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD: + self._sendPushalot(pushalot_authorizationtoken=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD], message=ep_name) + + def notify_subtitle_download(self, ep_name, lang): + if sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD: + self._sendPushalot(pushalot_authorizationtoken=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], message=ep_name + ": " + lang) + + def _sendPushalot(self, pushalot_authorizationtoken=None, event=None, message=None, force=False): + + if not sickbeard.USE_PUSHALOT and not force: + return False + + if pushalot_authorizationtoken == None: + pushalot_authorizationtoken = sickbeard.PUSHALOT_AUTHORIZATIONTOKEN + + logger.log(u"Pushalot event: " + event, logger.DEBUG) + logger.log(u"Pushalot message: " + message, logger.DEBUG) + logger.log(u"Pushalot api: " + pushalot_authorizationtoken, logger.DEBUG) + + http_handler = HTTPSConnection("pushalot.com") + + data = {'AuthorizationToken': pushalot_authorizationtoken, + 'Title': event.encode('utf-8'), + 'Body': message.encode('utf-8') } + + try: + http_handler.request("POST", + "/api/sendmessage", + headers = {'Content-type': "application/x-www-form-urlencoded"}, + body = urlencode(data)) + except (SSLError, HTTPException): + logger.log(u"Pushalot notification failed.", logger.ERROR) + return False + response = http_handler.getresponse() + request_status = response.status + + if request_status == 200: + logger.log(u"Pushalot notifications sent.", logger.DEBUG) + return True + elif request_status == 410: + logger.log(u"Pushalot auth failed: %s" % response.reason, logger.ERROR) + return False + else: + logger.log(u"Pushalot notification failed.", logger.ERROR) + return False + +notifier = PushalotNotifier diff --git a/sickbeard/notifiers/synologynotifier.py b/sickbeard/notifiers/synologynotifier.py new file mode 100644 index 0000000000000000000000000000000000000000..a7f9b6799c3035b7299601ee14a6407b05cbd167 --- /dev/null +++ b/sickbeard/notifiers/synologynotifier.py @@ -0,0 +1,55 @@ +# Author: Nyaran <nyayukko@gmail.com> +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + + + +import os +import subprocess + +import sickbeard + +from sickbeard import logger +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex +from sickbeard import common + +class synologyNotifier: + + def notify_snatch(self, ep_name): + if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH: + self._send_synologyNotifier(ep_name, common.notifyStrings[common.NOTIFY_SNATCH]) + + def notify_download(self, ep_name): + if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD: + self._send_synologyNotifier(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) + + def notify_subtitle_download(self, ep_name, lang): + if sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD: + self._send_synologyNotifier(ep_name + ": " + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) + + def _send_synologyNotifier(self, message, title): + synodsmnotify_cmd = ["/usr/syno/bin/synodsmnotify", "@administrators", title, message] + logger.log(u"Executing command "+str(synodsmnotify_cmd)) + logger.log(u"Absolute path to command: "+ek.ek(os.path.abspath, synodsmnotify_cmd[0]), logger.DEBUG) + try: + p = subprocess.Popen(synodsmnotify_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) + out, err = p.communicate() #@UnusedVariable + logger.log(u"Script result: "+str(out), logger.DEBUG) + except OSError, e: + logger.log(u"Unable to run synodsmnotify: "+ex(e)) + +notifier = synologyNotifier diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index 0eef7bb9604b4cc7fb1e7239437cc56bea70bce9..146e36d19155d6dc8ffd5876de3f4851b3921a4d 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -1,492 +1,521 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import traceback - -import sickbeard - -from lib.tvdb_api import tvdb_exceptions, tvdb_api - -from sickbeard.common import SKIPPED, WANTED - -from sickbeard.tv import TVShow -from sickbeard import exceptions, logger, ui, db -from sickbeard import generic_queue -from sickbeard import name_cache -from sickbeard.exceptions import ex - - -class ShowQueue(generic_queue.GenericQueue): - - def __init__(self): - generic_queue.GenericQueue.__init__(self) - self.queue_name = "SHOWQUEUE" - - def _isInQueue(self, show, actions): - return show in [x.show for x in self.queue if x.action_id in actions] - - def _isBeingSomethinged(self, show, actions): - return self.currentItem != None and show == self.currentItem.show and \ - self.currentItem.action_id in actions - - def isInUpdateQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) - - def isInRefreshQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.REFRESH,)) - - def isInRenameQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.RENAME,)) - - def isInSubtitleQueue(self, show): - return self._isInQueue(show, (ShowQueueActions.SUBTITLE,)) - - def isBeingAdded(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.ADD,)) - - def isBeingUpdated(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) - - def isBeingRefreshed(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.REFRESH,)) - - def isBeingRenamed(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.RENAME,)) - - def isBeingSubtitled(self, show): - return self._isBeingSomethinged(show, (ShowQueueActions.SUBTITLE,)) - - def _getLoadingShowList(self): - return [x for x in self.queue + [self.currentItem] if x != None and x.isLoading] - - loadingShowList = property(_getLoadingShowList) - - def updateShow(self, show, force=False): - - if self.isBeingAdded(show): - raise exceptions.CantUpdateException("Show is still being added, wait until it is finished before you update.") - - if self.isBeingUpdated(show): - raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") - - if self.isInUpdateQueue(show): - raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") - - if not force: - queueItemObj = QueueItemUpdate(show) - else: - queueItemObj = QueueItemForceUpdate(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def refreshShow(self, show, force=False): - - if self.isBeingRefreshed(show) and not force: - raise exceptions.CantRefreshException("This show is already being refreshed, not refreshing again.") - - if (self.isBeingUpdated(show) or self.isInUpdateQueue(show)) and not force: - logger.log(u"A refresh was attempted but there is already an update queued or in progress. Since updates do a refres at the end anyway I'm skipping this request.", logger.DEBUG) - return - - queueItemObj = QueueItemRefresh(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def renameShowEpisodes(self, show, force=False): - - queueItemObj = QueueItemRename(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def downloadSubtitles(self, show, force=False): - - queueItemObj = QueueItemSubtitle(show) - - self.add_item(queueItemObj) - - return queueItemObj - - def addShow(self, tvdb_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="fr", subtitles=None, audio_lang=None): - queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, flatten_folders, lang, subtitles, audio_lang) - - self.add_item(queueItemObj) - - return queueItemObj - - -class ShowQueueActions: - REFRESH = 1 - ADD = 2 - UPDATE = 3 - FORCEUPDATE = 4 - RENAME = 5 - SUBTITLE=6 - - names = {REFRESH: 'Refresh', - ADD: 'Add', - UPDATE: 'Update', - FORCEUPDATE: 'Force Update', - RENAME: 'Rename', - SUBTITLE: 'Subtitle', - } - - -class ShowQueueItem(generic_queue.QueueItem): - """ - Represents an item in the queue waiting to be executed - - Can be either: - - show being added (may or may not be associated with a show object) - - show being refreshed - - show being updated - - show being force updated - - show being subtitled - """ - def __init__(self, action_id, show): - generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id) - self.show = show - - def isInQueue(self): - return self in sickbeard.showQueueScheduler.action.queue + [sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable - - def _getName(self): - return str(self.show.tvdbid) - - def _isLoading(self): - return False - - show_name = property(_getName) - - isLoading = property(_isLoading) - - -class QueueItemAdd(ShowQueueItem): - def __init__(self, tvdb_id, showDir, default_status, quality, flatten_folders, lang, subtitles, audio_lang): - - self.tvdb_id = tvdb_id - self.showDir = showDir - self.default_status = default_status - self.quality = quality - self.flatten_folders = flatten_folders - self.lang = lang - self.audio_lang = audio_lang - self.subtitles = subtitles - - self.show = None - - # this will initialize self.show to None - ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show) - - def _getName(self): - """ - Returns the show name if there is a show object created, if not returns - the dir that the show is being added to. - """ - if self.show == None: - return self.showDir - return self.show.name - - show_name = property(_getName) - - def _isLoading(self): - """ - Returns True if we've gotten far enough to have a show object, or False - if we still only know the folder name. - """ - if self.show == None: - return True - return False - - isLoading = property(_isLoading) - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Starting to add show " + self.showDir) - - try: - # make sure the tvdb ids are valid - try: - ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() - if self.lang: - ltvdb_api_parms['language'] = self.lang - - logger.log(u"TVDB: " + repr(ltvdb_api_parms)) - - t = tvdb_api.Tvdb(**ltvdb_api_parms) - s = t[self.tvdb_id] - - # this usually only happens if they have an NFO in their show dir which gave us a TVDB ID that has no proper english version of the show - if not s['seriesname']: - logger.log(u"Show in " + self.showDir + " has no name on TVDB, probably the wrong language used to search with.", logger.ERROR) - ui.notifications.error("Unable to add show", "Show in " + self.showDir + " has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") - self._finishEarly() - return - # if the show has no episodes/seasons - if not s: - logger.log(u"Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.", logger.ERROR) - ui.notifications.error("Unable to add show", "Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.") - self._finishEarly() - return - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Error contacting TVDB: " + ex(e), logger.ERROR) - ui.notifications.error("Unable to add show", "Unable to look up the show in " + self.showDir + " on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") - self._finishEarly() - return - - # clear the name cache - name_cache.clearCache() - - newShow = TVShow(self.tvdb_id, self.lang, self.audio_lang) - newShow.loadFromTVDB() - - self.show = newShow - - # set up initial values - self.show.location = self.showDir - self.show.subtitles = self.subtitles if self.subtitles != None else sickbeard.SUBTITLES_DEFAULT - self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT - self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT - self.show.paused = 0 - - # be smartish about this - if self.show.genre and "talk show" in self.show.genre.lower(): - self.show.air_by_date = 1 - - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to add show due to an error with TVDB: " + ex(e), logger.ERROR) - if self.show: - ui.notifications.error("Unable to add " + str(self.show.name) + " due to an error with TVDB") - else: - ui.notifications.error("Unable to add show due to an error with TVDB") - self._finishEarly() - return - - except exceptions.MultipleShowObjectsException: - logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) - ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") - self._finishEarly() - return - - except Exception, e: - logger.log(u"Error trying to add show: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - self._finishEarly() - raise - - # add it to the show list - sickbeard.showList.append(self.show) - - try: - self.show.loadEpisodesFromDir() - except Exception, e: - logger.log(u"Error searching dir for episodes: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - try: - self.show.loadEpisodesFromTVDB() - self.show.setTVRID() - - self.show.writeMetadata() - self.show.populateCache() - - except Exception, e: - logger.log(u"Error with TVDB, not creating episode list: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - try: - self.show.saveToDB() - except Exception, e: - logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - # if they gave a custom status then change all the eps to it - if self.default_status != SKIPPED: - logger.log(u"Setting all episodes to the specified default status: " + str(self.default_status)) - myDB = db.DBConnection() - myDB.action("UPDATE tv_episodes SET status = ? WHERE status = ? AND showid = ? AND season != 0", [self.default_status, SKIPPED, self.show.tvdbid]) - - # if they started with WANTED eps then run the backlog - if self.default_status == WANTED: - logger.log(u"Launching backlog for this show since its episodes are WANTED") - sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable - - self.show.flushEpisodes() - - # if there are specific episodes that need to be added by trakt - sickbeard.traktWatchListCheckerSchedular.action.manageNewShow(self.show) - - self.finish() - - def _finishEarly(self): - if self.show != None: - self.show.deleteShow() - - self.finish() - - -class QueueItemRefresh(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show) - - # do refreshes first because they're quick - self.priority = generic_queue.QueuePriorities.HIGH - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Performing refresh on " + self.show.name) - - self.show.refreshDir() - self.show.writeMetadata() - self.show.populateCache() - - self.inProgress = False - - -class QueueItemRename(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show) - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Performing rename on " + self.show.name) - - try: - show_loc = self.show.location - except exceptions.ShowDirNotFoundException: - logger.log(u"Can't perform rename on " + self.show.name + " when the show dir is missing.", logger.WARNING) - return - - ep_obj_rename_list = [] - - ep_obj_list = self.show.getAllEpisodes(has_location=True) - for cur_ep_obj in ep_obj_list: - # Only want to rename if we have a location - if cur_ep_obj.location: - if cur_ep_obj.relatedEps: - # do we have one of multi-episodes in the rename list already - have_already = False - for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: - if cur_related_ep in ep_obj_rename_list: - have_already = True - break - if not have_already: - ep_obj_rename_list.append(cur_ep_obj) - - else: - ep_obj_rename_list.append(cur_ep_obj) - - for cur_ep_obj in ep_obj_rename_list: - cur_ep_obj.rename() - - self.inProgress = False - -class QueueItemSubtitle(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.SUBTITLE, show) - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Downloading subtitles for "+self.show.name) - - self.show.downloadSubtitles() - - self.inProgress = False - - -class QueueItemUpdate(ShowQueueItem): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show) - self.force = False - - def execute(self): - - ShowQueueItem.execute(self) - - logger.log(u"Beginning update of " + self.show.name) - - logger.log(u"Retrieving show info from TVDB", logger.DEBUG) - try: - self.show.loadFromTVDB(cache=not self.force) - except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to contact TVDB, aborting: " + ex(e), logger.WARNING) - return - - # get episode list from DB - logger.log(u"Loading all episodes from the database", logger.DEBUG) - DBEpList = self.show.loadEpisodesFromDB() - - # get episode list from TVDB - logger.log(u"Loading all episodes from theTVDB", logger.DEBUG) - try: - TVDBEpList = self.show.loadEpisodesFromTVDB(cache=not self.force) - except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: " + ex(e), logger.ERROR) - TVDBEpList = None - - if TVDBEpList == None: - logger.log(u"No data returned from TVDB, unable to update this show", logger.ERROR) - - else: - - # for each ep we found on TVDB delete it from the DB list - for curSeason in TVDBEpList: - for curEpisode in TVDBEpList[curSeason]: - logger.log(u"Removing " + str(curSeason) + "x" + str(curEpisode) + " from the DB list", logger.DEBUG) - if curSeason in DBEpList and curEpisode in DBEpList[curSeason]: - del DBEpList[curSeason][curEpisode] - - # for the remaining episodes in the DB list just delete them from the DB - for curSeason in DBEpList: - for curEpisode in DBEpList[curSeason]: - logger.log(u"Permanently deleting episode " + str(curSeason) + "x" + str(curEpisode) + " from the database", logger.MESSAGE) - curEp = self.show.getEpisode(curSeason, curEpisode) - try: - curEp.deleteEpisode() - except exceptions.EpisodeDeletedException: - pass - - # now that we've updated the DB from TVDB see if there's anything we can add from TVRage - with self.show.lock: - logger.log(u"Attempting to supplement show info with info from TVRage", logger.DEBUG) - self.show.loadLatestFromTVRage() - if self.show.tvrid == 0: - self.show.setTVRID() - - sickbeard.showQueueScheduler.action.refreshShow(self.show, True) #@UndefinedVariable - - -class QueueItemForceUpdate(QueueItemUpdate): - def __init__(self, show=None): - ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) - self.force = True +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import traceback + +import sickbeard + +from lib.tvdb_api import tvdb_exceptions, tvdb_api +from lib.imdb import _exceptions as imdb_exceptions + +from sickbeard.common import SKIPPED, WANTED + +from sickbeard.tv import TVShow +from sickbeard import exceptions, logger, ui, db +from sickbeard import generic_queue +from sickbeard import name_cache +from sickbeard.exceptions import ex + +class ShowQueue(generic_queue.GenericQueue): + + def __init__(self): + generic_queue.GenericQueue.__init__(self) + self.queue_name = "SHOWQUEUE" + + + def _isInQueue(self, show, actions): + return show in [x.show for x in self.queue if x.action_id in actions] + + def _isBeingSomethinged(self, show, actions): + return self.currentItem != None and show == self.currentItem.show and \ + self.currentItem.action_id in actions + + def isInUpdateQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) + + def isInRefreshQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.REFRESH,)) + + def isInRenameQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.RENAME,)) + + def isInSubtitleQueue(self, show): + return self._isInQueue(show, (ShowQueueActions.SUBTITLE,)) + + def isBeingAdded(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.ADD,)) + + def isBeingUpdated(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) + + def isBeingRefreshed(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.REFRESH,)) + + def isBeingRenamed(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.RENAME,)) + + def isBeingSubtitled(self, show): + return self._isBeingSomethinged(show, (ShowQueueActions.SUBTITLE,)) + + def _getLoadingShowList(self): + return [x for x in self.queue + [self.currentItem] if x != None and x.isLoading] + + loadingShowList = property(_getLoadingShowList) + + def updateShow(self, show, force=False): + + if self.isBeingAdded(show): + raise exceptions.CantUpdateException("Show is still being added, wait until it is finished before you update.") + + if self.isBeingUpdated(show): + raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") + + if self.isInUpdateQueue(show): + raise exceptions.CantUpdateException("This show is already being updated, can't update again until it's done.") + + if not force: + queueItemObj = QueueItemUpdate(show) + else: + queueItemObj = QueueItemForceUpdate(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def refreshShow(self, show, force=False): + + if self.isBeingRefreshed(show) and not force: + raise exceptions.CantRefreshException("This show is already being refreshed, not refreshing again.") + + if (self.isBeingUpdated(show) or self.isInUpdateQueue(show)) and not force: + logger.log(u"A refresh was attempted but there is already an update queued or in progress. Since updates do a refres at the end anyway I'm skipping this request.", logger.DEBUG) + return + + queueItemObj = QueueItemRefresh(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def renameShowEpisodes(self, show, force=False): + + queueItemObj = QueueItemRename(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def downloadSubtitles(self, show, force=False): + + queueItemObj = QueueItemSubtitle(show) + + self.add_item(queueItemObj) + + return queueItemObj + + def addShow(self, tvdb_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="fr", subtitles=None, audio_lang=None): + queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, flatten_folders, lang, subtitles, audio_lang) + + self.add_item(queueItemObj) + + return queueItemObj + + +class ShowQueueActions: + REFRESH = 1 + ADD = 2 + UPDATE = 3 + FORCEUPDATE = 4 + RENAME = 5 + SUBTITLE=6 + + names = {REFRESH: 'Refresh', + ADD: 'Add', + UPDATE: 'Update', + FORCEUPDATE: 'Force Update', + RENAME: 'Rename', + SUBTITLE: 'Subtitle', + } + +class ShowQueueItem(generic_queue.QueueItem): + """ + Represents an item in the queue waiting to be executed + + Can be either: + - show being added (may or may not be associated with a show object) + - show being refreshed + - show being updated + - show being force updated + - show being subtitled + """ + def __init__(self, action_id, show): + generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id) + self.show = show + + def isInQueue(self): + return self in sickbeard.showQueueScheduler.action.queue + [sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable + + def _getName(self): + return str(self.show.tvdbid) + + def _isLoading(self): + return False + + show_name = property(_getName) + + isLoading = property(_isLoading) + + +class QueueItemAdd(ShowQueueItem): + def __init__(self, tvdb_id, showDir, default_status, quality, flatten_folders, lang, subtitles, audio_lang): + + self.tvdb_id = tvdb_id + self.showDir = showDir + self.default_status = default_status + self.quality = quality + self.flatten_folders = flatten_folders + self.lang = lang + self.audio_lang = audio_lang + self.subtitles = subtitles + + self.show = None + + # this will initialize self.show to None + ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show) + + def _getName(self): + """ + Returns the show name if there is a show object created, if not returns + the dir that the show is being added to. + """ + if self.show == None: + return self.showDir + return self.show.name + + show_name = property(_getName) + + def _isLoading(self): + """ + Returns True if we've gotten far enough to have a show object, or False + if we still only know the folder name. + """ + if self.show == None: + return True + return False + + isLoading = property(_isLoading) + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Starting to add show " + self.showDir) + + try: + # make sure the tvdb ids are valid + try: + ltvdb_api_parms = sickbeard.TVDB_API_PARMS.copy() + if self.lang: + ltvdb_api_parms['language'] = self.lang + + logger.log(u"TVDB: " + repr(ltvdb_api_parms)) + + t = tvdb_api.Tvdb(**ltvdb_api_parms) + s = t[self.tvdb_id] + + # this usually only happens if they have an NFO in their show dir which gave us a TVDB ID that has no proper english version of the show + if not s['seriesname']: + logger.log(u"Show in " + self.showDir + " has no name on TVDB, probably the wrong language used to search with.", logger.ERROR) + ui.notifications.error("Unable to add show", "Show in " + self.showDir + " has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") + self._finishEarly() + return + # if the show has no episodes/seasons + if not s: + logger.log(u"Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.", logger.ERROR) + ui.notifications.error("Unable to add show", "Show " + str(s['seriesname']) + " is on TVDB but contains no season/episode data.") + self._finishEarly() + return + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Error contacting TVDB: " + ex(e), logger.ERROR) + ui.notifications.error("Unable to add show", "Unable to look up the show in " + self.showDir + " on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") + self._finishEarly() + return + + # clear the name cache + name_cache.clearCache() + + newShow = TVShow(self.tvdb_id, self.lang, self.audio_lang) + newShow.loadFromTVDB() + + self.show = newShow + + # set up initial values + self.show.location = self.showDir + self.show.subtitles = self.subtitles if self.subtitles != None else sickbeard.SUBTITLES_DEFAULT + self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT + self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT + self.show.paused = 0 + + # be smartish about this + if self.show.genre and "talk show" in self.show.genre.lower(): + self.show.air_by_date = 1 + + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Unable to add show due to an error with TVDB: " + ex(e), logger.ERROR) + if self.show: + ui.notifications.error("Unable to add " + str(self.show.name) + " due to an error with TVDB") + else: + ui.notifications.error("Unable to add show due to an error with TVDB") + self._finishEarly() + return + + except exceptions.MultipleShowObjectsException: + logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) + ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") + self._finishEarly() + return + + except Exception, e: + logger.log(u"Error trying to add show: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + self._finishEarly() + raise + + logger.log(u"Retrieving show info from IMDb", logger.DEBUG) + try: + self.show.loadIMDbInfo() + except imdb_exceptions.IMDbError, e: + #todo Insert UI notification + logger.log(u" Something wrong on IMDb api: " + ex(e), logger.WARNING) + except imdb_exceptions.IMDbParserError, e: + logger.log(u" IMDb_api parser error: " + ex(e), logger.WARNING) + except Exception, e: + logger.log(u"Error loading IMDb info: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + # add it to the show list + sickbeard.showList.append(self.show) + + try: + self.show.loadEpisodesFromDir() + except Exception, e: + logger.log(u"Error searching dir for episodes: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + try: + self.show.loadEpisodesFromTVDB() + self.show.setTVRID() + + self.show.writeMetadata() + self.show.populateCache() + + except Exception, e: + logger.log(u"Error with TVDB, not creating episode list: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + try: + self.show.saveToDB() + except Exception, e: + logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + # if they gave a custom status then change all the eps to it + if self.default_status != SKIPPED: + logger.log(u"Setting all episodes to the specified default status: " + str(self.default_status)) + myDB = db.DBConnection() + myDB.action("UPDATE tv_episodes SET status = ? WHERE status = ? AND showid = ? AND season != 0", [self.default_status, SKIPPED, self.show.tvdbid]) + + # if they started with WANTED eps then run the backlog + if self.default_status == WANTED: + logger.log(u"Launching backlog for this show since its episodes are WANTED") + sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable + + self.show.flushEpisodes() + + # if there are specific episodes that need to be added by trakt + sickbeard.traktWatchListCheckerSchedular.action.manageNewShow(self.show) + + self.finish() + + def _finishEarly(self): + if self.show != None: + self.show.deleteShow() + + self.finish() + + +class QueueItemRefresh(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show) + + # do refreshes first because they're quick + self.priority = generic_queue.QueuePriorities.HIGH + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Performing refresh on " + self.show.name) + + self.show.refreshDir() + self.show.writeMetadata() + self.show.populateCache() + + self.inProgress = False + + +class QueueItemRename(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show) + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Performing rename on " + self.show.name) + + try: + show_loc = self.show.location + except exceptions.ShowDirNotFoundException: + logger.log(u"Can't perform rename on " + self.show.name + " when the show dir is missing.", logger.WARNING) + return + + ep_obj_rename_list = [] + + ep_obj_list = self.show.getAllEpisodes(has_location=True) + for cur_ep_obj in ep_obj_list: + # Only want to rename if we have a location + if cur_ep_obj.location: + if cur_ep_obj.relatedEps: + # do we have one of multi-episodes in the rename list already + have_already = False + for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: + if cur_related_ep in ep_obj_rename_list: + have_already = True + break + if not have_already: + ep_obj_rename_list.append(cur_ep_obj) + + else: + ep_obj_rename_list.append(cur_ep_obj) + + for cur_ep_obj in ep_obj_rename_list: + cur_ep_obj.rename() + + self.inProgress = False + +class QueueItemSubtitle(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.SUBTITLE, show) + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Downloading subtitles for "+self.show.name) + + self.show.downloadSubtitles() + + self.inProgress = False + + +class QueueItemUpdate(ShowQueueItem): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show) + self.force = False + + def execute(self): + + ShowQueueItem.execute(self) + + logger.log(u"Beginning update of " + self.show.name) + + logger.log(u"Retrieving show info from TVDB", logger.DEBUG) + try: + self.show.loadFromTVDB(cache=not self.force) + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB, aborting: " + ex(e), logger.WARNING) + return + + logger.log(u"Retrieving show info from IMDb", logger.DEBUG) + try: + self.show.loadIMDbInfo() + except imdb_exceptions.IMDbError, e: + logger.log(u" Something wrong on IMDb api: " + ex(e), logger.WARNING) + except imdb_exceptions.IMDbParserError, e: + logger.log(u" IMDb api parser error: " + ex(e), logger.WARNING) + except Exception, e: + logger.log(u"Error loading IMDb info: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + try: + self.show.saveToDB() + except Exception, e: + logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + # get episode list from DB + logger.log(u"Loading all episodes from the database", logger.DEBUG) + DBEpList = self.show.loadEpisodesFromDB() + + # get episode list from TVDB + logger.log(u"Loading all episodes from theTVDB", logger.DEBUG) + try: + TVDBEpList = self.show.loadEpisodesFromTVDB(cache=not self.force) + except tvdb_exceptions.tvdb_exception, e: + logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: " + ex(e), logger.ERROR) + TVDBEpList = None + + if TVDBEpList == None: + logger.log(u"No data returned from TVDB, unable to update this show", logger.ERROR) + + else: + + # for each ep we found on TVDB delete it from the DB list + for curSeason in TVDBEpList: + for curEpisode in TVDBEpList[curSeason]: + logger.log(u"Removing " + str(curSeason) + "x" + str(curEpisode) + " from the DB list", logger.DEBUG) + if curSeason in DBEpList and curEpisode in DBEpList[curSeason]: + del DBEpList[curSeason][curEpisode] + + # for the remaining episodes in the DB list just delete them from the DB + for curSeason in DBEpList: + for curEpisode in DBEpList[curSeason]: + logger.log(u"Permanently deleting episode " + str(curSeason) + "x" + str(curEpisode) + " from the database", logger.MESSAGE) + curEp = self.show.getEpisode(curSeason, curEpisode) + try: + curEp.deleteEpisode() + except exceptions.EpisodeDeletedException: + pass + + # now that we've updated the DB from TVDB see if there's anything we can add from TVRage + with self.show.lock: + logger.log(u"Attempting to supplement show info with info from TVRage", logger.DEBUG) + self.show.loadLatestFromTVRage() + if self.show.tvrid == 0: + self.show.setTVRID() + + sickbeard.showQueueScheduler.action.refreshShow(self.show, True) #@UndefinedVariable + + +class QueueItemForceUpdate(QueueItemUpdate): + def __init__(self, show=None): + ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) + self.force = True diff --git a/sickbeard/tv.py b/sickbeard/tv.py index cac6be7e3332dc5dbdaec920ca88240fe273698c..32debdd08410c2e6c296359140341a0911a32b47 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -23,6 +23,7 @@ import datetime import threading import re import glob +import traceback import sickbeard @@ -34,6 +35,8 @@ from lib import subliminal from lib.tvdb_api import tvdb_api, tvdb_exceptions +from lib.imdb import imdb + from sickbeard import db from sickbeard import helpers, exceptions, logger from sickbeard.exceptions import ex @@ -50,6 +53,7 @@ from common import Quality, Overview from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_LIMITED_EXTEND, NAMING_SEPARATED_REPEAT, NAMING_LIMITED_EXTEND_E_PREFIXED + class TVShow(object): def __init__ (self, tvdbid, lang="", audio_lang=""): @@ -60,9 +64,11 @@ class TVShow(object): self.name = "" self.tvrid = 0 self.tvrname = "" + self.imdbid = "" self.network = "" self.genre = "" self.runtime = 0 + self.imdb_info = {} self.quality = int(sickbeard.QUALITY_DEFAULT) self.flatten_folders = int(sickbeard.FLATTEN_FOLDERS_DEFAULT) @@ -71,7 +77,7 @@ class TVShow(object): self.startyear = 0 self.paused = 0 self.air_by_date = 0 - self.subtitles = int(sickbeard.SUBTITLES_DEFAULT) + self.subtitles = int(sickbeard.SUBTITLES_DEFAULT if sickbeard.SUBTITLES_DEFAULT else 0) self.lang = lang self.audio_lang = audio_lang self.custom_search_names = "" @@ -283,7 +289,7 @@ class TVShow(object): logger.log(str(self.tvdbid) + ": Could not refresh subtitles", logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) curEpisode.saveToDB() - + def loadEpisodesFromDB(self): @@ -639,6 +645,18 @@ class TVShow(object): if self.custom_search_names == "": self.custom_search_names = sqlResults[0]["custom_search_names"] + + if self.imdbid == "": + self.imdbid = sqlResults[0]["imdb_id"] + + #Get IMDb_info from database + sqlResults = myDB.select("SELECT * FROM imdb_info WHERE tvdb_id = ?", [self.tvdbid]) + + if len(sqlResults) == 0: + logger.log(str(self.tvdbid) + ": Unable to find IMDb show info in the database") + return + else: + self.imdb_info = dict(zip(sqlResults[0].keys(), sqlResults[0])) def loadFromTVDB(self, cache=True, tvapi=None, cachedSeason=None): @@ -666,6 +684,8 @@ class TVShow(object): self.genre = myEp['genre'] self.network = myEp['network'] + self.runtime = myEp['runtime'] + self.imdbid = myEp['imdb_id'] if myEp["airs_dayofweek"] != None and myEp["airs_time"] != None: self.airs = myEp["airs_dayofweek"] + " " + myEp["airs_time"] @@ -682,9 +702,76 @@ class TVShow(object): if self.status == None: self.status = "" - self.saveToDB() - - +# self.saveToDB() + + def loadIMDbInfo(self, imdbapi=None): + + imdb_info = {'imdb_id' : self.imdbid, + 'title' : '', + 'year' : '', + 'akas' : [], + 'runtimes' : '', + 'genres' : [], + 'countries' : '', + 'country codes' : '', + 'certificates' : [], + 'rating' : '', + 'votes': '', + 'last_update': '' + } + + if self.imdbid: + + logger.log(str(self.tvdbid) + ": Loading show info from IMDb") + + i = imdb.IMDb() + imdbTv = i.get_movie(str(self.imdbid[2:])) + + for key in filter(lambda x: x in imdbTv.keys(), imdb_info.keys()): + # Store only the first value for string type + if type(imdb_info[key]) == type('') and type(imdbTv.get(key)) == type([]): + imdb_info[key] = imdbTv.get(key)[0] + else: + imdb_info[key] = imdbTv.get(key) + + #Filter only the value + if imdb_info['runtimes']: + imdb_info['runtimes'] = re.search('\d+',imdb_info['runtimes']).group(0) + else: + imdb_info['runtimes'] = self.runtime + + if imdb_info['akas']: + imdb_info['akas'] = '|'.join(imdb_info['akas']) + else: + imdb_info['akas'] = '' + + #Join all genres in a string + if imdb_info['genres']: + imdb_info['genres'] = '|'.join(imdb_info['genres']) + else: + imdb_info['genres'] = '' + + #Get only the production country certificate if any + if imdb_info['certificates'] and imdb_info['countries']: + dct = {} + try: + for item in imdb_info['certificates']: + dct[item.split(':')[0]] = item.split(':')[1] + + imdb_info['certificates'] = dct[imdb_info['countries']] + except: + imdb_info['certificates'] = '' + + else: + imdb_info['certificates'] = '' + + imdb_info['last_update'] = datetime.date.today().toordinal() + + #Rename dict keys without spaces for DB upsert + self.imdb_info = dict((k.replace(' ', '_'),f(v) if hasattr(v,'keys') else v) for k,v in imdb_info.items()) + + logger.log(str(self.tvdbid) + ": Obtained info from IMDb ->" + str(self.imdb_info), logger.DEBUG) + def loadNFO (self): if not os.path.isdir(self._location): @@ -772,7 +859,8 @@ class TVShow(object): myDB = db.DBConnection() myDB.action("DELETE FROM tv_episodes WHERE showid = ?", [self.tvdbid]) myDB.action("DELETE FROM tv_shows WHERE tvdb_id = ?", [self.tvdbid]) - + myDB.action("DELETE FROM imdb_info WHERE tvdb_id = ?", [self.tvdbid]) + # remove self from show list sickbeard.showList = [x for x in sickbeard.showList if x.tvdbid != self.tvdbid] @@ -872,12 +960,18 @@ class TVShow(object): "startyear": self.startyear, "tvr_name": self.tvrname, "lang": self.lang, + "imdb_id": self.imdbid, "audio_lang": self.audio_lang, "custom_search_names": self.custom_search_names } myDB.upsert("tv_shows", newValueDict, controlValueDict) - + + if self.imdbid: + controlValueDict = {"tvdb_id": self.tvdbid} + newValueDict = self.imdb_info + + myDB.upsert("imdb_info", newValueDict, controlValueDict) def __str__(self): toReturn = "" @@ -966,7 +1060,7 @@ class TVShow(object): maxBestQuality = None epStatus, curQuality = Quality.splitCompositeStatus(epStatus) - + if epStatus in (SNATCHED, SNATCHED_PROPER): return Overview.SNATCHED # if they don't want re-downloads then we call it good if they have anything @@ -978,7 +1072,7 @@ class TVShow(object): # if it's >= maxBestQuality then it's good else: return Overview.GOOD - + def dirty_setter(attr_name): def wrapper(self, val): if getattr(self, attr_name) != val: @@ -1072,7 +1166,7 @@ class TVEpisode(object): return self.refreshSubtitles() - self.subtitles_searchcount = self.subtitles_searchcount + 1 + self.subtitles_searchcount = self.subtitles_searchcount + 1 if self.subtitles_searchcount else 1 #added the if because sometime it raise an error self.subtitles_lastsearch = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.saveToDB() @@ -1686,7 +1780,7 @@ class TVEpisode(object): result_name = result_name.replace('%RN', '%S.N.S%0SE%0E.%E.N-SiCKBEARD') result_name = result_name.replace('%rn', '%s.n.s%0se%0e.%e.n-sickbeard') - result_name = result_name.replace('%RG', 'SiCKBEARD') + result_name = result_name.replace('%RG', 'SICKBEARD') result_name = result_name.replace('%rg', 'sickbeard') logger.log(u"Episode has no release name, replacing it with a generic one: "+result_name, logger.DEBUG) @@ -1760,10 +1854,10 @@ class TVEpisode(object): # add "E04" ep_string += ep_sep - + if multi == NAMING_LIMITED_EXTEND_E_PREFIXED: ep_string += 'E' - + ep_string += other_ep._format_string(ep_format.upper(), other_ep._replace_map()) if season_ep_match: @@ -1778,9 +1872,8 @@ class TVEpisode(object): result_name = result_name.replace(cur_name_group, cur_name_group_result) result_name = self._format_string(result_name, replace_map) - - logger.log(u"formatting pattern: "+pattern+" -> "+result_name, logger.DEBUG) + logger.log(u"formatting pattern: "+pattern+" -> "+result_name, logger.DEBUG) return result_name diff --git a/sickbeard/versionChecker.py b/sickbeard/versionChecker.py index 73e249c67c3997079693e94e64e0646bedd68be5..4549ae53819dbaa3aefb22fc97b7227b2cbf64b8 100644 --- a/sickbeard/versionChecker.py +++ b/sickbeard/versionChecker.py @@ -21,6 +21,7 @@ from sickbeard import version, ui from sickbeard import logger from sickbeard import scene_exceptions from sickbeard.exceptions import ex +from sickbeard import network_timezones import os, platform, shutil import subprocess, re @@ -52,6 +53,9 @@ class CheckVersion(): # refresh scene exceptions too scene_exceptions.retrieve_exceptions() + + # refresh network timezones + network_timezones.update_network_dict() def find_install_type(self): """ diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 31233ecdf771a17c9728dcea38aea46cd08e5ef3..522118f0d0c6195c30d22d4abdbddcfc3dd594c0 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -1,3526 +1,3734 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard 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. -# -# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import os.path - -import time -import urllib -import re -import threading -import datetime -import random - -from Cheetah.Template import Template -import cherrypy.lib - -import sickbeard - -from sickbeard import config, sab -from sickbeard import clients -from sickbeard import history, notifiers, processTV -from sickbeard import ui -from sickbeard import logger, helpers, exceptions, classes, db -from sickbeard import encodingKludge as ek -from sickbeard import search_queue -from sickbeard import image_cache -from sickbeard import scene_exceptions -from sickbeard import naming -from sickbeard import subtitles - -from sickbeard.providers import newznab -from sickbeard.common import Quality, Overview, statusStrings -from sickbeard.common import SNATCHED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED -from sickbeard.exceptions import ex -from sickbeard.webapi import Api - -from lib.tvdb_api import tvdb_api - -import subliminal - -try: - import json -except ImportError: - from lib import simplejson as json - -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree - -from sickbeard import browser - - -class PageTemplate (Template): - def __init__(self, *args, **KWs): - KWs['file'] = os.path.join(sickbeard.PROG_DIR, "data/interfaces/default/", KWs['file']) - super(PageTemplate, self).__init__(*args, **KWs) - self.sbRoot = sickbeard.WEB_ROOT - self.sbHttpPort = sickbeard.WEB_PORT - self.sbHttpsPort = sickbeard.WEB_PORT - self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS - if cherrypy.request.headers['Host'][0] == '[': - self.sbHost = re.match("^\[.*\]", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) - else: - self.sbHost = re.match("^[^:]+", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) - self.projectHomePage = "http://code.google.com/p/sickbeard/" - - if sickbeard.NZBS and sickbeard.NZBS_UID and sickbeard.NZBS_HASH: - logger.log(u"NZBs.org has been replaced, please check the config to configure the new provider!", logger.ERROR) - ui.notifications.error("NZBs.org Config Update", "NZBs.org has a new site. Please <a href=\""+sickbeard.WEB_ROOT+"/config/providers\">update your config</a> with the api key from <a href=\"http://nzbs.org/login\">http://nzbs.org</a> and then disable the old NZBs.org provider.") - - if "X-Forwarded-Host" in cherrypy.request.headers: - self.sbHost = cherrypy.request.headers['X-Forwarded-Host'] - if "X-Forwarded-Port" in cherrypy.request.headers: - self.sbHttpPort = cherrypy.request.headers['X-Forwarded-Port'] - self.sbHttpsPort = self.sbHttpPort - if "X-Forwarded-Proto" in cherrypy.request.headers: - self.sbHttpsEnabled = True if cherrypy.request.headers['X-Forwarded-Proto'] == 'https' else False - - logPageTitle = 'Logs & Errors' - if len(classes.ErrorViewer.errors): - logPageTitle += ' ('+str(len(classes.ErrorViewer.errors))+')' - self.logPageTitle = logPageTitle - self.sbPID = str(sickbeard.PID) - self.menu = [ - { 'title': 'Home', 'key': 'home' }, - { 'title': 'Coming Episodes', 'key': 'comingEpisodes' }, - { 'title': 'History', 'key': 'history' }, - { 'title': 'Manage', 'key': 'manage' }, - { 'title': 'Config', 'key': 'config' }, - { 'title': logPageTitle, 'key': 'errorlogs' }, - ] - -def redirect(abspath, *args, **KWs): - assert abspath[0] == '/' - raise cherrypy.HTTPRedirect(sickbeard.WEB_ROOT + abspath, *args, **KWs) - -class TVDBWebUI: - def __init__(self, config, log=None): - self.config = config - self.log = log - - def selectSeries(self, allSeries): - - searchList = ",".join([x['id'] for x in allSeries]) - showDirList = "" - for curShowDir in self.config['_showDir']: - showDirList += "showDir="+curShowDir+"&" - redirect("/home/addShows/addShow?" + showDirList + "seriesList=" + searchList) - -def _munge(string): - return unicode(string).encode('utf-8', 'xmlcharrefreplace') - -def _genericMessage(subject, message): - t = PageTemplate(file="genericMessage.tmpl") - t.submenu = HomeMenu() - t.subject = subject - t.message = message - return _munge(t) - -def _getEpisode(show, season, episode): - - if show == None or season == None or episode == None: - return "Invalid parameters" - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return "Show not in show list" - - epObj = showObj.getEpisode(int(season), int(episode)) - - if epObj == None: - return "Episode couldn't be retrieved" - - return epObj - -ManageMenu = [ - { 'title': 'Backlog Overview', 'path': 'manage/backlogOverview' }, - { 'title': 'Manage Searches', 'path': 'manage/manageSearches' }, - { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' }, - { 'title': 'Manage Missed Subtitles', 'path': 'manage/subtitleMissed' }, -] -if sickbeard.USE_SUBTITLES: - ManageMenu.append({ 'title': 'Missed Subtitle Management', 'path': 'manage/subtitleMissed' }) - -class ManageSearches: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="manage_manageSearches.tmpl") - #t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() - t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() #@UndefinedVariable - t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() #@UndefinedVariable - t.searchStatus = sickbeard.currentSearchScheduler.action.amActive #@UndefinedVariable - t.submenu = ManageMenu - - return _munge(t) - - @cherrypy.expose - def forceSearch(self): - - # force it to run the next time it looks - result = sickbeard.currentSearchScheduler.forceRun() - if result: - logger.log(u"Search forced") - ui.notifications.message('Episode search started', - 'Note: RSS feeds may not be updated if retrieved recently') - - redirect("/manage/manageSearches") - - @cherrypy.expose - def pauseBacklog(self, paused=None): - if paused == "1": - sickbeard.searchQueueScheduler.action.pause_backlog() #@UndefinedVariable - else: - sickbeard.searchQueueScheduler.action.unpause_backlog() #@UndefinedVariable - - redirect("/manage/manageSearches") - - @cherrypy.expose - def forceVersionCheck(self): - - # force a check to see if there is a new version - result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) #@UndefinedVariable - if result: - logger.log(u"Forcing version check") - - redirect("/manage/manageSearches") - - -class Manage: - - manageSearches = ManageSearches() - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="manage.tmpl") - t.submenu = ManageMenu - return _munge(t) - - @cherrypy.expose - def showEpisodeStatuses(self, tvdb_id, whichStatus): - myDB = db.DBConnection() - - status_list = [int(whichStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - - cur_show_results = myDB.select("SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN ("+','.join(['?']*len(status_list))+")", [int(tvdb_id)] + status_list) - - result = {} - for cur_result in cur_show_results: - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) - - if cur_season not in result: - result[cur_season] = {} - - result[cur_season][cur_episode] = cur_result["name"] - - return json.dumps(result) - - @cherrypy.expose - def episodeStatuses(self, whichStatus=None): - - if whichStatus: - whichStatus = int(whichStatus) - status_list = [whichStatus] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - else: - status_list = [] - - t = PageTemplate(file="manage_episodeStatuses.tmpl") - t.submenu = ManageMenu - t.whichStatus = whichStatus - - # if we have no status then this is as far as we need to go - if not status_list: - return _munge(t) - - myDB = db.DBConnection() - status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name", status_list) - - ep_counts = {} - show_names = {} - sorted_show_ids = [] - for cur_status_result in status_results: - cur_tvdb_id = int(cur_status_result["tvdb_id"]) - if cur_tvdb_id not in ep_counts: - ep_counts[cur_tvdb_id] = 1 - else: - ep_counts[cur_tvdb_id] += 1 - - show_names[cur_tvdb_id] = cur_status_result["show_name"] - if cur_tvdb_id not in sorted_show_ids: - sorted_show_ids.append(cur_tvdb_id) - - t.show_names = show_names - t.ep_counts = ep_counts - t.sorted_show_ids = sorted_show_ids - return _munge(t) - - @cherrypy.expose - def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): - - status_list = [int(oldStatus)] - if status_list[0] == SNATCHED: - status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER - - to_change = {} - - # make a list of all shows and their associated args - for arg in kwargs: - tvdb_id, what = arg.split('-') - - # we don't care about unchecked checkboxes - if kwargs[arg] != 'on': - continue - - if tvdb_id not in to_change: - to_change[tvdb_id] = [] - - to_change[tvdb_id].append(what) - - myDB = db.DBConnection() - - for cur_tvdb_id in to_change: - - # get a list of all the eps we want to change if they just said "all" - if 'all' in to_change[cur_tvdb_id]: - all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND showid = ?", status_list + [cur_tvdb_id]) - all_eps = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] - to_change[cur_tvdb_id] = all_eps - - Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) - - redirect('/manage/episodeStatuses') - - @cherrypy.expose - def showSubtitleMissed(self, tvdb_id, whichSubs): - myDB = db.DBConnection() - - cur_show_results = myDB.select("SELECT season, episode, name, subtitles FROM tv_episodes WHERE showid = ? AND season != 0 AND status LIKE '%4'", [int(tvdb_id)]) - - result = {} - for cur_result in cur_show_results: - if whichSubs == 'all': - if len(set(cur_result["subtitles"].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len(subtitles.wantedLanguages()): - continue - elif whichSubs in cur_result["subtitles"].split(','): - continue - - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) - - if cur_season not in result: - result[cur_season] = {} - - if cur_episode not in result[cur_season]: - result[cur_season][cur_episode] = {} - - result[cur_season][cur_episode]["name"] = cur_result["name"] - - result[cur_season][cur_episode]["subtitles"] = ",".join(subliminal.language.Language(subtitle).alpha2 for subtitle in cur_result["subtitles"].split(',')) if not cur_result["subtitles"] == '' else '' - - return json.dumps(result) - - @cherrypy.expose - def subtitleMissed(self, whichSubs=None): - - t = PageTemplate(file="manage_subtitleMissed.tmpl") - t.submenu = ManageMenu - t.whichSubs = whichSubs - - if not whichSubs: - return _munge(t) - - myDB = db.DBConnection() - status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id, tv_episodes.subtitles subtitles FROM tv_episodes, tv_shows WHERE tv_shows.subtitles = 1 AND tv_episodes.status LIKE '%4' AND tv_episodes.season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name") - - ep_counts = {} - show_names = {} - sorted_show_ids = [] - for cur_status_result in status_results: - if whichSubs == 'all': - if len(set(cur_status_result["subtitles"].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len(subtitles.wantedLanguages()): - continue - elif whichSubs in cur_status_result["subtitles"].split(','): - continue - - cur_tvdb_id = int(cur_status_result["tvdb_id"]) - if cur_tvdb_id not in ep_counts: - ep_counts[cur_tvdb_id] = 1 - else: - ep_counts[cur_tvdb_id] += 1 - - show_names[cur_tvdb_id] = cur_status_result["show_name"] - if cur_tvdb_id not in sorted_show_ids: - sorted_show_ids.append(cur_tvdb_id) - - t.show_names = show_names - t.ep_counts = ep_counts - t.sorted_show_ids = sorted_show_ids - return _munge(t) - - @cherrypy.expose - def downloadSubtitleMissed(self, *args, **kwargs): - - to_download = {} - - # make a list of all shows and their associated args - for arg in kwargs: - tvdb_id, what = arg.split('-') - - # we don't care about unchecked checkboxes - if kwargs[arg] != 'on': - continue - - if tvdb_id not in to_download: - to_download[tvdb_id] = [] - - to_download[tvdb_id].append(what) - - for cur_tvdb_id in to_download: - # get a list of all the eps we want to download subtitles if they just said "all" - if 'all' in to_download[cur_tvdb_id]: - myDB = db.DBConnection() - all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status LIKE '%4' AND season != 0 AND showid = ?", [cur_tvdb_id]) - to_download[cur_tvdb_id] = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] - - for epResult in to_download[cur_tvdb_id]: - season, episode = epResult.split('x'); - - show = sickbeard.helpers.findCertainShow(sickbeard.showList, int(cur_tvdb_id)) - subtitles = show.getEpisode(int(season), int(episode)).downloadSubtitles() - - - - - redirect('/manage/subtitleMissed') - - @cherrypy.expose - def backlogShow(self, tvdb_id): - - show_obj = helpers.findCertainShow(sickbeard.showList, int(tvdb_id)) - - if show_obj: - sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) #@UndefinedVariable - - redirect("/manage/backlogOverview") - - @cherrypy.expose - def backlogOverview(self): - - t = PageTemplate(file="manage_backlogOverview.tmpl") - t.submenu = ManageMenu - - myDB = db.DBConnection() - - showCounts = {} - showCats = {} - showSQLResults = {} - - for curShow in sickbeard.showList: - - epCounts = {} - epCats = {} - epCounts[Overview.SKIPPED] = 0 - epCounts[Overview.WANTED] = 0 - epCounts[Overview.QUAL] = 0 - epCounts[Overview.GOOD] = 0 - epCounts[Overview.UNAIRED] = 0 - epCounts[Overview.SNATCHED] = 0 - - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", [curShow.tvdbid]) - - for curResult in sqlResults: - - curEpCat = curShow.getOverview(int(curResult["status"])) - epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 - - showCounts[curShow.tvdbid] = epCounts - showCats[curShow.tvdbid] = epCats - showSQLResults[curShow.tvdbid] = sqlResults - - t.showCounts = showCounts - t.showCats = showCats - t.showSQLResults = showSQLResults - - return _munge(t) - - @cherrypy.expose - def massEdit(self, toEdit=None): - - t = PageTemplate(file="manage_massEdit.tmpl") - t.submenu = ManageMenu - - if not toEdit: - redirect("/manage") - - showIDs = toEdit.split("|") - showList = [] - for curID in showIDs: - curID = int(curID) - showObj = helpers.findCertainShow(sickbeard.showList, curID) - if showObj: - showList.append(showObj) - - flatten_folders_all_same = True - last_flatten_folders = None - - paused_all_same = True - last_paused = None - - quality_all_same = True - last_quality = None - - subtitles_all_same = True - last_subtitles = None - - lang_all_same = True - last_lang_metadata= None - - lang_audio_all_same = True - last_lang_audio = None - - root_dir_list = [] - - for curShow in showList: - - cur_root_dir = ek.ek(os.path.dirname, curShow._location) - if cur_root_dir not in root_dir_list: - root_dir_list.append(cur_root_dir) - - # if we know they're not all the same then no point even bothering - if paused_all_same: - # if we had a value already and this value is different then they're not all the same - if last_paused not in (curShow.paused, None): - paused_all_same = False - else: - last_paused = curShow.paused - - if flatten_folders_all_same: - if last_flatten_folders not in (None, curShow.flatten_folders): - flatten_folders_all_same = False - else: - last_flatten_folders = curShow.flatten_folders - - if quality_all_same: - if last_quality not in (None, curShow.quality): - quality_all_same = False - else: - last_quality = curShow.quality - - if subtitles_all_same: - if last_subtitles not in (None, curShow.subtitles): - subtitles_all_same = False - else: - last_subtitles = curShow.subtitles - - if lang_all_same: - if last_lang_metadata not in (None, curShow.lang): - lang_all_same = False - else: - last_lang_metadata = curShow.lang - - if lang_audio_all_same: - if last_lang_audio not in (None, curShow.audio_lang): - lang_audio_all_same = False - else: - last_lang_audio = curShow.audio_lang - - t.showList = toEdit - t.paused_value = last_paused if paused_all_same else None - t.flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None - t.quality_value = last_quality if quality_all_same else None - t.subtitles_value = last_subtitles if subtitles_all_same else None - t.root_dir_list = root_dir_list - t.lang_value = last_lang_metadata if lang_all_same else None - t.audio_value = last_lang_audio if lang_audio_all_same else None - return _munge(t) - - @cherrypy.expose - def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, subtitles=None, - anyQualities=[], bestQualities=[], tvdbLang=None, audioLang = None, toEdit=None, *args, **kwargs): - - dir_map = {} - for cur_arg in kwargs: - if not cur_arg.startswith('orig_root_dir_'): - continue - which_index = cur_arg.replace('orig_root_dir_', '') - end_dir = kwargs['new_root_dir_'+which_index] - dir_map[kwargs[cur_arg]] = end_dir - - showIDs = toEdit.split("|") - errors = [] - for curShow in showIDs: - curErrors = [] - showObj = helpers.findCertainShow(sickbeard.showList, int(curShow)) - if not showObj: - continue - - cur_root_dir = ek.ek(os.path.dirname, showObj._location) - cur_show_dir = ek.ek(os.path.basename, showObj._location) - if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: - new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) - logger.log(u"For show "+showObj.name+" changing dir from "+showObj._location+" to "+new_show_dir) - else: - new_show_dir = showObj._location - - if paused == 'keep': - new_paused = showObj.paused - else: - new_paused = True if paused == 'enable' else False - new_paused = 'on' if new_paused else 'off' - - if flatten_folders == 'keep': - new_flatten_folders = showObj.flatten_folders - else: - new_flatten_folders = True if flatten_folders == 'enable' else False - new_flatten_folders = 'on' if new_flatten_folders else 'off' - - if subtitles == 'keep': - new_subtitles = showObj.subtitles - else: - new_subtitles = True if subtitles == 'enable' else False - - new_subtitles = 'on' if new_subtitles else 'off' - - if quality_preset == 'keep': - anyQualities, bestQualities = Quality.splitQuality(showObj.quality) - - if tvdbLang == 'None': - new_lang = 'en' - else: - new_lang = tvdbLang - - if audioLang == 'None': - new_audio_lang = showObj.audio_lang; - else: - new_audio_lang = audioLang - - exceptions_list = [] - - curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, exceptions_list, new_flatten_folders, new_paused, subtitles=new_subtitles, tvdbLang=new_lang, audio_lang=new_audio_lang, custom_search_names=showObj.custom_search_names, directCall=True) - - if curErrors: - logger.log(u"Errors: "+str(curErrors), logger.ERROR) - errors.append('<b>%s:</b>\n<ul>' % showObj.name + ' '.join(['<li>%s</li>' % error for error in curErrors]) + "</ul>") - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - " ".join(errors)) - - redirect("/manage") - - @cherrypy.expose - def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toMetadata=None, toSubtitle=None): - - if toUpdate != None: - toUpdate = toUpdate.split('|') - else: - toUpdate = [] - - if toRefresh != None: - toRefresh = toRefresh.split('|') - else: - toRefresh = [] - - if toRename != None: - toRename = toRename.split('|') - else: - toRename = [] - - if toSubtitle != None: - toSubtitle = toSubtitle.split('|') - else: - toSubtitle = [] - - if toDelete != None: - toDelete = toDelete.split('|') - else: - toDelete = [] - - if toMetadata != None: - toMetadata = toMetadata.split('|') - else: - toMetadata = [] - - errors = [] - refreshes = [] - updates = [] - renames = [] - subtitles = [] - - for curShowID in set(toUpdate+toRefresh+toRename+toSubtitle+toDelete+toMetadata): - - if curShowID == '': - continue - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(curShowID)) - - if showObj == None: - continue - - if curShowID in toDelete: - showObj.deleteShow() - # don't do anything else if it's being deleted - continue - - if curShowID in toUpdate: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable - updates.append(showObj.name) - except exceptions.CantUpdateException, e: - errors.append("Unable to update show "+showObj.name+": "+ex(e)) - - # don't bother refreshing shows that were updated anyway - if curShowID in toRefresh and curShowID not in toUpdate: - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - refreshes.append(showObj.name) - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh show "+showObj.name+": "+ex(e)) - - if curShowID in toRename: - sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) #@UndefinedVariable - renames.append(showObj.name) - - if curShowID in toSubtitle: - sickbeard.showQueueScheduler.action.downloadSubtitles(showObj) #@UndefinedVariable - subtitles.append(showObj.name) - - if len(errors) > 0: - ui.notifications.error("Errors encountered", - '<br >\n'.join(errors)) - - messageDetail = "" - - if len(updates) > 0: - messageDetail += "<br /><b>Updates</b><br /><ul><li>" - messageDetail += "</li><li>".join(updates) - messageDetail += "</li></ul>" - - if len(refreshes) > 0: - messageDetail += "<br /><b>Refreshes</b><br /><ul><li>" - messageDetail += "</li><li>".join(refreshes) - messageDetail += "</li></ul>" - - if len(renames) > 0: - messageDetail += "<br /><b>Renames</b><br /><ul><li>" - messageDetail += "</li><li>".join(renames) - messageDetail += "</li></ul>" - - if len(subtitles) > 0: - messageDetail += "<br /><b>Subtitles</b><br /><ul><li>" - messageDetail += "</li><li>".join(subtitles) - messageDetail += "</li></ul>" - - if len(updates+refreshes+renames+subtitles) > 0: - ui.notifications.message("The following actions were queued:", - messageDetail) - - redirect("/manage") - - -class History: - - @cherrypy.expose - def index(self, limit=100): - - myDB = db.DBConnection() - -# sqlResults = myDB.select("SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.tvdb_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT "+str(numPerPage*(p-1))+", "+str(numPerPage)) - if limit == "0": - sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC") - else: - sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC LIMIT ?", [limit]) - - t = PageTemplate(file="history.tmpl") - t.historyResults = sqlResults - t.limit = limit - t.submenu = [ - { 'title': 'Clear History', 'path': 'history/clearHistory' }, - { 'title': 'Trim History', 'path': 'history/trimHistory' }, - { 'title': 'Trunc Episode History Links', 'path': 'history/truncEplinks' }, - ] - - return _munge(t) - - - @cherrypy.expose - def clearHistory(self): - - myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE 1=1") - ui.notifications.message('History cleared') - redirect("/history") - - - @cherrypy.expose - def trimHistory(self): - - myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE date < "+str((datetime.datetime.today()-datetime.timedelta(days=30)).strftime(history.dateFormat))) - ui.notifications.message('Removed history entries greater than 30 days old') - redirect("/history") - - - @cherrypy.expose - def truncEplinks(self): - - myDB = db.DBConnection() - nbep=myDB.select("SELECT count(*) from episode_links") - myDB.action("DELETE FROM episode_links WHERE 1=1") - messnum = str(nbep[0][0]) + ' history links deleted' - ui.notifications.message('All Episode Links Removed', messnum) - redirect("/history") - - -ConfigMenu = [ - { 'title': 'General', 'path': 'config/general/' }, - { 'title': 'Search Settings', 'path': 'config/search/' }, - { 'title': 'Search Providers', 'path': 'config/providers/' }, - { 'title': 'Subtitles Settings','path': 'config/subtitles/' }, - { 'title': 'Post Processing', 'path': 'config/postProcessing/' }, - { 'title': 'Notifications', 'path': 'config/notifications/' }, -] - -class ConfigGeneral: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_general.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveRootDirs(self, rootDirString=None): - sickbeard.ROOT_DIRS = rootDirString - sickbeard.save_config() - @cherrypy.expose - def saveAddShowDefaults(self, defaultFlattenFolders, defaultStatus, anyQualities, bestQualities, audio_lang, subtitles): - - if anyQualities: - anyQualities = anyQualities.split(',') - else: - anyQualities = [] - - if bestQualities: - bestQualities = bestQualities.split(',') - else: - bestQualities = [] - - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - - sickbeard.STATUS_DEFAULT = int(defaultStatus) - sickbeard.QUALITY_DEFAULT = int(newQuality) - sickbeard.AUDIO_SHOW_DEFAULT = str(audio_lang) - - if defaultFlattenFolders == "true": - defaultFlattenFolders = 1 - else: - defaultFlattenFolders = 0 - - sickbeard.FLATTEN_FOLDERS_DEFAULT = int(defaultFlattenFolders) - - if subtitles == "true": - subtitles = 1 - else: - subtitles = 0 - sickbeard.SUBTITLES_DEFAULT = int(subtitles) - - sickbeard.save_config() - - @cherrypy.expose - def generateKey(self): - """ Return a new randomized API_KEY - """ - - try: - from hashlib import md5 - except ImportError: - from md5 import md5 - - # Create some values to seed md5 - t = str(time.time()) - r = str(random.random()) - - # Create the md5 instance and give it the current time - m = md5(t) - - # Update the md5 instance with the random variable - m.update(r) - - # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b - logger.log(u"New API generated") - return m.hexdigest() - - @cherrypy.expose - def saveGeneral(self, log_dir=None, web_port=None, web_log=None, web_ipv6=None, - launch_browser=None, web_username=None, use_api=None, api_key=None, - web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, display_posters=None): - - results = [] - - if web_ipv6 == "on": - web_ipv6 = 1 - else: - web_ipv6 = 0 - - if web_log == "on": - web_log = 1 - else: - web_log = 0 - - if launch_browser == "on": - launch_browser = 1 - else: - launch_browser = 0 - - if display_posters == "on": - display_posters = 1 - else: - display_posters = 0 - - if version_notify == "on": - version_notify = 1 - else: - version_notify = 0 - - if not config.change_LOG_DIR(log_dir): - results += ["Unable to create directory " + os.path.normpath(log_dir) + ", log dir not changed."] - - sickbeard.LAUNCH_BROWSER = launch_browser - sickbeard.DISPLAY_POSTERS = display_posters - - sickbeard.WEB_PORT = int(web_port) - sickbeard.WEB_IPV6 = web_ipv6 - sickbeard.WEB_LOG = web_log - sickbeard.WEB_USERNAME = web_username - sickbeard.WEB_PASSWORD = web_password - - if use_api == "on": - use_api = 1 - else: - use_api = 0 - - sickbeard.USE_API = use_api - sickbeard.API_KEY = api_key - - if enable_https == "on": - enable_https = 1 - else: - enable_https = 0 - - sickbeard.ENABLE_HTTPS = enable_https - - if not config.change_HTTPS_CERT(https_cert): - results += ["Unable to create directory " + os.path.normpath(https_cert) + ", https cert dir not changed."] - - if not config.change_HTTPS_KEY(https_key): - results += ["Unable to create directory " + os.path.normpath(https_key) + ", https key dir not changed."] - - config.change_VERSION_NOTIFY(version_notify) - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/general/") - - -class ConfigSearch: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_search.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, - sab_apikey=None, sab_category=None, sab_host=None, nzbget_password=None, nzbget_category=None, nzbget_host=None, - torrent_dir=None,torrent_method=None, nzb_method=None, usenet_retention=None, search_frequency=None, download_propers=None, torrent_username=None, torrent_password=None, torrent_host=None, torrent_label=None, torrent_path=None, - torrent_ratio=None, torrent_paused=None, ignore_words=None, prefered_method=None): - - results = [] - - if not config.change_NZB_DIR(nzb_dir): - results += ["Unable to create directory " + os.path.normpath(nzb_dir) + ", dir not changed."] - - if not config.change_TORRENT_DIR(torrent_dir): - results += ["Unable to create directory " + os.path.normpath(torrent_dir) + ", dir not changed."] - - config.change_SEARCH_FREQUENCY(search_frequency) - - if download_propers == "on": - download_propers = 1 - else: - download_propers = 0 - - if use_nzbs == "on": - use_nzbs = 1 - else: - use_nzbs = 0 - - if use_torrents == "on": - use_torrents = 1 - else: - use_torrents = 0 - - if usenet_retention == None: - usenet_retention = 200 - - if ignore_words == None: - ignore_words = "" - - sickbeard.USE_NZBS = use_nzbs - sickbeard.USE_TORRENTS = use_torrents - - sickbeard.NZB_METHOD = nzb_method - sickbeard.PREFERED_METHOD = prefered_method - sickbeard.TORRENT_METHOD = torrent_method - sickbeard.USENET_RETENTION = int(usenet_retention) - - sickbeard.IGNORE_WORDS = ignore_words - - sickbeard.DOWNLOAD_PROPERS = download_propers - - sickbeard.SAB_USERNAME = sab_username - sickbeard.SAB_PASSWORD = sab_password - sickbeard.SAB_APIKEY = sab_apikey.strip() - sickbeard.SAB_CATEGORY = sab_category - - if sab_host and not re.match('https?://.*', sab_host): - sab_host = 'http://' + sab_host - - if not sab_host.endswith('/'): - sab_host = sab_host + '/' - - sickbeard.SAB_HOST = sab_host - - sickbeard.NZBGET_PASSWORD = nzbget_password - sickbeard.NZBGET_CATEGORY = nzbget_category - sickbeard.NZBGET_HOST = nzbget_host - - sickbeard.TORRENT_USERNAME = torrent_username - sickbeard.TORRENT_PASSWORD = torrent_password - sickbeard.TORRENT_LABEL = torrent_label - sickbeard.TORRENT_PATH = torrent_path - sickbeard.TORRENT_RATIO = torrent_ratio - if torrent_paused == "on": - torrent_paused = 1 - else: - torrent_paused = 0 - sickbeard.TORRENT_PAUSED = torrent_paused - - if torrent_host and not re.match('https?://.*', torrent_host): - torrent_host = 'http://' + torrent_host - - if not torrent_host.endswith('/'): - torrent_host = torrent_host + '/' - - sickbeard.TORRENT_HOST = torrent_host - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/search/") - -class ConfigPostProcessing: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config_postProcessing.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, - xbmc_data=None, xbmc__frodo__data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, - use_banner=None, keep_processed_dir=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, - move_associated_files=None, tv_download_dir=None, torrent_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): - - results = [] - - if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): - results += ["Unable to create directory " + os.path.normpath(tv_download_dir) + ", dir not changed."] - - if not config.change_TORRENT_DOWNLOAD_DIR(torrent_download_dir): - results += ["Unable to create directory " + os.path.normpath(torrent_download_dir) + ", dir not changed."] - - if use_banner == "on": - use_banner = 1 - else: - use_banner = 0 - - if process_automatically == "on": - process_automatically = 1 - else: - process_automatically = 0 - - if process_automatically_torrent == "on": - process_automatically_torrent = 1 - else: - process_automatically_torrent = 0 - - if rename_episodes == "on": - rename_episodes = 1 - else: - rename_episodes = 0 - - if keep_processed_dir == "on": - keep_processed_dir = 1 - else: - keep_processed_dir = 0 - - if move_associated_files == "on": - move_associated_files = 1 - else: - move_associated_files = 0 - - if naming_custom_abd == "on": - naming_custom_abd = 1 - else: - naming_custom_abd = 0 - - sickbeard.PROCESS_AUTOMATICALLY = process_automatically - sickbeard.PROCESS_AUTOMATICALLY_TORRENT = process_automatically_torrent - sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir - sickbeard.RENAME_EPISODES = rename_episodes - sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files - sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd - - sickbeard.metadata_provider_dict['XBMC'].set_config(xbmc_data) - sickbeard.metadata_provider_dict['XBMC (Frodo)'].set_config(xbmc__frodo__data) - sickbeard.metadata_provider_dict['MediaBrowser'].set_config(mediabrowser_data) - sickbeard.metadata_provider_dict['Synology'].set_config(synology_data) - sickbeard.metadata_provider_dict['Sony PS3'].set_config(sony_ps3_data) - sickbeard.metadata_provider_dict['WDTV'].set_config(wdtv_data) - sickbeard.metadata_provider_dict['TIVO'].set_config(tivo_data) - - if self.isNamingValid(naming_pattern, naming_multi_ep) != "invalid": - sickbeard.NAMING_PATTERN = naming_pattern - sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) - sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() - else: - results.append("You tried saving an invalid naming config, not saving your naming settings") - - if self.isNamingValid(naming_abd_pattern, None, True) != "invalid": - sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern - elif naming_custom_abd: - results.append("You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") - - sickbeard.USE_BANNER = use_banner - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/postProcessing/") - - @cherrypy.expose - def testNaming(self, pattern=None, multi=None, abd=False): - - if multi != None: - multi = int(multi) - - result = naming.test_name(pattern, multi, abd) - - result = ek.ek(os.path.join, result['dir'], result['name']) - - return result - - @cherrypy.expose - def isNamingValid(self, pattern=None, multi=None, abd=False): - if pattern == None: - return "invalid" - - # air by date shows just need one check, we don't need to worry about season folders - if abd: - is_valid = naming.check_valid_abd_naming(pattern) - require_season_folders = False - - else: - # check validity of single and multi ep cases for the whole path - is_valid = naming.check_valid_naming(pattern, multi) - - # check validity of single and multi ep cases for only the file name - require_season_folders = naming.check_force_season_folders(pattern, multi) - - if is_valid and not require_season_folders: - return "valid" - elif is_valid and require_season_folders: - return "seasonfolders" - else: - return "invalid" - - -class ConfigProviders: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="config_providers.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def canAddNewznabProvider(self, name): - - if not name: - return json.dumps({'error': 'Invalid name specified'}) - - providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - tempProvider = newznab.NewznabProvider(name, '') - - if tempProvider.getID() in providerDict: - return json.dumps({'error': 'Exists as '+providerDict[tempProvider.getID()].name}) - else: - return json.dumps({'success': tempProvider.getID()}) - - @cherrypy.expose - def saveNewznabProvider(self, name, url, key=''): - - if not name or not url: - return '0' - - if not url.endswith('/'): - url = url + '/' - - providerDict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if name in providerDict: - if not providerDict[name].default: - providerDict[name].name = name - providerDict[name].url = url - providerDict[name].key = key - - return providerDict[name].getID() + '|' + providerDict[name].configStr() - - else: - - newProvider = newznab.NewznabProvider(name, url, key) - sickbeard.newznabProviderList.append(newProvider) - return newProvider.getID() + '|' + newProvider.configStr() - - - - @cherrypy.expose - def deleteNewznabProvider(self, id): - - providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - if id not in providerDict or providerDict[id].default: - return '0' - - # delete it from the list - sickbeard.newznabProviderList.remove(providerDict[id]) - - if id in sickbeard.PROVIDER_ORDER: - sickbeard.PROVIDER_ORDER.remove(id) - - return '1' - - - @cherrypy.expose - def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None, - nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string='', - omgwtfnzbs_uid=None, omgwtfnzbs_key=None, - tvtorrents_digest=None, tvtorrents_hash=None, - torrentleech_key=None, - btn_api_key=None, - newzbin_username=None, newzbin_password=None,t411_username=None,t411_password=None, - gks_key=None, - provider_order=None): - - results = [] - - provider_str_list = provider_order.split() - provider_list = [] - - newznabProviderDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) - - finishedNames = [] - - # add all the newznab info we got into our list - for curNewznabProviderStr in newznab_string.split('!!!'): - - if not curNewznabProviderStr: - continue - - curName, curURL, curKey = curNewznabProviderStr.split('|') - - newProvider = newznab.NewznabProvider(curName, curURL, curKey) - - curID = newProvider.getID() - - # if it already exists then update it - if curID in newznabProviderDict: - newznabProviderDict[curID].name = curName - newznabProviderDict[curID].url = curURL - newznabProviderDict[curID].key = curKey - else: - sickbeard.newznabProviderList.append(newProvider) - - finishedNames.append(curID) - - # delete anything that is missing - for curProvider in sickbeard.newznabProviderList: - if curProvider.getID() not in finishedNames: - sickbeard.newznabProviderList.remove(curProvider) - - # do the enable/disable - for curProviderStr in provider_str_list: - curProvider, curEnabled = curProviderStr.split(':') - curEnabled = int(curEnabled) - - provider_list.append(curProvider) - - if curProvider == 'nzbs_r_us': - sickbeard.NZBSRUS = curEnabled - elif curProvider == 'nzbs_org_old': - sickbeard.NZBS = curEnabled - elif curProvider == 'nzbmatrix': - sickbeard.NZBMATRIX = curEnabled - elif curProvider == 'newzbin': - sickbeard.NEWZBIN = curEnabled - elif curProvider == 'bin_req': - sickbeard.BINREQ = curEnabled - elif curProvider == 'womble_s_index': - sickbeard.WOMBLE = curEnabled - elif curProvider == 'nzbx': - sickbeard.NZBX = curEnabled - elif curProvider == 'omgwtfnzbs': - sickbeard.OMGWTFNZBS = curEnabled - elif curProvider == 'ezrss': - sickbeard.EZRSS = curEnabled - elif curProvider == 'tvtorrents': - sickbeard.TVTORRENTS = curEnabled - elif curProvider == 'torrentleech': - sickbeard.TORRENTLEECH = curEnabled - elif curProvider == 'btn': - sickbeard.BTN = curEnabled - elif curProvider == 'binnewz': - sickbeard.BINNEWZ = curEnabled - elif curProvider == 't411': - sickbeard.T411 = curEnabled - elif curProvider == 'cpasbien': - sickbeard.Cpasbien = curEnabled - elif curProvider == 'kat': - sickbeard.kat = curEnabled - elif curProvider == 'piratebay': - sickbeard.THEPIRATEBAY = curEnabled - elif curProvider == 'gks': - sickbeard.GKS = curEnabled - elif curProvider in newznabProviderDict: - newznabProviderDict[curProvider].enabled = bool(curEnabled) - else: - logger.log(u"don't know what " + curProvider + " is, skipping") - - sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip() - sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip() - - sickbeard.TORRENTLEECH_KEY = torrentleech_key.strip() - - sickbeard.BTN_API_KEY = btn_api_key.strip() - - sickbeard.T411_USERNAME = t411_username - sickbeard.T411_PASSWORD = t411_password - - sickbeard.NZBSRUS_UID = nzbs_r_us_uid.strip() - sickbeard.NZBSRUS_HASH = nzbs_r_us_hash.strip() - - sickbeard.OMGWTFNZBS_UID = omgwtfnzbs_uid.strip() - sickbeard.OMGWTFNZBS_KEY = omgwtfnzbs_key.strip() - - sickbeard.GKS_KEY = gks_key.strip() - - sickbeard.PROVIDER_ORDER = provider_list - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/providers/") - - -class ConfigNotifications: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="config_notifications.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveNotifications(self, use_xbmc=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, xbmc_update_onlyfirst=None, xbmc_notify_onsubtitledownload=None, - xbmc_update_library=None, xbmc_update_full=None, xbmc_host=None, xbmc_username=None, xbmc_password=None, - use_plex=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, plex_notify_onsubtitledownload=None, plex_update_library=None, - plex_server_host=None, plex_host=None, plex_username=None, plex_password=None, - use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, - use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, - use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, twitter_notify_onsubtitledownload=None, - use_notifo=None, notifo_notify_onsnatch=None, notifo_notify_ondownload=None, notifo_notify_onsubtitledownload=None, notifo_username=None, notifo_apisecret=None, - use_boxcar=None, boxcar_notify_onsnatch=None, boxcar_notify_ondownload=None, boxcar_notify_onsubtitledownload=None, boxcar_username=None, - use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, pushover_notify_onsubtitledownload=None, pushover_userkey=None, - use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, libnotify_notify_onsubtitledownload=None, - use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, - use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, - use_trakt=None, trakt_username=None, trakt_password=None, trakt_api=None,trakt_remove_watchlist=None,trakt_use_watchlist=None,trakt_start_paused=None,trakt_method_add=None, - use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, pytivo_notify_onsubtitledownload=None, pytivo_update_library=None, - pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, - use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, - use_mail=None, mail_username=None, mail_password=None, mail_server=None, mail_ssl=None, mail_from=None, mail_to=None, mail_notify_onsnatch=None ): - - - - results = [] - - if xbmc_notify_onsnatch == "on": - xbmc_notify_onsnatch = 1 - else: - xbmc_notify_onsnatch = 0 - - if xbmc_notify_ondownload == "on": - xbmc_notify_ondownload = 1 - else: - xbmc_notify_ondownload = 0 - - if xbmc_notify_onsubtitledownload == "on": - xbmc_notify_onsubtitledownload = 1 - else: - xbmc_notify_onsubtitledownload = 0 - - if xbmc_update_library == "on": - xbmc_update_library = 1 - else: - xbmc_update_library = 0 - - if xbmc_update_full == "on": - xbmc_update_full = 1 - else: - xbmc_update_full = 0 - - if xbmc_update_onlyfirst == "on": - xbmc_update_onlyfirst = 1 - else: - xbmc_update_onlyfirst = 0 - - if use_xbmc == "on": - use_xbmc = 1 - else: - use_xbmc = 0 - - if plex_update_library == "on": - plex_update_library = 1 - else: - plex_update_library = 0 - - if plex_notify_onsnatch == "on": - plex_notify_onsnatch = 1 - else: - plex_notify_onsnatch = 0 - - if plex_notify_ondownload == "on": - plex_notify_ondownload = 1 - else: - plex_notify_ondownload = 0 - - if plex_notify_onsubtitledownload == "on": - plex_notify_onsubtitledownload = 1 - else: - plex_notify_onsubtitledownload = 0 - - if use_plex == "on": - use_plex = 1 - else: - use_plex = 0 - - if growl_notify_onsnatch == "on": - growl_notify_onsnatch = 1 - else: - growl_notify_onsnatch = 0 - - if growl_notify_ondownload == "on": - growl_notify_ondownload = 1 - else: - growl_notify_ondownload = 0 - - if growl_notify_onsubtitledownload == "on": - growl_notify_onsubtitledownload = 1 - else: - growl_notify_onsubtitledownload = 0 - - if use_growl == "on": - use_growl = 1 - else: - use_growl = 0 - - if prowl_notify_onsnatch == "on": - prowl_notify_onsnatch = 1 - else: - prowl_notify_onsnatch = 0 - - if prowl_notify_ondownload == "on": - prowl_notify_ondownload = 1 - else: - prowl_notify_ondownload = 0 - - if prowl_notify_onsubtitledownload == "on": - prowl_notify_onsubtitledownload = 1 - else: - prowl_notify_onsubtitledownload = 0 - - if use_prowl == "on": - use_prowl = 1 - else: - use_prowl = 0 - - if twitter_notify_onsnatch == "on": - twitter_notify_onsnatch = 1 - else: - twitter_notify_onsnatch = 0 - - if twitter_notify_ondownload == "on": - twitter_notify_ondownload = 1 - else: - twitter_notify_ondownload = 0 - - if twitter_notify_onsubtitledownload == "on": - twitter_notify_onsubtitledownload = 1 - else: - twitter_notify_onsubtitledownload = 0 - - if use_twitter == "on": - use_twitter = 1 - else: - use_twitter = 0 - - if notifo_notify_onsnatch == "on": - notifo_notify_onsnatch = 1 - else: - notifo_notify_onsnatch = 0 - - if notifo_notify_ondownload == "on": - notifo_notify_ondownload = 1 - else: - notifo_notify_ondownload = 0 - - if notifo_notify_onsubtitledownload == "on": - notifo_notify_onsubtitledownload = 1 - else: - notifo_notify_onsubtitledownload = 0 - - if use_notifo == "on": - use_notifo = 1 - else: - use_notifo = 0 - - if boxcar_notify_onsnatch == "on": - boxcar_notify_onsnatch = 1 - else: - boxcar_notify_onsnatch = 0 - - if boxcar_notify_ondownload == "on": - boxcar_notify_ondownload = 1 - else: - boxcar_notify_ondownload = 0 - - if boxcar_notify_onsubtitledownload == "on": - boxcar_notify_onsubtitledownload = 1 - else: - boxcar_notify_onsubtitledownload = 0 - - if use_boxcar == "on": - use_boxcar = 1 - else: - use_boxcar = 0 - - if pushover_notify_onsnatch == "on": - pushover_notify_onsnatch = 1 - else: - pushover_notify_onsnatch = 0 - - if pushover_notify_ondownload == "on": - pushover_notify_ondownload = 1 - else: - pushover_notify_ondownload = 0 - - if pushover_notify_onsubtitledownload == "on": - pushover_notify_onsubtitledownload = 1 - else: - pushover_notify_onsubtitledownload = 0 - - if use_pushover == "on": - use_pushover = 1 - else: - use_pushover = 0 - - if use_nmj == "on": - use_nmj = 1 - else: - use_nmj = 0 - - if use_synoindex == "on": - use_synoindex = 1 - else: - use_synoindex = 0 - - if use_nmjv2 == "on": - use_nmjv2 = 1 - else: - use_nmjv2 = 0 - - if use_trakt == "on": - use_trakt = 1 - else: - use_trakt = 0 - if trakt_remove_watchlist == "on": - trakt_remove_watchlist = 1 - else: - trakt_remove_watchlist = 0 - - if trakt_use_watchlist == "on": - trakt_use_watchlist = 1 - else: - trakt_use_watchlist = 0 - - if trakt_start_paused == "on": - trakt_start_paused = 1 - else: - trakt_start_paused = 0 - - if use_pytivo == "on": - use_pytivo = 1 - else: - use_pytivo = 0 - - if pytivo_notify_onsnatch == "on": - pytivo_notify_onsnatch = 1 - else: - pytivo_notify_onsnatch = 0 - - if pytivo_notify_ondownload == "on": - pytivo_notify_ondownload = 1 - else: - pytivo_notify_ondownload = 0 - - if pytivo_notify_onsubtitledownload == "on": - pytivo_notify_onsubtitledownload = 1 - else: - pytivo_notify_onsubtitledownload = 0 - - if pytivo_update_library == "on": - pytivo_update_library = 1 - else: - pytivo_update_library = 0 - - if use_nma == "on": - use_nma = 1 - else: - use_nma = 0 - - if nma_notify_onsnatch == "on": - nma_notify_onsnatch = 1 - else: - nma_notify_onsnatch = 0 - - if nma_notify_ondownload == "on": - nma_notify_ondownload = 1 - else: - nma_notify_ondownload = 0 - - if nma_notify_onsubtitledownload == "on": - nma_notify_onsubtitledownload = 1 - else: - nma_notify_onsubtitledownload = 0 - - if use_mail == "on": - use_mail = 1 - else: - use_mail = 0 - - if mail_ssl == "on": - mail_ssl = 1 - else: - mail_ssl = 0 - - if mail_notify_onsnatch == "on": - mail_notify_onsnatch = 1 - else: - mail_notify_onsnatch = 0 - - - sickbeard.USE_XBMC = use_xbmc - sickbeard.XBMC_NOTIFY_ONSNATCH = xbmc_notify_onsnatch - sickbeard.XBMC_NOTIFY_ONDOWNLOAD = xbmc_notify_ondownload - sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = xbmc_notify_onsubtitledownload - sickbeard.XBMC_UPDATE_LIBRARY = xbmc_update_library - sickbeard.XBMC_UPDATE_FULL = xbmc_update_full - sickbeard.XBMC_UPDATE_ONLYFIRST = xbmc_update_onlyfirst - sickbeard.XBMC_HOST = xbmc_host - sickbeard.XBMC_USERNAME = xbmc_username - sickbeard.XBMC_PASSWORD = xbmc_password - - sickbeard.USE_PLEX = use_plex - sickbeard.PLEX_NOTIFY_ONSNATCH = plex_notify_onsnatch - sickbeard.PLEX_NOTIFY_ONDOWNLOAD = plex_notify_ondownload - sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = plex_notify_onsubtitledownload - sickbeard.PLEX_UPDATE_LIBRARY = plex_update_library - sickbeard.PLEX_HOST = plex_host - sickbeard.PLEX_SERVER_HOST = plex_server_host - sickbeard.PLEX_USERNAME = plex_username - sickbeard.PLEX_PASSWORD = plex_password - - sickbeard.USE_GROWL = use_growl - sickbeard.GROWL_NOTIFY_ONSNATCH = growl_notify_onsnatch - sickbeard.GROWL_NOTIFY_ONDOWNLOAD = growl_notify_ondownload - sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = growl_notify_onsubtitledownload - sickbeard.GROWL_HOST = growl_host - sickbeard.GROWL_PASSWORD = growl_password - - sickbeard.USE_PROWL = use_prowl - sickbeard.PROWL_NOTIFY_ONSNATCH = prowl_notify_onsnatch - sickbeard.PROWL_NOTIFY_ONDOWNLOAD = prowl_notify_ondownload - sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = prowl_notify_onsubtitledownload - sickbeard.PROWL_API = prowl_api - sickbeard.PROWL_PRIORITY = prowl_priority - - sickbeard.USE_TWITTER = use_twitter - sickbeard.TWITTER_NOTIFY_ONSNATCH = twitter_notify_onsnatch - sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = twitter_notify_ondownload - sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = twitter_notify_onsubtitledownload - - sickbeard.USE_NOTIFO = use_notifo - sickbeard.NOTIFO_NOTIFY_ONSNATCH = notifo_notify_onsnatch - sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD = notifo_notify_ondownload - sickbeard.NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = notifo_notify_onsubtitledownload - sickbeard.NOTIFO_USERNAME = notifo_username - sickbeard.NOTIFO_APISECRET = notifo_apisecret - - sickbeard.USE_BOXCAR = use_boxcar - sickbeard.BOXCAR_NOTIFY_ONSNATCH = boxcar_notify_onsnatch - sickbeard.BOXCAR_NOTIFY_ONDOWNLOAD = boxcar_notify_ondownload - sickbeard.BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = boxcar_notify_onsubtitledownload - sickbeard.BOXCAR_USERNAME = boxcar_username - - sickbeard.USE_PUSHOVER = use_pushover - sickbeard.PUSHOVER_NOTIFY_ONSNATCH = pushover_notify_onsnatch - sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = pushover_notify_ondownload - sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = pushover_notify_onsubtitledownload - sickbeard.PUSHOVER_USERKEY = pushover_userkey - - sickbeard.USE_LIBNOTIFY = use_libnotify == "on" - sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = libnotify_notify_onsnatch == "on" - sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = libnotify_notify_ondownload == "on" - sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = libnotify_notify_onsubtitledownload == "on" - - sickbeard.USE_NMJ = use_nmj - sickbeard.NMJ_HOST = nmj_host - sickbeard.NMJ_DATABASE = nmj_database - sickbeard.NMJ_MOUNT = nmj_mount - - sickbeard.USE_SYNOINDEX = use_synoindex - - sickbeard.USE_NMJv2 = use_nmjv2 - sickbeard.NMJv2_HOST = nmjv2_host - sickbeard.NMJv2_DATABASE = nmjv2_database - sickbeard.NMJv2_DBLOC = nmjv2_dbloc - - sickbeard.USE_TRAKT = use_trakt - sickbeard.TRAKT_USERNAME = trakt_username - sickbeard.TRAKT_PASSWORD = trakt_password - sickbeard.TRAKT_API = trakt_api - sickbeard.TRAKT_REMOVE_WATCHLIST = trakt_remove_watchlist - sickbeard.TRAKT_USE_WATCHLIST = trakt_use_watchlist - sickbeard.TRAKT_METHOD_ADD = trakt_method_add - sickbeard.TRAKT_START_PAUSED = trakt_start_paused - - sickbeard.USE_PYTIVO = use_pytivo - sickbeard.PYTIVO_NOTIFY_ONSNATCH = pytivo_notify_onsnatch == "off" - sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = pytivo_notify_ondownload == "off" - sickbeard.PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = pytivo_notify_onsubtitledownload == "off" - sickbeard.PYTIVO_UPDATE_LIBRARY = pytivo_update_library - sickbeard.PYTIVO_HOST = pytivo_host - sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name - sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name - - sickbeard.USE_NMA = use_nma - sickbeard.NMA_NOTIFY_ONSNATCH = nma_notify_onsnatch - sickbeard.NMA_NOTIFY_ONDOWNLOAD = nma_notify_ondownload - sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD = nma_notify_onsubtitledownload - sickbeard.NMA_API = nma_api - sickbeard.NMA_PRIORITY = nma_priority - - sickbeard.USE_MAIL = use_mail - sickbeard.MAIL_USERNAME = mail_username - sickbeard.MAIL_PASSWORD = mail_password - sickbeard.MAIL_SERVER = mail_server - sickbeard.MAIL_SSL = mail_ssl - sickbeard.MAIL_FROM = mail_from - sickbeard.MAIL_TO = mail_to - sickbeard.MAIL_NOTIFY_ONSNATCH = mail_notify_onsnatch - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/notifications/") - -class ConfigSubtitles: - - @cherrypy.expose - def index(self): - t = PageTemplate(file="config_subtitles.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - @cherrypy.expose - def saveSubtitles(self, use_subtitles=None, subtitles_plugins=None, subtitles_languages=None, subtitles_dir=None, subtitles_dir_sub=None, subsnolang = None, service_order=None, subtitles_history=None): - results = [] - - if use_subtitles == "on": - use_subtitles = 1 - if sickbeard.subtitlesFinderScheduler.thread == None or not sickbeard.subtitlesFinderScheduler.thread.isAlive(): - sickbeard.subtitlesFinderScheduler.initThread() - else: - use_subtitles = 0 - sickbeard.subtitlesFinderScheduler.abort = True - logger.log(u"Waiting for the SUBTITLESFINDER thread to exit") - try: - sickbeard.subtitlesFinderScheduler.thread.join(5) - except: - pass - - if subtitles_history == "on": - subtitles_history = 1 - else: - subtitles_history = 0 - - if subtitles_dir_sub == "on": - subtitles_dir_sub = 1 - else: - subtitles_dir_sub = 0 - - if subsnolang == "on": - subsnolang = 1 - else: - subsnolang = 0 - - sickbeard.USE_SUBTITLES = use_subtitles - sickbeard.SUBTITLES_LANGUAGES = [lang.alpha2 for lang in subtitles.isValidLanguage(subtitles_languages.replace(' ', '').split(','))] if subtitles_languages != '' else '' - sickbeard.SUBTITLES_DIR = subtitles_dir - sickbeard.SUBTITLES_DIR_SUB = subtitles_dir_sub - sickbeard.SUBSNOLANG = subsnolang - sickbeard.SUBTITLES_HISTORY = subtitles_history - - # Subtitles services - services_str_list = service_order.split() - subtitles_services_list = [] - subtitles_services_enabled = [] - for curServiceStr in services_str_list: - curService, curEnabled = curServiceStr.split(':') - subtitles_services_list.append(curService) - subtitles_services_enabled.append(int(curEnabled)) - - sickbeard.SUBTITLES_SERVICES_LIST = subtitles_services_list - sickbeard.SUBTITLES_SERVICES_ENABLED = subtitles_services_enabled - - sickbeard.save_config() - - if len(results) > 0: - for x in results: - logger.log(x, logger.ERROR) - ui.notifications.error('Error(s) Saving Configuration', - '<br />\n'.join(results)) - else: - ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) - - redirect("/config/subtitles/") - -class Config: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="config.tmpl") - t.submenu = ConfigMenu - return _munge(t) - - general = ConfigGeneral() - - search = ConfigSearch() - - postProcessing = ConfigPostProcessing() - - providers = ConfigProviders() - - notifications = ConfigNotifications() - - subtitles = ConfigSubtitles() - -def haveXBMC(): - return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY - -def havePLEX(): - return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY - -def HomeMenu(): - return [ - { 'title': 'Add Shows', 'path': 'home/addShows/', }, - { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/' }, - { 'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': haveXBMC }, - { 'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': havePLEX }, - { 'title': 'Restart', 'path': 'home/restart/?pid='+str(sickbeard.PID), 'confirm': True }, - { 'title': 'Shutdown', 'path': 'home/shutdown/?pid='+str(sickbeard.PID), 'confirm': True }, - ] - -class HomePostProcess: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home_postprocess.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - @cherrypy.expose - def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None): - - if not dir: - redirect("/home/postprocess") - else: - result = processTV.processDir(dir, nzbName) - if quiet != None and int(quiet) == 1: - return result - - result = result.replace("\n","<br />\n") - return _genericMessage("Postprocessing results", result) - - -class NewHomeAddShows: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home_addShows.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - @cherrypy.expose - def getTVDBLanguages(self): - result = tvdb_api.Tvdb().config['valid_languages'] - - # Make sure list is sorted alphabetically but 'fr' is in front - if 'fr' in result: - del result[result.index('fr')] - result.sort() - result.insert(0, 'fr') - - return json.dumps({'results': result}) - - @cherrypy.expose - def sanitizeFileName(self, name): - return helpers.sanitizeFileName(name) - - @cherrypy.expose - def searchTVDBForShowName(self, name, lang="fr"): - if not lang or lang == 'null': - lang = "fr" - - baseURL = "http://thetvdb.com/api/GetSeries.php?" - nameUTF8 = name.encode('utf-8') - - logger.log(u"Trying to find Show on thetvdb.com with: " + nameUTF8.decode('utf-8'), logger.DEBUG) - - # Use each word in the show's name as a possible search term - keywords = nameUTF8.split(' ') - - # Insert the whole show's name as the first search term so best results are first - # ex: keywords = ['Some Show Name', 'Some', 'Show', 'Name'] - if len(keywords) > 1: - keywords.insert(0, nameUTF8) - - # Query the TVDB for each search term and build the list of results - results = [] - - for searchTerm in keywords: - params = {'seriesname': searchTerm, - 'language': lang} - - finalURL = baseURL + urllib.urlencode(params) - - logger.log(u"Searching for Show with searchterm: \'" + searchTerm.decode('utf-8') + u"\' on URL " + finalURL, logger.DEBUG) - urlData = helpers.getURL(finalURL) - - if urlData is None: - # When urlData is None, trouble connecting to TVDB, don't try the rest of the keywords - logger.log(u"Unable to get URL: " + finalURL, logger.ERROR) - break - else: - try: - seriesXML = etree.ElementTree(etree.XML(urlData)) - series = seriesXML.getiterator('Series') - - except Exception, e: - # use finalURL in log, because urlData can be too much information - logger.log(u"Unable to parse XML for some reason: " + ex(e) + " from XML: " + finalURL, logger.ERROR) - series = '' - - # add each result to our list - for curSeries in series: - tvdb_id = int(curSeries.findtext('seriesid')) - - # don't add duplicates - if tvdb_id in [x[0] for x in results]: - continue - - results.append((tvdb_id, curSeries.findtext('SeriesName'), curSeries.findtext('FirstAired'))) - - lang_id = tvdb_api.Tvdb().config['langabbv_to_id'][lang] - - return json.dumps({'results': results, 'langid': lang_id}) - - @cherrypy.expose - def massAddTable(self, rootDir=None): - t = PageTemplate(file="home_massAddTable.tmpl") - t.submenu = HomeMenu() - - myDB = db.DBConnection() - - if not rootDir: - return "No folders selected." - elif type(rootDir) != list: - root_dirs = [rootDir] - else: - root_dirs = rootDir - - root_dirs = [urllib.unquote_plus(x) for x in root_dirs] - - default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) - if len(root_dirs) > default_index: - tmp = root_dirs[default_index] - if tmp in root_dirs: - root_dirs.remove(tmp) - root_dirs = [tmp]+root_dirs - - dir_list = [] - - for root_dir in root_dirs: - try: - file_list = ek.ek(os.listdir, root_dir) - except: - continue - - for cur_file in file_list: - - cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) - if not ek.ek(os.path.isdir, cur_path): - continue - - cur_dir = { - 'dir': cur_path, - 'display_dir': '<b>'+ek.ek(os.path.dirname, cur_path)+os.sep+'</b>'+ek.ek(os.path.basename, cur_path), - } - - # see if the folder is in XBMC already - dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) - - if dirResults: - cur_dir['added_already'] = True - else: - cur_dir['added_already'] = False - - dir_list.append(cur_dir) - - tvdb_id = '' - show_name = '' - for cur_provider in sickbeard.metadata_provider_dict.values(): - (tvdb_id, show_name) = cur_provider.retrieveShowMetadata(cur_path) - if tvdb_id and show_name: - break - - cur_dir['existing_info'] = (tvdb_id, show_name) - - if tvdb_id and helpers.findCertainShow(sickbeard.showList, tvdb_id): - cur_dir['added_already'] = True - - t.dirList = dir_list - - return _munge(t) - - @cherrypy.expose - def newShow(self, show_to_add=None, other_shows=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - t = PageTemplate(file="home_newShow.tmpl") - t.submenu = HomeMenu() - - show_dir, tvdb_id, show_name = self.split_extra_show(show_to_add) - - if tvdb_id and show_name: - use_provided_info = True - else: - use_provided_info = False - - # tell the template whether we're giving it show name & TVDB ID - t.use_provided_info = use_provided_info - - # use the given show_dir for the tvdb search if available - if not show_dir: - t.default_show_name = '' - elif not show_name: - t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.',' ') - else: - t.default_show_name = show_name - - # carry a list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - if use_provided_info: - t.provided_tvdb_id = tvdb_id - t.provided_tvdb_name = show_name - - t.provided_show_dir = show_dir - t.other_shows = other_shows - - return _munge(t) - - @cherrypy.expose - def addNewShow(self, whichSeries=None, tvdbLang="fr", rootDir=None, defaultStatus=None, - anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, fullShowPath=None, - other_shows=None, skipShow=None, audio_lang=None): - """ - Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are - provided then it forwards back to newShow, if not it goes to /home. - """ - - # grab our list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - def finishAddShow(): - # if there are no extra shows then go home - if not other_shows: - redirect('/home') - - # peel off the next one - next_show_dir = other_shows[0] - rest_of_show_dirs = other_shows[1:] - - # go to add the next show - return self.newShow(next_show_dir, rest_of_show_dirs) - - # if we're skipping then behave accordingly - if skipShow: - return finishAddShow() - - # sanity check on our inputs - if (not rootDir and not fullShowPath) or not whichSeries: - return "Missing params, no tvdb id or folder:"+repr(whichSeries)+" and "+repr(rootDir)+"/"+repr(fullShowPath) - - # figure out what show we're adding and where - series_pieces = whichSeries.partition('|') - if len(series_pieces) < 3: - return "Error with show selection." - - tvdb_id = int(series_pieces[0]) - show_name = series_pieces[2] - - # use the whole path if it's given, or else append the show name to the root dir to get the full show path - if fullShowPath: - show_dir = ek.ek(os.path.normpath, fullShowPath) - else: - show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) - - # blanket policy - if the dir exists you should have used "add existing show" numbnuts - if ek.ek(os.path.isdir, show_dir) and not fullShowPath: - ui.notifications.error("Unable to add show", "Folder "+show_dir+" exists already") - redirect('/home/addShows/existingShows') - - # don't create show dir if config says not to - if sickbeard.ADD_SHOWS_WO_DIR: - logger.log(u"Skipping initial creation of "+show_dir+" due to config.ini setting") - else: - dir_exists = helpers.makeDir(show_dir) - if not dir_exists: - logger.log(u"Unable to create the folder "+show_dir+", can't add the show", logger.ERROR) - ui.notifications.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") - redirect("/home") - else: - helpers.chmodAsParent(show_dir) - - # prepare the inputs for passing along - if flatten_folders == "on": - flatten_folders = 1 - else: - flatten_folders = 0 - - if subtitles == "on": - subtitles = 1 - else: - subtitles = 0 - - if not anyQualities: - anyQualities = [] - if not bestQualities: - bestQualities = [] - if type(anyQualities) != list: - anyQualities = [anyQualities] - if type(bestQualities) != list: - bestQualities = [bestQualities] - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - - # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, flatten_folders, tvdbLang, subtitles, audio_lang) #@UndefinedVariable - ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) - - return finishAddShow() - - - @cherrypy.expose - def existingShows(self): - """ - Prints out the page to add existing shows from a root dir - """ - t = PageTemplate(file="home_addExistingShow.tmpl") - t.submenu = HomeMenu() - - return _munge(t) - - def split_extra_show(self, extra_show): - if not extra_show: - return (None, None, None) - split_vals = extra_show.split('|') - if len(split_vals) < 3: - return (extra_show, None, None) - show_dir = split_vals[0] - tvdb_id = split_vals[1] - show_name = '|'.join(split_vals[2:]) - - return (show_dir, tvdb_id, show_name) - - @cherrypy.expose - def addExistingShows(self, shows_to_add=None, promptForSettings=None): - """ - Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards - along to the newShow page. - """ - - # grab a list of other shows to add, if provided - if not shows_to_add: - shows_to_add = [] - elif type(shows_to_add) != list: - shows_to_add = [shows_to_add] - - shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] - - if promptForSettings == "on": - promptForSettings = 1 - else: - promptForSettings = 0 - - tvdb_id_given = [] - dirs_only = [] - # separate all the ones with TVDB IDs - for cur_dir in shows_to_add: - if not '|' in cur_dir: - dirs_only.append(cur_dir) - else: - show_dir, tvdb_id, show_name = self.split_extra_show(cur_dir) - if not show_dir or not tvdb_id or not show_name: - continue - tvdb_id_given.append((show_dir, int(tvdb_id), show_name)) - - - # if they want me to prompt for settings then I will just carry on to the newShow page - if promptForSettings and shows_to_add: - return self.newShow(shows_to_add[0], shows_to_add[1:]) - - # if they don't want me to prompt for settings then I can just add all the nfo shows now - num_added = 0 - for cur_show in tvdb_id_given: - show_dir, tvdb_id, show_name = cur_show - - # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.FLATTEN_FOLDERS_DEFAULT, sickbeard.SUBTITLES_DEFAULT) #@UndefinedVariable - num_added += 1 - - if num_added: - ui.notifications.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") - - # if we're done then go home - if not dirs_only: - redirect('/home') - - # for the remaining shows we need to prompt for each one, so forward this on to the newShow page - return self.newShow(dirs_only[0], dirs_only[1:]) - - - - -ErrorLogsMenu = [ - { 'title': 'Clear Errors', 'path': 'errorlogs/clearerrors' }, - #{ 'title': 'View Log', 'path': 'errorlogs/viewlog' }, -] - - -class ErrorLogs: - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="errorlogs.tmpl") - t.submenu = ErrorLogsMenu - - return _munge(t) - - - @cherrypy.expose - def clearerrors(self): - classes.ErrorViewer.clear() - redirect("/errorlogs") - - @cherrypy.expose - def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): - - t = PageTemplate(file="viewlogs.tmpl") - t.submenu = ErrorLogsMenu - - minLevel = int(minLevel) - - data = [] - if os.path.isfile(logger.sb_log_instance.log_file): - f = open(logger.sb_log_instance.log_file) - data = f.readlines() - f.close() - - regex = "^(\w+).?\-(\d\d)\s+(\d\d)\:(\d\d):(\d\d)\s+([A-Z]+)\s+(.*)$" - - finalData = [] - - numLines = 0 - lastLine = False - numToShow = min(maxLines, len(data)) - - for x in reversed(data): - - x = x.decode('utf-8') - match = re.match(regex, x) - - if match: - level = match.group(6) - if level not in logger.reverseNames: - lastLine = False - continue - - if logger.reverseNames[level] >= minLevel: - lastLine = True - finalData.append(x) - else: - lastLine = False - continue - - elif lastLine: - finalData.append("AA"+x) - - numLines += 1 - - if numLines >= numToShow: - break - - result = "".join(finalData) - - t.logLines = result - t.minLevel = minLevel - - return _munge(t) - - -class Home: - - @cherrypy.expose - def is_alive(self, *args, **kwargs): - if 'callback' in kwargs and '_' in kwargs: - callback, _ = kwargs['callback'], kwargs['_'] - else: - return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query stiring." - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - cherrypy.response.headers['Content-Type'] = 'text/javascript' - cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' - cherrypy.response.headers['Access-Control-Allow-Headers'] = 'x-requested-with' - - if sickbeard.started: - return callback+'('+json.dumps({"msg": str(sickbeard.PID)})+');' - else: - return callback+'('+json.dumps({"msg": "nope"})+');' - - @cherrypy.expose - def index(self): - - t = PageTemplate(file="home.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - addShows = NewHomeAddShows() - - postprocess = HomePostProcess() - - @cherrypy.expose - def testSABnzbd(self, host=None, username=None, password=None, apikey=None): - if not host.endswith("/"): - host = host + "/" - connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) - if connection: - authed, authMsg = sab.testAuthentication(host, username, password, apikey) #@UnusedVariable - if authed: - return "Success. Connected and authenticated" - else: - return "Authentication failed. SABnzbd expects '"+accesMsg+"' as authentication method" - else: - return "Unable to connect to host" - - @cherrypy.expose - def testTorrent(self, torrent_method=None, host=None, username=None, password=None): - if not host.endswith("/"): - host = host + "/" - - client = clients.getClientIstance(torrent_method) - - connection, accesMsg = client(host, username, password).testAuthentication() - - return accesMsg - - @cherrypy.expose - def testGrowl(self, host=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.growl_notifier.test_notify(host, password) - if password==None or password=='': - pw_append = '' - else: - pw_append = " with password: " + password - - if result: - return "Registered and Tested growl successfully "+urllib.unquote_plus(host)+pw_append - else: - return "Registration and Testing of growl failed "+urllib.unquote_plus(host)+pw_append - - @cherrypy.expose - def testProwl(self, prowl_api=None, prowl_priority=0): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) - if result: - return "Test prowl notice sent successfully" - else: - return "Test prowl notice failed" - - @cherrypy.expose - def testNotifo(self, username=None, apisecret=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.notifo_notifier.test_notify(username, apisecret) - if result: - return "Notifo notification succeeded. Check your Notifo clients to make sure it worked" - else: - return "Error sending Notifo notification" - - @cherrypy.expose - def testBoxcar(self, username=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.boxcar_notifier.test_notify(username) - if result: - return "Boxcar notification succeeded. Check your Boxcar clients to make sure it worked" - else: - return "Error sending Boxcar notification" - - @cherrypy.expose - def testPushover(self, userKey=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.pushover_notifier.test_notify(userKey) - if result: - return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" - else: - return "Error sending Pushover notification" - - @cherrypy.expose - def twitterStep1(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - return notifiers.twitter_notifier._get_authorization() - - @cherrypy.expose - def twitterStep2(self, key): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.twitter_notifier._get_credentials(key) - logger.log(u"result: "+str(result)) - if result: - return "Key verification successful" - else: - return "Unable to verify key" - - @cherrypy.expose - def testTwitter(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.twitter_notifier.test_notify() - if result: - return "Tweet successful, check your twitter to make sure it worked" - else: - return "Error sending tweet" - - @cherrypy.expose - def testXBMC(self, host=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test XBMC notice sent successfully to " + urllib.unquote_plus(curHost) - else: - finalResult += "Test XBMC notice failed to " + urllib.unquote_plus(curHost) - finalResult += "<br />\n" - - return finalResult - - @cherrypy.expose - def testPLEX(self, host=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test Plex notice sent successfully to " + urllib.unquote_plus(curHost) - else: - finalResult += "Test Plex notice failed to " + urllib.unquote_plus(curHost) - finalResult += "<br />\n" - - return finalResult - - @cherrypy.expose - def testLibnotify(self): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - if notifiers.libnotify_notifier.test_notify(): - return "Tried sending desktop notification via libnotify" - else: - return notifiers.libnotify.diagnose() - - @cherrypy.expose - def testNMJ(self, host=None, database=None, mount=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) - if result: - return "Successfull started the scan update" - else: - return "Test failed to start the scan update" - - @cherrypy.expose - def settingsNMJ(self, host=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) - if result: - return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % {"host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} - else: - return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - - @cherrypy.expose - def testNMJv2(self, host=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nmjv2_notifier.test_notify(urllib.unquote_plus(host)) - if result: - return "Test notice sent successfully to " + urllib.unquote_plus(host) - else: - return "Test notice failed to " + urllib.unquote_plus(host) - - @cherrypy.expose - def settingsNMJv2(self, host=None, dbloc=None, instance=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - result = notifiers.nmjv2_notifier.notify_settings(urllib.unquote_plus(host), dbloc, instance) - if result: - return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, "database": sickbeard.NMJv2_DATABASE} - else: - return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % {"dbloc": dbloc} - - @cherrypy.expose - def testTrakt(self, api=None, username=None, password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.trakt_notifier.test_notify(api, username, password) - if result: - return "Test notice sent successfully to Trakt" - else: - return "Test notice failed to Trakt" - - @cherrypy.expose - def testMail(self, mail_from=None, mail_to=None, mail_server=None, mail_ssl=None, mail_user=None, mail_password=None): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.mail_notifier.test_notify(mail_from, mail_to, mail_server, mail_ssl, mail_user, mail_password) - if result: - return "Mail sent" - else: - return "Can't sent mail." - - @cherrypy.expose - def testNMA(self, nma_api=None, nma_priority=0): - cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" - - result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) - if result: - return "Test NMA notice sent successfully" - else: - return "Test NMA notice failed" - - @cherrypy.expose - def shutdown(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - threading.Timer(2, sickbeard.invoke_shutdown).start() - - title = "Shutting down" - message = "Sick Beard is shutting down..." - - return _genericMessage(title, message) - - @cherrypy.expose - def restart(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - t = PageTemplate(file="restart.tmpl") - t.submenu = HomeMenu() - - # do a soft restart - threading.Timer(2, sickbeard.invoke_restart, [False]).start() - - return _munge(t) - - @cherrypy.expose - def update(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home") - - updated = sickbeard.versionCheckScheduler.action.update() #@UndefinedVariable - - if updated: - # do a hard restart - threading.Timer(2, sickbeard.invoke_restart, [False]).start() - t = PageTemplate(file="restart_bare.tmpl") - return _munge(t) - else: - return _genericMessage("Update Failed","Update wasn't successful, not restarting. Check your log for more information.") - - @cherrypy.expose - def displayShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Show not in show list") - - myDB = db.DBConnection() - - seasonResults = myDB.select( - "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc", - [showObj.tvdbid] - ) - - sqlResults = myDB.select( - "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", - [showObj.tvdbid] - ) - - t = PageTemplate(file="displayShow.tmpl") - t.submenu = [ { 'title': 'Edit', 'path': 'home/editShow?show=%d'%showObj.tvdbid } ] - - try: - t.showLoc = (showObj.location, True) - except sickbeard.exceptions.ShowDirNotFoundException: - t.showLoc = (showObj._location, False) - - show_message = '' - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable - show_message = 'This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.' - - elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - show_message = 'The information below is in the process of being updated.' - - elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): #@UndefinedVariable - show_message = 'The episodes below are currently being refreshed from disk' - - elif sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj): #@UndefinedVariable - show_message = 'Currently downloading subtitles for this show' - - elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): #@UndefinedVariable - show_message = 'This show is queued to be refreshed.' - - elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): #@UndefinedVariable - show_message = 'This show is queued and awaiting an update.' - - elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): #@UndefinedVariable - show_message = 'This show is queued and awaiting subtitles download.' - - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - t.submenu.append({ 'title': 'Delete', 'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True }) - t.submenu.append({ 'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d'%showObj.tvdbid }) - t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1'%showObj.tvdbid }) - t.submenu.append({ 'title': 'Update show in XBMC', 'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC }) - t.submenu.append({ 'title': 'Preview Rename', 'path': 'home/testRename?show=%d'%showObj.tvdbid }) - if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj) and showObj.subtitles: - t.submenu.append({ 'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d'%showObj.tvdbid }) - - t.show = showObj - t.sqlResults = sqlResults - t.seasonResults = seasonResults - t.show_message = show_message - - epCounts = {} - epCats = {} - epCounts[Overview.SKIPPED] = 0 - epCounts[Overview.WANTED] = 0 - epCounts[Overview.QUAL] = 0 - epCounts[Overview.GOOD] = 0 - epCounts[Overview.UNAIRED] = 0 - epCounts[Overview.SNATCHED] = 0 - - for curResult in sqlResults: - - curEpCat = showObj.getOverview(int(curResult["status"])) - epCats[str(curResult["season"])+"x"+str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 - - def titler(x): - if not x: - return x - if x.lower().startswith('a '): - x = x[2:] - elif x.lower().startswith('the '): - x = x[4:] - return x - t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) - - t.epCounts = epCounts - t.epCats = epCats - - return _munge(t) - - @cherrypy.expose - def plotDetails(self, show, season, episode): - result = db.DBConnection().action("SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", (show, season, episode)).fetchone() - return result['description'] if result else 'Episode not found.' - - @cherrypy.expose - def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[], flatten_folders=None, paused=None, directCall=False, air_by_date=None, tvdbLang=None, audio_lang=None, custom_search_names=None, subtitles=None): - - if show == None: - errString = "Invalid show ID: "+str(show) - if directCall: - return [errString] - else: - return _genericMessage("Error", errString) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - errString = "Unable to find the specified show: "+str(show) - if directCall: - return [errString] - else: - return _genericMessage("Error", errString) - - showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.tvdbid) - - if not location and not anyQualities and not bestQualities and not flatten_folders: - - t = PageTemplate(file="editShow.tmpl") - t.submenu = HomeMenu() - with showObj.lock: - t.show = showObj - - return _munge(t) - - if flatten_folders == "on": - flatten_folders = 1 - else: - flatten_folders = 0 - - logger.log(u"flatten folders: "+str(flatten_folders)) - - if paused == "on": - paused = 1 - else: - paused = 0 - - if air_by_date == "on": - air_by_date = 1 - else: - air_by_date = 0 - - if subtitles == "on": - subtitles = 1 - else: - subtitles = 0 - - - if tvdbLang and tvdbLang in tvdb_api.Tvdb().config['valid_languages']: - tvdb_lang = tvdbLang - else: - tvdb_lang = showObj.lang - - # if we changed the language then kick off an update - if tvdb_lang == showObj.lang: - do_update = False - else: - do_update = True - - if type(anyQualities) != list: - anyQualities = [anyQualities] - - if type(bestQualities) != list: - bestQualities = [bestQualities] - - if type(exceptions_list) != list: - exceptions_list = [exceptions_list] - - errors = [] - with showObj.lock: - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - showObj.quality = newQuality - - # reversed for now - if bool(showObj.flatten_folders) != bool(flatten_folders): - showObj.flatten_folders = flatten_folders - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show: "+ex(e)) - - showObj.paused = paused - showObj.air_by_date = air_by_date - showObj.subtitles = subtitles - showObj.lang = tvdb_lang - showObj.audio_lang = audio_lang - showObj.custom_search_names = custom_search_names - - # if we change location clear the db of episodes, change it, write to db, and rescan - if os.path.normpath(showObj._location) != os.path.normpath(location): - logger.log(os.path.normpath(showObj._location)+" != "+os.path.normpath(location), logger.DEBUG) - if not ek.ek(os.path.isdir, location): - errors.append("New location <tt>%s</tt> does not exist" % location) - - # don't bother if we're going to update anyway - elif not do_update: - # change it - try: - showObj.location = location - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show:"+ex(e)) - # grab updated info from TVDB - #showObj.loadEpisodesFromTVDB() - # rescan the episodes in the new folder - except exceptions.NoNFOException: - errors.append("The folder at <tt>%s</tt> doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in Sick Beard." % location) - - # save it to the DB - showObj.saveToDB() - - # force the update - if do_update: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable - time.sleep(1) - except exceptions.CantUpdateException, e: - errors.append("Unable to force an update on the show.") - - if directCall: - return errors - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - '<ul>' + '\n'.join(['<li>%s</li>' % error for error in errors]) + "</ul>") - - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def deleteShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable - return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") - - showObj.deleteShow() - - ui.notifications.message('<b>%s</b> has been deleted' % showObj.name) - redirect("/home") - - @cherrypy.expose - def refreshShow(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - # force the update from the DB - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable - except exceptions.CantRefreshException, e: - ui.notifications.error("Unable to refresh this show.", - ex(e)) - - time.sleep(3) - - redirect("/home/displayShow?show="+str(showObj.tvdbid)) - - @cherrypy.expose - def updateShow(self, show=None, force=0): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - # force the update - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) #@UndefinedVariable - except exceptions.CantUpdateException, e: - ui.notifications.error("Unable to update this show.", - ex(e)) - - # just give it some time - time.sleep(3) - - redirect("/home/displayShow?show=" + str(showObj.tvdbid)) - - @cherrypy.expose - def subtitleShow(self, show=None, force=0): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - # search and download subtitles - sickbeard.showQueueScheduler.action.downloadSubtitles(showObj, bool(force)) #@UndefinedVariable - - time.sleep(3) - - redirect("/home/displayShow?show="+str(showObj.tvdbid)) - - - @cherrypy.expose - def updateXBMC(self, showName=None): - if sickbeard.XBMC_UPDATE_ONLYFIRST: - # only send update to first host in the list -- workaround for xbmc sql backend users - host = sickbeard.XBMC_HOST.split(",")[0].strip() - else: - host = sickbeard.XBMC_HOST - - if notifiers.xbmc_notifier.update_library(showName=showName): - ui.notifications.message("Library update command sent to XBMC host(s): " + host) - else: - ui.notifications.error("Unable to contact one or more XBMC host(s): " + host) - redirect('/home') - - @cherrypy.expose - def updatePLEX(self): - if notifiers.plex_notifier.update_library(): - ui.notifications.message("Library update command sent to Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - else: - ui.notifications.error("Unable to contact Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) - redirect('/home') - - @cherrypy.expose - def setStatus(self, show=None, eps=None, status=None, direct=False): - - if show == None or eps == None or status == None: - errMsg = "You must specify a show and at least one episode" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - if not statusStrings.has_key(int(status)): - errMsg = "Invalid status" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - errMsg = "Error", "Show not in show list" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - segment_list = [] - - if eps != None: - - for curEp in eps.split('|'): - - logger.log(u"Attempting to set status on episode "+curEp+" to "+status, logger.DEBUG) - - epInfo = curEp.split('x') - - epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) - - if int(status) == WANTED: - # figure out what segment the episode is in and remember it so we can backlog it - if epObj.show.air_by_date: - ep_segment = str(epObj.airdate)[:7] - else: - ep_segment = epObj.season - - if ep_segment not in segment_list: - segment_list.append(ep_segment) - - if epObj == None: - return _genericMessage("Error", "Episode couldn't be retrieved") - - with epObj.lock: - # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - logger.log(u"Refusing to change status of "+curEp+" because it is UNAIRED", logger.ERROR) - continue - - if int(status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED + [IGNORED] and not ek.ek(os.path.isfile, epObj.location): - logger.log(u"Refusing to change status of "+curEp+" to DOWNLOADED because it's not SNATCHED/DOWNLOADED", logger.ERROR) - continue - - epObj.status = int(status) - epObj.saveToDB() - - msg = "Backlog was automatically started for the following seasons of <b>"+showObj.name+"</b>:<br />" - for cur_segment in segment_list: - msg += "<li>Season "+str(cur_segment)+"</li>" - logger.log(u"Sending backlog for "+showObj.name+" season "+str(cur_segment)+" because some eps were set to wanted") - cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable - msg += "</ul>" - - if segment_list: - ui.notifications.message("Backlog started", msg) - - if direct: - return json.dumps({'result': 'success'}) - else: - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def setAudio(self, show=None, eps=None, audio_langs=None, direct=False): - - if show == None or eps == None or audio_langs == None: - errMsg = "You must specify a show and at least one episode" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return _genericMessage("Error", errMsg) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Show not in show list") - - try: - show_loc = showObj.location #@UnusedVariable - except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - ep_obj_rename_list = [] - - for curEp in eps.split('|'): - - logger.log(u"Attempting to set audio on episode "+curEp+" to "+audio_langs, logger.DEBUG) - - epInfo = curEp.split('x') - - epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) - - epObj.audio_langs = str(audio_langs) - epObj.saveToDB() - - if direct: - return json.dumps({'result': 'success'}) - else: - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def testRename(self, show=None): - - if show == None: - return _genericMessage("Error", "You must specify a show") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Show not in show list") - - try: - show_loc = showObj.location #@UnusedVariable - except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - ep_obj_rename_list = [] - - ep_obj_list = showObj.getAllEpisodes(has_location=True) - - for cur_ep_obj in ep_obj_list: - # Only want to rename if we have a location - if cur_ep_obj.location: - if cur_ep_obj.relatedEps: - # do we have one of multi-episodes in the rename list already - have_already = False - for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: - if cur_related_ep in ep_obj_rename_list: - have_already = True - break - if not have_already: - ep_obj_rename_list.append(cur_ep_obj) - - else: - ep_obj_rename_list.append(cur_ep_obj) - - if ep_obj_rename_list: - # present season DESC episode DESC on screen - ep_obj_rename_list.reverse() - - t = PageTemplate(file="testRename.tmpl") - t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.tvdbid}] - t.ep_obj_list = ep_obj_rename_list - t.show = showObj - - return _munge(t) - - @cherrypy.expose - def doRename(self, show=None, eps=None): - - if show == None or eps == None: - errMsg = "You must specify a show and at least one episode" - return _genericMessage("Error", errMsg) - - show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if show_obj == None: - errMsg = "Error", "Show not in show list" - return _genericMessage("Error", errMsg) - - try: - show_loc = show_obj.location #@UnusedVariable - except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - myDB = db.DBConnection() - - if eps == None: - redirect("/home/displayShow?show=" + show) - - for curEp in eps.split('|'): - - epInfo = curEp.split('x') - - # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database - ep_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", [show, epInfo[0], epInfo[1]]) - if not ep_result: - logger.log(u"Unable to find an episode for "+curEp+", skipping", logger.WARNING) - continue - related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", [ep_result[0]["location"], epInfo[1]]) - - root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) - for cur_related_ep in related_eps_result: - related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) - if related_ep_obj not in root_ep_obj.relatedEps: - root_ep_obj.relatedEps.append(related_ep_obj) - - root_ep_obj.rename() - - redirect("/home/displayShow?show=" + show) - - @cherrypy.expose - def trunchistory(self, epid): - - myDB = db.DBConnection() - nbep = myDB.select("Select count(*) from episode_links where episode_id=?",[epid]) - myDB.action("DELETE from episode_links where episode_id=?",[epid]) - messnum = str(nbep[0][0]) + ' history links deleted' - ui.notifications.message('Episode History Truncated' , messnum) - return json.dumps({'result': 'ok'}) - - @cherrypy.expose - def searchEpisode(self, show=None, season=None, episode=None): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj) - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) #@UndefinedVariable - - # wait until the queue item tells us whether it worked or not - while ep_queue_item.success == None: #@UndefinedVariable - time.sleep(1) - - # return the correct json value - if ep_queue_item.success: - return json.dumps({'result': statusStrings[ep_obj.status]}) - - return json.dumps({'result': 'failure'}) - - @cherrypy.expose - def searchEpisodeSubtitles(self, show=None, season=None, episode=None): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # try do download subtitles for that episode - previous_subtitles = ep_obj.subtitles - try: - subtitles = ep_obj.downloadSubtitles() - - if sickbeard.SUBTITLES_DIR: - for video in subtitles: - subs_new_path = ek.ek(os.path.join, os.path.dirname(video.path), sickbeard.SUBTITLES_DIR) - dir_exists = helpers.makeDir(subs_new_path) - if not dir_exists: - logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) - else: - helpers.chmodAsParent(subs_new_path) - - for subtitle in subtitles.get(video): - new_file_path = ek.ek(os.path.join, subs_new_path, os.path.basename(subtitle.path)) - helpers.moveFile(subtitle.path, new_file_path) - if sickbeard.SUBSNOLANG: - helpers.copyFile(new_file_path,new_file_path[:-6]+"srt") - helpers.chmodAsParent(new_file_path[:-6]+"srt") - helpers.chmodAsParent(new_file_path) - else: - if sickbeard.SUBTITLES_DIR_SUB: - for video in subtitles: - subs_new_path = os.path.join(os.path.dirname(video.path),"Subs") - dir_exists = helpers.makeDir(subs_new_path) - if not dir_exists: - logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) - else: - helpers.chmodAsParent(subs_new_path) - - for subtitle in subtitles.get(video): - new_file_path = ek.ek(os.path.join, subs_new_path, os.path.basename(subtitle.path)) - helpers.moveFile(subtitle.path, new_file_path) - if sickbeard.SUBSNOLANG: - helpers.copyFile(new_file_path,new_file_path[:-6]+"srt") - helpers.chmodAsParent(new_file_path[:-6]+"srt") - helpers.chmodAsParent(new_file_path) - else: - for video in subtitles: - for subtitle in subtitles.get(video): - if sickbeard.SUBSNOLANG: - helpers.copyFile(subtitle.path,subtitle.path[:-6]+"srt") - helpers.chmodAsParent(subtitle.path[:-6]+"srt") - helpers.chmodAsParent(subtitle.path) - except: - return json.dumps({'result': 'failure'}) - - # return the correct json value - if previous_subtitles != ep_obj.subtitles: - status = 'New subtitles downloaded: %s' % ' '.join(["<img src='"+sickbeard.WEB_ROOT+"/images/flags/"+subliminal.language.Language(x).alpha2+".png' alt='"+subliminal.language.Language(x).name+"'/>" for x in sorted(list(set(ep_obj.subtitles).difference(previous_subtitles)))]) - else: - status = 'No subtitles downloaded' - ui.notifications.message('Subtitles Search', status) - return json.dumps({'result': status, 'subtitles': ','.join([x for x in ep_obj.subtitles])}) - - @cherrypy.expose - def mergeEpisodeSubtitles(self, show=None, season=None, episode=None): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # try do merge subtitles for that episode - try: - ep_obj.mergeSubtitles() - except Exception as e: - return json.dumps({'result': 'failure', 'exception': str(e)}) - - # return the correct json value - status = 'Subtitles merged successfully ' - ui.notifications.message('Merge Subtitles', status) - return json.dumps({'result': 'ok'}) - -class UI: - - @cherrypy.expose - def add_message(self): - - ui.notifications.message('Test 1', 'This is test number 1') - ui.notifications.error('Test 2', 'This is test number 2') - - return "ok" - - @cherrypy.expose - def get_messages(self): - messages = {} - cur_notification_num = 1 - for cur_notification in ui.notifications.get_notifications(): - messages['notification-'+str(cur_notification_num)] = {'title': cur_notification.title, - 'message': cur_notification.message, - 'type': cur_notification.type} - cur_notification_num += 1 - - return json.dumps(messages) - - -class WebInterface: - - @cherrypy.expose - def index(self): - - redirect("/home") - - @cherrypy.expose - def showPoster(self, show=None, which=None): - - #Redirect initial poster/banner thumb to default images - if which[0:6] == 'poster': - default_image_name = 'poster.png' - else: - default_image_name = 'banner.png' - - default_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', default_image_name) - if show is None: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - - cache_obj = image_cache.ImageCache() - - if which == 'poster': - image_file_name = cache_obj.poster_path(showObj.tvdbid) - if which == 'poster_thumb': - image_file_name = cache_obj.poster_thumb_path(showObj.tvdbid) - if which == 'banner': - image_file_name = cache_obj.banner_path(showObj.tvdbid) - if which == 'banner_thumb': - image_file_name = cache_obj.banner_thumb_path(showObj.tvdbid) - - if ek.ek(os.path.isfile, image_file_name): - return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") - else: - return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") - @cherrypy.expose - def setComingEpsLayout(self, layout): - if layout not in ('poster', 'banner', 'list'): - layout = 'banner' - - sickbeard.COMING_EPS_LAYOUT = layout - - redirect("/comingEpisodes") - - @cherrypy.expose - def toggleComingEpsDisplayPaused(self): - - sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED - - redirect("/comingEpisodes") - - @cherrypy.expose - def setComingEpsSort(self, sort): - if sort not in ('date', 'network', 'show'): - sort = 'date' - - sickbeard.COMING_EPS_SORT = sort - - redirect("/comingEpisodes") - - @cherrypy.expose - def comingEpisodes(self, layout="None"): - - myDB = db.DBConnection() - - today = datetime.date.today().toordinal() - next_week = (datetime.date.today() + datetime.timedelta(days=7)).toordinal() - recently = (datetime.date.today() - datetime.timedelta(days=3)).toordinal() - - done_show_list = [] - qualList = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] - sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND airdate >= ? AND airdate < ? AND tv_shows.tvdb_id = tv_episodes.showid AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, next_week] + qualList) - for cur_result in sql_results: - done_show_list.append(int(cur_result["showid"])) - - more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN ("+','.join(['?']*len(done_show_list))+") AND tv_shows.tvdb_id = outer_eps.showid AND airdate = (SELECT airdate FROM tv_episodes inner_eps WHERE inner_eps.showid = outer_eps.showid AND inner_eps.airdate >= ? ORDER BY inner_eps.airdate ASC LIMIT 1) AND outer_eps.status NOT IN ("+','.join(['?']*len(Quality.DOWNLOADED+Quality.SNATCHED))+")", done_show_list + [next_week] + Quality.DOWNLOADED + Quality.SNATCHED) - sql_results += more_sql_results - - more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.tvdb_id = tv_episodes.showid AND airdate < ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, recently, WANTED] + qualList) - sql_results += more_sql_results - - #epList = sickbeard.comingList - - # sort by air date - sorts = { - 'date': (lambda x, y: cmp(int(x["airdate"]), int(y["airdate"]))), - 'show': (lambda a, b: cmp(a["show_name"], b["show_name"])), - 'network': (lambda a, b: cmp(a["network"], b["network"])), - } - - #epList.sort(sorts[sort]) - sql_results.sort(sorts[sickbeard.COMING_EPS_SORT]) - - t = PageTemplate(file="comingEpisodes.tmpl") - paused_item = { 'title': '', 'path': 'toggleComingEpsDisplayPaused' } - paused_item['title'] = 'Hide Paused' if sickbeard.COMING_EPS_DISPLAY_PAUSED else 'Show Paused' - t.submenu = [ - { 'title': 'Sort by:', 'path': {'Date': 'setComingEpsSort/?sort=date', - 'Show': 'setComingEpsSort/?sort=show', - 'Network': 'setComingEpsSort/?sort=network', - }}, - - { 'title': 'Layout:', 'path': {'Banner': 'setComingEpsLayout/?layout=banner', - 'Poster': 'setComingEpsLayout/?layout=poster', - 'List': 'setComingEpsLayout/?layout=list', - }}, - paused_item, - ] - - t.next_week = next_week - t.today = today - t.sql_results = sql_results - - # Allow local overriding of layout parameter - if layout and layout in ('poster', 'banner', 'list'): - t.layout = layout - else: - t.layout = sickbeard.COMING_EPS_LAYOUT - - - return _munge(t) - - manage = Manage() - - history = History() - - config = Config() - - home = Home() - - api = Api() - - browser = browser.WebFileBrowser() - - errorlogs = ErrorLogs() - - ui = UI() +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import with_statement + +import os.path + +import time +import urllib +import re +import threading +import datetime +import random + +import locale + +from Cheetah.Template import Template +import cherrypy.lib + +import sickbeard + +from sickbeard import config, sab +from sickbeard import clients +from sickbeard import history, notifiers, processTV +from sickbeard import ui +from sickbeard import logger, helpers, exceptions, classes, db +from sickbeard import encodingKludge as ek +from sickbeard import search_queue +from sickbeard import image_cache +from sickbeard import scene_exceptions +from sickbeard import naming +from sickbeard import subtitles + +from sickbeard.providers import newznab +from sickbeard.common import Quality, Overview, statusStrings +from sickbeard.common import SNATCHED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED +from sickbeard.exceptions import ex +from sickbeard.webapi import Api + +from lib.tvdb_api import tvdb_api +from lib.dateutil import tz +import network_timezones + +import subliminal + +try: + import json +except ImportError: + from lib import simplejson as json + +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from sickbeard import browser + + +class PageTemplate (Template): + def __init__(self, *args, **KWs): + KWs['file'] = os.path.join(sickbeard.PROG_DIR, "data/interfaces/default/", KWs['file']) + super(PageTemplate, self).__init__(*args, **KWs) + self.sbRoot = sickbeard.WEB_ROOT + self.sbHttpPort = sickbeard.WEB_PORT + self.sbHttpsPort = sickbeard.WEB_PORT + self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS + if cherrypy.request.headers['Host'][0] == '[': + self.sbHost = re.match("^\[.*\]", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) + else: + self.sbHost = re.match("^[^:]+", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0) + self.projectHomePage = "http://code.google.com/p/sickbeard/" + + if sickbeard.NZBS and sickbeard.NZBS_UID and sickbeard.NZBS_HASH: + logger.log(u"NZBs.org has been replaced, please check the config to configure the new provider!", logger.ERROR) + ui.notifications.error("NZBs.org Config Update", "NZBs.org has a new site. Please <a href=\""+sickbeard.WEB_ROOT+"/config/providers\">update your config</a> with the api key from <a href=\"http://nzbs.org/login\">http://nzbs.org</a> and then disable the old NZBs.org provider.") + + if "X-Forwarded-Host" in cherrypy.request.headers: + self.sbHost = cherrypy.request.headers['X-Forwarded-Host'] + if "X-Forwarded-Port" in cherrypy.request.headers: + self.sbHttpPort = cherrypy.request.headers['X-Forwarded-Port'] + self.sbHttpsPort = self.sbHttpPort + if "X-Forwarded-Proto" in cherrypy.request.headers: + self.sbHttpsEnabled = True if cherrypy.request.headers['X-Forwarded-Proto'] == 'https' else False + + logPageTitle = 'Logs & Errors' + if len(classes.ErrorViewer.errors): + logPageTitle += ' ('+str(len(classes.ErrorViewer.errors))+')' + self.logPageTitle = logPageTitle + self.sbPID = str(sickbeard.PID) + self.menu = [ + { 'title': 'Home', 'key': 'home' }, + { 'title': 'Coming Episodes', 'key': 'comingEpisodes' }, + { 'title': 'History', 'key': 'history' }, + { 'title': 'Manage', 'key': 'manage' }, + { 'title': 'Config', 'key': 'config' }, + { 'title': logPageTitle, 'key': 'errorlogs' }, + ] + +def redirect(abspath, *args, **KWs): + assert abspath[0] == '/' + raise cherrypy.HTTPRedirect(sickbeard.WEB_ROOT + abspath, *args, **KWs) + +class TVDBWebUI: + def __init__(self, config, log=None): + self.config = config + self.log = log + + def selectSeries(self, allSeries): + + searchList = ",".join([x['id'] for x in allSeries]) + showDirList = "" + for curShowDir in self.config['_showDir']: + showDirList += "showDir="+curShowDir+"&" + redirect("/home/addShows/addShow?" + showDirList + "seriesList=" + searchList) + +def _munge(string): + return unicode(string).encode('utf-8', 'xmlcharrefreplace') + +def _genericMessage(subject, message): + t = PageTemplate(file="genericMessage.tmpl") + t.submenu = HomeMenu() + t.subject = subject + t.message = message + return _munge(t) + +def _getEpisode(show, season, episode): + + if show == None or season == None or episode == None: + return "Invalid parameters" + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return "Show not in show list" + + epObj = showObj.getEpisode(int(season), int(episode)) + + if epObj == None: + return "Episode couldn't be retrieved" + + return epObj + +ManageMenu = [ + { 'title': 'Backlog Overview', 'path': 'manage/backlogOverview' }, + { 'title': 'Manage Searches', 'path': 'manage/manageSearches' }, + { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' }, + { 'title': 'Manage Missed Subtitles', 'path': 'manage/subtitleMissed' }, +] +if sickbeard.USE_SUBTITLES: + ManageMenu.append({ 'title': 'Missed Subtitle Management', 'path': 'manage/subtitleMissed' }) + +class ManageSearches: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="manage_manageSearches.tmpl") + #t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() + t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() #@UndefinedVariable + t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() #@UndefinedVariable + t.searchStatus = sickbeard.currentSearchScheduler.action.amActive #@UndefinedVariable + t.submenu = ManageMenu + + return _munge(t) + + @cherrypy.expose + def forceSearch(self): + + # force it to run the next time it looks + result = sickbeard.currentSearchScheduler.forceRun() + if result: + logger.log(u"Search forced") + ui.notifications.message('Episode search started', + 'Note: RSS feeds may not be updated if retrieved recently') + + redirect("/manage/manageSearches") + + @cherrypy.expose + def pauseBacklog(self, paused=None): + if paused == "1": + sickbeard.searchQueueScheduler.action.pause_backlog() #@UndefinedVariable + else: + sickbeard.searchQueueScheduler.action.unpause_backlog() #@UndefinedVariable + + redirect("/manage/manageSearches") + + @cherrypy.expose + def forceVersionCheck(self): + + # force a check to see if there is a new version + result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) #@UndefinedVariable + if result: + logger.log(u"Forcing version check") + + redirect("/manage/manageSearches") + + +class Manage: + + manageSearches = ManageSearches() + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="manage.tmpl") + t.submenu = ManageMenu + return _munge(t) + + @cherrypy.expose + def showEpisodeStatuses(self, tvdb_id, whichStatus): + myDB = db.DBConnection() + + status_list = [int(whichStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + + cur_show_results = myDB.select("SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN ("+','.join(['?']*len(status_list))+")", [int(tvdb_id)] + status_list) + + result = {} + for cur_result in cur_show_results: + cur_season = int(cur_result["season"]) + cur_episode = int(cur_result["episode"]) + + if cur_season not in result: + result[cur_season] = {} + + result[cur_season][cur_episode] = cur_result["name"] + + return json.dumps(result) + + @cherrypy.expose + def episodeStatuses(self, whichStatus=None): + + if whichStatus: + whichStatus = int(whichStatus) + status_list = [whichStatus] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + else: + status_list = [] + + t = PageTemplate(file="manage_episodeStatuses.tmpl") + t.submenu = ManageMenu + t.whichStatus = whichStatus + + # if we have no status then this is as far as we need to go + if not status_list: + return _munge(t) + + myDB = db.DBConnection() + status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name", status_list) + + ep_counts = {} + show_names = {} + sorted_show_ids = [] + for cur_status_result in status_results: + cur_tvdb_id = int(cur_status_result["tvdb_id"]) + if cur_tvdb_id not in ep_counts: + ep_counts[cur_tvdb_id] = 1 + else: + ep_counts[cur_tvdb_id] += 1 + + show_names[cur_tvdb_id] = cur_status_result["show_name"] + if cur_tvdb_id not in sorted_show_ids: + sorted_show_ids.append(cur_tvdb_id) + + t.show_names = show_names + t.ep_counts = ep_counts + t.sorted_show_ids = sorted_show_ids + return _munge(t) + + @cherrypy.expose + def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): + + status_list = [int(oldStatus)] + if status_list[0] == SNATCHED: + status_list = Quality.SNATCHED + Quality.SNATCHED_PROPER + + to_change = {} + + # make a list of all shows and their associated args + for arg in kwargs: + tvdb_id, what = arg.split('-') + + # we don't care about unchecked checkboxes + if kwargs[arg] != 'on': + continue + + if tvdb_id not in to_change: + to_change[tvdb_id] = [] + + to_change[tvdb_id].append(what) + + myDB = db.DBConnection() + + for cur_tvdb_id in to_change: + + # get a list of all the eps we want to change if they just said "all" + if 'all' in to_change[cur_tvdb_id]: + all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status IN ("+','.join(['?']*len(status_list))+") AND season != 0 AND showid = ?", status_list + [cur_tvdb_id]) + all_eps = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] + to_change[cur_tvdb_id] = all_eps + + Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) + + redirect('/manage/episodeStatuses') + + @cherrypy.expose + def showSubtitleMissed(self, tvdb_id, whichSubs): + myDB = db.DBConnection() + + cur_show_results = myDB.select("SELECT season, episode, name, subtitles FROM tv_episodes WHERE showid = ? AND season != 0 AND status LIKE '%4'", [int(tvdb_id)]) + + result = {} + for cur_result in cur_show_results: + if whichSubs == 'all': + if len(set(cur_result["subtitles"].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len(subtitles.wantedLanguages()): + continue + elif whichSubs in cur_result["subtitles"].split(','): + continue + + cur_season = int(cur_result["season"]) + cur_episode = int(cur_result["episode"]) + + if cur_season not in result: + result[cur_season] = {} + + if cur_episode not in result[cur_season]: + result[cur_season][cur_episode] = {} + + result[cur_season][cur_episode]["name"] = cur_result["name"] + + result[cur_season][cur_episode]["subtitles"] = ",".join(subliminal.language.Language(subtitle).alpha2 for subtitle in cur_result["subtitles"].split(',')) if not cur_result["subtitles"] == '' else '' + + return json.dumps(result) + + @cherrypy.expose + def subtitleMissed(self, whichSubs=None): + + t = PageTemplate(file="manage_subtitleMissed.tmpl") + t.submenu = ManageMenu + t.whichSubs = whichSubs + + if not whichSubs: + return _munge(t) + + myDB = db.DBConnection() + status_results = myDB.select("SELECT show_name, tv_shows.tvdb_id as tvdb_id, tv_episodes.subtitles subtitles FROM tv_episodes, tv_shows WHERE tv_shows.subtitles = 1 AND tv_episodes.status LIKE '%4' AND tv_episodes.season != 0 AND tv_episodes.showid = tv_shows.tvdb_id ORDER BY show_name") + + ep_counts = {} + show_names = {} + sorted_show_ids = [] + for cur_status_result in status_results: + if whichSubs == 'all': + if len(set(cur_status_result["subtitles"].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len(subtitles.wantedLanguages()): + continue + elif whichSubs in cur_status_result["subtitles"].split(','): + continue + + cur_tvdb_id = int(cur_status_result["tvdb_id"]) + if cur_tvdb_id not in ep_counts: + ep_counts[cur_tvdb_id] = 1 + else: + ep_counts[cur_tvdb_id] += 1 + + show_names[cur_tvdb_id] = cur_status_result["show_name"] + if cur_tvdb_id not in sorted_show_ids: + sorted_show_ids.append(cur_tvdb_id) + + t.show_names = show_names + t.ep_counts = ep_counts + t.sorted_show_ids = sorted_show_ids + return _munge(t) + + @cherrypy.expose + def downloadSubtitleMissed(self, *args, **kwargs): + + to_download = {} + + # make a list of all shows and their associated args + for arg in kwargs: + tvdb_id, what = arg.split('-') + + # we don't care about unchecked checkboxes + if kwargs[arg] != 'on': + continue + + if tvdb_id not in to_download: + to_download[tvdb_id] = [] + + to_download[tvdb_id].append(what) + + for cur_tvdb_id in to_download: + # get a list of all the eps we want to download subtitles if they just said "all" + if 'all' in to_download[cur_tvdb_id]: + myDB = db.DBConnection() + all_eps_results = myDB.select("SELECT season, episode FROM tv_episodes WHERE status LIKE '%4' AND season != 0 AND showid = ?", [cur_tvdb_id]) + to_download[cur_tvdb_id] = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] + + for epResult in to_download[cur_tvdb_id]: + season, episode = epResult.split('x'); + + show = sickbeard.helpers.findCertainShow(sickbeard.showList, int(cur_tvdb_id)) + subtitles = show.getEpisode(int(season), int(episode)).downloadSubtitles() + + + + + redirect('/manage/subtitleMissed') + + @cherrypy.expose + def backlogShow(self, tvdb_id): + + show_obj = helpers.findCertainShow(sickbeard.showList, int(tvdb_id)) + + if show_obj: + sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) #@UndefinedVariable + + redirect("/manage/backlogOverview") + + @cherrypy.expose + def backlogOverview(self): + + t = PageTemplate(file="manage_backlogOverview.tmpl") + t.submenu = ManageMenu + + myDB = db.DBConnection() + + showCounts = {} + showCats = {} + showSQLResults = {} + + for curShow in sickbeard.showList: + + epCounts = {} + epCats = {} + epCounts[Overview.SKIPPED] = 0 + epCounts[Overview.WANTED] = 0 + epCounts[Overview.QUAL] = 0 + epCounts[Overview.GOOD] = 0 + epCounts[Overview.UNAIRED] = 0 + epCounts[Overview.SNATCHED] = 0 + + sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", [curShow.tvdbid]) + + for curResult in sqlResults: + + curEpCat = curShow.getOverview(int(curResult["status"])) + epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat + epCounts[curEpCat] += 1 + + showCounts[curShow.tvdbid] = epCounts + showCats[curShow.tvdbid] = epCats + showSQLResults[curShow.tvdbid] = sqlResults + + t.showCounts = showCounts + t.showCats = showCats + t.showSQLResults = showSQLResults + + return _munge(t) + + @cherrypy.expose + def massEdit(self, toEdit=None): + + t = PageTemplate(file="manage_massEdit.tmpl") + t.submenu = ManageMenu + + if not toEdit: + redirect("/manage") + + showIDs = toEdit.split("|") + showList = [] + for curID in showIDs: + curID = int(curID) + showObj = helpers.findCertainShow(sickbeard.showList, curID) + if showObj: + showList.append(showObj) + + flatten_folders_all_same = True + last_flatten_folders = None + + paused_all_same = True + last_paused = None + + quality_all_same = True + last_quality = None + + subtitles_all_same = True + last_subtitles = None + + lang_all_same = True + last_lang_metadata= None + + lang_audio_all_same = True + last_lang_audio = None + + root_dir_list = [] + + for curShow in showList: + + cur_root_dir = ek.ek(os.path.dirname, curShow._location) + if cur_root_dir not in root_dir_list: + root_dir_list.append(cur_root_dir) + + # if we know they're not all the same then no point even bothering + if paused_all_same: + # if we had a value already and this value is different then they're not all the same + if last_paused not in (curShow.paused, None): + paused_all_same = False + else: + last_paused = curShow.paused + + if flatten_folders_all_same: + if last_flatten_folders not in (None, curShow.flatten_folders): + flatten_folders_all_same = False + else: + last_flatten_folders = curShow.flatten_folders + + if quality_all_same: + if last_quality not in (None, curShow.quality): + quality_all_same = False + else: + last_quality = curShow.quality + + if subtitles_all_same: + if last_subtitles not in (None, curShow.subtitles): + subtitles_all_same = False + else: + last_subtitles = curShow.subtitles + + if lang_all_same: + if last_lang_metadata not in (None, curShow.lang): + lang_all_same = False + else: + last_lang_metadata = curShow.lang + + if lang_audio_all_same: + if last_lang_audio not in (None, curShow.audio_lang): + lang_audio_all_same = False + else: + last_lang_audio = curShow.audio_lang + + t.showList = toEdit + t.paused_value = last_paused if paused_all_same else None + t.flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None + t.quality_value = last_quality if quality_all_same else None + t.subtitles_value = last_subtitles if subtitles_all_same else None + t.root_dir_list = root_dir_list + t.lang_value = last_lang_metadata if lang_all_same else None + t.audio_value = last_lang_audio if lang_audio_all_same else None + return _munge(t) + + @cherrypy.expose + def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, subtitles=None, + anyQualities=[], bestQualities=[], tvdbLang=None, audioLang = None, toEdit=None, *args, **kwargs): + + dir_map = {} + for cur_arg in kwargs: + if not cur_arg.startswith('orig_root_dir_'): + continue + which_index = cur_arg.replace('orig_root_dir_', '') + end_dir = kwargs['new_root_dir_'+which_index] + dir_map[kwargs[cur_arg]] = end_dir + + showIDs = toEdit.split("|") + errors = [] + for curShow in showIDs: + curErrors = [] + showObj = helpers.findCertainShow(sickbeard.showList, int(curShow)) + if not showObj: + continue + + cur_root_dir = ek.ek(os.path.dirname, showObj._location) + cur_show_dir = ek.ek(os.path.basename, showObj._location) + if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: + new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) + logger.log(u"For show "+showObj.name+" changing dir from "+showObj._location+" to "+new_show_dir) + else: + new_show_dir = showObj._location + + if paused == 'keep': + new_paused = showObj.paused + else: + new_paused = True if paused == 'enable' else False + new_paused = 'on' if new_paused else 'off' + + if flatten_folders == 'keep': + new_flatten_folders = showObj.flatten_folders + else: + new_flatten_folders = True if flatten_folders == 'enable' else False + new_flatten_folders = 'on' if new_flatten_folders else 'off' + + if subtitles == 'keep': + new_subtitles = showObj.subtitles + else: + new_subtitles = True if subtitles == 'enable' else False + + new_subtitles = 'on' if new_subtitles else 'off' + + if quality_preset == 'keep': + anyQualities, bestQualities = Quality.splitQuality(showObj.quality) + + if tvdbLang == 'None': + new_lang = 'en' + else: + new_lang = tvdbLang + + if audioLang == 'None': + new_audio_lang = showObj.audio_lang; + else: + new_audio_lang = audioLang + + exceptions_list = [] + + curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, exceptions_list, new_flatten_folders, new_paused, subtitles=new_subtitles, tvdbLang=new_lang, audio_lang=new_audio_lang, custom_search_names=showObj.custom_search_names, directCall=True) + + if curErrors: + logger.log(u"Errors: "+str(curErrors), logger.ERROR) + errors.append('<b>%s:</b>\n<ul>' % showObj.name + ' '.join(['<li>%s</li>' % error for error in curErrors]) + "</ul>") + + if len(errors) > 0: + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + " ".join(errors)) + + redirect("/manage") + + @cherrypy.expose + def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toMetadata=None, toSubtitle=None): + + if toUpdate != None: + toUpdate = toUpdate.split('|') + else: + toUpdate = [] + + if toRefresh != None: + toRefresh = toRefresh.split('|') + else: + toRefresh = [] + + if toRename != None: + toRename = toRename.split('|') + else: + toRename = [] + + if toSubtitle != None: + toSubtitle = toSubtitle.split('|') + else: + toSubtitle = [] + + if toDelete != None: + toDelete = toDelete.split('|') + else: + toDelete = [] + + if toMetadata != None: + toMetadata = toMetadata.split('|') + else: + toMetadata = [] + + errors = [] + refreshes = [] + updates = [] + renames = [] + subtitles = [] + + for curShowID in set(toUpdate+toRefresh+toRename+toSubtitle+toDelete+toMetadata): + + if curShowID == '': + continue + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(curShowID)) + + if showObj == None: + continue + + if curShowID in toDelete: + showObj.deleteShow() + # don't do anything else if it's being deleted + continue + + if curShowID in toUpdate: + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable + updates.append(showObj.name) + except exceptions.CantUpdateException, e: + errors.append("Unable to update show "+showObj.name+": "+ex(e)) + + # don't bother refreshing shows that were updated anyway + if curShowID in toRefresh and curShowID not in toUpdate: + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + refreshes.append(showObj.name) + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh show "+showObj.name+": "+ex(e)) + + if curShowID in toRename: + sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) #@UndefinedVariable + renames.append(showObj.name) + + if curShowID in toSubtitle: + sickbeard.showQueueScheduler.action.downloadSubtitles(showObj) #@UndefinedVariable + subtitles.append(showObj.name) + + if len(errors) > 0: + ui.notifications.error("Errors encountered", + '<br >\n'.join(errors)) + + messageDetail = "" + + if len(updates) > 0: + messageDetail += "<br /><b>Updates</b><br /><ul><li>" + messageDetail += "</li><li>".join(updates) + messageDetail += "</li></ul>" + + if len(refreshes) > 0: + messageDetail += "<br /><b>Refreshes</b><br /><ul><li>" + messageDetail += "</li><li>".join(refreshes) + messageDetail += "</li></ul>" + + if len(renames) > 0: + messageDetail += "<br /><b>Renames</b><br /><ul><li>" + messageDetail += "</li><li>".join(renames) + messageDetail += "</li></ul>" + + if len(subtitles) > 0: + messageDetail += "<br /><b>Subtitles</b><br /><ul><li>" + messageDetail += "</li><li>".join(subtitles) + messageDetail += "</li></ul>" + + if len(updates+refreshes+renames+subtitles) > 0: + ui.notifications.message("The following actions were queued:", + messageDetail) + + redirect("/manage") + + +class History: + + @cherrypy.expose + def index(self, limit=100): + + myDB = db.DBConnection() + +# sqlResults = myDB.select("SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.tvdb_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT "+str(numPerPage*(p-1))+", "+str(numPerPage)) + if limit == "0": + sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC") + else: + sqlResults = myDB.select("SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.tvdb_id ORDER BY date DESC LIMIT ?", [limit]) + + t = PageTemplate(file="history.tmpl") + t.historyResults = sqlResults + t.limit = limit + t.submenu = [ + { 'title': 'Clear History', 'path': 'history/clearHistory' }, + { 'title': 'Trim History', 'path': 'history/trimHistory' }, + { 'title': 'Trunc Episode Links', 'path': 'history/truncEplinks' }, + ] + + return _munge(t) + + + @cherrypy.expose + def clearHistory(self): + + myDB = db.DBConnection() + myDB.action("DELETE FROM history WHERE 1=1") + ui.notifications.message('History cleared') + redirect("/history") + + + @cherrypy.expose + def trimHistory(self): + + myDB = db.DBConnection() + myDB.action("DELETE FROM history WHERE date < "+str((datetime.datetime.today()-datetime.timedelta(days=30)).strftime(history.dateFormat))) + ui.notifications.message('Removed history entries greater than 30 days old') + redirect("/history") + + + @cherrypy.expose + def truncEplinks(self): + + myDB = db.DBConnection() + nbep=myDB.select("SELECT count(*) from episode_links") + myDB.action("DELETE FROM episode_links WHERE 1=1") + messnum = str(nbep[0][0]) + ' history links deleted' + ui.notifications.message('All Episode Links Removed', messnum) + redirect("/history") + + +ConfigMenu = [ + { 'title': 'General', 'path': 'config/general/' }, + { 'title': 'Search Settings', 'path': 'config/search/' }, + { 'title': 'Search Providers', 'path': 'config/providers/' }, + { 'title': 'Subtitles Settings','path': 'config/subtitles/' }, + { 'title': 'Post Processing', 'path': 'config/postProcessing/' }, + { 'title': 'Notifications', 'path': 'config/notifications/' }, +] + +class ConfigGeneral: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_general.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveRootDirs(self, rootDirString=None): + sickbeard.ROOT_DIRS = rootDirString + sickbeard.save_config() + @cherrypy.expose + def saveAddShowDefaults(self, defaultFlattenFolders, defaultStatus, anyQualities, bestQualities, audio_lang, subtitles): + + if anyQualities: + anyQualities = anyQualities.split(',') + else: + anyQualities = [] + + if bestQualities: + bestQualities = bestQualities.split(',') + else: + bestQualities = [] + + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + + sickbeard.STATUS_DEFAULT = int(defaultStatus) + sickbeard.QUALITY_DEFAULT = int(newQuality) + sickbeard.AUDIO_SHOW_DEFAULT = str(audio_lang) + + if defaultFlattenFolders == "true": + defaultFlattenFolders = 1 + else: + defaultFlattenFolders = 0 + + sickbeard.FLATTEN_FOLDERS_DEFAULT = int(defaultFlattenFolders) + + if subtitles == "true": + subtitles = 1 + else: + subtitles = 0 + sickbeard.SUBTITLES_DEFAULT = int(subtitles) + + sickbeard.save_config() + + @cherrypy.expose + def generateKey(self): + """ Return a new randomized API_KEY + """ + + try: + from hashlib import md5 + except ImportError: + from md5 import md5 + + # Create some values to seed md5 + t = str(time.time()) + r = str(random.random()) + + # Create the md5 instance and give it the current time + m = md5(t) + + # Update the md5 instance with the random variable + m.update(r) + + # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b + logger.log(u"New API generated") + return m.hexdigest() + + @cherrypy.expose + def saveGeneral(self, log_dir=None, web_port=None, web_log=None, web_ipv6=None, + update_shows_on_start=None,launch_browser=None, web_username=None, use_api=None, api_key=None, + web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, sort_article=None): + + results = [] + + if web_ipv6 == "on": + web_ipv6 = 1 + else: + web_ipv6 = 0 + + if web_log == "on": + web_log = 1 + else: + web_log = 0 + + if launch_browser == "on": + launch_browser = 1 + else: + launch_browser = 0 + + if update_shows_on_start == "on": + update_shows_on_start = 1 + else: + update_shows_on_start = 0 + + if sort_article == "on": + sort_article = 1 + else: + sort_article = 0 + + if version_notify == "on": + version_notify = 1 + else: + version_notify = 0 + + if not config.change_LOG_DIR(log_dir): + results += ["Unable to create directory " + os.path.normpath(log_dir) + ", log dir not changed."] + + sickbeard.UPDATE_SHOWS_ON_START = update_shows_on_start + sickbeard.LAUNCH_BROWSER = launch_browser + sickbeard.SORT_ARTICLE = sort_article + + sickbeard.WEB_PORT = int(web_port) + sickbeard.WEB_IPV6 = web_ipv6 + sickbeard.WEB_LOG = web_log + sickbeard.WEB_USERNAME = web_username + sickbeard.WEB_PASSWORD = web_password + + if use_api == "on": + use_api = 1 + else: + use_api = 0 + + sickbeard.USE_API = use_api + sickbeard.API_KEY = api_key + + if enable_https == "on": + enable_https = 1 + else: + enable_https = 0 + + sickbeard.ENABLE_HTTPS = enable_https + + if not config.change_HTTPS_CERT(https_cert): + results += ["Unable to create directory " + os.path.normpath(https_cert) + ", https cert dir not changed."] + + if not config.change_HTTPS_KEY(https_key): + results += ["Unable to create directory " + os.path.normpath(https_key) + ", https key dir not changed."] + + config.change_VERSION_NOTIFY(version_notify) + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/general/") + + +class ConfigSearch: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_search.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, + sab_apikey=None, sab_category=None, sab_host=None, nzbget_password=None, nzbget_category=None, nzbget_host=None, + torrent_dir=None,torrent_method=None, nzb_method=None, usenet_retention=None, search_frequency=None, download_propers=None, torrent_username=None, torrent_password=None, torrent_host=None, torrent_label=None, torrent_path=None, + torrent_ratio=None, torrent_paused=None, ignore_words=None, prefered_method=None): + + results = [] + + if not config.change_NZB_DIR(nzb_dir): + results += ["Unable to create directory " + os.path.normpath(nzb_dir) + ", dir not changed."] + + if not config.change_TORRENT_DIR(torrent_dir): + results += ["Unable to create directory " + os.path.normpath(torrent_dir) + ", dir not changed."] + + config.change_SEARCH_FREQUENCY(search_frequency) + + if download_propers == "on": + download_propers = 1 + else: + download_propers = 0 + + if use_nzbs == "on": + use_nzbs = 1 + else: + use_nzbs = 0 + + if use_torrents == "on": + use_torrents = 1 + else: + use_torrents = 0 + + if usenet_retention == None: + usenet_retention = 200 + + if ignore_words == None: + ignore_words = "" + + sickbeard.USE_NZBS = use_nzbs + sickbeard.USE_TORRENTS = use_torrents + + sickbeard.NZB_METHOD = nzb_method + sickbeard.PREFERED_METHOD = prefered_method + sickbeard.TORRENT_METHOD = torrent_method + sickbeard.USENET_RETENTION = int(usenet_retention) + + sickbeard.IGNORE_WORDS = ignore_words + + sickbeard.DOWNLOAD_PROPERS = download_propers + + sickbeard.SAB_USERNAME = sab_username + sickbeard.SAB_PASSWORD = sab_password + sickbeard.SAB_APIKEY = sab_apikey.strip() + sickbeard.SAB_CATEGORY = sab_category + + if sab_host and not re.match('https?://.*', sab_host): + sab_host = 'http://' + sab_host + + if not sab_host.endswith('/'): + sab_host = sab_host + '/' + + sickbeard.SAB_HOST = sab_host + + sickbeard.NZBGET_PASSWORD = nzbget_password + sickbeard.NZBGET_CATEGORY = nzbget_category + sickbeard.NZBGET_HOST = nzbget_host + + sickbeard.TORRENT_USERNAME = torrent_username + sickbeard.TORRENT_PASSWORD = torrent_password + sickbeard.TORRENT_LABEL = torrent_label + sickbeard.TORRENT_PATH = torrent_path + sickbeard.TORRENT_RATIO = torrent_ratio + if torrent_paused == "on": + torrent_paused = 1 + else: + torrent_paused = 0 + sickbeard.TORRENT_PAUSED = torrent_paused + + if torrent_host and not re.match('https?://.*', torrent_host): + torrent_host = 'http://' + torrent_host + + if not torrent_host.endswith('/'): + torrent_host = torrent_host + '/' + + sickbeard.TORRENT_HOST = torrent_host + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/search/") + +class ConfigPostProcessing: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config_postProcessing.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, + xbmc_data=None, xbmc__frodo__data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, + use_banner=None, keep_processed_dir=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, + move_associated_files=None, tv_download_dir=None, torrent_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): + + results = [] + + if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): + results += ["Unable to create directory " + os.path.normpath(tv_download_dir) + ", dir not changed."] + + if not config.change_TORRENT_DOWNLOAD_DIR(torrent_download_dir): + results += ["Unable to create directory " + os.path.normpath(torrent_download_dir) + ", dir not changed."] + + if use_banner == "on": + use_banner = 1 + else: + use_banner = 0 + + if process_automatically == "on": + process_automatically = 1 + else: + process_automatically = 0 + + if process_automatically_torrent == "on": + process_automatically_torrent = 1 + else: + process_automatically_torrent = 0 + + if rename_episodes == "on": + rename_episodes = 1 + else: + rename_episodes = 0 + + if keep_processed_dir == "on": + keep_processed_dir = 1 + else: + keep_processed_dir = 0 + + if move_associated_files == "on": + move_associated_files = 1 + else: + move_associated_files = 0 + + if naming_custom_abd == "on": + naming_custom_abd = 1 + else: + naming_custom_abd = 0 + + sickbeard.PROCESS_AUTOMATICALLY = process_automatically + sickbeard.PROCESS_AUTOMATICALLY_TORRENT = process_automatically_torrent + sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir + sickbeard.RENAME_EPISODES = rename_episodes + sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files + sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd + + sickbeard.metadata_provider_dict['XBMC'].set_config(xbmc_data) + sickbeard.metadata_provider_dict['XBMC (Frodo)'].set_config(xbmc__frodo__data) + sickbeard.metadata_provider_dict['MediaBrowser'].set_config(mediabrowser_data) + sickbeard.metadata_provider_dict['Synology'].set_config(synology_data) + sickbeard.metadata_provider_dict['Sony PS3'].set_config(sony_ps3_data) + sickbeard.metadata_provider_dict['WDTV'].set_config(wdtv_data) + sickbeard.metadata_provider_dict['TIVO'].set_config(tivo_data) + + if self.isNamingValid(naming_pattern, naming_multi_ep) != "invalid": + sickbeard.NAMING_PATTERN = naming_pattern + sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) + sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() + else: + results.append("You tried saving an invalid naming config, not saving your naming settings") + + if self.isNamingValid(naming_abd_pattern, None, True) != "invalid": + sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern + elif naming_custom_abd: + results.append("You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") + + sickbeard.USE_BANNER = use_banner + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/postProcessing/") + + @cherrypy.expose + def testNaming(self, pattern=None, multi=None, abd=False): + + if multi != None: + multi = int(multi) + + result = naming.test_name(pattern, multi, abd) + + result = ek.ek(os.path.join, result['dir'], result['name']) + + return result + + @cherrypy.expose + def isNamingValid(self, pattern=None, multi=None, abd=False): + if pattern == None: + return "invalid" + + # air by date shows just need one check, we don't need to worry about season folders + if abd: + is_valid = naming.check_valid_abd_naming(pattern) + require_season_folders = False + + else: + # check validity of single and multi ep cases for the whole path + is_valid = naming.check_valid_naming(pattern, multi) + + # check validity of single and multi ep cases for only the file name + require_season_folders = naming.check_force_season_folders(pattern, multi) + + if is_valid and not require_season_folders: + return "valid" + elif is_valid and require_season_folders: + return "seasonfolders" + else: + return "invalid" + + +class ConfigProviders: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="config_providers.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def canAddNewznabProvider(self, name): + + if not name: + return json.dumps({'error': 'Invalid name specified'}) + + providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + tempProvider = newznab.NewznabProvider(name, '') + + if tempProvider.getID() in providerDict: + return json.dumps({'error': 'Exists as '+providerDict[tempProvider.getID()].name}) + else: + return json.dumps({'success': tempProvider.getID()}) + + @cherrypy.expose + def saveNewznabProvider(self, name, url, key=''): + + if not name or not url: + return '0' + + if not url.endswith('/'): + url = url + '/' + + providerDict = dict(zip([x.name for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if name in providerDict: + if not providerDict[name].default: + providerDict[name].name = name + providerDict[name].url = url + providerDict[name].key = key + + return providerDict[name].getID() + '|' + providerDict[name].configStr() + + else: + + newProvider = newznab.NewznabProvider(name, url, key) + sickbeard.newznabProviderList.append(newProvider) + return newProvider.getID() + '|' + newProvider.configStr() + + + + @cherrypy.expose + def deleteNewznabProvider(self, id): + + providerDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + if id not in providerDict or providerDict[id].default: + return '0' + + # delete it from the list + sickbeard.newznabProviderList.remove(providerDict[id]) + + if id in sickbeard.PROVIDER_ORDER: + sickbeard.PROVIDER_ORDER.remove(id) + + return '1' + + + @cherrypy.expose + def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None, + nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string='', + omgwtfnzbs_uid=None, omgwtfnzbs_key=None, + tvtorrents_digest=None, tvtorrents_hash=None, + torrentleech_key=None, + btn_api_key=None, + newzbin_username=None, newzbin_password=None,t411_username=None,t411_password=None, + gks_key=None, + provider_order=None): + + results = [] + + provider_str_list = provider_order.split() + provider_list = [] + + newznabProviderDict = dict(zip([x.getID() for x in sickbeard.newznabProviderList], sickbeard.newznabProviderList)) + + finishedNames = [] + + # add all the newznab info we got into our list + for curNewznabProviderStr in newznab_string.split('!!!'): + + if not curNewznabProviderStr: + continue + + curName, curURL, curKey = curNewznabProviderStr.split('|') + + newProvider = newznab.NewznabProvider(curName, curURL, curKey) + + curID = newProvider.getID() + + # if it already exists then update it + if curID in newznabProviderDict: + newznabProviderDict[curID].name = curName + newznabProviderDict[curID].url = curURL + newznabProviderDict[curID].key = curKey + else: + sickbeard.newznabProviderList.append(newProvider) + + finishedNames.append(curID) + + # delete anything that is missing + for curProvider in sickbeard.newznabProviderList: + if curProvider.getID() not in finishedNames: + sickbeard.newznabProviderList.remove(curProvider) + + # do the enable/disable + for curProviderStr in provider_str_list: + curProvider, curEnabled = curProviderStr.split(':') + curEnabled = int(curEnabled) + + provider_list.append(curProvider) + + if curProvider == 'nzbs_r_us': + sickbeard.NZBSRUS = curEnabled + elif curProvider == 'nzbs_org_old': + sickbeard.NZBS = curEnabled + elif curProvider == 'nzbmatrix': + sickbeard.NZBMATRIX = curEnabled + elif curProvider == 'newzbin': + sickbeard.NEWZBIN = curEnabled + elif curProvider == 'bin_req': + sickbeard.BINREQ = curEnabled + elif curProvider == 'womble_s_index': + sickbeard.WOMBLE = curEnabled + elif curProvider == 'nzbx': + sickbeard.NZBX = curEnabled + elif curProvider == 'omgwtfnzbs': + sickbeard.OMGWTFNZBS = curEnabled + elif curProvider == 'ezrss': + sickbeard.EZRSS = curEnabled + elif curProvider == 'tvtorrents': + sickbeard.TVTORRENTS = curEnabled + elif curProvider == 'torrentleech': + sickbeard.TORRENTLEECH = curEnabled + elif curProvider == 'btn': + sickbeard.BTN = curEnabled + elif curProvider == 'binnewz': + sickbeard.BINNEWZ = curEnabled + elif curProvider == 't411': + sickbeard.T411 = curEnabled + elif curProvider == 'cpasbien': + sickbeard.Cpasbien = curEnabled + elif curProvider == 'kat': + sickbeard.kat = curEnabled + elif curProvider == 'piratebay': + sickbeard.THEPIRATEBAY = curEnabled + elif curProvider == 'gks': + sickbeard.GKS = curEnabled + elif curProvider in newznabProviderDict: + newznabProviderDict[curProvider].enabled = bool(curEnabled) + else: + logger.log(u"don't know what " + curProvider + " is, skipping") + + sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip() + sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip() + + sickbeard.TORRENTLEECH_KEY = torrentleech_key.strip() + + sickbeard.BTN_API_KEY = btn_api_key.strip() + + sickbeard.T411_USERNAME = t411_username + sickbeard.T411_PASSWORD = t411_password + + sickbeard.NZBSRUS_UID = nzbs_r_us_uid.strip() + sickbeard.NZBSRUS_HASH = nzbs_r_us_hash.strip() + + sickbeard.OMGWTFNZBS_UID = omgwtfnzbs_uid.strip() + sickbeard.OMGWTFNZBS_KEY = omgwtfnzbs_key.strip() + + sickbeard.GKS_KEY = gks_key.strip() + + sickbeard.PROVIDER_ORDER = provider_list + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/providers/") + + +class ConfigNotifications: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="config_notifications.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveNotifications(self, use_xbmc=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, xbmc_update_onlyfirst=None, xbmc_notify_onsubtitledownload=None, + xbmc_update_library=None, xbmc_update_full=None, xbmc_host=None, xbmc_username=None, xbmc_password=None, + use_plex=None, plex_notify_onsnatch=None, plex_notify_ondownload=None, plex_notify_onsubtitledownload=None, plex_update_library=None, + plex_server_host=None, plex_host=None, plex_username=None, plex_password=None, + use_growl=None, growl_notify_onsnatch=None, growl_notify_ondownload=None, growl_notify_onsubtitledownload=None, growl_host=None, growl_password=None, + use_prowl=None, prowl_notify_onsnatch=None, prowl_notify_ondownload=None, prowl_notify_onsubtitledownload=None, prowl_api=None, prowl_priority=0, + use_twitter=None, twitter_notify_onsnatch=None, twitter_notify_ondownload=None, twitter_notify_onsubtitledownload=None, + use_notifo=None, notifo_notify_onsnatch=None, notifo_notify_ondownload=None, notifo_notify_onsubtitledownload=None, notifo_username=None, notifo_apisecret=None, + use_boxcar=None, boxcar_notify_onsnatch=None, boxcar_notify_ondownload=None, boxcar_notify_onsubtitledownload=None, boxcar_username=None, + use_pushover=None, pushover_notify_onsnatch=None, pushover_notify_ondownload=None, pushover_notify_onsubtitledownload=None, pushover_userkey=None, + use_libnotify=None, libnotify_notify_onsnatch=None, libnotify_notify_ondownload=None, libnotify_notify_onsubtitledownload=None, + use_nmj=None, nmj_host=None, nmj_database=None, nmj_mount=None, use_synoindex=None, + use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None, + use_trakt=None, trakt_username=None, trakt_password=None, trakt_api=None,trakt_remove_watchlist=None,trakt_use_watchlist=None,trakt_start_paused=None,trakt_method_add=None, + use_synologynotifier=None, synologynotifier_notify_onsnatch=None, synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None, + use_pytivo=None, pytivo_notify_onsnatch=None, pytivo_notify_ondownload=None, pytivo_notify_onsubtitledownload=None, pytivo_update_library=None, + pytivo_host=None, pytivo_share_name=None, pytivo_tivo_name=None, + use_nma=None, nma_notify_onsnatch=None, nma_notify_ondownload=None, nma_notify_onsubtitledownload=None, nma_api=None, nma_priority=0, + use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, + use_mail=None, mail_username=None, mail_password=None, mail_server=None, mail_ssl=None, mail_from=None, mail_to=None, mail_notify_onsnatch=None ): + + + + results = [] + + if xbmc_notify_onsnatch == "on": + xbmc_notify_onsnatch = 1 + else: + xbmc_notify_onsnatch = 0 + + if xbmc_notify_ondownload == "on": + xbmc_notify_ondownload = 1 + else: + xbmc_notify_ondownload = 0 + + if xbmc_notify_onsubtitledownload == "on": + xbmc_notify_onsubtitledownload = 1 + else: + xbmc_notify_onsubtitledownload = 0 + + if xbmc_update_library == "on": + xbmc_update_library = 1 + else: + xbmc_update_library = 0 + + if xbmc_update_full == "on": + xbmc_update_full = 1 + else: + xbmc_update_full = 0 + + if xbmc_update_onlyfirst == "on": + xbmc_update_onlyfirst = 1 + else: + xbmc_update_onlyfirst = 0 + + if use_xbmc == "on": + use_xbmc = 1 + else: + use_xbmc = 0 + + if plex_update_library == "on": + plex_update_library = 1 + else: + plex_update_library = 0 + + if plex_notify_onsnatch == "on": + plex_notify_onsnatch = 1 + else: + plex_notify_onsnatch = 0 + + if plex_notify_ondownload == "on": + plex_notify_ondownload = 1 + else: + plex_notify_ondownload = 0 + + if plex_notify_onsubtitledownload == "on": + plex_notify_onsubtitledownload = 1 + else: + plex_notify_onsubtitledownload = 0 + + if use_plex == "on": + use_plex = 1 + else: + use_plex = 0 + + if growl_notify_onsnatch == "on": + growl_notify_onsnatch = 1 + else: + growl_notify_onsnatch = 0 + + if growl_notify_ondownload == "on": + growl_notify_ondownload = 1 + else: + growl_notify_ondownload = 0 + + if growl_notify_onsubtitledownload == "on": + growl_notify_onsubtitledownload = 1 + else: + growl_notify_onsubtitledownload = 0 + + if use_growl == "on": + use_growl = 1 + else: + use_growl = 0 + + if prowl_notify_onsnatch == "on": + prowl_notify_onsnatch = 1 + else: + prowl_notify_onsnatch = 0 + + if prowl_notify_ondownload == "on": + prowl_notify_ondownload = 1 + else: + prowl_notify_ondownload = 0 + + if prowl_notify_onsubtitledownload == "on": + prowl_notify_onsubtitledownload = 1 + else: + prowl_notify_onsubtitledownload = 0 + + if use_prowl == "on": + use_prowl = 1 + else: + use_prowl = 0 + + if twitter_notify_onsnatch == "on": + twitter_notify_onsnatch = 1 + else: + twitter_notify_onsnatch = 0 + + if twitter_notify_ondownload == "on": + twitter_notify_ondownload = 1 + else: + twitter_notify_ondownload = 0 + + if twitter_notify_onsubtitledownload == "on": + twitter_notify_onsubtitledownload = 1 + else: + twitter_notify_onsubtitledownload = 0 + + if use_twitter == "on": + use_twitter = 1 + else: + use_twitter = 0 + + if notifo_notify_onsnatch == "on": + notifo_notify_onsnatch = 1 + else: + notifo_notify_onsnatch = 0 + + if notifo_notify_ondownload == "on": + notifo_notify_ondownload = 1 + else: + notifo_notify_ondownload = 0 + + if notifo_notify_onsubtitledownload == "on": + notifo_notify_onsubtitledownload = 1 + else: + notifo_notify_onsubtitledownload = 0 + + if use_notifo == "on": + use_notifo = 1 + else: + use_notifo = 0 + + if boxcar_notify_onsnatch == "on": + boxcar_notify_onsnatch = 1 + else: + boxcar_notify_onsnatch = 0 + + if boxcar_notify_ondownload == "on": + boxcar_notify_ondownload = 1 + else: + boxcar_notify_ondownload = 0 + + if boxcar_notify_onsubtitledownload == "on": + boxcar_notify_onsubtitledownload = 1 + else: + boxcar_notify_onsubtitledownload = 0 + + if use_boxcar == "on": + use_boxcar = 1 + else: + use_boxcar = 0 + + if pushover_notify_onsnatch == "on": + pushover_notify_onsnatch = 1 + else: + pushover_notify_onsnatch = 0 + + if pushover_notify_ondownload == "on": + pushover_notify_ondownload = 1 + else: + pushover_notify_ondownload = 0 + + if pushover_notify_onsubtitledownload == "on": + pushover_notify_onsubtitledownload = 1 + else: + pushover_notify_onsubtitledownload = 0 + + if use_pushover == "on": + use_pushover = 1 + else: + use_pushover = 0 + + if use_nmj == "on": + use_nmj = 1 + else: + use_nmj = 0 + + if use_synoindex == "on": + use_synoindex = 1 + else: + use_synoindex = 0 + + if use_synologynotifier == "on": + use_synologynotifier = 1 + else: + use_synologynotifier = 0 + + if synologynotifier_notify_onsnatch == "on": + synologynotifier_notify_onsnatch = 1 + else: + synologynotifier_notify_onsnatch = 0 + + if synologynotifier_notify_ondownload == "on": + synologynotifier_notify_ondownload = 1 + else: + synologynotifier_notify_ondownload = 0 + + if synologynotifier_notify_onsubtitledownload == "on": + synologynotifier_notify_onsubtitledownload = 1 + else: + synologynotifier_notify_onsubtitledownload = 0 + + if use_nmjv2 == "on": + use_nmjv2 = 1 + else: + use_nmjv2 = 0 + + if use_trakt == "on": + use_trakt = 1 + else: + use_trakt = 0 + if trakt_remove_watchlist == "on": + trakt_remove_watchlist = 1 + else: + trakt_remove_watchlist = 0 + + if trakt_use_watchlist == "on": + trakt_use_watchlist = 1 + else: + trakt_use_watchlist = 0 + + if trakt_start_paused == "on": + trakt_start_paused = 1 + else: + trakt_start_paused = 0 + + if use_pytivo == "on": + use_pytivo = 1 + else: + use_pytivo = 0 + + if pytivo_notify_onsnatch == "on": + pytivo_notify_onsnatch = 1 + else: + pytivo_notify_onsnatch = 0 + + if pytivo_notify_ondownload == "on": + pytivo_notify_ondownload = 1 + else: + pytivo_notify_ondownload = 0 + + if pytivo_notify_onsubtitledownload == "on": + pytivo_notify_onsubtitledownload = 1 + else: + pytivo_notify_onsubtitledownload = 0 + + if pytivo_update_library == "on": + pytivo_update_library = 1 + else: + pytivo_update_library = 0 + + if use_nma == "on": + use_nma = 1 + else: + use_nma = 0 + + if nma_notify_onsnatch == "on": + nma_notify_onsnatch = 1 + else: + nma_notify_onsnatch = 0 + + if nma_notify_ondownload == "on": + nma_notify_ondownload = 1 + else: + nma_notify_ondownload = 0 + + if nma_notify_onsubtitledownload == "on": + nma_notify_onsubtitledownload = 1 + else: + nma_notify_onsubtitledownload = 0 + + if use_mail == "on": + use_mail = 1 + else: + use_mail = 0 + + if mail_ssl == "on": + mail_ssl = 1 + else: + mail_ssl = 0 + + if mail_notify_onsnatch == "on": + mail_notify_onsnatch = 1 + else: + mail_notify_onsnatch = 0 + + if use_pushalot == "on": + use_pushalot = 1 + else: + use_pushalot = 0 + + if pushalot_notify_onsnatch == "on": + pushalot_notify_onsnatch = 1 + else: + pushalot_notify_onsnatch = 0 + + if pushalot_notify_ondownload == "on": + pushalot_notify_ondownload = 1 + else: + pushalot_notify_ondownload = 0 + + if pushalot_notify_onsubtitledownload == "on": + pushalot_notify_onsubtitledownload = 1 + else: + pushalot_notify_onsubtitledownload = 0 + + + sickbeard.USE_XBMC = use_xbmc + sickbeard.XBMC_NOTIFY_ONSNATCH = xbmc_notify_onsnatch + sickbeard.XBMC_NOTIFY_ONDOWNLOAD = xbmc_notify_ondownload + sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD = xbmc_notify_onsubtitledownload + sickbeard.XBMC_UPDATE_LIBRARY = xbmc_update_library + sickbeard.XBMC_UPDATE_FULL = xbmc_update_full + sickbeard.XBMC_UPDATE_ONLYFIRST = xbmc_update_onlyfirst + sickbeard.XBMC_HOST = xbmc_host + sickbeard.XBMC_USERNAME = xbmc_username + sickbeard.XBMC_PASSWORD = xbmc_password + + sickbeard.USE_PLEX = use_plex + sickbeard.PLEX_NOTIFY_ONSNATCH = plex_notify_onsnatch + sickbeard.PLEX_NOTIFY_ONDOWNLOAD = plex_notify_ondownload + sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = plex_notify_onsubtitledownload + sickbeard.PLEX_UPDATE_LIBRARY = plex_update_library + sickbeard.PLEX_HOST = plex_host + sickbeard.PLEX_SERVER_HOST = plex_server_host + sickbeard.PLEX_USERNAME = plex_username + sickbeard.PLEX_PASSWORD = plex_password + + sickbeard.USE_GROWL = use_growl + sickbeard.GROWL_NOTIFY_ONSNATCH = growl_notify_onsnatch + sickbeard.GROWL_NOTIFY_ONDOWNLOAD = growl_notify_ondownload + sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD = growl_notify_onsubtitledownload + sickbeard.GROWL_HOST = growl_host + sickbeard.GROWL_PASSWORD = growl_password + + sickbeard.USE_PROWL = use_prowl + sickbeard.PROWL_NOTIFY_ONSNATCH = prowl_notify_onsnatch + sickbeard.PROWL_NOTIFY_ONDOWNLOAD = prowl_notify_ondownload + sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD = prowl_notify_onsubtitledownload + sickbeard.PROWL_API = prowl_api + sickbeard.PROWL_PRIORITY = prowl_priority + + sickbeard.USE_TWITTER = use_twitter + sickbeard.TWITTER_NOTIFY_ONSNATCH = twitter_notify_onsnatch + sickbeard.TWITTER_NOTIFY_ONDOWNLOAD = twitter_notify_ondownload + sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD = twitter_notify_onsubtitledownload + + sickbeard.USE_NOTIFO = use_notifo + sickbeard.NOTIFO_NOTIFY_ONSNATCH = notifo_notify_onsnatch + sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD = notifo_notify_ondownload + sickbeard.NOTIFO_NOTIFY_ONSUBTITLEDOWNLOAD = notifo_notify_onsubtitledownload + sickbeard.NOTIFO_USERNAME = notifo_username + sickbeard.NOTIFO_APISECRET = notifo_apisecret + + sickbeard.USE_BOXCAR = use_boxcar + sickbeard.BOXCAR_NOTIFY_ONSNATCH = boxcar_notify_onsnatch + sickbeard.BOXCAR_NOTIFY_ONDOWNLOAD = boxcar_notify_ondownload + sickbeard.BOXCAR_NOTIFY_ONSUBTITLEDOWNLOAD = boxcar_notify_onsubtitledownload + sickbeard.BOXCAR_USERNAME = boxcar_username + + sickbeard.USE_PUSHOVER = use_pushover + sickbeard.PUSHOVER_NOTIFY_ONSNATCH = pushover_notify_onsnatch + sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD = pushover_notify_ondownload + sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = pushover_notify_onsubtitledownload + sickbeard.PUSHOVER_USERKEY = pushover_userkey + + sickbeard.USE_LIBNOTIFY = use_libnotify == "on" + sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH = libnotify_notify_onsnatch == "on" + sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD = libnotify_notify_ondownload == "on" + sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD = libnotify_notify_onsubtitledownload == "on" + + sickbeard.USE_NMJ = use_nmj + sickbeard.NMJ_HOST = nmj_host + sickbeard.NMJ_DATABASE = nmj_database + sickbeard.NMJ_MOUNT = nmj_mount + + sickbeard.USE_SYNOINDEX = use_synoindex + + sickbeard.USE_SYNOLOGYNOTIFIER = use_synologynotifier + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH = synologynotifier_notify_onsnatch + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD = synologynotifier_notify_ondownload + sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD = synologynotifier_notify_onsubtitledownload + + sickbeard.USE_NMJv2 = use_nmjv2 + sickbeard.NMJv2_HOST = nmjv2_host + sickbeard.NMJv2_DATABASE = nmjv2_database + sickbeard.NMJv2_DBLOC = nmjv2_dbloc + + sickbeard.USE_TRAKT = use_trakt + sickbeard.TRAKT_USERNAME = trakt_username + sickbeard.TRAKT_PASSWORD = trakt_password + sickbeard.TRAKT_API = trakt_api + sickbeard.TRAKT_REMOVE_WATCHLIST = trakt_remove_watchlist + sickbeard.TRAKT_USE_WATCHLIST = trakt_use_watchlist + sickbeard.TRAKT_METHOD_ADD = trakt_method_add + sickbeard.TRAKT_START_PAUSED = trakt_start_paused + + sickbeard.USE_PYTIVO = use_pytivo + sickbeard.PYTIVO_NOTIFY_ONSNATCH = pytivo_notify_onsnatch == "off" + sickbeard.PYTIVO_NOTIFY_ONDOWNLOAD = pytivo_notify_ondownload == "off" + sickbeard.PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD = pytivo_notify_onsubtitledownload == "off" + sickbeard.PYTIVO_UPDATE_LIBRARY = pytivo_update_library + sickbeard.PYTIVO_HOST = pytivo_host + sickbeard.PYTIVO_SHARE_NAME = pytivo_share_name + sickbeard.PYTIVO_TIVO_NAME = pytivo_tivo_name + + sickbeard.USE_NMA = use_nma + sickbeard.NMA_NOTIFY_ONSNATCH = nma_notify_onsnatch + sickbeard.NMA_NOTIFY_ONDOWNLOAD = nma_notify_ondownload + sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD = nma_notify_onsubtitledownload + sickbeard.NMA_API = nma_api + sickbeard.NMA_PRIORITY = nma_priority + + sickbeard.USE_MAIL = use_mail + sickbeard.MAIL_USERNAME = mail_username + sickbeard.MAIL_PASSWORD = mail_password + sickbeard.MAIL_SERVER = mail_server + sickbeard.MAIL_SSL = mail_ssl + sickbeard.MAIL_FROM = mail_from + sickbeard.MAIL_TO = mail_to + sickbeard.MAIL_NOTIFY_ONSNATCH = mail_notify_onsnatch + + sickbeard.USE_PUSHALOT = use_pushalot + sickbeard.PUSHALOT_NOTIFY_ONSNATCH = pushalot_notify_onsnatch + sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD = pushalot_notify_ondownload + sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD = pushalot_notify_onsubtitledownload + sickbeard.PUSHALOT_AUTHORIZATIONTOKEN = pushalot_authorizationtoken + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/notifications/") + +class ConfigSubtitles: + + @cherrypy.expose + def index(self): + t = PageTemplate(file="config_subtitles.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + @cherrypy.expose + def saveSubtitles(self, use_subtitles=None, subtitles_plugins=None, subtitles_languages=None, subtitles_dir=None, subtitles_dir_sub=None, subsnolang = None, service_order=None, subtitles_history=None): + results = [] + + if use_subtitles == "on": + use_subtitles = 1 + if sickbeard.subtitlesFinderScheduler.thread == None or not sickbeard.subtitlesFinderScheduler.thread.isAlive(): + sickbeard.subtitlesFinderScheduler.initThread() + else: + use_subtitles = 0 + sickbeard.subtitlesFinderScheduler.abort = True + logger.log(u"Waiting for the SUBTITLESFINDER thread to exit") + try: + sickbeard.subtitlesFinderScheduler.thread.join(5) + except: + pass + + if subtitles_history == "on": + subtitles_history = 1 + else: + subtitles_history = 0 + + if subtitles_dir_sub == "on": + subtitles_dir_sub = 1 + else: + subtitles_dir_sub = 0 + + if subsnolang == "on": + subsnolang = 1 + else: + subsnolang = 0 + + sickbeard.USE_SUBTITLES = use_subtitles + sickbeard.SUBTITLES_LANGUAGES = [lang.alpha2 for lang in subtitles.isValidLanguage(subtitles_languages.replace(' ', '').split(','))] if subtitles_languages != '' else '' + sickbeard.SUBTITLES_DIR = subtitles_dir + sickbeard.SUBTITLES_DIR_SUB = subtitles_dir_sub + sickbeard.SUBSNOLANG = subsnolang + sickbeard.SUBTITLES_HISTORY = subtitles_history + + # Subtitles services + services_str_list = service_order.split() + subtitles_services_list = [] + subtitles_services_enabled = [] + for curServiceStr in services_str_list: + curService, curEnabled = curServiceStr.split(':') + subtitles_services_list.append(curService) + subtitles_services_enabled.append(int(curEnabled)) + + sickbeard.SUBTITLES_SERVICES_LIST = subtitles_services_list + sickbeard.SUBTITLES_SERVICES_ENABLED = subtitles_services_enabled + + sickbeard.save_config() + + if len(results) > 0: + for x in results: + logger.log(x, logger.ERROR) + ui.notifications.error('Error(s) Saving Configuration', + '<br />\n'.join(results)) + else: + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) + + redirect("/config/subtitles/") + +class Config: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="config.tmpl") + t.submenu = ConfigMenu + return _munge(t) + + general = ConfigGeneral() + + search = ConfigSearch() + + postProcessing = ConfigPostProcessing() + + providers = ConfigProviders() + + notifications = ConfigNotifications() + + subtitles = ConfigSubtitles() + +def haveXBMC(): + return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY + +def havePLEX(): + return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY + +def HomeMenu(): + return [ + { 'title': 'Add Shows', 'path': 'home/addShows/', }, + { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/' }, + { 'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': haveXBMC }, + { 'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': havePLEX }, + { 'title': 'Restart', 'path': 'home/restart/?pid='+str(sickbeard.PID), 'confirm': True }, + { 'title': 'Shutdown', 'path': 'home/shutdown/?pid='+str(sickbeard.PID), 'confirm': True }, + ] + +class HomePostProcess: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home_postprocess.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + @cherrypy.expose + def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None): + + if not dir: + redirect("/home/postprocess") + else: + result = processTV.processDir(dir, nzbName) + if quiet != None and int(quiet) == 1: + return result + + result = result.replace("\n","<br />\n") + return _genericMessage("Postprocessing results", result) + + +class NewHomeAddShows: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home_addShows.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + @cherrypy.expose + def getTVDBLanguages(self): + result = tvdb_api.Tvdb().config['valid_languages'] + + # Make sure list is sorted alphabetically but 'fr' is in front + if 'fr' in result: + del result[result.index('fr')] + result.sort() + result.insert(0, 'fr') + + return json.dumps({'results': result}) + + @cherrypy.expose + def sanitizeFileName(self, name): + return helpers.sanitizeFileName(name) + + @cherrypy.expose + def searchTVDBForShowName(self, name, lang="fr"): + if not lang or lang == 'null': + lang = "fr" + + baseURL = "http://thetvdb.com/api/GetSeries.php?" + nameUTF8 = name.encode('utf-8') + + logger.log(u"Trying to find Show on thetvdb.com with: " + nameUTF8.decode('utf-8'), logger.DEBUG) + + # Use each word in the show's name as a possible search term + keywords = nameUTF8.split(' ') + + # Insert the whole show's name as the first search term so best results are first + # ex: keywords = ['Some Show Name', 'Some', 'Show', 'Name'] + if len(keywords) > 1: + keywords.insert(0, nameUTF8) + + # Query the TVDB for each search term and build the list of results + results = [] + + for searchTerm in keywords: + params = {'seriesname': searchTerm, + 'language': lang} + + finalURL = baseURL + urllib.urlencode(params) + + logger.log(u"Searching for Show with searchterm: \'" + searchTerm.decode('utf-8') + u"\' on URL " + finalURL, logger.DEBUG) + urlData = helpers.getURL(finalURL) + + if urlData is None: + # When urlData is None, trouble connecting to TVDB, don't try the rest of the keywords + logger.log(u"Unable to get URL: " + finalURL, logger.ERROR) + break + else: + try: + seriesXML = etree.ElementTree(etree.XML(urlData)) + series = seriesXML.getiterator('Series') + + except Exception, e: + # use finalURL in log, because urlData can be too much information + logger.log(u"Unable to parse XML for some reason: " + ex(e) + " from XML: " + finalURL, logger.ERROR) + series = '' + + # add each result to our list + for curSeries in series: + tvdb_id = int(curSeries.findtext('seriesid')) + + # don't add duplicates + if tvdb_id in [x[0] for x in results]: + continue + + results.append((tvdb_id, curSeries.findtext('SeriesName'), curSeries.findtext('FirstAired'))) + + lang_id = tvdb_api.Tvdb().config['langabbv_to_id'][lang] + + return json.dumps({'results': results, 'langid': lang_id}) + + @cherrypy.expose + def massAddTable(self, rootDir=None): + t = PageTemplate(file="home_massAddTable.tmpl") + t.submenu = HomeMenu() + + myDB = db.DBConnection() + + if not rootDir: + return "No folders selected." + elif type(rootDir) != list: + root_dirs = [rootDir] + else: + root_dirs = rootDir + + root_dirs = [urllib.unquote_plus(x) for x in root_dirs] + + default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) + if len(root_dirs) > default_index: + tmp = root_dirs[default_index] + if tmp in root_dirs: + root_dirs.remove(tmp) + root_dirs = [tmp]+root_dirs + + dir_list = [] + + for root_dir in root_dirs: + try: + file_list = ek.ek(os.listdir, root_dir) + except: + continue + + for cur_file in file_list: + + cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) + if not ek.ek(os.path.isdir, cur_path): + continue + + cur_dir = { + 'dir': cur_path, + 'display_dir': '<b>'+ek.ek(os.path.dirname, cur_path)+os.sep+'</b>'+ek.ek(os.path.basename, cur_path), + } + + # see if the folder is in XBMC already + dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) + + if dirResults: + cur_dir['added_already'] = True + else: + cur_dir['added_already'] = False + + dir_list.append(cur_dir) + + tvdb_id = '' + show_name = '' + for cur_provider in sickbeard.metadata_provider_dict.values(): + (tvdb_id, show_name) = cur_provider.retrieveShowMetadata(cur_path) + if tvdb_id and show_name: + break + + cur_dir['existing_info'] = (tvdb_id, show_name) + + if tvdb_id and helpers.findCertainShow(sickbeard.showList, tvdb_id): + cur_dir['added_already'] = True + + t.dirList = dir_list + + return _munge(t) + + @cherrypy.expose + def newShow(self, show_to_add=None, other_shows=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(file="home_newShow.tmpl") + t.submenu = HomeMenu() + + show_dir, tvdb_id, show_name = self.split_extra_show(show_to_add) + + if tvdb_id and show_name: + use_provided_info = True + else: + use_provided_info = False + + # tell the template whether we're giving it show name & TVDB ID + t.use_provided_info = use_provided_info + + # use the given show_dir for the tvdb search if available + if not show_dir: + t.default_show_name = '' + elif not show_name: + t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.',' ') + else: + t.default_show_name = show_name + + # carry a list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + if use_provided_info: + t.provided_tvdb_id = tvdb_id + t.provided_tvdb_name = show_name + + t.provided_show_dir = show_dir + t.other_shows = other_shows + + return _munge(t) + + @cherrypy.expose + def addNewShow(self, whichSeries=None, tvdbLang="fr", rootDir=None, defaultStatus=None, + anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, fullShowPath=None, + other_shows=None, skipShow=None, audio_lang=None): + """ + Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are + provided then it forwards back to newShow, if not it goes to /home. + """ + + # grab our list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + def finishAddShow(): + # if there are no extra shows then go home + if not other_shows: + redirect('/home') + + # peel off the next one + next_show_dir = other_shows[0] + rest_of_show_dirs = other_shows[1:] + + # go to add the next show + return self.newShow(next_show_dir, rest_of_show_dirs) + + # if we're skipping then behave accordingly + if skipShow: + return finishAddShow() + + # sanity check on our inputs + if (not rootDir and not fullShowPath) or not whichSeries: + return "Missing params, no tvdb id or folder:"+repr(whichSeries)+" and "+repr(rootDir)+"/"+repr(fullShowPath) + + # figure out what show we're adding and where + series_pieces = whichSeries.partition('|') + if len(series_pieces) < 3: + return "Error with show selection." + + tvdb_id = int(series_pieces[0]) + show_name = series_pieces[2] + + # use the whole path if it's given, or else append the show name to the root dir to get the full show path + if fullShowPath: + show_dir = ek.ek(os.path.normpath, fullShowPath) + else: + show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) + + # blanket policy - if the dir exists you should have used "add existing show" numbnuts + if ek.ek(os.path.isdir, show_dir) and not fullShowPath: + ui.notifications.error("Unable to add show", "Folder "+show_dir+" exists already") + redirect('/home/addShows/existingShows') + + # don't create show dir if config says not to + if sickbeard.ADD_SHOWS_WO_DIR: + logger.log(u"Skipping initial creation of "+show_dir+" due to config.ini setting") + else: + dir_exists = helpers.makeDir(show_dir) + if not dir_exists: + logger.log(u"Unable to create the folder "+show_dir+", can't add the show", logger.ERROR) + ui.notifications.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") + redirect("/home") + else: + helpers.chmodAsParent(show_dir) + + # prepare the inputs for passing along + if flatten_folders == "on": + flatten_folders = 1 + else: + flatten_folders = 0 + + if subtitles == "on": + subtitles = 1 + else: + subtitles = 0 + + if not anyQualities: + anyQualities = [] + if not bestQualities: + bestQualities = [] + if type(anyQualities) != list: + anyQualities = [anyQualities] + if type(bestQualities) != list: + bestQualities = [bestQualities] + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + + # add the show + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, flatten_folders, tvdbLang, subtitles, audio_lang) #@UndefinedVariable + ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) + + return finishAddShow() + + + @cherrypy.expose + def existingShows(self): + """ + Prints out the page to add existing shows from a root dir + """ + t = PageTemplate(file="home_addExistingShow.tmpl") + t.submenu = HomeMenu() + + return _munge(t) + + def split_extra_show(self, extra_show): + if not extra_show: + return (None, None, None) + split_vals = extra_show.split('|') + if len(split_vals) < 3: + return (extra_show, None, None) + show_dir = split_vals[0] + tvdb_id = split_vals[1] + show_name = '|'.join(split_vals[2:]) + + return (show_dir, tvdb_id, show_name) + + @cherrypy.expose + def addExistingShows(self, shows_to_add=None, promptForSettings=None): + """ + Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards + along to the newShow page. + """ + + # grab a list of other shows to add, if provided + if not shows_to_add: + shows_to_add = [] + elif type(shows_to_add) != list: + shows_to_add = [shows_to_add] + + shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] + + if promptForSettings == "on": + promptForSettings = 1 + else: + promptForSettings = 0 + + tvdb_id_given = [] + dirs_only = [] + # separate all the ones with TVDB IDs + for cur_dir in shows_to_add: + if not '|' in cur_dir: + dirs_only.append(cur_dir) + else: + show_dir, tvdb_id, show_name = self.split_extra_show(cur_dir) + if not show_dir or not tvdb_id or not show_name: + continue + tvdb_id_given.append((show_dir, int(tvdb_id), show_name)) + + + # if they want me to prompt for settings then I will just carry on to the newShow page + if promptForSettings and shows_to_add: + return self.newShow(shows_to_add[0], shows_to_add[1:]) + + # if they don't want me to prompt for settings then I can just add all the nfo shows now + num_added = 0 + for cur_show in tvdb_id_given: + show_dir, tvdb_id, show_name = cur_show + + # add the show + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.FLATTEN_FOLDERS_DEFAULT, sickbeard.SUBTITLES_DEFAULT) #@UndefinedVariable + num_added += 1 + + if num_added: + ui.notifications.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") + + # if we're done then go home + if not dirs_only: + redirect('/home') + + # for the remaining shows we need to prompt for each one, so forward this on to the newShow page + return self.newShow(dirs_only[0], dirs_only[1:]) + + + + +ErrorLogsMenu = [ + { 'title': 'Clear Errors', 'path': 'errorlogs/clearerrors' }, + #{ 'title': 'View Log', 'path': 'errorlogs/viewlog' }, +] + + +class ErrorLogs: + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="errorlogs.tmpl") + t.submenu = ErrorLogsMenu + + return _munge(t) + + + @cherrypy.expose + def clearerrors(self): + classes.ErrorViewer.clear() + redirect("/errorlogs") + + @cherrypy.expose + def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): + + t = PageTemplate(file="viewlogs.tmpl") + t.submenu = ErrorLogsMenu + + minLevel = int(minLevel) + + data = [] + if os.path.isfile(logger.sb_log_instance.log_file): + f = open(logger.sb_log_instance.log_file) + data = f.readlines() + f.close() + + regex = "^(\w+).?\-(\d\d)\s+(\d\d)\:(\d\d):(\d\d)\s+([A-Z]+)\s+(.*)$" + + finalData = [] + + numLines = 0 + lastLine = False + numToShow = min(maxLines, len(data)) + + for x in reversed(data): + + x = x.decode('utf-8') + match = re.match(regex, x) + + if match: + level = match.group(6) + if level not in logger.reverseNames: + lastLine = False + continue + + if logger.reverseNames[level] >= minLevel: + lastLine = True + finalData.append(x) + else: + lastLine = False + continue + + elif lastLine: + finalData.append("AA"+x) + + numLines += 1 + + if numLines >= numToShow: + break + + result = "".join(finalData) + + t.logLines = result + t.minLevel = minLevel + + return _munge(t) + + +class Home: + + @cherrypy.expose + def is_alive(self, *args, **kwargs): + if 'callback' in kwargs and '_' in kwargs: + callback, _ = kwargs['callback'], kwargs['_'] + else: + return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query stiring." + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + cherrypy.response.headers['Content-Type'] = 'text/javascript' + cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' + cherrypy.response.headers['Access-Control-Allow-Headers'] = 'x-requested-with' + + if sickbeard.started: + return callback+'('+json.dumps({"msg": str(sickbeard.PID)})+');' + else: + return callback+'('+json.dumps({"msg": "nope"})+');' + + @cherrypy.expose + def index(self): + + t = PageTemplate(file="home.tmpl") + t.submenu = HomeMenu() + return _munge(t) + + addShows = NewHomeAddShows() + + postprocess = HomePostProcess() + + @cherrypy.expose + def testSABnzbd(self, host=None, username=None, password=None, apikey=None): + if not host.endswith("/"): + host = host + "/" + connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) + if connection: + authed, authMsg = sab.testAuthentication(host, username, password, apikey) #@UnusedVariable + if authed: + return "Success. Connected and authenticated" + else: + return "Authentication failed. SABnzbd expects '"+accesMsg+"' as authentication method" + else: + return "Unable to connect to host" + + @cherrypy.expose + def testTorrent(self, torrent_method=None, host=None, username=None, password=None): + if not host.endswith("/"): + host = host + "/" + + client = clients.getClientIstance(torrent_method) + + connection, accesMsg = client(host, username, password).testAuthentication() + + return accesMsg + + @cherrypy.expose + def testGrowl(self, host=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.growl_notifier.test_notify(host, password) + if password==None or password=='': + pw_append = '' + else: + pw_append = " with password: " + password + + if result: + return "Registered and Tested growl successfully "+urllib.unquote_plus(host)+pw_append + else: + return "Registration and Testing of growl failed "+urllib.unquote_plus(host)+pw_append + + @cherrypy.expose + def testProwl(self, prowl_api=None, prowl_priority=0): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) + if result: + return "Test prowl notice sent successfully" + else: + return "Test prowl notice failed" + + @cherrypy.expose + def testNotifo(self, username=None, apisecret=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.notifo_notifier.test_notify(username, apisecret) + if result: + return "Notifo notification succeeded. Check your Notifo clients to make sure it worked" + else: + return "Error sending Notifo notification" + + @cherrypy.expose + def testBoxcar(self, username=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.boxcar_notifier.test_notify(username) + if result: + return "Boxcar notification succeeded. Check your Boxcar clients to make sure it worked" + else: + return "Error sending Boxcar notification" + + @cherrypy.expose + def testPushover(self, userKey=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.pushover_notifier.test_notify(userKey) + if result: + return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" + else: + return "Error sending Pushover notification" + + @cherrypy.expose + def twitterStep1(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + return notifiers.twitter_notifier._get_authorization() + + @cherrypy.expose + def twitterStep2(self, key): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.twitter_notifier._get_credentials(key) + logger.log(u"result: "+str(result)) + if result: + return "Key verification successful" + else: + return "Unable to verify key" + + @cherrypy.expose + def testTwitter(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.twitter_notifier.test_notify() + if result: + return "Tweet successful, check your twitter to make sure it worked" + else: + return "Error sending tweet" + + @cherrypy.expose + def testXBMC(self, host=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + finalResult = '' + for curHost in [x.strip() for x in host.split(",")]: + curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: + finalResult += "Test XBMC notice sent successfully to " + urllib.unquote_plus(curHost) + else: + finalResult += "Test XBMC notice failed to " + urllib.unquote_plus(curHost) + finalResult += "<br />\n" + + return finalResult + + @cherrypy.expose + def testPLEX(self, host=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + finalResult = '' + for curHost in [x.strip() for x in host.split(",")]: + curResult = notifiers.plex_notifier.test_notify(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: + finalResult += "Test Plex notice sent successfully to " + urllib.unquote_plus(curHost) + else: + finalResult += "Test Plex notice failed to " + urllib.unquote_plus(curHost) + finalResult += "<br />\n" + + return finalResult + + @cherrypy.expose + def testLibnotify(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + if notifiers.libnotify_notifier.test_notify(): + return "Tried sending desktop notification via libnotify" + else: + return notifiers.libnotify.diagnose() + + @cherrypy.expose + def testNMJ(self, host=None, database=None, mount=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) + if result: + return "Successfull started the scan update" + else: + return "Test failed to start the scan update" + + @cherrypy.expose + def settingsNMJ(self, host=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) + if result: + return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % {"host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} + else: + return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' + + @cherrypy.expose + def testNMJv2(self, host=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nmjv2_notifier.test_notify(urllib.unquote_plus(host)) + if result: + return "Test notice sent successfully to " + urllib.unquote_plus(host) + else: + return "Test notice failed to " + urllib.unquote_plus(host) + + @cherrypy.expose + def settingsNMJv2(self, host=None, dbloc=None, instance=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + result = notifiers.nmjv2_notifier.notify_settings(urllib.unquote_plus(host), dbloc, instance) + if result: + return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, "database": sickbeard.NMJv2_DATABASE} + else: + return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % {"dbloc": dbloc} + + @cherrypy.expose + def testTrakt(self, api=None, username=None, password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.trakt_notifier.test_notify(api, username, password) + if result: + return "Test notice sent successfully to Trakt" + else: + return "Test notice failed to Trakt" + + @cherrypy.expose + def testMail(self, mail_from=None, mail_to=None, mail_server=None, mail_ssl=None, mail_user=None, mail_password=None): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.mail_notifier.test_notify(mail_from, mail_to, mail_server, mail_ssl, mail_user, mail_password) + if result: + return "Mail sent" + else: + return "Can't sent mail." + + @cherrypy.expose + def testNMA(self, nma_api=None, nma_priority=0): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + + result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) + if result: + return "Test NMA notice sent successfully" + else: + return "Test NMA notice failed" + + @cherrypy.expose + def shutdown(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + threading.Timer(2, sickbeard.invoke_shutdown).start() + + title = "Shutting down" + message = "Sick Beard is shutting down..." + + return _genericMessage(title, message) + + @cherrypy.expose + def restart(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + t = PageTemplate(file="restart.tmpl") + t.submenu = HomeMenu() + + # do a soft restart + threading.Timer(2, sickbeard.invoke_restart, [False]).start() + + return _munge(t) + + @cherrypy.expose + def update(self, pid=None): + + if str(pid) != str(sickbeard.PID): + redirect("/home") + + updated = sickbeard.versionCheckScheduler.action.update() #@UndefinedVariable + + if updated: + # do a hard restart + threading.Timer(2, sickbeard.invoke_restart, [False]).start() + t = PageTemplate(file="restart_bare.tmpl") + return _munge(t) + else: + return _genericMessage("Update Failed","Update wasn't successful, not restarting. Check your log for more information.") + + @cherrypy.expose + def displayShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + else: + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + myDB = db.DBConnection() + + seasonResults = myDB.select( + "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc", + [showObj.tvdbid] + ) + + sqlResults = myDB.select( + "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", + [showObj.tvdbid] + ) + + t = PageTemplate(file="displayShow.tmpl") + t.submenu = [ { 'title': 'Edit', 'path': 'home/editShow?show=%d'%showObj.tvdbid } ] + + try: + t.showLoc = (showObj.location, True) + except sickbeard.exceptions.ShowDirNotFoundException: + t.showLoc = (showObj._location, False) + + show_message = '' + + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + show_message = 'This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.' + + elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + show_message = 'The information below is in the process of being updated.' + + elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): #@UndefinedVariable + show_message = 'The episodes below are currently being refreshed from disk' + + elif sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj): #@UndefinedVariable + show_message = 'Currently downloading subtitles for this show' + + elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued to be refreshed.' + + elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued and awaiting an update.' + + elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued and awaiting subtitles download.' + + if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + t.submenu.append({ 'title': 'Delete', 'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True }) + t.submenu.append({ 'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d'%showObj.tvdbid }) + t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1'%showObj.tvdbid }) + t.submenu.append({ 'title': 'Update show in XBMC', 'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC }) + t.submenu.append({ 'title': 'Preview Rename', 'path': 'home/testRename?show=%d'%showObj.tvdbid }) + if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj) and showObj.subtitles: + t.submenu.append({ 'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d'%showObj.tvdbid }) + + t.show = showObj + t.sqlResults = sqlResults + t.seasonResults = seasonResults + t.show_message = show_message + + epCounts = {} + epCats = {} + epCounts[Overview.SKIPPED] = 0 + epCounts[Overview.WANTED] = 0 + epCounts[Overview.QUAL] = 0 + epCounts[Overview.GOOD] = 0 + epCounts[Overview.UNAIRED] = 0 + epCounts[Overview.SNATCHED] = 0 + + for curResult in sqlResults: + + curEpCat = showObj.getOverview(int(curResult["status"])) + epCats[str(curResult["season"])+"x"+str(curResult["episode"])] = curEpCat + epCounts[curEpCat] += 1 + + def titler(x): + if not x: + return x + if x.lower().startswith('a '): + x = x[2:] + elif x.lower().startswith('the '): + x = x[4:] + return x + t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) + + t.epCounts = epCounts + t.epCats = epCats + + return _munge(t) + + @cherrypy.expose + def plotDetails(self, show, season, episode): + result = db.DBConnection().action("SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", (show, season, episode)).fetchone() + return result['description'] if result else 'Episode not found.' + + @cherrypy.expose + def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[], flatten_folders=None, paused=None, directCall=False, air_by_date=None, tvdbLang=None, audio_lang=None, custom_search_names=None, subtitles=None): + + if show == None: + errString = "Invalid show ID: "+str(show) + if directCall: + return [errString] + else: + return _genericMessage("Error", errString) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + errString = "Unable to find the specified show: "+str(show) + if directCall: + return [errString] + else: + return _genericMessage("Error", errString) + + showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.tvdbid) + + if not location and not anyQualities and not bestQualities and not flatten_folders: + + t = PageTemplate(file="editShow.tmpl") + t.submenu = HomeMenu() + with showObj.lock: + t.show = showObj + + return _munge(t) + + if flatten_folders == "on": + flatten_folders = 1 + else: + flatten_folders = 0 + + logger.log(u"flatten folders: "+str(flatten_folders)) + + if paused == "on": + paused = 1 + else: + paused = 0 + + if air_by_date == "on": + air_by_date = 1 + else: + air_by_date = 0 + + if subtitles == "on": + subtitles = 1 + else: + subtitles = 0 + + + if tvdbLang and tvdbLang in tvdb_api.Tvdb().config['valid_languages']: + tvdb_lang = tvdbLang + else: + tvdb_lang = showObj.lang + + # if we changed the language then kick off an update + if tvdb_lang == showObj.lang: + do_update = False + else: + do_update = True + + if type(anyQualities) != list: + anyQualities = [anyQualities] + + if type(bestQualities) != list: + bestQualities = [bestQualities] + + if type(exceptions_list) != list: + exceptions_list = [exceptions_list] + + errors = [] + with showObj.lock: + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + showObj.quality = newQuality + + # reversed for now + if bool(showObj.flatten_folders) != bool(flatten_folders): + showObj.flatten_folders = flatten_folders + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh this show: "+ex(e)) + + showObj.paused = paused + showObj.air_by_date = air_by_date + showObj.subtitles = subtitles + showObj.lang = tvdb_lang + showObj.audio_lang = audio_lang + showObj.custom_search_names = custom_search_names + + # if we change location clear the db of episodes, change it, write to db, and rescan + if os.path.normpath(showObj._location) != os.path.normpath(location): + logger.log(os.path.normpath(showObj._location)+" != "+os.path.normpath(location), logger.DEBUG) + if not ek.ek(os.path.isdir, location): + errors.append("New location <tt>%s</tt> does not exist" % location) + + # don't bother if we're going to update anyway + elif not do_update: + # change it + try: + showObj.location = location + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append("Unable to refresh this show:"+ex(e)) + # grab updated info from TVDB + #showObj.loadEpisodesFromTVDB() + # rescan the episodes in the new folder + except exceptions.NoNFOException: + errors.append("The folder at <tt>%s</tt> doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in Sick Beard." % location) + + # save it to the DB + showObj.saveToDB() + + # force the update + if do_update: + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable + time.sleep(1) + except exceptions.CantUpdateException, e: + errors.append("Unable to force an update on the show.") + + if directCall: + return errors + + if len(errors) > 0: + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + '<ul>' + '\n'.join(['<li>%s</li>' % error for error in errors]) + "</ul>") + + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def deleteShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") + + showObj.deleteShow() + + ui.notifications.message('<b>%s</b> has been deleted' % showObj.name) + redirect("/home") + + @cherrypy.expose + def refreshShow(self, show=None): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + # force the update from the DB + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable + except exceptions.CantRefreshException, e: + ui.notifications.error("Unable to refresh this show.", + ex(e)) + + time.sleep(3) + + redirect("/home/displayShow?show="+str(showObj.tvdbid)) + + @cherrypy.expose + def updateShow(self, show=None, force=0): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + # force the update + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) #@UndefinedVariable + except exceptions.CantUpdateException, e: + ui.notifications.error("Unable to update this show.", + ex(e)) + + # just give it some time + time.sleep(3) + + redirect("/home/displayShow?show=" + str(showObj.tvdbid)) + + @cherrypy.expose + def subtitleShow(self, show=None, force=0): + + if show == None: + return _genericMessage("Error", "Invalid show ID") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Unable to find the specified show") + + # search and download subtitles + sickbeard.showQueueScheduler.action.downloadSubtitles(showObj, bool(force)) #@UndefinedVariable + + time.sleep(3) + + redirect("/home/displayShow?show="+str(showObj.tvdbid)) + + + @cherrypy.expose + def updateXBMC(self, showName=None): + if sickbeard.XBMC_UPDATE_ONLYFIRST: + # only send update to first host in the list -- workaround for xbmc sql backend users + host = sickbeard.XBMC_HOST.split(",")[0].strip() + else: + host = sickbeard.XBMC_HOST + + if notifiers.xbmc_notifier.update_library(showName=showName): + ui.notifications.message("Library update command sent to XBMC host(s): " + host) + else: + ui.notifications.error("Unable to contact one or more XBMC host(s): " + host) + redirect('/home') + + @cherrypy.expose + def updatePLEX(self): + if notifiers.plex_notifier.update_library(): + ui.notifications.message("Library update command sent to Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) + else: + ui.notifications.error("Unable to contact Plex Media Server host: " + sickbeard.PLEX_SERVER_HOST) + redirect('/home') + + @cherrypy.expose + def setStatus(self, show=None, eps=None, status=None, direct=False): + + if show == None or eps == None or status == None: + errMsg = "You must specify a show and at least one episode" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + if not statusStrings.has_key(int(status)): + errMsg = "Invalid status" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + errMsg = "Error", "Show not in show list" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + segment_list = [] + + if eps != None: + + for curEp in eps.split('|'): + + logger.log(u"Attempting to set status on episode "+curEp+" to "+status, logger.DEBUG) + + epInfo = curEp.split('x') + + epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + + if int(status) == WANTED: + # figure out what segment the episode is in and remember it so we can backlog it + if epObj.show.air_by_date: + ep_segment = str(epObj.airdate)[:7] + else: + ep_segment = epObj.season + + if ep_segment not in segment_list: + segment_list.append(ep_segment) + + if epObj == None: + return _genericMessage("Error", "Episode couldn't be retrieved") + + with epObj.lock: + # don't let them mess up UNAIRED episodes + if epObj.status == UNAIRED: + logger.log(u"Refusing to change status of "+curEp+" because it is UNAIRED", logger.ERROR) + continue + + if int(status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED + [IGNORED] and not ek.ek(os.path.isfile, epObj.location): + logger.log(u"Refusing to change status of "+curEp+" to DOWNLOADED because it's not SNATCHED/DOWNLOADED", logger.ERROR) + continue + + epObj.status = int(status) + epObj.saveToDB() + + msg = "Backlog was automatically started for the following seasons of <b>"+showObj.name+"</b>:<br />" + for cur_segment in segment_list: + msg += "<li>Season "+str(cur_segment)+"</li>" + logger.log(u"Sending backlog for "+showObj.name+" season "+str(cur_segment)+" because some eps were set to wanted") + cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable + msg += "</ul>" + + if segment_list: + ui.notifications.message("Backlog started", msg) + + if direct: + return json.dumps({'result': 'success'}) + else: + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def setAudio(self, show=None, eps=None, audio_langs=None, direct=False): + + if show == None or eps == None or audio_langs == None: + errMsg = "You must specify a show and at least one episode" + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return _genericMessage("Error", errMsg) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + try: + show_loc = showObj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + ep_obj_rename_list = [] + + for curEp in eps.split('|'): + + logger.log(u"Attempting to set audio on episode "+curEp+" to "+audio_langs, logger.DEBUG) + + epInfo = curEp.split('x') + + epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + + epObj.audio_langs = str(audio_langs) + epObj.saveToDB() + + if direct: + return json.dumps({'result': 'success'}) + else: + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def testRename(self, show=None): + + if show == None: + return _genericMessage("Error", "You must specify a show") + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj == None: + return _genericMessage("Error", "Show not in show list") + + try: + show_loc = showObj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + ep_obj_rename_list = [] + + ep_obj_list = showObj.getAllEpisodes(has_location=True) + + for cur_ep_obj in ep_obj_list: + # Only want to rename if we have a location + if cur_ep_obj.location: + if cur_ep_obj.relatedEps: + # do we have one of multi-episodes in the rename list already + have_already = False + for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: + if cur_related_ep in ep_obj_rename_list: + have_already = True + break + if not have_already: + ep_obj_rename_list.append(cur_ep_obj) + + else: + ep_obj_rename_list.append(cur_ep_obj) + + if ep_obj_rename_list: + # present season DESC episode DESC on screen + ep_obj_rename_list.reverse() + + t = PageTemplate(file="testRename.tmpl") + t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.tvdbid}] + t.ep_obj_list = ep_obj_rename_list + t.show = showObj + + return _munge(t) + + @cherrypy.expose + def doRename(self, show=None, eps=None): + + if show == None or eps == None: + errMsg = "You must specify a show and at least one episode" + return _genericMessage("Error", errMsg) + + show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if show_obj == None: + errMsg = "Error", "Show not in show list" + return _genericMessage("Error", errMsg) + + try: + show_loc = show_obj.location #@UnusedVariable + except exceptions.ShowDirNotFoundException: + return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + + myDB = db.DBConnection() + + if eps == None: + redirect("/home/displayShow?show=" + show) + + for curEp in eps.split('|'): + + epInfo = curEp.split('x') + + # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database + ep_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", [show, epInfo[0], epInfo[1]]) + if not ep_result: + logger.log(u"Unable to find an episode for "+curEp+", skipping", logger.WARNING) + continue + related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", [ep_result[0]["location"], epInfo[1]]) + + root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) + for cur_related_ep in related_eps_result: + related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) + if related_ep_obj not in root_ep_obj.relatedEps: + root_ep_obj.relatedEps.append(related_ep_obj) + + root_ep_obj.rename() + + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def trunchistory(self, epid): + + myDB = db.DBConnection() + nbep = myDB.select("Select count(*) from episode_links where episode_id=?",[epid]) + myDB.action("DELETE from episode_links where episode_id=?",[epid]) + messnum = str(nbep[0][0]) + ' history links deleted' + ui.notifications.message('Episode History Truncated' , messnum) + return json.dumps({'result': 'ok'}) + + @cherrypy.expose + def searchEpisode(self, show=None, season=None, episode=None): + + # retrieve the episode object and fail if we can't get one + ep_obj = _getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj) + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) #@UndefinedVariable + + # wait until the queue item tells us whether it worked or not + while ep_queue_item.success == None: #@UndefinedVariable + time.sleep(1) + + # return the correct json value + if ep_queue_item.success: + return json.dumps({'result': statusStrings[ep_obj.status]}) + + return json.dumps({'result': 'failure'}) + + @cherrypy.expose + def searchEpisodeSubtitles(self, show=None, season=None, episode=None): + + # retrieve the episode object and fail if we can't get one + ep_obj = _getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # try do download subtitles for that episode + previous_subtitles = ep_obj.subtitles + try: + subtitles = ep_obj.downloadSubtitles() + + if sickbeard.SUBTITLES_DIR: + for video in subtitles: + subs_new_path = ek.ek(os.path.join, os.path.dirname(video.path), sickbeard.SUBTITLES_DIR) + dir_exists = helpers.makeDir(subs_new_path) + if not dir_exists: + logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) + else: + helpers.chmodAsParent(subs_new_path) + + for subtitle in subtitles.get(video): + new_file_path = ek.ek(os.path.join, subs_new_path, os.path.basename(subtitle.path)) + helpers.moveFile(subtitle.path, new_file_path) + if sickbeard.SUBSNOLANG: + helpers.copyFile(new_file_path,new_file_path[:-6]+"srt") + helpers.chmodAsParent(new_file_path[:-6]+"srt") + helpers.chmodAsParent(new_file_path) + else: + if sickbeard.SUBTITLES_DIR_SUB: + for video in subtitles: + subs_new_path = os.path.join(os.path.dirname(video.path),"Subs") + dir_exists = helpers.makeDir(subs_new_path) + if not dir_exists: + logger.log(u"Unable to create subtitles folder "+subs_new_path, logger.ERROR) + else: + helpers.chmodAsParent(subs_new_path) + + for subtitle in subtitles.get(video): + new_file_path = ek.ek(os.path.join, subs_new_path, os.path.basename(subtitle.path)) + helpers.moveFile(subtitle.path, new_file_path) + if sickbeard.SUBSNOLANG: + helpers.copyFile(new_file_path,new_file_path[:-6]+"srt") + helpers.chmodAsParent(new_file_path[:-6]+"srt") + helpers.chmodAsParent(new_file_path) + else: + for video in subtitles: + for subtitle in subtitles.get(video): + if sickbeard.SUBSNOLANG: + helpers.copyFile(subtitle.path,subtitle.path[:-6]+"srt") + helpers.chmodAsParent(subtitle.path[:-6]+"srt") + helpers.chmodAsParent(subtitle.path) + except: + return json.dumps({'result': 'failure'}) + + # return the correct json value + if previous_subtitles != ep_obj.subtitles: + status = 'New subtitles downloaded: %s' % ' '.join(["<img src='"+sickbeard.WEB_ROOT+"/images/flags/"+subliminal.language.Language(x).alpha2+".png' alt='"+subliminal.language.Language(x).name+"'/>" for x in sorted(list(set(ep_obj.subtitles).difference(previous_subtitles)))]) + else: + status = 'No subtitles downloaded' + ui.notifications.message('Subtitles Search', status) + return json.dumps({'result': status, 'subtitles': ','.join([x for x in ep_obj.subtitles])}) + + @cherrypy.expose + def mergeEpisodeSubtitles(self, show=None, season=None, episode=None): + + # retrieve the episode object and fail if we can't get one + ep_obj = _getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # try do merge subtitles for that episode + try: + ep_obj.mergeSubtitles() + except Exception as e: + return json.dumps({'result': 'failure', 'exception': str(e)}) + + # return the correct json value + status = 'Subtitles merged successfully ' + ui.notifications.message('Merge Subtitles', status) + return json.dumps({'result': 'ok'}) + +class UI: + + @cherrypy.expose + def add_message(self): + + ui.notifications.message('Test 1', 'This is test number 1') + ui.notifications.error('Test 2', 'This is test number 2') + + return "ok" + + @cherrypy.expose + def get_messages(self): + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(): + messages['notification-'+str(cur_notification_num)] = {'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type} + cur_notification_num += 1 + + return json.dumps(messages) + + +class WebInterface: + + @cherrypy.expose + def index(self): + + redirect("/home") + + @cherrypy.expose + def showPoster(self, show=None, which=None): + + #Redirect initial poster/banner thumb to default images + if which[0:6] == 'poster': + default_image_name = 'poster.png' + else: + default_image_name = 'banner.png' + + default_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', default_image_name) + if show is None: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + else: + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + + cache_obj = image_cache.ImageCache() + + if which == 'poster': + image_file_name = cache_obj.poster_path(showObj.tvdbid) + if which == 'poster_thumb': + image_file_name = cache_obj.poster_thumb_path(showObj.tvdbid) + if which == 'banner': + image_file_name = cache_obj.banner_path(showObj.tvdbid) + if which == 'banner_thumb': + image_file_name = cache_obj.banner_thumb_path(showObj.tvdbid) + + if ek.ek(os.path.isfile, image_file_name): + return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg") + else: + return cherrypy.lib.static.serve_file(default_image_path, content_type="image/png") + @cherrypy.expose + def setHomeLayout(self, layout): + + if layout not in ('poster', 'banner', 'simple'): + layout = 'poster' + + sickbeard.HOME_LAYOUT = layout + + redirect("/home") + @cherrypy.expose + def toggleDisplayShowSpecials(self, show): + + sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS + + redirect("/home/displayShow?show=" + show) + + @cherrypy.expose + def setComingEpsLayout(self, layout): + if layout not in ('poster', 'banner', 'list'): + layout = 'banner' + + sickbeard.COMING_EPS_LAYOUT = layout + + redirect("/comingEpisodes") + + @cherrypy.expose + def toggleComingEpsDisplayPaused(self): + + sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED + + redirect("/comingEpisodes") + + @cherrypy.expose + def setComingEpsSort(self, sort): + if sort not in ('date', 'network', 'show'): + sort = 'date' + + sickbeard.COMING_EPS_SORT = sort + + redirect("/comingEpisodes") + + @cherrypy.expose + def comingEpisodes(self, layout="None"): + + # get local timezone and load network timezones + sb_timezone = tz.tzlocal() + network_dict = network_timezones.load_network_dict() + + myDB = db.DBConnection() + + today1 = datetime.date.today() + today = today1.toordinal() + next_week1 = (datetime.date.today() + datetime.timedelta(days=7)) + next_week = next_week1.toordinal() + recently = (datetime.date.today() - datetime.timedelta(days=sickbeard.COMING_EPS_MISSED_RANGE)).toordinal() + + done_show_list = [] + qualList = Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED, IGNORED] + sql_results1 = myDB.select("SELECT *, 0 as localtime, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND airdate >= ? AND airdate < ? AND tv_shows.tvdb_id = tv_episodes.showid AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, next_week] + qualList) + for cur_result in sql_results1: + done_show_list.append(helpers.tryInt(cur_result["showid"])) + + more_sql_results = myDB.select("SELECT *, tv_shows.status as show_status FROM tv_episodes outer_eps, tv_shows WHERE season != 0 AND showid NOT IN ("+','.join(['?']*len(done_show_list))+") AND tv_shows.tvdb_id = outer_eps.showid AND airdate IN (SELECT airdate FROM tv_episodes inner_eps WHERE inner_eps.showid = outer_eps.showid AND inner_eps.airdate >= ? AND inner_eps.status NOT IN ("+','.join(['?']*len(Quality.DOWNLOADED+Quality.SNATCHED))+") ORDER BY inner_eps.airdate ASC LIMIT 1)", done_show_list + [next_week] + Quality.DOWNLOADED + Quality.SNATCHED) + sql_results1 += more_sql_results + + more_sql_results = myDB.select("SELECT *, 0 as localtime, tv_shows.status as show_status FROM tv_episodes, tv_shows WHERE season != 0 AND tv_shows.tvdb_id = tv_episodes.showid AND airdate < ? AND airdate >= ? AND tv_episodes.status = ? AND tv_episodes.status NOT IN ("+','.join(['?']*len(qualList))+")", [today, recently, WANTED] + qualList) + sql_results1 += more_sql_results + + # sort by localtime + sorts = { + 'date': (lambda x, y: cmp(x["localtime"], y["localtime"])), + 'show': (lambda a, b: cmp((a["show_name"], a["localtime"]), (b["show_name"], b["localtime"]))), + 'network': (lambda a, b: cmp((a["network"], a["localtime"]), (b["network"], b["localtime"]))), + } + + # make a dict out of the sql results + sql_results = [dict(row) for row in sql_results1] + + # regex to parse time (12/24 hour format) + time_regex = re.compile(r"(\d{1,2}):(\d{2,2})( [PA]M)?\b", flags=re.IGNORECASE) + + # add localtime to the dict + for index, item in enumerate(sql_results1): + mo = time_regex.search(item['airs']) + if mo != None and len(mo.groups()) >= 2: + try: + hr = helpers.tryInt(mo.group(1)) + m = helpers.tryInt(mo.group(2)) + ap = mo.group(3) + # convert am/pm to 24 hour clock + if ap != None: + if ap.lower() == u" pm" and hr != 12: + hr += 12 + elif ap.lower() == u" am" and hr == 12: + hr -= 12 + except: + hr = 0 + m = 0 + else: + hr = 0 + m = 0 + if hr < 0 or hr > 23 or m < 0 or m > 59: + hr = 0 + m = 0 + + te = datetime.datetime.fromordinal(helpers.tryInt(item['airdate'])) + foreign_timezone = network_timezones.get_network_timezone(item['network'], network_dict, sb_timezone) + foreign_naive = datetime.datetime(te.year, te.month, te.day, hr, m,tzinfo=foreign_timezone) + sql_results[index]['localtime'] = foreign_naive.astimezone(sb_timezone) + + #Normalize/Format the Airing Time + try: + locale.setlocale(locale.LC_TIME, 'us_US') + sql_results[index]['localtime_string'] = sql_results[index]['localtime'].strftime("%A %H:%M %p") + locale.setlocale(locale.LC_ALL, '') #Reseting to default locale + except: + sql_results[index]['localtime_string'] = sql_results[index]['localtime'].strftime("%A %H:%M %p") + + sql_results.sort(sorts[sickbeard.COMING_EPS_SORT]) + + t = PageTemplate(file="comingEpisodes.tmpl") +# paused_item = { 'title': '', 'path': 'toggleComingEpsDisplayPaused' } +# paused_item['title'] = 'Hide Paused' if sickbeard.COMING_EPS_DISPLAY_PAUSED else 'Show Paused' + paused_item = { 'title': 'View Paused:', 'path': {'': ''} } + paused_item['path'] = {'Hide': 'toggleComingEpsDisplayPaused'} if sickbeard.COMING_EPS_DISPLAY_PAUSED else {'Show': 'toggleComingEpsDisplayPaused'} + t.submenu = [ + { 'title': 'Sort by:', 'path': {'Date': 'setComingEpsSort/?sort=date', + 'Show': 'setComingEpsSort/?sort=show', + 'Network': 'setComingEpsSort/?sort=network', + }}, + + { 'title': 'Layout:', 'path': {'Banner': 'setComingEpsLayout/?layout=banner', + 'Poster': 'setComingEpsLayout/?layout=poster', + 'List': 'setComingEpsLayout/?layout=list', + }}, + paused_item, + ] + + t.next_week = datetime.datetime.combine(next_week1, datetime.time(tzinfo=sb_timezone)) + t.today = datetime.datetime.now().replace(tzinfo=sb_timezone) + t.sql_results = sql_results + + # Allow local overriding of layout parameter + if layout and layout in ('poster', 'banner', 'list'): + t.layout = layout + else: + t.layout = sickbeard.COMING_EPS_LAYOUT + + + return _munge(t) + + # Raw iCalendar implementation by Pedro Jose Pereira Vieito (@pvieito). + # + # iCalendar (iCal) - Standard RFC 5545 <http://tools.ietf.org/html/rfc5546> + # Works with iCloud, Google Calendar and Outlook. + @cherrypy.expose + def calendar(self): + """ Provides a subscribeable URL for iCal subscriptions + """ + + logger.log(u"Receiving iCal request from %s" % cherrypy.request.remote.ip) + + poster_url = cherrypy.url().replace('ical', '') + + time_re = re.compile('([0-9]{1,2})\:([0-9]{2})(\ |)([AM|am|PM|pm]{2})') + + # Create a iCal string + ical = 'BEGIN:VCALENDAR\n' + ical += 'VERSION:2.0\n' + ical += 'PRODID://Sick-Beard Upcoming Episodes//\n' + + # Get shows info + myDB = db.DBConnection() + + # Limit dates + past_date = (datetime.date.today() + datetime.timedelta(weeks=-52)).toordinal() + future_date = (datetime.date.today() + datetime.timedelta(weeks=52)).toordinal() + + # Get all the shows that are not paused and are currently on air (from kjoconnor Fork) + calendar_shows = myDB.select("SELECT show_name, tvdb_id, network, airs, runtime FROM tv_shows WHERE status = 'Continuing' AND paused != '1'") + for show in calendar_shows: + # Get all episodes of this show airing between today and next month + episode_list = myDB.select("SELECT tvdbid, name, season, episode, description, airdate FROM tv_episodes WHERE airdate >= ? AND airdate < ? AND showid = ?", (past_date, future_date, int(show["tvdb_id"]))) + + for episode in episode_list: + + # Get local timezone and load network timezones + local_zone = tz.tzlocal() + try: + network_zone = network_timezones.get_network_timezone(show['network'], network_timezones.load_network_dict(), local_zone) + except: + # Dummy network_zone for exceptions + network_zone = None + + # Get the air date and time + air_date = datetime.datetime.fromordinal(int(episode['airdate'])) + air_time = re.compile('([0-9]{1,2})\:([0-9]{2})(\ |)([AM|am|PM|pm]{2})').search(show["airs"]) + + # Parse out the air time + try: + if (air_time.group(4).lower() == 'pm' and int(air_time.group(1)) == 12): + t = datetime.time(12, int(air_time.group(2)), 0, tzinfo=network_zone) + elif (air_time.group(4).lower() == 'pm'): + t = datetime.time((int(air_time.group(1)) + 12), int(air_time.group(2)), 0, tzinfo=network_zone) + elif (air_time.group(4).lower() == 'am' and int(air_time.group(1)) == 12): + t = datetime.time(0, int(air_time.group(2)), 0, tzinfo=network_zone) + else: + t = datetime.time(int(air_time.group(1)), int(air_time.group(2)), 0, tzinfo=network_zone) + except: + # Dummy time for exceptions + t = datetime.time(22, 0, 0, tzinfo=network_zone) + + # Combine air time and air date into one datetime object + air_date_time = datetime.datetime.combine(air_date, t).astimezone(local_zone) + + # Create event for episode + ical = ical + 'BEGIN:VEVENT\n' + ical = ical + 'DTSTART:' + str(air_date_time.date()).replace("-", "") + '\n' + ical = ical + 'SUMMARY:' + show['show_name'] + ': ' + episode['name'] + '\n' + ical = ical + 'UID:' + str(datetime.date.today().isoformat()) + '-' + str(random.randint(10000,99999)) + '@Sick-Beard\n' + if (episode['description'] != ''): + ical = ical + 'DESCRIPTION:' + show['airs'] + ' on ' + show['network'] + '\\n\\n' + episode['description'] + '\n' + else: + ical = ical + 'DESCRIPTION:' + show['airs'] + ' on ' + show['network'] + '\n' + ical = ical + 'LOCATION:' + 'Episode ' + str(episode['episode']) + ' - Season ' + str(episode['season']) + '\n' + ical = ical + 'END:VEVENT\n' + + # Ending the iCal + ical += 'END:VCALENDAR\n' + + return ical + + manage = Manage() + + history = History() + + config = Config() + + home = Home() + + api = Api() + + browser = browser.WebFileBrowser() + + errorlogs = ErrorLogs() + + ui = UI()