diff --git a/data/css/style.css b/data/css/style.css index 5efc2acc..3dfb01c5 100644 --- a/data/css/style.css +++ b/data/css/style.css @@ -33,7 +33,7 @@ table { border-collapse: collapse; border-spacing: 0; } hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } input, select { vertical-align: middle; } -body { font:13px/1.231 sans-serif; *font-size:small; } +body { font:13px/1.231 sans-serif; *font-size:small; } select, input, textarea, button { font:99% sans-serif; } pre, code, kbd, samp { font-family: monospace, sans-serif; } @@ -52,7 +52,7 @@ sub { bottom: -0.25em; } pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; } textarea { overflow: auto; } -.ie6 legend, .ie7 legend { margin-left: -7px; } +.ie6 legend, .ie7 legend { margin-left: -7px; } input[type="radio"] { vertical-align: text-bottom; } input[type="checkbox"] { vertical-align: bottom; } .ie7 input[type="checkbox"] { vertical-align: baseline; } @@ -81,20 +81,20 @@ h1, h2, h3, h4, h5, h6 { font-weight: bold; } */ a:link { - color: #5E2612; - text-decoration: none; + color: #5E2612; + text-decoration: none; } a:visited { - color: #5E2612; - text-decoration: none; + color: #5E2612; + text-decoration: none; } a:hover { /*this effect is not shown in NN4.xx*/ - color: #999999; + color: #999999; text-decoration: underline; } -a:active {/*colour in NN4.xx is red*/ - color: #5E2612; - text-decoration: underline; +a:active {/*colour in NN4.xx is red*/ + color: #5E2612; + text-decoration: underline; } a.blue { color: blue; @@ -274,8 +274,6 @@ div#shutdown{ text-align: center; vertical-align: middle; } #cloud { margin: 0; } #cloud li { display: inline; } - - .ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; } .hidden { display: none; visibility: hidden; } .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } @@ -303,11 +301,11 @@ div#shutdown{ text-align: center; vertical-align: middle; } @media print { * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; - -ms-filter: none !important; } + -ms-filter: none !important; } a, a:visited { color: #444 !important; text-decoration: underline; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } - .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } diff --git a/data/images/favicon.ico b/data/images/favicon.ico index bc5b5d35..84cfd30f 100644 Binary files a/data/images/favicon.ico and b/data/images/favicon.ico differ diff --git a/data/images/headphoneslogo.png b/data/images/headphoneslogo.png index 86bfc0a4..2b77ac93 100644 Binary files a/data/images/headphoneslogo.png and b/data/images/headphoneslogo.png differ diff --git a/data/interfaces/default/album.html b/data/interfaces/default/album.html index c3de67cb..008dac4e 100644 --- a/data/interfaces/default/album.html +++ b/data/interfaces/default/album.html @@ -7,17 +7,17 @@ <%def name="headerIncludes()">
- Delete Album + Delete Album %if album['Status'] == 'Skipped': - Mark Album as Wanted + Mark Album as Wanted %elif album['Status'] == 'Wanted': - Force Check - Mark Album as Skipped + Force Check + Mark Album as Skipped %else: - Retry Download - Try New Version + Retry Download + Try New Version %endif - Choose Alternate Release + Choose Alternate Release - Edit Search Term + Edit Search Term +
-
Mark selected albums as +
Mark selected albums as - +
+
+ + + Set this to where your download client puts its completed downloads in +
@@ -524,6 +530,214 @@
+ + + + + +
+
+

Prowl

+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+
+
+

XBMC

+
+ +
+
+
+ + + e.g. http://localhost:8080. Separate hosts with commas +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+

Plex Media Server

+
+ +
+
+
+ + + Host running Plex Media Server (eg. http://192.168.1.100:32400) +
+
+ + + Host running Plex Client (eg. http://192.168.1.100:3000) +
+
+ + Username of your Plex client API (blank for none) +
+
+ + Password of your Plex client API (blank for none) +
+
+ +
+
+ +
+
+
+
+

NotifyMyAndroid

+
+ +
+
+
+ +
+
+ + + Separate multiple api keys with commas +
+
+ + +
+
+
+
+
+

Pushalot

+
+ +
+
+
+ +
+
+ + + Separate multiple api keys with commas +
+
+
+
+

Synology NAS

+
+ +
+
+ +
+

Pushover

+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+
+ +
+

Pushbullet

+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+

Twitter

+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+
+ +
+
@@ -548,7 +762,7 @@
Re-Encoding Options - Note: this option requires the lame, ffmpeg or xld encoder + Note: this option requires the lame, ffmpeg or xld encoder
@@ -669,7 +883,7 @@
- +
@@ -753,119 +967,15 @@
+ +
+ Songkick +
+ + +
+
-

Notifications

-
-

Prowl

-
- -
-
-
- -
-
- -
-
- - -
-
-
-
-

XBMC

-
- -
-
-
- - - e.g. http://localhost:8080. Separate hosts with commas -
-
- -
-
- -
-
- -
-
- -
-
-
-
-

NotifyMyAndroid

-
- -
-
-
- -
-
- - - Separate multiple api keys with commas -
-
- - -
-
-
- -
-

Synology NAS

-
- -
-
- -
-

Pushover

-
- -
-
-
- -
-
- -
-
- - -
-
-
-
Musicbrainz
@@ -1098,6 +1208,26 @@ } }); + if ($("#plex").is(":checked")) + { + $("#plexoptions").show(); + } + else + { + $("#plexoptions").hide(); + } + + $("#plex").click(function(){ + if ($("#plex").is(":checked")) + { + $("#plexoptions").slideDown(); + } + else + { + $("#plexoptions").slideUp(); + } + }); + if ($("#nma").is(":checked")) { $("#nmaoptions").show(); @@ -1117,6 +1247,27 @@ $("#nmaoptions").slideUp(); } }); + + if ($("#pushalot").is(":checked")) + { + $("#pushalotoptions").show(); + } + else + { + $("#pushalotoptions").hide(); + } + + $("#pushalot").click(function(){ + if ($("#pushalot").is(":checked")) + { + $("#pushalotoptions").slideDown(); + } + else + { + $("#pushalotoptions").slideUp(); + } + }); + if ($("#pushover").is(":checked")) { $("#pushoveroptions").show(); @@ -1137,6 +1288,46 @@ } }); + if ($("#pushbullet").is(":checked")) + { + $("#pushbulletoptions").show(); + } + else + { + $("#pushbulletoptions").hide(); + } + + $("#pushbullet").click(function(){ + if ($("#pushbullet").is(":checked")) + { + $("#pushbulletoptions").slideDown(); + } + else + { + $("#pushbulletoptions").slideUp(); + } + }); + + if ($("#twitter").is(":checked")) + { + $("#twitteroptions").show(); + } + else + { + $("#twitteroptions").hide(); + } + + $("#twitter").click(function(){ + if ($("#twitter").is(":checked")) + { + $("#twitteroptions").slideDown(); + } + else + { + $("#twitteroptions").slideUp(); + } + }); + if ($("#preferred_bitrate").is(":checked")) { $("#preferred_bitrate_options").show(); @@ -1290,7 +1481,28 @@ initConfigCheckbox("#usewhatcd"); initConfigCheckbox("#useapi"); initConfigCheckbox("#enable_https"); + + + + $('#twitterStep1').click(function () { + $.get("/twitterStep1", function (data) {window.open(data); }) + .done(function () { $('#ajaxMsg').html("
Confirm Authorization. Check pop-up blocker if no response.
"); }); + $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + }); + + $('#twitterStep2').click(function () { + var twitter_key = $("#twitter_key").val(); + $.get("/twitterStep2", {'key': twitter_key}, function (data) { $('#ajaxMsg').html("
"+data+"
"); }); + $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + }); + + $('#testTwitter').click(function () { + $.get("/testTwitter", + function (data) { $('#ajaxMsg').html("
"+data+"
"); }); + $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); + }); } + $(document).ready(function() { initThisPage(); }); diff --git a/data/interfaces/default/css/font-awesome.min.css b/data/interfaces/default/css/font-awesome.min.css new file mode 100644 index 00000000..449d6ac5 --- /dev/null +++ b/data/interfaces/default/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file diff --git a/data/interfaces/default/css/style.css b/data/interfaces/default/css/style.css index 5c6d59cb..82c3d066 100644 --- a/data/interfaces/default/css/style.css +++ b/data/interfaces/default/css/style.css @@ -1,4 +1,5 @@ -/* Variables *//* Mixins */ +/* Variables */ +/* Mixins */ html, body, div, @@ -158,7 +159,7 @@ img.albumArt { float: left; min-height: 100%; min-width: 100%; - max-width: 300px; + max-width: 250px; max-height: 300px; position: relative; } @@ -179,8 +180,8 @@ table th { background-image: linear-gradient(#fafafa, #eaeaea) !important; background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important; background-image: -o-linear-gradient(#fafafa, #eaeaea) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; border-left: 1px solid #E0E0E0; -moz-box-shadow: 1px 0 0 #fafafa; -webkit-box-shadow: 1px 0 0 #fafafa; @@ -204,8 +205,8 @@ table th.sorting_asc { background-image: linear-gradient(#fafbfd, #dce6ef) !important; background-image: -webkit-linear-gradient(#fafbfd, #dce6ef) !important; background-image: -o-linear-gradient(#fafbfd, #dce6ef) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important; color: #4183c4; } table td { @@ -296,23 +297,6 @@ input[type=button] { text-decoration: none; text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); } -.configsubmit { - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; - background: #222222 url("../images/button.png") repeat-x; - border: 0; - border-bottom: 1px solid rgba(0, 0, 0, 0.25); - color: #fff; - cursor: pointer; - margin-left: 75%; - margin-top: 5px; - text-align: center; - width: 200px; - padding: 4px 10px; - text-decoration: none; - text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); -} form legend, form h2 { font-size: 16px; @@ -365,7 +349,7 @@ form .row input[type=password] { line-height: normal; max-width: 230px; margin-right: 5px; - padding: 2px 5px; + padding: 3px 5px; } form .row small { color: #999; @@ -378,28 +362,24 @@ form .row small { form .left label { float: none; line-height: normal; - margin-bottom: 5px; - padding-top: 2px; + margin-bottom: 10px; + padding-top: 1px; width: auto; } form .left input { float: left; - margin-bottom: 5px; - clear: left; - display: block; + margin-bottom: 10px; } form .radio label { float: none; line-height: normal; - margin-bottom: 5px; - padding-top: 0px; + margin-bottom: 10px; + padding-top: 1px; width: auto; } form .radio input { float: left; - margin-bottom: 5px; - clear: left; - display: block; + margin-bottom: 10px; } form .radio small { display: inline !important; @@ -413,15 +393,6 @@ form .checkbox small { margin: 0 !important; width: auto; } -.override-float { - float: none !important; - margin-bottom: 0px !important; - clear: none !important; - display: inline !important; - font-size: 10px; - line-height: 10px; - height: 10px; -} ul, ol { margin-left: 2em; @@ -468,8 +439,8 @@ ul#nav li a:hover { background-image: linear-gradient(#f1f1f1, #e0e0e0) !important; background-image: -webkit-linear-gradient(#f1f1f1, #e0e0e0) !important; background-image: -o-linear-gradient(#f1f1f1, #e0e0e0) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important; border: 1px solid #DDDDDD; -moz-border-radius: 3px; -webkit-border-radius: 3px; @@ -491,10 +462,10 @@ ul#nav li a.config { height: 28px; width: 10px; } -ul#nav li a.config img { +ul#nav li a.config i { position: relative; - top: -7px; - left: -7px; + top: 0px; + left: -6px; } ul#nav li a.log { font-size: 13px; @@ -505,8 +476,8 @@ header { background-image: linear-gradient(#fafafa, #eaeaea) !important; background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important; background-image: -o-linear-gradient(#fafafa, #eaeaea) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; border-bottom: 1px solid #CACACA; -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); @@ -532,7 +503,7 @@ header #logo { } footer { display: table; - margin: 40px auto 50px auto; + margin: 60px auto 50px auto; width: 960px; padding-top: 10px; border-top: 1px solid #EEE; @@ -551,8 +522,8 @@ footer { background-image: linear-gradient(#fcf5c2, #fff6a9) !important; background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important; background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important; display: inline-block; padding: 5px 10px; margin-top: 10px; @@ -563,37 +534,14 @@ footer { position: relative; top: 4px; } -.configmessage { - -moz-border-radius: 10px; - -webkit-border-radius: 10px; - border-radius: 10px; - background-image: -moz-linear-gradient(#fcf5c2, #fff6a9) !important; - background-image: linear-gradient(#fcf5c2, #fff6a9) !important; - background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important; - background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - text-align: center; - padding: 3px 5px; - width: 400px; - margin-left: auto; - margin-right: auto; - margin-bottom: 5px; -} -.configmessage .ui-icon { - float: left; - margin-right: 5px; - position: relative; - top: 4px; -} #ajaxMsg { border: 1px solid #cccccc; background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important; background-image: linear-gradient(#ffffff, #eeeeee) !important; background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important; background-image: -o-linear-gradient(#ffffff, #eeeeee) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important; -moz-border-radius: 7px; -webkit-border-radius: 7px; border-radius: 7px; @@ -631,8 +579,8 @@ footer { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important; padding: 15px 10px; text-align: left; } @@ -641,8 +589,8 @@ footer { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important; padding: 15px 10px; text-align: left; } @@ -659,8 +607,8 @@ footer { background-image: linear-gradient(#ffffff, #eeeeee) !important; background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important; background-image: -o-linear-gradient(#ffffff, #eeeeee) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important; -moz-border-radius: 7px; -webkit-border-radius: 7px; border-radius: 7px; @@ -688,8 +636,8 @@ footer { background-image: linear-gradient(#fcf5c2, #fff6a9) !important; background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important; background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important; } #updatebar .msg { font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; @@ -705,8 +653,8 @@ footer { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important; padding: 15px 10px; text-align: left; } @@ -715,8 +663,8 @@ footer { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important; padding: 15px 10px; text-align: left; } @@ -727,13 +675,6 @@ footer { position: relative; margin-right: 3px; } -#dialog { - padding: 40px; -} -#dialog input#submit { - margin-left: 50px; - margin-right: auto; -} #subhead .back { float: left; margin-top: -25px; @@ -756,8 +697,8 @@ footer { background-image: linear-gradient(#f4f4f4, #e7e7e7) !important; background-image: -webkit-linear-gradient(#f4f4f4, #e7e7e7) !important; background-image: -o-linear-gradient(#f4f4f4, #e7e7e7) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important; font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; font-size: 12px; font-weight: normal; @@ -767,8 +708,8 @@ footer { background-image: linear-gradient(#599bdc, #3072b3) !important; background-image: -webkit-linear-gradient(#599bdc, #3072b3) !important; background-image: -o-linear-gradient(#599bdc, #3072b3) !important; - filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important; color: #FFF; border-color: #518CC6 #518CC6 #2A65A0; } @@ -804,13 +745,11 @@ div#searchbar input[type=text] { width: 150px; } div#searchbar .mini-icon { - height: 20px; - width: 20px; - background: url("../images/icon_search.gif") left top no-repeat; + color: #999; position: absolute; display: block; - margin-left: 6px; - margin-top: 6px; + margin-left: 9px; + margin-top: 7px; } .configtable legend { font-size: 16px; @@ -880,6 +819,18 @@ div#artistheader #artistImg { overflow: hidden; text-indent: -3000px; width: 200px; + position: relative; +} +div#artistheader #artistImg.on-tour:before { + content: ''; + position: absolute; + top: 0; + left: -2px; + width: 100px; + height: 100px; + z-index: 2; + background-image: url('../images/songkick_ribon.png'); + background-repeat: no-repeat; } div#artistheader #artistBio { font-size: 16px; @@ -896,6 +847,22 @@ div#artistheader h2 a { font-weight: bold; font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; } +#artistCalendar { + list-style-type: none; + margin: 0px; + display: block; + background: #FEF2EB; + padding: 10px; + font-size: 16px; +} +#artistCalendar .sk-location { + display: inline-block; + padding-left: 6px; +} +#artistCalendar .sk-logo { + width: 100px; + padding-top: 16px; +} #artist_table { background-color: #FFF; padding: 20px; @@ -920,24 +887,16 @@ div#artistheader h2 a { #artist_table th#name { min-width: 200px; text-align: left; - width:200px; } #artist_table th#album { min-width: 300px; text-align: left; } -#artist_table th#albumart, -#artist_table th#status{ - width:50px; -} - #artist_table th#status, -#artist_table th#albumart, -#artist_table th#lastupdated { +#artist_table th#albumart { min-width: 50px; text-align: left; } - #artist_table th#have { text-align: center; } @@ -946,13 +905,11 @@ div#artistheader h2 a { text-align: left; vertical-align: middle; } -#artist_table td#status, -#artist_table td#lastupdated { +#artist_table td#status { min-width: 50px; text-align: left; vertical-align: middle; } - #artist_table td#album { min-width: 300px; text-align: left; @@ -1185,8 +1142,8 @@ div#artistheader h2 a { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important; } .progress-container { background: #FFF; @@ -1197,13 +1154,13 @@ div#artistheader h2 a { padding: 1px; width: 100px; } -.progress-container > div { +.progress-container > div { 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=#fafafa, endColorstr=#eaeaea) !important; - -ms-filter: progid:dximagetransform.microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important; + -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important; height: 14px; } .havetracks { @@ -1239,11 +1196,11 @@ div#artistheader h2 a { background-image: url("../images/ui-icons_70b2e1_256x240.png"); } #toTop { - background: url("../images/toTop.gif") no-repeat scroll 10px center #f7f7f7; + background: #f7f7f7; border-radius: 5px 0 0 0; bottom: 0; display: none; - padding: 10px 10px 10px 40px; + padding: 10px 10px 10px 10px; position: fixed; right: 0; } @@ -1365,7 +1322,6 @@ div#artistheader h2 a { clear: both; } #album_table th#albumname, -#album_table th#artistname, #upcoming_table th#artistname, #wanted_table th#artistname { min-width: 150px; @@ -1388,7 +1344,6 @@ div#artistheader h2 a { vertical-align: middle; } #album_table td#albumname, -#album_table td#artistname, #album_table td#reldate, #album_table td#type, #track_table td#duration, @@ -1495,6 +1450,3 @@ table tr td#status a { .ie7 legend { margin-left: -7px; } -#preferred_bitrate_options { - margin-left: 20px; -} diff --git a/data/interfaces/default/css/style.less b/data/interfaces/default/css/style.less index 2c8c67a6..c9e1b19c 100644 --- a/data/interfaces/default/css/style.less +++ b/data/interfaces/default/css/style.less @@ -1,4 +1,4 @@ -// Config +// Config @import "config.less"; html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video { @@ -26,7 +26,7 @@ body { padding: 0; } -// Links +// Links a { color: @link-color; text-decoration: none; @@ -37,11 +37,11 @@ a { &.blue { color: blue; } - .ui-icon { + .ui-icon { display:inline-block; position: relative; top: 2px; - + } } .links { @@ -204,7 +204,7 @@ form { line-height: normal; max-width: 230px; margin-right: 5px; - padding: 3px 5px; + padding: 3px 5px; } small { color: #999; @@ -295,18 +295,18 @@ ul#nav { .rounded(3px); .shadow(0 1px 0 #FAFAFA); .shadow(0 1px 0 #FAFAFA inset); - -webkit-transition:color .2s ease-in; - -moz-transition:color .2s ease-in; - -o-transition:color .2s ease-in; - transition:color .2s ease-in; + -webkit-transition:color .2s ease-in; + -moz-transition:color .2s ease-in; + -o-transition:color .2s ease-in; + transition:color .2s ease-in; } &.config { height: 28px; width: 10px; - img { + i { position: relative; - top: -7px; - left: -7px; + top: 0px; + left: -6px; } } &.log { @@ -412,7 +412,7 @@ footer { } #updatebar { - #ajaxMsg; + #ajaxMsg; display: block; .gradient(#FCF5C2,@msg-bg); } @@ -442,7 +442,7 @@ footer { &:hover { .gradient(#599BDC, #3072B3); color: #FFF; - border-color: #518CC6 #518CC6 #2A65A0; + border-color: #518CC6 #518CC6 #2A65A0; } } } @@ -472,13 +472,11 @@ div#searchbar { width: 150px; } .mini-icon { - height: 20px; - width: 20px; - background: url("../images/icon_search.gif") left top no-repeat; + color: #999; position: absolute; display: block; - margin-left: 6px; - margin-top: 6px; + margin-left: 9px; + margin-top: 7px; } } @@ -504,7 +502,7 @@ div#searchbar { } } -// TABLES +// TABLES // wrappers .table_wrapper { @@ -533,8 +531,8 @@ div#searchbar { h1 { line-height: 33px; width: 450px; - img { - float:left; + img { + float:left; margin-right: 5px; } } @@ -545,7 +543,7 @@ div#nopaddingheader { text-align: center; } -// Artist +// Artist div#artistheader { margin-top: 50px; min-height: 200px; @@ -560,12 +558,26 @@ div#artistheader { overflow: hidden; text-indent: -3000px; width: 200px; + position: relative; + + &.on-tour:before { + content: ''; + position: absolute; + top: 0; + left: -2px; + width: 100px; + height: 100px; + z-index: 2; + background-image: url('../images/songkick_ribon.png'); + background-repeat: no-repeat; + } } #artistBio { font-size: 16px; line-height: 24px; margin-top: 10px; } + h1 { a { font-size: 32px; @@ -582,6 +594,25 @@ div#artistheader { } } +#artistCalendar { + list-style-type: none; + margin: 0px; + display: block; + background: #FEF2EB; + padding: 10px; + font-size: 16px; + + .sk-location { + display: inline-block; + padding-left: 6px; + } + + .sk-logo { + width: 100px; + padding-top: 16px; + } +} + #artist_table { background-color: #FFF; padding: 20px; @@ -625,7 +656,7 @@ div#artistheader { text-align: left; vertical-align: middle; } - td#album { + td#album { min-width: 300px; text-align: left; vertical-align: middle; @@ -638,7 +669,7 @@ div#artistheader { display: inline-block; } -// Album +// Album #albumheader { margin-top: 50px; min-height: 200px; @@ -733,7 +764,7 @@ div#artistheader { } } -// Manage +// Manage #manageheader { margin-top: 45px; margin-bottom: 0; @@ -777,7 +808,7 @@ div#artistheader { } } -// History +// History #history_table { background-color: #FFF; font-size: 13px; @@ -802,7 +833,7 @@ div#artistheader { } } -// Logs +// Logs #log_table { background-color: #FFF; th#timestamp { @@ -867,7 +898,7 @@ div#artistheader { font-size: 11px; vertical-align: middle; line-height: normal; - .gradient(#A3E532,#90CC2A); + .gradient(#A3E532,#90CC2A); } } } @@ -920,11 +951,11 @@ div#artistheader { } } #toTop { - background: url("../images/toTop.gif") no-repeat scroll 10px center #f7f7f7; + background: #f7f7f7; border-radius: 5px 0 0 0; bottom: 0; display: none; - padding: 10px 10px 10px 40px; + padding: 10px 10px 10px 10px; position: fixed; right: 0; } @@ -988,7 +1019,7 @@ div#artistheader { font-weight: 900; } } - li { + li { display: inline-block; margin: 5px 10px; } @@ -1041,7 +1072,7 @@ div#artistheader { &:after { clear: both; } } -// Table width +// Table width #album_table th#albumname, #upcoming_table th#artistname, #wanted_table th#artistname { min-width: 150px; text-align: center; diff --git a/data/interfaces/default/extras.html b/data/interfaces/default/extras.html index 2c25d357..f6c8fdc7 100644 --- a/data/interfaces/default/extras.html +++ b/data/interfaces/default/extras.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%def name="body()">
-

extraArtists You Might Like

+

Artists You Might Like

diff --git a/data/interfaces/default/fonts/fontawesome-webfont.eot b/data/interfaces/default/fonts/fontawesome-webfont.eot new file mode 100755 index 00000000..7c79c6a6 Binary files /dev/null and b/data/interfaces/default/fonts/fontawesome-webfont.eot differ diff --git a/data/interfaces/default/fonts/fontawesome-webfont.svg b/data/interfaces/default/fonts/fontawesome-webfont.svg new file mode 100755 index 00000000..45fdf338 --- /dev/null +++ b/data/interfaces/default/fonts/fontawesome-webfont.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/interfaces/default/fonts/fontawesome-webfont.ttf b/data/interfaces/default/fonts/fontawesome-webfont.ttf new file mode 100755 index 00000000..e89738de Binary files /dev/null and b/data/interfaces/default/fonts/fontawesome-webfont.ttf differ diff --git a/data/interfaces/default/fonts/fontawesome-webfont.woff b/data/interfaces/default/fonts/fontawesome-webfont.woff new file mode 100755 index 00000000..8c1748aa Binary files /dev/null and b/data/interfaces/default/fonts/fontawesome-webfont.woff differ diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index 64a9ef2c..baf72b3d 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -6,17 +6,17 @@ <%def name="headerIncludes()"> <%def name="body()">
-

HistoryHistory

+

History

diff --git a/data/interfaces/default/images/songkick.png b/data/interfaces/default/images/songkick.png new file mode 100644 index 00000000..c18e440d Binary files /dev/null and b/data/interfaces/default/images/songkick.png differ diff --git a/data/interfaces/default/images/songkick_ribon.png b/data/interfaces/default/images/songkick_ribon.png new file mode 100644 index 00000000..57ad4ccb Binary files /dev/null and b/data/interfaces/default/images/songkick_ribon.png differ diff --git a/data/interfaces/default/js/script.js b/data/interfaces/default/js/script.js index a0bb860e..9b46369c 100644 --- a/data/interfaces/default/js/script.js +++ b/data/interfaces/default/js/script.js @@ -142,21 +142,21 @@ function initConfigCheckbox(elem) { }); } function initActions() { - $("#subhead_menu #menu_link_refresh").button({ icons: { primary: "ui-icon-refresh" } }); - $("#subhead_menu #menu_link_edit").button({ icons: { primary: "ui-icon-pencil" } }); - $("#subhead_menu .menu_link_edit").button({ icons: { primary: "ui-icon-pencil" } }); - $("#subhead_menu #menu_link_delete" ).button({ icons: { primary: "ui-icon-trash" } }); - $("#subhead_menu #menu_link_pauze").button({ icons: { primary: "ui-icon-pause"} }); - $("#subhead_menu #menu_link_resume").button({ icons: { primary: "ui-icon-play"} }); - $("#subhead_menu #menu_link_getextra").button({ icons: { primary: "ui-icon-plus"} }); - $("#subhead_menu #menu_link_removeextra").button({ icons: { primary: "ui-icon-minus" } }); - $("#subhead_menu #menu_link_wanted" ).button({ icons: { primary: "ui-icon-heart" } }); - $("#subhead_menu #menu_link_check").button({ icons: { primary: "ui-icon-arrowrefresh-1-w"} }); - $("#subhead_menu #menu_link_skipped").button({ icons: { primary: "ui-icon-seek-end"} }); - $("#subhead_menu #menu_link_retry").button({ icons: { primary: "ui-icon-arrowrefresh-1-e"} }); - $("#subhead_menu #menu_link_new").button({ icons: { primary: "ui-icon-arrowreturnthick-1-s" } }); - $("#subhead_menu #menu_link_shutdown").button({ icons: { primary: "ui-icon-power"} }); - $("#subhead_menu #menu_link_scan").button({ icons: { primary: "ui-icon-search"} }); + $("#subhead_menu #menu_link_refresh").button(); + $("#subhead_menu #menu_link_edit").button(); + $("#subhead_menu .menu_link_edit").button(); + $("#subhead_menu #menu_link_delete" ).button(); + $("#subhead_menu #menu_link_pauze").button(); + $("#subhead_menu #menu_link_resume").button(); + $("#subhead_menu #menu_link_getextra").button(); + $("#subhead_menu #menu_link_removeextra").button(); + $("#subhead_menu #menu_link_wanted" ).button(); + $("#subhead_menu #menu_link_check").button(); + $("#subhead_menu #menu_link_skipped").button(); + $("#subhead_menu #menu_link_retry").button(); + $("#subhead_menu #menu_link_new").button(); + $("#subhead_menu #menu_link_shutdown").button(); + $("#subhead_menu #menu_link_scan").button(); } function refreshSubmenu() { @@ -214,7 +214,7 @@ function showMsg(msg,loader,timeout,ms) { feedback.fadeIn(); var message = $("
" + msg + "
"); if (loader) { - var message = $("
loading" + msg + "
"); + var message = $(" " + msg + ""); feedback.css("padding","14px 10px") } $(feedback).prepend(message); @@ -246,7 +246,7 @@ function doAjaxCall(url,elem,reload,form) { var dataString = $(formID).serialize(); } // Loader Image - var loader = $("loading"); + var loader = $(""); // Data Success Message var dataSucces = $(elem).data('success'); if (typeof dataSucces === "undefined") { @@ -260,8 +260,8 @@ function doAjaxCall(url,elem,reload,form) { var dataError = "There was an error"; } // Get Success & Error message from inline data, else use standard message - var succesMsg = $("
" + dataSucces + "
"); - var errorMsg = $("
" + dataError + "
"); + var succesMsg = $("
" + dataSucces + "
"); + var errorMsg = $("
" + dataError + "
"); // Check if checkbox is selected if ( form ) { diff --git a/data/interfaces/default/logs.html b/data/interfaces/default/logs.html index 0f3fb8e5..e6ed4aeb 100644 --- a/data/interfaces/default/logs.html +++ b/data/interfaces/default/logs.html @@ -5,7 +5,7 @@ lossless<%inherit file="base.html"/> <%def name="body()">
-

LogsLogs

+

Logs

diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index 6d98bb53..c0c67d25 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -6,30 +6,30 @@ <%def name="headerIncludes()"> <%def name="body()">
-

manageManage

+

Manage

@@ -120,11 +120,11 @@
Force Search
diff --git a/data/interfaces/default/managenew.html b/data/interfaces/default/managenew.html index 41f9263d..d7617e52 100644 --- a/data/interfaces/default/managenew.html +++ b/data/interfaces/default/managenew.html @@ -16,7 +16,7 @@ <%def name="body()">
-

manageManage New Artists

+

Manage New Artists

diff --git a/data/interfaces/default/manageunmatched.html b/data/interfaces/default/manageunmatched.html index 3fdc7eaa..946c8a5b 100644 --- a/data/interfaces/default/manageunmatched.html +++ b/data/interfaces/default/manageunmatched.html @@ -16,7 +16,7 @@ <%def name="headerIncludes()"> « Back to manage overview @@ -26,7 +26,7 @@ <%def name="body()">
-

manageManage Unmatched Albums

+

Manage Unmatched Albums

diff --git a/data/interfaces/default/searchresults.html b/data/interfaces/default/searchresults.html index 0207a223..b790f644 100644 --- a/data/interfaces/default/searchresults.html +++ b/data/interfaces/default/searchresults.html @@ -3,7 +3,7 @@ <%def name="body()">
-

Search resultsSearch Result

+

Search Result

@@ -38,9 +38,9 @@ %if type == 'album': - + %else: - + %endif %endfor diff --git a/data/interfaces/default/shutdown.html b/data/interfaces/default/shutdown.html index 312d1ef3..e2d3b467 100644 --- a/data/interfaces/default/shutdown.html +++ b/data/interfaces/default/shutdown.html @@ -7,7 +7,7 @@ <%def name="body()">
-

Headphones is ${message}

+

Headphones is ${message}

\ No newline at end of file diff --git a/data/interfaces/default/upcoming.html b/data/interfaces/default/upcoming.html index 3be664e9..2d3903f2 100644 --- a/data/interfaces/default/upcoming.html +++ b/data/interfaces/default/upcoming.html @@ -3,7 +3,7 @@ <%def name="headerIncludes()"> @@ -12,7 +12,7 @@ <%def name="body()">
-

Wanted AlbumsWanted Albums

+

Wanted Albums

@@ -53,7 +53,7 @@
-

Upcoming AlbumsUpcoming Albums

+

Upcoming Albums

${result['uniquename']}
${result['score']}
Add this album Add this album Add this artist Add this artist
diff --git a/headphones/__init__.py b/headphones/__init__.py index 7f567afc..45301577 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -92,6 +92,7 @@ CHECK_GITHUB = False CHECK_GITHUB_ON_STARTUP = False CHECK_GITHUB_INTERVAL = None +POST_PROCESSING_DIR = None MUSIC_DIR = None DESTINATION_DIR = None LOSSLESS_DESTINATION_DIR = None @@ -229,15 +230,34 @@ XBMC_USERNAME = None XBMC_PASSWORD = None XBMC_UPDATE = False XBMC_NOTIFY = False +PLEX_ENABLED = False +PLEX_SERVER_HOST = None +PLEX_CLIENT_HOST = None +PLEX_USERNAME = None +PLEX_PASSWORD = None +PLEX_UPDATE = False +PLEX_NOTIFY = False NMA_ENABLED = False NMA_APIKEY = None NMA_PRIORITY = None NMA_ONSNATCH = None +PUSHALOT_ENABLED = False +PUSHALOT_APIKEY = None +PUSHALOT_ONSNATCH = None SYNOINDEX_ENABLED = False PUSHOVER_ENABLED = True PUSHOVER_PRIORITY = 1 PUSHOVER_KEYS = None PUSHOVER_ONSNATCH = True +PUSHBULLET_ENABLED = True +PUSHBULLET_APIKEY = None +PUSHBULLET_DEVICEID = None +PUSHBULLET_ONSNATCH = True +TWITTER_ENABLED = False +TWITTER_ONSNATCH = False +TWITTER_USERNAME = None +TWITTER_PASSWORD = None +TWITTER_PREFIX = None MIRRORLIST = ["musicbrainz.org","headphones","custom"] MIRROR = None CUSTOMHOST = None @@ -245,6 +265,7 @@ CUSTOMPORT = None CUSTOMSLEEP = None HPUSER = None HPPASS = None +SONGKICK_APIKEY = "nd1We7dFW2RqxPw8" CACHE_SIZEMB = 32 JOURNAL_MODE = None @@ -312,13 +333,18 @@ def initialize(): NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, HEADPHONES_INDEXER, NZBMATRIX, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, \ UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \ NZBSORG, NZBSORG_UID, NZBSORG_HASH, NZBSRUS, NZBSRUS_UID, NZBSRUS_APIKEY, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_APIKEY, \ - NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \ + NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \ INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \ MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, DELETE_LOSSLESS_FILES, \ PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, MIRRORLIST, \ + TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \ + PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \ MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ XBMC_NOTIFY, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ - PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY + PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ + PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, PUSHALOT_ONSNATCH, \ + POST_PROCESSING_DIR + if __INITIALIZED__: return False @@ -339,9 +365,13 @@ def initialize(): CheckSection('What.cd') CheckSection('Prowl') CheckSection('Pushover') + CheckSection('PushBullet') CheckSection('XBMC') + CheckSection('Plex') CheckSection('NMA') + CheckSection('Pushalot') CheckSection('Synoindex') + CheckSection('Twitter') CheckSection('Advanced') # Set global variables based on config file or use defaults @@ -376,6 +406,7 @@ def initialize(): CHECK_GITHUB_ON_STARTUP = bool(check_setting_int(CFG, 'General', 'check_github_on_startup', 1)) CHECK_GITHUB_INTERVAL = check_setting_int(CFG, 'General', 'check_github_interval', 360) + POST_PROCESSING_DIR = check_setting_str(CFG, 'General', 'post_processing_dir', '') MUSIC_DIR = check_setting_str(CFG, 'General', 'music_dir', '') DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '') LOSSLESS_DESTINATION_DIR = check_setting_str(CFG, 'General', 'lossless_destination_dir', '') @@ -449,11 +480,11 @@ def initialize(): NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '') HEADPHONES_INDEXER = bool(check_setting_int(CFG, 'Headphones', 'headphones_indexer', 0)) - + TRANSMISSION_HOST = check_setting_str(CFG, 'Transmission', 'transmission_host', '') TRANSMISSION_USERNAME = check_setting_str(CFG, 'Transmission', 'transmission_username', '') TRANSMISSION_PASSWORD = check_setting_str(CFG, 'Transmission', 'transmission_password', '') - + UTORRENT_HOST = check_setting_str(CFG, 'uTorrent', 'utorrent_host', '') UTORRENT_USERNAME = check_setting_str(CFG, 'uTorrent', 'utorrent_username', '') UTORRENT_PASSWORD = check_setting_str(CFG, 'uTorrent', 'utorrent_password', '') @@ -515,11 +546,23 @@ def initialize(): XBMC_UPDATE = bool(check_setting_int(CFG, 'XBMC', 'xbmc_update', 0)) XBMC_NOTIFY = bool(check_setting_int(CFG, 'XBMC', 'xbmc_notify', 0)) + PLEX_ENABLED = bool(check_setting_int(CFG, 'Plex', 'plex_enabled', 0)) + PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '') + PLEX_CLIENT_HOST = check_setting_str(CFG, 'Plex', 'plex_client_host', '') + PLEX_USERNAME = check_setting_str(CFG, 'Plex', 'plex_username', '') + PLEX_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_password', '') + PLEX_UPDATE = bool(check_setting_int(CFG, 'Plex', 'plex_update', 0)) + PLEX_NOTIFY = bool(check_setting_int(CFG, 'Plex', 'plex_notify', 0)) + NMA_ENABLED = bool(check_setting_int(CFG, 'NMA', 'nma_enabled', 0)) NMA_APIKEY = check_setting_str(CFG, 'NMA', 'nma_apikey', '') NMA_PRIORITY = check_setting_int(CFG, 'NMA', 'nma_priority', 0) NMA_ONSNATCH = bool(check_setting_int(CFG, 'NMA', 'nma_onsnatch', 0)) + PUSHALOT_ENABLED = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_enabled', 0)) + PUSHALOT_APIKEY = check_setting_str(CFG, 'Pushalot', 'pushalot_apikey', '') + PUSHALOT_ONSNATCH = bool(check_setting_int(CFG, 'Pushalot', 'pushalot_onsnatch', 0)) + SYNOINDEX_ENABLED = bool(check_setting_int(CFG, 'Synoindex', 'synoindex_enabled', 0)) PUSHOVER_ENABLED = bool(check_setting_int(CFG, 'Pushover', 'pushover_enabled', 0)) @@ -527,6 +570,17 @@ def initialize(): PUSHOVER_ONSNATCH = bool(check_setting_int(CFG, 'Pushover', 'pushover_onsnatch', 0)) PUSHOVER_PRIORITY = check_setting_int(CFG, 'Pushover', 'pushover_priority', 0) + PUSHBULLET_ENABLED = bool(check_setting_int(CFG, 'PushBullet', 'pushbullet_enabled', 0)) + PUSHBULLET_APIKEY = check_setting_str(CFG, 'PushBullet', 'pushbullet_apikey', '') + PUSHBULLET_DEVICEID = check_setting_str(CFG, 'PushBullet', 'pushbullet_deviceid', '') + PUSHBULLET_ONSNATCH = bool(check_setting_int(CFG, 'PushBullet', 'pushbullet_onsnatch', 0)) + + TWITTER_ENABLED = bool(check_setting_int(CFG, 'Twitter', 'twitter_enabled', 0)) + TWITTER_ONSNATCH = bool(check_setting_int(CFG, 'Twitter', 'twitter_onsnatch', 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', 'Headphones') + MIRROR = check_setting_str(CFG, 'General', 'mirror', 'musicbrainz.org') CUSTOMHOST = check_setting_str(CFG, 'General', 'customhost', 'localhost') CUSTOMPORT = check_setting_int(CFG, 'General', 'customport', 5000) @@ -592,7 +646,7 @@ def initialize(): if BLACKHOLE: NZB_DOWNLOADER = 2 CONFIG_VERSION = '4' - + # Enable Headphones Indexer if they have a VIP account if CONFIG_VERSION == '4': if HPUSER and HPPASS: @@ -706,7 +760,7 @@ def launch_browser(host, port, root): if host == '0.0.0.0': host = 'localhost' - + if ENABLE_HTTPS: protocol = 'https' else: @@ -746,6 +800,7 @@ def config_write(): new_config['General']['check_github_on_startup'] = int(CHECK_GITHUB_ON_STARTUP) new_config['General']['check_github_interval'] = CHECK_GITHUB_INTERVAL + new_config['General']['post_processing_dir'] = POST_PROCESSING_DIR new_config['General']['music_dir'] = MUSIC_DIR new_config['General']['destination_dir'] = DESTINATION_DIR new_config['General']['lossless_destination_dir'] = LOSSLESS_DESTINATION_DIR @@ -829,7 +884,7 @@ def config_write(): new_config['Transmission']['transmission_host'] = TRANSMISSION_HOST new_config['Transmission']['transmission_username'] = TRANSMISSION_USERNAME new_config['Transmission']['transmission_password'] = TRANSMISSION_PASSWORD - + new_config['uTorrent'] = {} new_config['uTorrent']['utorrent_host'] = UTORRENT_HOST new_config['uTorrent']['utorrent_username'] = UTORRENT_USERNAME @@ -881,11 +936,25 @@ def config_write(): new_config['XBMC']['xbmc_update'] = int(XBMC_UPDATE) new_config['XBMC']['xbmc_notify'] = int(XBMC_NOTIFY) + new_config['Plex'] = {} + new_config['Plex']['plex_enabled'] = int(PLEX_ENABLED) + new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST + new_config['Plex']['plex_client_host'] = PLEX_CLIENT_HOST + new_config['Plex']['plex_username'] = PLEX_USERNAME + new_config['Plex']['plex_password'] = PLEX_PASSWORD + new_config['Plex']['plex_update'] = int(PLEX_UPDATE) + new_config['Plex']['plex_notify'] = int(PLEX_NOTIFY) + new_config['NMA'] = {} new_config['NMA']['nma_enabled'] = int(NMA_ENABLED) new_config['NMA']['nma_apikey'] = NMA_APIKEY new_config['NMA']['nma_priority'] = NMA_PRIORITY - new_config['NMA']['nma_onsnatch'] = int(PROWL_ONSNATCH) + new_config['NMA']['nma_onsnatch'] = int(NMA_ONSNATCH) + + new_config['Pushalot'] = {} + new_config['Pushalot']['pushalot_enabled'] = int(PUSHALOT_ENABLED) + new_config['Pushalot']['pushalot_apikey'] = PUSHALOT_APIKEY + new_config['Pushalot']['pushalot_onsnatch'] = int(PUSHALOT_ONSNATCH) new_config['Pushover'] = {} new_config['Pushover']['pushover_enabled'] = int(PUSHOVER_ENABLED) @@ -893,6 +962,19 @@ def config_write(): new_config['Pushover']['pushover_onsnatch'] = int(PUSHOVER_ONSNATCH) new_config['Pushover']['pushover_priority'] = int(PUSHOVER_PRIORITY) + new_config['PushBullet'] = {} + new_config['PushBullet']['pushbullet_enabled'] = int(PUSHBULLET_ENABLED) + new_config['PushBullet']['pushbullet_apikey'] = PUSHBULLET_APIKEY + new_config['PushBullet']['pushbullet_deviceid'] = PUSHBULLET_DEVICEID + new_config['PushBullet']['pushbullet_onsnatch'] = int(PUSHBULLET_ONSNATCH) + + new_config['Twitter'] = {} + new_config['Twitter']['twitter_enabled'] = int(TWITTER_ENABLED) + new_config['Twitter']['twitter_onsnatch'] = int(TWITTER_ONSNATCH) + new_config['Twitter']['twitter_username'] = TWITTER_USERNAME + new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD + new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX + new_config['Synoindex'] = {} new_config['Synoindex']['synoindex_enabled'] = int(SYNOINDEX_ENABLED) @@ -945,7 +1027,8 @@ def start(): if CHECK_GITHUB: SCHED.add_interval_job(versioncheck.checkGithub, minutes=CHECK_GITHUB_INTERVAL) - SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL) + if DOWNLOAD_SCAN_INTERVAL > 0: + SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL) SCHED.start() diff --git a/headphones/importer.py b/headphones/importer.py index b78c69ab..a7481f76 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -224,8 +224,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): #Make a user configurable variable to skip update of albums with release dates older than this date (in days) pause_delta = headphones.MB_IGNORE_AGE + check_release_date = myDB.action("SELECT ReleaseDate, Status from albums WHERE ArtistID=? AND AlbumTitle=?", (artistid, al_title)).fetchone() + + #Skip update if Status set + if check_release_date and check_release_date[1]: + logger.info("[%s] Not updating: %s (Status is %s, skipping)" % (artist['artist_name'], rg['title'], check_release_date[1])) + continue + if not forcefull: - check_release_date = myDB.action("SELECT ReleaseDate from albums WHERE ArtistID=? AND AlbumTitle=?", (artistid, al_title)).fetchone() if check_release_date: if check_release_date[0] is None: logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title'])) diff --git a/headphones/librarysync.py b/headphones/librarysync.py index 843eaf11..8706e0de 100644 --- a/headphones/librarysync.py +++ b/headphones/librarysync.py @@ -179,8 +179,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal # Now we start track matching logger.info("%s new/modified songs found and added to the database" % new_song_count) - song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir+"%"]) - total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir+"%"]).fetchone()[0] + song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]) + total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0] logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) @@ -363,4 +363,4 @@ def update_album_status(AlbumID=None): myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']}) if new_album_status != album['Status']: logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status)) - logger.info('Album status update complete') \ No newline at end of file + logger.info('Album status update complete') diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 11a5e57c..3da78a6e 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -13,7 +13,8 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import logger +from headphones import logger, helpers, common +from headphones.exceptions import ex import base64 import cherrypy import urllib @@ -24,6 +25,15 @@ from urllib import urlencode import os.path import subprocess import lib.simplejson as simplejson +from xml.dom import minidom + +try: + from urlparse import parse_qsl #@UnusedImport +except: + from cgi import parse_qsl #@Reimport + +import lib.oauth2 as oauth +import lib.pythontwitter as twitter class PROWL: @@ -183,6 +193,93 @@ class XBMC: except: logger.warn('Error sending notification request to XBMC') +class Plex: + + def __init__(self): + + self.server_hosts = headphones.PLEX_SERVER_HOST + self.client_hosts = headphones.PLEX_CLIENT_HOST + self.username = headphones.PLEX_USERNAME + self.password = headphones.PLEX_PASSWORD + + def _sendhttp(self, host, command): + + username = self.username + password = self.password + + url_command = urllib.urlencode(command) + + url = host + '/xbmcCmds/xbmcHttp/?' + url_command + + req = urllib2.Request(url) + + if password: + base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') + req.add_header("Authorization", "Basic %s" % base64string) + + logger.info('Plex url: %s' % url) + + try: + handle = urllib2.urlopen(req) + except Exception, e: + logger.warn('Error opening Plex url: %s' % e) + return + + response = handle.read().decode(headphones.SYS_ENCODING) + + return response + + def update(self): + + # From what I read you can't update the music library on a per directory or per path basis + # so need to update the whole thing + + hosts = [x.strip() for x in self.server_hosts.split(',')] + + for host in hosts: + logger.info('Sending library update command to Plex Media Server@ '+host) + url = "%s/library/sections" % host + try: + xml_sections = minidom.parse(urllib.urlopen(url)) + except IOError, e: + logger.warn("Error while trying to contact Plex Media Server: %s" % e) + return False + + sections = xml_sections.getElementsByTagName('Directory') + if not sections: + logger.info(u"Plex Media Server not running on: " + host) + return False + + for s in sections: + if s.getAttribute('type') == "artist": + url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) + try: + urllib.urlopen(url) + except Exception, e: + logger.warn("Error updating library section for Plex Media Server: %s" % e) + return False + + def notify(self, artist, album, albumartpath): + + hosts = [x.strip() for x in self.client_hosts.split(',')] + + header = "Headphones" + message = "%s - %s added to your library" % (artist, album) + time = "3000" # in ms + + for host in hosts: + logger.info('Sending notification command to Plex Media Server @ '+host) + try: + notification = header + "," + message + "," + time + "," + albumartpath + notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'} + request = self._sendhttp(host, notifycommand) + + if not request: + raise Exception + + except: + logger.warn('Error sending notification request to Plex Media Server') + class NMA: def __init__(self): @@ -226,7 +323,99 @@ class NMA: if not request: logger.warn('Error sending notification request to NotifyMyAndroid') + +class PUSHBULLET: + + def __init__(self): + self.apikey = headphones.PUSHBULLET_APIKEY + self.deviceid = headphones.PUSHBULLET_DEVICEID + + def conf(self, options): + return cherrypy.config['config'].get('PUSHBULLET', options) + + def notify(self, message, event): + if not headphones.PUSHBULLET_ENABLED: + return + + http_handler = HTTPSConnection("api.pushbullet.com") + + data = {'device_iden': headphones.PUSHBULLET_DEVICEID, + 'type': "note", + 'title': "Headphones", + 'body': message.encode("utf-8") } + + http_handler.request("POST", + "/api/pushes", + headers = {'Content-type': "application/x-www-form-urlencoded", + 'Authorization' : 'Basic %s' % base64.b64encode(headphones.PUSHBULLET_APIKEY + ":") }, + body = urlencode(data)) + response = http_handler.getresponse() + request_status = response.status + logger.debug(u"PushBullet response status: %r" % request_status) + logger.debug(u"PushBullet response headers: %r" % response.getheaders()) + logger.debug(u"PushBullet response body: %r" % response.read()) + + if request_status == 200: + logger.info(u"PushBullet notifications sent.") + return True + elif request_status >= 400 and request_status < 500: + logger.info(u"PushBullet request failed: %s" % response.reason) + return False + else: + logger.info(u"PushBullet notification failed serverside.") + return False + + def updateLibrary(self): + #For uniformity reasons not removed + return + + def test(self, apikey, deviceid): + + self.enabled = True + self.apikey = apikey + self.deviceid = deviceid + + self.notify('Main Screen Activate', 'Test Message') + +class PUSHALOT: + + def notify(self, message, event): + if not headphones.PUSHALOT_ENABLED: + return + + pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY + + logger.debug(u"Pushalot event: " + event) + logger.debug(u"Pushalot message: " + message) + logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) + + http_handler = HTTPSConnection("pushalot.com") + + data = {'AuthorizationToken': pushalot_authorizationtoken, + 'Title': event.encode('utf-8'), + 'Body': message.encode("utf-8") } + + http_handler.request("POST", + "/api/sendmessage", + headers = {'Content-type': "application/x-www-form-urlencoded"}, + body = urlencode(data)) + response = http_handler.getresponse() + request_status = response.status + + logger.debug(u"Pushalot response status: %r" % request_status) + logger.debug(u"Pushalot response headers: %r" % response.getheaders()) + logger.debug(u"Pushalot response body: %r" % response.read()) + + if request_status == 200: + logger.info(u"Pushalot notifications sent.") + return True + elif request_status == 410: + logger.info(u"Pushalot auth failed: %s" % response.reason) + return False + else: + logger.info(u"Pushalot notification failed.") + return False class Synoindex: def __init__(self, util_loc='/usr/syno/bin/synoindex'): @@ -322,3 +511,107 @@ class PUSHOVER: self.notify('Main Screen Activate', 'Test Message') +class TwitterNotifier: + + consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" + consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk" + + REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' + ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' + AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' + SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + + def notify_snatch(self, title): + if headphones.TWITTER_ONSNATCH: + self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now()) + + def notify_download(self, title): + if headphones.TWITTER_ENABLED: + self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD]+': '+title+' at '+helpers.now()) + + def test_notify(self): + return self._notifyTwitter("This is a test notification from Headphones at "+helpers.now(), force=True) + + def _get_authorization(self): + + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable + oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + oauth_client = oauth.Client(oauth_consumer) + + logger.info('Requesting temp token from Twitter') + + resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') + + if resp['status'] != '200': + logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status']) + else: + request_token = dict(parse_qsl(content)) + + headphones.TWITTER_USERNAME = request_token['oauth_token'] + headphones.TWITTER_PASSWORD = request_token['oauth_token_secret'] + + return self.AUTHORIZATION_URL+"?oauth_token="+ request_token['oauth_token'] + + def _get_credentials(self, key): + request_token = {} + + request_token['oauth_token'] = headphones.TWITTER_USERNAME + request_token['oauth_token_secret'] = headphones.TWITTER_PASSWORD + request_token['oauth_callback_confirmed'] = 'true' + + token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) + token.set_verifier(key) + + logger.info('Generating and signing request for an access token using key '+key) + + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable + oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + logger.info('oauth_consumer: '+str(oauth_consumer)) + oauth_client = oauth.Client(oauth_consumer, token) + logger.info('oauth_client: '+str(oauth_client)) + resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) + logger.info('resp, content: '+str(resp)+','+str(content)) + + access_token = dict(parse_qsl(content)) + logger.info('access_token: '+str(access_token)) + + logger.info('resp[status] = '+str(resp['status'])) + if resp['status'] != '200': + logger.info('The request for a token with did not succeed: '+str(resp['status']), logger.ERROR) + return False + else: + logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token']) + logger.info('Access Token secret: %s' % access_token['oauth_token_secret']) + headphones.TWITTER_USERNAME = access_token['oauth_token'] + headphones.TWITTER_PASSWORD = access_token['oauth_token_secret'] + return True + + + def _send_tweet(self, message=None): + + username=self.consumer_key + password=self.consumer_secret + access_token_key=headphones.TWITTER_USERNAME + access_token_secret=headphones.TWITTER_PASSWORD + + logger.info(u"Sending tweet: "+message) + + api = twitter.Api(username, password, access_token_key, access_token_secret) + + try: + api.PostUpdate(message) + except Exception, e: + logger.info(u"Error Sending Tweet: %s" % e) + return False + + return True + + def _notifyTwitter(self, message='', force=False): + prefix = headphones.TWITTER_PREFIX + + if not headphones.TWITTER_ENABLED and not force: + return False + + return self._send_tweet(prefix+": "+message) + +notifier = TwitterNotifier diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 48e6c3ec..d2fcc8cc 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -55,7 +55,7 @@ def checkFolder(): for nzb_folder_name in nzb_album_possibilities: - nzb_album_path = os.path.join(headphones.DOWNLOAD_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace') + nzb_album_path = os.path.join(headphones.POST_PROCESSING_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace') if os.path.exists(nzb_album_path): logger.debug('Found %s in NZB download folder. Verifying....' % album['FolderName']) @@ -63,7 +63,7 @@ def checkFolder(): if album['Kind'] == 'torrent': - torrent_album_path = os.path.join(headphones.DOWNLOAD_TORRENT_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace') + torrent_album_path = os.path.join(headphones.POST_PROCESSING_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace') if os.path.exists(torrent_album_path): logger.debug('Found %s in torrent download folder. Verifying....' % album['FolderName']) @@ -420,10 +420,23 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, if headphones.XBMC_NOTIFY: xbmc.notify(release['ArtistName'], release['AlbumTitle'], album_art_path) + if headphones.PLEX_ENABLED: + plex = notifiers.Plex() + if headphones.PLEX_UPDATE: + plex.update() + if headphones.PLEX_NOTIFY: + plex.notify(release['ArtistName'], release['AlbumTitle'], album_art_path) + if headphones.NMA_ENABLED: nma = notifiers.NMA() nma.notify(release['ArtistName'], release['AlbumTitle']) + if headphones.PUSHALOT_ENABLED: + pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] + logger.info(u"Pushalot request") + pushalot = notifiers.PUSHALOT() + pushalot.notify(pushmessage,"Download and Postprocessing completed") + if headphones.SYNOINDEX_ENABLED: syno = notifiers.Synoindex() for albumpath in albumpaths: @@ -434,6 +447,18 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, logger.info(u"Pushover request") pushover = notifiers.PUSHOVER() pushover.notify(pushmessage,"Download and Postprocessing completed") + + if headphones.PUSHBULLET_ENABLED: + pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] + logger.info(u"PushBullet request") + pushbullet = notifiers.PUSHBULLET() + pushbullet.notify(pushmessage, "Download and Postprocessing completed") + + if headphones.TWITTER_ENABLED: + pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] + logger.info(u"Sending Twitter notification") + twitter = notifiers.TwitterNotifier() + twitter.notify_download(pushmessage) def embedAlbumArt(artwork, downloaded_track_list): logger.info('Embedding album art') @@ -896,15 +921,10 @@ def renameUnprocessedFolder(albumpath): def forcePostProcess(): download_dirs = [] - if headphones.DOWNLOAD_DIR: - download_dirs.append(headphones.DOWNLOAD_DIR.encode(headphones.SYS_ENCODING, 'replace')) - if headphones.DOWNLOAD_TORRENT_DIR: - download_dirs.append(headphones.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace')) + if headphones.POST_PROCESSING_DIR: + download_dirs.append(headphones.POST_PROCESSING_DIR.encode(headphones.SYS_ENCODING, 'replace')) - # If DOWNLOAD_DIR and DOWNLOAD_TORRENT_DIR are the same, remove the duplicate to prevent us from trying to process the same folder twice. - download_dirs = list(set(download_dirs)) - - logger.info('Checking to see if there are any folders to process in download_dir(s): %s' % str(download_dirs).decode(headphones.SYS_ENCODING, 'replace')) + logger.info('Checking to see if there are any folders to process in: %s' % str(download_dirs).decode(headphones.SYS_ENCODING, 'replace')) # Get a list of folders in the download_dir folders = [] for download_dir in download_dirs: diff --git a/headphones/sab.py b/headphones/sab.py index b337199b..45b494a6 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -127,10 +127,22 @@ def sendNZB(nzb): logger.info(u"Sending Pushover notification") prowl = notifiers.PUSHOVER() prowl.notify(nzb.name,"Download started") + if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH: + logger.info(u"Sending PushBullet notification") + pushbullet = notifiers.PUSHBULLET() + pushbullet.notify(nzb.name + " has been snatched!", "Download started") + if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: + logger.info(u"Sending Twitter notification") + twitter = notifiers.TwitterNotifier() + twitter.notify_snatch(nzb.name) if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH: logger.debug(u"Sending NMA notification") nma = notifiers.NMA() nma.notify(snatched_nzb=nzb.name) + if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: + logger.info(u"Sending Pushalot notification") + pushalot = notifiers.PUSHALOT() + pushalot.notify(nzb.name,"Download started") return True elif sabText == "Missing authentication": diff --git a/headphones/searcher.py b/headphones/searcher.py index 108feae4..bb7fadda 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -33,6 +33,7 @@ import string import shutil import headphones, exceptions +from headphones.common import USER_AGENT from headphones import logger, db, helpers, classes, sab, nzbget from headphones import transmission @@ -113,23 +114,26 @@ def searchforalbum(albumid=None, new=False, lossless=False): myDB = db.DBConnection() - results = myDB.select('SELECT AlbumID, Status from albums WHERE Status="Wanted" OR Status="Wanted Lossless"') + results = myDB.select('SELECT AlbumID, AlbumTitle, ArtistName, Status from albums WHERE Status="Wanted" OR Status="Wanted Lossless"') new = True - + for result in results: foundNZB = "none" - if (headphones.HEADPHONES_INDEXER or headphones.NEWZNAB or headphones.NZBSORG or headphones.NZBSRUS or headphones.OMGWTFNZBS) and (headphones.SAB_HOST or headphones.BLACKHOLE_DIR or headphones.NZBGET_HOST): - if result['Status'] == "Wanted Lossless": - foundNZB = searchNZB(result['AlbumID'], new, losslessOnly=True) - else: - foundNZB = searchNZB(result['AlbumID'], new) - - if (headphones.KAT or headphones.PIRATEBAY or headphones.ISOHUNT or headphones.MININOVA or headphones.WAFFLES or headphones.RUTRACKER or headphones.WHATCD) and foundNZB == "none": - - if result['Status'] == "Wanted Lossless": - searchTorrent(result['AlbumID'], new, losslessOnly=True) - else: - searchTorrent(result['AlbumID'], new) + if not result['AlbumTitle'] or not result['ArtistName']: + logger.warn('Skipping release %s. No title available' % result['AlbumID']) + else: + if (headphones.HEADPHONES_INDEXER or headphones.NEWZNAB or headphones.NZBSORG or headphones.NZBSRUS or headphones.OMGWTFNZBS) and (headphones.SAB_HOST or headphones.BLACKHOLE_DIR or headphones.NZBGET_HOST): + if result['Status'] == "Wanted Lossless": + foundNZB = searchNZB(result['AlbumID'], new, losslessOnly=True) + else: + foundNZB = searchNZB(result['AlbumID'], new) + + if (headphones.KAT or headphones.PIRATEBAY or headphones.ISOHUNT or headphones.MININOVA or headphones.WAFFLES or headphones.RUTRACKER or headphones.WHATCD) and foundNZB == "none": + + if result['Status'] == "Wanted Lossless": + searchTorrent(result['AlbumID'], new, losslessOnly=True) + else: + searchTorrent(result['AlbumID'], new) else: @@ -217,7 +221,7 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): # Add a user-agent request = urllib2.Request(searchURL) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) base64string = base64.encodestring('%s:%s' % (headphones.HPUSER, headphones.HPPASS)).replace('\n', '') request.add_header("Authorization", "Basic %s" % base64string) @@ -296,7 +300,7 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): # Add a user-agent request = urllib2.Request(searchURL) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) opener = urllib2.build_opener() logger.info(u'Parsing results from %s' % (searchURL, newznab_host[0])) @@ -407,7 +411,7 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): # Add a user-agent request = urllib2.Request(searchURL) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) opener = urllib2.build_opener() logger.info(u'Parsing results from NZBsRus' % searchURL) @@ -466,7 +470,7 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): # Add a user-agent request = urllib2.Request(searchURL) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) opener = urllib2.build_opener() logger.info(u'Parsing results from omgwtfnzbs' % searchURL) @@ -743,7 +747,7 @@ def getresultNZB(result): logger.warn("AttributeError in getresultNZB.") elif result[3] == 'headphones': request = urllib2.Request(result[2]) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) base64string = base64.encodestring('%s:%s' % (headphones.HPUSER, headphones.HPPASS)).replace('\n', '') request.add_header("Authorization", "Basic %s" % base64string) @@ -755,7 +759,7 @@ def getresultNZB(result): logger.warn('Error fetching nzb from url: ' + result[2] + ' %s' % e) else: request = urllib2.Request(result[2]) - request.add_header('User-Agent', 'headphones/0.0 +https://github.com/rembo10/headphones') + request.add_header('User-Agent', USER_AGENT) opener = urllib2.build_opener() try: @@ -871,7 +875,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): if headphones.KAT: provider = "Kick Ass Torrent" - providerurl = url_fix("http://www.kat.ph/search/" + term) + providerurl = url_fix("http://kickass.to/usearch/" + term) if headphones.PREFERRED_QUALITY == 3 or losslessOnly: categories = "7" #music format = "2" #flac @@ -1156,7 +1160,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): providerurl = url_fix(pirate_proxy + "/search/" + term + "/0/99/") else: - providerurl = url_fix("http://thepiratebay.sx/search/" + term + "/0/99/") + providerurl = url_fix("http://thepiratebay.se/search/" + term + "/0/99/") if headphones.PREFERRED_QUALITY == 3 or losslessOnly: category = '104' #flac @@ -1522,7 +1526,11 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): return torrent_folder_name = transmission.getTorrentFolder(torrentid) - logger.info('Torrent folder name: %s' % torrent_folder_name) + if torrent_folder_name: + logger.info('Torrent folder name: %s' % torrent_folder_name) + else: + logger.error('Torrent folder name could not be determined') + return # remove temp .torrent file created above if bestqual[3] == 'rutracker.org': @@ -1553,6 +1561,9 @@ def preprocesstorrent(resultlist, pre_sorted_list=False): if result[3] == 'Kick Ass Torrent': request.add_header('Referer', 'http://kat.ph/') + if result[3] == 'What.cd': + request.add_header('User-Agent', 'Headphones') + response = urllib2.urlopen(request) if response.info().get('Content-Encoding') == 'gzip': buf = StringIO(response.read()) diff --git a/headphones/transmission.py b/headphones/transmission.py index 3cbb021f..92073683 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -38,7 +38,16 @@ def addTorrent(link): return False if response['result'] == 'success': - name = response['arguments']['torrent-added']['name'] + if 'torrent-added' in response['arguments']: + name = response['arguments']['torrent-added']['name'] + retid = response['arguments']['torrent-added']['id'] + elif 'torrent-duplicate' in response['arguments']: + name = response['arguments']['torrent-duplicate']['name'] + retid = response['arguments']['torrent-duplicate']['id'] + else: + name = link + retid = False + logger.info(u"Torrent sent to Transmission successfully") if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH: logger.info(u"Sending Prowl notification") @@ -48,12 +57,24 @@ def addTorrent(link): logger.info(u"Sending Pushover notification") prowl = notifiers.PUSHOVER() prowl.notify(name,"Download started") + if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: + logger.info(u"Sending Twitter notification") + twitter = notifiers.TwitterNotifier() + twitter.notify_snatch(nzb.name) if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH: - logger.debug(u"Sending NMA notification") + logger.info(u"Sending NMA notification") nma = notifiers.NMA() nma.notify(snatched_nzb=name) + if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: + logger.info(u"Sending Pushalot notification") + pushalot = notifiers.PUSHALOT() + pushalot.notify(name,"Download started") - return response['arguments']['torrent-added']['id'] + return retid + + else: + logger.info('Transmission returned status %s' % response['result']) + return False def getTorrentFolder(torrentid): method = 'torrent-get' @@ -63,7 +84,10 @@ def getTorrentFolder(torrentid): percentdone = response['arguments']['torrents'][0]['percentDone'] torrent_folder_name = response['arguments']['torrents'][0]['name'] - while percentdone == 0: + tries = 1 + + while percentdone == 0 and tries <10: + tries+=1 time.sleep(5) response = torrentAction(method, arguments) percentdone = response['arguments']['torrents'][0]['percentDone'] diff --git a/headphones/webserve.py b/headphones/webserve.py index 130a17b6..2721d8bb 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -30,7 +30,7 @@ from operator import itemgetter import headphones -from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers +from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers from headphones.helpers import checked, radio,today, cleanName import lib.simplejson as simplejson @@ -110,7 +110,14 @@ class WebInterface(object): album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() tracks = myDB.select('SELECT * from tracks WHERE AlbumID=? ORDER BY CAST(TrackNumber AS INTEGER)', [AlbumID]) description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?', [AlbumID]).fetchone() - title = album['ArtistName'] + ' - ' + album['AlbumTitle'] + if not album['ArtistName']: + title = ' - ' + else: + title = album['ArtistName'] + ' - ' + if not album['AlbumTitle']: + title = title + "" + else: + title = title + album['AlbumTitle'] return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, description=description) albumPage.exposed = True @@ -365,12 +372,12 @@ class WebInterface(object): # else: # original_clean = None if original_clean == albums['CleanName']: - have_dict = { 'ArtistName' : albums['ArtistName'], 'AlbumTitle' : albums['AlbumTitle'] } + have_dict = { 'ArtistName' : albums['ArtistName'], 'AlbumTitle' : albums['AlbumTitle'] } have_album_dictionary.append(have_dict) headphones_albums = myDB.select('SELECT ArtistName, AlbumTitle from albums ORDER BY ArtistName') for albums in headphones_albums: - headphones_dict = { 'ArtistName' : albums['ArtistName'], 'AlbumTitle' : albums['AlbumTitle'] } - headphones_album_dictionary.append(headphones_dict) + headphones_dict = { 'ArtistName' : albums['ArtistName'], 'AlbumTitle' : albums['AlbumTitle'] } + headphones_album_dictionary.append(headphones_dict) #unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]] check = set([(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in headphones_album_dictionary]) @@ -382,16 +389,16 @@ class WebInterface(object): def markUnmatched(self, action=None, existing_artist=None, existing_album=None, new_artist=None, new_album=None): myDB = db.DBConnection() - + if action == "ignoreArtist": artist = existing_artist myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND Matched = "Failed"', [artist]) - + elif action == "ignoreAlbum": artist = existing_artist album = existing_album myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND AlbumTitle=? AND Matched = "Failed"', (artist, album)) - + elif action == "matchArtist": existing_artist_clean = helpers.cleanName(existing_artist).lower() new_artist_clean = helpers.cleanName(new_artist).lower() @@ -464,7 +471,7 @@ class WebInterface(object): librarysync.update_album_status(album_id) else: logger.info("Artist %s / Album %s already named appropriately; nothing to modify" % (existing_artist, existing_album)) - + markUnmatched.exposed = True def manageManual(self): @@ -480,7 +487,7 @@ class WebInterface(object): elif albums['Matched'] == "Manual" or albums['CleanName'] != original_clean: album_status = "Matched" manual_dict = { 'ArtistName' : albums['ArtistName'], 'AlbumTitle' : albums['AlbumTitle'], 'AlbumStatus' : album_status } - if manual_dict not in manual_albums: + if manual_dict not in manual_albums: manual_albums.append(manual_dict) manual_albums_sorted = sorted(manual_albums, key=itemgetter('ArtistName', 'AlbumTitle')) @@ -755,7 +762,7 @@ class WebInterface(object): album_json[counter] = album['AlbumTitle'] counter+=1 json_albums = json.dumps(album_json) - + cherrypy.response.headers['Content-type'] = 'application/json' return json_albums getAlbumsByArtist_json.exposed=True @@ -933,15 +940,31 @@ class WebInterface(object): "xbmc_password": headphones.XBMC_PASSWORD, "xbmc_update": checked(headphones.XBMC_UPDATE), "xbmc_notify": checked(headphones.XBMC_NOTIFY), + "plex_enabled": checked(headphones.PLEX_ENABLED), + "plex_server_host": headphones.PLEX_SERVER_HOST, + "plex_client_host": headphones.PLEX_CLIENT_HOST, + "plex_username": headphones.PLEX_USERNAME, + "plex_password": headphones.PLEX_PASSWORD, + "plex_update": checked(headphones.PLEX_UPDATE), + "plex_notify": checked(headphones.PLEX_NOTIFY), "nma_enabled": checked(headphones.NMA_ENABLED), "nma_apikey": headphones.NMA_APIKEY, "nma_priority": int(headphones.NMA_PRIORITY), "nma_onsnatch": checked(headphones.NMA_ONSNATCH), + "pushalot_enabled": checked(headphones.PUSHALOT_ENABLED), + "pushalot_apikey": headphones.PUSHALOT_APIKEY, + "pushalot_onsnatch": checked(headphones.PUSHALOT_ONSNATCH), "synoindex_enabled": checked(headphones.SYNOINDEX_ENABLED), "pushover_enabled": checked(headphones.PUSHOVER_ENABLED), "pushover_onsnatch": checked(headphones.PUSHOVER_ONSNATCH), "pushover_keys": headphones.PUSHOVER_KEYS, "pushover_priority": headphones.PUSHOVER_PRIORITY, + "pushbullet_enabled": checked(headphones.PUSHBULLET_ENABLED), + "pushbullet_onsnatch": checked(headphones.PUSHBULLET_ONSNATCH), + "pushbullet_apikey": headphones.PUSHBULLET_APIKEY, + "pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID, + "twitter_enabled": checked(headphones.TWITTER_ENABLED), + "twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH), "mirror_list": headphones.MIRRORLIST, "mirror": headphones.MIRROR, "customhost": headphones.CUSTOMHOST, @@ -949,9 +972,11 @@ class WebInterface(object): "customsleep": headphones.CUSTOMSLEEP, "hpuser": headphones.HPUSER, "hppass": headphones.HPPASS, + "songkick_apikey": headphones.SONGKICK_APIKEY, "cache_sizemb": headphones.CACHE_SIZEMB, "file_permissions": headphones.FILE_PERMISSIONS, - "folder_permissions": headphones.FOLDER_PERMISSIONS + "folder_permissions": headphones.FOLDER_PERMISSIONS, + "post_processing_dir" : headphones.POST_PROCESSING_DIR } # Need to convert EXTRAS to a dictionary we can pass to the config: it'll come in as a string like 2,5,6,8 @@ -973,9 +998,9 @@ class WebInterface(object): def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, api_enabled=0, api_key=None, download_scan_interval=None, update_db_interval=None, mb_ignore_age=None, nzb_search_interval=None, libraryscan_interval=None, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None, - sab_category=None, nzbget_host=None, nzbget_username=None, nzbget_password=None, nzbget_category=None, transmission_host=None, transmission_username=None, transmission_password=None, - utorrent_host=None, utorrent_username=None, utorrent_password=None, nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None, - use_headphones_indexer=0, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0, nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, nzbsrus=0, nzbsrus_uid=None, nzbsrus_apikey=None, omgwtfnzbs=0, omgwtfnzbs_uid=None, omgwtfnzbs_apikey=None, + sab_category=None, nzbget_host=None, nzbget_username=None, nzbget_password=None, nzbget_category=None, transmission_host=None, transmission_username=None, transmission_password=None, + utorrent_host=None, utorrent_username=None, utorrent_password=None, nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None, + use_headphones_indexer=0, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0, nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, nzbsrus=0, nzbsrus_uid=None, nzbsrus_apikey=None, omgwtfnzbs=0, omgwtfnzbs_uid=None, omgwtfnzbs_apikey=None, preferred_words=None, required_words=None, ignored_words=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, torrentblackhole_dir=None, download_torrent_dir=None, numberofseeders=None, use_piratebay=0, piratebay_proxy_url=None, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None, whatcd=0, whatcd_username=None, whatcd_password=None, rutracker=0, rutracker_user=None, rutracker_password=None, rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, album_art_format=None, embed_album_art=0, embed_lyrics=0, @@ -983,10 +1008,11 @@ class WebInterface(object): remix=0, spokenword=0, audiobook=0, autowant_upcoming=False, autowant_all=False, keep_torrent_files=False, interface=None, log_dir=None, cache_dir=None, music_encoder=0, encoder=None, xldprofile=None, bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, delete_lossless_files=0, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None, - xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, synoindex_enabled=False, - pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, mirror=None, customhost=None, customport=None, + xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, + pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, - enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, **kwargs): + enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, + plex_password=None, plex_update=0, plex_notify=0, post_processing_dir=None, songkick_apikey=None, **kwargs): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port @@ -1105,24 +1131,42 @@ class WebInterface(object): headphones.XBMC_PASSWORD = xbmc_password headphones.XBMC_UPDATE = xbmc_update headphones.XBMC_NOTIFY = xbmc_notify + headphones.PLEX_ENABLED = plex_enabled + headphones.PLEX_SERVER_HOST = plex_server_host + headphones.PLEX_CLIENT_HOST = plex_client_host + headphones.PLEX_USERNAME = plex_username + headphones.PLEX_PASSWORD = plex_password + headphones.PLEX_UPDATE = plex_update + headphones.PLEX_NOTIFY = plex_notify headphones.NMA_ENABLED = nma_enabled headphones.NMA_APIKEY = nma_apikey headphones.NMA_PRIORITY = nma_priority headphones.NMA_ONSNATCH = nma_onsnatch + headphones.PUSHALOT_ENABLED = pushalot_enabled + headphones.PUSHALOT_APIKEY = pushalot_apikey + headphones.PUSHALOT_ONSNATCH = pushalot_onsnatch headphones.SYNOINDEX_ENABLED = synoindex_enabled headphones.PUSHOVER_ENABLED = pushover_enabled headphones.PUSHOVER_ONSNATCH = pushover_onsnatch headphones.PUSHOVER_KEYS = pushover_keys headphones.PUSHOVER_PRIORITY = pushover_priority + headphones.PUSHBULLET_ENABLED = pushbullet_enabled + headphones.PUSHBULLET_ONSNATCH = pushbullet_onsnatch + headphones.PUSHBULLET_APIKEY = pushbullet_apikey + headphones.PUSHBULLET_DEVICEID = pushbullet_deviceid + headphones.TWITTER_ENABLED = twitter_enabled + headphones.TWITTER_ONSNATCH = twitter_onsnatch headphones.MIRROR = mirror headphones.CUSTOMHOST = customhost headphones.CUSTOMPORT = customport headphones.CUSTOMSLEEP = customsleep headphones.HPUSER = hpuser headphones.HPPASS = hppass + headphones.SONGKICK_APIKEY = songkick_apikey headphones.CACHE_SIZEMB = int(cache_sizemb) headphones.FILE_PERMISSIONS = file_permissions headphones.FOLDER_PERMISSIONS = folder_permissions + headphones.POST_PROCESSING_DIR = post_processing_dir # Handle the variable config options. Note - keys with False values aren't getting passed @@ -1254,6 +1298,33 @@ class WebInterface(object): getImageLinks.exposed = True + def twitterStep1(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + tweet = notifiers.TwitterNotifier() + return tweet._get_authorization() + twitterStep1.exposed = True + + def twitterStep2(self, key): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + tweet = notifiers.TwitterNotifier() + result = tweet._get_credentials(key) + logger.info(u"result: "+str(result)) + if result: + return "Key verification successful" + else: + return "Unable to verify key" + twitterStep2.exposed = True + + def testTwitter(self): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + tweet = notifiers.TwitterNotifier() + result = tweet.test_notify() + if result: + return "Tweet successful, check your twitter to make sure it worked" + else: + return "Error sending tweet" + testTwitter.exposed = True + class Artwork(object): def index(self): return "Artwork" diff --git a/init.ubuntu b/init.ubuntu index c5bbf6a5..5c525ff1 100755 --- a/init.ubuntu +++ b/init.ubuntu @@ -196,9 +196,12 @@ case "$1" in stop_headphones start_headphones ;; + status) + status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC" + ;; *) N=/etc/init.d/$NAME - echo "Usage: $N {start|stop|restart|force-reload}" >&2 + echo "Usage: $N {start|stop|restart|force-reload|status}" >&2 exit 1 ;; esac diff --git a/init.upstart b/init.upstart new file mode 100644 index 00000000..79918a89 --- /dev/null +++ b/init.upstart @@ -0,0 +1,18 @@ +# headphones - Automatic music downloader +# +# This is a session/user job. Install this file into /usr/share/upstart/sessions +# if headphones is installed system wide, and into $XDG_CONFIG_HOME/upstart if +# headphones is installed per user. Change the executable path appropiately. + +start on desktop-start +stop on desktop-end + +env CONFIG=""$XDG_CONFIG_HOME"/headphones" +env DATA=""$XDG_DATA_HOME"/headphones" + +pre-start script + [ -d "$CONFIG" ] || mkdir -p "$CONFIG" + [ -d "$DATA" ] || mkdir -p "$DATA" +end script + +exec Headphones.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA" diff --git a/lib/oauth2/__init__.py b/lib/oauth2/__init__.py new file mode 100644 index 00000000..2b938909 --- /dev/null +++ b/lib/oauth2/__init__.py @@ -0,0 +1,704 @@ +""" +The MIT License + +Copyright (c) 2007 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import urllib +import time +import random +import urlparse +import hmac +import binascii +import lib.httplib2 as httplib2 + +try: + from urlparse import parse_qs, parse_qsl +except ImportError: + from cgi import parse_qs, parse_qsl + + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + + +class Error(RuntimeError): + """Generic exception class.""" + + def __init__(self, message='OAuth error occured.'): + self._message = message + + @property + def message(self): + """A hack to get around the deprecation errors in 2.6.""" + return self._message + + def __str__(self): + return self._message + +class MissingSignature(Error): + pass + +def build_authenticate_header(realm=''): + """Optional WWW-Authenticate header (401 error)""" + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + +def escape(s): + """Escape a URL including any /.""" + return urllib.quote(s, safe='~') + + +def generate_timestamp(): + """Get seconds since epoch (UTC).""" + return int(time.time()) + + +def generate_nonce(length=8): + """Generate pseudorandom number.""" + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + + +def generate_verifier(length=8): + """Generate pseudorandom number.""" + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + + +class Consumer(object): + """A consumer of OAuth-protected services. + + The OAuth consumer is a "third-party" service that wants to access + protected resources from an OAuth service provider on behalf of an end + user. It's kind of the OAuth client. + + Usually a consumer must be registered with the service provider by the + developer of the consumer software. As part of that process, the service + provider gives the consumer a *key* and a *secret* with which the consumer + software can identify itself to the service. The consumer will include its + key in each request to identify itself, but will use its secret only when + signing requests, to prove that the request is from that particular + registered consumer. + + Once registered, the consumer can then use its consumer credentials to ask + the service provider for a request token, kicking off the OAuth + authorization process. + """ + + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + + if self.key is None or self.secret is None: + raise ValueError("Key and secret must be set.") + + def __str__(self): + data = { + 'oauth_consumer_key': self.key, + 'oauth_consumer_secret': self.secret + } + + return urllib.urlencode(data) + + +class Token(object): + """An OAuth credential used to request authorization or a protected + resource. + + Tokens in OAuth comprise a *key* and a *secret*. The key is included in + requests to identify the token being used, but the secret is used only in + the signature, to prove that the requester is who the server gave the + token to. + + When first negotiating the authorization, the consumer asks for a *request + token* that the live user authorizes with the service provider. The + consumer then exchanges the request token for an *access token* that can + be used to access protected resources. + """ + + key = None + secret = None + callback = None + callback_confirmed = None + verifier = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + + if self.key is None or self.secret is None: + raise ValueError("Key and secret must be set.") + + def set_callback(self, callback): + self.callback = callback + self.callback_confirmed = 'true' + + def set_verifier(self, verifier=None): + if verifier is not None: + self.verifier = verifier + else: + self.verifier = generate_verifier() + + def get_callback_url(self): + if self.callback and self.verifier: + # Append the oauth_verifier. + parts = urlparse.urlparse(self.callback) + scheme, netloc, path, params, query, fragment = parts[:6] + if query: + query = '%s&oauth_verifier=%s' % (query, self.verifier) + else: + query = 'oauth_verifier=%s' % self.verifier + return urlparse.urlunparse((scheme, netloc, path, params, + query, fragment)) + return self.callback + + def to_string(self): + """Returns this token as a plain string, suitable for storage. + + The resulting string includes the token's secret, so you should never + send or store this string where a third party can read it. + """ + + data = { + 'oauth_token': self.key, + 'oauth_token_secret': self.secret, + } + + if self.callback_confirmed is not None: + data['oauth_callback_confirmed'] = self.callback_confirmed + return urllib.urlencode(data) + + @staticmethod + def from_string(s): + """Deserializes a token from a string like one returned by + `to_string()`.""" + + if not len(s): + raise ValueError("Invalid parameter string.") + + params = parse_qs(s, keep_blank_values=False) + if not len(params): + raise ValueError("Invalid parameter string.") + + try: + key = params['oauth_token'][0] + except Exception: + raise ValueError("'oauth_token' not found in OAuth request.") + + try: + secret = params['oauth_token_secret'][0] + except Exception: + raise ValueError("'oauth_token_secret' not found in " + "OAuth request.") + + token = Token(key, secret) + try: + token.callback_confirmed = params['oauth_callback_confirmed'][0] + except KeyError: + pass # 1.0, no callback confirmed. + return token + + def __str__(self): + return self.to_string() + + +def setter(attr): + name = attr.__name__ + + def getter(self): + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) + + def deleter(self): + del self.__dict__[name] + + return property(getter, attr, deleter) + + +class Request(dict): + + """The parameters and information for an HTTP request, suitable for + authorizing with OAuth credentials. + + When a consumer wants to access a service's protected resources, it does + so using a signed HTTP request identifying itself (the consumer) with its + key, and providing an access token authorized by the end user to access + those resources. + + """ + + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, method=HTTP_METHOD, url=None, parameters=None): + if method is not None: + self.method = method + + if url is not None: + self.url = url + + if parameters is not None: + self.update(parameters) + + @setter + def url(self, value): + parts = urlparse.urlparse(value) + scheme, netloc, path = parts[:3] + + # Exclude default port numbers. + if scheme == 'http' and netloc[-3:] == ':80': + netloc = netloc[:-3] + elif scheme == 'https' and netloc[-4:] == ':443': + netloc = netloc[:-4] + + if scheme != 'http' and scheme != 'https': + raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) + + value = '%s://%s%s' % (scheme, netloc, path) + self.__dict__['url'] = value + + @setter + def method(self, value): + self.__dict__['method'] = value.upper() + + def _get_timestamp_nonce(self): + return self['oauth_timestamp'], self['oauth_nonce'] + + def get_nonoauth_parameters(self): + """Get any non-OAuth parameters.""" + return dict([(k, v) for k, v in self.iteritems() + if not k.startswith('oauth_')]) + + def to_header(self, realm=''): + """Serialize as a header for an HTTPAuth request.""" + oauth_params = ((k, v) for k, v in self.items() + if k.startswith('oauth_')) + stringy_params = ((k, escape(str(v))) for k, v in oauth_params) + header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) + params_header = ', '.join(header_params) + + auth_header = 'OAuth realm="%s"' % realm + if params_header: + auth_header = "%s, %s" % (auth_header, params_header) + + return {'Authorization': auth_header} + + def to_postdata(self): + """Serialize as post data for a POST request.""" + return self.encode_postdata(self) + + def encode_postdata(self, data): + # tell urlencode to deal with sequence values and map them correctly + # to resulting querystring. for example self["k"] = ["v1", "v2"] will + # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D + return urllib.urlencode(data, True) + + def to_url(self): + """Serialize as a URL for a GET request.""" + return '%s?%s' % (self.url, self.to_postdata()) + + def get_parameter(self, parameter): + ret = self.get(parameter) + if ret is None: + raise Error('Parameter not found: %s' % parameter) + + return ret + + def get_normalized_parameters(self): + """Return a string that contains the parameters that must be signed.""" + items = [(k, v) for k, v in self.items() if k != 'oauth_signature'] + encoded_str = urllib.urlencode(sorted(items), True) + # Encode signature parameters per Oauth Core 1.0 protocol + # spec draft 7, section 3.6 + # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) + # Spaces must be encoded with "%20" instead of "+" + return encoded_str.replace('+', '%20') + + def sign_request(self, signature_method, consumer, token): + """Set the signature parameter to the result of sign.""" + + if 'oauth_consumer_key' not in self: + self['oauth_consumer_key'] = consumer.key + + if token and 'oauth_token' not in self: + self['oauth_token'] = token.key + + self['oauth_signature_method'] = signature_method.name + self['oauth_signature'] = signature_method.sign(self, consumer, token) + + @classmethod + def make_timestamp(cls): + """Get seconds since epoch (UTC).""" + return str(int(time.time())) + + @classmethod + def make_nonce(cls): + """Generate pseudorandom number.""" + return str(random.randint(0, 100000000)) + + @classmethod + def from_request(cls, http_method, http_url, headers=None, parameters=None, + query_string=None): + """Combines multiple parameter sources.""" + if parameters is None: + parameters = {} + + # Headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # Check that the authorization header is OAuth. + if auth_header[:6] == 'OAuth ': + auth_header = auth_header[6:] + try: + # Get the parameters from the header. + header_params = cls._split_header(auth_header) + parameters.update(header_params) + except: + raise Error('Unable to parse OAuth parameters from ' + 'Authorization header.') + + # GET or POST query string. + if query_string: + query_params = cls._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters. + param_str = urlparse.urlparse(http_url)[4] # query + url_params = cls._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return cls(http_method, http_url, parameters) + + return None + + @classmethod + def from_consumer_and_token(cls, consumer, token=None, + http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': consumer.key, + 'oauth_timestamp': cls.make_timestamp(), + 'oauth_nonce': cls.make_nonce(), + 'oauth_version': cls.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return Request(http_method, http_url, parameters) + + @classmethod + def from_token_and_callback(cls, token, callback=None, + http_method=HTTP_METHOD, http_url=None, parameters=None): + + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return cls(http_method, http_url, parameters) + + @staticmethod + def _split_header(header): + """Turn Authorization: header into parameters.""" + params = {} + parts = header.split(',') + for param in parts: + # Ignore realm parameter. + if param.find('realm') > -1: + continue + # Remove whitespace. + param = param.strip() + # Split key-value. + param_parts = param.split('=', 1) + # Remove quotes and unescape the value. + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + + @staticmethod + def _split_url_string(param_str): + """Turn URL string into parameters.""" + parameters = parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + + +class Server(object): + """A skeletal implementation of a service provider, providing protected + resources to requests from authorized consumers. + + This class implements the logic to check requests for authorization. You + can use it with your web server or web framework to protect certain + resources with OAuth. + """ + + timestamp_threshold = 300 # In seconds, five minutes. + version = VERSION + signature_methods = None + + def __init__(self, signature_methods=None): + self.signature_methods = signature_methods or {} + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.name] = signature_method + return self.signature_methods + + def verify_request(self, request, consumer, token): + """Verifies an api call and checks all the parameters.""" + + version = self._get_version(request) + self._check_signature(request, consumer, token) + parameters = request.get_nonoauth_parameters() + return parameters + + def build_authenticate_header(self, realm=''): + """Optional support for the authenticate header.""" + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + def _get_version(self, request): + """Verify the correct version request for this server.""" + try: + version = request.get_parameter('oauth_version') + except: + version = VERSION + + if version and version != self.version: + raise Error('OAuth version %s not supported.' % str(version)) + + return version + + def _get_signature_method(self, request): + """Figure out the signature with some defaults.""" + try: + signature_method = request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + + try: + # Get the signature method object. + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_verifier(self, request): + return request.get_parameter('oauth_verifier') + + def _check_signature(self, request, consumer, token): + timestamp, nonce = request._get_timestamp_nonce() + self._check_timestamp(timestamp) + signature_method = self._get_signature_method(request) + + try: + signature = request.get_parameter('oauth_signature') + except: + raise MissingSignature('Missing oauth_signature.') + + # Validate the signature. + valid = signature_method.check(request, consumer, token, signature) + + if not valid: + key, base = signature_method.signing_base(request, consumer, token) + + raise Error('Invalid signature. Expected signature base ' + 'string: %s' % base) + + built = signature_method.sign(request, consumer, token) + + def _check_timestamp(self, timestamp): + """Verify that timestamp is recentish.""" + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise Error('Expired timestamp: given %d and now %s has a ' + 'greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + +class Client(httplib2.Http): + """OAuthClient is a worker to attempt to execute a request.""" + + def __init__(self, consumer, token=None, cache=None, timeout=None, + proxy_info=None): + + if consumer is not None and not isinstance(consumer, Consumer): + raise ValueError("Invalid consumer.") + + if token is not None and not isinstance(token, Token): + raise ValueError("Invalid token.") + + self.consumer = consumer + self.token = token + self.method = SignatureMethod_HMAC_SHA1() + + httplib2.Http.__init__(self, cache=cache, timeout=timeout, + proxy_info=proxy_info) + + def set_signature_method(self, method): + if not isinstance(method, SignatureMethod): + raise ValueError("Invalid signature method.") + + self.method = method + + def request(self, uri, method="GET", body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None, + force_auth_header=False): + + if not isinstance(headers, dict): + headers = {} + + if body and method == "POST": + parameters = dict(parse_qsl(body)) + elif method == "GET": + parsed = urlparse.urlparse(uri) + parameters = parse_qs(parsed.query) + else: + parameters = None + + req = Request.from_consumer_and_token(self.consumer, token=self.token, + http_method=method, http_url=uri, parameters=parameters) + + req.sign_request(self.method, self.consumer, self.token) + + if force_auth_header: + # ensure we always send Authorization + headers.update(req.to_header()) + + if method == "POST": + if not force_auth_header: + body = req.to_postdata() + else: + body = req.encode_postdata(req.get_nonoauth_parameters()) + headers['Content-Type'] = 'application/x-www-form-urlencoded' + elif method == "GET": + if not force_auth_header: + uri = req.to_url() + else: + if not force_auth_header: + # don't call update twice. + headers.update(req.to_header()) + + return httplib2.Http.request(self, uri, method=method, body=body, + headers=headers, redirections=redirections, + connection_type=connection_type) + + +class SignatureMethod(object): + """A way of signing requests. + + The OAuth protocol lets consumers and service providers pick a way to sign + requests. This interface shows the methods expected by the other `oauth` + modules for signing requests. Subclass it and implement its methods to + provide a new way to sign requests. + """ + + def signing_base(self, request, consumer, token): + """Calculates the string that needs to be signed. + + This method returns a 2-tuple containing the starting key for the + signing and the message to be signed. The latter may be used in error + messages to help clients debug their software. + + """ + raise NotImplementedError + + def sign(self, request, consumer, token): + """Returns the signature for the given request, based on the consumer + and token also provided. + + You should use your implementation of `signing_base()` to build the + message to sign. Otherwise it may be less useful for debugging. + + """ + raise NotImplementedError + + def check(self, request, consumer, token, signature): + """Returns whether the given signature is the correct signature for + the given consumer and token signing the given request.""" + built = self.sign(request, consumer, token) + return built == signature + + +class SignatureMethod_HMAC_SHA1(SignatureMethod): + name = 'HMAC-SHA1' + + def signing_base(self, request, consumer, token): + sig = ( + escape(request.method), + escape(request.url), + escape(request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def sign(self, request, consumer, token): + """Builds the base signature string.""" + key, raw = self.signing_base(request, consumer, token) + + # HMAC object. + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except ImportError: + import sha # Deprecated + hashed = hmac.new(key, raw, sha) + + # Calculate the digest base 64. + return binascii.b2a_base64(hashed.digest())[:-1] + +class SignatureMethod_PLAINTEXT(SignatureMethod): + + name = 'PLAINTEXT' + + def signing_base(self, request, consumer, token): + """Concatenates the consumer key and secret with the token's + secret.""" + sig = '%s&' % escape(consumer.secret) + if token: + sig = sig + escape(token.secret) + return sig, sig + + def sign(self, request, consumer, token): + key, raw = self.signing_base(request, consumer, token) + return raw + diff --git a/lib/pygazelle/api.py b/lib/pygazelle/api.py index 4e93b47d..a778b93a 100644 --- a/lib/pygazelle/api.py +++ b/lib/pygazelle/api.py @@ -98,7 +98,7 @@ class GazelleAPI(object): loginpage = 'https://what.cd/login.php' data = {'username': self.username, 'password': self.password} - r = self.session.post(loginpage, data=data, timeout=self.default_timeout) + r = self.session.post(loginpage, data=data, timeout=self.default_timeout, headers=self.default_headers) self.past_request_timestamps.append(time.time()) if r.status_code != 200: raise LoginException("Login returned status code %s" % r.status_code) diff --git a/lib/pythontwitter/__init__.py b/lib/pythontwitter/__init__.py new file mode 100644 index 00000000..dedfe227 --- /dev/null +++ b/lib/pythontwitter/__init__.py @@ -0,0 +1,4651 @@ +#!/usr/bin/env python +# +# vim: sw=2 ts=2 sts=2 +# +# Copyright 2007 The Python-Twitter Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +'''A library that provides a Python interface to the Twitter API''' + +__author__ = 'python-twitter@googlegroups.com' +__version__ = '1.0.1' + + +import base64 +import calendar +import datetime +import httplib +import os +import rfc822 +import sys +import tempfile +import textwrap +import time +import urllib +import urllib2 +import urlparse +import gzip +import StringIO + +try: + # Python >= 2.6 + import json as simplejson +except ImportError: + try: + # Python < 2.6 + import lib.simplejson as simplejson + except ImportError: + try: + # Google App Engine + from django.utils import simplejson + except ImportError: + raise ImportError, "Unable to load a json library" + +# parse_qsl moved to urlparse module in v2.6 +try: + from urlparse import parse_qsl, parse_qs +except ImportError: + from cgi import parse_qsl, parse_qs + +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +import lib.oauth2 as oauth + + +CHARACTER_LIMIT = 140 + +# A singleton representing a lazily instantiated FileCache. +DEFAULT_CACHE = object() + +REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' +SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + + +class TwitterError(Exception): + '''Base class for Twitter errors''' + + @property + def message(self): + '''Returns the first argument used to construct this error.''' + return self.args[0] + + +class Status(object): + '''A class representing the Status structure used by the twitter API. + + The Status structure exposes the following properties: + + status.created_at + status.created_at_in_seconds # read only + status.favorited + status.favorite_count + status.in_reply_to_screen_name + status.in_reply_to_user_id + status.in_reply_to_status_id + status.truncated + status.source + status.id + status.text + status.location + status.relative_created_at # read only + status.user + status.urls + status.user_mentions + status.hashtags + status.geo + status.place + status.coordinates + status.contributors + ''' + def __init__(self, + created_at=None, + favorited=None, + favorite_count=None, + id=None, + text=None, + location=None, + user=None, + in_reply_to_screen_name=None, + in_reply_to_user_id=None, + in_reply_to_status_id=None, + truncated=None, + source=None, + now=None, + urls=None, + user_mentions=None, + hashtags=None, + media=None, + geo=None, + place=None, + coordinates=None, + contributors=None, + retweeted=None, + retweeted_status=None, + current_user_retweet=None, + retweet_count=None, + possibly_sensitive=None, + scopes=None, + withheld_copyright=None, + withheld_in_countries=None, + withheld_scope=None): + '''An object to hold a Twitter status message. + + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + + Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" + + Args: + created_at: + The time this status message was posted. [Optional] + favorited: + Whether this is a favorite of the authenticated user. [Optional] + favorite_count: + Number of times this status message has been favorited. [Optional] + id: + The unique id of this status message. [Optional] + text: + The text of this status message. [Optional] + location: + the geolocation string associated with this message. [Optional] + relative_created_at: + A human readable string representing the posting time. [Optional] + user: + A twitter.User instance representing the person posting the + message. [Optional] + now: + The current time, if the client chooses to set it. + Defaults to the wall clock time. [Optional] + urls: + user_mentions: + hashtags: + geo: + place: + coordinates: + contributors: + retweeted: + retweeted_status: + current_user_retweet: + retweet_count: + possibly_sensitive: + scopes: + withheld_copyright: + withheld_in_countries: + withheld_scope: + ''' + self.created_at = created_at + self.favorited = favorited + self.favorite_count = favorite_count + self.id = id + self.text = text + self.location = location + self.user = user + self.now = now + self.in_reply_to_screen_name = in_reply_to_screen_name + self.in_reply_to_user_id = in_reply_to_user_id + self.in_reply_to_status_id = in_reply_to_status_id + self.truncated = truncated + self.retweeted = retweeted + self.source = source + self.urls = urls + self.user_mentions = user_mentions + self.hashtags = hashtags + self.media = media + self.geo = geo + self.place = place + self.coordinates = coordinates + self.contributors = contributors + self.retweeted_status = retweeted_status + self.current_user_retweet = current_user_retweet + self.retweet_count = retweet_count + self.possibly_sensitive = possibly_sensitive + self.scopes = scopes + self.withheld_copyright = withheld_copyright + self.withheld_in_countries = withheld_in_countries + self.withheld_scope = withheld_scope + + def GetCreatedAt(self): + '''Get the time this status message was posted. + + Returns: + The time this status message was posted + ''' + return self._created_at + + def SetCreatedAt(self, created_at): + '''Set the time this status message was posted. + + Args: + created_at: + The time this status message was created + ''' + self._created_at = created_at + + created_at = property(GetCreatedAt, SetCreatedAt, + doc='The time this status message was posted.') + + def GetCreatedAtInSeconds(self): + '''Get the time this status message was posted, in seconds since the epoch. + + Returns: + The time this status message was posted, in seconds since the epoch. + ''' + return calendar.timegm(rfc822.parsedate(self.created_at)) + + created_at_in_seconds = property(GetCreatedAtInSeconds, + doc="The time this status message was " + "posted, in seconds since the epoch") + + def GetFavorited(self): + '''Get the favorited setting of this status message. + + Returns: + True if this status message is favorited; False otherwise + ''' + return self._favorited + + def SetFavorited(self, favorited): + '''Set the favorited state of this status message. + + Args: + favorited: + boolean True/False favorited state of this status message + ''' + self._favorited = favorited + + favorited = property(GetFavorited, SetFavorited, + doc='The favorited state of this status message.') + + def GetFavoriteCount(self): + '''Get the favorite count of this status message. + + Returns: + number of times this status message has been favorited + ''' + return self._favorite_count + + def SetFavoriteCount(self, favorite_count): + '''Set the favorited state of this status message. + + Args: + favorite_count: + int number of favorites for this status message + ''' + self._favorite_count = favorite_count + + favorite_count = property(GetFavoriteCount, SetFavoriteCount, + doc='The number of favorites for this status message.') + + def GetId(self): + '''Get the unique id of this status message. + + Returns: + The unique id of this status message + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this status message. + + Args: + id: + The unique id of this status message + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this status message.') + + def GetInReplyToScreenName(self): + return self._in_reply_to_screen_name + + def SetInReplyToScreenName(self, in_reply_to_screen_name): + self._in_reply_to_screen_name = in_reply_to_screen_name + + in_reply_to_screen_name = property(GetInReplyToScreenName, SetInReplyToScreenName, + doc='') + + def GetInReplyToUserId(self): + return self._in_reply_to_user_id + + def SetInReplyToUserId(self, in_reply_to_user_id): + self._in_reply_to_user_id = in_reply_to_user_id + + in_reply_to_user_id = property(GetInReplyToUserId, SetInReplyToUserId, + doc='') + + def GetInReplyToStatusId(self): + return self._in_reply_to_status_id + + def SetInReplyToStatusId(self, in_reply_to_status_id): + self._in_reply_to_status_id = in_reply_to_status_id + + in_reply_to_status_id = property(GetInReplyToStatusId, SetInReplyToStatusId, + doc='') + + def GetTruncated(self): + return self._truncated + + def SetTruncated(self, truncated): + self._truncated = truncated + + truncated = property(GetTruncated, SetTruncated, + doc='') + + def GetRetweeted(self): + return self._retweeted + + def SetRetweeted(self, retweeted): + self._retweeted = retweeted + + retweeted = property(GetRetweeted, SetRetweeted, + doc='') + + def GetSource(self): + return self._source + + def SetSource(self, source): + self._source = source + + source = property(GetSource, SetSource, + doc='') + + def GetText(self): + '''Get the text of this status message. + + Returns: + The text of this status message. + ''' + return self._text + + def SetText(self, text): + '''Set the text of this status message. + + Args: + text: + The text of this status message + ''' + self._text = text + + text = property(GetText, SetText, + doc='The text of this status message') + + def GetLocation(self): + '''Get the geolocation associated with this status message + + Returns: + The geolocation string of this status message. + ''' + return self._location + + def SetLocation(self, location): + '''Set the geolocation associated with this status message + + Args: + location: + The geolocation string of this status message + ''' + self._location = location + + location = property(GetLocation, SetLocation, + doc='The geolocation string of this status message') + + def GetRelativeCreatedAt(self): + '''Get a human readable string representing the posting time + + Returns: + A human readable string representing the posting time + ''' + fudge = 1.25 + delta = long(self.now) - long(self.created_at_in_seconds) + + if delta < (1 * fudge): + return 'about a second ago' + elif delta < (60 * (1/fudge)): + return 'about %d seconds ago' % (delta) + elif delta < (60 * fudge): + return 'about a minute ago' + elif delta < (60 * 60 * (1/fudge)): + return 'about %d minutes ago' % (delta / 60) + elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1: + return 'about an hour ago' + elif delta < (60 * 60 * 24 * (1/fudge)): + return 'about %d hours ago' % (delta / (60 * 60)) + elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1: + return 'about a day ago' + else: + return 'about %d days ago' % (delta / (60 * 60 * 24)) + + relative_created_at = property(GetRelativeCreatedAt, + doc='Get a human readable string representing ' + 'the posting time') + + def GetUser(self): + '''Get a twitter.User representing the entity posting this status message. + + Returns: + A twitter.User representing the entity posting this status message + ''' + return self._user + + def SetUser(self, user): + '''Set a twitter.User representing the entity posting this status message. + + Args: + user: + A twitter.User representing the entity posting this status message + ''' + self._user = user + + user = property(GetUser, SetUser, + doc='A twitter.User representing the entity posting this ' + 'status message') + + def GetNow(self): + '''Get the wallclock time for this status message. + + Used to calculate relative_created_at. Defaults to the time + the object was instantiated. + + Returns: + Whatever the status instance believes the current time to be, + in seconds since the epoch. + ''' + if self._now is None: + self._now = time.time() + return self._now + + def SetNow(self, now): + '''Set the wallclock time for this status message. + + Used to calculate relative_created_at. Defaults to the time + the object was instantiated. + + Args: + now: + The wallclock time for this instance. + ''' + self._now = now + + now = property(GetNow, SetNow, + doc='The wallclock time for this status instance.') + + def GetGeo(self): + return self._geo + + def SetGeo(self, geo): + self._geo = geo + + geo = property(GetGeo, SetGeo, + doc='') + + def GetPlace(self): + return self._place + + def SetPlace(self, place): + self._place = place + + place = property(GetPlace, SetPlace, + doc='') + + def GetCoordinates(self): + return self._coordinates + + def SetCoordinates(self, coordinates): + self._coordinates = coordinates + + coordinates = property(GetCoordinates, SetCoordinates, + doc='') + + def GetContributors(self): + return self._contributors + + def SetContributors(self, contributors): + self._contributors = contributors + + contributors = property(GetContributors, SetContributors, + doc='') + + def GetRetweeted_status(self): + return self._retweeted_status + + def SetRetweeted_status(self, retweeted_status): + self._retweeted_status = retweeted_status + + retweeted_status = property(GetRetweeted_status, SetRetweeted_status, + doc='') + + def GetRetweetCount(self): + return self._retweet_count + + def SetRetweetCount(self, retweet_count): + self._retweet_count = retweet_count + + retweet_count = property(GetRetweetCount, SetRetweetCount, + doc='') + + def GetCurrent_user_retweet(self): + return self._current_user_retweet + + def SetCurrent_user_retweet(self, current_user_retweet): + self._current_user_retweet = current_user_retweet + + current_user_retweet = property(GetCurrent_user_retweet, SetCurrent_user_retweet, + doc='') + + def GetPossibly_sensitive(self): + return self._possibly_sensitive + + def SetPossibly_sensitive(self, possibly_sensitive): + self._possibly_sensitive = possibly_sensitive + + possibly_sensitive = property(GetPossibly_sensitive, SetPossibly_sensitive, + doc='') + + def GetScopes(self): + return self._scopes + + def SetScopes(self, scopes): + self._scopes = scopes + + scopes = property(GetScopes, SetScopes, doc='') + + def GetWithheld_copyright(self): + return self._withheld_copyright + + def SetWithheld_copyright(self, withheld_copyright): + self._withheld_copyright = withheld_copyright + + withheld_copyright = property(GetWithheld_copyright, SetWithheld_copyright, + doc='') + + def GetWithheld_in_countries(self): + return self._withheld_in_countries + + def SetWithheld_in_countries(self, withheld_in_countries): + self._withheld_in_countries = withheld_in_countries + + withheld_in_countries = property(GetWithheld_in_countries, SetWithheld_in_countries, + doc='') + + def GetWithheld_scope(self): + return self._withheld_scope + + def SetWithheld_scope(self, withheld_scope): + self._withheld_scope = withheld_scope + + withheld_scope = property(GetWithheld_scope, SetWithheld_scope, + doc='') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.created_at == other.created_at and \ + self.id == other.id and \ + self.text == other.text and \ + self.location == other.location and \ + self.user == other.user and \ + self.in_reply_to_screen_name == other.in_reply_to_screen_name and \ + self.in_reply_to_user_id == other.in_reply_to_user_id and \ + self.in_reply_to_status_id == other.in_reply_to_status_id and \ + self.truncated == other.truncated and \ + self.retweeted == other.retweeted and \ + self.favorited == other.favorited and \ + self.favorite_count == other.favorite_count and \ + self.source == other.source and \ + self.geo == other.geo and \ + self.place == other.place and \ + self.coordinates == other.coordinates and \ + self.contributors == other.contributors and \ + self.retweeted_status == other.retweeted_status and \ + self.retweet_count == other.retweet_count and \ + self.current_user_retweet == other.current_user_retweet and \ + self.possibly_sensitive == other.possibly_sensitive and \ + self.scopes == other.scopes and \ + self.withheld_copyright == other.withheld_copyright and \ + self.withheld_in_countries == other.withheld_in_countries and \ + self.withheld_scope == other.withheld_scope + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.Status instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.Status instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.Status instance. + + Returns: + A JSON string representation of this twitter.Status instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.Status instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.Status instance + ''' + data = {} + if self.created_at: + data['created_at'] = self.created_at + if self.favorited: + data['favorited'] = self.favorited + if self.favorite_count: + data['favorite_count'] = self.favorite_count + if self.id: + data['id'] = self.id + if self.text: + data['text'] = self.text + if self.location: + data['location'] = self.location + if self.user: + data['user'] = self.user.AsDict() + if self.in_reply_to_screen_name: + data['in_reply_to_screen_name'] = self.in_reply_to_screen_name + if self.in_reply_to_user_id: + data['in_reply_to_user_id'] = self.in_reply_to_user_id + if self.in_reply_to_status_id: + data['in_reply_to_status_id'] = self.in_reply_to_status_id + if self.truncated is not None: + data['truncated'] = self.truncated + if self.retweeted is not None: + data['retweeted'] = self.retweeted + if self.favorited is not None: + data['favorited'] = self.favorited + if self.source: + data['source'] = self.source + if self.geo: + data['geo'] = self.geo + if self.place: + data['place'] = self.place + if self.coordinates: + data['coordinates'] = self.coordinates + if self.contributors: + data['contributors'] = self.contributors + if self.hashtags: + data['hashtags'] = [h.text for h in self.hashtags] + if self.retweeted_status: + data['retweeted_status'] = self.retweeted_status.AsDict() + if self.retweet_count: + data['retweet_count'] = self.retweet_count + if self.urls: + data['urls'] = dict([(url.url, url.expanded_url) for url in self.urls]) + if self.user_mentions: + data['user_mentions'] = [um.AsDict() for um in self.user_mentions] + if self.current_user_retweet: + data['current_user_retweet'] = self.current_user_retweet + if self.possibly_sensitive: + data['possibly_sensitive'] = self.possibly_sensitive + if self.scopes: + data['scopes'] = self.scopes + if self.withheld_copyright: + data['withheld_copyright'] = self.withheld_copyright + if self.withheld_in_countries: + data['withheld_in_countries'] = self.withheld_in_countries + if self.withheld_scope: + data['withheld_scope'] = self.withheld_scope + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.Status instance + ''' + if 'user' in data: + user = User.NewFromJsonDict(data['user']) + else: + user = None + if 'retweeted_status' in data: + retweeted_status = Status.NewFromJsonDict(data['retweeted_status']) + else: + retweeted_status = None + + if 'current_user_retweet' in data: + current_user_retweet = data['current_user_retweet']['id'] + else: + current_user_retweet = None + + urls = None + user_mentions = None + hashtags = None + media = None + if 'entities' in data: + if 'urls' in data['entities']: + urls = [Url.NewFromJsonDict(u) for u in data['entities']['urls']] + if 'user_mentions' in data['entities']: + user_mentions = [User.NewFromJsonDict(u) for u in data['entities']['user_mentions']] + if 'hashtags' in data['entities']: + hashtags = [Hashtag.NewFromJsonDict(h) for h in data['entities']['hashtags']] + if 'media' in data['entities']: + media = data['entities']['media'] + else: + media = [] + return Status(created_at=data.get('created_at', None), + favorited=data.get('favorited', None), + favorite_count=data.get('favorite_count', None), + id=data.get('id', None), + text=data.get('text', None), + location=data.get('location', None), + in_reply_to_screen_name=data.get('in_reply_to_screen_name', None), + in_reply_to_user_id=data.get('in_reply_to_user_id', None), + in_reply_to_status_id=data.get('in_reply_to_status_id', None), + truncated=data.get('truncated', None), + retweeted=data.get('retweeted', None), + source=data.get('source', None), + user=user, + urls=urls, + user_mentions=user_mentions, + hashtags=hashtags, + media=media, + geo=data.get('geo', None), + place=data.get('place', None), + coordinates=data.get('coordinates', None), + contributors=data.get('contributors', None), + retweeted_status=retweeted_status, + current_user_retweet=current_user_retweet, + retweet_count=data.get('retweet_count', None), + possibly_sensitive=data.get('possibly_sensitive', None), + scopes=data.get('scopes', None), + withheld_copyright=data.get('withheld_copyright', None), + withheld_in_countries=data.get('withheld_in_countries', None), + withheld_scope=data.get('withheld_scope', None)) + + +class User(object): + '''A class representing the User structure used by the twitter API. + + The User structure exposes the following properties: + + user.id + user.name + user.screen_name + user.location + user.description + user.profile_image_url + user.profile_background_tile + user.profile_background_image_url + user.profile_sidebar_fill_color + user.profile_background_color + user.profile_link_color + user.profile_text_color + user.protected + user.utc_offset + user.time_zone + user.url + user.status + user.statuses_count + user.followers_count + user.friends_count + user.favourites_count + user.geo_enabled + user.verified + user.lang + user.notifications + user.contributors_enabled + user.created_at + user.listed_count + ''' + def __init__(self, + id=None, + name=None, + screen_name=None, + location=None, + description=None, + profile_image_url=None, + profile_background_tile=None, + profile_background_image_url=None, + profile_sidebar_fill_color=None, + profile_background_color=None, + profile_link_color=None, + profile_text_color=None, + protected=None, + utc_offset=None, + time_zone=None, + followers_count=None, + friends_count=None, + statuses_count=None, + favourites_count=None, + url=None, + status=None, + geo_enabled=None, + verified=None, + lang=None, + notifications=None, + contributors_enabled=None, + created_at=None, + listed_count=None): + self.id = id + self.name = name + self.screen_name = screen_name + self.location = location + self.description = description + self.profile_image_url = profile_image_url + self.profile_background_tile = profile_background_tile + self.profile_background_image_url = profile_background_image_url + self.profile_sidebar_fill_color = profile_sidebar_fill_color + self.profile_background_color = profile_background_color + self.profile_link_color = profile_link_color + self.profile_text_color = profile_text_color + self.protected = protected + self.utc_offset = utc_offset + self.time_zone = time_zone + self.followers_count = followers_count + self.friends_count = friends_count + self.statuses_count = statuses_count + self.favourites_count = favourites_count + self.url = url + self.status = status + self.geo_enabled = geo_enabled + self.verified = verified + self.lang = lang + self.notifications = notifications + self.contributors_enabled = contributors_enabled + self.created_at = created_at + self.listed_count = listed_count + + def GetId(self): + '''Get the unique id of this user. + + Returns: + The unique id of this user + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this user. + + Args: + id: The unique id of this user. + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this user.') + + def GetName(self): + '''Get the real name of this user. + + Returns: + The real name of this user + ''' + return self._name + + def SetName(self, name): + '''Set the real name of this user. + + Args: + name: The real name of this user + ''' + self._name = name + + name = property(GetName, SetName, + doc='The real name of this user.') + + def GetScreenName(self): + '''Get the short twitter name of this user. + + Returns: + The short twitter name of this user + ''' + return self._screen_name + + def SetScreenName(self, screen_name): + '''Set the short twitter name of this user. + + Args: + screen_name: the short twitter name of this user + ''' + self._screen_name = screen_name + + screen_name = property(GetScreenName, SetScreenName, + doc='The short twitter name of this user.') + + def GetLocation(self): + '''Get the geographic location of this user. + + Returns: + The geographic location of this user + ''' + return self._location + + def SetLocation(self, location): + '''Set the geographic location of this user. + + Args: + location: The geographic location of this user + ''' + self._location = location + + location = property(GetLocation, SetLocation, + doc='The geographic location of this user.') + + def GetDescription(self): + '''Get the short text description of this user. + + Returns: + The short text description of this user + ''' + return self._description + + def SetDescription(self, description): + '''Set the short text description of this user. + + Args: + description: The short text description of this user + ''' + self._description = description + + description = property(GetDescription, SetDescription, + doc='The short text description of this user.') + + def GetUrl(self): + '''Get the homepage url of this user. + + Returns: + The homepage url of this user + ''' + return self._url + + def SetUrl(self, url): + '''Set the homepage url of this user. + + Args: + url: The homepage url of this user + ''' + self._url = url + + url = property(GetUrl, SetUrl, + doc='The homepage url of this user.') + + def GetProfileImageUrl(self): + '''Get the url of the thumbnail of this user. + + Returns: + The url of the thumbnail of this user + ''' + return self._profile_image_url + + def SetProfileImageUrl(self, profile_image_url): + '''Set the url of the thumbnail of this user. + + Args: + profile_image_url: The url of the thumbnail of this user + ''' + self._profile_image_url = profile_image_url + + profile_image_url= property(GetProfileImageUrl, SetProfileImageUrl, + doc='The url of the thumbnail of this user.') + + def GetProfileBackgroundTile(self): + '''Boolean for whether to tile the profile background image. + + Returns: + True if the background is to be tiled, False if not, None if unset. + ''' + return self._profile_background_tile + + def SetProfileBackgroundTile(self, profile_background_tile): + '''Set the boolean flag for whether to tile the profile background image. + + Args: + profile_background_tile: Boolean flag for whether to tile or not. + ''' + self._profile_background_tile = profile_background_tile + + profile_background_tile = property(GetProfileBackgroundTile, SetProfileBackgroundTile, + doc='Boolean for whether to tile the background image.') + + def GetProfileBackgroundImageUrl(self): + return self._profile_background_image_url + + def SetProfileBackgroundImageUrl(self, profile_background_image_url): + self._profile_background_image_url = profile_background_image_url + + profile_background_image_url = property(GetProfileBackgroundImageUrl, SetProfileBackgroundImageUrl, + doc='The url of the profile background of this user.') + + def GetProfileSidebarFillColor(self): + return self._profile_sidebar_fill_color + + def SetProfileSidebarFillColor(self, profile_sidebar_fill_color): + self._profile_sidebar_fill_color = profile_sidebar_fill_color + + profile_sidebar_fill_color = property(GetProfileSidebarFillColor, SetProfileSidebarFillColor) + + def GetProfileBackgroundColor(self): + return self._profile_background_color + + def SetProfileBackgroundColor(self, profile_background_color): + self._profile_background_color = profile_background_color + + profile_background_color = property(GetProfileBackgroundColor, SetProfileBackgroundColor) + + def GetProfileLinkColor(self): + return self._profile_link_color + + def SetProfileLinkColor(self, profile_link_color): + self._profile_link_color = profile_link_color + + profile_link_color = property(GetProfileLinkColor, SetProfileLinkColor) + + def GetProfileTextColor(self): + return self._profile_text_color + + def SetProfileTextColor(self, profile_text_color): + self._profile_text_color = profile_text_color + + profile_text_color = property(GetProfileTextColor, SetProfileTextColor) + + def GetProtected(self): + return self._protected + + def SetProtected(self, protected): + self._protected = protected + + protected = property(GetProtected, SetProtected) + + def GetUtcOffset(self): + return self._utc_offset + + def SetUtcOffset(self, utc_offset): + self._utc_offset = utc_offset + + utc_offset = property(GetUtcOffset, SetUtcOffset) + + def GetTimeZone(self): + '''Returns the current time zone string for the user. + + Returns: + The descriptive time zone string for the user. + ''' + return self._time_zone + + def SetTimeZone(self, time_zone): + '''Sets the user's time zone string. + + Args: + time_zone: + The descriptive time zone to assign for the user. + ''' + self._time_zone = time_zone + + time_zone = property(GetTimeZone, SetTimeZone) + + def GetStatus(self): + '''Get the latest twitter.Status of this user. + + Returns: + The latest twitter.Status of this user + ''' + return self._status + + def SetStatus(self, status): + '''Set the latest twitter.Status of this user. + + Args: + status: + The latest twitter.Status of this user + ''' + self._status = status + + status = property(GetStatus, SetStatus, + doc='The latest twitter.Status of this user.') + + def GetFriendsCount(self): + '''Get the friend count for this user. + + Returns: + The number of users this user has befriended. + ''' + return self._friends_count + + def SetFriendsCount(self, count): + '''Set the friend count for this user. + + Args: + count: + The number of users this user has befriended. + ''' + self._friends_count = count + + friends_count = property(GetFriendsCount, SetFriendsCount, + doc='The number of friends for this user.') + + def GetListedCount(self): + '''Get the listed count for this user. + + Returns: + The number of lists this user belongs to. + ''' + return self._listed_count + + def SetListedCount(self, count): + '''Set the listed count for this user. + + Args: + count: + The number of lists this user belongs to. + ''' + self._listed_count = count + + listed_count = property(GetListedCount, SetListedCount, + doc='The number of lists this user belongs to.') + + def GetFollowersCount(self): + '''Get the follower count for this user. + + Returns: + The number of users following this user. + ''' + return self._followers_count + + def SetFollowersCount(self, count): + '''Set the follower count for this user. + + Args: + count: + The number of users following this user. + ''' + self._followers_count = count + + followers_count = property(GetFollowersCount, SetFollowersCount, + doc='The number of users following this user.') + + def GetStatusesCount(self): + '''Get the number of status updates for this user. + + Returns: + The number of status updates for this user. + ''' + return self._statuses_count + + def SetStatusesCount(self, count): + '''Set the status update count for this user. + + Args: + count: + The number of updates for this user. + ''' + self._statuses_count = count + + statuses_count = property(GetStatusesCount, SetStatusesCount, + doc='The number of updates for this user.') + + def GetFavouritesCount(self): + '''Get the number of favourites for this user. + + Returns: + The number of favourites for this user. + ''' + return self._favourites_count + + def SetFavouritesCount(self, count): + '''Set the favourite count for this user. + + Args: + count: + The number of favourites for this user. + ''' + self._favourites_count = count + + favourites_count = property(GetFavouritesCount, SetFavouritesCount, + doc='The number of favourites for this user.') + + def GetGeoEnabled(self): + '''Get the setting of geo_enabled for this user. + + Returns: + True/False if Geo tagging is enabled + ''' + return self._geo_enabled + + def SetGeoEnabled(self, geo_enabled): + '''Set the latest twitter.geo_enabled of this user. + + Args: + geo_enabled: + True/False if Geo tagging is to be enabled + ''' + self._geo_enabled = geo_enabled + + geo_enabled = property(GetGeoEnabled, SetGeoEnabled, + doc='The value of twitter.geo_enabled for this user.') + + def GetVerified(self): + '''Get the setting of verified for this user. + + Returns: + True/False if user is a verified account + ''' + return self._verified + + def SetVerified(self, verified): + '''Set twitter.verified for this user. + + Args: + verified: + True/False if user is a verified account + ''' + self._verified = verified + + verified = property(GetVerified, SetVerified, + doc='The value of twitter.verified for this user.') + + def GetLang(self): + '''Get the setting of lang for this user. + + Returns: + language code of the user + ''' + return self._lang + + def SetLang(self, lang): + '''Set twitter.lang for this user. + + Args: + lang: + language code for the user + ''' + self._lang = lang + + lang = property(GetLang, SetLang, + doc='The value of twitter.lang for this user.') + + def GetNotifications(self): + '''Get the setting of notifications for this user. + + Returns: + True/False for the notifications setting of the user + ''' + return self._notifications + + def SetNotifications(self, notifications): + '''Set twitter.notifications for this user. + + Args: + notifications: + True/False notifications setting for the user + ''' + self._notifications = notifications + + notifications = property(GetNotifications, SetNotifications, + doc='The value of twitter.notifications for this user.') + + def GetContributorsEnabled(self): + '''Get the setting of contributors_enabled for this user. + + Returns: + True/False contributors_enabled of the user + ''' + return self._contributors_enabled + + def SetContributorsEnabled(self, contributors_enabled): + '''Set twitter.contributors_enabled for this user. + + Args: + contributors_enabled: + True/False contributors_enabled setting for the user + ''' + self._contributors_enabled = contributors_enabled + + contributors_enabled = property(GetContributorsEnabled, SetContributorsEnabled, + doc='The value of twitter.contributors_enabled for this user.') + + def GetCreatedAt(self): + '''Get the setting of created_at for this user. + + Returns: + created_at value of the user + ''' + return self._created_at + + def SetCreatedAt(self, created_at): + '''Set twitter.created_at for this user. + + Args: + created_at: + created_at value for the user + ''' + self._created_at = created_at + + created_at = property(GetCreatedAt, SetCreatedAt, + doc='The value of twitter.created_at for this user.') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.name == other.name and \ + self.screen_name == other.screen_name and \ + self.location == other.location and \ + self.description == other.description and \ + self.profile_image_url == other.profile_image_url and \ + self.profile_background_tile == other.profile_background_tile and \ + self.profile_background_image_url == other.profile_background_image_url and \ + self.profile_sidebar_fill_color == other.profile_sidebar_fill_color and \ + self.profile_background_color == other.profile_background_color and \ + self.profile_link_color == other.profile_link_color and \ + self.profile_text_color == other.profile_text_color and \ + self.protected == other.protected and \ + self.utc_offset == other.utc_offset and \ + self.time_zone == other.time_zone and \ + self.url == other.url and \ + self.statuses_count == other.statuses_count and \ + self.followers_count == other.followers_count and \ + self.favourites_count == other.favourites_count and \ + self.friends_count == other.friends_count and \ + self.status == other.status and \ + self.geo_enabled == other.geo_enabled and \ + self.verified == other.verified and \ + self.lang == other.lang and \ + self.notifications == other.notifications and \ + self.contributors_enabled == other.contributors_enabled and \ + self.created_at == other.created_at and \ + self.listed_count == other.listed_count + + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.User instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.User instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.User instance. + + Returns: + A JSON string representation of this twitter.User instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.User instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.User instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.name: + data['name'] = self.name + if self.screen_name: + data['screen_name'] = self.screen_name + if self.location: + data['location'] = self.location + if self.description: + data['description'] = self.description + if self.profile_image_url: + data['profile_image_url'] = self.profile_image_url + if self.profile_background_tile is not None: + data['profile_background_tile'] = self.profile_background_tile + if self.profile_background_image_url: + data['profile_sidebar_fill_color'] = self.profile_background_image_url + if self.profile_background_color: + data['profile_background_color'] = self.profile_background_color + if self.profile_link_color: + data['profile_link_color'] = self.profile_link_color + if self.profile_text_color: + data['profile_text_color'] = self.profile_text_color + if self.protected is not None: + data['protected'] = self.protected + if self.utc_offset: + data['utc_offset'] = self.utc_offset + if self.time_zone: + data['time_zone'] = self.time_zone + if self.url: + data['url'] = self.url + if self.status: + data['status'] = self.status.AsDict() + if self.friends_count: + data['friends_count'] = self.friends_count + if self.followers_count: + data['followers_count'] = self.followers_count + if self.statuses_count: + data['statuses_count'] = self.statuses_count + if self.favourites_count: + data['favourites_count'] = self.favourites_count + if self.geo_enabled: + data['geo_enabled'] = self.geo_enabled + if self.verified: + data['verified'] = self.verified + if self.lang: + data['lang'] = self.lang + if self.notifications: + data['notifications'] = self.notifications + if self.contributors_enabled: + data['contributors_enabled'] = self.contributors_enabled + if self.created_at: + data['created_at'] = self.created_at + if self.listed_count: + data['listed_count'] = self.listed_count + + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.User instance + ''' + if 'status' in data: + status = Status.NewFromJsonDict(data['status']) + else: + status = None + return User(id=data.get('id', None), + name=data.get('name', None), + screen_name=data.get('screen_name', None), + location=data.get('location', None), + description=data.get('description', None), + statuses_count=data.get('statuses_count', None), + followers_count=data.get('followers_count', None), + favourites_count=data.get('favourites_count', None), + friends_count=data.get('friends_count', None), + profile_image_url=data.get('profile_image_url_https', data.get('profile_image_url', None)), + profile_background_tile = data.get('profile_background_tile', None), + profile_background_image_url = data.get('profile_background_image_url', None), + profile_sidebar_fill_color = data.get('profile_sidebar_fill_color', None), + profile_background_color = data.get('profile_background_color', None), + profile_link_color = data.get('profile_link_color', None), + profile_text_color = data.get('profile_text_color', None), + protected = data.get('protected', None), + utc_offset = data.get('utc_offset', None), + time_zone = data.get('time_zone', None), + url=data.get('url', None), + status=status, + geo_enabled=data.get('geo_enabled', None), + verified=data.get('verified', None), + lang=data.get('lang', None), + notifications=data.get('notifications', None), + contributors_enabled=data.get('contributors_enabled', None), + created_at=data.get('created_at', None), + listed_count=data.get('listed_count', None)) + +class List(object): + '''A class representing the List structure used by the twitter API. + + The List structure exposes the following properties: + + list.id + list.name + list.slug + list.description + list.full_name + list.mode + list.uri + list.member_count + list.subscriber_count + list.following + ''' + def __init__(self, + id=None, + name=None, + slug=None, + description=None, + full_name=None, + mode=None, + uri=None, + member_count=None, + subscriber_count=None, + following=None, + user=None): + self.id = id + self.name = name + self.slug = slug + self.description = description + self.full_name = full_name + self.mode = mode + self.uri = uri + self.member_count = member_count + self.subscriber_count = subscriber_count + self.following = following + self.user = user + + def GetId(self): + '''Get the unique id of this list. + + Returns: + The unique id of this list + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this list. + + Args: + id: + The unique id of this list. + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this list.') + + def GetName(self): + '''Get the real name of this list. + + Returns: + The real name of this list + ''' + return self._name + + def SetName(self, name): + '''Set the real name of this list. + + Args: + name: + The real name of this list + ''' + self._name = name + + name = property(GetName, SetName, + doc='The real name of this list.') + + def GetSlug(self): + '''Get the slug of this list. + + Returns: + The slug of this list + ''' + return self._slug + + def SetSlug(self, slug): + '''Set the slug of this list. + + Args: + slug: + The slug of this list. + ''' + self._slug = slug + + slug = property(GetSlug, SetSlug, + doc='The slug of this list.') + + def GetDescription(self): + '''Get the description of this list. + + Returns: + The description of this list + ''' + return self._description + + def SetDescription(self, description): + '''Set the description of this list. + + Args: + description: + The description of this list. + ''' + self._description = description + + description = property(GetDescription, SetDescription, + doc='The description of this list.') + + def GetFull_name(self): + '''Get the full_name of this list. + + Returns: + The full_name of this list + ''' + return self._full_name + + def SetFull_name(self, full_name): + '''Set the full_name of this list. + + Args: + full_name: + The full_name of this list. + ''' + self._full_name = full_name + + full_name = property(GetFull_name, SetFull_name, + doc='The full_name of this list.') + + def GetMode(self): + '''Get the mode of this list. + + Returns: + The mode of this list + ''' + return self._mode + + def SetMode(self, mode): + '''Set the mode of this list. + + Args: + mode: + The mode of this list. + ''' + self._mode = mode + + mode = property(GetMode, SetMode, + doc='The mode of this list.') + + def GetUri(self): + '''Get the uri of this list. + + Returns: + The uri of this list + ''' + return self._uri + + def SetUri(self, uri): + '''Set the uri of this list. + + Args: + uri: + The uri of this list. + ''' + self._uri = uri + + uri = property(GetUri, SetUri, + doc='The uri of this list.') + + def GetMember_count(self): + '''Get the member_count of this list. + + Returns: + The member_count of this list + ''' + return self._member_count + + def SetMember_count(self, member_count): + '''Set the member_count of this list. + + Args: + member_count: + The member_count of this list. + ''' + self._member_count = member_count + + member_count = property(GetMember_count, SetMember_count, + doc='The member_count of this list.') + + def GetSubscriber_count(self): + '''Get the subscriber_count of this list. + + Returns: + The subscriber_count of this list + ''' + return self._subscriber_count + + def SetSubscriber_count(self, subscriber_count): + '''Set the subscriber_count of this list. + + Args: + subscriber_count: + The subscriber_count of this list. + ''' + self._subscriber_count = subscriber_count + + subscriber_count = property(GetSubscriber_count, SetSubscriber_count, + doc='The subscriber_count of this list.') + + def GetFollowing(self): + '''Get the following status of this list. + + Returns: + The following status of this list + ''' + return self._following + + def SetFollowing(self, following): + '''Set the following status of this list. + + Args: + following: + The following of this list. + ''' + self._following = following + + following = property(GetFollowing, SetFollowing, + doc='The following status of this list.') + + def GetUser(self): + '''Get the user of this list. + + Returns: + The owner of this list + ''' + return self._user + + def SetUser(self, user): + '''Set the user of this list. + + Args: + user: + The owner of this list. + ''' + self._user = user + + user = property(GetUser, SetUser, + doc='The owner of this list.') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.name == other.name and \ + self.slug == other.slug and \ + self.description == other.description and \ + self.full_name == other.full_name and \ + self.mode == other.mode and \ + self.uri == other.uri and \ + self.member_count == other.member_count and \ + self.subscriber_count == other.subscriber_count and \ + self.following == other.following and \ + self.user == other.user + + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.List instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.List instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.List instance. + + Returns: + A JSON string representation of this twitter.List instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.List instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.List instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.name: + data['name'] = self.name + if self.slug: + data['slug'] = self.slug + if self.description: + data['description'] = self.description + if self.full_name: + data['full_name'] = self.full_name + if self.mode: + data['mode'] = self.mode + if self.uri: + data['uri'] = self.uri + if self.member_count is not None: + data['member_count'] = self.member_count + if self.subscriber_count is not None: + data['subscriber_count'] = self.subscriber_count + if self.following is not None: + data['following'] = self.following + if self.user is not None: + data['user'] = self.user.AsDict() + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.List instance + ''' + if 'user' in data: + user = User.NewFromJsonDict(data['user']) + else: + user = None + return List(id=data.get('id', None), + name=data.get('name', None), + slug=data.get('slug', None), + description=data.get('description', None), + full_name=data.get('full_name', None), + mode=data.get('mode', None), + uri=data.get('uri', None), + member_count=data.get('member_count', None), + subscriber_count=data.get('subscriber_count', None), + following=data.get('following', None), + user=user) + +class DirectMessage(object): + '''A class representing the DirectMessage structure used by the twitter API. + + The DirectMessage structure exposes the following properties: + + direct_message.id + direct_message.created_at + direct_message.created_at_in_seconds # read only + direct_message.sender_id + direct_message.sender_screen_name + direct_message.recipient_id + direct_message.recipient_screen_name + direct_message.text + ''' + + def __init__(self, + id=None, + created_at=None, + sender_id=None, + sender_screen_name=None, + recipient_id=None, + recipient_screen_name=None, + text=None): + '''An object to hold a Twitter direct message. + + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + + Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" + + Args: + id: + The unique id of this direct message. [Optional] + created_at: + The time this direct message was posted. [Optional] + sender_id: + The id of the twitter user that sent this message. [Optional] + sender_screen_name: + The name of the twitter user that sent this message. [Optional] + recipient_id: + The id of the twitter that received this message. [Optional] + recipient_screen_name: + The name of the twitter that received this message. [Optional] + text: + The text of this direct message. [Optional] + ''' + self.id = id + self.created_at = created_at + self.sender_id = sender_id + self.sender_screen_name = sender_screen_name + self.recipient_id = recipient_id + self.recipient_screen_name = recipient_screen_name + self.text = text + + def GetId(self): + '''Get the unique id of this direct message. + + Returns: + The unique id of this direct message + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this direct message. + + Args: + id: + The unique id of this direct message + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this direct message.') + + def GetCreatedAt(self): + '''Get the time this direct message was posted. + + Returns: + The time this direct message was posted + ''' + return self._created_at + + def SetCreatedAt(self, created_at): + '''Set the time this direct message was posted. + + Args: + created_at: + The time this direct message was created + ''' + self._created_at = created_at + + created_at = property(GetCreatedAt, SetCreatedAt, + doc='The time this direct message was posted.') + + def GetCreatedAtInSeconds(self): + '''Get the time this direct message was posted, in seconds since the epoch. + + Returns: + The time this direct message was posted, in seconds since the epoch. + ''' + return calendar.timegm(rfc822.parsedate(self.created_at)) + + created_at_in_seconds = property(GetCreatedAtInSeconds, + doc="The time this direct message was " + "posted, in seconds since the epoch") + + def GetSenderId(self): + '''Get the unique sender id of this direct message. + + Returns: + The unique sender id of this direct message + ''' + return self._sender_id + + def SetSenderId(self, sender_id): + '''Set the unique sender id of this direct message. + + Args: + sender_id: + The unique sender id of this direct message + ''' + self._sender_id = sender_id + + sender_id = property(GetSenderId, SetSenderId, + doc='The unique sender id of this direct message.') + + def GetSenderScreenName(self): + '''Get the unique sender screen name of this direct message. + + Returns: + The unique sender screen name of this direct message + ''' + return self._sender_screen_name + + def SetSenderScreenName(self, sender_screen_name): + '''Set the unique sender screen name of this direct message. + + Args: + sender_screen_name: + The unique sender screen name of this direct message + ''' + self._sender_screen_name = sender_screen_name + + sender_screen_name = property(GetSenderScreenName, SetSenderScreenName, + doc='The unique sender screen name of this direct message.') + + def GetRecipientId(self): + '''Get the unique recipient id of this direct message. + + Returns: + The unique recipient id of this direct message + ''' + return self._recipient_id + + def SetRecipientId(self, recipient_id): + '''Set the unique recipient id of this direct message. + + Args: + recipient_id: + The unique recipient id of this direct message + ''' + self._recipient_id = recipient_id + + recipient_id = property(GetRecipientId, SetRecipientId, + doc='The unique recipient id of this direct message.') + + def GetRecipientScreenName(self): + '''Get the unique recipient screen name of this direct message. + + Returns: + The unique recipient screen name of this direct message + ''' + return self._recipient_screen_name + + def SetRecipientScreenName(self, recipient_screen_name): + '''Set the unique recipient screen name of this direct message. + + Args: + recipient_screen_name: + The unique recipient screen name of this direct message + ''' + self._recipient_screen_name = recipient_screen_name + + recipient_screen_name = property(GetRecipientScreenName, SetRecipientScreenName, + doc='The unique recipient screen name of this direct message.') + + def GetText(self): + '''Get the text of this direct message. + + Returns: + The text of this direct message. + ''' + return self._text + + def SetText(self, text): + '''Set the text of this direct message. + + Args: + text: + The text of this direct message + ''' + self._text = text + + text = property(GetText, SetText, + doc='The text of this direct message') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.created_at == other.created_at and \ + self.sender_id == other.sender_id and \ + self.sender_screen_name == other.sender_screen_name and \ + self.recipient_id == other.recipient_id and \ + self.recipient_screen_name == other.recipient_screen_name and \ + self.text == other.text + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.DirectMessage instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.DirectMessage instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.DirectMessage instance. + + Returns: + A JSON string representation of this twitter.DirectMessage instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.DirectMessage instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.DirectMessage instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.created_at: + data['created_at'] = self.created_at + if self.sender_id: + data['sender_id'] = self.sender_id + if self.sender_screen_name: + data['sender_screen_name'] = self.sender_screen_name + if self.recipient_id: + data['recipient_id'] = self.recipient_id + if self.recipient_screen_name: + data['recipient_screen_name'] = self.recipient_screen_name + if self.text: + data['text'] = self.text + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.DirectMessage instance + ''' + return DirectMessage(created_at=data.get('created_at', None), + recipient_id=data.get('recipient_id', None), + sender_id=data.get('sender_id', None), + text=data.get('text', None), + sender_screen_name=data.get('sender_screen_name', None), + id=data.get('id', None), + recipient_screen_name=data.get('recipient_screen_name', None)) + +class Hashtag(object): + ''' A class representing a twitter hashtag + ''' + def __init__(self, + text=None): + self.text = text + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.Hashtag instance + ''' + return Hashtag(text = data.get('text', None)) + +class Trend(object): + ''' A class representing a trending topic + ''' + def __init__(self, name=None, query=None, timestamp=None, url=None): + self.name = name + self.query = query + self.timestamp = timestamp + self.url = url + + def __str__(self): + return 'Name: %s\nQuery: %s\nTimestamp: %s\nSearch URL: %s\n' % (self.name, self.query, self.timestamp, self.url) + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.name == other.name and \ + self.query == other.query and \ + self.timestamp == other.timestamp and \ + self.url == self.url + except AttributeError: + return False + + @staticmethod + def NewFromJsonDict(data, timestamp = None): + '''Create a new instance based on a JSON dict + + Args: + data: + A JSON dict + timestamp: + Gets set as the timestamp property of the new object + + Returns: + A twitter.Trend object + ''' + return Trend(name=data.get('name', None), + query=data.get('query', None), + url=data.get('url', None), + timestamp=timestamp) + +class Url(object): + '''A class representing an URL contained in a tweet''' + def __init__(self, + url=None, + expanded_url=None): + self.url = url + self.expanded_url = expanded_url + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.Url instance + ''' + return Url(url=data.get('url', None), + expanded_url=data.get('expanded_url', None)) + +class Api(object): + '''A python interface into the Twitter API + + By default, the Api caches results for 1 minute. + + Example usage: + + To create an instance of the twitter.Api class, with no authentication: + + >>> import twitter + >>> api = twitter.Api() + + To fetch the most recently posted public twitter status messages: + + >>> statuses = api.GetPublicTimeline() + >>> print [s.user.name for s in statuses] + [u'DeWitt', u'Kesuke Miyagi', u'ev', u'Buzz Andersen', u'Biz Stone'] #... + + To fetch a single user's public status messages, where "user" is either + a Twitter "short name" or their user id. + + >>> statuses = api.GetUserTimeline(user) + >>> print [s.text for s in statuses] + + To use authentication, instantiate the twitter.Api class with a + consumer key and secret; and the oAuth key and secret: + + >>> api = twitter.Api(consumer_key='twitter consumer key', + consumer_secret='twitter consumer secret', + access_token_key='the_key_given', + access_token_secret='the_key_secret') + + To fetch your friends (after being authenticated): + + >>> users = api.GetFriends() + >>> print [u.name for u in users] + + To post a twitter status message (after being authenticated): + + >>> status = api.PostUpdate('I love python-twitter!') + >>> print status.text + I love python-twitter! + + There are many other methods, including: + + >>> api.PostUpdates(status) + >>> api.PostDirectMessage(user, text) + >>> api.GetUser(user) + >>> api.GetReplies() + >>> api.GetUserTimeline(user) + >>> api.GetHomeTimeLine() + >>> api.GetStatus(id) + >>> api.DestroyStatus(id) + >>> api.GetFriendsTimeline(user) + >>> api.GetFriends(user) + >>> api.GetFollowers() + >>> api.GetFeatured() + >>> api.GetDirectMessages() + >>> api.GetSentDirectMessages() + >>> api.PostDirectMessage(user, text) + >>> api.DestroyDirectMessage(id) + >>> api.DestroyFriendship(user) + >>> api.CreateFriendship(user) + >>> api.GetUserByEmail(email) + >>> api.VerifyCredentials() + ''' + + DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute + _API_REALM = 'Twitter API' + + def __init__(self, + consumer_key=None, + consumer_secret=None, + access_token_key=None, + access_token_secret=None, + input_encoding=None, + request_headers=None, + cache=DEFAULT_CACHE, + shortner=None, + base_url=None, + use_gzip_compression=False, + debugHTTP=False): + '''Instantiate a new twitter.Api object. + + Args: + consumer_key: + Your Twitter user's consumer_key. + consumer_secret: + Your Twitter user's consumer_secret. + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + input_encoding: + The encoding used to encode input strings. [Optional] + request_header: + A dictionary of additional HTTP request headers. [Optional] + cache: + The cache instance to use. Defaults to DEFAULT_CACHE. + Use None to disable caching. [Optional] + shortner: + The shortner instance to use. Defaults to None. + See shorten_url.py for an example shortner. [Optional] + base_url: + The base URL to use to contact the Twitter API. + Defaults to https://api.twitter.com. [Optional] + use_gzip_compression: + Set to True to tell enable gzip compression for any call + made to Twitter. Defaults to False. [Optional] + debugHTTP: + Set to True to enable debug output from urllib2 when performing + any HTTP requests. Defaults to False. [Optional] + ''' + self.SetCache(cache) + self._urllib = urllib2 + self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT + self._input_encoding = input_encoding + self._use_gzip = use_gzip_compression + self._debugHTTP = debugHTTP + self._oauth_consumer = None + self._shortlink_size = 19 + + self._InitializeRequestHeaders(request_headers) + self._InitializeUserAgent() + self._InitializeDefaultParameters() + + if base_url is None: + self.base_url = 'https://api.twitter.com/1.1' + else: + self.base_url = base_url + + if consumer_key is not None and (access_token_key is None or + access_token_secret is None): + print >> sys.stderr, 'Twitter now requires an oAuth Access Token for API calls.' + print >> sys.stderr, 'If your using this library from a command line utility, please' + print >> sys.stderr, 'run the the included get_access_token.py tool to generate one.' + + raise TwitterError('Twitter requires oAuth Access Token for all API access') + + self.SetCredentials(consumer_key, consumer_secret, access_token_key, access_token_secret) + + def SetCredentials(self, + consumer_key, + consumer_secret, + access_token_key=None, + access_token_secret=None): + '''Set the consumer_key and consumer_secret for this instance + + Args: + consumer_key: + The consumer_key of the twitter account. + consumer_secret: + The consumer_secret for the twitter account. + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + ''' + self._consumer_key = consumer_key + self._consumer_secret = consumer_secret + self._access_token_key = access_token_key + self._access_token_secret = access_token_secret + self._oauth_consumer = None + + if consumer_key is not None and consumer_secret is not None and \ + access_token_key is not None and access_token_secret is not None: + self._signature_method_plaintext = oauth.SignatureMethod_PLAINTEXT() + self._signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + + self._oauth_token = oauth.Token(key=access_token_key, secret=access_token_secret) + self._oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) + + def ClearCredentials(self): + '''Clear the any credentials for this instance + ''' + self._consumer_key = None + self._consumer_secret = None + self._access_token_key = None + self._access_token_secret = None + self._oauth_consumer = None + + def GetSearch(self, + term=None, + geocode=None, + since_id=None, + max_id=None, + until=None, + count=15, + lang=None, + locale=None, + result_type="mixed", + include_entities=None): + '''Return twitter search results for a given term. + + Args: + term: + Term to search by. Optional if you include geocode. + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than (that is, older + than) or equal to the specified ID. [Optional] + until: + Returns tweets generated before the given date. Date should be + formatted as YYYY-MM-DD. [Optional] + geocode: + Geolocation information in the form (latitude, longitude, radius) + [Optional] + count: + Number of results to return. Default is 15 [Optional] + lang: + Language for results as ISO 639-1 code. Default is None (all languages) + [Optional] + locale: + Language of the search query. Currently only 'ja' is effective. This is + intended for language-specific consumers and the default should work in + the majority of cases. + result_type: + Type of result which should be returned. Default is "mixed". Other + valid options are "recent" and "popular". [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discrete structure, including: user_mentions, urls, and + hashtags. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message containing + the term + ''' + # Build request parameters + parameters = {} + + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + + if until: + parameters['until'] = until + + if lang: + parameters['lang'] = lang + + if locale: + parameters['locale'] = locale + + if term is None and geocode is None: + return [] + + if term is not None: + parameters['q'] = term + + if geocode is not None: + parameters['geocode'] = ','.join(map(str, geocode)) + + if include_entities: + parameters['include_entities'] = 1 + + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if result_type in ["mixed", "popular", "recent"]: + parameters['result_type'] = result_type + + # Make and send requests + url = '%s/search/tweets.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + + # Return built list of statuses + return [Status.NewFromJsonDict(x) for x in data['statuses']] + + def GetUsersSearch(self, + term=None, + page=1, + count=20, + include_entities=None): + '''Return twitter user search results for a given term. + + Args: + term: + Term to search by. + page: + Page of results to return. Default is 1 + [Optional] + count: + Number of results to return. Default is 20 + [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discrete structure, including: user_mentions, urls, and hashtags. + [Optional] + + Returns: + A sequence of twitter.User instances, one for each message containing + the term + ''' + # Build request parameters + parameters = {} + + if term is not None: + parameters['q'] = term + + if include_entities: + parameters['include_entities'] = 1 + + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + # Make and send requests + url = '%s/users/search.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [User.NewFromJsonDict(x) for x in data] + + def GetTrendsCurrent(self, exclude=None): + '''Get the current top trending topics (global) + + Args: + exclude: + Appends the exclude parameter as a request parameter. + Currently only exclude=hashtags is supported. [Optional] + + Returns: + A list with 10 entries. Each entry contains a trend. + ''' + return self.GetTrendsWoeid(id=1, exclude=exclude) + + def GetTrendsWoeid(self, id, exclude=None): + '''Return the top 10 trending topics for a specific WOEID, if trending + information is available for it. + + Args: + woeid: + the Yahoo! Where On Earth ID for a location. + exclude: + Appends the exclude parameter as a request parameter. + Currently only exclude=hashtags is supported. [Optional] + + Returns: + A list with 10 entries. Each entry contains a trend. + ''' + url = '%s/trends/place.json' % (self.base_url) + parameters = {'id': id} + + if exclude: + parameters['exclude'] = exclude + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + + trends = [] + timestamp = data[0]['as_of'] + + for trend in data[0]['trends']: + trends.append(Trend.NewFromJsonDict(trend, timestamp = timestamp)) + return trends + + def GetHomeTimeline(self, + count=None, + since_id=None, + max_id=None, + trim_user=False, + exclude_replies=False, + contributor_details=False, + include_entities=True): + ''' + Fetch a collection of the most recent Tweets and retweets posted by the + authenticating user and the users they follow. + + The home timeline is central to how most users interact with the Twitter + service. + + The twitter.Api instance must be authenticated. + + Args: + count: + Specifies the number of statuses to retrieve. May not be + greater than 200. Defaults to 20. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + trim_user: + When True, each tweet returned in a timeline will include a user + object including only the status authors numerical ID. Omit this + parameter to receive the complete user object. [Optional] + exclude_replies: + This parameter will prevent replies from appearing in the + returned timeline. Using exclude_replies with the count + parameter will mean you will receive up-to count tweets - + this is because the count parameter retrieves that many + tweets before filtering out retweets and replies. + [Optional] + contributor_details: + This parameter enhances the contributors element of the + status response to include the screen_name of the contributor. + By default only the user_id of the contributor is included. + [Optional] + include_entities: + The entities node will be disincluded when set to false. + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message + ''' + url = '%s/statuses/home_timeline.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("API must be authenticated.") + parameters = {} + if count is not None: + try: + if int(count) > 200: + raise TwitterError("'count' may not be greater than 200") + except ValueError: + raise TwitterError("'count' must be an integer") + parameters['count'] = count + if since_id: + try: + parameters['since_id'] = long(since_id) + except ValueError: + raise TwitterError("'since_id' must be an integer") + if max_id: + try: + parameters['max_id'] = long(max_id) + except ValueError: + raise TwitterError("'max_id' must be an integer") + if trim_user: + parameters['trim_user'] = 1 + if exclude_replies: + parameters['exclude_replies'] = 1 + if contributor_details: + parameters['contributor_details'] = 1 + if not include_entities: + parameters['include_entities'] = 'false' + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(x) for x in data] + + def GetUserTimeline(self, + user_id=None, + screen_name=None, + since_id=None, + max_id=None, + count=None, + include_rts=None, + trim_user=None, + exclude_replies=None): + '''Fetch the sequence of public Status messages for a single user. + + The twitter.Api instance must be authenticated if the user is private. + + Args: + user_id: + Specifies the ID of the user for whom to return the + user_timeline. Helpful for disambiguating when a valid user ID + is also a valid screen name. [Optional] + screen_name: + Specifies the screen name of the user for whom to return the + user_timeline. Helpful for disambiguating when a valid screen + name is also a user ID. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than (that is, older + than) or equal to the specified ID. [Optional] + count: + Specifies the number of statuses to retrieve. May not be + greater than 200. [Optional] + include_rts: + If True, the timeline will contain native retweets (if they + exist) in addition to the standard stream of tweets. [Optional] + trim_user: + If True, statuses will only contain the numerical user ID only. + Otherwise a full user object will be returned for each status. + [Optional] + exclude_replies: + If True, this will prevent replies from appearing in the returned + timeline. Using exclude_replies with the count parameter will mean you + will receive up-to count tweets - this is because the count parameter + retrieves that many tweets before filtering out retweets and replies. + This parameter is only supported for JSON and XML responses. [Optional] + + Returns: + A sequence of Status instances, one for each message up to count + ''' + parameters = {} + + url = '%s/statuses/user_timeline.json' % (self.base_url) + + if user_id: + parameters['user_id'] = user_id + elif screen_name: + parameters['screen_name'] = screen_name + + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if include_rts: + parameters['include_rts'] = 1 + + if trim_user: + parameters['trim_user'] = 1 + + if exclude_replies: + parameters['exclude_replies'] = 1 + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(x) for x in data] + + def GetStatus(self, + id, + trim_user=False, + include_my_retweet=True, + include_entities=True): + '''Returns a single status message, specified by the id parameter. + + The twitter.Api instance must be authenticated. + + Args: + id: + The numeric ID of the status you are trying to retrieve. + trim_user: + When set to True, each tweet returned in a timeline will include + a user object including only the status authors numerical ID. + Omit this parameter to receive the complete user object. + [Optional] + include_my_retweet: + When set to True, any Tweets returned that have been retweeted by + the authenticating user will include an additional + current_user_retweet node, containing the ID of the source status + for the retweet. [Optional] + include_entities: + If False, the entities node will be disincluded. + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] + Returns: + A twitter.Status instance representing that status message + ''' + url = '%s/statuses/show.json' % (self.base_url) + + if not self._oauth_consumer: + raise TwitterError("API must be authenticated.") + + parameters = {} + + try: + parameters['id'] = long(id) + except ValueError: + raise TwitterError("'id' must be an integer.") + + if trim_user: + parameters['trim_user'] = 1 + if include_my_retweet: + parameters['include_my_retweet'] = 1 + if not include_entities: + parameters['include_entities'] = 'none' + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + def DestroyStatus(self, id, trim_user=False): + '''Destroys the status specified by the required ID parameter. + + The twitter.Api instance must be authenticated and the + authenticating user must be the author of the specified status. + + Args: + id: + The numerical ID of the status you're trying to destroy. + + Returns: + A twitter.Status instance representing the destroyed status message + ''' + if not self._oauth_consumer: + raise TwitterError("API must be authenticated.") + + try: + post_data = {'id': long(id)} + except: + raise TwitterError("id must be an integer") + url = '%s/statuses/destroy/%s.json' % (self.base_url, id) + if trim_user: + post_data['trim_user'] = 1 + json = self._FetchUrl(url, post_data=post_data) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + @classmethod + def _calculate_status_length(cls, status, linksize=19): + dummy_link_replacement = 'https://-%d-chars%s/' % (linksize, '-'*(linksize - 18)) + shortened = ' '.join([x if not (x.startswith('http://') or + x.startswith('https://')) + else + dummy_link_replacement + for x in status.split(' ')]) + return len(shortened) + + def PostUpdate(self, status, in_reply_to_status_id=None, latitude=None, longitude=None, place_id=None, display_coordinates=False, trim_user=False): + '''Post a twitter status message from the authenticated user. + + The twitter.Api instance must be authenticated. + + https://dev.twitter.com/docs/api/1.1/post/statuses/update + + Args: + status: + The message text to be posted. + Must be less than or equal to 140 characters. + in_reply_to_status_id: + The ID of an existing status that the status to be posted is + in reply to. This implicitly sets the in_reply_to_user_id + attribute of the resulting status to the user ID of the + message being replied to. Invalid/missing status IDs will be + ignored. [Optional] + latitude: + Latitude coordinate of the tweet in degrees. Will only work + in conjunction with longitude argument. Both longitude and + latitude will be ignored by twitter if the user has a false + geo_enabled setting. [Optional] + longitude: + Longitude coordinate of the tweet in degrees. Will only work + in conjunction with latitude argument. Both longitude and + latitude will be ignored by twitter if the user has a false + geo_enabled setting. [Optional] + place_id: + A place in the world. These IDs can be retrieved from + GET geo/reverse_geocode. [Optional] + display_coordinates: + Whether or not to put a pin on the exact coordinates a tweet + has been sent from. [Optional] + trim_user: + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + [Optional] + Returns: + A twitter.Status instance representing the message posted. + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + url = '%s/statuses/update.json' % self.base_url + + if isinstance(status, unicode) or self._input_encoding is None: + u_status = status + else: + u_status = unicode(status, self._input_encoding) + + #if self._calculate_status_length(u_status, self._shortlink_size) > CHARACTER_LIMIT: + # raise TwitterError("Text must be less than or equal to %d characters. " + # "Consider using PostUpdates." % CHARACTER_LIMIT) + + data = {'status': status} + if in_reply_to_status_id: + data['in_reply_to_status_id'] = in_reply_to_status_id + if latitude is not None and longitude is not None: + data['lat'] = str(latitude) + data['long'] = str(longitude) + if place_id is not None: + data['place_id'] = str(place_id) + if display_coordinates: + data['display_coordinates'] = 'true' + if trim_user: + data['trim_user'] = 'true' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + def PostUpdates(self, status, continuation=None, **kwargs): + '''Post one or more twitter status messages from the authenticated user. + + Unlike api.PostUpdate, this method will post multiple status updates + if the message is longer than 140 characters. + + The twitter.Api instance must be authenticated. + + Args: + status: + The message text to be posted. + May be longer than 140 characters. + continuation: + The character string, if any, to be appended to all but the + last message. Note that Twitter strips trailing '...' strings + from messages. Consider using the unicode \u2026 character + (horizontal ellipsis) instead. [Defaults to None] + **kwargs: + See api.PostUpdate for a list of accepted parameters. + + Returns: + A of list twitter.Status instance representing the messages posted. + ''' + results = list() + if continuation is None: + continuation = '' + line_length = CHARACTER_LIMIT - len(continuation) + lines = textwrap.wrap(status, line_length) + for line in lines[0:-1]: + results.append(self.PostUpdate(line + continuation, **kwargs)) + results.append(self.PostUpdate(lines[-1], **kwargs)) + return results + + def PostRetweet(self, original_id, trim_user=False): + '''Retweet a tweet with the Retweet API. + + The twitter.Api instance must be authenticated. + + Args: + original_id: + The numerical id of the tweet that will be retweeted + trim_user: + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + [Optional] + + Returns: + A twitter.Status instance representing the original tweet with retweet details embedded. + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + try: + if int(original_id) <= 0: + raise TwitterError("'original_id' must be a positive number") + except ValueError: + raise TwitterError("'original_id' must be an integer") + + url = '%s/statuses/retweet/%s.json' % (self.base_url, original_id) + + data = {'id': original_id} + if trim_user: + data['trim_user'] = 'true' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + def GetUserRetweets(self, count=None, since_id=None, max_id=None, trim_user=False): + '''Fetch the sequence of retweets made by the authenticated user. + + The twitter.Api instance must be authenticated. + + Args: + count: + The number of status messages to retrieve. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + trim_user: + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message up to count + ''' + return self.GetUserTimeline(since_id=since_id, count=count, max_id=max_id, trim_user=trim_user, exclude_replies=True, include_rts=True) + + def GetReplies(self, since_id=None, count=None, max_id=None, trim_user=False): + '''Get a sequence of status messages representing the 20 most + recent replies (status updates prefixed with @twitterID) to the + authenticating user. + + Args: + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + trim_user: + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + [Optional] + + Returns: + A sequence of twitter.Status instances, one for each reply to the user. + ''' + return self.GetUserTimeline(since_id=since_id, count=count, max_id=max_id, trim_user=trim_user, exclude_replies=False, include_rts=False) + + def GetRetweets(self, statusid, count=None, trim_user=False): + '''Returns up to 100 of the first retweets of the tweet identified + by statusid + + Args: + statusid: + The ID of the tweet for which retweets should be searched for + count: + The number of status messages to retrieve. [Optional] + trim_user: + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + [Optional] + + Returns: + A list of twitter.Status instances, which are retweets of statusid + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instsance must be authenticated.") + url = '%s/statuses/retweets/%s.json' % (self.base_url, statusid) + parameters = {} + if trim_user: + parameters['trim_user'] = 'true' + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(s) for s in data] + + def GetRetweetsOfMe(self, + count=None, + since_id=None, + max_id=None, + trim_user=False, + include_entities=True, + include_user_entities=True): + '''Returns up to 100 of the most recent tweets of the user that have been + retweeted by others. + + Args: + count: + The number of retweets to retrieve, up to 100. If omitted, 20 is + assumed. + since_id: + Returns results with an ID greater than (newer than) this ID. + max_id: + Returns results with an ID less than or equal to this ID. + trim_user: + When True, the user object for each tweet will only be an ID. + include_entities: + When True, the tweet entities will be included. + include_user_entities: + When True, the user entities will be included. + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + url = '%s/statuses/retweets_of_me.json' % self.base_url + parameters = {} + if count is not None: + try: + if int(count) > 100: + raise TwitterError("'count' may not be greater than 100") + except ValueError: + raise TwitterError("'count' must be an integer") + if count: + parameters['count'] = count + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if trim_user: + parameters['trim_user'] = trim_user + if not include_entities: + parameters['include_entities'] = include_entities + if not include_user_entities: + parameters['include_user_entities'] = include_user_entities + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(s) for s in data] + + def GetFriends(self, user_id=None, screen_name=None, cursor=-1, skip_status=False, include_user_entities=False): + '''Fetch the sequence of twitter.User instances, one for each friend. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + The twitter id of the user whose friends you are fetching. + If not specified, defaults to the authenticated user. [Optional] + screen_name: + The twitter name of the user whose friends you are fetching. + If not specified, defaults to the authenticated user. [Optional] + cursor: + Should be set to -1 for the initial call and then is used to + control what result page Twitter returns [Optional(ish)] + skip_status: + If True the statuses will not be returned in the user items. + [Optional] + include_user_entities: + When True, the user entities will be included. + + Returns: + A sequence of twitter.User instances, one for each friend + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + url = '%s/friends/list.json' % self.base_url + result = [] + parameters = {} + if user_id is not None: + parameters['user_id'] = user_id + if screen_name is not None: + parameters['screen_name'] = screen_name + if skip_status: + parameters['skip_status'] = True + if include_user_entities: + parameters['include_user_entities'] = True + while True: + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + result += [User.NewFromJsonDict(x) for x in data['users']] + if 'next_cursor' in data: + if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: + break + else: + cursor = data['next_cursor'] + else: + break + return result + + def GetFriendIDs(self, user_id=None, screen_name=None, cursor=-1, stringify_ids=False, count=None): + '''Returns a list of twitter user id's for every person + the specified user is following. + + Args: + user_id: + The id of the user to retrieve the id list for + [Optional] + screen_name: + The screen_name of the user to retrieve the id list for + [Optional] + cursor: + Specifies the Twitter API Cursor location to start at. + Note: there are pagination limits. + [Optional] + stringify_ids: + if True then twitter will return the ids as strings instead of integers. + [Optional] + count: + The number of status messages to retrieve. [Optional] + + Returns: + A list of integers, one for each user id. + ''' + url = '%s/friends/ids.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + parameters = {} + if user_id is not None: + parameters['user_id'] = user_id + if screen_name is not None: + parameters['screen_name'] = screen_name + if stringify_ids: + parameters['stringify_ids'] = True + if count is not None: + parameters['count'] = count + result = [] + while True: + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + result += [x for x in data['ids']] + if 'next_cursor' in data: + if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: + break + else: + cursor = data['next_cursor'] + else: + break + return result + + + def GetFollowerIDs(self, user_id=None, screen_name=None, cursor=-1, stringify_ids=False, count=None, total_count=None): + '''Returns a list of twitter user id's for every person + that is following the specified user. + + Args: + user_id: + The id of the user to retrieve the id list for + [Optional] + screen_name: + The screen_name of the user to retrieve the id list for + [Optional] + cursor: + Specifies the Twitter API Cursor location to start at. + Note: there are pagination limits. + [Optional] + stringify_ids: + if True then twitter will return the ids as strings instead of integers. + [Optional] + count: + The number of user id's to retrieve per API request. Please be aware that + this might get you rate-limited if set to a small number. By default Twitter + will retrieve 5000 UIDs per call. + [Optional] + total_count: + The total amount of UIDs to retrieve. Good if the account has many followers + and you don't want to get rate limited. The data returned might contain more + UIDs if total_count is not a multiple of count (5000 by default). + [Optional] + + + Returns: + A list of integers, one for each user id. + ''' + url = '%s/followers/ids.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + parameters = {} + if user_id is not None: + parameters['user_id'] = user_id + if screen_name is not None: + parameters['screen_name'] = screen_name + if stringify_ids: + parameters['stringify_ids'] = True + if count is not None: + parameters['count'] = count + result = [] + while True: + if total_count and total_count < count: + parameters['count'] = total_count + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + result += [x for x in data['ids']] + if 'next_cursor' in data: + if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: + break + else: + cursor = data['next_cursor'] + total_count -= len(data['ids']) + if total_count < 1: + break + else: + break + return result + + def GetFollowers(self, user_id=None, screen_name=None, cursor=-1, skip_status=False, include_user_entities=False): + '''Fetch the sequence of twitter.User instances, one for each follower + + The twitter.Api instance must be authenticated. + + Args: + user_id: + The twitter id of the user whose followers you are fetching. + If not specified, defaults to the authenticated user. [Optional] + screen_name: + The twitter name of the user whose followers you are fetching. + If not specified, defaults to the authenticated user. [Optional] + cursor: + Should be set to -1 for the initial call and then is used to + control what result page Twitter returns [Optional(ish)] + skip_status: + If True the statuses will not be returned in the user items. + [Optional] + include_user_entities: + When True, the user entities will be included. + + Returns: + A sequence of twitter.User instances, one for each follower + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + url = '%s/followers/list.json' % self.base_url + result = [] + parameters = {} + if user_id is not None: + parameters['user_id'] = user_id + if screen_name is not None: + parameters['screen_name'] = screen_name + if skip_status: + parameters['skip_status'] = True + if include_user_entities: + parameters['include_user_entities'] = True + while True: + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + result += [User.NewFromJsonDict(x) for x in data['users']] + if 'next_cursor' in data: + if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: + break + else: + cursor = data['next_cursor'] + else: + break + return result + + def UsersLookup(self, user_id=None, screen_name=None, users=None, include_entities=True): + '''Fetch extended information for the specified users. + + Users may be specified either as lists of either user_ids, + screen_names, or twitter.User objects. The list of users that + are queried is the union of all specified parameters. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + A list of user_ids to retrieve extended information. + [Optional] + screen_name: + A list of screen_names to retrieve extended information. + [Optional] + users: + A list of twitter.User objects to retrieve extended information. + [Optional] + include_entities: + The entities node that may appear within embedded statuses will be + disincluded when set to False. + [Optional] + + Returns: + A list of twitter.User objects for the requested users + ''' + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + if not user_id and not screen_name and not users: + raise TwitterError("Specify at least one of user_id, screen_name, or users.") + url = '%s/users/lookup.json' % self.base_url + parameters = {} + uids = list() + if user_id: + uids.extend(user_id) + if users: + uids.extend([u.id for u in users]) + if len(uids): + parameters['user_id'] = ','.join(["%s" % u for u in uids]) + if screen_name: + parameters['screen_name'] = ','.join(screen_name) + if not include_entities: + parameters['include_entities'] = 'false' + json = self._FetchUrl(url, parameters=parameters) + try: + data = self._ParseAndCheckTwitter(json) + except TwitterError, e: + _, e, _ = sys.exc_info() + t = e.args[0] + if len(t) == 1 and ('code' in t[0]) and (t[0]['code'] == 34): + data = [] + else: + raise + + return [User.NewFromJsonDict(u) for u in data] + + def GetUser(self, user_id=None, screen_name=None, include_entities=True): + '''Returns a single user. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + The id of the user to retrieve. + [Optional] + screen_name: + The screen name of the user for whom to return results for. Either a + user_id or screen_name is required for this method. + [Optional] + include_entities: + if set to False, the 'entities' node will not be included. + [Optional] + + + Returns: + A twitter.User instance representing that user + ''' + url = '%s/users/show.json' % (self.base_url) + parameters = {} + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + if user_id: + parameters['user_id'] = user_id + elif screen_name: + parameters['screen_name'] = screen_name + else: + raise TwitterError("Specify at least one of user_id or screen_name.") + + if not include_entities: + parameters['include_entities'] = 'false' + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return User.NewFromJsonDict(data) + + def GetDirectMessages(self, since_id=None, max_id=None, count=None, include_entities=True, skip_status=False): + '''Returns a list of the direct messages sent to the authenticating user. + + The twitter.Api instance must be authenticated. + + Args: + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + count: + Specifies the number of direct messages to try and retrieve, up to a + maximum of 200. The value of count is best thought of as a limit to the + number of Tweets to return because suspended or deleted content is + removed after the count has been applied. [Optional] + include_entities: + The entities node will not be included when set to False. + [Optional] + skip_status: + When set to True statuses will not be included in the returned user + objects. [Optional] + + Returns: + A sequence of twitter.DirectMessage instances + ''' + url = '%s/direct_messages.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {} + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + if not include_entities: + parameters['include_entities'] = 'false' + if skip_status: + parameters['skip_status'] = 1 + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [DirectMessage.NewFromJsonDict(x) for x in data] + + def GetSentDirectMessages(self, since_id=None, max_id=None, count=None, page=None, include_entities=True): + '''Returns a list of the direct messages sent by the authenticating user. + + The twitter.Api instance must be authenticated. + + Args: + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + count: + Specifies the number of direct messages to try and retrieve, up to a + maximum of 200. The value of count is best thought of as a limit to the + number of Tweets to return because suspended or deleted content is + removed after the count has been applied. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + include_entities: + The entities node will not be included when set to False. + [Optional] + + Returns: + A sequence of twitter.DirectMessage instances + ''' + url = '%s/direct_messages/sent.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {} + if since_id: + parameters['since_id'] = since_id + if page: + parameters['page'] = page + if max_id: + parameters['max_id'] = max_id + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + if not include_entities: + parameters['include_entities'] = 'false' + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [DirectMessage.NewFromJsonDict(x) for x in data] + + def PostDirectMessage(self, text, user_id=None, screen_name=None): + '''Post a twitter direct message from the authenticated user + + The twitter.Api instance must be authenticated. user_id or screen_name + must be specified. + + Args: + text: The message text to be posted. Must be less than 140 characters. + user_id: + The ID of the user who should receive the direct message. + [Optional] + screen_name: + The screen name of the user who should receive the direct message. + [Optional] + + Returns: + A twitter.DirectMessage instance representing the message posted + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + url = '%s/direct_messages/new.json' % self.base_url + data = {'text': text} + if user_id: + data['user_id'] = user_id + elif screen_name: + data['screen_name'] = screen_name + else: + raise TwitterError("Specify at least one of user_id or screen_name.") + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return DirectMessage.NewFromJsonDict(data) + + def DestroyDirectMessage(self, id, include_entities=True): + '''Destroys the direct message specified in the required ID parameter. + + The twitter.Api instance must be authenticated, and the + authenticating user must be the recipient of the specified direct + message. + + Args: + id: The id of the direct message to be destroyed + + Returns: + A twitter.DirectMessage instance representing the message destroyed + ''' + url = '%s/direct_messages/destroy.json' % self.base_url + data = {'id': id} + if not include_entities: + data['include_entities'] = 'false' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return DirectMessage.NewFromJsonDict(data) + + def CreateFriendship(self, user_id=None, screen_name=None, follow=True): + '''Befriends the user specified by the user_id or screen_name. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + A user_id to follow [Optional] + screen_name: + A screen_name to follow [Optional] + follow: + Set to False to disable notifications for the target user + Returns: + A twitter.User instance representing the befriended user. + ''' + url = '%s/friendships/create.json' % (self.base_url) + data = {} + if user_id: + data['user_id'] = user_id + elif screen_name: + data['screen_name'] = screen_name + else: + raise TwitterError("Specify at least one of user_id or screen_name.") + if follow: + data['follow'] = 'true' + else: + data['follow'] = 'false' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return User.NewFromJsonDict(data) + + def DestroyFriendship(self, user_id=None, screen_name=None): + '''Discontinues friendship with a user_id or screen_name. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + A user_id to unfollow [Optional] + screen_name: + A screen_name to unfollow [Optional] + Returns: + A twitter.User instance representing the discontinued friend. + ''' + url = '%s/friendships/destroy.json' % self.base_url + data = {} + if user_id: + data['user_id'] = user_id + elif screen_name: + data['screen_name'] = screen_name + else: + raise TwitterError("Specify at least one of user_id or screen_name.") + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return User.NewFromJsonDict(data) + + def CreateFavorite(self, status=None, id=None, include_entities=True): + '''Favorites the specified status object or id as the authenticating user. + Returns the favorite status when successful. + + The twitter.Api instance must be authenticated. + + Args: + id: + The id of the twitter status to mark as a favorite. + [Optional] + status: + The twitter.Status object to mark as a favorite. + [Optional] + include_entities: + The entities node will be omitted when set to False. + Returns: + A twitter.Status instance representing the newly-marked favorite. + ''' + url = '%s/favorites/create.json' % self.base_url + data = {} + if id: + data['id'] = id + elif status: + data['id'] = status.id + else: + raise TwitterError("Specify id or status") + if not include_entities: + data['include_entities'] = 'false' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + def DestroyFavorite(self, status=None, id=None, include_entities=True): + '''Un-Favorites the specified status object or id as the authenticating user. + Returns the un-favorited status when successful. + + The twitter.Api instance must be authenticated. + + Args: + id: + The id of the twitter status to unmark as a favorite. + [Optional] + status: + The twitter.Status object to unmark as a favorite. + [Optional] + include_entities: + The entities node will be omitted when set to False. + Returns: + A twitter.Status instance representing the newly-unmarked favorite. + ''' + url = '%s/favorites/destroy.json' % self.base_url + data = {} + if id: + data['id'] = id + elif status: + data['id'] = status.id + else: + raise TwitterError("Specify id or status") + if not include_entities: + data['include_entities'] = 'false' + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return Status.NewFromJsonDict(data) + + def GetFavorites(self, + user_id=None, + screen_name=None, + count=None, + since_id=None, + max_id=None, + include_entities=True): + '''Return a list of Status objects representing favorited tweets. + By default, returns the (up to) 20 most recent tweets for the + authenticated user. + + Args: + user: + The twitter name or id of the user whose favorites you are fetching. + If not specified, defaults to the authenticated user. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + ''' + parameters = {} + + url = '%s/favorites/list.json' % self.base_url + + if user_id: + parameters['user_id'] = user_id + elif screen_name: + parameters['screen_name'] = user_id + + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if include_entities: + parameters['include_entities'] = True + + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(x) for x in data] + + def GetMentions(self, + count=None, + since_id=None, + max_id=None, + trim_user=False, + contributor_details=False, + include_entities=True): + '''Returns the 20 most recent mentions (status containing @screen_name) + for the authenticating user. + + Args: + count: + Specifies the number of tweets to try and retrieve, up to a maximum of + 200. The value of count is best thought of as a limit to the number of + tweets to return because suspended or deleted content is removed after + the count has been applied. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occurred since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [Optional] + trim_user: + When set to True, each tweet returned in a timeline will include a user + object including only the status authors numerical ID. Omit this + parameter to receive the complete user object. + contributor_details: + If set to True, this parameter enhances the contributors element of the + status response to include the screen_name of the contributor. By + default only the user_id of the contributor is included. + include_entities: + The entities node will be disincluded when set to False. + + Returns: + A sequence of twitter.Status instances, one for each mention of the user. + ''' + + url = '%s/statuses/mentions_timeline.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if count: + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + if since_id: + try: + parameters['since_id'] = long(since_id) + except: + raise TwitterError("since_id must be an integer") + if max_id: + try: + parameters['max_id'] = long(max_id) + except: + raise TwitterError("max_id must be an integer") + if trim_user: + parameters['trim_user'] = 1 + if contributor_details: + parameters['contributor_details'] = 'true' + if not include_entities: + parameters['include_entities'] = 'false' + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [Status.NewFromJsonDict(x) for x in data] + + def CreateList(self, name, mode=None, description=None): + '''Creates a new list with the give name for the authenticated user. + + The twitter.Api instance must be authenticated. + + Args: + name: + New name for the list + mode: + 'public' or 'private'. + Defaults to 'public'. [Optional] + description: + Description of the list. [Optional] + + Returns: + A twitter.List instance representing the new list + ''' + url = '%s/lists/create.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {'name': name} + if mode is not None: + parameters['mode'] = mode + if description is not None: + parameters['description'] = description + json = self._FetchUrl(url, post_data=parameters) + data = self._ParseAndCheckTwitter(json) + return List.NewFromJsonDict(data) + + def DestroyList(self, + owner_screen_name=False, + owner_id=False, + list_id=None, + slug=None): + ''' + Destroys the list identified by list_id or owner_screen_name/owner_id and + slug. + + The twitter.Api instance must be authenticated. + + Args: + owner_screen_name: + The screen_name of the user who owns the list being requested by a slug. + owner_id: + The user ID of the user who owns the list being requested by a slug. + list_id: + The numerical id of the list. + slug: + You can identify a list by its slug instead of its numerical id. If you + decide to do so, note that you'll also have to specify the list owner + using the owner_id or owner_screen_name parameters. + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/lists/destroy.json' % self.base_url + data = {} + if list_id: + try: + data['list_id']= long(list_id) + except: + raise TwitterError("list_id must be an integer") + elif slug: + data['slug'] = slug + if owner_id: + try: + data['owner_id'] = long(owner_id) + except: + raise TwitterError("owner_id must be an integer") + elif owner_screen_name: + data['owner_screen_name'] = owner_screen_name + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return List.NewFromJsonDict(data) + + def CreateSubscription(self, + owner_screen_name=False, + owner_id=False, + list_id=None, + slug=None): + '''Creates a subscription to a list by the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner_screen_name: + The screen_name of the user who owns the list being requested by a slug. + owner_id: + The user ID of the user who owns the list being requested by a slug. + list_id: + The numerical id of the list. + slug: + You can identify a list by its slug instead of its numerical id. If you + decide to do so, note that you'll also have to specify the list owner + using the owner_id or owner_screen_name parameters. + Returns: + A twitter.List instance representing the list subscribed to + ''' + url = '%s/lists/subscribers/create.json' % (self.base_url) + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + data = {} + if list_id: + try: + data['list_id']= long(list_id) + except: + raise TwitterError("list_id must be an integer") + elif slug: + data['slug'] = slug + if owner_id: + try: + data['owner_id'] = long(owner_id) + except: + raise TwitterError("owner_id must be an integer") + elif owner_screen_name: + data['owner_screen_name'] = owner_screen_name + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return List.NewFromJsonDict(data) + + def DestroySubscription(self, + owner_screen_name=False, + owner_id=False, + list_id=None, + slug=None): + '''Destroys the subscription to a list for the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner_screen_name: + The screen_name of the user who owns the list being requested by a slug. + owner_id: + The user ID of the user who owns the list being requested by a slug. + list_id: + The numerical id of the list. + slug: + You can identify a list by its slug instead of its numerical id. If you + decide to do so, note that you'll also have to specify the list owner + using the owner_id or owner_screen_name parameters. + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/lists/subscribers/destroy.json' % (self.base_url) + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + data = {} + if list_id: + try: + data['list_id']= long(list_id) + except: + raise TwitterError("list_id must be an integer") + elif slug: + data['slug'] = slug + if owner_id: + try: + data['owner_id'] = long(owner_id) + except: + raise TwitterError("owner_id must be an integer") + elif owner_screen_name: + data['owner_screen_name'] = owner_screen_name + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + else: + raise TwitterError("Identify list by list_id or owner_screen_name/owner_id and slug") + json = self._FetchUrl(url, post_data=data) + data = self._ParseAndCheckTwitter(json) + return List.NewFromJsonDict(data) + + def GetSubscriptions(self, user_id=None, screen_name=None, count=20, cursor=-1): + ''' + Obtain a collection of the lists the specified user is subscribed to, 20 + lists per page by default. Does not include the user's own lists. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + The ID of the user for whom to return results for. [Optional] + screen_name: + The screen name of the user for whom to return results for. + [Optional] + count: + The amount of results to return per page. Defaults to 20. + No more than 1000 results will ever be returned in a single page. + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/lists/subscriptions.json' % (self.base_url) + parameters = {} + + try: + parameters['cursor'] = int(cursor) + except: + raise TwitterError("cursor must be an integer") + + try: + parameters['count'] = int(count) + except: + raise TwitterError("count must be an integer") + + if user_id is not None: + try: + parameters['user_id'] = long(user_id) + except: + raise TwitterError('user_id must be an integer') + elif screen_name is not None: + parameters['screen_name'] = screen_name + else: + raise TwitterError('Specify user_id or screen_name') + + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + return [List.NewFromJsonDict(x) for x in data['lists']] + + def GetLists(self, user_id=None, screen_name=None, count=None, cursor=-1): + '''Fetch the sequence of lists for a user. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + The ID of the user for whom to return results for. [Optional] + screen_name: + The screen name of the user for whom to return results for. + [Optional] + count: + The amount of results to return per page. Defaults to 20. No more than + 1000 results will ever be returned in a single page. + [Optional] + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/lists/ownerships.json' % self.base_url + result = [] + parameters = {} + if user_id is not None: + try: + parameters['user_id'] = long(user_id) + except: + raise TwitterError('user_id must be an integer') + elif screen_name is not None: + parameters['screen_name'] = screen_name + else: + raise TwitterError('Specify user_id or screen_name') + if count is not None: + parameters['count'] = count + + while True: + parameters['cursor'] = cursor + json = self._FetchUrl(url, parameters=parameters) + data = self._ParseAndCheckTwitter(json) + result += [List.NewFromJsonDict(x) for x in data['lists']] + if 'next_cursor' in data: + if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: + break + else: + cursor = data['next_cursor'] + else: + break + return result + + def VerifyCredentials(self): + '''Returns a twitter.User instance if the authenticating user is valid. + + Returns: + A twitter.User instance representing that user if the + credentials are valid, None otherwise. + ''' + if not self._oauth_consumer: + raise TwitterError("Api instance must first be given user credentials.") + url = '%s/account/verify_credentials.json' % self.base_url + try: + json = self._FetchUrl(url, no_cache=True) + except urllib2.HTTPError, http_error: + if http_error.code == httplib.UNAUTHORIZED: + return None + else: + raise http_error + data = self._ParseAndCheckTwitter(json) + return User.NewFromJsonDict(data) + + def SetCache(self, cache): + '''Override the default cache. Set to None to prevent caching. + + Args: + cache: + An instance that supports the same API as the twitter._FileCache + ''' + if cache == DEFAULT_CACHE: + self._cache = _FileCache() + else: + self._cache = cache + + def SetUrllib(self, urllib): + '''Override the default urllib implementation. + + Args: + urllib: + An instance that supports the same API as the urllib2 module + ''' + self._urllib = urllib + + def SetCacheTimeout(self, cache_timeout): + '''Override the default cache timeout. + + Args: + cache_timeout: + Time, in seconds, that responses should be reused. + ''' + self._cache_timeout = cache_timeout + + def SetUserAgent(self, user_agent): + '''Override the default user agent + + Args: + user_agent: + A string that should be send to the server as the User-agent + ''' + self._request_headers['User-Agent'] = user_agent + + def SetXTwitterHeaders(self, client, url, version): + '''Set the X-Twitter HTTP headers that will be sent to the server. + + Args: + client: + The client name as a string. Will be sent to the server as + the 'X-Twitter-Client' header. + url: + The URL of the meta.xml as a string. Will be sent to the server + as the 'X-Twitter-Client-URL' header. + version: + The client version as a string. Will be sent to the server + as the 'X-Twitter-Client-Version' header. + ''' + self._request_headers['X-Twitter-Client'] = client + self._request_headers['X-Twitter-Client-URL'] = url + self._request_headers['X-Twitter-Client-Version'] = version + + def SetSource(self, source): + '''Suggest the "from source" value to be displayed on the Twitter web site. + + The value of the 'source' parameter must be first recognized by + the Twitter server. New source values are authorized on a case by + case basis by the Twitter development team. + + Args: + source: + The source name as a string. Will be sent to the server as + the 'source' parameter. + ''' + self._default_params['source'] = source + + def GetRateLimitStatus(self, resources=None): + '''Fetch the rate limit status for the currently authorized user. + + Args: + resources: + A comma seperated list of resource families you want to know the current + rate limit disposition of. + [Optional] + + Returns: + A dictionary containing the time the limit will reset (reset_time), + the number of remaining hits allowed before the reset (remaining_hits), + the number of hits allowed in a 60-minute period (hourly_limit), and + the time of the reset in seconds since The Epoch (reset_time_in_seconds). + ''' + parameters = {} + if resources is not None: + parameters['resources'] = resources + + url = '%s/application/rate_limit_status.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters, no_cache=True) + data = self._ParseAndCheckTwitter(json) + return data + + def MaximumHitFrequency(self): + '''Determines the minimum number of seconds that a program must wait + before hitting the server again without exceeding the rate_limit + imposed for the currently authenticated user. + + Returns: + The minimum second interval that a program must use so as to not + exceed the rate_limit imposed for the user. + ''' + rate_status = self.GetRateLimitStatus() + reset_time = rate_status.get('reset_time', None) + limit = rate_status.get('remaining_hits', None) + + if reset_time: + # put the reset time into a datetime object + reset = datetime.datetime(*rfc822.parsedate(reset_time)[:7]) + + # find the difference in time between now and the reset time + 1 hour + delta = reset + datetime.timedelta(hours=1) - datetime.datetime.utcnow() + + if not limit: + return int(delta.seconds) + + # determine the minimum number of seconds allowed as a regular interval + max_frequency = int(delta.seconds / limit) + 1 + + # return the number of seconds + return max_frequency + + return 60 + + def _BuildUrl(self, url, path_elements=None, extra_params=None): + # Break url into constituent parts + (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) + + # Add any additional path elements to the path + if path_elements: + # Filter out the path elements that have a value of None + p = [i for i in path_elements if i] + if not path.endswith('/'): + path += '/' + path += '/'.join(p) + + # Add any additional query parameters to the query string + if extra_params and len(extra_params) > 0: + extra_query = self._EncodeParameters(extra_params) + # Add it to the existing query + if query: + query += '&' + extra_query + else: + query = extra_query + + # Return the rebuilt URL + return urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) + + def _InitializeRequestHeaders(self, request_headers): + if request_headers: + self._request_headers = request_headers + else: + self._request_headers = {} + + def _InitializeUserAgent(self): + user_agent = 'Python-urllib/%s (python-twitter/%s)' % \ + (self._urllib.__version__, __version__) + self.SetUserAgent(user_agent) + + def _InitializeDefaultParameters(self): + self._default_params = {} + + def _DecompressGzippedResponse(self, response): + raw_data = response.read() + if response.headers.get('content-encoding', None) == 'gzip': + url_data = gzip.GzipFile(fileobj=StringIO.StringIO(raw_data)).read() + else: + url_data = raw_data + return url_data + + def _Encode(self, s): + if self._input_encoding: + return unicode(s, self._input_encoding).encode('utf-8') + else: + return unicode(s).encode('utf-8') + + def _EncodeParameters(self, parameters): + '''Return a string in key=value&key=value form + + Values of None are not included in the output string. + + Args: + parameters: + A dict of (key, value) tuples, where value is encoded as + specified by self._encoding + + Returns: + A URL-encoded string in "key=value&key=value" form + ''' + if parameters is None: + return None + else: + return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in parameters.items() if v is not None])) + + def _EncodePostData(self, post_data): + '''Return a string in key=value&key=value form + + Values are assumed to be encoded in the format specified by self._encoding, + and are subsequently URL encoded. + + Args: + post_data: + A dict of (key, value) tuples, where value is encoded as + specified by self._encoding + + Returns: + A URL-encoded string in "key=value&key=value" form + ''' + if post_data is None: + return None + else: + return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in post_data.items()])) + + def _ParseAndCheckTwitter(self, json): + """Try and parse the JSON returned from Twitter and return + an empty dictionary if there is any error. This is a purely + defensive check because during some Twitter network outages + it will return an HTML failwhale page.""" + try: + data = simplejson.loads(json) + self._CheckForTwitterError(data) + except ValueError: + if "Twitter / Over capacity" in json: + raise TwitterError("Capacity Error") + if "Twitter / Error" in json: + raise TwitterError("Technical Error") + raise TwitterError("json decoding") + + return data + + def _CheckForTwitterError(self, data): + """Raises a TwitterError if twitter returns an error message. + + Args: + data: + A python dict created from the Twitter json response + + Raises: + TwitterError wrapping the twitter error message if one exists. + """ + # Twitter errors are relatively unlikely, so it is faster + # to check first, rather than try and catch the exception + if 'error' in data: + raise TwitterError(data['error']) + if 'errors' in data: + raise TwitterError(data['errors']) + + def _FetchUrl(self, + url, + post_data=None, + parameters=None, + no_cache=None, + use_gzip_compression=None): + '''Fetch a URL, optionally caching for a specified time. + + Args: + url: + The URL to retrieve + post_data: + A dict of (str, unicode) key/value pairs. + If set, POST will be used. + parameters: + A dict whose key/value pairs should encoded and added + to the query string. [Optional] + no_cache: + If true, overrides the cache on the current request + use_gzip_compression: + If True, tells the server to gzip-compress the response. + It does not apply to POST requests. + Defaults to None, which will get the value to use from + the instance variable self._use_gzip [Optional] + + Returns: + A string containing the body of the response. + ''' + # Build the extra parameters dict + extra_params = {} + if self._default_params: + extra_params.update(self._default_params) + if parameters: + extra_params.update(parameters) + + if post_data: + http_method = "POST" + else: + http_method = "GET" + + if self._debugHTTP: + _debug = 1 + else: + _debug = 0 + + http_handler = self._urllib.HTTPHandler(debuglevel=_debug) + https_handler = self._urllib.HTTPSHandler(debuglevel=_debug) + http_proxy = os.environ.get('http_proxy') + https_proxy = os.environ.get('https_proxy') + + if http_proxy is None or https_proxy is None : + proxy_status = False + else : + proxy_status = True + + opener = self._urllib.OpenerDirector() + opener.add_handler(http_handler) + opener.add_handler(https_handler) + + if proxy_status is True : + proxy_handler = self._urllib.ProxyHandler({'http':str(http_proxy),'https': str(https_proxy)}) + opener.add_handler(proxy_handler) + + if use_gzip_compression is None: + use_gzip = self._use_gzip + else: + use_gzip = use_gzip_compression + + # Set up compression + if use_gzip and not post_data: + opener.addheaders.append(('Accept-Encoding', 'gzip')) + + if self._oauth_consumer is not None: + if post_data and http_method == "POST": + parameters = post_data.copy() + + req = oauth.Request.from_consumer_and_token(self._oauth_consumer, + token=self._oauth_token, + http_method=http_method, + http_url=url, parameters=parameters) + + req.sign_request(self._signature_method_hmac_sha1, self._oauth_consumer, self._oauth_token) + + headers = req.to_header() + + if http_method == "POST": + encoded_post_data = req.to_postdata() + else: + encoded_post_data = None + url = req.to_url() + else: + url = self._BuildUrl(url, extra_params=extra_params) + encoded_post_data = self._EncodePostData(post_data) + + # Open and return the URL immediately if we're not going to cache + if encoded_post_data or no_cache or not self._cache or not self._cache_timeout: + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) + opener.close() + else: + # Unique keys are a combination of the url and the oAuth Consumer Key + if self._consumer_key: + key = self._consumer_key + ':' + url + else: + key = url + + # See if it has been cached before + last_cached = self._cache.GetCachedTime(key) + + # If the cached version is outdated then fetch another and store it + if not last_cached or time.time() >= last_cached + self._cache_timeout: + try: + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) + self._cache.Set(key, url_data) + except urllib2.HTTPError, e: + print e + opener.close() + else: + url_data = self._cache.Get(key) + + # Always return the latest version + return url_data + +class _FileCacheError(Exception): + '''Base exception class for FileCache related errors''' + +class _FileCache(object): + + DEPTH = 3 + + def __init__(self,root_directory=None): + self._InitializeRootDirectory(root_directory) + + def Get(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return open(path).read() + else: + return None + + def Set(self,key,data): + path = self._GetPath(key) + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.isdir(directory): + raise _FileCacheError('%s exists but is not a directory' % directory) + temp_fd, temp_path = tempfile.mkstemp() + temp_fp = os.fdopen(temp_fd, 'w') + temp_fp.write(data) + temp_fp.close() + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory)) + if os.path.exists(path): + os.remove(path) + os.rename(temp_path, path) + + def Remove(self,key): + path = self._GetPath(key) + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory )) + if os.path.exists(path): + os.remove(path) + + def GetCachedTime(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return os.path.getmtime(path) + else: + return None + + def _GetUsername(self): + '''Attempt to find the username in a cross-platform fashion.''' + try: + return os.getenv('USER') or \ + os.getenv('LOGNAME') or \ + os.getenv('USERNAME') or \ + os.getlogin() or \ + 'nobody' + except (AttributeError, IOError, OSError), e: + return 'nobody' + + def _GetTmpCachePath(self): + username = self._GetUsername() + cache_directory = 'python.cache_' + username + return os.path.join(tempfile.gettempdir(), cache_directory) + + def _InitializeRootDirectory(self, root_directory): + if not root_directory: + root_directory = self._GetTmpCachePath() + root_directory = os.path.abspath(root_directory) + if not os.path.exists(root_directory): + os.mkdir(root_directory) + if not os.path.isdir(root_directory): + raise _FileCacheError('%s exists but is not a directory' % + root_directory) + self._root_directory = root_directory + + def _GetPath(self,key): + try: + hashed_key = md5(key).hexdigest() + except TypeError: + hashed_key = md5.new(key).hexdigest() + + return os.path.join(self._root_directory, + self._GetPrefix(hashed_key), + hashed_key) + + def _GetPrefix(self,hashed_key): + return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])